mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
Make ExternalSecret a provisioned service (#2263)
The Service Binding for Kubernetes project (servicebinding.io) is a spec to make it easier for workloads to consume services. At runtime, the ServiceBinding resource references a service resources and workload resource to connect to the service. The Secret for a service is projected into a workload resource at a well known path. Services can advertise the name of the Secret representing the service on it's status at `.status.binding.name`. Hosting the name of a Secret at this location is the Provisioned Service duck type. It has the effect of decoupling the logical consumption of a service from the physical Secret holding state. Using ServiceBindings with ExternalSecrets today requires the user to directly know and reference the Secret created by the ExternalSecret as the service reference. This PR adds the name of the Secret to the status of the ExternalSecret at a well known location where it is be discovered by a ServiceBinding. With this change, user can reference an ExternalSecret from a ServiceBinding. A ClusterRole is also added with a well known label for the ServiceBinding controller to have permission to watch ExternalSecrets and read the binding Secret. ClusterExternalSecret was not modified as ServiceBindings are limited to the scope of a single namespace. Signed-off-by: Scott Andrews <andrewssc@vmware.com>
This commit is contained in:
parent
08bb2291fe
commit
2174a67575
15 changed files with 121 additions and 1 deletions
|
@ -44,6 +44,9 @@ func newExternalSecretV1Alpha1() *ExternalSecret {
|
||||||
Message: "...why wouldn't it be?",
|
Message: "...why wouldn't it be?",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Binding: corev1.LocalObjectReference{
|
||||||
|
Name: "test-target",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Spec: ExternalSecretSpec{
|
Spec: ExternalSecretSpec{
|
||||||
SecretStoreRef: SecretStoreRef{
|
SecretStoreRef: SecretStoreRef{
|
||||||
|
@ -126,6 +129,9 @@ func newExternalSecretV1Beta1() *esv1beta1.ExternalSecret {
|
||||||
Message: "...why wouldn't it be?",
|
Message: "...why wouldn't it be?",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Binding: corev1.LocalObjectReference{
|
||||||
|
Name: "test-target",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Spec: esv1beta1.ExternalSecretSpec{
|
Spec: esv1beta1.ExternalSecretSpec{
|
||||||
SecretStoreRef: esv1beta1.SecretStoreRef{
|
SecretStoreRef: esv1beta1.SecretStoreRef{
|
||||||
|
|
|
@ -222,6 +222,9 @@ type ExternalSecretStatus struct {
|
||||||
|
|
||||||
// +optional
|
// +optional
|
||||||
Conditions []ExternalSecretStatusCondition `json:"conditions,omitempty"`
|
Conditions []ExternalSecretStatusCondition `json:"conditions,omitempty"`
|
||||||
|
|
||||||
|
// Binding represents a servicebinding.io Provisioned Service reference to the secret
|
||||||
|
Binding corev1.LocalObjectReference `json:"binding,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|
|
@ -574,6 +574,7 @@ func (in *ExternalSecretStatus) DeepCopyInto(out *ExternalSecretStatus) {
|
||||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
out.Binding = in.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretStatus.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretStatus.
|
||||||
|
|
|
@ -411,6 +411,9 @@ type ExternalSecretStatus struct {
|
||||||
|
|
||||||
// +optional
|
// +optional
|
||||||
Conditions []ExternalSecretStatusCondition `json:"conditions,omitempty"`
|
Conditions []ExternalSecretStatusCondition `json:"conditions,omitempty"`
|
||||||
|
|
||||||
|
// Binding represents a servicebinding.io Provisioned Service reference to the secret
|
||||||
|
Binding corev1.LocalObjectReference `json:"binding,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|
|
@ -915,6 +915,7 @@ func (in *ExternalSecretStatus) DeepCopyInto(out *ExternalSecretStatus) {
|
||||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
out.Binding = in.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretStatus.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretStatus.
|
||||||
|
|
|
@ -226,6 +226,16 @@ spec:
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
properties:
|
properties:
|
||||||
|
binding:
|
||||||
|
description: Binding represents a servicebinding.io Provisioned Service
|
||||||
|
reference to the secret
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||||
|
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
conditions:
|
conditions:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
@ -657,6 +667,16 @@ spec:
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
properties:
|
properties:
|
||||||
|
binding:
|
||||||
|
description: Binding represents a servicebinding.io Provisioned Service
|
||||||
|
reference to the secret
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||||
|
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
conditions:
|
conditions:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -121,6 +121,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||||
| prometheus.enabled | bool | `false` | deprecated. will be removed with 0.7.0, use serviceMonitor instead. |
|
| prometheus.enabled | bool | `false` | deprecated. will be removed with 0.7.0, use serviceMonitor instead. |
|
||||||
| prometheus.service.port | int | `8080` | deprecated. will be removed with 0.7.0, use serviceMonitor instead. |
|
| prometheus.service.port | int | `8080` | deprecated. will be removed with 0.7.0, use serviceMonitor instead. |
|
||||||
| rbac.create | bool | `true` | Specifies whether role and rolebinding resources should be created. |
|
| rbac.create | bool | `true` | Specifies whether role and rolebinding resources should be created. |
|
||||||
|
| rbac.servicebindings.create | bool | `true` | Specifies whether a clusterrole to give servicebindings read access should be created. |
|
||||||
| replicaCount | int | `1` | |
|
| replicaCount | int | `1` | |
|
||||||
| resources | object | `{}` | |
|
| resources | object | `{}` | |
|
||||||
| revisionHistoryLimit | int | `10` | Specifies the amount of historic ReplicaSets k8s should keep (see https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#clean-up-policy) |
|
| revisionHistoryLimit | int | `10` | Specifies the amount of historic ReplicaSets k8s should keep (see https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#clean-up-policy) |
|
||||||
|
|
|
@ -272,4 +272,23 @@ subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: {{ include "external-secrets.serviceAccountName" . }}
|
name: {{ include "external-secrets.serviceAccountName" . }}
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
namespace: {{ .Release.Namespace | quote }}
|
||||||
|
{{- if .Values.rbac.servicebindings.create }}
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "external-secrets.fullname" . }}-servicebindings
|
||||||
|
labels:
|
||||||
|
servicebinding.io/controller: "true"
|
||||||
|
{{- include "external-secrets.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- "external-secrets.io"
|
||||||
|
resources:
|
||||||
|
- "externalsecrets"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -80,6 +80,10 @@ rbac:
|
||||||
# -- Specifies whether role and rolebinding resources should be created.
|
# -- Specifies whether role and rolebinding resources should be created.
|
||||||
create: true
|
create: true
|
||||||
|
|
||||||
|
servicebindings:
|
||||||
|
# -- Specifies whether a clusterrole to give servicebindings read access should be created.
|
||||||
|
create: true
|
||||||
|
|
||||||
## -- Extra environment variables to add to container.
|
## -- Extra environment variables to add to container.
|
||||||
extraEnv: []
|
extraEnv: []
|
||||||
|
|
||||||
|
|
|
@ -3377,6 +3377,14 @@ spec:
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
properties:
|
properties:
|
||||||
|
binding:
|
||||||
|
description: Binding represents a servicebinding.io Provisioned Service reference to the secret
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
conditions:
|
conditions:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
@ -3751,6 +3759,14 @@ spec:
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
properties:
|
properties:
|
||||||
|
binding:
|
||||||
|
description: Binding represents a servicebinding.io Provisioned Service reference to the secret
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
conditions:
|
conditions:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -207,6 +207,9 @@ status:
|
||||||
reason: "SecretSynced"
|
reason: "SecretSynced"
|
||||||
message: "Secret was synced"
|
message: "Secret was synced"
|
||||||
lastTransitionTime: "2019-08-12T12:33:02Z"
|
lastTransitionTime: "2019-08-12T12:33:02Z"
|
||||||
|
# servicebinding.io Provisioned Service reference to the secret
|
||||||
|
binding:
|
||||||
|
name: my-secret
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -2554,6 +2554,19 @@ string
|
||||||
<em>(Optional)</em>
|
<em>(Optional)</em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>binding</code></br>
|
||||||
|
<em>
|
||||||
|
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#localobjectreference-v1-core">
|
||||||
|
Kubernetes core/v1.LocalObjectReference
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p>Binding represents a servicebinding.io Provisioned Service reference to the secret</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h3 id="external-secrets.io/v1beta1.ExternalSecretStatusCondition">ExternalSecretStatusCondition
|
<h3 id="external-secrets.io/v1beta1.ExternalSecretStatusCondition">ExternalSecretStatusCondition
|
||||||
|
|
|
@ -64,6 +64,8 @@ kubectl describe externalsecret example
|
||||||
# [...]
|
# [...]
|
||||||
Name: example
|
Name: example
|
||||||
Status:
|
Status:
|
||||||
|
Binding:
|
||||||
|
Name: secret-to-be-created
|
||||||
Conditions:
|
Conditions:
|
||||||
Last Transition Time: 2021-02-24T16:45:23Z
|
Last Transition Time: 2021-02-24T16:45:23Z
|
||||||
Message: Secret was synced
|
Message: Secret was synced
|
||||||
|
|
|
@ -287,11 +287,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
|
||||||
switch externalSecret.Spec.Target.CreationPolicy {
|
switch externalSecret.Spec.Target.CreationPolicy {
|
||||||
case esv1beta1.CreatePolicyMerge:
|
case esv1beta1.CreatePolicyMerge:
|
||||||
err = patchSecret(ctx, r.Client, r.Scheme, secret, mutationFunc, externalSecret.Name)
|
err = patchSecret(ctx, r.Client, r.Scheme, secret, mutationFunc, externalSecret.Name)
|
||||||
|
if err == nil {
|
||||||
|
externalSecret.Status.Binding = v1.LocalObjectReference{Name: secret.Name}
|
||||||
|
}
|
||||||
case esv1beta1.CreatePolicyNone:
|
case esv1beta1.CreatePolicyNone:
|
||||||
log.V(1).Info("secret creation skipped due to creationPolicy=None")
|
log.V(1).Info("secret creation skipped due to creationPolicy=None")
|
||||||
err = nil
|
err = nil
|
||||||
default:
|
default:
|
||||||
err = createOrUpdate(ctx, r.Client, secret, mutationFunc, externalSecret.Name)
|
err = createOrUpdate(ctx, r.Client, secret, mutationFunc, externalSecret.Name)
|
||||||
|
if err == nil {
|
||||||
|
externalSecret.Status.Binding = v1.LocalObjectReference{Name: secret.Name}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -269,11 +269,31 @@ var _ = Describe("ExternalSecret controller", func() {
|
||||||
syncWithoutTargetName := func(tc *testCase) {
|
syncWithoutTargetName := func(tc *testCase) {
|
||||||
tc.externalSecret.Spec.Target.Name = ""
|
tc.externalSecret.Spec.Target.Name = ""
|
||||||
tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
|
tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
|
||||||
|
|
||||||
// check secret name
|
// check secret name
|
||||||
Expect(secret.ObjectMeta.Name).To(Equal(ExternalSecretName))
|
Expect(secret.ObjectMeta.Name).To(Equal(ExternalSecretName))
|
||||||
|
|
||||||
|
// check binding secret on external secret
|
||||||
|
Expect(es.Status.Binding.Name).To(Equal(secret.ObjectMeta.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the secret name is reflected on the external secret's status as the binding secret
|
||||||
|
syncBindingSecret := func(tc *testCase) {
|
||||||
|
tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
|
||||||
|
// check binding secret on external secret
|
||||||
|
Expect(es.Status.Binding.Name).To(Equal(secret.ObjectMeta.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// their is no binding secret when a secret is not synced
|
||||||
|
skipBindingSecret := func(tc *testCase) {
|
||||||
|
tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyNone
|
||||||
|
tc.checkExternalSecret = func(es *esv1beta1.ExternalSecret) {
|
||||||
|
// check binding secret is not set
|
||||||
|
Expect(es.Status.Binding.Name).To(BeEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// labels and annotations from the Kind=ExternalSecret
|
// labels and annotations from the Kind=ExternalSecret
|
||||||
// should be copied over to the Kind=Secret
|
// should be copied over to the Kind=Secret
|
||||||
syncLabelsAnnotations := func(tc *testCase) {
|
syncLabelsAnnotations := func(tc *testCase) {
|
||||||
|
@ -1991,6 +2011,8 @@ var _ = Describe("ExternalSecret controller", func() {
|
||||||
Entry("should create proper hash annotation for the external secret", checkSecretDataHashAnnotation),
|
Entry("should create proper hash annotation for the external secret", checkSecretDataHashAnnotation),
|
||||||
Entry("should refresh when the hash annotation doesn't correspond to secret data", checkSecretDataHashAnnotationChange),
|
Entry("should refresh when the hash annotation doesn't correspond to secret data", checkSecretDataHashAnnotationChange),
|
||||||
Entry("should use external secret name if target secret name isn't defined", syncWithoutTargetName),
|
Entry("should use external secret name if target secret name isn't defined", syncWithoutTargetName),
|
||||||
|
Entry("should expose the secret as a provisioned service binding secret", syncBindingSecret),
|
||||||
|
Entry("should not expose a provisioned service when no secret is synced", skipBindingSecret),
|
||||||
Entry("should set the condition eventually", syncLabelsAnnotations),
|
Entry("should set the condition eventually", syncLabelsAnnotations),
|
||||||
Entry("should set prometheus counters", checkPrometheusCounters),
|
Entry("should set prometheus counters", checkPrometheusCounters),
|
||||||
Entry("should merge with existing secret using creationPolicy=Merge", mergeWithSecret),
|
Entry("should merge with existing secret using creationPolicy=Merge", mergeWithSecret),
|
||||||
|
|
Loading…
Reference in a new issue