diff --git a/api/v1alpha2/provider_types.go b/api/v1alpha2/provider_types.go index b015b823a..8a7a55088 100644 --- a/api/v1alpha2/provider_types.go +++ b/api/v1alpha2/provider_types.go @@ -37,6 +37,7 @@ const ( ) // ProviderSpec is the desired state of the Provider. +// +kubebuilder:validation:XValidation:rule="!(has(self.manifestPatches) && has(self.patches))",message="Cannot set both 'patches' and 'manifestPatches'" type ProviderSpec struct { // Version indicates the provider version. // +optional @@ -79,10 +80,17 @@ type ProviderSpec struct { // provider manifests. Patches are applied in the order they are specified. // The `kind` field must match the target object, and // if `apiVersion` is specified it will only be applied to matching objects. - // This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + // This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + // This will be deprecated in future releases in favor of `patches`. // +optional ManifestPatches []string `json:"manifestPatches,omitempty"` + // Patches are applied to the rendered provider manifests to customize the + // provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + // Both `patches` and `manifestPatches` cannot be set at the same time. + // +optional + Patches []*Patch `json:"patches,omitempty"` + // AdditionalDeployments is a map of additional deployments that the provider // should manage. The key is the name of the deployment and the value is the // DeploymentSpec. @@ -90,6 +98,42 @@ type ProviderSpec struct { AdditionalDeployments map[string]AdditionalDeployments `json:"additionalDeployments,omitempty"` } +// Patch defines a generic patch to be applied to provider manifests. +type Patch struct { + // Patch is content of the patch to be applied. It should be an inline yaml blob-string. + // +optional + Patch string `json:"patch,omitempty"` + // Target defines the target object to which the patch should be applied. + Target *PatchSelector `json:"target,omitempty"` +} + +type PatchSelector struct { + // Group is the API Group of the target object. + // +optional + Group string `json:"group,omitempty"` + + // Version is the API version of the target object. + // +optional + Version string `json:"version,omitempty"` + + // Kind is the kind of the target object. + // +optional + Kind string `json:"kind,omitempty"` + + // Name is the name of the target object. + // +optional + Name string `json:"name,omitempty"` + + // Namespace is the namespace of the target object. + // +optional + Namespace string `json:"namespace,omitempty"` + + // LabelSelector is a string that follows the label selection expression + // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + // +optional + LabelSelector string `json:"labelSelector,omitempty"` +} + // AdditionalDeployments defines the properties that can be enabled on the controller // manager and deployment for the provider if the provider is managing additional deployments. type AdditionalDeployments struct { diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index 0c16ea1ff..1ea4b40eb 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -895,6 +895,41 @@ func (in *OCIConfiguration) DeepCopy() *OCIConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Patch) DeepCopyInto(out *Patch) { + *out = *in + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(PatchSelector) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Patch. +func (in *Patch) DeepCopy() *Patch { + if in == nil { + return nil + } + out := new(Patch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchSelector) DeepCopyInto(out *PatchSelector) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchSelector. +func (in *PatchSelector) DeepCopy() *PatchSelector { + if in == nil { + return nil + } + out := new(PatchSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { *out = *in @@ -928,6 +963,17 @@ func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Patches != nil { + in, out := &in.Patches, &out.Patches + *out = make([]*Patch, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Patch) + (*in).DeepCopyInto(*out) + } + } + } if in.AdditionalDeployments != nil { in, out := &in.AdditionalDeployments, &out.AdditionalDeployments *out = make(map[string]AdditionalDeployments, len(*in)) diff --git a/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml index 629ec114f..5251e2c92 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_addonproviders.yaml @@ -3037,14 +3037,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: AddonProviderStatus defines the observed state of AddonProvider. properties: diff --git a/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml index bb39c15a8..8d0f41c3a 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_bootstrapproviders.yaml @@ -3037,14 +3037,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: BootstrapProviderStatus defines the observed state of BootstrapProvider. properties: diff --git a/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml index 229dbeabf..050dbdabc 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_controlplaneproviders.yaml @@ -3038,14 +3038,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: ControlPlaneProviderStatus defines the observed state of ControlPlaneProvider. diff --git a/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml index f9ed4489d..1b2c87d77 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml @@ -3037,14 +3037,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: CoreProviderStatus defines the observed state of CoreProvider. properties: diff --git a/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml index dcc5b9121..ac3fd7c0d 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_infrastructureproviders.yaml @@ -3038,14 +3038,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: InfrastructureProviderStatus defines the observed state of InfrastructureProvider. diff --git a/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml index 4758994b7..eabc7a038 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_ipamproviders.yaml @@ -3037,14 +3037,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: IPAMProviderStatus defines the observed state of IPAMProvider. properties: diff --git a/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml b/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml index a2109cd99..5e1289ec3 100644 --- a/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml +++ b/config/crd/bases/operator.cluster.x-k8s.io_runtimeextensionproviders.yaml @@ -3039,14 +3039,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: RuntimeExtensionProviderStatus defines the observed state of RuntimeExtensionProvider. diff --git a/internal/controller/component_patches.go b/internal/controller/component_patches.go index f1b4648d0..e0d353bf5 100644 --- a/internal/controller/component_patches.go +++ b/internal/controller/component_patches.go @@ -18,6 +18,7 @@ package controller import ( "context" + "errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -30,13 +31,18 @@ func applyPatches(ctx context.Context, provider operatorv1.GenericProvider) func log := ctrl.LoggerFrom(ctx) return func(objs []unstructured.Unstructured) ([]unstructured.Unstructured, error) { - if len(provider.GetSpec().ManifestPatches) == 0 { + switch { + case len(provider.GetSpec().Patches) != 0 && len(provider.GetSpec().ManifestPatches) != 0: + return objs, errors.New("cannot use both 'patches' and 'manifestPatches'; please choose one") + case len(provider.GetSpec().Patches) != 0: + log.V(5).Info("Applying generic resource patches") + return patch.ApplyGenericPatches(objs, provider.GetSpec().Patches) + case len(provider.GetSpec().ManifestPatches) != 0: + log.V(5).Info("Applying manifest resource patches") + return patch.ApplyPatches(objs, provider.GetSpec().ManifestPatches) + default: log.V(5).Info("No resource patches to apply") return objs, nil } - - log.V(5).Info("Applying resource patches") - - return patch.ApplyPatches(objs, provider.GetSpec().ManifestPatches) } } diff --git a/internal/patch/matchinfo.go b/internal/patch/matchinfo.go index bd143d571..20b86befa 100644 --- a/internal/patch/matchinfo.go +++ b/internal/patch/matchinfo.go @@ -19,6 +19,9 @@ package patch import ( "fmt" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" "sigs.k8s.io/yaml" ) @@ -42,3 +45,41 @@ func parseYAMLMatchInfo(raw []byte) (matchInfo, error) { return m, nil } + +func matchSelector(obj *unstructured.Unstructured, sel *operatorv1.PatchSelector) (bool, error) { + if sel == nil { + return true, nil + } + + gvk := obj.GroupVersionKind() + + if sel.Group != "" && sel.Group != gvk.Group { + return false, nil + } + if sel.Version != "" && sel.Version != gvk.Version { + return false, nil + } + if sel.Kind != "" && sel.Kind != gvk.Kind { + return false, nil + } + + if sel.Name != "" && sel.Name != obj.GetName() { + return false, nil + } + if sel.Namespace != "" && sel.Namespace != obj.GetNamespace() { + return false, nil + } + + if sel.LabelSelector != "" { + ls, err := labels.Parse(sel.LabelSelector) + if err != nil { + return false, fmt.Errorf("failed to parse label selector %q: %w", sel.LabelSelector, err) + } + + if !ls.Matches(labels.Set(obj.GetLabels())) { + return false, nil + } + } + + return true, nil +} diff --git a/internal/patch/mergepatch.go b/internal/patch/mergepatch.go index c8da48971..2a8236000 100644 --- a/internal/patch/mergepatch.go +++ b/internal/patch/mergepatch.go @@ -19,6 +19,9 @@ package patch import ( "fmt" + jsonpatch "github.com/evanphx/json-patch/v5" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" ) @@ -27,6 +30,16 @@ type mergePatch struct { matchInfo matchInfo } +type strategicMergePatch struct { + Patch *apiextensionsv1.JSON `json:",inline"` +} + +func NewStrategicMergePatch(patch *apiextensionsv1.JSON) Patch { + return &strategicMergePatch{ + Patch: patch, + } +} + func parseMergePatches(rawPatches []string) ([]mergePatch, error) { patches := []mergePatch{} @@ -49,3 +62,20 @@ func parseMergePatches(rawPatches []string) ([]mergePatch, error) { return patches, nil } + +func (s *strategicMergePatch) Apply(obj *unstructured.Unstructured) error { + objJSON, err := obj.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal object to JSON: %w", err) + } + + if patched, err := jsonpatch.MergePatch(objJSON, s.Patch.Raw); err == nil { + if err = obj.UnmarshalJSON(patched); err != nil { + return fmt.Errorf("failed to unmarshal patched JSON to object: %w", err) + } + + return nil + } + + return fmt.Errorf("failed to apply merge patch: %w", err) +} diff --git a/internal/patch/patch.go b/internal/patch/patch.go index 1deba7214..ec661bd48 100644 --- a/internal/patch/patch.go +++ b/internal/patch/patch.go @@ -17,13 +17,21 @@ limitations under the License. package patch import ( + "encoding/json" "fmt" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" utilyaml "sigs.k8s.io/cluster-api/util/yaml" "sigs.k8s.io/yaml" ) +// Patch defines an interface for applying patches to unstructured objects. +type Patch interface { + Apply(obj *unstructured.Unstructured) error +} + // ApplyPatches patches a list of unstructured objects with a list of patches. // Patches match if their kind and apiVersion match a document, with the exception // that if the patch does not set apiVersion it will be ignored. @@ -66,3 +74,62 @@ func ApplyPatches(toPatch []unstructured.Unstructured, patches []string) ([]unst return result, nil } + +// ApplyGenericPatches patches a list of unstructured objects with a list of patches. +// It is similar to the above function except in the fact that the list of patches could be strategic merge patch or RFC6902 json patches. +func ApplyGenericPatches(toPatches []unstructured.Unstructured, patches []*operatorv1.Patch) ([]unstructured.Unstructured, error) { + afterPatch := make([]unstructured.Unstructured, len(toPatches)) + copy(afterPatch, toPatches) + + for _, p := range patches { + patchYaml := []byte(p.Patch) + + patchJSON, err := yaml.YAMLToJSON(patchYaml) + if err != nil { + return nil, fmt.Errorf("failed to convert patch YAML to JSON: %w", err) + } + + for i := range afterPatch { + obj := &afterPatch[i] + + match, err := matchSelector(obj, p.Target) + if err != nil { + return nil, fmt.Errorf("failed to match patch selector: %w", err) + } + + if !match { + continue + } + + err = inferAndApplyPatchType(obj, patchJSON) + if err != nil { + return nil, fmt.Errorf("failed to apply patch to %s/%s: %w", obj.GetNamespace(), obj.GetName(), err) + } + } + } + + return afterPatch, nil +} + +func inferAndApplyPatchType(obj *unstructured.Unstructured, patchByte []byte) error { + var patch Patch + var rfc6902Patches []*RFC6902 + if err := json.Unmarshal(patchByte, &rfc6902Patches); err == nil { + patch = NewRFC6902Patch(rfc6902Patches) + if err := patch.Apply(obj); err != nil { + return err + } + return nil + } + + var strategicMerge apiextensionsv1.JSON + if err := json.Unmarshal(patchByte, &strategicMerge); err == nil { + patch = NewStrategicMergePatch(&strategicMerge) + if err = patch.Apply(obj); err != nil { + return fmt.Errorf("failed to apply strategic merge patch: %w", err) + } + return nil + } + + return fmt.Errorf("unable to infer patch type") +} diff --git a/internal/patch/patch_test.go b/internal/patch/patch_test.go index 79ee9e431..fd2a55698 100644 --- a/internal/patch/patch_test.go +++ b/internal/patch/patch_test.go @@ -20,6 +20,7 @@ import ( "testing" . "github.com/onsi/gomega" + operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2" utilyaml "sigs.k8s.io/cluster-api/util/yaml" ) @@ -61,6 +62,106 @@ func TestApplyPatches(t *testing.T) { } } +func TestApplyGenericPatches(t *testing.T) { + testCases := []struct { + name string + objectsToPatchYaml string + patches []*operatorv1.Patch + expectError bool + expectedOutput string + }{ + { + name: "strategic merge test", + objectsToPatchYaml: testObjectsToPatchYaml, + expectedOutput: expectedTestPatchedObjectsYaml, + patches: []*operatorv1.Patch{ + { + Patch: addServiceAccoungPatchRBAC, + Target: &operatorv1.PatchSelector{ + Group: "rbac.authorization.k8s.io", + Kind: "ClusterRoleBinding", + }, + }, + { + Patch: addLabelPatchService, + Target: &operatorv1.PatchSelector{ + Kind: "Service", + }, + }, + { + Patch: removeSelectorPatchService, + Target: &operatorv1.PatchSelector{ + Kind: "Service", + }, + }, + { + Patch: addSelectorPatchService, + Target: &operatorv1.PatchSelector{ + Kind: "Service", + }, + }, + { + Patch: changePortOnSecondService, + Target: &operatorv1.PatchSelector{ + Kind: "Service", + Name: "service-name-2", + Namespace: "namespace-name", + }, + }, + }, + }, + { + name: "rfc6902 patch test add", + objectsToPatchYaml: testObjectsToPatchYaml, + expectedOutput: expectedTestPatchedObjectsYaml, + patches: []*operatorv1.Patch{ + { + Patch: rfc6902PatchAdd, + Target: &operatorv1.PatchSelector{ + Group: "rbac.authorization.k8s.io", + Kind: "ClusterRoleBinding", + }, + }, + { + Patch: rfc6902PatchesService, + Target: &operatorv1.PatchSelector{ + Kind: "Service", + }, + }, + { + Patch: rfc6902PatchChangePortOnSecondService, + Target: &operatorv1.PatchSelector{ + Kind: "Service", + Name: "service-name-2", + Namespace: "namespace-name", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + objectToPatch, err := utilyaml.ToUnstructured([]byte(tc.objectsToPatchYaml)) + g.Expect(err).NotTo(HaveOccurred()) + + res, err := ApplyGenericPatches(objectToPatch, tc.patches) + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } + + g.Expect(err).NotTo(HaveOccurred()) + + resultYaml, err := utilyaml.FromUnstructured(res) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(string(resultYaml)).To(Equal(tc.expectedOutput)) + }) + } +} + const testObjectsToPatchYaml = `--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -191,3 +292,33 @@ spec: targetPort: webhook-server selector: test-label: test-value` + +const rfc6902PatchAdd = `--- +- op: add + path: /subjects/- + value: + kind: ServiceAccount + name: test-service-account + namespace: test-namespace +` + +const rfc6902PatchesService = `--- +- op: add + path: /metadata/labels/test-label + value: test-value +- op: remove + path: /spec/selector +- op: add + path: /spec/selector + value: + test-label: test-value +` + +const rfc6902PatchChangePortOnSecondService = `--- +- op: replace + path: /spec/ports/0/port + value: 7777 +- op: replace + path: /spec/ports/0/targetPort + value: webhook-server +` diff --git a/internal/patch/rfc6902.go b/internal/patch/rfc6902.go new file mode 100644 index 000000000..80ae39427 --- /dev/null +++ b/internal/patch/rfc6902.go @@ -0,0 +1,57 @@ +package patch + +import ( + "encoding/json" + "fmt" + + jsonpatch "github.com/evanphx/json-patch/v5" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// RFC6902 defines a single RF6902 JSON Patch as defined by the https://www.rfc-editor.org/rfc/rfc6902. +type RFC6902 struct { + Op string `json:"op"` + Path string `json:"path"` + Value *apiextensionsv1.JSON `json:"value"` + // From is an optional field used in "move" and "copy" operations. + From string `json:"from,omitempty"` +} + +type rfc6902Patch struct { + Patches []*RFC6902 `json:",inline"` +} + +func NewRFC6902Patch(patches []*RFC6902) Patch { + return &rfc6902Patch{ + Patches: patches, + } +} + +func (r *rfc6902Patch) Apply(obj *unstructured.Unstructured) error { + objJSON, err := obj.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal object to JSON: %w", err) + } + + patchJSON, err := json.Marshal(r.Patches) + if err != nil { + return fmt.Errorf("failed to marshal patch to JSON: %w", err) + } + + p, err := jsonpatch.DecodePatch(patchJSON) + if err != nil { + return fmt.Errorf("failed to decode RFC6902 patch: %w", err) + } + + mp, err := p.Apply(objJSON) + if err != nil { + return fmt.Errorf("failed to apply RFC6902 patch: %w", err) + } + + if err := obj.UnmarshalJSON(mp); err != nil { + return fmt.Errorf("failed to unmarshal patched JSON to object: %w", err) + } + + return nil +} diff --git a/test/e2e/resources/full-chart-install.yaml b/test/e2e/resources/full-chart-install.yaml index 8a35440cb..507a5e4f6 100644 --- a/test/e2e/resources/full-chart-install.yaml +++ b/test/e2e/resources/full-chart-install.yaml @@ -3062,14 +3062,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: AddonProviderStatus defines the observed state of AddonProvider. properties: @@ -6205,14 +6249,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: BootstrapProviderStatus defines the observed state of BootstrapProvider. properties: @@ -9349,14 +9437,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: ControlPlaneProviderStatus defines the observed state of ControlPlaneProvider. @@ -12493,14 +12625,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: CoreProviderStatus defines the observed state of CoreProvider. properties: @@ -15637,14 +15813,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: InfrastructureProviderStatus defines the observed state of InfrastructureProvider. @@ -18781,14 +19001,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: IPAMProviderStatus defines the observed state of IPAMProvider. properties: @@ -21926,14 +22190,58 @@ spec: provider manifests. Patches are applied in the order they are specified. The `kind` field must match the target object, and if `apiVersion` is specified it will only be applied to matching objects. - This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396 + This should be an inline yaml blob-string https://datatracker.ietf.org/doc/html/rfc7396. + This will be deprecated in future releases in favor of `patches`. items: type: string type: array + patches: + description: |- + Patches are applied to the rendered provider manifests to customize the + provider manifests. Pathces support both strategic merge patch and RFC6902 JSON patches. + Both `patches` and `manifestPatches` cannot be set at the same time. + items: + description: Patch defines a generic patch to be applied to provider + manifests. + properties: + patch: + description: Patch is content of the patch to be applied. It + should be an inline yaml blob-string. + type: string + target: + description: Target defines the target object to which the patch + should be applied. + properties: + group: + description: Group is the API Group of the target object. + type: string + kind: + description: Kind is the kind of the target object. + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + type: string + name: + description: Name is the name of the target object. + type: string + namespace: + description: Namespace is the namespace of the target object. + type: string + version: + description: Version is the API version of the target object. + type: string + type: object + type: object + type: array version: description: Version indicates the provider version. type: string type: object + x-kubernetes-validations: + - message: Cannot set both 'patches' and 'manifestPatches' + rule: '!(has(self.manifestPatches) && has(self.patches))' status: description: RuntimeExtensionProviderStatus defines the observed state of RuntimeExtensionProvider.