diff --git a/api/v2beta1/reference_types.go b/api/v2beta1/reference_types.go index d948194b9..2620dfc77 100644 --- a/api/v2beta1/reference_types.go +++ b/api/v2beta1/reference_types.go @@ -50,13 +50,21 @@ type ValuesReference struct { // +required Kind string `json:"kind"` - // Name of the values referent. Should reside in the same namespace as the - // referring resource. + // Name of the values referent. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 // +required Name string `json:"name"` + // Namespace of the values referent. If not present, then the namespace of + // the referring resource will be used. If present, the referred resource + // must have an approriate `helm.toolkit.fluxcd.io/share-with` annotation value, + // which contains the namespace of the referring resource. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +optional + Namespace string `json:"namespace,omitempty"` + // ValuesKey is the data key where the values.yaml or a specific value can be // found at. Defaults to 'values.yaml'. // +optional diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index 51e9b3815..f6b4f099b 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -525,8 +525,16 @@ spec: - ConfigMap type: string name: - description: Name of the values referent. Should reside in the - same namespace as the referring resource. + description: Name of the values referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: Namespace of the values referent. If not present, + then the namespace of the referring resource will be used. + If present, the referred resource must have an approriate + `helm.toolkit.fluxcd.io/share-with` annotation value, which + contains the namespace of the referring resource. maxLength: 253 minLength: 1 type: string diff --git a/controllers/helmrelease_controller.go b/controllers/helmrelease_controller.go index 8031a3e0e..277d885a8 100644 --- a/controllers/helmrelease_controller.go +++ b/controllers/helmrelease_controller.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "encoding/json" "errors" "fmt" "strings" @@ -38,6 +39,7 @@ import ( "k8s.io/client-go/rest" kuberecorder "k8s.io/client-go/tools/record" "k8s.io/client-go/tools/reference" + "k8s.io/kubectl/pkg/util/slice" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -499,6 +501,57 @@ func (r *HelmReleaseReconciler) getServiceAccountToken(ctx context.Context, hr v return token, nil } +const ( + // Annotation on resources to explicitly share them with other + // namespaces. The annotation must be either an array of strings, containing the + // names of the namespaces with which the resource is shared or a string with + // the wildcard value "*" to share the resource with all namesapces. + shareWithAnnotation = "helm.toolkit.fluxcd.io/share-with" +) + +// Check if a resource is shared with a given namespace, according to the following rules: +// +// 1. If resource is in the same namespace, it is shared by default and can be accessed. +// 2. If resource is in a different namespace and has a "helm.toolkit.fluxcd.io/share-with" +// annotation of type string with the wildcard value "*", then it is shared and +// can be accessed. +// 3. If resource is in a different namespace and has a "helm.toolkit.fluxcd.io/share-with" +// annotation (an array of namespace names) which contains the namespace, it is +// shared and can be accessed. +// 4. If non of the above rules applies, the resource is not shared and cannot be +// accessed. +func isSharedWith(kind string, resource *metav1.ObjectMeta, namespace string) error { + if resource != nil { + if resource.Namespace == namespace { + // Rule 1. + return nil + } + if resource.Annotations != nil { + shareWithString := strings.TrimSpace(resource.Annotations[shareWithAnnotation]) + if shareWithString == "*" { + // Rule 2. + return nil + } + if shareWithString != "" { + var shareWith []string = make([]string, 0) + if err := json.Unmarshal([]byte(shareWithString), &shareWith); err != nil { + return fmt.Errorf( + "%s '%s/%s' has invalid %s annotation value, expected string or array of string got '%s': %w", + kind, resource.Namespace, resource.Name, shareWithAnnotation, shareWithString, err) + } + if slice.ContainsString(shareWith, namespace, nil) { + // Rule 3. + return nil + } + } + } + } + // Rule 4. + return fmt.Errorf( + "%s '%s/%s' is not shared with namespace '%s' (change its '%s' annotation to enable sharing)", + kind, resource.Namespace, resource.Name, namespace, shareWithAnnotation) +} + // composeValues attempts to resolve all v2beta1.ValuesReference resources // and merges them as defined. Referenced resources are only retrieved once // to ensure a single version is taken into account during the merge. @@ -509,7 +562,11 @@ func (r *HelmReleaseReconciler) composeValues(ctx context.Context, hr v2.HelmRel secrets := make(map[string]*corev1.Secret) for _, v := range hr.Spec.ValuesFrom { - namespacedName := types.NamespacedName{Namespace: hr.Namespace, Name: v.Name} + namespace := v.Namespace + if namespace == "" { + namespace = hr.Namespace + } + namespacedName := types.NamespacedName{Namespace: namespace, Name: v.Name} var valuesData []byte switch v.Kind { @@ -531,7 +588,15 @@ func (r *HelmReleaseReconciler) composeValues(ctx context.Context, hr v2.HelmRel } return nil, err } - configMaps[namespacedName.String()] = resource + if err := isSharedWith(resource.Kind, &resource.ObjectMeta, hr.Namespace); err != nil { + if !v.Optional { + return nil, err + } + (logr.FromContext(ctx)).Error(err, "Found optional %s '%s', but %s", v.Kind, namespacedName, err) + resource = nil + } else { + configMaps[namespacedName.String()] = resource + } } if resource == nil { if v.Optional { @@ -563,7 +628,15 @@ func (r *HelmReleaseReconciler) composeValues(ctx context.Context, hr v2.HelmRel } return nil, err } - secrets[namespacedName.String()] = resource + if err := isSharedWith(resource.Kind, &resource.ObjectMeta, hr.Namespace); err != nil { + if !v.Optional { + return nil, err + } + (logr.FromContext(ctx)).Error(err, "Found optional %s '%s', but %s", v.Kind, namespacedName, err) + resource = nil + } else { + secrets[namespacedName.String()] = resource + } } if resource == nil { if v.Optional { diff --git a/docs/api/helmrelease.md b/docs/api/helmrelease.md index 2b4d52fca..fe057c7d6 100644 --- a/docs/api/helmrelease.md +++ b/docs/api/helmrelease.md @@ -1877,8 +1877,22 @@ string -

Name of the values referent. Should reside in the same namespace as the -referring resource.

+

Name of the values referent.

+ + + + +namespace
+ +string + + + +(Optional) +

Namespace of the values referent. If not present, then the namespace of +the referring resource will be used. If present, the referred resource +must have an approriate helm.toolkit.fluxcd.io/share-with annotation value, +which contains the namespace of the referring resource.

diff --git a/docs/spec/v2beta1/helmreleases.md b/docs/spec/v2beta1/helmreleases.md index d7bd16a56..1c0870d17 100644 --- a/docs/spec/v2beta1/helmreleases.md +++ b/docs/spec/v2beta1/helmreleases.md @@ -427,13 +427,21 @@ type ValuesReference struct { // +required Kind string `json:"kind"` - // Name of the values referent. Should reside in the same namespace as the - // referring resource. + // Name of the values referent. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 // +required Name string `json:"name"` + // Namespace of the values referent. If not present, then the namespace of + // the referring resource will be used. If present, the referred resource + // must have an approriate `helm.toolkit.fluxcd.io/share-with` annotation value, + // which contains the namespace of the referring resource. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +optional + Namespace string `json:"namespace,omitempty"` + // ValuesKey is the data key where the values.yaml or a specific value can be // found at. Defaults to 'values.yaml'. // +optional @@ -644,7 +652,8 @@ spec: name: prod-env-values valuesKey: values-prod.yaml - kind: Secret - name: prod-tls-values + name: prod-tls-values + namespace: other-namespace valuesKey: crt targetPath: tls.crt optional: true @@ -653,8 +662,9 @@ spec: The definition of the listed keys for items in `spec.valuesFrom` is as follows: - `kind`: Kind of the values referent (`ConfigMap` or `Secret`). -- `name`: Name of the values referent, in the same namespace as the - `HelmRelease`. +- `name`: Name of the values referent. +- `namespace` _(Optional)_: Namespace of the values referent. Defaults to + the namespace of the `HelmRelease`. - `valuesKey` _(Optional)_: The data key where the values.yaml or a specific value can be found. Defaults to `values.yaml` when omitted. - `targetPath` _(Optional)_: The YAML dot notation path at which the diff --git a/go.mod b/go.mod index 083c3c2f8..7d3718e4b 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( k8s.io/apimachinery v0.20.2 k8s.io/cli-runtime v0.20.2 k8s.io/client-go v0.20.2 + k8s.io/kubectl v0.20.1 rsc.io/letsencrypt v0.0.3 // indirect sigs.k8s.io/controller-runtime v0.8.0 sigs.k8s.io/kustomize/api v0.7.2