1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

feat: implement ClusterExternalSecret (#542)

Co-authored-by: Gustavo Fernandes de Carvalho <gusfcarvalho@gmail.com>
This commit is contained in:
Daniel Hix 2022-03-20 03:32:27 -05:00 committed by GitHub
parent 332977caba
commit 324c7def06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 2037 additions and 71 deletions

View file

@ -11,7 +11,7 @@ resources:
- group: external-secrets - group: external-secrets
kind: ExternalSecret kind: ExternalSecret
version: v1alpha1 version: v1alpha1
- group: external-secrets version: "2"
kind: ClusterSecretStore kind: ClusterSecretStore
version: v1beta1 version: v1beta1
- group: external-secrets - group: external-secrets

View file

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

View file

@ -44,6 +44,14 @@ var (
ExtSecretGroupVersionKind = SchemeGroupVersion.WithKind(ExtSecretKind) 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. // SecretStore type metadata.
var ( var (
SecretStoreKind = reflect.TypeOf(SecretStore{}).Name() SecretStoreKind = reflect.TypeOf(SecretStore{}).Name()
@ -62,6 +70,7 @@ var (
func init() { func init() {
SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{}) SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{})
SchemeBuilder.Register(&ClusterExternalSecret{}, &ClusterExternalSecretList{})
SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{}) SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{})
SchemeBuilder.Register(&ClusterSecretStore{}, &ClusterSecretStoreList{}) SchemeBuilder.Register(&ClusterSecretStore{}, &ClusterSecretStoreList{})
} }

View file

@ -317,6 +317,147 @@ func (in *CertAuth) DeepCopy() *CertAuth {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterSecretStore) DeepCopyInto(out *ClusterSecretStore) { func (in *ClusterSecretStore) DeepCopyInto(out *ClusterSecretStore) {
*out = *in *out = *in

View file

@ -35,6 +35,7 @@ import (
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" 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/externalsecret"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore" "github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
) )
@ -137,6 +138,17 @@ var rootCmd = &cobra.Command{
setupLog.Error(err, errCreateController, "controller", "ExternalSecret") setupLog.Error(err, errCreateController, "controller", "ExternalSecret")
os.Exit(1) 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") setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager") setupLog.Error(err, "problem running manager")

View file

@ -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.<key>) 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: []

View file

@ -12,6 +12,7 @@ rules:
- "secretstores" - "secretstores"
- "clustersecretstores" - "clustersecretstores"
- "externalsecrets" - "externalsecrets"
- "clusterexternalsecrets"
verbs: verbs:
- "get" - "get"
- "list" - "list"
@ -28,6 +29,9 @@ rules:
- "clustersecretstores" - "clustersecretstores"
- "clustersecretstores/status" - "clustersecretstores/status"
- "clustersecretstores/finalizers" - "clustersecretstores/finalizers"
- "clusterexternalsecrets"
- "clusterexternalsecrets/status"
- "clusterexternalsecrets/finalizers"
verbs: verbs:
- "update" - "update"
- "patch" - "patch"
@ -35,6 +39,7 @@ rules:
- "" - ""
resources: resources:
- "serviceaccounts" - "serviceaccounts"
- "namespaces"
verbs: verbs:
- "get" - "get"
- "list" - "list"
@ -72,6 +77,13 @@ rules:
verbs: verbs:
- "create" - "create"
- "patch" - "patch"
- apiGroups:
- "external-secrets.io"
resources:
- "externalsecrets"
verbs:
- "create"
- "update"
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole

View file

@ -1,5 +1,317 @@
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition 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.<key>) 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: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.8.0 controller-gen.kubebuilder.io/version: v0.8.0

View file

@ -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
<!-- toc -->
- [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)
<!-- /toc -->
## 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.

View file

@ -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' %}
```

View file

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

View file

@ -199,7 +199,7 @@ func isPodReady(p *v1.Pod) bool {
// Timeout is 5min. // Timeout is 5min.
func WaitForURL(url string) error { func WaitForURL(url string) error {
return wait.PollImmediate(2*time.Second, time.Minute*5, func() (bool, 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 { if err != nil {
return false, nil return false, nil
} }

View file

@ -258,7 +258,7 @@ func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)
targetSecretValue1 := "great-name" targetSecretValue1 := "great-name"
targetSecretKey2 := "surname" targetSecretKey2 := "surname"
targetSecretValue2 := "great-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{ tc.Secrets = map[string]string{
secretKey1: secretValue, secretKey1: secretValue,
} }
@ -519,7 +519,7 @@ func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.Tes
PKDc8xGEXdd4A6jnwJBifJs+UpPrHAh0c63KfjO3rryDycvmxeWRnyU1yRCUjIuH31vi+L PKDc8xGEXdd4A6jnwJBifJs+UpPrHAh0c63KfjO3rryDycvmxeWRnyU1yRCUjIuH31vi+L
OkcGfqTaOoz2KVAAAAFGtpYW5AREVTS1RPUC1TNFI5S1JQAQIDBAUG OkcGfqTaOoz2KVAAAAFGtpYW5AREVTS1RPUC1TNFI5S1JQAQIDBAUG
-----END OPENSSH PRIVATE KEY-----` -----END OPENSSH PRIVATE KEY-----`
cloudSecretValue := fmt.Sprintf(`{"ssh-auth": "%s"}`, SSHKey) cloudSecretValue := fmt.Sprintf(`{"ssh-auth": %q}`, SSHKey)
tc.Secrets = map[string]string{ tc.Secrets = map[string]string{
cloudSecretName: cloudSecretValue, cloudSecretName: cloudSecretValue,
} }

View file

@ -31,6 +31,7 @@ nav:
ExternalSecret: api-externalsecret.md ExternalSecret: api-externalsecret.md
SecretStore: api-secretstore.md SecretStore: api-secretstore.md
ClusterSecretStore: api-clustersecretstore.md ClusterSecretStore: api-clustersecretstore.md
ClusterExternalSecret: api-clusterexternalsecret.md
- Guides: - Guides:
- Introduction: guides-introduction.md - Introduction: guides-introduction.md
- Getting started: guides-getting-started.md - Getting started: guides-getting-started.md

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,6 +15,7 @@ limitations under the License.
package crds package crds
import ( import (
"bytes"
"context" "context"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
@ -151,16 +152,16 @@ func TestPopulateSecret(t *testing.T) {
cert := []byte("foobarcert") cert := []byte("foobarcert")
key := []byte("foobarkey") key := []byte("foobarkey")
populateSecret(cert, key, &caArtifacts, &secret) 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) 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) 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) 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) t.Errorf("secret value for ca.key is wrong:%v", cert)
} }
} }

View file

@ -26,10 +26,10 @@ import (
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" 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" "github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
) )
@ -149,7 +149,7 @@ var _ = Describe("ExternalSecret controller", func() {
BeforeEach(func() { BeforeEach(func() {
var err error var err error
ExternalSecretNamespace, err = CreateNamespace("test-ns", k8sClient) ExternalSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
metric.Reset() metric.Reset()
syncCallsTotal.Reset() syncCallsTotal.Reset()
@ -257,7 +257,7 @@ var _ = Describe("ExternalSecret controller", func() {
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v)) Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v))
} }
// ownerRef must not not be set! // 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 { for k, v := range es.ObjectMeta.Annotations {
Expect(secret.ObjectMeta.Annotations).To(HaveKeyWithValue(k, v)) 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(secret.ObjectMeta.ManagedFields).To(HaveLen(2))
Expect(hasFieldOwnership( Expect(ctest.HasFieldOwnership(
secret.ObjectMeta, secret.ObjectMeta,
"external-secrets", "external-secrets",
fmt.Sprintf("{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:%s\":{}}}}", esv1beta1.AnnotationDataHash)), fmt.Sprintf("{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:%s\":{}}}}", esv1beta1.AnnotationDataHash)),
).To(BeTrue()) ).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)) Expect(string(secret.Data[existingKey])).To(Equal(secretVal))
// check owner/managedFields // 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(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 { func externalSecretConditionShouldBe(name, ns string, ct esv1beta1.ExternalSecretConditionType, cs v1.ConditionStatus, v float64) bool {
return Eventually(func() float64 { return Eventually(func() float64 {
Expect(externalSecretCondition.WithLabelValues(name, ns, string(ct), string(cs)).Write(&metric)).To(Succeed()) Expect(externalSecretCondition.WithLabelValues(name, ns, string(ct), string(cs)).Write(&metric)).To(Succeed())

View file

@ -20,7 +20,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"os" "os"
"strings" "strings"
@ -239,7 +239,7 @@ func readK8SServiceAccountJWT() (string, error) {
} }
defer data.Close() defer data.Close()
contentBytes, err := ioutil.ReadAll(data) contentBytes, err := io.ReadAll(data)
if err != nil { if err != nil {
return "", err return "", err
} }

View file

@ -15,7 +15,7 @@ package akeyless
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -79,7 +79,7 @@ func getV2Url(path string) string {
} }
func sendReq(url 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 { if err != nil {
return "" return ""
} }
@ -94,6 +94,6 @@ func sendReq(url string) string {
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
return string(body) return string(body)
} }

View file

@ -18,7 +18,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"time" "time"
@ -251,7 +251,7 @@ func (g *gcpIDBindTokenGenerator) Generate(ctx context.Context, client *http.Cli
} }
defer resp.Body.Close() defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -16,7 +16,7 @@ package secretmanager
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"io/ioutil" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -182,7 +182,7 @@ func TestSATokenGen(t *testing.T) {
func TestIDBTokenGen(t *testing.T) { func TestIDBTokenGen(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
payload := make(map[string]string) payload := make(map[string]string)
rb, err := ioutil.ReadAll(r.Body) rb, err := io.ReadAll(r.Body)
assert.Nil(t, err) assert.Nil(t, err)
err = json.Unmarshal(rb, &payload) err = json.Unmarshal(rb, &payload)
assert.Nil(t, err) assert.Nil(t, err)

View file

@ -21,7 +21,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
@ -945,7 +944,7 @@ func getJwtString(ctx context.Context, v *client, kubernetesAuth *esv1beta1.Vaul
if _, err := os.Stat(serviceAccTokenPath); err != nil { if _, err := os.Stat(serviceAccTokenPath); err != nil {
return "", fmt.Errorf(errServiceAccount, err) return "", fmt.Errorf(errServiceAccount, err)
} }
jwtByte, err := ioutil.ReadFile(serviceAccTokenPath) jwtByte, err := os.ReadFile(serviceAccTokenPath)
if err != nil { if err != nil {
return "", fmt.Errorf(errServiceAccount, err) return "", fmt.Errorf(errServiceAccount, err)
} }

View file

@ -20,7 +20,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"testing" "testing"
@ -175,7 +175,7 @@ func newVaultResponse(data *vault.Secret) *vault.Response {
jsonData, _ := json.Marshal(data) jsonData, _ := json.Marshal(data)
return &vault.Response{ return &vault.Response{
Response: &http.Response{ Response: &http.Response{
Body: ioutil.NopCloser(bytes.NewReader(jsonData)), Body: io.NopCloser(bytes.NewReader(jsonData)),
}, },
} }
} }

View file

@ -209,7 +209,7 @@ func TestWebhookGetSecret(t *testing.T) {
var tc testCase var tc testCase
if err := ydec.Decode(&tc); err != nil { if err := ydec.Decode(&tc); err != nil {
if !errors.Is(err, io.EOF) { if !errors.Is(err, io.EOF) {
t.Errorf("testcase decode error %w", err) t.Errorf("testcase decode error %v", err)
} }
break break
} }

View file

@ -27,6 +27,7 @@ In: https://github.com/Azure/secrets-store-csi-driver-provider-azure/pull/332
package template package template
import ( import (
"bytes"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "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 // if ith node AuthorityKeyId is same as jth node SubjectKeyId, jth node was used
// to sign the ith certificate // 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[j].isParent = true
nodes[i].parent = nodes[j] nodes[i].parent = nodes[j]
break break