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:
parent
332977caba
commit
324c7def06
29 changed files with 2037 additions and 71 deletions
2
PROJECT
2
PROJECT
|
@ -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
|
||||||
|
|
99
apis/externalsecrets/v1beta1/clusterexternalsecret_types.go
Normal file
99
apis/externalsecrets/v1beta1/clusterexternalsecret_types.go
Normal 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"`
|
||||||
|
}
|
|
@ -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{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
12
cmd/root.go
12
cmd/root.go
|
@ -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")
|
||||||
|
|
|
@ -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: []
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
118
design/cluster-external-secret-spec.md
Normal file
118
design/cluster-external-secret-spec.md
Normal 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.
|
||||||
|
|
||||||
|
|
11
docs/api-clusterexternalsecret.md
Normal file
11
docs/api-clusterexternalsecret.md
Normal 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' %}
|
||||||
|
```
|
78
docs/snippets/full-cluster-external-secret.yaml
Normal file
78
docs/snippets/full-cluster-external-secret.yaml
Normal 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 %}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
107
pkg/controllers/clusterexternalsecret/suite_test.go
Normal file
107
pkg/controllers/clusterexternalsecret/suite_test.go
Normal 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())
|
||||||
|
})
|
65
pkg/controllers/clusterexternalsecret/util.go
Normal file
65
pkg/controllers/clusterexternalsecret/util.go
Normal 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
|
||||||
|
}
|
72
pkg/controllers/commontest/common.go
Normal file
72
pkg/controllers/commontest/common.go
Normal 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
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue