mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
feat: add namespace list selector to ClusterExternalSecrets (#2803)
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
This commit is contained in:
parent
0a0fd050c0
commit
7fbae000d6
7 changed files with 197 additions and 42 deletions
|
@ -33,9 +33,14 @@ type ClusterExternalSecretSpec struct {
|
|||
ExternalSecretMetadata ExternalSecretMetadata `json:"externalSecretMetadata"`
|
||||
|
||||
// The labels to select by to find the Namespaces to create the ExternalSecrets in.
|
||||
NamespaceSelector metav1.LabelSelector `json:"namespaceSelector"`
|
||||
// +optional
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
|
||||
|
||||
// The time in which the controller should reconcile it's objects and recheck namespaces for labels.
|
||||
// Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.
|
||||
// +optional
|
||||
Namespaces []string `json:"namespaces,omitempty"`
|
||||
|
||||
// The time in which the controller should reconcile its objects and recheck namespaces for labels.
|
||||
RefreshInterval *metav1.Duration `json:"refreshTime,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
@ -492,7 +492,16 @@ func (in *ClusterExternalSecretSpec) DeepCopyInto(out *ClusterExternalSecretSpec
|
|||
*out = *in
|
||||
in.ExternalSecretSpec.DeepCopyInto(&out.ExternalSecretSpec)
|
||||
in.ExternalSecretMetadata.DeepCopyInto(&out.ExternalSecretMetadata)
|
||||
in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector)
|
||||
if in.NamespaceSelector != nil {
|
||||
in, out := &in.NamespaceSelector, &out.NamespaceSelector
|
||||
*out = new(v1.LabelSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Namespaces != nil {
|
||||
in, out := &in.Namespaces, &out.Namespaces
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.RefreshInterval != nil {
|
||||
in, out := &in.RefreshInterval, &out.RefreshInterval
|
||||
*out = new(v1.Duration)
|
||||
|
|
|
@ -490,13 +490,18 @@ spec:
|
|||
type: object
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
namespaces:
|
||||
description: Choose namespaces by name. This field is ORed with anything
|
||||
that NamespaceSelector ends up choosing.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
refreshTime:
|
||||
description: The time in which the controller should reconcile it's
|
||||
description: The time in which the controller should reconcile its
|
||||
objects and recheck namespaces for labels.
|
||||
type: string
|
||||
required:
|
||||
- externalSecretSpec
|
||||
- namespaceSelector
|
||||
type: object
|
||||
status:
|
||||
description: ClusterExternalSecretStatus defines the observed state of
|
||||
|
|
|
@ -411,12 +411,16 @@ spec:
|
|||
type: object
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
namespaces:
|
||||
description: Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
refreshTime:
|
||||
description: The time in which the controller should reconcile it's objects and recheck namespaces for labels.
|
||||
description: The time in which the controller should reconcile its objects and recheck namespaces for labels.
|
||||
type: string
|
||||
required:
|
||||
- externalSecretSpec
|
||||
- namespaceSelector
|
||||
type: object
|
||||
status:
|
||||
description: ClusterExternalSecretStatus defines the observed state of ClusterExternalSecret.
|
||||
|
|
|
@ -1183,11 +1183,24 @@ Kubernetes meta/v1.LabelSelector
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The labels to select by to find the Namespaces to create the ExternalSecrets in.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>namespaces</code></br>
|
||||
<em>
|
||||
[]string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>refreshTime</code></br>
|
||||
<em>
|
||||
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
||||
|
@ -1196,7 +1209,7 @@ Kubernetes meta/v1.Duration
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>The time in which the controller should reconcile it’s objects and recheck namespaces for labels.</p>
|
||||
<p>The time in which the controller should reconcile its objects and recheck namespaces for labels.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1343,11 +1356,24 @@ Kubernetes meta/v1.LabelSelector
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The labels to select by to find the Namespaces to create the ExternalSecrets in.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>namespaces</code></br>
|
||||
<em>
|
||||
[]string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>refreshTime</code></br>
|
||||
<em>
|
||||
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
||||
|
@ -1356,7 +1382,7 @@ Kubernetes meta/v1.Duration
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>The time in which the controller should reconcile it’s objects and recheck namespaces for labels.</p>
|
||||
<p>The time in which the controller should reconcile its objects and recheck namespaces for labels.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
|
@ -96,18 +97,41 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
|||
refreshInt = clusterExternalSecret.Spec.RefreshInterval.Duration
|
||||
}
|
||||
|
||||
labelSelector, err := metav1.LabelSelectorAsSelector(&clusterExternalSecret.Spec.NamespaceSelector)
|
||||
namespaceList := v1.NamespaceList{}
|
||||
|
||||
if clusterExternalSecret.Spec.NamespaceSelector != nil {
|
||||
labelSelector, err := metav1.LabelSelectorAsSelector(clusterExternalSecret.Spec.NamespaceSelector)
|
||||
if err != nil {
|
||||
log.Error(err, errConvertLabelSelector)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
namespaceList := v1.NamespaceList{}
|
||||
err = r.List(ctx, &namespaceList, &client.ListOptions{LabelSelector: labelSelector})
|
||||
if err != nil {
|
||||
log.Error(err, errNamespaces)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(clusterExternalSecret.Spec.Namespaces) > 0 {
|
||||
var additionalNamespace []v1.Namespace
|
||||
|
||||
for _, ns := range clusterExternalSecret.Spec.Namespaces {
|
||||
namespace := &v1.Namespace{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Name: ns}, namespace); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Error(err, errNamespaces)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
additionalNamespace = append(additionalNamespace, *namespace)
|
||||
}
|
||||
|
||||
namespaceList.Items = append(namespaceList.Items, additionalNamespace...)
|
||||
}
|
||||
|
||||
esName := clusterExternalSecret.Spec.ExternalSecretName
|
||||
if esName == "" {
|
||||
|
@ -298,7 +322,8 @@ func (r *Reconciler) findObjectsForNamespace(ctx context.Context, namespace clie
|
|||
var requests []reconcile.Request
|
||||
for i := range clusterExternalSecrets.Items {
|
||||
clusterExternalSecret := &clusterExternalSecrets.Items[i]
|
||||
labelSelector, err := metav1.LabelSelectorAsSelector(&clusterExternalSecret.Spec.NamespaceSelector)
|
||||
if clusterExternalSecret.Spec.NamespaceSelector != nil {
|
||||
labelSelector, err := metav1.LabelSelectorAsSelector(clusterExternalSecret.Spec.NamespaceSelector)
|
||||
if err != nil {
|
||||
r.Log.Error(err, errConvertLabelSelector)
|
||||
return []reconcile.Request{}
|
||||
|
@ -311,6 +336,22 @@ func (r *Reconciler) findObjectsForNamespace(ctx context.Context, namespace clie
|
|||
Namespace: clusterExternalSecret.GetNamespace(),
|
||||
},
|
||||
})
|
||||
|
||||
// Prevent the object from being added twice if it happens to be listed
|
||||
// by Namespaces selector as well.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(clusterExternalSecret.Spec.Namespaces) > 0 {
|
||||
if slices.Contains(clusterExternalSecret.Spec.Namespaces, namespace.GetName()) {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: clusterExternalSecret.GetName(),
|
||||
Namespace: clusterExternalSecret.GetNamespace(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -156,7 +156,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
|
||||
}
|
||||
return *ces
|
||||
},
|
||||
expectedClusterExternalSecret: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) esv1beta1.ClusterExternalSecret {
|
||||
|
@ -195,7 +197,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
|
||||
}
|
||||
ces.Spec.ExternalSecretName = "test-es"
|
||||
ces.Spec.ExternalSecretMetadata = esv1beta1.ExternalSecretMetadata{
|
||||
Labels: map[string]string{"test-label-key1": "test-label-value1", "test-label-key2": "test-label-value2"},
|
||||
|
@ -241,7 +245,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
|
||||
}
|
||||
ces.Spec.ExternalSecretName = "old-es-name"
|
||||
return *ces
|
||||
},
|
||||
|
@ -296,7 +302,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
|
||||
}
|
||||
return *ces
|
||||
},
|
||||
beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) {
|
||||
|
@ -366,7 +374,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
|
||||
}
|
||||
|
||||
es := &esv1beta1.ExternalSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -426,7 +436,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
|
||||
}
|
||||
|
||||
es := &esv1beta1.ExternalSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -501,7 +513,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.RefreshInterval = &metav1.Duration{Duration: 100 * time.Millisecond}
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"no-longer-match-label-key": "no-longer-match-label-value"}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"no-longer-match-label-key": "no-longer-match-label-value"},
|
||||
}
|
||||
return *ces
|
||||
},
|
||||
beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) {
|
||||
|
@ -570,12 +584,14 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.RefreshInterval = &metav1.Duration{Duration: 100 * time.Millisecond}
|
||||
ces.Spec.NamespaceSelector.MatchExpressions = []metav1.LabelSelectorRequirement{
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "prefix",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"foo", "bar"}, // "baz" is excluded
|
||||
},
|
||||
},
|
||||
}
|
||||
return *ces
|
||||
},
|
||||
|
@ -628,7 +644,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": "no-namespace-matches"}
|
||||
ces.Spec.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"kubernetes.io/metadata.name": "no-namespace-matches"},
|
||||
}
|
||||
return *ces
|
||||
},
|
||||
expectedClusterExternalSecret: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) esv1beta1.ClusterExternalSecret {
|
||||
|
@ -652,6 +670,53 @@ var _ = Describe("ClusterExternalSecret controller", func() {
|
|||
expectedExternalSecrets: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) []esv1beta1.ExternalSecret {
|
||||
return []esv1beta1.ExternalSecret{}
|
||||
},
|
||||
}),
|
||||
Entry("Should be ready if namespace is selected via the namespace selector", testCase{
|
||||
namespaces: []v1.Namespace{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "not-matching-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
|
||||
ces := defaultClusterExternalSecret()
|
||||
// does-not-exists tests that we would continue on to the next and not stop if the
|
||||
// namespace hasn't been created yet.
|
||||
ces.Spec.Namespaces = []string{"does-not-exist", "not-matching-namespace"}
|
||||
return *ces
|
||||
},
|
||||
expectedClusterExternalSecret: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) esv1beta1.ClusterExternalSecret {
|
||||
return esv1beta1.ClusterExternalSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: created.Name,
|
||||
},
|
||||
Spec: created.Spec,
|
||||
Status: esv1beta1.ClusterExternalSecretStatus{
|
||||
ExternalSecretName: created.Name,
|
||||
ProvisionedNamespaces: []string{
|
||||
"not-matching-namespace",
|
||||
},
|
||||
Conditions: []esv1beta1.ClusterExternalSecretStatusCondition{
|
||||
{
|
||||
Type: esv1beta1.ClusterExternalSecretReady,
|
||||
Status: v1.ConditionTrue,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedExternalSecrets: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) []esv1beta1.ExternalSecret {
|
||||
return []esv1beta1.ExternalSecret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "not-matching-namespace",
|
||||
Name: created.Name,
|
||||
},
|
||||
Spec: created.Spec.ExternalSecretSpec,
|
||||
},
|
||||
}
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue