From f1fb6cfa068fa536645fc2de0866664152b65b22 Mon Sep 17 00:00:00 2001 From: Kellin McAvoy Date: Mon, 30 Nov 2020 21:56:51 +0100 Subject: [PATCH] feat: implement provider interface adds the provider interface, generic store and schema registration. mostly taken from itscontained/secret-manager Co-authored-by: Moritz Johner --- api/v1alpha1/generic_store.go | 52 ++++ api/v1alpha1/groupversion_info.go | 8 + api/v1alpha1/secretstore_types.go | 8 + api/v1alpha1/zz_generated.deepcopy.go | 30 +- .../external-secrets.io_externalsecrets.yaml | 288 +++++++++--------- .../external-secrets.io_secretstores.yaml | 218 ++++++------- go.mod | 1 + go.sum | 2 + .../aws/secretsmanager/secretsmanager.go | 49 +++ .../aws/secretsmanager/secretsmanager_test.go | 1 + pkg/provider/fake/fake.go | 100 ++++++ pkg/provider/provider.go | 35 +++ pkg/provider/register/register.go | 21 ++ pkg/provider/schema/schema.go | 114 +++++++ pkg/provider/schema/schema_test.go | 53 ++++ pkg/providers/awssm/awssm.go | 1 - pkg/providers/awssm/awssm_test.go | 1 - 17 files changed, 725 insertions(+), 257 deletions(-) create mode 100644 api/v1alpha1/generic_store.go create mode 100644 pkg/provider/aws/secretsmanager/secretsmanager.go create mode 100644 pkg/provider/aws/secretsmanager/secretsmanager_test.go create mode 100644 pkg/provider/fake/fake.go create mode 100644 pkg/provider/provider.go create mode 100644 pkg/provider/register/register.go create mode 100644 pkg/provider/schema/schema.go create mode 100644 pkg/provider/schema/schema_test.go delete mode 100644 pkg/providers/awssm/awssm.go delete mode 100644 pkg/providers/awssm/awssm_test.go diff --git a/api/v1alpha1/generic_store.go b/api/v1alpha1/generic_store.go new file mode 100644 index 000000000..b2d0a8be6 --- /dev/null +++ b/api/v1alpha1/generic_store.go @@ -0,0 +1,52 @@ +/* +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +kubebuilder:object:root=false +// +kubebuilder:object:generate:false +// +k8s:deepcopy-gen:interfaces=nil +// +k8s:deepcopy-gen=nil + +// GenericStore is a common interface for interacting with ClusterSecretStore +// or a namespaced SecretStore +type GenericStore interface { + runtime.Object + metav1.Object + GetProvider() *SecretStoreProvider +} + +// +kubebuilder:object:root:false +// +kubebuilder:object:generate:false +var _ GenericStore = &SecretStore{} + +// GetProvider returns the underlying provider +func (c *SecretStore) GetProvider() *SecretStoreProvider { + return c.Spec.Provider +} + +// SetProvider sets the underlying provider +func (c *SecretStore) SetProvider(provider SecretStoreProvider) { + c.Spec.Provider = &provider +} + +// Copy returns a DeepCopy of the Store +func (c *SecretStore) Copy() GenericStore { + return c.DeepCopy() +} diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index a2b7095af..e5ef795f0 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -18,6 +18,8 @@ limitations under the License. package v1alpha1 import ( + "reflect" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) @@ -32,3 +34,9 @@ var ( // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) + +// SecretStore type metadata. +var ( + SecretStoreKind = reflect.TypeOf(SecretStore{}).Name() + SecretStoreKindAPIVersion = SecretStoreKind + "." + GroupVersion.String() +) diff --git a/api/v1alpha1/secretstore_types.go b/api/v1alpha1/secretstore_types.go index df8b0d1a9..5204c0957 100644 --- a/api/v1alpha1/secretstore_types.go +++ b/api/v1alpha1/secretstore_types.go @@ -34,6 +34,14 @@ type SecretStoreSpec struct { // +optional Controller string `json:"controller"` + // Used to configure the provider. Only one provider may be set + Provider *SecretStoreProvider `json:"provider"` +} + +// SecretStoreProvider contains the provider-specific configration +// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:validation:MaxProperties=1 +type SecretStoreProvider struct { // AWSSM configures this store to sync secrets using AWS Secret Manager provider // +optional AWSSM *AWSSMProvider `json:"awssm,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a4b3e4a83..62a63a2de 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,8 +1,6 @@ // +build !ignore_autogenerated /* - - 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 @@ -21,7 +19,7 @@ limitations under the License. package v1alpha1 import ( - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -368,6 +366,26 @@ func (in *SecretStoreList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) { + *out = *in + if in.AWSSM != nil { + in, out := &in.AWSSM, &out.AWSSM + *out = new(AWSSMProvider) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider. +func (in *SecretStoreProvider) DeepCopy() *SecretStoreProvider { + if in == nil { + return nil + } + out := new(SecretStoreProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretStoreRef) DeepCopyInto(out *SecretStoreRef) { *out = *in @@ -386,9 +404,9 @@ func (in *SecretStoreRef) DeepCopy() *SecretStoreRef { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) { *out = *in - if in.AWSSM != nil { - in, out := &in.AWSSM, &out.AWSSM - *out = new(AWSSMProvider) + if in.Provider != nil { + in, out := &in.Provider, &out.Provider + *out = new(SecretStoreProvider) (*in).DeepCopyInto(*out) } } diff --git a/config/crd/bases/external-secrets.io_externalsecrets.yaml b/config/crd/bases/external-secrets.io_externalsecrets.yaml index 301b5bf43..420437b7e 100644 --- a/config/crd/bases/external-secrets.io_externalsecrets.yaml +++ b/config/crd/bases/external-secrets.io_externalsecrets.yaml @@ -1,10 +1,10 @@ --- -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.2.5 + controller-gen.kubebuilder.io/version: v0.4.1 creationTimestamp: null name: externalsecrets.external-secrets.io spec: @@ -15,150 +15,150 @@ spec: plural: externalsecrets singular: externalsecret scope: Namespaced - validation: - openAPIV3Schema: - description: ExternalSecret is the Schema for the externalsecrets API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: ExternalSecretSpec defines the desired state of ExternalSecret - properties: - data: - description: Data defines the connection between the Kubernetes Secret - keys and the Provider data - items: - description: ExternalSecretData defines the connection between the - Kubernetes Secret key (spec.data.) and the Provider data - properties: - remoteRef: - description: ExternalSecretDataRemoteRef defines Provider data - location - properties: - key: - description: Key is the key used in the Provider, mandatory - type: string - property: - description: Used to select a specific property of the Provider - value (if a map), if supported - type: string - version: - description: Used to select a specific version of the Provider - value, if supported - type: string - required: - - key - type: object - secretKey: - type: string - required: - - remoteRef - - secretKey - type: object - type: array - dataFrom: - description: DataFrom is used to fetch all properties from a specific - Provider data If multiple entries are specified, the Secret keys are - merged in the specified order - items: - description: ExternalSecretDataRemoteRef defines Provider data location - properties: - key: - description: Key is the key used in the Provider, mandatory - type: string - property: - description: Used to select a specific property of the Provider - value (if a map), if supported - type: string - version: - description: Used to select a specific version of the Provider - value, if supported - type: string - required: - - key - type: object - type: array - refreshInterval: - description: 'RefreshInterval is the amount of time before the values - reading again from the SecretStore provider Valid time units are "ns", - "us" (or "µs"), "ms", "s", "m", "h" (from time.ParseDuration) May - be set to zero to fetch and create it once TODO: Default to some value?' - type: string - secretStoreRef: - description: SecretStoreRef defines which SecretStore to fetch the ExternalSecret - data - properties: - kind: - description: Kind of the SecretStore resource (SecretStore or ClusterSecretStore) - Defaults to `SecretStore` - type: string - name: - description: Name of the SecretStore resource - type: string - required: - - name - type: object - target: - description: ExternalSecretTarget defines the Kubernetes Secret to be - created There can be only one target per ExternalSecret - properties: - creationPolicy: - description: CreationPolicy defines rules on how to create the resulting - Secret Defaults to 'Owner' - type: string - name: - description: Name defines the name of the Secret resource to be - managed This field is immutable Defaults to the .metadata.name - of the ExternalSecret resource - type: string - type: object - required: - - secretStoreRef - - target - type: object - status: - properties: - conditions: - items: - properties: - lastSyncTime: - format: date-time - type: string - lastTransitionTime: - format: date-time - type: string - message: - type: string - reason: - type: string - status: - type: string - type: - type: string - required: - - status - - type - type: object - type: array - phase: - description: ExternalSecretStatusPhase represents the current phase - of the Secret sync - type: string - type: object - type: object - version: v1alpha1 versions: - name: v1alpha1 + schema: + openAPIV3Schema: + description: ExternalSecret is the Schema for the externalsecrets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ExternalSecretSpec defines the desired state of ExternalSecret + properties: + data: + description: Data defines the connection between the Kubernetes Secret + keys and the Provider data + items: + description: ExternalSecretData defines the connection between the + Kubernetes Secret key (spec.data.) and the Provider data + properties: + remoteRef: + description: ExternalSecretDataRemoteRef defines Provider data + location + properties: + key: + description: Key is the key used in the Provider, mandatory + type: string + property: + description: Used to select a specific property of the Provider + value (if a map), if supported + type: string + version: + description: Used to select a specific version of the Provider + value, if supported + type: string + required: + - key + type: object + secretKey: + type: string + required: + - remoteRef + - secretKey + type: object + type: array + dataFrom: + description: DataFrom is used to fetch all properties from a specific + Provider data If multiple entries are specified, the Secret keys + are merged in the specified order + items: + description: ExternalSecretDataRemoteRef defines Provider data location + properties: + key: + description: Key is the key used in the Provider, mandatory + type: string + property: + description: Used to select a specific property of the Provider + value (if a map), if supported + type: string + version: + description: Used to select a specific version of the Provider + value, if supported + type: string + required: + - key + type: object + type: array + refreshInterval: + description: 'RefreshInterval is the amount of time before the values + reading again from the SecretStore provider Valid time units are + "ns", "us" (or "µs"), "ms", "s", "m", "h" (from time.ParseDuration) + May be set to zero to fetch and create it once TODO: Default to + some value?' + type: string + secretStoreRef: + description: SecretStoreRef defines which SecretStore to fetch the + ExternalSecret data + properties: + kind: + description: Kind of the SecretStore resource (SecretStore or + ClusterSecretStore) Defaults to `SecretStore` + type: string + name: + description: Name of the SecretStore resource + type: string + required: + - name + type: object + target: + description: ExternalSecretTarget defines the Kubernetes Secret to + be created There can be only one target per ExternalSecret + properties: + creationPolicy: + description: CreationPolicy defines rules on how to create the + resulting Secret Defaults to 'Owner' + type: string + name: + description: Name defines the name of the Secret resource to be + managed This field is immutable Defaults to the .metadata.name + of the ExternalSecret resource + type: string + type: object + required: + - secretStoreRef + - target + type: object + status: + properties: + conditions: + items: + properties: + lastSyncTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + phase: + description: ExternalSecretStatusPhase represents the current phase + of the Secret sync + type: string + type: object + type: object served: true storage: true status: diff --git a/config/crd/bases/external-secrets.io_secretstores.yaml b/config/crd/bases/external-secrets.io_secretstores.yaml index 6186b1d25..f8aba4ae4 100644 --- a/config/crd/bases/external-secrets.io_secretstores.yaml +++ b/config/crd/bases/external-secrets.io_secretstores.yaml @@ -1,10 +1,10 @@ --- -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.2.5 + controller-gen.kubebuilder.io/version: v0.4.1 creationTimestamp: null name: secretstores.external-secrets.io spec: @@ -15,111 +15,119 @@ spec: plural: secretstores singular: secretstore scope: Namespaced - validation: - openAPIV3Schema: - description: SecretStore is the Schema for the secretstores API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: SecretStoreSpec defines the desired state of SecretStore - properties: - awssm: - description: AWSSM configures this store to sync secrets using AWS Secret - Manager provider - properties: - auth: - description: Auth defines the information necessary to authenticate - against AWS - properties: - secretRef: - properties: - accessKeyIDSecretRef: - description: The AccessKeyID is used for authentication - properties: - key: - type: string - name: - type: string - namespace: - type: string - required: - - key - - name - type: object - secretAccessKeySecretRef: - description: The SecretAccessKey is used for authentication - properties: - key: - type: string - name: - type: string - namespace: - type: string - required: - - key - - name - type: object - type: object - required: - - secretRef - type: object - region: - description: AWS Region to be used for the provider - type: string - role: - description: Role is a Role ARN which the SecretManager provider - will assume - type: string - required: - - auth - - region - type: object - controller: - description: 'Used to select the correct KES controller (think: ingress.ingressClassName) - The KES controller is instantiated with a specific controller name - and filters ES based on this property' - type: string - type: object - status: - description: SecretStoreStatus defines the observed state of the SecretStore - properties: - conditions: - items: - properties: - lastTransitionTime: - format: date-time - type: string - message: - type: string - reason: - type: string - status: - type: string - type: - type: string - required: - - status - - type - type: object - type: array - phase: - type: string - type: object - type: object - version: v1alpha1 versions: - name: v1alpha1 + schema: + openAPIV3Schema: + description: SecretStore is the Schema for the secretstores API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SecretStoreSpec defines the desired state of SecretStore + properties: + controller: + description: 'Used to select the correct KES controller (think: ingress.ingressClassName) + The KES controller is instantiated with a specific controller name + and filters ES based on this property' + type: string + provider: + description: Used to configure the provider. Only one provider may + be set + maxProperties: 1 + minProperties: 1 + properties: + awssm: + description: AWSSM configures this store to sync secrets using + AWS Secret Manager provider + properties: + auth: + description: Auth defines the information necessary to authenticate + against AWS + properties: + secretRef: + properties: + accessKeyIDSecretRef: + description: The AccessKeyID is used for authentication + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + secretAccessKeySecretRef: + description: The SecretAccessKey is used for authentication + properties: + key: + type: string + name: + type: string + namespace: + type: string + required: + - key + - name + type: object + type: object + required: + - secretRef + type: object + region: + description: AWS Region to be used for the provider + type: string + role: + description: Role is a Role ARN which the SecretManager provider + will assume + type: string + required: + - auth + - region + type: object + type: object + required: + - provider + type: object + status: + description: SecretStoreStatus defines the observed state of the SecretStore + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + phase: + type: string + type: object + type: object served: true storage: true status: diff --git a/go.mod b/go.mod index 62fd67dd5..9ddf8c20b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-logr/logr v0.1.0 github.com/onsi/ginkgo v1.11.0 github.com/onsi/gomega v1.8.1 + github.com/stretchr/testify v1.4.0 k8s.io/api v0.17.2 k8s.io/apimachinery v0.17.2 k8s.io/client-go v0.17.2 diff --git a/go.sum b/go.sum index 23fff210a..0a79b4765 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,7 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -277,6 +278,7 @@ github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv 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= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= diff --git a/pkg/provider/aws/secretsmanager/secretsmanager.go b/pkg/provider/aws/secretsmanager/secretsmanager.go new file mode 100644 index 000000000..2fa119930 --- /dev/null +++ b/pkg/provider/aws/secretsmanager/secretsmanager.go @@ -0,0 +1,49 @@ +/* +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 secretsmanager + +import ( + "context" + + esv1alpha1 "github.com/external-secrets/external-secrets/api/v1alpha1" + "github.com/external-secrets/external-secrets/pkg/provider" + "github.com/external-secrets/external-secrets/pkg/provider/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// SecretsManager is a provider for AWS SecretsManager +type SecretsManager struct{} + +// New constructs a SecretsManager Provider +func (sm *SecretsManager) New(ctx context.Context, store esv1alpha1.SecretStoreProvider, kube client.Client, namespace string) (provider.Provider, error) { + return sm, nil // stub +} + +// GetSecret returns a single secret from the provider +func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + return []byte("NOOP"), nil +} + +// GetSecretMap returns multiple k/v pairs from the provider +func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + return map[string][]byte{ + "noop": []byte("NOOP"), + }, nil +} + +func init() { + schema.Register(&SecretsManager{}, &esv1alpha1.SecretStoreProvider{ + AWSSM: &esv1alpha1.AWSSMProvider{}, + }) +} diff --git a/pkg/provider/aws/secretsmanager/secretsmanager_test.go b/pkg/provider/aws/secretsmanager/secretsmanager_test.go new file mode 100644 index 000000000..c032eb81a --- /dev/null +++ b/pkg/provider/aws/secretsmanager/secretsmanager_test.go @@ -0,0 +1 @@ +package secretsmanager diff --git a/pkg/provider/fake/fake.go b/pkg/provider/fake/fake.go new file mode 100644 index 000000000..46023aed9 --- /dev/null +++ b/pkg/provider/fake/fake.go @@ -0,0 +1,100 @@ +/* +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" + + esv1alpha1 "github.com/external-secrets/external-secrets/api/v1alpha1" + "github.com/external-secrets/external-secrets/pkg/provider" + "github.com/external-secrets/external-secrets/pkg/provider/schema" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ provider.Provider = &Client{} + +// Client is a fake client for testing +type Client struct { + NewFn func(context.Context, esv1alpha1.SecretStoreProvider, client.Client, + string) (provider.Provider, error) + GetSecretFn func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) + GetSecretMapFn func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) +} + +// New returns a fake client +func New() *Client { + v := &Client{ + GetSecretFn: func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + return nil, nil + }, + GetSecretMapFn: func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + return nil, nil + }, + } + + v.NewFn = func(context.Context, esv1alpha1.SecretStoreProvider, client.Client, string) (provider.Provider, error) { + return nil, nil + } + + return v +} + +// RegisterAs registers the fake client in the schema +func (v *Client) RegisterAs(provider *esv1alpha1.SecretStoreProvider) { + schema.ForceRegister(v, provider) +} + +// GetSecret implements the provider.Provider interface +func (v *Client) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + return v.GetSecretFn(ctx, ref) +} + +// WithGetSecret wraps secret data returned by this provider +func (v *Client) WithGetSecret(secData []byte, err error) *Client { + v.GetSecretFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + return secData, err + } + return v +} + +// GetSecretMap imeplements the provider.Provider interface +func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + return v.GetSecretMapFn(ctx, ref) +} + +// WithGetSecretMap wraps the secret data map returned by this fake provider +func (v *Client) WithGetSecretMap(secData map[string][]byte, err error) *Client { + v.GetSecretMapFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + return secData, err + } + return v +} + +// WithNew wraps the fake provider factory function +func (v *Client) WithNew(f func(context.Context, esv1alpha1.SecretStoreProvider, client.Client, + string) (provider.Provider, error)) *Client { + v.NewFn = f + return v +} + +// New returns a new fake provider +func (v *Client) New(ctx context.Context, store esv1alpha1.SecretStoreProvider, kube client.Client, namespace string) (provider.Provider, error) { + client, err := v.NewFn(ctx, store, kube, namespace) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go new file mode 100644 index 000000000..aedff83d5 --- /dev/null +++ b/pkg/provider/provider.go @@ -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 provider + +import ( + "context" + + esv1alpha1 "github.com/external-secrets/external-secrets/api/v1alpha1" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Provider is a common interface for interacting with secret backends +type Provider interface { + // New constructs a SecretsManager Provider + New(ctx context.Context, store esv1alpha1.SecretStoreProvider, kube client.Client, namespace string) (Provider, error) + + // GetSecret returns a single secret from the provider + GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) + + // GetSecretMap returns multiple k/v pairs from the provider + GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) +} diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go new file mode 100644 index 000000000..df3e0f06e --- /dev/null +++ b/pkg/provider/register/register.go @@ -0,0 +1,21 @@ +/* +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 register + +// packages imported here are registered to the controller schema +import ( + // register awssm provider + _ "github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager" +) diff --git a/pkg/provider/schema/schema.go b/pkg/provider/schema/schema.go new file mode 100644 index 000000000..052ae52fb --- /dev/null +++ b/pkg/provider/schema/schema.go @@ -0,0 +1,114 @@ +/* +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 schema + +import ( + "encoding/json" + "fmt" + "sync" + + esv1alpha1 "github.com/external-secrets/external-secrets/api/v1alpha1" + "github.com/external-secrets/external-secrets/pkg/provider" +) + +var builder map[string]provider.Provider +var buildlock sync.RWMutex + +func init() { + builder = make(map[string]provider.Provider) +} + +// Register a store backend type. Register panics if a +// backend with the same store is already registered +func Register(s provider.Provider, storeSpec *esv1alpha1.SecretStoreProvider) { + storeName, err := getProviderName(storeSpec) + if err != nil { + panic(fmt.Sprintf("store error registering schema: %s", err.Error())) + } + + buildlock.Lock() + defer buildlock.Unlock() + _, exists := builder[storeName] + if exists { + panic(fmt.Sprintf("store %q already registered", storeName)) + } + + builder[storeName] = s +} + +// ForceRegister adds to store schema, overwriting a store if +// already registered. Should only be used for testing +func ForceRegister(s provider.Provider, storeSpec *esv1alpha1.SecretStoreProvider) { + storeName, err := getProviderName(storeSpec) + if err != nil { + panic(fmt.Sprintf("store error registering schema: %s", err.Error())) + } + + buildlock.Lock() + builder[storeName] = s + buildlock.Unlock() +} + +// GetProviderByName returns the provider implementation by name +func GetProviderByName(name string) (provider.Provider, bool) { + buildlock.RLock() + f, ok := builder[name] + buildlock.RUnlock() + return f, ok +} + +// GetProvider returns the provider from the generic store +func GetProvider(s esv1alpha1.GenericStore) (provider.Provider, error) { + provider := s.GetProvider() + storeName, err := getProviderName(provider) + if err != nil { + return nil, fmt.Errorf("store error for %s: %w", s.GetName(), err) + } + + buildlock.RLock() + f, ok := builder[storeName] + buildlock.RUnlock() + + if !ok { + return nil, fmt.Errorf("failed to find registered store backend for type: %s, name: %s", storeName, s.GetName()) + } + + return f, nil +} + +// getProviderName returns the name of the configured provider +// or an error if the provider is not configured +func getProviderName(storeSpec *esv1alpha1.SecretStoreProvider) (string, error) { + storeBytes, err := json.Marshal(storeSpec) + if err != nil { + return "", fmt.Errorf("failed to marshal store spec: %w", err) + } + + storeMap := make(map[string]interface{}) + err = json.Unmarshal(storeBytes, &storeMap) + if err != nil { + return "", fmt.Errorf("failed to unmarshal store spec: %w", err) + } + + if len(storeMap) != 1 { + return "", fmt.Errorf("secret stores must only have exactly one backend specified, found %d", len(storeMap)) + } + + for k := range storeMap { + return k, nil + } + + return "", fmt.Errorf("failed to find registered store backend") +} diff --git a/pkg/provider/schema/schema_test.go b/pkg/provider/schema/schema_test.go new file mode 100644 index 000000000..88fdd3b15 --- /dev/null +++ b/pkg/provider/schema/schema_test.go @@ -0,0 +1,53 @@ +/* +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 schema + +import ( + "context" + "testing" + + esv1alpha1 "github.com/external-secrets/external-secrets/api/v1alpha1" + "github.com/external-secrets/external-secrets/pkg/provider" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type PP struct{} + +// New constructs a SecretsManager Provider +func (p *PP) New(ctx context.Context, store esv1alpha1.SecretStoreProvider, kube client.Client, namespace string) (provider.Provider, error) { + return p, nil +} + +// GetSecret returns a single secret from the provider +func (p *PP) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + return []byte("NOOP"), nil +} + +// GetSecretMap returns multiple k/v pairs from the provider +func (p *PP) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + return map[string][]byte{}, nil +} + +func TestRegister(t *testing.T) { + p, ok := GetProviderByName("awssm") + assert.Nil(t, p) + assert.False(t, ok) + ForceRegister(&PP{}, &esv1alpha1.SecretStoreProvider{ + AWSSM: &esv1alpha1.AWSSMProvider{}, + }) + p, ok = GetProviderByName("awssm") + assert.NotNil(t, p) + assert.True(t, ok) +} diff --git a/pkg/providers/awssm/awssm.go b/pkg/providers/awssm/awssm.go deleted file mode 100644 index d181b8e36..000000000 --- a/pkg/providers/awssm/awssm.go +++ /dev/null @@ -1 +0,0 @@ -package awssm diff --git a/pkg/providers/awssm/awssm_test.go b/pkg/providers/awssm/awssm_test.go deleted file mode 100644 index d181b8e36..000000000 --- a/pkg/providers/awssm/awssm_test.go +++ /dev/null @@ -1 +0,0 @@ -package awssm