mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
Merge branch 'main' of https://github.com/external-secrets/external-secrets into external-secrets-main
This commit is contained in:
commit
9d6f7ac46f
71 changed files with 3872 additions and 129 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -176,7 +176,7 @@ jobs:
|
|||
make test
|
||||
|
||||
- name: Publish Unit Test Coverage
|
||||
uses: codecov/codecov-action@v2.0.2
|
||||
uses: codecov/codecov-action@v2.0.3
|
||||
with:
|
||||
flags: unittests
|
||||
file: ./cover.out
|
||||
|
|
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
|
@ -151,7 +151,7 @@ jobs:
|
|||
make test.e2e
|
||||
|
||||
# Update check run called "integration-fork"
|
||||
- uses: actions/github-script@v1
|
||||
- uses: actions/github-script@v4.1
|
||||
id: update-check-run
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3.14.0
|
||||
FROM alpine:3.14.2
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
COPY bin/external-secrets-${TARGETOS}-${TARGETARCH} /bin/external-secrets
|
||||
|
|
|
@ -17,6 +17,9 @@ Multiple people and organizations are joining efforts to create a single Externa
|
|||
- [Google Cloud Secrets Manager](https://external-secrets.io/provider-google-secrets-manager/)
|
||||
- [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/)
|
||||
- [IBM Cloud Secrets Manager](https://external-secrets.io/provider-ibm-secrets-manager/)
|
||||
- [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/)
|
||||
- [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/)
|
||||
- [Alibaba Cloud KMS](https://www.alibabacloud.com/product/kms) (Docs still missing, PRs welcomed!)
|
||||
|
||||
## Stability and Support Level
|
||||
|
||||
|
@ -35,6 +38,9 @@ Multiple people and organizations are joining efforts to create a single Externa
|
|||
| ------------------------------------------------------------------- | :-------: | :----------------------------------------: |
|
||||
| [Azure KV](https://external-secrets.io/provider-azure-key-vault/) | alpha | @ahmedmus-1A @asnowfix @ncourbet-1A @1A-mj |
|
||||
| [IBM SM](https://external-secrets.io/provider-ibm-secrets-manager/) | alpha | @knelasevero @sebagomez @ricardoptcosta |
|
||||
| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) | alpha | @AndreyZamyslov @knelasevero |
|
||||
| [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) | alpha | @Jabray5 |
|
||||
| Alibaba Cloud KMS | alpha | @ElsaChelala |
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
41
apis/externalsecrets/v1alpha1/secretstore_alibaba_types.go
Normal file
41
apis/externalsecrets/v1alpha1/secretstore_alibaba_types.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
)
|
||||
|
||||
// AlibabaAuth contains a secretRef for credentials.
|
||||
type AlibabaAuth struct {
|
||||
SecretRef AlibabaAuthSecretRef `json:"secretRef"`
|
||||
}
|
||||
|
||||
// AlibabaAuthSecretRef holds secret references for Alibaba credentials.
|
||||
type AlibabaAuthSecretRef struct {
|
||||
// The AccessKeyID is used for authentication
|
||||
AccessKeyID esmeta.SecretKeySelector `json:"accessKeyIDSecretRef"`
|
||||
// The AccessKeySecret is used for authentication
|
||||
AccessKeySecret esmeta.SecretKeySelector `json:"accessKeySecretSecretRef"`
|
||||
}
|
||||
|
||||
// AlibabaProvider configures a store to sync secrets using the Alibaba Secret Manager provider.
|
||||
type AlibabaProvider struct {
|
||||
Auth *AlibabaAuth `json:"auth"`
|
||||
// +optional
|
||||
Endpoint string `json:"endpoint"`
|
||||
// Alibaba Region to be used for the provider
|
||||
RegionID string `json:"regionID"`
|
||||
}
|
|
@ -31,7 +31,8 @@ type GCPSMAuthSecretRef struct {
|
|||
// GCPSMProvider Configures a store to sync secrets using the GCP Secret Manager provider.
|
||||
type GCPSMProvider struct {
|
||||
// Auth defines the information necessary to authenticate against GCP
|
||||
Auth GCPSMAuth `json:"auth"`
|
||||
// +optional
|
||||
Auth GCPSMAuth `json:"auth,omitempty"`
|
||||
|
||||
// ProjectID project where secret is located
|
||||
ProjectID string `json:"projectID,omitempty"`
|
||||
|
|
40
apis/externalsecrets/v1alpha1/secretstore_gitlab_types.go
Normal file
40
apis/externalsecrets/v1alpha1/secretstore_gitlab_types.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Configures a store to sync secrets with a GitLab instance.
|
||||
type GitlabProvider struct {
|
||||
// URL configures the GitLab instance URL. Defaults to https://gitlab.com/.
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
// Auth configures how secret-manager authenticates with a GitLab instance.
|
||||
Auth GitlabAuth `json:"auth"`
|
||||
|
||||
// ProjectID specifies a project where secrets are located.
|
||||
ProjectID string `json:"projectID,omitempty"`
|
||||
}
|
||||
|
||||
type GitlabAuth struct {
|
||||
SecretRef GitlabSecretRef `json:"SecretRef"`
|
||||
}
|
||||
|
||||
type GitlabSecretRef struct {
|
||||
// AccessToken is used for authentication.
|
||||
AccessToken esmeta.SecretKeySelector `json:"accessToken,omitempty"`
|
||||
}
|
|
@ -57,6 +57,18 @@ type SecretStoreProvider struct {
|
|||
// IBM configures this store to sync secrets using IBM Cloud provider
|
||||
// +optional
|
||||
IBM *IBMProvider `json:"ibm,omitempty"`
|
||||
|
||||
// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
|
||||
// +optional
|
||||
YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
|
||||
|
||||
// GItlab configures this store to sync secrets using Gitlab Variables provider
|
||||
// +optional
|
||||
Gitlab *GitlabProvider `json:"gitlab,omitempty"`
|
||||
|
||||
// Alibaba configures this store to sync secrets using Alibaba Cloud provider
|
||||
// +optional
|
||||
Alibaba *AlibabaProvider `json:"alibaba,omitempty"`
|
||||
}
|
||||
|
||||
type SecretStoreConditionType string
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
)
|
||||
|
||||
type YandexLockboxAuth struct {
|
||||
// The authorized key used for authentication
|
||||
// +optional
|
||||
AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
|
||||
}
|
||||
|
||||
// YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.
|
||||
type YandexLockboxProvider struct {
|
||||
// Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
|
||||
// +optional
|
||||
APIEndpoint string `json:"apiEndpoint,omitempty"`
|
||||
|
||||
// Auth defines the information necessary to authenticate against Yandex Lockbox
|
||||
Auth YandexLockboxAuth `json:"auth"`
|
||||
}
|
|
@ -102,6 +102,59 @@ func (in *AWSProvider) DeepCopy() *AWSProvider {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AlibabaAuth) DeepCopyInto(out *AlibabaAuth) {
|
||||
*out = *in
|
||||
in.SecretRef.DeepCopyInto(&out.SecretRef)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlibabaAuth.
|
||||
func (in *AlibabaAuth) DeepCopy() *AlibabaAuth {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AlibabaAuth)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AlibabaAuthSecretRef) DeepCopyInto(out *AlibabaAuthSecretRef) {
|
||||
*out = *in
|
||||
in.AccessKeyID.DeepCopyInto(&out.AccessKeyID)
|
||||
in.AccessKeySecret.DeepCopyInto(&out.AccessKeySecret)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlibabaAuthSecretRef.
|
||||
func (in *AlibabaAuthSecretRef) DeepCopy() *AlibabaAuthSecretRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AlibabaAuthSecretRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AlibabaProvider) DeepCopyInto(out *AlibabaProvider) {
|
||||
*out = *in
|
||||
if in.Auth != nil {
|
||||
in, out := &in.Auth, &out.Auth
|
||||
*out = new(AlibabaAuth)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlibabaProvider.
|
||||
func (in *AlibabaProvider) DeepCopy() *AlibabaProvider {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AlibabaProvider)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AzureKVAuth) DeepCopyInto(out *AzureKVAuth) {
|
||||
*out = *in
|
||||
|
@ -504,6 +557,54 @@ func (in *GCPSMProvider) DeepCopy() *GCPSMProvider {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
|
||||
*out = *in
|
||||
in.SecretRef.DeepCopyInto(&out.SecretRef)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabAuth.
|
||||
func (in *GitlabAuth) DeepCopy() *GitlabAuth {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GitlabAuth)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GitlabProvider) DeepCopyInto(out *GitlabProvider) {
|
||||
*out = *in
|
||||
in.Auth.DeepCopyInto(&out.Auth)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabProvider.
|
||||
func (in *GitlabProvider) DeepCopy() *GitlabProvider {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GitlabProvider)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GitlabSecretRef) DeepCopyInto(out *GitlabSecretRef) {
|
||||
*out = *in
|
||||
in.AccessToken.DeepCopyInto(&out.AccessToken)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabSecretRef.
|
||||
func (in *GitlabSecretRef) DeepCopy() *GitlabSecretRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GitlabSecretRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *IBMAuth) DeepCopyInto(out *IBMAuth) {
|
||||
*out = *in
|
||||
|
@ -698,6 +799,21 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
|
|||
*out = new(IBMProvider)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.YandexLockbox != nil {
|
||||
in, out := &in.YandexLockbox, &out.YandexLockbox
|
||||
*out = new(YandexLockboxProvider)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Gitlab != nil {
|
||||
in, out := &in.Gitlab, &out.Gitlab
|
||||
*out = new(GitlabProvider)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Alibaba != nil {
|
||||
in, out := &in.Alibaba, &out.Alibaba
|
||||
*out = new(AlibabaProvider)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
|
||||
|
@ -1003,3 +1119,35 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
|
||||
*out = *in
|
||||
in.AuthorizedKey.DeepCopyInto(&out.AuthorizedKey)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxAuth.
|
||||
func (in *YandexLockboxAuth) DeepCopy() *YandexLockboxAuth {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(YandexLockboxAuth)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *YandexLockboxProvider) DeepCopyInto(out *YandexLockboxProvider) {
|
||||
*out = *in
|
||||
in.Auth.DeepCopyInto(&out.Auth)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxProvider.
|
||||
func (in *YandexLockboxProvider) DeepCopy() *YandexLockboxProvider {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(YandexLockboxProvider)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ package v1
|
|||
// In some instances, `key` is a required field.
|
||||
type SecretKeySelector struct {
|
||||
// The name of the Secret resource being referred to.
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name,omitempty"`
|
||||
// Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
|
||||
// to the namespace of the referent.
|
||||
// +optional
|
||||
|
|
|
@ -2,8 +2,8 @@ apiVersion: v2
|
|||
name: external-secrets
|
||||
description: External secret management for Kubernetes
|
||||
type: application
|
||||
version: "0.3.3"
|
||||
appVersion: "v0.3.3"
|
||||
version: "0.3.5"
|
||||
appVersion: "v0.3.5"
|
||||
kubeVersion: ">= 1.11.0-0"
|
||||
keywords:
|
||||
- kubernetes-external-secrets
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
[//]: # (README.md generated by gotmpl. DO NOT EDIT.)
|
||||
|
||||
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.3](https://img.shields.io/badge/Version-0.3.3-informational?style=flat-square)
|
||||
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.5](https://img.shields.io/badge/Version-0.3.5-informational?style=flat-square)
|
||||
|
||||
External secret management for Kubernetes
|
||||
|
||||
|
@ -49,11 +49,13 @@ The command removes all the Kubernetes components associated with the chart and
|
|||
| podAnnotations | object | `{}` | |
|
||||
| podLabels | object | `{}` | |
|
||||
| podSecurityContext | object | `{}` | |
|
||||
| priorityClassName | string | `""` | Pod priority class name. |
|
||||
| prometheus.enabled | bool | `false` | Specifies whether to expose Service resource for collecting Prometheus metrics |
|
||||
| prometheus.service.port | int | `8080` | |
|
||||
| rbac.create | bool | `true` | Specifies whether role and rolebinding resources should be created. |
|
||||
| replicaCount | int | `1` | |
|
||||
| resources | object | `{}` | |
|
||||
| scopedNamespace | string | `""` | If set external secrets are only reconciled in the provided namespace |
|
||||
| securityContext | object | `{}` | |
|
||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account. |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. |
|
||||
|
|
|
@ -2,6 +2,7 @@ apiVersion: apps/v1
|
|||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "external-secrets.fullname" . }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "external-secrets.labels" . | nindent 4 }}
|
||||
spec:
|
||||
|
@ -38,11 +39,14 @@ spec:
|
|||
{{- end }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if or (.Values.leaderElect) (.Values.extraArgs) }}
|
||||
{{- if or (.Values.leaderElect) (.Values.scopedNamespace) (.Values.extraArgs) }}
|
||||
args:
|
||||
{{- if .Values.leaderElect }}
|
||||
- --enable-leader-election=true
|
||||
{{- end }}
|
||||
{{- if .Values.scopedNamespace }}
|
||||
- --namespace={{ .Values.scopedNamespace }}
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Values.extraArgs }}
|
||||
{{- if $value }}
|
||||
- --{{ $key }}={{ $value }}
|
||||
|
@ -74,3 +78,6 @@ spec:
|
|||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.priorityClassName }}
|
||||
priorityClassName: {{ .Values.priorityClassName }}
|
||||
{{- end }}
|
||||
|
|
|
@ -3,6 +3,7 @@ apiVersion: v1
|
|||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "external-secrets.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "external-secrets.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
|
|
|
@ -17,6 +17,10 @@ fullnameOverride: ""
|
|||
# than one instance of external-secrets operates at a time.
|
||||
leaderElect: false
|
||||
|
||||
# -- If set external secrets are only reconciled in the
|
||||
# provided namespace
|
||||
scopedNamespace: ""
|
||||
|
||||
serviceAccount:
|
||||
# -- Specifies whether a service account should be created.
|
||||
create: true
|
||||
|
@ -66,3 +70,6 @@ nodeSelector: {}
|
|||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
# -- Pod priority class name.
|
||||
priorityClassName: ""
|
||||
|
|
|
@ -54,6 +54,73 @@ spec:
|
|||
maxProperties: 1
|
||||
minProperties: 1
|
||||
properties:
|
||||
alibaba:
|
||||
description: Alibaba configures this store to sync secrets using
|
||||
Alibaba Cloud provider
|
||||
properties:
|
||||
auth:
|
||||
description: AlibabaAuth contains a secretRef for credentials.
|
||||
properties:
|
||||
secretRef:
|
||||
description: AlibabaAuthSecretRef holds secret references
|
||||
for Alibaba credentials.
|
||||
properties:
|
||||
accessKeyIDSecretRef:
|
||||
description: The AccessKeyID is used for authentication
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret
|
||||
resource's `data` field to be used. Some instances
|
||||
of this field may be defaulted, in others it
|
||||
may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped.
|
||||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
type: object
|
||||
accessKeySecretSecretRef:
|
||||
description: The AccessKeySecret is used for authentication
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret
|
||||
resource's `data` field to be used. Some instances
|
||||
of this field may be defaulted, in others it
|
||||
may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped.
|
||||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- accessKeyIDSecretRef
|
||||
- accessKeySecretSecretRef
|
||||
type: object
|
||||
required:
|
||||
- secretRef
|
||||
type: object
|
||||
endpoint:
|
||||
type: string
|
||||
regionID:
|
||||
description: Alibaba Region to be used for the provider
|
||||
type: string
|
||||
required:
|
||||
- auth
|
||||
- regionID
|
||||
type: object
|
||||
aws:
|
||||
description: AWS configures this store to sync secrets using AWS
|
||||
Secret Manager provider
|
||||
|
@ -108,8 +175,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
secretAccessKeySecretRef:
|
||||
description: The SecretAccessKey is used for authentication
|
||||
|
@ -130,8 +195,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
|
@ -179,8 +242,6 @@ spec:
|
|||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
clientSecret:
|
||||
description: The Azure ClientSecret of the service principle
|
||||
|
@ -200,8 +261,6 @@ spec:
|
|||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
|
@ -249,8 +308,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
|
@ -259,6 +316,49 @@ spec:
|
|||
projectID:
|
||||
description: ProjectID project where secret is located
|
||||
type: string
|
||||
type: object
|
||||
gitlab:
|
||||
description: GItlab configures this store to sync secrets using
|
||||
Gitlab Variables provider
|
||||
properties:
|
||||
auth:
|
||||
description: Auth configures how secret-manager authenticates
|
||||
with a GitLab instance.
|
||||
properties:
|
||||
SecretRef:
|
||||
properties:
|
||||
accessToken:
|
||||
description: AccessToken is used for authentication.
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret
|
||||
resource's `data` field to be used. Some instances
|
||||
of this field may be defaulted, in others it
|
||||
may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped.
|
||||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- SecretRef
|
||||
type: object
|
||||
projectID:
|
||||
description: ProjectID specifies a project where secrets are
|
||||
located.
|
||||
type: string
|
||||
url:
|
||||
description: URL configures the GitLab instance URL. Defaults
|
||||
to https://gitlab.com/.
|
||||
type: string
|
||||
required:
|
||||
- auth
|
||||
type: object
|
||||
|
@ -291,8 +391,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
|
@ -425,8 +523,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- path
|
||||
|
@ -458,8 +554,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
secretRef:
|
||||
description: SecretRef to a key in a Secret resource
|
||||
|
@ -482,8 +576,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
jwt:
|
||||
|
@ -515,8 +607,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
kubernetes:
|
||||
|
@ -557,8 +647,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
serviceAccountRef:
|
||||
description: Optional service account field containing
|
||||
|
@ -611,8 +699,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
username:
|
||||
description: Username is a LDAP user name used to
|
||||
|
@ -640,8 +726,6 @@ spec:
|
|||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
caBundle:
|
||||
|
@ -682,6 +766,39 @@ spec:
|
|||
- path
|
||||
- server
|
||||
type: object
|
||||
yandexlockbox:
|
||||
description: YandexLockbox configures this store to sync secrets
|
||||
using Yandex Lockbox provider
|
||||
properties:
|
||||
apiEndpoint:
|
||||
description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
|
||||
type: string
|
||||
auth:
|
||||
description: Auth defines the information necessary to authenticate
|
||||
against Yandex Lockbox
|
||||
properties:
|
||||
authorizedKeySecretRef:
|
||||
description: The authorized key used for authentication
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret resource's
|
||||
`data` field to be used. Some instances of this
|
||||
field may be defaulted, in others it may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- auth
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- provider
|
||||
|
|
|
@ -54,6 +54,73 @@ spec:
|
|||
maxProperties: 1
|
||||
minProperties: 1
|
||||
properties:
|
||||
alibaba:
|
||||
description: Alibaba configures this store to sync secrets using
|
||||
Alibaba Cloud provider
|
||||
properties:
|
||||
auth:
|
||||
description: AlibabaAuth contains a secretRef for credentials.
|
||||
properties:
|
||||
secretRef:
|
||||
description: AlibabaAuthSecretRef holds secret references
|
||||
for Alibaba credentials.
|
||||
properties:
|
||||
accessKeyIDSecretRef:
|
||||
description: The AccessKeyID is used for authentication
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret
|
||||
resource's `data` field to be used. Some instances
|
||||
of this field may be defaulted, in others it
|
||||
may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped.
|
||||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
type: object
|
||||
accessKeySecretSecretRef:
|
||||
description: The AccessKeySecret is used for authentication
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret
|
||||
resource's `data` field to be used. Some instances
|
||||
of this field may be defaulted, in others it
|
||||
may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped.
|
||||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- accessKeyIDSecretRef
|
||||
- accessKeySecretSecretRef
|
||||
type: object
|
||||
required:
|
||||
- secretRef
|
||||
type: object
|
||||
endpoint:
|
||||
type: string
|
||||
regionID:
|
||||
description: Alibaba Region to be used for the provider
|
||||
type: string
|
||||
required:
|
||||
- auth
|
||||
- regionID
|
||||
type: object
|
||||
aws:
|
||||
description: AWS configures this store to sync secrets using AWS
|
||||
Secret Manager provider
|
||||
|
@ -108,8 +175,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
secretAccessKeySecretRef:
|
||||
description: The SecretAccessKey is used for authentication
|
||||
|
@ -130,8 +195,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
|
@ -179,8 +242,6 @@ spec:
|
|||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
clientSecret:
|
||||
description: The Azure ClientSecret of the service principle
|
||||
|
@ -200,8 +261,6 @@ spec:
|
|||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
|
@ -249,8 +308,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
|
@ -259,6 +316,49 @@ spec:
|
|||
projectID:
|
||||
description: ProjectID project where secret is located
|
||||
type: string
|
||||
type: object
|
||||
gitlab:
|
||||
description: GItlab configures this store to sync secrets using
|
||||
Gitlab Variables provider
|
||||
properties:
|
||||
auth:
|
||||
description: Auth configures how secret-manager authenticates
|
||||
with a GitLab instance.
|
||||
properties:
|
||||
SecretRef:
|
||||
properties:
|
||||
accessToken:
|
||||
description: AccessToken is used for authentication.
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret
|
||||
resource's `data` field to be used. Some instances
|
||||
of this field may be defaulted, in others it
|
||||
may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped.
|
||||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- SecretRef
|
||||
type: object
|
||||
projectID:
|
||||
description: ProjectID specifies a project where secrets are
|
||||
located.
|
||||
type: string
|
||||
url:
|
||||
description: URL configures the GitLab instance URL. Defaults
|
||||
to https://gitlab.com/.
|
||||
type: string
|
||||
required:
|
||||
- auth
|
||||
type: object
|
||||
|
@ -291,8 +391,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
|
@ -425,8 +523,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- path
|
||||
|
@ -458,8 +554,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
secretRef:
|
||||
description: SecretRef to a key in a Secret resource
|
||||
|
@ -482,8 +576,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
jwt:
|
||||
|
@ -515,8 +607,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
kubernetes:
|
||||
|
@ -557,8 +647,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
serviceAccountRef:
|
||||
description: Optional service account field containing
|
||||
|
@ -611,8 +699,6 @@ spec:
|
|||
cluster-scoped defaults to the namespace of
|
||||
the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
username:
|
||||
description: Username is a LDAP user name used to
|
||||
|
@ -640,8 +726,6 @@ spec:
|
|||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
caBundle:
|
||||
|
@ -682,6 +766,39 @@ spec:
|
|||
- path
|
||||
- server
|
||||
type: object
|
||||
yandexlockbox:
|
||||
description: YandexLockbox configures this store to sync secrets
|
||||
using Yandex Lockbox provider
|
||||
properties:
|
||||
apiEndpoint:
|
||||
description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
|
||||
type: string
|
||||
auth:
|
||||
description: Auth defines the information necessary to authenticate
|
||||
against Yandex Lockbox
|
||||
properties:
|
||||
authorizedKeySecretRef:
|
||||
description: The authorized key used for authentication
|
||||
properties:
|
||||
key:
|
||||
description: The key of the entry in the Secret resource's
|
||||
`data` field to be used. Some instances of this
|
||||
field may be defaulted, in others it may be required.
|
||||
type: string
|
||||
name:
|
||||
description: The name of the Secret resource being
|
||||
referred to.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the resource being referred
|
||||
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- auth
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- provider
|
||||
|
|
|
@ -8,6 +8,17 @@ git clone https://github.com/external-secrets/external-secrets.git
|
|||
cd external-secrets
|
||||
```
|
||||
|
||||
If you want to run controller tests you also need to install kubebuilder's `envtest`:
|
||||
|
||||
```
|
||||
export KUBEBUILDER_TOOLS_VERSION='1.20.2' # check for latest version or a version that has support to what you are testing
|
||||
|
||||
curl -sSLo envtest-bins.tar.gz "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-$KUBEBUILDER_TOOLS_VERSION-linux-amd64.tar.gz"
|
||||
|
||||
sudo mkdir -p /usr/local/kubebuilder
|
||||
sudo tar -C /usr/local/kubebuilder --strip-components=1 -zvxf envtest-bins.tar.gz
|
||||
```
|
||||
|
||||
## Building & Testing
|
||||
|
||||
The project uses the `make` build system. It'll run code generators, tests and
|
||||
|
@ -33,21 +44,19 @@ make docs
|
|||
|
||||
## Installing
|
||||
|
||||
To install the External Secret Operator's CRDs into a Kubernetes Cluster run:
|
||||
To install the External Secret Operator into a Kubernetes Cluster run:
|
||||
|
||||
```shell
|
||||
helm repo add external-secrets https://charts.external-secrets.io
|
||||
helm repo update
|
||||
helm install external-secrets external-secrets/external-secrets
|
||||
```
|
||||
|
||||
You can alternatively run the controller on your host system for development purposes:
|
||||
|
||||
|
||||
```shell
|
||||
make crds.install
|
||||
```
|
||||
|
||||
Apply the sample resources:
|
||||
```shell
|
||||
kubectl apply -f docs/snippets/basic-secret-store.yaml
|
||||
kubectl apply -f docs/snippets/basic-external-secret.yaml
|
||||
```
|
||||
|
||||
You can run the controller on your host system for development purposes:
|
||||
|
||||
```shell
|
||||
make run
|
||||
```
|
||||
|
||||
|
@ -57,6 +66,20 @@ To remove the CRDs run:
|
|||
make crds.uninstall
|
||||
```
|
||||
|
||||
If you need to test some other k8s integrations and need the operator to be deployed to the actuall cluster while developing, you can use the following workflow:
|
||||
|
||||
```
|
||||
kind create cluster --name external-secrets
|
||||
|
||||
export TAG=v2
|
||||
export IMAGE=eso-local
|
||||
|
||||
docker build . -t $IMAGE:$TAG --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux
|
||||
|
||||
make helm.generate
|
||||
helm upgrade --install external-secrets ./deploy/charts/external-secrets/ --set image.repository=$IMAGE --set image.tag=$TAG
|
||||
```
|
||||
|
||||
!!! note "Contributing Flow"
|
||||
The HOW TO guide for contributing is at the [Contributing Process](contributing-process.md) page.
|
||||
|
||||
|
|
BIN
docs/pictures/screenshot_gitlab_projectID.png
Normal file
BIN
docs/pictures/screenshot_gitlab_projectID.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
BIN
docs/pictures/screenshot_gitlab_token.png
Normal file
BIN
docs/pictures/screenshot_gitlab_token.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
BIN
docs/pictures/screenshot_gitlab_token_created.png
Normal file
BIN
docs/pictures/screenshot_gitlab_token_created.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -19,7 +19,7 @@ way users of the `SecretStore` can only access the secrets necessary.
|
|||
|
||||
### IAM Policy
|
||||
|
||||
Create a IAM Policy to pin down access to secrets matching `dev-*`, for futher information see [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html):
|
||||
Create a IAM Policy to pin down access to secrets matching `dev-*`, for further information see [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html):
|
||||
|
||||
``` json
|
||||
{
|
||||
|
|
54
docs/provider-gitlab-project-variables.md
Normal file
54
docs/provider-gitlab-project-variables.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
## Gitlab Project Variables
|
||||
|
||||
External Secrets Operator integrates with [Gitlab API](https://docs.gitlab.com/ee/api/project_level_variables.html) to sync Gitlab project variables to secrets held on the Kubernetes cluster.
|
||||
|
||||
### Authentication
|
||||
|
||||
The API requires an access token and project ID. To create a new access token, go to your user settings and select 'access tokens'. Give your token a name, expiration date, and select the permissions required (Note 'api' is required).
|
||||
|
||||
![token-details](./pictures/screenshot_gitlab_token.png)
|
||||
|
||||
Click 'Create personal access token', and your token will be generated and displayed on screen. Copy or save this token since you can't access it again.
|
||||
![token-created](./pictures/screenshot_gitlab_token_created.png)
|
||||
|
||||
|
||||
|
||||
### Access Token secret
|
||||
|
||||
Create a secret containing your access token:
|
||||
|
||||
```yaml
|
||||
{% include 'gitlab-credentials-secret.yaml' %}
|
||||
```
|
||||
|
||||
### Update secret store
|
||||
Be sure the `gitlab` provider is listed in the `Kind=SecretStore` and the ProjectID is set. If you are not using `https://gitlab.com`, you must set the `url` field as well.
|
||||
|
||||
```yaml
|
||||
{% include 'gitlab-secret-store.yaml' %}
|
||||
```
|
||||
|
||||
Your project ID can be found on your project's page.
|
||||
![projectID](./pictures/screenshot_gitlab_projectID.png)
|
||||
|
||||
### Creating external secret
|
||||
|
||||
To sync a Gitlab variable to a secret on the Kubernetes cluster, a `Kind=ExternalSecret` is needed.
|
||||
|
||||
```yaml
|
||||
{% include 'gitlab-external-secret.yaml' %}
|
||||
```
|
||||
|
||||
#### Using DataFrom
|
||||
|
||||
DataFrom can be used to get a variable as a JSON string and attempt to parse it.
|
||||
|
||||
```yaml
|
||||
{% include 'gitlab-external-secret-json.yaml' %}
|
||||
```
|
||||
|
||||
### Getting the Kubernetes secret
|
||||
The operator will fetch the project variable and inject it as a `Kind=Secret`.
|
||||
```
|
||||
kubectl get secret gitlab-secret-to-create -o jsonpath='{.data.secretKey}' | base64 -d
|
||||
```
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
External Secrets Operator integrates with [GCP Secret Manager](https://cloud.google.com/secret-manager) for secret management.
|
||||
|
||||
### Authentication
|
||||
|
||||
At the moment, we only support [service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) authentication.
|
||||
|
||||
#### Service account key authentication
|
||||
### Service account key authentication
|
||||
|
||||
A service account key is created and the JSON keyfile is stored in a `Kind=Secret`. The `project_id` and `private_key` should be configured for the project.
|
||||
|
||||
|
@ -33,3 +29,63 @@ The operator will fetch the GCP Secret Manager secret and inject it as a `Kind=S
|
|||
```
|
||||
kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
|
||||
```
|
||||
|
||||
## Authentication with Workload Identity
|
||||
|
||||
This makes it possible for your Google Kubernetes Engine (GKE) applications to consume services provided by Google APIs, namely Secrets Manager service in this case.
|
||||
|
||||
Here we will assume that you installed ESO using helm and that you named the chart installation `external-secrets` and the namespace where it lives `es` like:
|
||||
|
||||
```sh
|
||||
helm install external-secrets external-secrets/external-secrets --namespace es
|
||||
```
|
||||
|
||||
Then most of the resources would have this name, the important one here being the k8s service account attached to the external-secrets operator deployment:
|
||||
|
||||
```
|
||||
# ...
|
||||
containers:
|
||||
- image: ghcr.io/external-secrets/external-secrets:vVERSION
|
||||
name: external-secrets
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
serviceAccount: external-secrets
|
||||
serviceAccountName: external-secrets # <--- here
|
||||
```
|
||||
|
||||
### Following the documentation
|
||||
|
||||
You can find the documentation for Workload Identity under [this url](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). We will walk you through how to navigate it here.
|
||||
|
||||
#### Changing Values
|
||||
|
||||
Search [the documment](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for this editable values and change them to your values:
|
||||
|
||||
- CLUSTER_NAME: The name of your cluster
|
||||
- PROJECT_ID: Your project ID (not your Project number nor your Project name)
|
||||
- K8S_NAMESPACE: For us folowing these steps here it will be `es`, but this will be the namespace where you deployed the external-secrets operator
|
||||
- KSA_NAME: external-secrets (if you are not creating a new one to attach to the deployemnt)
|
||||
- GSA_NAME: external-secrets for simplicity, or something else if you have to follow different naming convetions for cloud resources
|
||||
- ROLE_NAME: roles/secretmanager.secretAccessor so you make the pod only be able to access secrets on Secret Manager
|
||||
|
||||
#### Following through
|
||||
|
||||
You can follow through the documentation and adapt it to your specific use case. If you want to just use the serviceaccount that we deployed with the helm chart, for example, you don't need to create a new service account on 2 of [Authenticating to Google Cloud](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#authenticating_to).
|
||||
|
||||
#### SecretStore with WorkloadIdentity
|
||||
|
||||
To use workload identity you can just omit the auth field of the secret store and let the operator client fall back to defaults using the roles attached to your service account.
|
||||
|
||||
```
|
||||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: SecretStore
|
||||
metadata:
|
||||
name: example
|
||||
spec:
|
||||
provider:
|
||||
gcpsm:
|
||||
projectID: pid
|
||||
```
|
86
docs/provider-yandex-lockbox.md
Normal file
86
docs/provider-yandex-lockbox.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
## Yandex Lockbox
|
||||
|
||||
External Secrets Operator integrates with [Yandex Lockbox](https://cloud.yandex.com/docs/lockbox/)
|
||||
for secret management.
|
||||
|
||||
### Prerequisites
|
||||
* [External Secrets Operator installed](../guides-getting-started/#installing-with-helm)
|
||||
* [Yandex.Cloud CLI installed](https://cloud.yandex.com/docs/cli/quickstart)
|
||||
|
||||
### Authentication
|
||||
At the moment, [authorized key](https://cloud.yandex.com/docs/iam/concepts/authorization/key) authentication is only supported:
|
||||
|
||||
* Create a [service account](https://cloud.yandex.com/docs/iam/concepts/users/service-accounts) in Yandex.Cloud:
|
||||
```bash
|
||||
yc iam service-account create --name eso-service-account
|
||||
```
|
||||
* Create an authorized key for the service account and save it to `authorized-key.json` file:
|
||||
```bash
|
||||
yc iam key create \
|
||||
--service-account-name eso-service-account \
|
||||
--output authorized-key.json
|
||||
```
|
||||
* Create a k8s secret containing the authorized key saved above:
|
||||
```bash
|
||||
kubectl create secret generic yc-auth --from-file=authorized-key=authorized-key.json
|
||||
```
|
||||
* Create a [SecretStore](../api-secretstore/) pointing to `yc-auth` k8s secret:
|
||||
```yaml
|
||||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: SecretStore
|
||||
metadata:
|
||||
name: secret-store
|
||||
spec:
|
||||
provider:
|
||||
yandexlockbox:
|
||||
auth:
|
||||
authorizedKeySecretRef:
|
||||
name: yc-auth
|
||||
key: authorized-key
|
||||
```
|
||||
|
||||
### Creating external secret
|
||||
To make External Secrets Operator sync a k8s secret with a Lockbox secret:
|
||||
|
||||
* Create a Lockbox secret, if not already created:
|
||||
```bash
|
||||
yc lockbox secret create \
|
||||
--name lockbox-secret \
|
||||
--payload '[{"key": "password","textValue": "p@$$w0rd"}]'
|
||||
```
|
||||
* Assign the [`lockbox.payloadViewer`](https://cloud.yandex.com/docs/lockbox/security/#roles-list) role
|
||||
for accessing the `lockbox-secret` payload to the service account used for authentication:
|
||||
```bash
|
||||
yc lockbox secret add-access-binding \
|
||||
--name lockbox-secret \
|
||||
--service-account-name eso-service-account \
|
||||
--role lockbox.payloadViewer
|
||||
```
|
||||
Run the following command to ensure that the correct access binding has been added:
|
||||
```bash
|
||||
yc lockbox secret list-access-bindings --name lockbox-secret
|
||||
```
|
||||
* Create an [ExternalSecret](../api-externalsecret/) pointing to `secret-store` and `lockbox-secret`:
|
||||
```yaml
|
||||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: external-secret
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: secret-store
|
||||
kind: SecretStore
|
||||
target:
|
||||
name: k8s-secret # the target k8s secret name
|
||||
data:
|
||||
- secretKey: password # the target k8s secret key
|
||||
remoteRef:
|
||||
key: ***** # ID of lockbox-secret
|
||||
property: password # (optional) payload entry key of lockbox-secret
|
||||
```
|
||||
|
||||
The operator will fetch the Yandex Lockbox secret and inject it as a `Kind=Secret`
|
||||
```yaml
|
||||
kubectl get secret k8s-secret -n <namespace> | -o jsonpath='{.data.password}' | base64 -d
|
||||
```
|
9
docs/snippets/gitlab-credentials-secret.yaml
Normal file
9
docs/snippets/gitlab-credentials-secret.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gitlab-secret
|
||||
labels:
|
||||
type: gitlab
|
||||
type: Opaque
|
||||
stringData:
|
||||
token: "**access token goes here**"
|
18
docs/snippets/gitlab-external-secret-json.yaml
Normal file
18
docs/snippets/gitlab-external-secret-json.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: gitlab-external-secret-example
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
|
||||
secretStoreRef:
|
||||
kind: SecretStore
|
||||
name: gitlab-secret-store # Must match SecretStore on the cluster
|
||||
|
||||
target:
|
||||
name: gitlab-secret-to-create # Name for the secret to be created on the cluster
|
||||
creationPolicy: Owner
|
||||
|
||||
# each secret name in the KV will be used as the secret key in the SECRET k8s target object
|
||||
dataFrom:
|
||||
- key: "myJsonVariable" # Key of the variable on Gitlab
|
19
docs/snippets/gitlab-external-secret.yaml
Normal file
19
docs/snippets/gitlab-external-secret.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: gitlab-external-secret-example
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
|
||||
secretStoreRef:
|
||||
kind: SecretStore
|
||||
name: gitlab-secret-store # Must match SecretStore on the cluster
|
||||
|
||||
target:
|
||||
name: gitlab-secret-to-create # Name for the secret to be created on the cluster
|
||||
creationPolicy: Owner
|
||||
|
||||
data:
|
||||
- secretKey: secretKey # Key given to the secret to be created on the cluster
|
||||
remoteRef:
|
||||
key: myGitlabVariable # Key of the variable on Gitlab
|
15
docs/snippets/gitlab-secret-store.yaml
Normal file
15
docs/snippets/gitlab-secret-store.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: SecretStore
|
||||
metadata:
|
||||
name: gitlab-secret-store
|
||||
spec:
|
||||
provider:
|
||||
# provider type: gitlab
|
||||
gitlab:
|
||||
# url: https://gitlab.mydomain.com/
|
||||
auth:
|
||||
SecretRef:
|
||||
accessToken:
|
||||
name: gitlab-secret
|
||||
key: token
|
||||
projectID: "**project ID goes here**"
|
|
@ -19,6 +19,7 @@ spec:
|
|||
provider:
|
||||
aws:
|
||||
service: SecretsManager
|
||||
region: eu-central-1
|
||||
# optional: do a sts:assumeRole before fetching secrets
|
||||
role: team-b
|
||||
```
|
||||
|
@ -37,6 +38,7 @@ spec:
|
|||
provider:
|
||||
aws:
|
||||
service: SecretsManager
|
||||
region: eu-central-1
|
||||
# optional: assume role before fetching secrets
|
||||
role: team-b
|
||||
auth:
|
||||
|
@ -78,6 +80,7 @@ spec:
|
|||
provider:
|
||||
aws:
|
||||
service: SecretsManager
|
||||
region: eu-central-1
|
||||
auth:
|
||||
jwt:
|
||||
serviceAccountRef:
|
||||
|
|
|
@ -18,7 +18,8 @@ spec:
|
|||
path: "approle"
|
||||
# RoleID configured in the App Role authentication backend
|
||||
roleId: "db02de05-fa39-4855-059b-67221c5c2f63"
|
||||
# Reference to a key in a K8 Secret that contains the App Role SecretId
|
||||
secretRef:
|
||||
name: "my-secret"
|
||||
namespace: "secret-admin"
|
||||
key: "vault-token"
|
||||
key: "secret-id"
|
||||
|
|
220
docs/spec.md
220
docs/spec.md
|
@ -543,7 +543,9 @@ ExternalSecretStatus
|
|||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr><td><p>"Ready"</p></td>
|
||||
<tbody><tr><td><p>"Deleted"</p></td>
|
||||
<td></td>
|
||||
</tr><tr><td><p>"Ready"</p></td>
|
||||
<td></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
@ -1151,6 +1153,7 @@ GCPSMAuth
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Auth defines the information necessary to authenticate against GCP</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1173,6 +1176,119 @@ string
|
|||
<p>GenericStore is a common interface for interacting with ClusterSecretStore
|
||||
or a namespaced SecretStore.</p>
|
||||
</p>
|
||||
<h3 id="external-secrets.io/v1alpha1.GitlabAuth">GitlabAuth
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#external-secrets.io/v1alpha1.GitlabProvider">GitlabProvider</a>)
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>SecretRef</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1alpha1.GitlabSecretRef">
|
||||
GitlabSecretRef
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.GitlabProvider">GitlabProvider
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
|
||||
</p>
|
||||
<p>
|
||||
<p>Configures a store to sync secrets with a GitLab instance.</p>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>url</code></br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>URL configures the GitLab instance URL. Defaults to <a href="https://gitlab.com/">https://gitlab.com/</a>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>auth</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1alpha1.GitlabAuth">
|
||||
GitlabAuth
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Auth configures how secret-manager authenticates with a GitLab instance.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>projectID</code></br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>ProjectID specifies a project where secrets are located.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.GitlabSecretRef">GitlabSecretRef
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#external-secrets.io/v1alpha1.GitlabAuth">GitlabAuth</a>)
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>accessToken</code></br>
|
||||
<em>
|
||||
github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>AccessToken is used for authentication.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.IBMAuth">IBMAuth
|
||||
</h3>
|
||||
<p>
|
||||
|
@ -1466,6 +1582,34 @@ IBMProvider
|
|||
<p>IBM configures this store to sync secrets using IBM Cloud provider</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>yandexlockbox</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">
|
||||
YandexLockboxProvider
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>YandexLockbox configures this store to sync secrets using Yandex Lockbox provider</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>gitlab</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1alpha1.GitlabProvider">
|
||||
GitlabProvider
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>GItlab configures this store to sync secrets using Gitlab Variables provider</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.SecretStoreRef">SecretStoreRef
|
||||
|
@ -2274,6 +2418,80 @@ are used to validate the TLS connection.</p>
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.YandexLockboxAuth">YandexLockboxAuth
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">YandexLockboxProvider</a>)
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>authorizedKeySecretRef</code></br>
|
||||
<em>
|
||||
github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The authorized key used for authentication</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.YandexLockboxProvider">YandexLockboxProvider
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
|
||||
</p>
|
||||
<p>
|
||||
<p>YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.</p>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>apiEndpoint</code></br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Yandex.Cloud API endpoint (e.g. ‘api.cloud.yandex.net:443’)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>auth</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1alpha1.YandexLockboxAuth">
|
||||
YandexLockboxAuth
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Auth defines the information necessary to authenticate against Yandex Lockbox</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr/>
|
||||
<p><em>
|
||||
Generated with <code>gen-crd-api-reference-docs</code>.
|
||||
|
|
BIN
e2e/.DS_Store
vendored
Normal file
BIN
e2e/.DS_Store
vendored
Normal file
Binary file not shown.
|
@ -40,6 +40,9 @@ var _ = SynchronizedBeforeSuite(func() []byte {
|
|||
|
||||
By("installing eso")
|
||||
addon.InstallGlobalAddon(addon.NewESO(), cfg)
|
||||
|
||||
By("installing scoped eso")
|
||||
addon.InstallGlobalAddon(addon.NewScopedESO(), cfg)
|
||||
return nil
|
||||
}, func([]byte) {})
|
||||
|
||||
|
|
|
@ -27,3 +27,14 @@ func NewESO() *ESO {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewScopedESO() *ESO {
|
||||
return &ESO{
|
||||
&HelmChart{
|
||||
Namespace: "default",
|
||||
ReleaseName: "eso-aws-sm",
|
||||
Chart: "/k8s/deploy/charts/external-secrets",
|
||||
Values: []string{"/k8s/eso.scoped.values.yaml"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
12
e2e/k8s/eso.scoped.values.yaml
Normal file
12
e2e/k8s/eso.scoped.values.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
installCRDs: false
|
||||
image:
|
||||
repository: local/external-secrets
|
||||
tag: test
|
||||
scopedNamespace: test
|
||||
extraEnv:
|
||||
- name: AWS_SECRETSMANAGER_ENDPOINT
|
||||
value: "http://localstack.default"
|
||||
- name: AWS_STS_ENDPOINT
|
||||
value: "http://localstack.default"
|
||||
- name: AWS_SSM_ENDPOINT
|
||||
value: "http://localstack.default"
|
|
@ -58,5 +58,7 @@ kubectl run --rm \
|
|||
--env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \
|
||||
--env="TENANT_ID=${TENANT_ID:-}" \
|
||||
--env="VAULT_URL=${VAULT_URL:-}" \
|
||||
--env="GITLAB_TOKEN=${GITLAB_TOKEN:-}" \
|
||||
--env="GITLAB_PROJECT_ID=${GITLAB_PROJECT_ID:-}" \
|
||||
--overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
|
||||
e2e --image=local/external-secrets-e2e:test
|
||||
|
|
47
e2e/suite/alibaba/alibaba.go
Normal file
47
e2e/suite/alibaba/alibaba.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package alibaba
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
// nolint
|
||||
. "github.com/onsi/ginkgo"
|
||||
// nolint
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
|
||||
"github.com/external-secrets/external-secrets/e2e/framework"
|
||||
"github.com/external-secrets/external-secrets/e2e/suite/common"
|
||||
)
|
||||
|
||||
var _ = Describe("[alibaba] ", func() {
|
||||
f := framework.New("eso-alibaba")
|
||||
accessKeyID := os.Getenv("ACCESS_KEY_ID")
|
||||
accessKeySecret := os.Getenv("ACCESS_KEY_SECRET")
|
||||
regionID := os.Getenv("REGION_ID")
|
||||
prov := newAlibabaProvider(f, accessKeyID, accessKeySecret, regionID)
|
||||
|
||||
DescribeTable("sync secrets", framework.TableFunc(f, prov),
|
||||
Entry(common.SimpleDataSync(f)),
|
||||
Entry(common.NestedJSONWithGJSON(f)),
|
||||
Entry(common.JSONDataFromSync(f)),
|
||||
Entry(common.JSONDataWithProperty(f)),
|
||||
Entry(common.JSONDataWithTemplate(f)),
|
||||
Entry(common.DockerJSONConfig(f)),
|
||||
Entry(common.DataPropertyDockerconfigJSON(f)),
|
||||
Entry(common.SSHKeySync(f)),
|
||||
Entry(common.SSHKeySyncDataProperty(f)),
|
||||
)
|
||||
})
|
118
e2e/suite/alibaba/provider.go
Normal file
118
e2e/suite/alibaba/provider.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package alibaba
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
|
||||
|
||||
//nolint
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
||||
//nolint
|
||||
. "github.com/onsi/gomega"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
"github.com/external-secrets/external-secrets/e2e/framework"
|
||||
)
|
||||
|
||||
type alibabaProvider struct {
|
||||
accessKeyID string
|
||||
accessKeySecret string
|
||||
regionID string
|
||||
framework *framework.Framework
|
||||
}
|
||||
|
||||
const (
|
||||
secretName = "secretName"
|
||||
)
|
||||
|
||||
func newAlibabaProvider(f *framework.Framework, accessKeyID, accessKeySecret, regionID string) *alibabaProvider {
|
||||
prov := &alibabaProvider{
|
||||
accessKeyID: accessKeyID,
|
||||
accessKeySecret: accessKeySecret,
|
||||
regionID: regionID,
|
||||
framework: f,
|
||||
}
|
||||
BeforeEach(prov.BeforeEach)
|
||||
return prov
|
||||
}
|
||||
|
||||
// CreateSecret creates a secret in both kv v1 and v2 provider.
|
||||
func (s *alibabaProvider) CreateSecret(key, val string) {
|
||||
client, err := kms.NewClientWithAccessKey(s.regionID, s.accessKeyID, s.accessKeySecret)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
kmssecretrequest := kms.CreateCreateSecretRequest()
|
||||
kmssecretrequest.SecretName = secretName
|
||||
kmssecretrequest.SecretData = "value"
|
||||
_, err = client.CreateSecret(kmssecretrequest)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
func (s *alibabaProvider) DeleteSecret(key string) {
|
||||
client, err := kms.NewClientWithAccessKey(s.regionID, s.accessKeyID, s.accessKeySecret)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
kmssecretrequest := kms.CreateDeleteSecretRequest()
|
||||
kmssecretrequest.SecretName = secretName
|
||||
_, err = client.DeleteSecret(kmssecretrequest)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
func (s *alibabaProvider) BeforeEach() {
|
||||
// Creating an Alibaba secret
|
||||
alibabaCreds := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: s.framework.Namespace.Name,
|
||||
},
|
||||
StringData: map[string]string{
|
||||
secretName: "value",
|
||||
},
|
||||
}
|
||||
err := s.framework.CRClient.Create(context.Background(), alibabaCreds)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Creating Alibaba secret store
|
||||
secretStore := &esv1alpha1.SecretStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.framework.Namespace.Name,
|
||||
Namespace: s.framework.Namespace.Name,
|
||||
},
|
||||
Spec: esv1alpha1.SecretStoreSpec{
|
||||
Provider: &esv1alpha1.SecretStoreProvider{
|
||||
Alibaba: &esv1alpha1.AlibabaProvider{
|
||||
Auth: &esv1alpha1.AlibabaAuth{
|
||||
SecretRef: esv1alpha1.AlibabaAuthSecretRef{
|
||||
AccessKeyID: esmeta.SecretKeySelector{
|
||||
Name: "kms-secret",
|
||||
Key: "keyid",
|
||||
},
|
||||
AccessKeySecret: esmeta.SecretKeySelector{
|
||||
Name: "kms-secret",
|
||||
Key: "accesskey",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = s.framework.CRClient.Create(context.Background(), secretStore)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
45
e2e/suite/gitlab/gitlab.go
Normal file
45
e2e/suite/gitlab/gitlab.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
limitations under the License.
|
||||
*/
|
||||
package gitlab
|
||||
|
||||
// TODO - Gitlab only accepts variable names with alphanumeric and '_'
|
||||
// whereas ESO only accepts names with alphanumeric and '-'.
|
||||
// Current workaround is to remove all hyphens and underscores set in e2e/framework/util/util.go
|
||||
// and in e2e/suite/common/common.go, but this breaks Azure provider.
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
// nolint
|
||||
. "github.com/onsi/ginkgo"
|
||||
// nolint
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
|
||||
"github.com/external-secrets/external-secrets/e2e/framework"
|
||||
"github.com/external-secrets/external-secrets/e2e/suite/common"
|
||||
)
|
||||
|
||||
var _ = Describe("[gitlab] ", func() {
|
||||
f := framework.New("esogitlab")
|
||||
credentials := os.Getenv("GITLAB_TOKEN")
|
||||
projectID := os.Getenv("GITLAB_PROJECT_ID")
|
||||
prov := newGitlabProvider(f, credentials, projectID)
|
||||
|
||||
DescribeTable("sync secrets", framework.TableFunc(f, prov),
|
||||
Entry(common.SimpleDataSync(f)),
|
||||
Entry(common.JSONDataWithProperty(f)),
|
||||
Entry(common.JSONDataFromSync(f)),
|
||||
Entry(common.NestedJSONWithGJSON(f)),
|
||||
Entry(common.JSONDataWithTemplate(f)),
|
||||
)
|
||||
})
|
131
e2e/suite/gitlab/provider.go
Normal file
131
e2e/suite/gitlab/provider.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
// nolint
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
||||
// nolint
|
||||
. "github.com/onsi/gomega"
|
||||
gitlab "github.com/xanzy/go-gitlab"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
"github.com/external-secrets/external-secrets/e2e/framework"
|
||||
)
|
||||
|
||||
type gitlabProvider struct {
|
||||
credentials string
|
||||
projectID string
|
||||
framework *framework.Framework
|
||||
}
|
||||
|
||||
func newGitlabProvider(f *framework.Framework, credentials, projectID string) *gitlabProvider {
|
||||
prov := &gitlabProvider{
|
||||
credentials: credentials,
|
||||
projectID: projectID,
|
||||
framework: f,
|
||||
}
|
||||
BeforeEach(prov.BeforeEach)
|
||||
return prov
|
||||
}
|
||||
|
||||
func (s *gitlabProvider) CreateSecret(key, val string) {
|
||||
// **Open the client
|
||||
client, err := gitlab.NewClient(s.credentials)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Open the client**
|
||||
|
||||
// Set variable options
|
||||
variableKey := strings.ReplaceAll(key, "-", "_")
|
||||
variableValue := val
|
||||
|
||||
opt := gitlab.CreateProjectVariableOptions{
|
||||
Key: &variableKey,
|
||||
Value: &variableValue,
|
||||
VariableType: nil,
|
||||
Protected: nil,
|
||||
Masked: nil,
|
||||
EnvironmentScope: nil,
|
||||
}
|
||||
|
||||
// Create a variable
|
||||
_, _, err = client.ProjectVariables.CreateVariable(s.projectID, &opt)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Versions aren't supported by Gitlab, but we could add
|
||||
// more parameters to test
|
||||
}
|
||||
|
||||
func (s *gitlabProvider) DeleteSecret(key string) {
|
||||
// **Open a client
|
||||
client, err := gitlab.NewClient(s.credentials)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Open a client**
|
||||
|
||||
// Delete the secret
|
||||
_, err = client.ProjectVariables.RemoveVariable(s.projectID, strings.ReplaceAll(key, "-", "_"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
func (s *gitlabProvider) BeforeEach() {
|
||||
By("creating a gitlab variable")
|
||||
gitlabCreds := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "provider-secret",
|
||||
Namespace: s.framework.Namespace.Name,
|
||||
},
|
||||
// Puts access token into StringData
|
||||
|
||||
StringData: map[string]string{
|
||||
"token": s.credentials,
|
||||
"projectID": s.projectID,
|
||||
},
|
||||
}
|
||||
err := s.framework.CRClient.Create(context.Background(), gitlabCreds)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Create a secret store - change these values to match YAML
|
||||
By("creating a secret store for credentials")
|
||||
secretStore := &esv1alpha1.SecretStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.framework.Namespace.Name,
|
||||
Namespace: s.framework.Namespace.Name,
|
||||
},
|
||||
Spec: esv1alpha1.SecretStoreSpec{
|
||||
Provider: &esv1alpha1.SecretStoreProvider{
|
||||
Gitlab: &esv1alpha1.GitlabProvider{
|
||||
ProjectID: s.projectID,
|
||||
Auth: esv1alpha1.GitlabAuth{
|
||||
SecretRef: esv1alpha1.GitlabSecretRef{
|
||||
AccessToken: esmeta.SecretKeySelector{
|
||||
Name: "provider-secret",
|
||||
Key: "token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = s.framework.CRClient.Create(context.Background(), secretStore)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
17
go.mod
17
go.mod
|
@ -3,8 +3,9 @@ module github.com/external-secrets/external-secrets
|
|||
go 1.16
|
||||
|
||||
replace (
|
||||
github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1 => ./apis/externalsecrets/v1alpha1
|
||||
github.com/external-secrets/external-secrets/pkg/provider/gitlab => ./pkg/provider/gitlab
|
||||
google.golang.org/grpc => google.golang.org/grpc v1.27.0
|
||||
|
||||
k8s.io/api => k8s.io/api v0.21.2
|
||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.2
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.21.2
|
||||
|
@ -39,6 +40,7 @@ require (
|
|||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/IBM/go-sdk-core/v5 v5.5.0
|
||||
github.com/IBM/secrets-manager-go-sdk v1.0.23
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192
|
||||
github.com/aws/aws-sdk-go v1.38.6
|
||||
github.com/crossplane/crossplane-runtime v0.13.0
|
||||
github.com/fatih/color v1.10.0 // indirect
|
||||
|
@ -51,13 +53,12 @@ require (
|
|||
github.com/googleapis/gax-go v1.0.3
|
||||
github.com/hashicorp/go-hclog v0.14.1 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.6.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault // indirect
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.1
|
||||
github.com/onsi/ginkgo v1.16.4
|
||||
github.com/onsi/gomega v1.13.0
|
||||
github.com/onsi/gomega v1.16.0
|
||||
github.com/oracle/oci-go-sdk/v45 v45.2.0
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
|
@ -65,6 +66,9 @@ require (
|
|||
github.com/spf13/cobra v1.1.3 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tidwall/gjson v1.7.5
|
||||
github.com/xanzy/go-gitlab v0.50.1
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
go.uber.org/zap v1.17.0
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
|
@ -73,12 +77,13 @@ require (
|
|||
golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect
|
||||
google.golang.org/api v0.30.0
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a
|
||||
google.golang.org/grpc v1.31.0
|
||||
honnef.co/go/tools v0.1.4 // indirect
|
||||
k8s.io/api v0.21.2
|
||||
k8s.io/apimachinery v0.21.2
|
||||
k8s.io/api v0.21.3
|
||||
k8s.io/apimachinery v0.21.3
|
||||
k8s.io/client-go v0.21.2
|
||||
k8s.io/utils v0.0.0-20210527160623-6fdb442a123b
|
||||
sigs.k8s.io/controller-runtime v0.9.2
|
||||
sigs.k8s.io/controller-runtime v0.9.3
|
||||
sigs.k8s.io/controller-tools v0.5.0
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
|
||||
)
|
||||
|
|
38
go.sum
38
go.sum
|
@ -77,6 +77,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
|||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192 h1:rRuMCkcoxoQ/kWSBN190JmD292PrYnpl7KyRWhYrjnY=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
|
@ -98,6 +100,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
|
|||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
|
@ -135,6 +138,7 @@ github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D
|
|||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
|
@ -166,6 +170,7 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq
|
|||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
|
@ -242,6 +247,7 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
|||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
|
@ -291,6 +297,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
|
@ -320,6 +328,7 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i
|
|||
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
|
||||
github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
|
||||
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
|
@ -352,8 +361,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
|
|||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
|
@ -401,6 +410,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
|
|||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
|
@ -408,6 +418,7 @@ github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMW
|
|||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
|
@ -479,6 +490,7 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
|
|||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
|
@ -530,8 +542,9 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
|
|||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
|
||||
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
|
||||
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/oracle/oci-go-sdk/v45 v45.2.0 h1:vCPoQlE+DOrM2heJn66rvPU6fbsc/0Cxtzs2jnFut6U=
|
||||
github.com/oracle/oci-go-sdk/v45 v45.2.0/go.mod h1:ZM6LGiRO5TPQJxTlrXbcHMbClE775wnGD5U/EerCsRw=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
|
@ -596,7 +609,10 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
|
|||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
|
@ -642,11 +658,17 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
|
|||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ=
|
||||
github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588 h1:Lbz8X5Nre0Lg5QgCblmo0AhScWxeN3CVnX+mZ5Hxksk=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa h1:Un1jWl/YWbK1179aMbsEZ6uLlDjjBAjL8KXldho1Umo=
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa/go.mod h1:UkgAKjyQo+Pylt2HTYz/G0PgnxmKOJ9IX/3XiRYQ9Ns=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||
|
@ -765,6 +787,7 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
|
@ -783,6 +806,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
|||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -971,6 +995,7 @@ google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
|||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
|
@ -998,6 +1023,7 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG
|
|||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
|
@ -1041,6 +1067,8 @@ gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+a
|
|||
gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
|
@ -1106,8 +1134,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
|||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
|
||||
sigs.k8s.io/controller-runtime v0.8.0/go.mod h1:v9Lbj5oX443uR7GXYY46E0EE2o7k2YxQ58GxVNeXSW4=
|
||||
sigs.k8s.io/controller-runtime v0.9.2 h1:MnCAsopQno6+hI9SgJHKddzXpmv2wtouZz6931Eax+Q=
|
||||
sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk=
|
||||
sigs.k8s.io/controller-runtime v0.9.3 h1:n075bHQ1wb8hpX7C27pNrqsb0fj8mcfCQfNX+oKTbYE=
|
||||
sigs.k8s.io/controller-runtime v0.9.3/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk=
|
||||
sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA=
|
||||
sigs.k8s.io/controller-tools v0.5.0 h1:3u2RCwOlp0cjCALAigpOcbAf50pE+kHSdueUosrC/AE=
|
||||
sigs.k8s.io/controller-tools v0.5.0/go.mod h1:JTsstrMpxs+9BUj6eGuAaEb6SDSPTeVtUyp0jmnAM/I=
|
||||
|
|
|
@ -46,6 +46,10 @@ nav:
|
|||
- IBM:
|
||||
- Secrets Manager: provider-ibm-secrets-manager.md
|
||||
- HashiCorp Vault: provider-hashicorp-vault.md
|
||||
- Yandex:
|
||||
- Lockbox: provider-yandex-lockbox.md
|
||||
- Gitlab:
|
||||
- Gitlab Project Variables: provider-gitlab-project-variables.md
|
||||
- References:
|
||||
- API specification: spec.md
|
||||
- Contributing:
|
||||
|
|
3
main.go
3
main.go
|
@ -46,12 +46,14 @@ func main() {
|
|||
var controllerClass string
|
||||
var enableLeaderElection bool
|
||||
var loglevel string
|
||||
var namespace string
|
||||
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.StringVar(&controllerClass, "controller-class", "default", "the controller is instantiated with a specific controller name and filters ES based on this property")
|
||||
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")
|
||||
flag.StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only")
|
||||
flag.Parse()
|
||||
|
||||
var lvl zapcore.Level
|
||||
|
@ -69,6 +71,7 @@ func main() {
|
|||
Port: 9443,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "external-secrets-controller",
|
||||
Namespace: namespace,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
|
|
|
@ -147,7 +147,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
}
|
||||
|
||||
defer func() {
|
||||
err = secretClient.Close()
|
||||
err = secretClient.Close(ctx)
|
||||
if err != nil {
|
||||
log.Error(err, errCloseStoreClient)
|
||||
}
|
||||
|
@ -167,9 +167,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
return ctrl.Result{RequeueAfter: refreshInt}, nil
|
||||
}
|
||||
|
||||
// Target Secret Name should default to the ExternalSecret name if not explicitly specified
|
||||
secretName := externalSecret.Spec.Target.Name
|
||||
if secretName == "" {
|
||||
secretName = externalSecret.ObjectMeta.Name
|
||||
}
|
||||
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: externalSecret.Spec.Target.Name,
|
||||
Name: secretName,
|
||||
Namespace: externalSecret.Namespace,
|
||||
},
|
||||
Data: make(map[string][]byte),
|
||||
|
@ -194,9 +200,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
|
||||
// no template: copy data and return
|
||||
if externalSecret.Spec.Target.Template == nil {
|
||||
for k, v := range dataMap {
|
||||
secret.Data[k] = v
|
||||
}
|
||||
secret.Data = dataMap
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
35
pkg/provider/alibaba/fake/fake.go
Normal file
35
pkg/provider/alibaba/fake/fake.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package fake
|
||||
|
||||
import (
|
||||
kmssdk "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
|
||||
)
|
||||
|
||||
type AlibabaMockClient struct {
|
||||
getSecretValue func(request *kmssdk.GetSecretValueRequest) (response *kmssdk.GetSecretValueResponse, err error)
|
||||
}
|
||||
|
||||
func (mc *AlibabaMockClient) GetSecretValue(*kmssdk.GetSecretValueRequest) (result *kmssdk.GetSecretValueResponse, err error) {
|
||||
return mc.getSecretValue(&kmssdk.GetSecretValueRequest{})
|
||||
}
|
||||
|
||||
func (mc *AlibabaMockClient) WithValue(in *kmssdk.GetSecretValueRequest, val *kmssdk.GetSecretValueResponse, err error) {
|
||||
if mc != nil {
|
||||
mc.getSecretValue = func(paramIn *kmssdk.GetSecretValueRequest) (*kmssdk.GetSecretValueResponse, error) {
|
||||
return val, err
|
||||
}
|
||||
}
|
||||
}
|
193
pkg/provider/alibaba/kms.go
Normal file
193
pkg/provider/alibaba/kms.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package alibaba
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
kmssdk "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
|
||||
"github.com/tidwall/gjson"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/schema"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
errAlibabaClient = "cannot setup new Alibaba client: %w"
|
||||
errAlibabaCredSecretName = "invalid Alibaba SecretStore resource: missing Alibaba APIKey"
|
||||
errUninitalizedAlibabaProvider = "provider Alibaba is not initialized"
|
||||
errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterStore, missing AccessKeyID namespace"
|
||||
errInvalidClusterStoreMissingSKNamespace = "invalid ClusterStore, missing namespace"
|
||||
errFetchAKIDSecret = "could not fetch AccessKeyID secret: %w"
|
||||
errMissingSAK = "missing AccessSecretKey"
|
||||
errMissingAKID = "missing AccessKeyID"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
kube kclient.Client
|
||||
store *esv1alpha1.AlibabaProvider
|
||||
namespace string
|
||||
storeKind string
|
||||
regionID string
|
||||
keyID []byte
|
||||
accessKey []byte
|
||||
}
|
||||
|
||||
type KeyManagementService struct {
|
||||
Client SMInterface
|
||||
}
|
||||
|
||||
type SMInterface interface {
|
||||
GetSecretValue(request *kmssdk.GetSecretValueRequest) (response *kmssdk.GetSecretValueResponse, err error)
|
||||
}
|
||||
|
||||
// setAuth creates a new Alibaba session based on a store.
|
||||
func (c *Client) setAuth(ctx context.Context) error {
|
||||
credentialsSecret := &corev1.Secret{}
|
||||
credentialsSecretName := c.store.Auth.SecretRef.AccessKeyID.Name
|
||||
if credentialsSecretName == "" {
|
||||
return fmt.Errorf(errAlibabaCredSecretName)
|
||||
}
|
||||
objectKey := types.NamespacedName{
|
||||
Name: credentialsSecretName,
|
||||
Namespace: c.namespace,
|
||||
}
|
||||
|
||||
// only ClusterStore is allowed to set namespace (and then it's required)
|
||||
if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
|
||||
if c.store.Auth.SecretRef.AccessKeyID.Namespace == nil {
|
||||
return fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace)
|
||||
}
|
||||
objectKey.Namespace = *c.store.Auth.SecretRef.AccessKeyID.Namespace
|
||||
}
|
||||
|
||||
err := c.kube.Get(ctx, objectKey, credentialsSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errFetchAKIDSecret, err)
|
||||
}
|
||||
|
||||
objectKey = types.NamespacedName{
|
||||
Name: c.store.Auth.SecretRef.AccessKeySecret.Name,
|
||||
Namespace: c.namespace,
|
||||
}
|
||||
if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
|
||||
if c.store.Auth.SecretRef.AccessKeySecret.Namespace == nil {
|
||||
return fmt.Errorf(errInvalidClusterStoreMissingSKNamespace)
|
||||
}
|
||||
objectKey.Namespace = *c.store.Auth.SecretRef.AccessKeySecret.Namespace
|
||||
}
|
||||
c.keyID = credentialsSecret.Data[c.store.Auth.SecretRef.AccessKeyID.Key]
|
||||
fmt.Println(c.keyID)
|
||||
fmt.Println(c.accessKey)
|
||||
if (c.keyID == nil) || (len(c.keyID) == 0) {
|
||||
return fmt.Errorf(errMissingAKID)
|
||||
}
|
||||
c.accessKey = credentialsSecret.Data[c.store.Auth.SecretRef.AccessKeySecret.Key]
|
||||
if (c.accessKey == nil) || (len(c.accessKey) == 0) {
|
||||
return fmt.Errorf(errMissingSAK)
|
||||
}
|
||||
c.regionID = c.store.RegionID
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSecret returns a single secret from the provider.
|
||||
func (kms *KeyManagementService) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
if utils.IsNil(kms.Client) {
|
||||
return nil, fmt.Errorf(errUninitalizedAlibabaProvider)
|
||||
}
|
||||
kmsRequest := kmssdk.CreateGetSecretValueRequest()
|
||||
kmsRequest.VersionId = ref.Version
|
||||
kmsRequest.SecretName = ref.Key
|
||||
kmsRequest.SetScheme("https")
|
||||
secretOut, err := kms.Client.GetSecretValue(kmsRequest)
|
||||
if err != nil {
|
||||
return nil, util.SanitizeErr(err)
|
||||
}
|
||||
if ref.Property == "" {
|
||||
if secretOut.SecretData != "" {
|
||||
return []byte(secretOut.SecretData), nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid secret received. no secret string nor binary for key: %s", ref.Key)
|
||||
}
|
||||
var payload string
|
||||
if secretOut.SecretData != "" {
|
||||
payload = secretOut.SecretData
|
||||
}
|
||||
val := gjson.Get(payload, ref.Property)
|
||||
if !val.Exists() {
|
||||
return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
|
||||
}
|
||||
return []byte(val.String()), nil
|
||||
}
|
||||
|
||||
// GetSecretMap returns multiple k/v pairs from the provider.
|
||||
func (kms *KeyManagementService) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||
data, err := kms.GetSecret(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kv := make(map[string]string)
|
||||
err = json.Unmarshal(data, &kv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
|
||||
}
|
||||
secretData := make(map[string][]byte)
|
||||
for k, v := range kv {
|
||||
secretData[k] = []byte(v)
|
||||
}
|
||||
return secretData, nil
|
||||
}
|
||||
|
||||
// NewClient constructs a new secrets client based on the provided store.
|
||||
func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
|
||||
storeSpec := store.GetSpec()
|
||||
alibabaSpec := storeSpec.Provider.Alibaba
|
||||
iStore := &Client{
|
||||
kube: kube,
|
||||
store: alibabaSpec,
|
||||
namespace: namespace,
|
||||
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
|
||||
}
|
||||
if err := iStore.setAuth(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alibabaRegion := iStore.regionID
|
||||
alibabaKeyID := iStore.keyID
|
||||
alibabaSecretKey := iStore.accessKey
|
||||
keyManagementService, err := kmssdk.NewClientWithAccessKey(alibabaRegion, string(alibabaKeyID), string(alibabaSecretKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errAlibabaClient, err)
|
||||
}
|
||||
kms.Client = keyManagementService
|
||||
return kms, nil
|
||||
}
|
||||
|
||||
func (kms *KeyManagementService) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
schema.Register(&KeyManagementService{}, &esv1alpha1.SecretStoreProvider{
|
||||
Alibaba: &esv1alpha1.AlibabaProvider{},
|
||||
})
|
||||
}
|
197
pkg/provider/alibaba/kms_test.go
Normal file
197
pkg/provider/alibaba/kms_test.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package alibaba
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
|
||||
kmssdk "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
fakesm "github.com/external-secrets/external-secrets/pkg/provider/alibaba/fake"
|
||||
)
|
||||
|
||||
const (
|
||||
secretName = "test-example"
|
||||
secretValue = "value"
|
||||
)
|
||||
|
||||
type keyManagementServiceTestCase struct {
|
||||
mockClient *fakesm.AlibabaMockClient
|
||||
apiInput *kmssdk.GetSecretValueRequest
|
||||
apiOutput *kmssdk.GetSecretValueResponse
|
||||
ref *esv1alpha1.ExternalSecretDataRemoteRef
|
||||
apiErr error
|
||||
expectError string
|
||||
expectedSecret string
|
||||
// for testing secretmap
|
||||
expectedData map[string][]byte
|
||||
}
|
||||
|
||||
func makeValidKMSTestCase() *keyManagementServiceTestCase {
|
||||
kmstc := keyManagementServiceTestCase{
|
||||
mockClient: &fakesm.AlibabaMockClient{},
|
||||
apiInput: makeValidAPIInput(),
|
||||
ref: makeValidRef(),
|
||||
apiOutput: makeValidAPIOutput(),
|
||||
apiErr: nil,
|
||||
expectError: "",
|
||||
expectedSecret: "",
|
||||
expectedData: make(map[string][]byte),
|
||||
}
|
||||
kmstc.mockClient.WithValue(kmstc.apiInput, kmstc.apiOutput, kmstc.apiErr)
|
||||
return &kmstc
|
||||
}
|
||||
|
||||
func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
|
||||
return &esv1alpha1.ExternalSecretDataRemoteRef{
|
||||
Key: secretName,
|
||||
}
|
||||
}
|
||||
|
||||
func makeValidAPIInput() *kmssdk.GetSecretValueRequest {
|
||||
return &kmssdk.GetSecretValueRequest{
|
||||
SecretName: secretName,
|
||||
}
|
||||
}
|
||||
|
||||
func makeValidAPIOutput() *kmssdk.GetSecretValueResponse {
|
||||
kmsresponse := &kmssdk.GetSecretValueResponse{
|
||||
BaseResponse: &responses.BaseResponse{},
|
||||
RequestId: "",
|
||||
SecretName: secretName,
|
||||
VersionId: "",
|
||||
CreateTime: "",
|
||||
SecretData: secretValue,
|
||||
SecretDataType: "",
|
||||
AutomaticRotation: "",
|
||||
RotationInterval: "",
|
||||
NextRotationDate: "",
|
||||
ExtendedConfig: "",
|
||||
LastRotationDate: "",
|
||||
SecretType: "",
|
||||
VersionStages: kmssdk.VersionStagesInGetSecretValue{},
|
||||
}
|
||||
return kmsresponse
|
||||
}
|
||||
|
||||
func makeValidKMSTestCaseCustom(tweaks ...func(kmstc *keyManagementServiceTestCase)) *keyManagementServiceTestCase {
|
||||
kmstc := makeValidKMSTestCase()
|
||||
for _, fn := range tweaks {
|
||||
fn(kmstc)
|
||||
}
|
||||
kmstc.mockClient.WithValue(kmstc.apiInput, kmstc.apiOutput, kmstc.apiErr)
|
||||
return kmstc
|
||||
}
|
||||
|
||||
var setAPIErr = func(kmstc *keyManagementServiceTestCase) {
|
||||
kmstc.apiErr = fmt.Errorf("oh no")
|
||||
kmstc.expectError = "oh no"
|
||||
}
|
||||
|
||||
var setNilMockClient = func(kmstc *keyManagementServiceTestCase) {
|
||||
kmstc.mockClient = nil
|
||||
kmstc.expectError = errUninitalizedAlibabaProvider
|
||||
}
|
||||
|
||||
func TestAlibabaKMSGetSecret(t *testing.T) {
|
||||
secretData := make(map[string]interface{})
|
||||
secretValue := "changedvalue"
|
||||
secretData["payload"] = secretValue
|
||||
|
||||
// good case: default version is set
|
||||
// key is passed in, output is sent back
|
||||
setSecretString := func(kmstc *keyManagementServiceTestCase) {
|
||||
kmstc.apiOutput.SecretName = secretName
|
||||
kmstc.apiOutput.SecretData = secretValue
|
||||
kmstc.expectedSecret = secretValue
|
||||
}
|
||||
|
||||
// good case: custom version set
|
||||
setCustomKey := func(kmstc *keyManagementServiceTestCase) {
|
||||
kmstc.apiOutput.SecretName = "test-example-other"
|
||||
kmstc.ref.Key = "test-example-other"
|
||||
kmstc.apiOutput.SecretData = secretValue
|
||||
kmstc.expectedSecret = secretValue
|
||||
}
|
||||
|
||||
successCases := []*keyManagementServiceTestCase{
|
||||
makeValidKMSTestCaseCustom(setSecretString),
|
||||
makeValidKMSTestCaseCustom(setCustomKey),
|
||||
makeValidKMSTestCaseCustom(setAPIErr),
|
||||
makeValidKMSTestCaseCustom(setNilMockClient),
|
||||
}
|
||||
|
||||
sm := KeyManagementService{}
|
||||
for k, v := range successCases {
|
||||
sm.Client = v.mockClient
|
||||
out, err := sm.GetSecret(context.Background(), *v.ref)
|
||||
if !ErrorContains(err, v.expectError) {
|
||||
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
|
||||
}
|
||||
if string(out) != v.expectedSecret {
|
||||
t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecretMap(t *testing.T) {
|
||||
// good case: default version & deserialization
|
||||
setDeserialization := func(kmstc *keyManagementServiceTestCase) {
|
||||
kmstc.apiOutput.SecretName = "foo"
|
||||
kmstc.expectedData["foo"] = []byte("bar")
|
||||
kmstc.apiOutput.SecretData = `{"foo":"bar"}`
|
||||
}
|
||||
|
||||
// bad case: invalid json
|
||||
setInvalidJSON := func(kmstc *keyManagementServiceTestCase) {
|
||||
kmstc.apiOutput.SecretData = "-----------------"
|
||||
kmstc.expectError = "unable to unmarshal secret"
|
||||
}
|
||||
|
||||
successCases := []*keyManagementServiceTestCase{
|
||||
makeValidKMSTestCaseCustom(setDeserialization),
|
||||
makeValidKMSTestCaseCustom(setInvalidJSON),
|
||||
makeValidKMSTestCaseCustom(setNilMockClient),
|
||||
makeValidKMSTestCaseCustom(setAPIErr),
|
||||
}
|
||||
|
||||
sm := KeyManagementService{}
|
||||
for k, v := range successCases {
|
||||
sm.Client = v.mockClient
|
||||
out, err := sm.GetSecretMap(context.Background(), *v.ref)
|
||||
if !ErrorContains(err, v.expectError) {
|
||||
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
|
||||
}
|
||||
if err == nil && !reflect.DeepEqual(out, v.expectedData) {
|
||||
t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorContains(out error, want string) bool {
|
||||
if out == nil {
|
||||
return want == ""
|
||||
}
|
||||
if want == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(out.Error(), want)
|
||||
}
|
|
@ -90,6 +90,6 @@ func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
|
|||
return secretData, nil
|
||||
}
|
||||
|
||||
func (pm *ParameterStore) Close() error {
|
||||
func (pm *ParameterStore) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package fake
|
||||
|
||||
import (
|
||||
|
@ -22,15 +23,38 @@ import (
|
|||
|
||||
// Client implements the aws secretsmanager interface.
|
||||
type Client struct {
|
||||
valFn func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
|
||||
ExecutionCounter int
|
||||
valFn map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
|
||||
}
|
||||
|
||||
// NewClient init a new fake client.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
valFn: make(map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)),
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *Client) GetSecretValue(in *awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) {
|
||||
return sm.valFn(in)
|
||||
sm.ExecutionCounter++
|
||||
if entry, found := sm.valFn[sm.cacheKeyForInput(in)]; found {
|
||||
return entry(in)
|
||||
}
|
||||
return nil, fmt.Errorf("test case not found")
|
||||
}
|
||||
|
||||
func (sm *Client) cacheKeyForInput(in *awssm.GetSecretValueInput) string {
|
||||
var secretID, versionID string
|
||||
if in.SecretId != nil {
|
||||
secretID = *in.SecretId
|
||||
}
|
||||
if in.VersionId != nil {
|
||||
versionID = *in.VersionId
|
||||
}
|
||||
return fmt.Sprintf("%s#%s", secretID, versionID)
|
||||
}
|
||||
|
||||
func (sm *Client) WithValue(in *awssm.GetSecretValueInput, val *awssm.GetSecretValueOutput, err error) {
|
||||
sm.valFn = func(paramIn *awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) {
|
||||
sm.valFn[sm.cacheKeyForInput(in)] = func(paramIn *awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) {
|
||||
if !cmp.Equal(paramIn, in) {
|
||||
return nil, fmt.Errorf("unexpected test argument")
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package secretsmanager
|
||||
|
||||
import (
|
||||
|
@ -30,6 +31,7 @@ import (
|
|||
// SecretsManager is a provider for AWS SecretsManager.
|
||||
type SecretsManager struct {
|
||||
client SMInterface
|
||||
cache map[string]*awssm.GetSecretValueOutput
|
||||
}
|
||||
|
||||
// SMInterface is a subset of the smiface api.
|
||||
|
@ -44,20 +46,37 @@ var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager
|
|||
func New(sess client.ConfigProvider) (*SecretsManager, error) {
|
||||
return &SecretsManager{
|
||||
client: awssm.New(sess),
|
||||
cache: make(map[string]*awssm.GetSecretValueOutput),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSecret returns a single secret from the provider.
|
||||
func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
func (sm *SecretsManager) fetch(_ context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (*awssm.GetSecretValueOutput, error) {
|
||||
ver := "AWSCURRENT"
|
||||
if ref.Version != "" {
|
||||
ver = ref.Version
|
||||
}
|
||||
log.Info("fetching secret value", "key", ref.Key, "version", ver)
|
||||
|
||||
cacheKey := fmt.Sprintf("%s#%s", ref.Key, ver)
|
||||
if secretOut, found := sm.cache[cacheKey]; found {
|
||||
log.Info("found secret in cache", "key", ref.Key, "version", ver)
|
||||
return secretOut, nil
|
||||
}
|
||||
secretOut, err := sm.client.GetSecretValue(&awssm.GetSecretValueInput{
|
||||
SecretId: &ref.Key,
|
||||
VersionStage: &ver,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sm.cache[cacheKey] = secretOut
|
||||
|
||||
return secretOut, nil
|
||||
}
|
||||
|
||||
// GetSecret returns a single secret from the provider.
|
||||
func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
secretOut, err := sm.fetch(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, util.SanitizeErr(err)
|
||||
}
|
||||
|
@ -77,6 +96,7 @@ func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1alpha1.External
|
|||
if secretOut.SecretBinary != nil {
|
||||
payload = string(secretOut.SecretBinary)
|
||||
}
|
||||
|
||||
val := gjson.Get(payload, ref.Property)
|
||||
if !val.Exists() {
|
||||
return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
|
||||
|
@ -103,6 +123,6 @@ func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
|
|||
return secretData, nil
|
||||
}
|
||||
|
||||
func (sm *SecretsManager) Close() error {
|
||||
func (sm *SecretsManager) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package secretsmanager
|
||||
|
||||
import (
|
||||
|
@ -37,11 +38,13 @@ type secretsManagerTestCase struct {
|
|||
expectedSecret string
|
||||
// for testing secretmap
|
||||
expectedData map[string][]byte
|
||||
// for testing caching
|
||||
expectedCounter *int
|
||||
}
|
||||
|
||||
func makeValidSecretsManagerTestCase() *secretsManagerTestCase {
|
||||
smtc := secretsManagerTestCase{
|
||||
fakeClient: &fakesm.Client{},
|
||||
fakeClient: fakesm.NewClient(),
|
||||
apiInput: makeValidAPIInput(),
|
||||
remoteRef: makeValidRemoteRef(),
|
||||
apiOutput: makeValidAPIOutput(),
|
||||
|
@ -164,9 +167,11 @@ func TestSecretsManagerGetSecret(t *testing.T) {
|
|||
makeValidSecretsManagerTestCaseCustom(setAPIErr),
|
||||
}
|
||||
|
||||
sm := SecretsManager{}
|
||||
for k, v := range successCases {
|
||||
sm.client = v.fakeClient
|
||||
sm := SecretsManager{
|
||||
cache: make(map[string]*awssm.GetSecretValueOutput),
|
||||
client: v.fakeClient,
|
||||
}
|
||||
out, err := sm.GetSecret(context.Background(), *v.remoteRef)
|
||||
if !ErrorContains(err, v.expectError) {
|
||||
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
|
||||
|
@ -176,6 +181,58 @@ func TestSecretsManagerGetSecret(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
func TestCaching(t *testing.T) {
|
||||
fakeClient := fakesm.NewClient()
|
||||
|
||||
// good case: first call, since we are using the same key, results should be cached and the counter should not go
|
||||
// over 1
|
||||
firstCall := func(smtc *secretsManagerTestCase) {
|
||||
smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "bar":"vodka"}`)
|
||||
smtc.remoteRef.Property = "foo"
|
||||
smtc.expectedSecret = "bar"
|
||||
smtc.expectedCounter = aws.Int(1)
|
||||
smtc.fakeClient = fakeClient
|
||||
}
|
||||
secondCall := func(smtc *secretsManagerTestCase) {
|
||||
smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "bar":"vodka"}`)
|
||||
smtc.remoteRef.Property = "bar"
|
||||
smtc.expectedSecret = "vodka"
|
||||
smtc.expectedCounter = aws.Int(1)
|
||||
smtc.fakeClient = fakeClient
|
||||
}
|
||||
notCachedCall := func(smtc *secretsManagerTestCase) {
|
||||
smtc.apiOutput.SecretString = aws.String(`{"sheldon":"bazinga", "bar":"foo"}`)
|
||||
smtc.remoteRef.Property = "sheldon"
|
||||
smtc.expectedSecret = "bazinga"
|
||||
smtc.expectedCounter = aws.Int(2)
|
||||
smtc.fakeClient = fakeClient
|
||||
smtc.apiInput.SecretId = aws.String("xyz")
|
||||
smtc.remoteRef.Key = "xyz" // it should reset the cache since the key is different
|
||||
}
|
||||
|
||||
cachedCases := []*secretsManagerTestCase{
|
||||
makeValidSecretsManagerTestCaseCustom(firstCall),
|
||||
makeValidSecretsManagerTestCaseCustom(firstCall),
|
||||
makeValidSecretsManagerTestCaseCustom(secondCall),
|
||||
makeValidSecretsManagerTestCaseCustom(notCachedCall),
|
||||
}
|
||||
sm := SecretsManager{
|
||||
cache: make(map[string]*awssm.GetSecretValueOutput),
|
||||
}
|
||||
for k, v := range cachedCases {
|
||||
sm.client = v.fakeClient
|
||||
out, err := sm.GetSecret(context.Background(), *v.remoteRef)
|
||||
if !ErrorContains(err, v.expectError) {
|
||||
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
|
||||
}
|
||||
if err == nil && string(out) != v.expectedSecret {
|
||||
t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
|
||||
}
|
||||
if v.expectedCounter != nil && v.fakeClient.ExecutionCounter != *v.expectedCounter {
|
||||
t.Errorf("[%d] unexpected counter value: expected %d, got %d", k, v.expectedCounter, v.fakeClient.ExecutionCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecretMap(t *testing.T) {
|
||||
// good case: default version & deserialization
|
||||
|
@ -184,6 +241,14 @@ func TestGetSecretMap(t *testing.T) {
|
|||
smtc.expectedData["foo"] = []byte("bar")
|
||||
}
|
||||
|
||||
// good case: caching
|
||||
cachedMap := func(smtc *secretsManagerTestCase) {
|
||||
smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "plus": "one"}`)
|
||||
smtc.expectedData["foo"] = []byte("bar")
|
||||
smtc.expectedData["plus"] = []byte("one")
|
||||
smtc.expectedCounter = aws.Int(1)
|
||||
}
|
||||
|
||||
// bad case: invalid json
|
||||
setInvalidJSON := func(smtc *secretsManagerTestCase) {
|
||||
smtc.apiOutput.SecretString = aws.String(`-----------------`)
|
||||
|
@ -194,11 +259,14 @@ func TestGetSecretMap(t *testing.T) {
|
|||
makeValidSecretsManagerTestCaseCustom(setDeserialization),
|
||||
makeValidSecretsManagerTestCaseCustom(setAPIErr),
|
||||
makeValidSecretsManagerTestCaseCustom(setInvalidJSON),
|
||||
makeValidSecretsManagerTestCaseCustom(cachedMap),
|
||||
}
|
||||
|
||||
sm := SecretsManager{}
|
||||
for k, v := range successCases {
|
||||
sm.client = v.fakeClient
|
||||
sm := SecretsManager{
|
||||
cache: make(map[string]*awssm.GetSecretValueOutput),
|
||||
client: v.fakeClient,
|
||||
}
|
||||
out, err := sm.GetSecretMap(context.Background(), *v.remoteRef)
|
||||
if !ErrorContains(err, v.expectError) {
|
||||
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
|
||||
|
@ -206,6 +274,9 @@ func TestGetSecretMap(t *testing.T) {
|
|||
if err == nil && !cmp.Equal(out, v.expectedData) {
|
||||
t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
|
||||
}
|
||||
if v.expectedCounter != nil && v.fakeClient.ExecutionCounter != *v.expectedCounter {
|
||||
t.Errorf("[%d] unexpected counter value: expected %d, got %d", k, v.expectedCounter, v.fakeClient.ExecutionCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -227,7 +227,7 @@ func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef sm
|
|||
return value, nil
|
||||
}
|
||||
|
||||
func (a *Azure) Close() error {
|
||||
func (a *Azure) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ func (v *Client) WithGetSecret(secData []byte, err error) *Client {
|
|||
func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||
return v.GetSecretMapFn(ctx, ref)
|
||||
}
|
||||
func (v *Client) Close() error {
|
||||
func (v *Client) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -39,12 +39,12 @@ const (
|
|||
defaultVersion = "latest"
|
||||
|
||||
errGCPSMStore = "received invalid GCPSM SecretStore resource"
|
||||
errGCPSMCredSecretName = "invalid GCPSM SecretStore resource: missing GCP Secret Access Key"
|
||||
errClientClose = "unable to close SecretManager client: %w"
|
||||
errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace"
|
||||
errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
|
||||
errMissingSAK = "missing SecretAccessKey"
|
||||
errUnableProcessJSONCredentials = "failed to process the provided JSON credentials: %w"
|
||||
errUnableProcessDefaultCredentials = "failed to process the default credentials: %w"
|
||||
errUnableCreateGCPSMClient = "failed to create GCP secretmanager client: %w"
|
||||
errUninitalizedGCPProvider = "provider GCP is not initialized"
|
||||
errClientGetSecretAccess = "unable to access Secret from SecretManager Client: %w"
|
||||
|
@ -73,9 +73,6 @@ type gClient struct {
|
|||
func (c *gClient) setAuth(ctx context.Context) error {
|
||||
credentialsSecret := &corev1.Secret{}
|
||||
credentialsSecretName := c.store.Auth.SecretRef.SecretAccessKey.Name
|
||||
if credentialsSecretName == "" {
|
||||
return fmt.Errorf(errGCPSMCredSecretName)
|
||||
}
|
||||
objectKey := types.NamespacedName{
|
||||
Name: credentialsSecretName,
|
||||
Namespace: c.namespace,
|
||||
|
@ -83,12 +80,16 @@ func (c *gClient) setAuth(ctx context.Context) error {
|
|||
|
||||
// only ClusterStore is allowed to set namespace (and then it's required)
|
||||
if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
|
||||
if c.store.Auth.SecretRef.SecretAccessKey.Namespace == nil {
|
||||
if credentialsSecretName != "" && c.store.Auth.SecretRef.SecretAccessKey.Namespace == nil {
|
||||
return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
|
||||
} else if credentialsSecretName != "" {
|
||||
objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace
|
||||
}
|
||||
objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace
|
||||
}
|
||||
|
||||
if credentialsSecretName == "" {
|
||||
c.credentials = nil
|
||||
return nil
|
||||
}
|
||||
err := c.kube.Get(ctx, objectKey, credentialsSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errFetchSAKSecret, err)
|
||||
|
@ -122,12 +123,23 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1alpha1.GenericSt
|
|||
|
||||
sm.projectID = cliStore.store.ProjectID
|
||||
|
||||
config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
|
||||
if cliStore.credentials != nil {
|
||||
config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
|
||||
}
|
||||
ts := config.TokenSource(ctx)
|
||||
clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
|
||||
}
|
||||
sm.SecretManagerClient = clientGCPSM
|
||||
return sm, nil
|
||||
}
|
||||
ts, err := google.DefaultTokenSource(ctx, CloudPlatformRole)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errUnableProcessDefaultCredentials, err)
|
||||
}
|
||||
ts := config.TokenSource(ctx)
|
||||
|
||||
clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
|
||||
|
@ -199,7 +211,7 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
|
|||
return secretData, nil
|
||||
}
|
||||
|
||||
func (sm *ProviderGCP) Close() error {
|
||||
func (sm *ProviderGCP) Close(ctx context.Context) error {
|
||||
err := sm.SecretManagerClient.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf(errClientClose, err)
|
||||
|
|
39
pkg/provider/gitlab/fake/fake.go
Normal file
39
pkg/provider/gitlab/fake/fake.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package fake
|
||||
|
||||
import (
|
||||
gitlab "github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
type GitlabMockClient struct {
|
||||
getVariable func(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error)
|
||||
}
|
||||
|
||||
func (mc *GitlabMockClient) GetVariable(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error) {
|
||||
return mc.getVariable(pid, key, nil)
|
||||
}
|
||||
|
||||
func (mc *GitlabMockClient) WithValue(projectIDinput, keyInput string, output *gitlab.ProjectVariable, err error) {
|
||||
if mc != nil {
|
||||
mc.getVariable = func(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error) {
|
||||
// type secretmanagerpb.AccessSecretVersionRequest contains unexported fields
|
||||
// use cmpopts.IgnoreUnexported to ignore all the unexported fields in the cmp.
|
||||
// if !cmp.Equal(paramReq, input, cmpopts.IgnoreUnexported(gitlab.ProjectVariable{})) {
|
||||
// return nil, nil, fmt.Errorf("unexpected test argument")
|
||||
// }
|
||||
return output, nil, err
|
||||
}
|
||||
}
|
||||
}
|
212
pkg/provider/gitlab/gitlab.go
Normal file
212
pkg/provider/gitlab/gitlab.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
gitlab "github.com/xanzy/go-gitlab"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
"github.com/external-secrets/external-secrets/e2e/framework/log"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/schema"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
)
|
||||
|
||||
// Requires GITLAB_TOKEN and GITLAB_PROJECT_ID to be set in environment variables
|
||||
|
||||
const (
|
||||
errGitlabCredSecretName = "credentials are empty"
|
||||
errInvalidClusterStoreMissingSAKNamespace = "invalid clusterStore missing SAK namespace"
|
||||
errFetchSAKSecret = "couldn't find secret on cluster: %w"
|
||||
errMissingSAK = "missing credentials while setting auth"
|
||||
errUninitalizedGitlabProvider = "provider gitlab is not initialized"
|
||||
errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
GetVariable(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error)
|
||||
}
|
||||
|
||||
// Gitlab Provider struct with reference to a GitLab client and a projectID.
|
||||
type Gitlab struct {
|
||||
client Client
|
||||
projectID interface{}
|
||||
}
|
||||
|
||||
// Client for interacting with kubernetes cluster...?
|
||||
type gClient struct {
|
||||
kube kclient.Client
|
||||
store *esv1alpha1.GitlabProvider
|
||||
namespace string
|
||||
storeKind string
|
||||
credentials []byte
|
||||
}
|
||||
|
||||
func init() {
|
||||
schema.Register(&Gitlab{}, &esv1alpha1.SecretStoreProvider{
|
||||
Gitlab: &esv1alpha1.GitlabProvider{},
|
||||
})
|
||||
}
|
||||
|
||||
// Set gClient credentials to Access Token.
|
||||
func (c *gClient) setAuth(ctx context.Context) error {
|
||||
credentialsSecret := &corev1.Secret{}
|
||||
credentialsSecretName := c.store.Auth.SecretRef.AccessToken.Name
|
||||
if credentialsSecretName == "" {
|
||||
return fmt.Errorf(errGitlabCredSecretName)
|
||||
}
|
||||
objectKey := types.NamespacedName{
|
||||
Name: credentialsSecretName,
|
||||
Namespace: c.namespace,
|
||||
}
|
||||
// only ClusterStore is allowed to set namespace (and then it's required)
|
||||
if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
|
||||
if c.store.Auth.SecretRef.AccessToken.Namespace == nil {
|
||||
return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
|
||||
}
|
||||
objectKey.Namespace = *c.store.Auth.SecretRef.AccessToken.Namespace
|
||||
}
|
||||
|
||||
err := c.kube.Get(ctx, objectKey, credentialsSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errFetchSAKSecret, err)
|
||||
}
|
||||
|
||||
c.credentials = credentialsSecret.Data[c.store.Auth.SecretRef.AccessToken.Key]
|
||||
if (c.credentials == nil) || (len(c.credentials) == 0) {
|
||||
return fmt.Errorf(errMissingSAK)
|
||||
}
|
||||
// I don't know where ProjectID is being set
|
||||
// This line SHOULD set it, but instead just breaks everything :)
|
||||
// c.store.ProjectID = string(credentialsSecret.Data[c.store.ProjectID])
|
||||
return nil
|
||||
}
|
||||
|
||||
// Function newGitlabProvider returns a reference to a new instance of a 'Gitlab' struct.
|
||||
func NewGitlabProvider() *Gitlab {
|
||||
return &Gitlab{}
|
||||
}
|
||||
|
||||
// Method on Gitlab Provider to set up client with credentials and populate projectID.
|
||||
func (g *Gitlab) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
|
||||
storeSpec := store.GetSpec()
|
||||
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Gitlab == nil {
|
||||
return nil, fmt.Errorf("no store type or wrong store type")
|
||||
}
|
||||
storeSpecGitlab := storeSpec.Provider.Gitlab
|
||||
|
||||
cliStore := gClient{
|
||||
kube: kube,
|
||||
store: storeSpecGitlab,
|
||||
namespace: namespace,
|
||||
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
|
||||
}
|
||||
|
||||
if err := cliStore.setAuth(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Create client options
|
||||
var opts []gitlab.ClientOptionFunc
|
||||
if cliStore.store.URL != "" {
|
||||
opts = append(opts, gitlab.WithBaseURL(cliStore.store.URL))
|
||||
}
|
||||
// ClientOptionFunc from the gitlab package can be mapped with the CRD
|
||||
// in a similar way to extend functionality of the provider
|
||||
|
||||
// Create a new Gitlab client using credentials and options
|
||||
gitlabClient, err := gitlab.NewClient(string(cliStore.credentials), opts...)
|
||||
if err != nil {
|
||||
log.Logf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
g.client = gitlabClient.ProjectVariables
|
||||
g.projectID = cliStore.store.ProjectID
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *Gitlab) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
if utils.IsNil(g.client) {
|
||||
return nil, fmt.Errorf(errUninitalizedGitlabProvider)
|
||||
}
|
||||
// Need to replace hyphens with underscores to work with Gitlab API
|
||||
ref.Key = strings.ReplaceAll(ref.Key, "-", "_")
|
||||
// Retrieves a gitlab variable in the form
|
||||
// {
|
||||
// "key": "TEST_VARIABLE_1",
|
||||
// "variable_type": "env_var",
|
||||
// "value": "TEST_1",
|
||||
// "protected": false,
|
||||
// "masked": true
|
||||
data, _, err := g.client.GetVariable(g.projectID, ref.Key, nil) // Optional 'filter' parameter could be added later
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ref.Property == "" {
|
||||
if data.Value != "" {
|
||||
return []byte(data.Value), nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid secret received. no secret string for key: %s", ref.Key)
|
||||
}
|
||||
|
||||
var payload string
|
||||
if data.Value != "" {
|
||||
payload = data.Value
|
||||
}
|
||||
|
||||
val := gjson.Get(payload, ref.Property)
|
||||
if !val.Exists() {
|
||||
return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
|
||||
}
|
||||
return []byte(val.String()), nil
|
||||
}
|
||||
|
||||
func (g *Gitlab) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||
// Gets a secret as normal, expecting secret value to be a json object
|
||||
data, err := g.GetSecret(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
|
||||
}
|
||||
|
||||
// Maps the json data to a string:string map
|
||||
kv := make(map[string]string)
|
||||
err = json.Unmarshal(data, &kv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
|
||||
}
|
||||
|
||||
// Converts values in K:V pairs into bytes, while leaving keys as strings
|
||||
secretData := make(map[string][]byte)
|
||||
for k, v := range kv {
|
||||
secretData[k] = []byte(v)
|
||||
}
|
||||
return secretData, nil
|
||||
}
|
||||
|
||||
func (g *Gitlab) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
178
pkg/provider/gitlab/gitlab_test.go
Normal file
178
pkg/provider/gitlab/gitlab_test.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
gitlab "github.com/xanzy/go-gitlab"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
fakegitlab "github.com/external-secrets/external-secrets/pkg/provider/gitlab/fake"
|
||||
)
|
||||
|
||||
type secretManagerTestCase struct {
|
||||
mockClient *fakegitlab.GitlabMockClient
|
||||
apiInputProjectID string
|
||||
apiInputKey string
|
||||
apiOutput *gitlab.ProjectVariable
|
||||
ref *esv1alpha1.ExternalSecretDataRemoteRef
|
||||
projectID *string
|
||||
apiErr error
|
||||
expectError string
|
||||
expectedSecret string
|
||||
// for testing secretmap
|
||||
expectedData map[string][]byte
|
||||
}
|
||||
|
||||
func makeValidSecretManagerTestCase() *secretManagerTestCase {
|
||||
smtc := secretManagerTestCase{
|
||||
mockClient: &fakegitlab.GitlabMockClient{},
|
||||
apiInputProjectID: makeValidAPIInputProjectID(),
|
||||
apiInputKey: makeValidAPIInputKey(),
|
||||
ref: makeValidRef(),
|
||||
projectID: nil,
|
||||
apiOutput: makeValidAPIOutput(),
|
||||
apiErr: nil,
|
||||
expectError: "",
|
||||
expectedSecret: "",
|
||||
expectedData: map[string][]byte{},
|
||||
}
|
||||
smtc.mockClient.WithValue(smtc.apiInputProjectID, smtc.apiInputKey, smtc.apiOutput, smtc.apiErr)
|
||||
return &smtc
|
||||
}
|
||||
|
||||
func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
|
||||
return &esv1alpha1.ExternalSecretDataRemoteRef{
|
||||
Key: "test-secret",
|
||||
Version: "default",
|
||||
}
|
||||
}
|
||||
|
||||
func makeValidAPIInputProjectID() string {
|
||||
return "testID"
|
||||
}
|
||||
|
||||
func makeValidAPIInputKey() string {
|
||||
return "testKey"
|
||||
}
|
||||
|
||||
func makeValidAPIOutput() *gitlab.ProjectVariable {
|
||||
return &gitlab.ProjectVariable{
|
||||
Key: "testKey",
|
||||
Value: "",
|
||||
}
|
||||
}
|
||||
|
||||
func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTestCase)) *secretManagerTestCase {
|
||||
smtc := makeValidSecretManagerTestCase()
|
||||
for _, fn := range tweaks {
|
||||
fn(smtc)
|
||||
}
|
||||
smtc.mockClient.WithValue(smtc.apiInputProjectID, smtc.apiInputKey, smtc.apiOutput, smtc.apiErr)
|
||||
return smtc
|
||||
}
|
||||
|
||||
// This case can be shared by both GetSecret and GetSecretMap tests.
|
||||
// bad case: set apiErr.
|
||||
var setAPIErr = func(smtc *secretManagerTestCase) {
|
||||
smtc.apiErr = fmt.Errorf("oh no")
|
||||
smtc.expectError = "oh no"
|
||||
}
|
||||
|
||||
var setNilMockClient = func(smtc *secretManagerTestCase) {
|
||||
smtc.mockClient = nil
|
||||
smtc.expectError = errUninitalizedGitlabProvider
|
||||
}
|
||||
|
||||
// test the sm<->gcp interface
|
||||
// make sure correct values are passed and errors are handled accordingly.
|
||||
func TestGitlabSecretManagerGetSecret(t *testing.T) {
|
||||
secretValue := "changedvalue"
|
||||
// good case: default version is set
|
||||
// key is passed in, output is sent back
|
||||
|
||||
setSecretString := func(smtc *secretManagerTestCase) {
|
||||
smtc.apiOutput = &gitlab.ProjectVariable{
|
||||
Key: "testkey",
|
||||
Value: "changedvalue",
|
||||
}
|
||||
smtc.expectedSecret = secretValue
|
||||
}
|
||||
|
||||
successCases := []*secretManagerTestCase{
|
||||
makeValidSecretManagerTestCaseCustom(setSecretString),
|
||||
makeValidSecretManagerTestCaseCustom(setAPIErr),
|
||||
makeValidSecretManagerTestCaseCustom(setNilMockClient),
|
||||
}
|
||||
|
||||
sm := Gitlab{}
|
||||
for k, v := range successCases {
|
||||
sm.client = v.mockClient
|
||||
out, err := sm.GetSecret(context.Background(), *v.ref)
|
||||
if !ErrorContains(err, v.expectError) {
|
||||
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
|
||||
}
|
||||
if string(out) != v.expectedSecret {
|
||||
t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecretMap(t *testing.T) {
|
||||
// good case: default version & deserialization
|
||||
setDeserialization := func(smtc *secretManagerTestCase) {
|
||||
smtc.apiOutput.Value = `{"foo":"bar"}`
|
||||
smtc.expectedData["foo"] = []byte("bar")
|
||||
}
|
||||
|
||||
// bad case: invalid json
|
||||
setInvalidJSON := func(smtc *secretManagerTestCase) {
|
||||
smtc.apiOutput.Value = `-----------------`
|
||||
smtc.expectError = "unable to unmarshal secret"
|
||||
}
|
||||
|
||||
successCases := []*secretManagerTestCase{
|
||||
makeValidSecretManagerTestCaseCustom(setDeserialization),
|
||||
makeValidSecretManagerTestCaseCustom(setInvalidJSON),
|
||||
makeValidSecretManagerTestCaseCustom(setNilMockClient),
|
||||
makeValidSecretManagerTestCaseCustom(setAPIErr),
|
||||
}
|
||||
|
||||
sm := Gitlab{}
|
||||
for k, v := range successCases {
|
||||
sm.client = v.mockClient
|
||||
out, err := sm.GetSecretMap(context.Background(), *v.ref)
|
||||
if !ErrorContains(err, v.expectError) {
|
||||
t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
|
||||
}
|
||||
if err == nil && !reflect.DeepEqual(out, v.expectedData) {
|
||||
t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorContains(out error, want string) bool {
|
||||
if out == nil {
|
||||
return want == ""
|
||||
}
|
||||
if want == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(out.Error(), want)
|
||||
}
|
|
@ -289,7 +289,7 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa
|
|||
}
|
||||
}
|
||||
|
||||
func (ibm *providerIBM) Close() error {
|
||||
func (ibm *providerIBM) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -35,5 +35,5 @@ type SecretsClient interface {
|
|||
|
||||
// GetSecretMap returns multiple k/v pairs from the provider
|
||||
GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
|
||||
Close() error
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
|
|
@ -17,10 +17,13 @@ package register
|
|||
// packages imported here are registered to the controller schema.
|
||||
// nolint:golint
|
||||
import (
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
|
||||
_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
|
||||
)
|
||||
|
|
|
@ -92,7 +92,7 @@ func GetProvider(s esv1alpha1.GenericStore) (provider.Provider, error) {
|
|||
// or an error if the provider is not configured.
|
||||
func getProviderName(storeSpec *esv1alpha1.SecretStoreProvider) (string, error) {
|
||||
storeBytes, err := json.Marshal(storeSpec)
|
||||
if err != nil {
|
||||
if err != nil || storeBytes == nil {
|
||||
return "", fmt.Errorf("failed to marshal store spec: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ func (p *PP) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretData
|
|||
return map[string][]byte{}, nil
|
||||
}
|
||||
|
||||
func (p *PP) Close() error {
|
||||
func (p *PP) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecret
|
|||
return v.readSecret(ctx, ref.Key, ref.Version)
|
||||
}
|
||||
|
||||
func (v *client) Close() error {
|
||||
func (v *client) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
39
pkg/provider/yandex/lockbox/client/client.go
Normal file
39
pkg/provider/yandex/lockbox/client/client.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
|
||||
"github.com/yandex-cloud/go-sdk/iamkey"
|
||||
)
|
||||
|
||||
// Creates Lockbox clients and Yandex.Cloud IAM tokens.
|
||||
type YandexCloudCreator interface {
|
||||
CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (LockboxClient, error)
|
||||
CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*IamToken, error)
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
type IamToken struct {
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Responsible for accessing Lockbox secrets.
|
||||
type LockboxClient interface {
|
||||
GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
|
||||
}
|
151
pkg/provider/yandex/lockbox/client/fake/fake.go
Normal file
151
pkg/provider/yandex/lockbox/client/fake/fake.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package fake
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
|
||||
"github.com/yandex-cloud/go-sdk/iamkey"
|
||||
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
|
||||
)
|
||||
|
||||
// Fake implementation of YandexCloudCreator.
|
||||
type YandexCloudCreator struct {
|
||||
Backend *LockboxBackend
|
||||
}
|
||||
|
||||
func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
|
||||
return &LockboxClient{c.Backend}, nil
|
||||
}
|
||||
|
||||
func (c *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
|
||||
return c.Backend.getToken(authorizedKey)
|
||||
}
|
||||
|
||||
func (c *YandexCloudCreator) Now() time.Time {
|
||||
return c.Backend.now
|
||||
}
|
||||
|
||||
// Fake implementation of LockboxClient.
|
||||
type LockboxClient struct {
|
||||
fakeLockboxBackend *LockboxBackend
|
||||
}
|
||||
|
||||
func (c *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
|
||||
return c.fakeLockboxBackend.getEntries(iamToken, secretID, versionID)
|
||||
}
|
||||
|
||||
// Fakes Yandex Lockbox service backend.
|
||||
type LockboxBackend struct {
|
||||
secretMap map[secretKey]secretValue // secret specific data
|
||||
versionMap map[versionKey]versionValue // version specific data
|
||||
tokenMap map[tokenKey]tokenValue // token specific data
|
||||
|
||||
tokenExpirationDuration time.Duration
|
||||
now time.Time // fakes the current time
|
||||
}
|
||||
|
||||
type secretKey struct {
|
||||
secretID string
|
||||
}
|
||||
|
||||
type secretValue struct {
|
||||
expectedAuthorizedKey *iamkey.Key // authorized key expected to access the secret
|
||||
}
|
||||
|
||||
type versionKey struct {
|
||||
secretID string
|
||||
versionID string
|
||||
}
|
||||
|
||||
type versionValue struct {
|
||||
entries []*lockbox.Payload_Entry
|
||||
}
|
||||
|
||||
type tokenKey struct {
|
||||
token string
|
||||
}
|
||||
|
||||
type tokenValue struct {
|
||||
authorizedKey *iamkey.Key
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func NewLockboxBackend(tokenExpirationDuration time.Duration) *LockboxBackend {
|
||||
return &LockboxBackend{
|
||||
secretMap: make(map[secretKey]secretValue),
|
||||
versionMap: make(map[versionKey]versionValue),
|
||||
tokenMap: make(map[tokenKey]tokenValue),
|
||||
tokenExpirationDuration: tokenExpirationDuration,
|
||||
now: time.Time{},
|
||||
}
|
||||
}
|
||||
|
||||
func (lb *LockboxBackend) CreateSecret(authorizedKey *iamkey.Key, entries ...*lockbox.Payload_Entry) (string, string) {
|
||||
secretID := uuid.NewString()
|
||||
versionID := uuid.NewString()
|
||||
|
||||
lb.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
|
||||
lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
|
||||
lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
|
||||
|
||||
return secretID, versionID
|
||||
}
|
||||
|
||||
func (lb *LockboxBackend) AddVersion(secretID string, entries ...*lockbox.Payload_Entry) string {
|
||||
versionID := uuid.NewString()
|
||||
|
||||
lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
|
||||
lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
|
||||
|
||||
return versionID
|
||||
}
|
||||
|
||||
func (lb *LockboxBackend) AdvanceClock(duration time.Duration) {
|
||||
lb.now = lb.now.Add(duration)
|
||||
}
|
||||
|
||||
func (lb *LockboxBackend) getToken(authorizedKey *iamkey.Key) (*client.IamToken, error) {
|
||||
token := uuid.NewString()
|
||||
expiresAt := lb.now.Add(lb.tokenExpirationDuration)
|
||||
lb.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
|
||||
return &client.IamToken{Token: token, ExpiresAt: expiresAt}, nil
|
||||
}
|
||||
|
||||
func (lb *LockboxBackend) getEntries(iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
|
||||
if _, ok := lb.secretMap[secretKey{secretID}]; !ok {
|
||||
return nil, fmt.Errorf("secret not found")
|
||||
}
|
||||
if _, ok := lb.versionMap[versionKey{secretID, versionID}]; !ok {
|
||||
return nil, fmt.Errorf("version not found")
|
||||
}
|
||||
if _, ok := lb.tokenMap[tokenKey{iamToken}]; !ok {
|
||||
return nil, fmt.Errorf("unauthenticated")
|
||||
}
|
||||
|
||||
if lb.tokenMap[tokenKey{iamToken}].expiresAt.Before(lb.now) {
|
||||
return nil, fmt.Errorf("iam token expired")
|
||||
}
|
||||
if !cmp.Equal(lb.tokenMap[tokenKey{iamToken}].authorizedKey, lb.secretMap[secretKey{secretID}].expectedAuthorizedKey) {
|
||||
return nil, fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
return lb.versionMap[versionKey{secretID, versionID}].entries, nil
|
||||
}
|
144
pkg/provider/yandex/lockbox/client/grpc/grpc.go
Normal file
144
pkg/provider/yandex/lockbox/client/grpc/grpc.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
|
||||
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
|
||||
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||
"github.com/yandex-cloud/go-sdk/iamkey"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
|
||||
)
|
||||
|
||||
// Implementation of YandexCloudCreator.
|
||||
type YandexCloudCreator struct {
|
||||
}
|
||||
|
||||
func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
|
||||
sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
|
||||
ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = closeSDK(ctx, sdk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(payloadAPIEndpoint.Address,
|
||||
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: time.Second * 30,
|
||||
Timeout: time.Second * 10,
|
||||
PermitWithoutStream: false,
|
||||
}),
|
||||
grpc.WithUserAgent("external-secrets"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LockboxClient{lockbox.NewPayloadServiceClient(conn)}, nil
|
||||
}
|
||||
|
||||
func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
|
||||
sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iamToken, err := sdk.CreateIAMToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = closeSDK(ctx, sdk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
|
||||
}
|
||||
|
||||
func (lb *YandexCloudCreator) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
|
||||
creds, err := ycsdk.ServiceAccountKey(authorizedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sdk, err := ycsdk.Build(ctx, ycsdk.Config{
|
||||
Credentials: creds,
|
||||
Endpoint: apiEndpoint,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sdk, nil
|
||||
}
|
||||
|
||||
func closeSDK(ctx context.Context, sdk *ycsdk.SDK) error {
|
||||
return sdk.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// Implementation of LockboxClient.
|
||||
type LockboxClient struct {
|
||||
lockboxPayloadClient lockbox.PayloadServiceClient
|
||||
}
|
||||
|
||||
func (lc *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
|
||||
payload, err := lc.lockboxPayloadClient.Get(
|
||||
ctx,
|
||||
&lockbox.GetPayloadRequest{
|
||||
SecretId: secretID,
|
||||
VersionId: versionID,
|
||||
},
|
||||
grpc.PerRPCCredentials(perRPCCredentials{iamToken: iamToken}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload.Entries, nil
|
||||
}
|
||||
|
||||
type perRPCCredentials struct {
|
||||
iamToken string
|
||||
}
|
||||
|
||||
func (t perRPCCredentials) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
|
||||
return map[string]string{"Authorization": "Bearer " + t.iamToken}, nil
|
||||
}
|
||||
|
||||
func (perRPCCredentials) RequireTransportSecurity() bool {
|
||||
return true
|
||||
}
|
299
pkg/provider/yandex/lockbox/lockbox.go
Normal file
299
pkg/provider/yandex/lockbox/lockbox.go
Normal file
|
@ -0,0 +1,299 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package lockbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
|
||||
"github.com/yandex-cloud/go-sdk/iamkey"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
kclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/schema"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
|
||||
)
|
||||
|
||||
const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
|
||||
const iamTokenCleanupDelay = 1 * time.Hour // specifies how often cleanUpIamTokenMap() is performed
|
||||
|
||||
var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
|
||||
|
||||
type iamTokenKey struct {
|
||||
authorizedKeyID string
|
||||
serviceAccountID string
|
||||
privateKeyHash string
|
||||
}
|
||||
|
||||
// lockboxProvider is a provider for Yandex Lockbox.
|
||||
type lockboxProvider struct {
|
||||
yandexCloudCreator client.YandexCloudCreator
|
||||
|
||||
lockboxClientMap map[string]client.LockboxClient // apiEndpoint -> LockboxClient
|
||||
lockboxClientMapMutex sync.Mutex
|
||||
iamTokenMap map[iamTokenKey]*client.IamToken
|
||||
iamTokenMapMutex sync.Mutex
|
||||
}
|
||||
|
||||
func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
|
||||
return &lockboxProvider{
|
||||
yandexCloudCreator: yandexCloudCreator,
|
||||
lockboxClientMap: make(map[string]client.LockboxClient),
|
||||
iamTokenMap: make(map[iamTokenKey]*client.IamToken),
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient constructs a Yandex Lockbox Provider.
|
||||
func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
|
||||
storeSpec := store.GetSpec()
|
||||
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
|
||||
return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
|
||||
}
|
||||
storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
|
||||
|
||||
authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
|
||||
if authorizedKeySecretName == "" {
|
||||
return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
|
||||
}
|
||||
objectKey := types.NamespacedName{
|
||||
Name: authorizedKeySecretName,
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
// only ClusterStore is allowed to set namespace (and then it's required)
|
||||
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
|
||||
if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
|
||||
return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
|
||||
}
|
||||
objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
|
||||
}
|
||||
|
||||
authorizedKeySecret := &corev1.Secret{}
|
||||
err := kube.Get(ctx, objectKey, authorizedKeySecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
|
||||
}
|
||||
|
||||
authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
|
||||
if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
|
||||
return nil, fmt.Errorf("missing AuthorizedKey")
|
||||
}
|
||||
|
||||
var authorizedKey iamkey.Key
|
||||
err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
|
||||
}
|
||||
|
||||
lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
|
||||
}
|
||||
|
||||
iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IAM token: %w", err)
|
||||
}
|
||||
|
||||
return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
|
||||
}
|
||||
|
||||
func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
|
||||
p.lockboxClientMapMutex.Lock()
|
||||
defer p.lockboxClientMapMutex.Unlock()
|
||||
|
||||
if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
|
||||
log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
|
||||
|
||||
lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.lockboxClientMap[apiEndpoint] = lockboxClient
|
||||
}
|
||||
return p.lockboxClientMap[apiEndpoint], nil
|
||||
}
|
||||
|
||||
func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
|
||||
p.iamTokenMapMutex.Lock()
|
||||
defer p.iamTokenMapMutex.Unlock()
|
||||
|
||||
iamTokenKey := buildIamTokenKey(authorizedKey)
|
||||
if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
|
||||
log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
|
||||
|
||||
iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
|
||||
|
||||
p.iamTokenMap[iamTokenKey] = iamToken
|
||||
}
|
||||
return p.iamTokenMap[iamTokenKey], nil
|
||||
}
|
||||
|
||||
func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
|
||||
now := p.yandexCloudCreator.Now()
|
||||
return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
|
||||
}
|
||||
|
||||
func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
|
||||
privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
|
||||
return iamTokenKey{
|
||||
authorizedKey.GetId(),
|
||||
authorizedKey.GetServiceAccountId(),
|
||||
hex.EncodeToString(privateKeyHash[:]),
|
||||
}
|
||||
}
|
||||
|
||||
// Used for testing.
|
||||
func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
|
||||
p.iamTokenMapMutex.Lock()
|
||||
defer p.iamTokenMapMutex.Unlock()
|
||||
|
||||
_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *lockboxProvider) cleanUpIamTokenMap() {
|
||||
p.iamTokenMapMutex.Lock()
|
||||
defer p.iamTokenMapMutex.Unlock()
|
||||
|
||||
for key, value := range p.iamTokenMap {
|
||||
if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
|
||||
log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
|
||||
delete(p.iamTokenMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lockboxSecretsClient is a secrets client for Yandex Lockbox.
|
||||
type lockboxSecretsClient struct {
|
||||
lockboxClient client.LockboxClient
|
||||
iamToken string
|
||||
}
|
||||
|
||||
// GetSecret returns a single secret from the provider.
|
||||
func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
|
||||
}
|
||||
|
||||
if ref.Property == "" {
|
||||
keyToValue := make(map[string]interface{}, len(entries))
|
||||
for _, entry := range entries {
|
||||
value, err := getValueAsIs(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyToValue[entry.Key] = value
|
||||
}
|
||||
out, err := json.Marshal(keyToValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal secret: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
entry, err := findEntryByKey(entries, ref.Property)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getValueAsBinary(entry)
|
||||
}
|
||||
|
||||
// GetSecretMap returns multiple k/v pairs from the provider.
|
||||
func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||
entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
|
||||
}
|
||||
|
||||
secretMap := make(map[string][]byte, len(entries))
|
||||
for _, entry := range entries {
|
||||
value, err := getValueAsBinary(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secretMap[entry.Key] = value
|
||||
}
|
||||
return secretMap, nil
|
||||
}
|
||||
|
||||
func (c *lockboxSecretsClient) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
|
||||
switch entry.Value.(type) {
|
||||
case *lockbox.Payload_Entry_TextValue:
|
||||
return entry.GetTextValue(), nil
|
||||
case *lockbox.Payload_Entry_BinaryValue:
|
||||
return entry.GetBinaryValue(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
|
||||
switch entry.Value.(type) {
|
||||
case *lockbox.Payload_Entry_TextValue:
|
||||
return []byte(entry.GetTextValue()), nil
|
||||
case *lockbox.Payload_Entry_BinaryValue:
|
||||
return entry.GetBinaryValue(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
|
||||
for i := range entries {
|
||||
if entries[i].Key == key {
|
||||
return entries[i], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("payload entry with key '%s' not found", key)
|
||||
}
|
||||
|
||||
func init() {
|
||||
lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(iamTokenCleanupDelay)
|
||||
lockboxProvider.cleanUpIamTokenMap()
|
||||
}
|
||||
}()
|
||||
|
||||
schema.Register(
|
||||
lockboxProvider,
|
||||
&esv1alpha1.SecretStoreProvider{
|
||||
YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
|
||||
},
|
||||
)
|
||||
}
|
677
pkg/provider/yandex/lockbox/lockbox_test.go
Normal file
677
pkg/provider/yandex/lockbox/lockbox_test.go
Normal file
|
@ -0,0 +1,677 @@
|
|||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package lockbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
tassert "github.com/stretchr/testify/assert"
|
||||
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
|
||||
"github.com/yandex-cloud/go-sdk/iamkey"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
||||
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/schema"
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const namespace = "namespace"
|
||||
|
||||
store := &esv1alpha1.SecretStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: esv1alpha1.SecretStoreSpec{
|
||||
Provider: &esv1alpha1.SecretStoreProvider{
|
||||
YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
|
||||
},
|
||||
},
|
||||
}
|
||||
provider, err := schema.GetProvider(store)
|
||||
tassert.Nil(t, err)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
|
||||
tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
|
||||
tassert.Nil(t, secretClient)
|
||||
|
||||
store.Spec.Provider.YandexLockbox.Auth = esv1alpha1.YandexLockboxAuth{}
|
||||
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
|
||||
tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
|
||||
tassert.Nil(t, secretClient)
|
||||
|
||||
store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey = esmeta.SecretKeySelector{}
|
||||
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
|
||||
tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
|
||||
tassert.Nil(t, secretClient)
|
||||
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Name = authorizedKeySecretName
|
||||
store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Key = authorizedKeySecretKey
|
||||
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
|
||||
tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
|
||||
tassert.Nil(t, secretClient)
|
||||
|
||||
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
|
||||
tassert.Nil(t, err)
|
||||
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
|
||||
tassert.EqualError(t, err, "failed to create Yandex Lockbox client: private key parsing failed: Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
|
||||
tassert.Nil(t, secretClient)
|
||||
}
|
||||
|
||||
func TestGetSecretForAllEntries(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
k1, v1 := "k1", "v1"
|
||||
k2, v2 := "k2", []byte("v2")
|
||||
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry(k1, v1),
|
||||
binaryEntry(k2, v2),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.Equal(
|
||||
t,
|
||||
map[string]string{
|
||||
k1: v1,
|
||||
k2: base64(v2),
|
||||
},
|
||||
unmarshalStringMap(t, data),
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetSecretForTextEntry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
k1, v1 := "k1", "v1"
|
||||
k2, v2 := "k2", []byte("v2")
|
||||
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry(k1, v1),
|
||||
binaryEntry(k2, v2),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.Equal(t, v1, string(data))
|
||||
}
|
||||
|
||||
func TestGetSecretForBinaryEntry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
k1, v1 := "k1", "v1"
|
||||
k2, v2 := "k2", []byte("v2")
|
||||
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry(k1, v1),
|
||||
binaryEntry(k2, v2),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.Equal(t, v2, data)
|
||||
}
|
||||
|
||||
func TestGetSecretByVersionID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
oldKey, oldVal := "oldKey", "oldVal"
|
||||
secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry(oldKey, oldVal),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
|
||||
|
||||
newKey, newVal := "newKey", "newVal"
|
||||
newVersionID := lockboxBackend.AddVersion(secretID,
|
||||
textEntry(newKey, newVal),
|
||||
)
|
||||
|
||||
data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
|
||||
tassert.Nil(t, err)
|
||||
tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
|
||||
|
||||
data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
|
||||
tassert.Nil(t, err)
|
||||
tassert.Equal(t, map[string]string{newKey: newVal}, unmarshalStringMap(t, data))
|
||||
}
|
||||
|
||||
func TestGetSecretUnauthorized(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKeyA := newFakeAuthorizedKey()
|
||||
authorizedKeyB := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
|
||||
textEntry("k1", "v1"),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
|
||||
}
|
||||
|
||||
func TestGetSecretNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
|
||||
|
||||
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry("k1", "v1"),
|
||||
)
|
||||
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: version not found")
|
||||
}
|
||||
|
||||
func TestGetSecretWithTwoNamespaces(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace1 := uuid.NewString()
|
||||
namespace2 := uuid.NewString()
|
||||
authorizedKey1 := newFakeAuthorizedKey()
|
||||
authorizedKey2 := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
k1, v1 := "k1", "v1"
|
||||
secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
|
||||
textEntry(k1, v1),
|
||||
)
|
||||
k2, v2 := "k2", "v2"
|
||||
secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
|
||||
textEntry(k2, v2),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
|
||||
tassert.Nil(t, err)
|
||||
err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
|
||||
tassert.Nil(t, err)
|
||||
store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
|
||||
tassert.Nil(t, err)
|
||||
secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
|
||||
tassert.Nil(t, err)
|
||||
|
||||
data, err := secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
|
||||
tassert.Equal(t, v1, string(data))
|
||||
tassert.Nil(t, err)
|
||||
data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
|
||||
tassert.Nil(t, data)
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
|
||||
|
||||
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
|
||||
tassert.Nil(t, data)
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
|
||||
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
|
||||
tassert.Equal(t, v2, string(data))
|
||||
tassert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
apiEndpoint1 := uuid.NewString()
|
||||
apiEndpoint2 := uuid.NewString()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey1 := newFakeAuthorizedKey()
|
||||
authorizedKey2 := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
|
||||
k1, v1 := "k1", "v1"
|
||||
secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
|
||||
textEntry(k1, v1),
|
||||
)
|
||||
lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
|
||||
k2, v2 := "k2", "v2"
|
||||
secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
|
||||
textEntry(k2, v2),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName1 = "authorizedKeySecretName1"
|
||||
const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
|
||||
tassert.Nil(t, err)
|
||||
const authorizedKeySecretName2 = "authorizedKeySecretName2"
|
||||
const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
|
||||
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
|
||||
tassert.Nil(t, err)
|
||||
|
||||
store1 := newYandexLockboxSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
|
||||
store2 := newYandexLockboxSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
|
||||
|
||||
provider1 := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend1,
|
||||
})
|
||||
provider2 := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend2,
|
||||
})
|
||||
|
||||
secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
secretsClient2, err := provider2.NewClient(ctx, store2, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
|
||||
var data []byte
|
||||
|
||||
data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
|
||||
tassert.Equal(t, v1, string(data))
|
||||
tassert.Nil(t, err)
|
||||
data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
|
||||
tassert.Nil(t, data)
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
|
||||
|
||||
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
|
||||
tassert.Nil(t, data)
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
|
||||
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
|
||||
tassert.Equal(t, v2, string(data))
|
||||
tassert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGetSecretWithIamTokenExpiration(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
tokenExpirationTime := time.Hour
|
||||
lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
|
||||
k1, v1 := "k1", "v1"
|
||||
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry(k1, v1),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
|
||||
var data []byte
|
||||
|
||||
oldSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
|
||||
tassert.Equal(t, v1, string(data))
|
||||
tassert.Nil(t, err)
|
||||
|
||||
lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
|
||||
|
||||
data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
|
||||
tassert.Nil(t, data)
|
||||
tassert.EqualError(t, err, "unable to request secret payload to get secret: iam token expired")
|
||||
|
||||
newSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err = newSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
|
||||
tassert.Equal(t, v1, string(data))
|
||||
tassert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGetSecretWithIamTokenCleanup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey1 := newFakeAuthorizedKey()
|
||||
authorizedKey2 := newFakeAuthorizedKey()
|
||||
|
||||
tokenExpirationDuration := time.Hour
|
||||
lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
|
||||
secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
|
||||
textEntry("k1", "v1"),
|
||||
)
|
||||
secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
|
||||
textEntry("k2", "v2"),
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName1 = "authorizedKeySecretName1"
|
||||
const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
|
||||
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
|
||||
tassert.Nil(t, err)
|
||||
const authorizedKeySecretName2 = "authorizedKeySecretName2"
|
||||
const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
|
||||
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
|
||||
tassert.Nil(t, err)
|
||||
|
||||
store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
|
||||
store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
|
||||
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
|
||||
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
|
||||
|
||||
// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
|
||||
secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
|
||||
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
|
||||
|
||||
lockboxBackend.AdvanceClock(tokenExpirationDuration * 2)
|
||||
|
||||
// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
|
||||
secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
|
||||
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
|
||||
|
||||
lockboxBackend.AdvanceClock(tokenExpirationDuration)
|
||||
|
||||
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
|
||||
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
|
||||
|
||||
provider.cleanUpIamTokenMap()
|
||||
|
||||
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
|
||||
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
|
||||
|
||||
lockboxBackend.AdvanceClock(tokenExpirationDuration)
|
||||
|
||||
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
|
||||
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
|
||||
|
||||
provider.cleanUpIamTokenMap()
|
||||
|
||||
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
|
||||
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
|
||||
}
|
||||
|
||||
func TestGetSecretMap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
k1, v1 := "k1", "v1"
|
||||
k2, v2 := "k2", []byte("v2")
|
||||
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry(k1, v1),
|
||||
binaryEntry(k2, v2),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.Equal(
|
||||
t,
|
||||
map[string][]byte{
|
||||
k1: []byte(v1),
|
||||
k2: v2,
|
||||
},
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetSecretMapByVersionID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
namespace := uuid.NewString()
|
||||
authorizedKey := newFakeAuthorizedKey()
|
||||
|
||||
lockboxBackend := fake.NewLockboxBackend(time.Hour)
|
||||
oldKey, oldVal := "oldKey", "oldVal"
|
||||
secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
|
||||
textEntry(oldKey, oldVal),
|
||||
)
|
||||
|
||||
k8sClient := clientfake.NewClientBuilder().Build()
|
||||
const authorizedKeySecretName = "authorizedKeySecretName"
|
||||
const authorizedKeySecretKey = "authorizedKeySecretKey"
|
||||
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
|
||||
tassert.Nil(t, err)
|
||||
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
|
||||
|
||||
provider := newLockboxProvider(&fake.YandexCloudCreator{
|
||||
Backend: lockboxBackend,
|
||||
})
|
||||
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
|
||||
tassert.Nil(t, err)
|
||||
data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
|
||||
tassert.Nil(t, err)
|
||||
|
||||
tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
|
||||
|
||||
newKey, newVal := "newKey", "newVal"
|
||||
newVersionID := lockboxBackend.AddVersion(secretID,
|
||||
textEntry(newKey, newVal),
|
||||
)
|
||||
|
||||
data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
|
||||
tassert.Nil(t, err)
|
||||
tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
|
||||
|
||||
data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
|
||||
tassert.Nil(t, err)
|
||||
tassert.Equal(t, map[string][]byte{newKey: []byte(newVal)}, data)
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1alpha1.GenericStore {
|
||||
return &esv1alpha1.SecretStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: esv1alpha1.SecretStoreSpec{
|
||||
Provider: &esv1alpha1.SecretStoreProvider{
|
||||
YandexLockbox: &esv1alpha1.YandexLockboxProvider{
|
||||
APIEndpoint: apiEndpoint,
|
||||
Auth: esv1alpha1.YandexLockboxAuth{
|
||||
AuthorizedKey: esmeta.SecretKeySelector{
|
||||
Name: authorizedKeySecretName,
|
||||
Key: authorizedKeySecretKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createK8sSecret(ctx context.Context, k8sClient client.Client, namespace, secretName, secretKey string, secretContent interface{}) error {
|
||||
data, err := json.Marshal(secretContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = k8sClient.Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: secretName,
|
||||
},
|
||||
Data: map[string][]byte{secretKey: data},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFakeAuthorizedKey() *iamkey.Key {
|
||||
uniqueLabel := uuid.NewString()
|
||||
return &iamkey.Key{
|
||||
Id: uniqueLabel,
|
||||
Subject: &iamkey.Key_ServiceAccountId{
|
||||
ServiceAccountId: uniqueLabel,
|
||||
},
|
||||
PrivateKey: uniqueLabel,
|
||||
}
|
||||
}
|
||||
|
||||
func textEntry(key, value string) *lockbox.Payload_Entry {
|
||||
return &lockbox.Payload_Entry{
|
||||
Key: key,
|
||||
Value: &lockbox.Payload_Entry_TextValue{
|
||||
TextValue: value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func binaryEntry(key string, value []byte) *lockbox.Payload_Entry {
|
||||
return &lockbox.Payload_Entry{
|
||||
Key: key,
|
||||
Value: &lockbox.Payload_Entry_BinaryValue{
|
||||
BinaryValue: value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalStringMap(t *testing.T, data []byte) map[string]string {
|
||||
stringMap := make(map[string]string)
|
||||
err := json.Unmarshal(data, &stringMap)
|
||||
tassert.Nil(t, err)
|
||||
return stringMap
|
||||
}
|
||||
|
||||
func base64(data []byte) string {
|
||||
return b64.StdEncoding.EncodeToString(data)
|
||||
}
|
Loading…
Reference in a new issue