1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-15 17:51:01 +00:00

Merge branch 'main' of https://github.com/external-secrets/external-secrets into external-secrets-main

This commit is contained in:
KianTigger 2021-09-02 15:23:54 +01:00
commit 9d6f7ac46f
71 changed files with 3872 additions and 129 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View 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"`
}

View file

@ -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"`

View 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"`
}

View file

@ -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

View 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 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"`
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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. |

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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: ""

View file

@ -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

View file

@ -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

View file

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -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
{

View 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
```

View file

@ -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
```

View 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
```

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: gitlab-secret
labels:
type: gitlab
type: Opaque
stringData:
token: "**access token goes here**"

View 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

View 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

View 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**"

View file

@ -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:

View file

@ -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"

View file

@ -543,7 +543,9 @@ ExternalSecretStatus
<th>Description</th>
</tr>
</thead>
<tbody><tr><td><p>&#34;Ready&#34;</p></td>
<tbody><tr><td><p>&#34;Deleted&#34;</p></td>
<td></td>
</tr><tr><td><p>&#34;Ready&#34;</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. &lsquo;api.cloud.yandex.net:443&rsquo;)</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

Binary file not shown.

View file

@ -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) {})

View file

@ -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"},
},
}
}

View 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"

View file

@ -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

View 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)),
)
})

View 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())
}

View 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)),
)
})

View 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
View file

@ -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
View file

@ -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=

View file

@ -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:

View file

@ -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")

View file

@ -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
}

View 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
View 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{},
})
}

View 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)
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View 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
}
}
}

View 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
}

View 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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View 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)
}

View 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
}

View 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
}

View 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{},
},
)
}

View 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)
}