1
0
Fork 0
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:
Scott Andrews 2023-05-16 16:06:55 -04:00 committed by GitHub
parent 08bb2291fe
commit 2174a67575
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 121 additions and 1 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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