From 324c7def0673df9208b73ce2f4e77341e24f392c Mon Sep 17 00:00:00 2001 From: Daniel Hix <41391321+ADustyOldMuffin@users.noreply.github.com> Date: Sun, 20 Mar 2022 03:32:27 -0500 Subject: [PATCH] feat: implement ClusterExternalSecret (#542) Co-authored-by: Gustavo Fernandes de Carvalho --- PROJECT | 2 +- .../v1beta1/clusterexternalsecret_types.go | 99 +++++ apis/externalsecrets/v1beta1/register.go | 9 + .../v1beta1/zz_generated.deepcopy.go | 141 +++++++ cmd/root.go | 12 + ...nal-secrets.io_clusterexternalsecrets.yaml | 353 ++++++++++++++++++ .../external-secrets/templates/rbac.yaml | 12 + deploy/crds/bundle.yaml | 312 ++++++++++++++++ design/cluster-external-secret-spec.md | 118 ++++++ docs/api-clusterexternalsecret.md | 11 + .../full-cluster-external-secret.yaml | 78 ++++ e2e/framework/util/util.go | 2 +- e2e/suite/common/common.go | 4 +- hack/api-docs/mkdocs.yml | 1 + .../clusterexternalsecret_controller.go | 275 ++++++++++++++ .../clusterexternalsecret_controller_test.go | 340 +++++++++++++++++ .../clusterexternalsecret/suite_test.go | 107 ++++++ pkg/controllers/clusterexternalsecret/util.go | 65 ++++ pkg/controllers/commontest/common.go | 72 ++++ pkg/controllers/crds/crds_controller_test.go | 9 +- .../externalsecret_controller_test.go | 56 +-- pkg/provider/akeyless/akeyless_api.go | 4 +- pkg/provider/akeyless/utils.go | 6 +- .../secretsmanager_workload_identity.go | 4 +- .../secretsmanager_workload_identity_test.go | 4 +- pkg/provider/vault/vault.go | 3 +- pkg/provider/vault/vault_test.go | 4 +- pkg/provider/webhook/webhook_test.go | 2 +- pkg/template/v2/pem_chain.go | 3 +- 29 files changed, 2037 insertions(+), 71 deletions(-) create mode 100644 apis/externalsecrets/v1beta1/clusterexternalsecret_types.go create mode 100644 config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml create mode 100644 design/cluster-external-secret-spec.md create mode 100644 docs/api-clusterexternalsecret.md create mode 100644 docs/snippets/full-cluster-external-secret.yaml create mode 100644 pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go create mode 100644 pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go create mode 100644 pkg/controllers/clusterexternalsecret/suite_test.go create mode 100644 pkg/controllers/clusterexternalsecret/util.go create mode 100644 pkg/controllers/commontest/common.go diff --git a/PROJECT b/PROJECT index 8f2e7ca7d..40c92b020 100644 --- a/PROJECT +++ b/PROJECT @@ -11,7 +11,7 @@ resources: - group: external-secrets kind: ExternalSecret version: v1alpha1 -- group: external-secrets +version: "2" kind: ClusterSecretStore version: v1beta1 - group: external-secrets diff --git a/apis/externalsecrets/v1beta1/clusterexternalsecret_types.go b/apis/externalsecrets/v1beta1/clusterexternalsecret_types.go new file mode 100644 index 000000000..36dad84c9 --- /dev/null +++ b/apis/externalsecrets/v1beta1/clusterexternalsecret_types.go @@ -0,0 +1,99 @@ +/* +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 v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterExternalSecretSpec defines the desired state of ClusterExternalSecret. +type ClusterExternalSecretSpec struct { + // The spec for the ExternalSecrets to be created + ExternalSecretSpec ExternalSecretSpec `json:"externalSecretSpec"` + + // The name of the external secrets to be created defaults to the name of the ClusterExternalSecret + // +optional + ExternalSecretName string `json:"externalSecretName"` + + // The labels to select by to find the Namespaces to create the ExternalSecrets in. + NamespaceSelector metav1.LabelSelector `json:"namespaceSelector"` + + // The time in which the controller should reconcile it's objects and recheck namespaces for labels. + RefreshInterval *metav1.Duration `json:"refreshTime,omitempty"` +} + +type ClusterExternalSecretConditionType string + +const ( + ClusterExternalSecretReady ClusterExternalSecretConditionType = "Ready" + ClusterExternalSecretPartiallyReady ClusterExternalSecretConditionType = "PartiallyReady" + ClusterExternalSecretNotReady ClusterExternalSecretConditionType = "NotReady" +) + +type ClusterExternalSecretStatusCondition struct { + Type ClusterExternalSecretConditionType `json:"type"` + Status corev1.ConditionStatus `json:"status"` + + // +optional + Message string `json:"message,omitempty"` +} + +// ClusterExternalSecretNamespaceFailure represents a failed namespace deployment and it's reason. +type ClusterExternalSecretNamespaceFailure struct { + + // Namespace is the namespace that failed when trying to apply an ExternalSecret + Namespace string `json:"namespace"` + + // Reason is why the ExternalSecret failed to apply to the namespace + // +optional + Reason string `json:"reason,omitempty"` +} + +// ClusterExternalSecretStatus defines the observed state of ClusterExternalSecret. +type ClusterExternalSecretStatus struct { + // Failed namespaces are the namespaces that failed to apply an ExternalSecret + // +optional + FailedNamespaces []ClusterExternalSecretNamespaceFailure `json:"failedNamespaces,omitempty"` + + // ProvisionedNamespaces are the namespaces where the ClusterExternalSecret has secrets + // +optional + ProvisionedNamespaces []string `json:"provisionedNamespaces,omitempty"` + + // +optional + Conditions []ClusterExternalSecretStatusCondition `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:resource:scope=Cluster,categories={externalsecrets},shortName=ces +//+kubebuilder:subresource:status +// ClusterExternalSecret is the Schema for the clusterexternalsecrets API. +type ClusterExternalSecret struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterExternalSecretSpec `json:"spec,omitempty"` + Status ClusterExternalSecretStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ClusterExternalSecretList contains a list of ClusterExternalSecret. +type ClusterExternalSecretList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterExternalSecret `json:"items"` +} diff --git a/apis/externalsecrets/v1beta1/register.go b/apis/externalsecrets/v1beta1/register.go index 4718ea735..3075ac97d 100644 --- a/apis/externalsecrets/v1beta1/register.go +++ b/apis/externalsecrets/v1beta1/register.go @@ -44,6 +44,14 @@ var ( ExtSecretGroupVersionKind = SchemeGroupVersion.WithKind(ExtSecretKind) ) +// ClusterExternalSecret type metadata. +var ( + ClusterExtSecretKind = reflect.TypeOf(ClusterExternalSecret{}).Name() + ClusterExtSecretGroupKind = schema.GroupKind{Group: Group, Kind: ClusterExtSecretKind}.String() + ClusterExtSecretKindAPIVersion = ClusterExtSecretKind + "." + SchemeGroupVersion.String() + ClusterExtSecretGroupVersionKind = SchemeGroupVersion.WithKind(ClusterExtSecretKind) +) + // SecretStore type metadata. var ( SecretStoreKind = reflect.TypeOf(SecretStore{}).Name() @@ -62,6 +70,7 @@ var ( func init() { SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{}) + SchemeBuilder.Register(&ClusterExternalSecret{}, &ClusterExternalSecretList{}) SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{}) SchemeBuilder.Register(&ClusterSecretStore{}, &ClusterSecretStoreList{}) } diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go index fb13065c4..740b10227 100644 --- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go @@ -317,6 +317,147 @@ func (in *CertAuth) DeepCopy() *CertAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExternalSecret) DeepCopyInto(out *ClusterExternalSecret) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExternalSecret. +func (in *ClusterExternalSecret) DeepCopy() *ClusterExternalSecret { + if in == nil { + return nil + } + out := new(ClusterExternalSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterExternalSecret) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExternalSecretList) DeepCopyInto(out *ClusterExternalSecretList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterExternalSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExternalSecretList. +func (in *ClusterExternalSecretList) DeepCopy() *ClusterExternalSecretList { + if in == nil { + return nil + } + out := new(ClusterExternalSecretList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterExternalSecretList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExternalSecretNamespaceFailure) DeepCopyInto(out *ClusterExternalSecretNamespaceFailure) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExternalSecretNamespaceFailure. +func (in *ClusterExternalSecretNamespaceFailure) DeepCopy() *ClusterExternalSecretNamespaceFailure { + if in == nil { + return nil + } + out := new(ClusterExternalSecretNamespaceFailure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExternalSecretSpec) DeepCopyInto(out *ClusterExternalSecretSpec) { + *out = *in + in.ExternalSecretSpec.DeepCopyInto(&out.ExternalSecretSpec) + in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) + if in.RefreshInterval != nil { + in, out := &in.RefreshInterval, &out.RefreshInterval + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExternalSecretSpec. +func (in *ClusterExternalSecretSpec) DeepCopy() *ClusterExternalSecretSpec { + if in == nil { + return nil + } + out := new(ClusterExternalSecretSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExternalSecretStatus) DeepCopyInto(out *ClusterExternalSecretStatus) { + *out = *in + if in.FailedNamespaces != nil { + in, out := &in.FailedNamespaces, &out.FailedNamespaces + *out = make([]ClusterExternalSecretNamespaceFailure, len(*in)) + copy(*out, *in) + } + if in.ProvisionedNamespaces != nil { + in, out := &in.ProvisionedNamespaces, &out.ProvisionedNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]ClusterExternalSecretStatusCondition, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExternalSecretStatus. +func (in *ClusterExternalSecretStatus) DeepCopy() *ClusterExternalSecretStatus { + if in == nil { + return nil + } + out := new(ClusterExternalSecretStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExternalSecretStatusCondition) DeepCopyInto(out *ClusterExternalSecretStatusCondition) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExternalSecretStatusCondition. +func (in *ClusterExternalSecretStatusCondition) DeepCopy() *ClusterExternalSecretStatusCondition { + if in == nil { + return nil + } + out := new(ClusterExternalSecretStatusCondition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterSecretStore) DeepCopyInto(out *ClusterSecretStore) { *out = *in diff --git a/cmd/root.go b/cmd/root.go index bd1b6e6af..8bf85b982 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -35,6 +35,7 @@ import ( esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + "github.com/external-secrets/external-secrets/pkg/controllers/clusterexternalsecret" "github.com/external-secrets/external-secrets/pkg/controllers/externalsecret" "github.com/external-secrets/external-secrets/pkg/controllers/secretstore" ) @@ -137,6 +138,17 @@ var rootCmd = &cobra.Command{ setupLog.Error(err, errCreateController, "controller", "ExternalSecret") os.Exit(1) } + if err = (&clusterexternalsecret.Reconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("ClusterExternalSecret"), + Scheme: mgr.GetScheme(), + RequeueInterval: time.Hour, + }).SetupWithManager(mgr, controller.Options{ + MaxConcurrentReconciles: concurrent, + }); err != nil { + setupLog.Error(err, errCreateController, "controller", "ClusterExternalSecret") + os.Exit(1) + } setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") diff --git a/config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml b/config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml new file mode 100644 index 000000000..ada71aa6f --- /dev/null +++ b/config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml @@ -0,0 +1,353 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: clusterexternalsecrets.external-secrets.io +spec: + group: external-secrets.io + names: + categories: + - externalsecrets + kind: ClusterExternalSecret + listKind: ClusterExternalSecretList + plural: clusterexternalsecrets + shortNames: + - ces + singular: clusterexternalsecret + scope: Cluster + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: ClusterExternalSecret is the Schema for the clusterexternalsecrets + 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: ClusterExternalSecretSpec defines the desired state of ClusterExternalSecret. + properties: + externalSecretName: + description: The name of the external secrets to be created defaults + to the name of the ClusterExternalSecret + type: string + externalSecretSpec: + description: The spec for the ExternalSecrets to be created + 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: + maxProperties: 1 + minProperties: 1 + properties: + extract: + description: Used to extract multiple key/value pairs from + one secret + 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 + find: + description: Used to find secrets based on tags or regular + expressions + maxProperties: 1 + minProperties: 1 + properties: + name: + description: Finds secrets based on the name. + properties: + regexp: + description: Finds secrets base + type: string + type: object + tags: + additionalProperties: + type: string + description: Find secrets based on tags. + type: object + type: object + type: object + type: array + refreshInterval: + default: 1h + description: RefreshInterval is the amount of time before the + values are read again from the SecretStore provider Valid time + units are "ns", "us" (or "µs"), "ms", "s", "m", "h" May be set + to zero to fetch and create it once. Defaults to 1h. + 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: + default: Owner + description: CreationPolicy defines rules on how to create + the resulting Secret Defaults to 'Owner' + type: string + deletionPolicy: + default: None + description: DeletionPolicy defines rules on how to delete + the resulting Secret Defaults to 'None' + type: string + immutable: + description: Immutable defines if the final secret will be + immutable + type: boolean + 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 + template: + description: Template defines a blueprint for the created + Secret resource. + properties: + data: + additionalProperties: + type: string + type: object + engineVersion: + default: v2 + type: string + metadata: + description: ExternalSecretTemplateMetadata defines metadata + fields for the Secret blueprint. + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + templateFrom: + items: + maxProperties: 1 + minProperties: 1 + properties: + configMap: + properties: + items: + items: + properties: + key: + type: string + required: + - key + type: object + type: array + name: + type: string + required: + - items + - name + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + required: + - key + type: object + type: array + name: + type: string + required: + - items + - name + type: object + type: object + type: array + type: + type: string + type: object + type: object + required: + - secretStoreRef + - target + type: object + namespaceSelector: + description: The labels to select by to find the Namespaces to create + the ExternalSecrets in. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + refreshTime: + description: The time in which the controller should reconcile it's + objects and recheck namespaces for labels. + type: string + required: + - externalSecretSpec + - namespaceSelector + type: object + status: + description: ClusterExternalSecretStatus defines the observed state of + ClusterExternalSecret. + properties: + conditions: + items: + properties: + message: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + failedNamespaces: + description: Failed namespaces are the namespaces that failed to apply + an ExternalSecret + items: + description: ClusterExternalSecretNamespaceFailure represents a + failed namespace deployment and it's reason. + properties: + namespace: + description: Namespace is the namespace that failed when trying + to apply an ExternalSecret + type: string + reason: + description: Reason is why the ExternalSecret failed to apply + to the namespace + type: string + required: + - namespace + type: object + type: array + provisionedNamespaces: + description: ProvisionedNamespaces are the namespaces where the ClusterExternalSecret + has secrets + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/charts/external-secrets/templates/rbac.yaml b/deploy/charts/external-secrets/templates/rbac.yaml index 28a75a094..9a22df9d2 100644 --- a/deploy/charts/external-secrets/templates/rbac.yaml +++ b/deploy/charts/external-secrets/templates/rbac.yaml @@ -12,6 +12,7 @@ rules: - "secretstores" - "clustersecretstores" - "externalsecrets" + - "clusterexternalsecrets" verbs: - "get" - "list" @@ -28,6 +29,9 @@ rules: - "clustersecretstores" - "clustersecretstores/status" - "clustersecretstores/finalizers" + - "clusterexternalsecrets" + - "clusterexternalsecrets/status" + - "clusterexternalsecrets/finalizers" verbs: - "update" - "patch" @@ -35,6 +39,7 @@ rules: - "" resources: - "serviceaccounts" + - "namespaces" verbs: - "get" - "list" @@ -72,6 +77,13 @@ rules: verbs: - "create" - "patch" + - apiGroups: + - "external-secrets.io" + resources: + - "externalsecrets" + verbs: + - "create" + - "update" --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml index c80e03293..e9a6349d9 100644 --- a/deploy/crds/bundle.yaml +++ b/deploy/crds/bundle.yaml @@ -1,5 +1,317 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: clusterexternalsecrets.external-secrets.io +spec: + group: external-secrets.io + names: + categories: + - externalsecrets + kind: ClusterExternalSecret + listKind: ClusterExternalSecretList + plural: clusterexternalsecrets + shortNames: + - ces + singular: clusterexternalsecret + scope: Cluster + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: ClusterExternalSecret is the Schema for the clusterexternalsecrets 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: ClusterExternalSecretSpec defines the desired state of ClusterExternalSecret. + properties: + externalSecretName: + description: The name of the external secrets to be created defaults to the name of the ClusterExternalSecret + type: string + externalSecretSpec: + description: The spec for the ExternalSecrets to be created + 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: + maxProperties: 1 + minProperties: 1 + properties: + extract: + description: Used to extract multiple key/value pairs from one secret + 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 + find: + description: Used to find secrets based on tags or regular expressions + maxProperties: 1 + minProperties: 1 + properties: + name: + description: Finds secrets based on the name. + properties: + regexp: + description: Finds secrets base + type: string + type: object + tags: + additionalProperties: + type: string + description: Find secrets based on tags. + type: object + type: object + type: object + type: array + refreshInterval: + default: 1h + description: RefreshInterval is the amount of time before the values are read again from the SecretStore provider Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" May be set to zero to fetch and create it once. Defaults to 1h. + 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: + default: Owner + description: CreationPolicy defines rules on how to create the resulting Secret Defaults to 'Owner' + type: string + deletionPolicy: + default: None + description: DeletionPolicy defines rules on how to delete the resulting Secret Defaults to 'None' + type: string + immutable: + description: Immutable defines if the final secret will be immutable + type: boolean + 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 + template: + description: Template defines a blueprint for the created Secret resource. + properties: + data: + additionalProperties: + type: string + type: object + engineVersion: + default: v2 + type: string + metadata: + description: ExternalSecretTemplateMetadata defines metadata fields for the Secret blueprint. + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + templateFrom: + items: + maxProperties: 1 + minProperties: 1 + properties: + configMap: + properties: + items: + items: + properties: + key: + type: string + required: + - key + type: object + type: array + name: + type: string + required: + - items + - name + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + required: + - key + type: object + type: array + name: + type: string + required: + - items + - name + type: object + type: object + type: array + type: + type: string + type: object + type: object + required: + - secretStoreRef + - target + type: object + namespaceSelector: + description: The labels to select by to find the Namespaces to create the ExternalSecrets in. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + refreshTime: + description: The time in which the controller should reconcile it's objects and recheck namespaces for labels. + type: string + required: + - externalSecretSpec + - namespaceSelector + type: object + status: + description: ClusterExternalSecretStatus defines the observed state of ClusterExternalSecret. + properties: + conditions: + items: + properties: + message: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + failedNamespaces: + description: Failed namespaces are the namespaces that failed to apply an ExternalSecret + items: + description: ClusterExternalSecretNamespaceFailure represents a failed namespace deployment and it's reason. + properties: + namespace: + description: Namespace is the namespace that failed when trying to apply an ExternalSecret + type: string + reason: + description: Reason is why the ExternalSecret failed to apply to the namespace + type: string + required: + - namespace + type: object + type: array + provisionedNamespaces: + description: ProvisionedNamespaces are the namespaces where the ClusterExternalSecret has secrets + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1 + clientConfig: + caBundle: Cg== + service: + name: kubernetes + namespace: default + path: /convert +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.8.0 diff --git a/design/cluster-external-secret-spec.md b/design/cluster-external-secret-spec.md new file mode 100644 index 000000000..3e22f83c9 --- /dev/null +++ b/design/cluster-external-secret-spec.md @@ -0,0 +1,118 @@ +```yaml +--- +title: Adding Cluster External Secrets +version: v1alpha1 +authors: Daniel "ADustyOldMuffin" Hix +creation-date: 2020-09-01 +status: draft +--- +``` + +# Adding Cluster External Secrets + +## Table of Contents + + +- [Adding Cluster External Secrets](#adding-cluster-external-secrets) + - [Table of Contents](#table-of-contents) + - [Summary](#summary) + - [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) + - [Proposal](#proposal) + - [User Stories](#user-stories) + - [API](#api) + - [Behavior](#behavior) + - [Drawbacks](#drawbacks) + - [Acceptance Criteria](#acceptance-criteria) + - [Alternatives](#alternatives) + + + +## Summary +This would provide a way to template an `ExternalSecret` and based on a namespace selector in the CRD it would generate `ExternalSecret`s in matching namespaces. + +## Motivation +It's a pain point to have to create a `Secret`/`ExternalSecret` in every namespace where it's needed, and this would provide a way to to do this easily. Another motivation is the possible creation of a Kubernetes secret provider which would provide an `ExternalSecret` from a secret already located in cluster. This in combination with that provider could provide a way to sync a secret across namespaces with a single provider call. + +### Goals +To provide a way to deploy multiple `ExternalSecret`s with a single CRD. + +### Non-Goals +Lower provider calls based on the created `ExternalSecrets` or manage their sync. + +## Proposal + +### User Stories +As an ESO User I would like to create the same `Secret`/`ExternalSecret` in multiple namespaces. + +### API +``` yaml +apiVersion: external-secrets.io/v1alpha1 +kind: ClusterExternalSecret +metadata: + name: testing-secret +spec: + # The selector used to select the namespaces to deploy to + namespaceSelector: + matchLabels: + foo: bar + # This spec is the exact same spec as an ExternalSecret, and is used as a template + externalSecretSpec: + refreshInterval: "15s" + secretStoreRef: + name: some-provider + kind: ClusterSecretStore + target: + name: my-cool-new-secret + creationPolicy: Owner + data: + - secretKey: my-cool-new-secret-key + remoteRef: + key: test + property: foo +``` + +### Behavior +When created the controller will find namespaces via a label selector, and matching namespaces will then have an `ExternalSecret` deployed to them. + +Edge cases are, + +1. namespaces being labeled after creation - currently handled via a re-queue interval, but the interval is a tad high and the changes won't take place right away. -- +This has been handled by adding a refreshInterval to the spec which can be defined and controls when the controller is requeued. + +1. Template being changed after deployment - Handled via the `createOrUpdate` function which should reconcile the `ExternalSecrets` + +### Drawbacks +This will incur a high load on providers as it will create N number of `ExternalSecret`s which will also poll the provider separately. This can be fixed in two ways, + +- In this CRD by adding the ability to reference an existing secret to replicate instead of creating `ExternalSecret`s, but this is not the "spirit" of the CRD and is less of a `ClusterExternalSecret` and more of a `ClusterSecret`. This is not the preferred way. + +- The creation of a new Kubernetes Provider which will allow for the targeting of secrets in Kubernetes for `ExternalSecret`s + +### Acceptance Criteria +What does it take to make this feature producation ready? Please take the time to think about: +* how would you rollout this feature and rollback if it causes harm? +* Test Roadmap: what kinds of tests do we want to ensure a good user experience? +* observability: Do users need to get insights into the inner workings of that feature? +* monitoring: How can users tell whether the feature is working as expected or not? + can we provide dashboards, metrics, reasonable SLIs/SLOs + or example alerts for this feature? +* troubleshooting: How would users want to troubleshoot this particular feature? + Think about different failure modes of this feature. + +For this to be production ready it will need to be tested to ensure the expected behavior occurs, specifically around edge cases like + +- Adding labels after creation of resource +- Changing of created `ExternalSecret`s +- Cleanup of `ExternalSecret`s when resources is deleted +- Deletion of owned resource +- Removal of label from namespace after `ExternalSecret` is created + +Everything else is on the `ExternalSecret` and not the `ClusterExternalSecret` and troubleshooting would be the same. + +## Alternatives + +Adding a namespace selector to the regular `ExternalSecret`, but this would cause issues since it's not cluster scoped, and can't use "owned by" which would cause issues for cleanup. + + diff --git a/docs/api-clusterexternalsecret.md b/docs/api-clusterexternalsecret.md new file mode 100644 index 000000000..61c7d906e --- /dev/null +++ b/docs/api-clusterexternalsecret.md @@ -0,0 +1,11 @@ +The `ClusterExternalSecret` is a cluster scoped resource that can be used to push an `ExternalSecret` to specific namespaces. + +Using the `namespaceSelector` you can select namespaces, and any matching namespaces will have the `ExternalSecret` specified in the `externalSecretSpec` created in it. + +## Example + +Below is an example of the `ClusterExternalSecret` in use. + +```yaml +{% include 'full-cluster-external-secret.yaml' %} +``` diff --git a/docs/snippets/full-cluster-external-secret.yaml b/docs/snippets/full-cluster-external-secret.yaml new file mode 100644 index 000000000..94a6ea1f2 --- /dev/null +++ b/docs/snippets/full-cluster-external-secret.yaml @@ -0,0 +1,78 @@ +{% raw %} +apiVersion: external-secrets.io/v1beta1 +kind: ClusterExternalSecret +metadata: + name: "hello-world" +spec: + # The name to be used on the ExternalSecrets + externalSecretName: "hello-world-es" + + # This is a basic label selector to select the namespaces to deploy ExternalSecrets to. + # you can read more about them here https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#resources-that-support-set-based-requirements + namespaceSelector: + matchLabels: + cool: label + + # How often the ClusterExternalSecret should reconcile itself + # This will decide how often to check and make sure that the ExternalSecrets exist in the matching namespaces + refreshTime: "1m" + + # This is the spec of the ExternalSecrets to be created + # The content of this was taken from our ExternalSecret example + externalSecretSpec: + secretStoreRef: + name: secret-store-name + kind: SecretStore + + refreshInterval: "1h" + target: + name: my-secret + creationPolicy: 'Merge' + template: + type: kubernetes.io/dockerconfigjson + + metadata: + annotations: {} + labels: {} + data: + config.yml: | + endpoints: + - https://{{ .data.user }}:{{ .data.password }}@api.exmaple.com + templateFrom: + - configMap: + name: alertmanager + items: + - key: alertmanager.yaml + data: + - secretKey: secret-key-to-be-managed + remoteRef: + key: provider-key + version: provider-key-version + property: provider-key-property + dataFrom: + - key: provider-key + version: provider-key-version + property: provider-key-property + +status: + # This will list any namespaces where the creation of the ExternalSecret failed + # This will not list any issues with the ExternalSecrets, you will have to check the + # ExternalSecrets to see any issues with them. + failedNamespaces: + - namespace: "matching-ns-1" + # This is one of the possible messages, and likely the most common + reason: "external secret already exists in namespace" + + # You can find all matching and successfully deployed namespaces here + provisionedNamespaces: + - "matching-ns-3" + - "matching-ns-2" + + # The condition can be Ready, PartiallyReady, or NotReady + # PartiallyReady would indicate an error in 1 or more namespaces + # NotReady would indicate errors in all namespaces meaning all ExternalSecrets resulted in errors + conditions: + - type: PartiallyReady + status: "True" + lastTransitionTime: "2022-01-12T12:33:02Z" +{% endraw %} diff --git a/e2e/framework/util/util.go b/e2e/framework/util/util.go index c29a90752..2d0bdc7bb 100644 --- a/e2e/framework/util/util.go +++ b/e2e/framework/util/util.go @@ -199,7 +199,7 @@ func isPodReady(p *v1.Pod) bool { // Timeout is 5min. func WaitForURL(url string) error { return wait.PollImmediate(2*time.Second, time.Minute*5, func() (bool, error) { - req, err := http.NewRequest(http.MethodGet, url, nil) + req, err := http.NewRequest(http.MethodGet, url, http.NoBody) if err != nil { return false, nil } diff --git a/e2e/suite/common/common.go b/e2e/suite/common/common.go index dee04ebe1..1ae785ea9 100644 --- a/e2e/suite/common/common.go +++ b/e2e/suite/common/common.go @@ -258,7 +258,7 @@ func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase) targetSecretValue1 := "great-name" targetSecretKey2 := "surname" targetSecretValue2 := "great-surname" - secretValue := fmt.Sprintf("{ \"%s\": \"%s\", \"%s\": \"%s\" }", targetSecretKey1, targetSecretValue1, targetSecretKey2, targetSecretValue2) + secretValue := fmt.Sprintf("{ %q: %q, %q: %q }", targetSecretKey1, targetSecretValue1, targetSecretKey2, targetSecretValue2) tc.Secrets = map[string]string{ secretKey1: secretValue, } @@ -519,7 +519,7 @@ func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.Tes PKDc8xGEXdd4A6jnwJBifJs+UpPrHAh0c63KfjO3rryDycvmxeWRnyU1yRCUjIuH31vi+L OkcGfqTaOoz2KVAAAAFGtpYW5AREVTS1RPUC1TNFI5S1JQAQIDBAUG -----END OPENSSH PRIVATE KEY-----` - cloudSecretValue := fmt.Sprintf(`{"ssh-auth": "%s"}`, SSHKey) + cloudSecretValue := fmt.Sprintf(`{"ssh-auth": %q}`, SSHKey) tc.Secrets = map[string]string{ cloudSecretName: cloudSecretValue, } diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml index e20dc92ca..3edae5591 100644 --- a/hack/api-docs/mkdocs.yml +++ b/hack/api-docs/mkdocs.yml @@ -31,6 +31,7 @@ nav: ExternalSecret: api-externalsecret.md SecretStore: api-secretstore.md ClusterSecretStore: api-clustersecretstore.md + ClusterExternalSecret: api-clusterexternalsecret.md - Guides: - Introduction: guides-introduction.md - Getting started: guides-getting-started.md diff --git a/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go b/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go new file mode 100644 index 000000000..802706a68 --- /dev/null +++ b/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go @@ -0,0 +1,275 @@ +/* +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 clusterexternalsecret + +import ( + "context" + "time" + + "github.com/go-logr/logr" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" +) + +// ClusterExternalSecretReconciler reconciles a ClusterExternalSecret object. +type Reconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + RequeueInterval time.Duration +} + +const ( + errGetCES = "could not get ClusterExternalSecret" + errPatchStatus = "unable to patch status" + errLabelMap = "unable to get map from labels" + errNamespaces = "could not get namespaces from selector" + errGetExistingES = "could not get existing ExternalSecret" + errCreatingOrUpdating = "could not create or update ExternalSecret" + errSetCtrlReference = "could not set the controller owner reference" + errSecretAlreadyExists = "external secret already exists in namespace" + errNamespacesFailed = "one or more namespaces failed" + errFailedToDelete = "external secret in non matching namespace could not be deleted" +) + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("ClusterExternalSecret", req.NamespacedName) + + var clusterExternalSecret esv1beta1.ClusterExternalSecret + + err := r.Get(ctx, req.NamespacedName, &clusterExternalSecret) + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } else if err != nil { + log.Error(err, errGetCES) + return ctrl.Result{}, nil + } + + p := client.MergeFrom(clusterExternalSecret.DeepCopy()) + defer r.deferPatch(ctx, log, &clusterExternalSecret, p) + + refreshInt := r.RequeueInterval + if clusterExternalSecret.Spec.RefreshInterval != nil { + refreshInt = clusterExternalSecret.Spec.RefreshInterval.Duration + } + + labelMap, err := metav1.LabelSelectorAsMap(&clusterExternalSecret.Spec.NamespaceSelector) + if err != nil { + log.Error(err, errLabelMap) + return ctrl.Result{RequeueAfter: refreshInt}, err + } + + namespaceList := v1.NamespaceList{} + + err = r.List(ctx, &namespaceList, &client.ListOptions{LabelSelector: labels.SelectorFromSet(labelMap)}) + if err != nil { + log.Error(err, errNamespaces) + return ctrl.Result{RequeueAfter: refreshInt}, err + } + + esName := clusterExternalSecret.Spec.ExternalSecretName + if esName == "" { + esName = clusterExternalSecret.ObjectMeta.Name + } + + failedNamespaces := r.removeOldNamespaces(ctx, namespaceList, esName, clusterExternalSecret.Status.ProvisionedNamespaces) + provisionedNamespaces := []string{} + + for _, namespace := range namespaceList.Items { + var existingES esv1beta1.ExternalSecret + err = r.Get(ctx, types.NamespacedName{ + Name: esName, + Namespace: namespace.Name, + }, &existingES) + + if result := checkForError(err, &existingES); result != "" { + log.Error(err, result) + failedNamespaces[namespace.Name] = result + continue + } + + if result, err := r.resolveExternalSecret(ctx, &clusterExternalSecret, &existingES, namespace, esName); err != nil { + log.Error(err, result) + failedNamespaces[namespace.Name] = result + continue + } + + provisionedNamespaces = append(provisionedNamespaces, namespace.ObjectMeta.Name) + } + + conditionType := getCondition(failedNamespaces, &namespaceList) + + condition := NewClusterExternalSecretCondition(conditionType, v1.ConditionTrue) + + if conditionType != esv1beta1.ClusterExternalSecretReady { + condition.Message = errNamespacesFailed + } + + SetClusterExternalSecretCondition(&clusterExternalSecret, *condition) + setFailedNamespaces(&clusterExternalSecret, failedNamespaces) + + if len(provisionedNamespaces) > 0 { + clusterExternalSecret.Status.ProvisionedNamespaces = provisionedNamespaces + } + + return ctrl.Result{RequeueAfter: refreshInt}, nil +} + +func (r *Reconciler) resolveExternalSecret(ctx context.Context, clusterExternalSecret *esv1beta1.ClusterExternalSecret, existingES *esv1beta1.ExternalSecret, namespace v1.Namespace, esName string) (string, error) { + // this means the existing ES does not belong to us + if err := controllerutil.SetControllerReference(clusterExternalSecret, existingES, r.Scheme); err != nil { + return errSetCtrlReference, err + } + + externalSecret := esv1beta1.ExternalSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: esName, + Namespace: namespace.Name, + }, + Spec: clusterExternalSecret.Spec.ExternalSecretSpec, + } + + if err := controllerutil.SetControllerReference(clusterExternalSecret, &externalSecret, r.Scheme); err != nil { + return errSetCtrlReference, err + } + + mutateFunc := func() error { + externalSecret.Spec = clusterExternalSecret.Spec.ExternalSecretSpec + return nil + } + + // An empty mutate func as nothing needs to happen currently + if _, err := ctrl.CreateOrUpdate(ctx, r.Client, &externalSecret, mutateFunc); err != nil { + return errCreatingOrUpdating, err + } + + return "", nil +} + +func (r *Reconciler) removeExternalSecret(ctx context.Context, esName, namespace string) (string, error) { + // + var existingES esv1beta1.ExternalSecret + err := r.Get(ctx, types.NamespacedName{ + Name: esName, + Namespace: namespace, + }, &existingES) + + // If we can't find it then just leave + if err != nil && apierrors.IsNotFound(err) { + return "", nil + } + + if result := checkForError(err, &existingES); result != "" { + return result, err + } + + err = r.Delete(ctx, &existingES, &client.DeleteOptions{}) + + if err != nil { + return errFailedToDelete, err + } + + return "", nil +} + +func (r *Reconciler) deferPatch(ctx context.Context, log logr.Logger, clusterExternalSecret *esv1beta1.ClusterExternalSecret, p client.Patch) { + if err := r.Status().Patch(ctx, clusterExternalSecret, p); err != nil { + log.Error(err, errPatchStatus) + } +} + +func (r *Reconciler) removeOldNamespaces(ctx context.Context, namespaceList v1.NamespaceList, esName string, provisionedNamespaces []string) map[string]string { + failedNamespaces := map[string]string{} + // Loop through existing namespaces first to make sure they still have our labels + for _, namespace := range getRemovedNamespaces(namespaceList, provisionedNamespaces) { + if result, _ := r.removeExternalSecret(ctx, esName, namespace); result != "" { + failedNamespaces[namespace] = result + } + } + + return failedNamespaces +} + +func checkForError(getError error, existingES *esv1beta1.ExternalSecret) string { + if getError != nil && !apierrors.IsNotFound(getError) { + return errGetExistingES + } + + // No one owns this resource so error out + if !apierrors.IsNotFound(getError) && len(existingES.ObjectMeta.OwnerReferences) == 0 { + return errSecretAlreadyExists + } + + return "" +} + +func getCondition(namespaces map[string]string, namespaceList *v1.NamespaceList) esv1beta1.ClusterExternalSecretConditionType { + if len(namespaces) == 0 { + return esv1beta1.ClusterExternalSecretReady + } + + if len(namespaces) < len(namespaceList.Items) { + return esv1beta1.ClusterExternalSecretPartiallyReady + } + + return esv1beta1.ClusterExternalSecretNotReady +} + +func getRemovedNamespaces(nsList v1.NamespaceList, provisionedNs []string) []string { + result := []string{} + + for _, ns := range provisionedNs { + if !ContainsNamespace(nsList, ns) { + result = append(result, ns) + } + } + + return result +} + +func setFailedNamespaces(ces *esv1beta1.ClusterExternalSecret, failedNamespaces map[string]string) { + if len(failedNamespaces) == 0 { + return + } + + ces.Status.FailedNamespaces = []esv1beta1.ClusterExternalSecretNamespaceFailure{} + + for namespace, message := range failedNamespaces { + ces.Status.FailedNamespaces = append(ces.Status.FailedNamespaces, esv1beta1.ClusterExternalSecretNamespaceFailure{ + Namespace: namespace, + Reason: message, + }) + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error { + return ctrl.NewControllerManagedBy(mgr). + WithOptions(opts). + For(&esv1beta1.ClusterExternalSecret{}). + Owns(&esv1beta1.ExternalSecret{}, builder.OnlyMetadata). + Complete(r) +} diff --git a/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go b/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go new file mode 100644 index 000000000..1760dc7c5 --- /dev/null +++ b/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go @@ -0,0 +1,340 @@ +/* +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 clusterexternalsecret + +import ( + "context" + "math/rand" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest" +) + +var ( + timeout = time.Second * 10 + interval = time.Millisecond * 250 +) + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz") + +func RandString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} + +type testNamespace struct { + namespace v1.Namespace + containsES bool + deletedES bool +} + +type testCase struct { + clusterExternalSecret *esv1beta1.ClusterExternalSecret + + // These are the namespaces that are being tested + externalSecretNamespaces []testNamespace + + // The labels to be used for the namespaces + namespaceLabels map[string]string + + // This is a setup function called for each test much like BeforeEach but with knowledge of the test case + // This is used by default to create namespaces and random labels + setup func(*testCase) + + // Is a method that's ran after everything has been created, but before the check methods are called + beforeCheck func(*testCase) + + // A function to do any work needed before a test is ran + preTest func() + + // checkCondition should return true if the externalSecret + // has the expected condition + checkCondition func(*esv1beta1.ClusterExternalSecret) bool + + // checkExternalSecret is called after the condition has been verified + // use this to verify the externalSecret + checkClusterExternalSecret func(*esv1beta1.ClusterExternalSecret) + + // checkExternalSecret is called after the condition has been verified + // use this to verify the externalSecret + checkExternalSecret func(*esv1beta1.ClusterExternalSecret, *esv1beta1.ExternalSecret) +} + +type testTweaks func(*testCase) + +var _ = Describe("ClusterExternalSecret controller", func() { + const ( + ClusterExternalSecretName = "test-ces" + ExternalSecretName = "test-es" + ExternalSecretStore = "test-store" + ExternalSecretTargetSecretName = "test-secret" + ClusterSecretStoreNamespace = "css-test-ns" + FakeManager = "fake.manager" + FooValue = "map-foo-value" + BarValue = "map-bar-value" + ) + + var ExternalSecretNamespaceTargets = []testNamespace{ + { + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ns-1", + }, + }, + containsES: true, + }, + { + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ns-2", + }, + }, + containsES: true, + }, + { + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ns-5", + }, + }, + containsES: false, + }, + } + + const targetProp = "targetProperty" + const remoteKey = "barz" + const remoteProperty = "bang" + + makeDefaultTestCase := func() *testCase { + return &testCase{ + checkCondition: func(ces *esv1beta1.ClusterExternalSecret) bool { + cond := GetClusterExternalSecretCondition(ces.Status, esv1beta1.ClusterExternalSecretReady) + if cond == nil || cond.Status != v1.ConditionTrue { + return false + } + return true + }, + checkClusterExternalSecret: func(es *esv1beta1.ClusterExternalSecret) { + // To be implemented by the tests + }, + checkExternalSecret: func(*esv1beta1.ClusterExternalSecret, *esv1beta1.ExternalSecret) { + // To be implemented by the tests + }, + clusterExternalSecret: &esv1beta1.ClusterExternalSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: ClusterExternalSecretName, + }, + Spec: esv1beta1.ClusterExternalSecretSpec{ + NamespaceSelector: metav1.LabelSelector{}, + ExternalSecretName: ExternalSecretName, + ExternalSecretSpec: esv1beta1.ExternalSecretSpec{ + SecretStoreRef: esv1beta1.SecretStoreRef{ + Name: ExternalSecretStore, + }, + Target: esv1beta1.ExternalSecretTarget{ + Name: ExternalSecretTargetSecretName, + }, + Data: []esv1beta1.ExternalSecretData{ + { + SecretKey: targetProp, + RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{ + Key: remoteKey, + Property: remoteProperty, + }, + }, + }, + }, + }, + }, + setup: func(tc *testCase) { + // Generate a random label since we don't want to match previous ones. + tc.namespaceLabels = map[string]string{ + RandString(5): RandString(5), + } + + namespaces := []testNamespace{} + for _, ns := range ExternalSecretNamespaceTargets { + name, err := ctest.CreateNamespaceWithLabels(ns.namespace.Name, k8sClient, tc.namespaceLabels) + Expect(err).ToNot(HaveOccurred()) + + newNs := ns + newNs.namespace.ObjectMeta.Name = name + namespaces = append(namespaces, newNs) + } + + tc.externalSecretNamespaces = namespaces + + tc.clusterExternalSecret.Spec.NamespaceSelector.MatchLabels = tc.namespaceLabels + }, + } + } + + // If the ES does noes not have a name specified then it should use the CES name + syncWithoutESName := func(tc *testCase) { + tc.clusterExternalSecret.Spec.ExternalSecretName = "" + tc.checkExternalSecret = func(ces *esv1beta1.ClusterExternalSecret, es *esv1beta1.ExternalSecret) { + Expect(es.ObjectMeta.Name).To(Equal(ces.ObjectMeta.Name)) + } + } + + doNotOverwriteExistingES := func(tc *testCase) { + tc.preTest = func() { + es := &esv1beta1.ExternalSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: ExternalSecretName, + Namespace: tc.externalSecretNamespaces[0].namespace.Name, + }, + } + + err := k8sClient.Create(context.Background(), es, &client.CreateOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + } + tc.checkCondition = func(ces *esv1beta1.ClusterExternalSecret) bool { + cond := GetClusterExternalSecretCondition(ces.Status, esv1beta1.ClusterExternalSecretPartiallyReady) + return cond != nil + } + tc.checkClusterExternalSecret = func(ces *esv1beta1.ClusterExternalSecret) { + Expect(len(ces.Status.FailedNamespaces)).Should(Equal(1)) + + failure := ces.Status.FailedNamespaces[0] + + Expect(failure.Namespace).Should(Equal(tc.externalSecretNamespaces[0].namespace.Name)) + Expect(failure.Reason).Should(Equal(errSecretAlreadyExists)) + } + } + + populatedProvisionedNamespaces := func(tc *testCase) { + tc.checkClusterExternalSecret = func(ces *esv1beta1.ClusterExternalSecret) { + for _, namespace := range tc.externalSecretNamespaces { + if !namespace.containsES { + continue + } + + Expect(sliceContainsString(namespace.namespace.Name, ces.Status.ProvisionedNamespaces)).To(BeTrue()) + } + } + } + + deleteESInNonMatchingNS := func(tc *testCase) { + tc.beforeCheck = func(tc *testCase) { + ns := tc.externalSecretNamespaces[0] + + // Remove the labels, but leave the should contain ES so we can still check it + ns.namespace.ObjectMeta.Labels = map[string]string{} + tc.externalSecretNamespaces[0].deletedES = true + + err := k8sClient.Update(context.Background(), &ns.namespace, &client.UpdateOptions{}) + Expect(err).ToNot(HaveOccurred()) + time.Sleep(time.Second) // Sleep to make sure the controller gets it. + } + } + + DescribeTable("When reconciling a ClusterExternal Secret", + func(tweaks ...testTweaks) { + tc := makeDefaultTestCase() + for _, tweak := range tweaks { + tweak(tc) + } + + // Run test setup + tc.setup(tc) + + if tc.preTest != nil { + By("running pre-test") + tc.preTest() + } + ctx := context.Background() + By("creating namespaces and cluster external secret") + err := k8sClient.Create(ctx, tc.clusterExternalSecret) + Expect(err).ShouldNot(HaveOccurred()) + cesKey := types.NamespacedName{Name: tc.clusterExternalSecret.Name} + createdCES := &esv1beta1.ClusterExternalSecret{} + + By("checking the ces condition") + Eventually(func() bool { + err := k8sClient.Get(ctx, cesKey, createdCES) + if err != nil { + return false + } + return tc.checkCondition(createdCES) + }, timeout, interval).Should(BeTrue()) + + // Run before check + if tc.beforeCheck != nil { + tc.beforeCheck(tc) + } + + tc.checkClusterExternalSecret(createdCES) + + if tc.checkExternalSecret != nil { + for _, ns := range tc.externalSecretNamespaces { + + if !ns.containsES { + continue + } + + es := &esv1beta1.ExternalSecret{} + + esName := createdCES.Spec.ExternalSecretName + if esName == "" { + esName = createdCES.ObjectMeta.Name + } + + esLookupKey := types.NamespacedName{ + Name: esName, + Namespace: ns.namespace.Name, + } + + Eventually(func() bool { + err := k8sClient.Get(ctx, esLookupKey, es) + + if ns.deletedES && apierrors.IsNotFound(err) { + return true + } + + return err == nil + }, timeout, interval).Should(BeTrue()) + tc.checkExternalSecret(createdCES, es) + } + } + }, + + Entry("Should use cluster external secret name if external secret name isn't defined", syncWithoutESName), + Entry("Should not overwrite existing external secrets and error out if one is present", doNotOverwriteExistingES), + Entry("Should have list of all provisioned namespaces", populatedProvisionedNamespaces), + Entry("Should delete external secrets when namespaces no longer match", deleteESInNonMatchingNS)) +}) + +func sliceContainsString(toFind string, collection []string) bool { + for _, val := range collection { + if val == toFind { + return true + } + } + + return false +} diff --git a/pkg/controllers/clusterexternalsecret/suite_test.go b/pkg/controllers/clusterexternalsecret/suite_test.go new file mode 100644 index 000000000..4a67ca9bc --- /dev/null +++ b/pkg/controllers/clusterexternalsecret/suite_test.go @@ -0,0 +1,107 @@ +/* +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 clusterexternalsecret + +import ( + "context" + "math/rand" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + rand.Seed(time.Now().UnixNano()) + log := zap.New(zap.WriteTo(GinkgoWriter), zap.Level(zapcore.DebugLevel)) + + logf.SetLogger(log) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "deploy", "crds")}, + } + + var ctx context.Context + ctx, cancel = context.WithCancel(context.Background()) + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = esv1beta1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + MetricsBindAddress: "0", // Avoid port collision + }) + Expect(err).ToNot(HaveOccurred()) + + // do not use k8sManager.GetClient() + // see https://github.com/kubernetes-sigs/controller-runtime/issues/343#issuecomment-469435686 + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(k8sClient).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + + err = (&Reconciler{ + Client: k8sClient, + Scheme: k8sManager.GetScheme(), + Log: ctrl.Log.WithName("controllers").WithName("ClusterExternalSecrets"), + RequeueInterval: time.Second, + }).SetupWithManager(k8sManager, controller.Options{ + MaxConcurrentReconciles: 1, + }) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + Expect(k8sManager.Start(ctx)).ToNot(HaveOccurred()) + }() +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() // stop manager + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) diff --git a/pkg/controllers/clusterexternalsecret/util.go b/pkg/controllers/clusterexternalsecret/util.go new file mode 100644 index 000000000..4388d9206 --- /dev/null +++ b/pkg/controllers/clusterexternalsecret/util.go @@ -0,0 +1,65 @@ +/* +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 clusterexternalsecret + +import ( + v1 "k8s.io/api/core/v1" + + esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" +) + +func NewClusterExternalSecretCondition(condType esv1beta1.ClusterExternalSecretConditionType, status v1.ConditionStatus) *esv1beta1.ClusterExternalSecretStatusCondition { + return &esv1beta1.ClusterExternalSecretStatusCondition{ + Type: condType, + Status: status, + } +} + +// GetExternalSecretCondition returns the condition with the provided type. +func GetClusterExternalSecretCondition(status esv1beta1.ClusterExternalSecretStatus, condType esv1beta1.ClusterExternalSecretConditionType) *esv1beta1.ClusterExternalSecretStatusCondition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == condType { + return &c + } + } + return nil +} + +func SetClusterExternalSecretCondition(ces *esv1beta1.ClusterExternalSecret, condition esv1beta1.ClusterExternalSecretStatusCondition) { + ces.Status.Conditions = append(filterOutCondition(ces.Status.Conditions, condition.Type), condition) +} + +// filterOutCondition returns an empty set of conditions with the provided type. +func filterOutCondition(conditions []esv1beta1.ClusterExternalSecretStatusCondition, condType esv1beta1.ClusterExternalSecretConditionType) []esv1beta1.ClusterExternalSecretStatusCondition { + newConditions := make([]esv1beta1.ClusterExternalSecretStatusCondition, 0, len(conditions)) + for _, c := range conditions { + if c.Type == condType { + continue + } + newConditions = append(newConditions, c) + } + return newConditions +} + +func ContainsNamespace(namespaces v1.NamespaceList, namespace string) bool { + for _, ns := range namespaces.Items { + if ns.ObjectMeta.Name == namespace { + return true + } + } + + return false +} diff --git a/pkg/controllers/commontest/common.go b/pkg/controllers/commontest/common.go new file mode 100644 index 000000000..46806b84c --- /dev/null +++ b/pkg/controllers/commontest/common.go @@ -0,0 +1,72 @@ +/* +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 commontest + +import ( + "context" + "fmt" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// CreateNamespace creates a new namespace in the cluster. +func CreateNamespace(baseName string, c client.Client) (string, error) { + return CreateNamespaceWithLabels(baseName, c, map[string]string{}) +} + +func CreateNamespaceWithLabels(baseName string, c client.Client, labels map[string]string) (string, error) { + genName := fmt.Sprintf("ctrl-test-%v", baseName) + ns := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: genName, + Labels: labels, + }, + } + + var err error + err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) { + err = c.Create(context.Background(), ns) + if err != nil { + return false, nil + } + return true, nil + }) + if err != nil { + return "", err + } + return ns.Name, nil +} + +func HasOwnerRef(meta metav1.ObjectMeta, kind, name string) bool { + for _, ref := range meta.OwnerReferences { + if ref.Kind == kind && ref.Name == name { + return true + } + } + return false +} + +func HasFieldOwnership(meta metav1.ObjectMeta, mgr, rawFields string) bool { + for _, ref := range meta.ManagedFields { + if ref.Manager == mgr && string(ref.FieldsV1.Raw) == rawFields { + return true + } + } + return false +} diff --git a/pkg/controllers/crds/crds_controller_test.go b/pkg/controllers/crds/crds_controller_test.go index c2faf79b3..e75782b37 100644 --- a/pkg/controllers/crds/crds_controller_test.go +++ b/pkg/controllers/crds/crds_controller_test.go @@ -15,6 +15,7 @@ limitations under the License. package crds import ( + "bytes" "context" "crypto/rsa" "crypto/x509" @@ -151,16 +152,16 @@ func TestPopulateSecret(t *testing.T) { cert := []byte("foobarcert") key := []byte("foobarkey") populateSecret(cert, key, &caArtifacts, &secret) - if string(secret.Data["tls.crt"]) != string(cert) { + if !bytes.Equal(secret.Data["tls.crt"], cert) { t.Errorf("secret value for tls.crt is wrong:%v", cert) } - if string(secret.Data["tls.key"]) != string(key) { + if !bytes.Equal(secret.Data["tls.key"], key) { t.Errorf("secret value for tls.key is wrong:%v", cert) } - if string(secret.Data["ca.crt"]) != string(caArtifacts.CertPEM) { + if !bytes.Equal(secret.Data["ca.crt"], caArtifacts.CertPEM) { t.Errorf("secret value for ca.crt is wrong:%v", cert) } - if string(secret.Data["ca.key"]) != string(caArtifacts.KeyPEM) { + if !bytes.Equal(secret.Data["ca.key"], caArtifacts.KeyPEM) { t.Errorf("secret value for ca.key is wrong:%v", cert) } } diff --git a/pkg/controllers/externalsecret/externalsecret_controller_test.go b/pkg/controllers/externalsecret/externalsecret_controller_test.go index 8cd5fd3ec..95d917714 100644 --- a/pkg/controllers/externalsecret/externalsecret_controller_test.go +++ b/pkg/controllers/externalsecret/externalsecret_controller_test.go @@ -26,10 +26,10 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest" "github.com/external-secrets/external-secrets/pkg/provider/testing/fake" ) @@ -149,7 +149,7 @@ var _ = Describe("ExternalSecret controller", func() { BeforeEach(func() { var err error - ExternalSecretNamespace, err = CreateNamespace("test-ns", k8sClient) + ExternalSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient) Expect(err).ToNot(HaveOccurred()) metric.Reset() syncCallsTotal.Reset() @@ -257,7 +257,7 @@ var _ = Describe("ExternalSecret controller", func() { Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v)) } // ownerRef must not not be set! - Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeTrue()) + Expect(ctest.HasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeTrue()) } } @@ -305,14 +305,14 @@ var _ = Describe("ExternalSecret controller", func() { for k, v := range es.ObjectMeta.Annotations { Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v)) } - Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse()) + Expect(ctest.HasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse()) Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2)) - Expect(hasFieldOwnership( + Expect(ctest.HasFieldOwnership( secret.ObjectMeta, "external-secrets", fmt.Sprintf("{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:%s\":{}}}}", esv1beta1.AnnotationDataHash)), ).To(BeTrue()) - Expect(hasFieldOwnership(secret.ObjectMeta, FakeManager, "{\"f:data\":{\".\":{},\"f:pre-existing-key\":{}},\"f:type\":{}}")).To(BeTrue()) + Expect(ctest.HasFieldOwnership(secret.ObjectMeta, FakeManager, "{\"f:data\":{\".\":{},\"f:pre-existing-key\":{}},\"f:type\":{}}")).To(BeTrue()) } } @@ -405,9 +405,9 @@ var _ = Describe("ExternalSecret controller", func() { Expect(string(secret.Data[existingKey])).To(Equal(secretVal)) // check owner/managedFields - Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse()) + Expect(ctest.HasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse()) Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2)) - Expect(hasFieldOwnership(secret.ObjectMeta, "external-secrets", "{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:reconcile.external-secrets.io/data-hash\":{}}}}")).To(BeTrue()) + Expect(ctest.HasFieldOwnership(secret.ObjectMeta, "external-secrets", "{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:reconcile.external-secrets.io/data-hash\":{}}}}")).To(BeTrue()) } } @@ -1343,46 +1343,6 @@ var _ = Describe("Controller Reconcile logic", func() { }) }) -// CreateNamespace creates a new namespace in the cluster. -func CreateNamespace(baseName string, c client.Client) (string, error) { - genName := fmt.Sprintf("ctrl-test-%v", baseName) - ns := &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: genName, - }, - } - var err error - err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) { - err = c.Create(context.Background(), ns) - if err != nil { - return false, nil - } - return true, nil - }) - if err != nil { - return "", err - } - return ns.Name, nil -} - -func hasOwnerRef(meta metav1.ObjectMeta, kind, name string) bool { - for _, ref := range meta.OwnerReferences { - if ref.Kind == kind && ref.Name == name { - return true - } - } - return false -} - -func hasFieldOwnership(meta metav1.ObjectMeta, mgr, rawFields string) bool { - for _, ref := range meta.ManagedFields { - if ref.Manager == mgr && string(ref.FieldsV1.Raw) == rawFields { - return true - } - } - return false -} - func externalSecretConditionShouldBe(name, ns string, ct esv1beta1.ExternalSecretConditionType, cs v1.ConditionStatus, v float64) bool { return Eventually(func() float64 { Expect(externalSecretCondition.WithLabelValues(name, ns, string(ct), string(cs)).Write(&metric)).To(Succeed()) diff --git a/pkg/provider/akeyless/akeyless_api.go b/pkg/provider/akeyless/akeyless_api.go index f4a5b0400..43749c7a3 100644 --- a/pkg/provider/akeyless/akeyless_api.go +++ b/pkg/provider/akeyless/akeyless_api.go @@ -20,7 +20,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "os" "strings" @@ -239,7 +239,7 @@ func readK8SServiceAccountJWT() (string, error) { } defer data.Close() - contentBytes, err := ioutil.ReadAll(data) + contentBytes, err := io.ReadAll(data) if err != nil { return "", err } diff --git a/pkg/provider/akeyless/utils.go b/pkg/provider/akeyless/utils.go index e8f49c9e9..e0cc13f92 100644 --- a/pkg/provider/akeyless/utils.go +++ b/pkg/provider/akeyless/utils.go @@ -15,7 +15,7 @@ package akeyless import ( "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -79,7 +79,7 @@ func getV2Url(path string) string { } func sendReq(url string) string { - req, err := http.NewRequest("POST", url, nil) + req, err := http.NewRequest("POST", url, http.NoBody) if err != nil { return "" } @@ -94,6 +94,6 @@ func sendReq(url string) string { } defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) return string(body) } diff --git a/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go b/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go index 53d4846d6..da53e164f 100644 --- a/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go +++ b/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go @@ -18,7 +18,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "time" @@ -251,7 +251,7 @@ func (g *gcpIDBindTokenGenerator) Generate(ctx context.Context, client *http.Cli } defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go b/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go index 9e8436c5f..2ab875a6c 100644 --- a/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go +++ b/pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go @@ -16,7 +16,7 @@ package secretmanager import ( "context" "encoding/json" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -182,7 +182,7 @@ func TestSATokenGen(t *testing.T) { func TestIDBTokenGen(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { payload := make(map[string]string) - rb, err := ioutil.ReadAll(r.Body) + rb, err := io.ReadAll(r.Body) assert.Nil(t, err) err = json.Unmarshal(rb, &payload) assert.Nil(t, err) diff --git a/pkg/provider/vault/vault.go b/pkg/provider/vault/vault.go index 4766f3b38..59f81f8e0 100644 --- a/pkg/provider/vault/vault.go +++ b/pkg/provider/vault/vault.go @@ -21,7 +21,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "net/http" "os" "regexp" @@ -945,7 +944,7 @@ func getJwtString(ctx context.Context, v *client, kubernetesAuth *esv1beta1.Vaul if _, err := os.Stat(serviceAccTokenPath); err != nil { return "", fmt.Errorf(errServiceAccount, err) } - jwtByte, err := ioutil.ReadFile(serviceAccTokenPath) + jwtByte, err := os.ReadFile(serviceAccTokenPath) if err != nil { return "", fmt.Errorf(errServiceAccount, err) } diff --git a/pkg/provider/vault/vault_test.go b/pkg/provider/vault/vault_test.go index dd5ba24e8..93965e60c 100644 --- a/pkg/provider/vault/vault_test.go +++ b/pkg/provider/vault/vault_test.go @@ -20,7 +20,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "testing" @@ -175,7 +175,7 @@ func newVaultResponse(data *vault.Secret) *vault.Response { jsonData, _ := json.Marshal(data) return &vault.Response{ Response: &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader(jsonData)), + Body: io.NopCloser(bytes.NewReader(jsonData)), }, } } diff --git a/pkg/provider/webhook/webhook_test.go b/pkg/provider/webhook/webhook_test.go index 99aefe4a2..439a16cc3 100644 --- a/pkg/provider/webhook/webhook_test.go +++ b/pkg/provider/webhook/webhook_test.go @@ -209,7 +209,7 @@ func TestWebhookGetSecret(t *testing.T) { var tc testCase if err := ydec.Decode(&tc); err != nil { if !errors.Is(err, io.EOF) { - t.Errorf("testcase decode error %w", err) + t.Errorf("testcase decode error %v", err) } break } diff --git a/pkg/template/v2/pem_chain.go b/pkg/template/v2/pem_chain.go index a12d2574c..d29e7529c 100644 --- a/pkg/template/v2/pem_chain.go +++ b/pkg/template/v2/pem_chain.go @@ -27,6 +27,7 @@ In: https://github.com/Azure/secrets-store-csi-driver-provider-azure/pull/332 package template import ( + "bytes" "crypto/x509" "encoding/pem" "fmt" @@ -65,7 +66,7 @@ func fetchCertChains(data []byte) ([]byte, error) { } // if ith node AuthorityKeyId is same as jth node SubjectKeyId, jth node was used // to sign the ith certificate - if string(nodes[i].cert.AuthorityKeyId) == string(nodes[j].cert.SubjectKeyId) { + if bytes.Equal(nodes[i].cert.AuthorityKeyId, nodes[j].cert.SubjectKeyId) { nodes[j].isParent = true nodes[i].parent = nodes[j] break