From 91fc30622c59f4e367d4817827d4ada75b05d80f Mon Sep 17 00:00:00 2001 From: Gianluca Mardente Date: Tue, 9 Sep 2025 07:24:42 +0200 Subject: [PATCH] (feat) Kustomize components support This PR introduces the `components` field to the ClusterProfile's `KustomizationRef` section, allowing users to include reusable Kustomize components in their deployments. The `components` field is a list of relative paths to Kustomize component directories. Sveltos will now read the existing kustomization.yaml file at the specified path, add the paths from the components field, and then run a Kustomize build on the complete configuration. Finally, Sveltos applies the resulting manifest to the cluster. Example usage: ```yaml apiVersion: config.projectsveltos.io/v1beta1 kind: ClusterProfile metadata: name: component-demo spec: clusterSelector: matchLabels: env: fv syncMode: Continuous kustomizationRefs: - namespace: flux-system name: flux-system kind: GitRepository path: ./import_components/overlays/community/ targetNamespace: eng components: - ../../components/external_db - ../../components/recaptcha ``` --- api/v1beta1/spec.go | 6 ++ api/v1beta1/zz_generated.deepcopy.go | 5 ++ ...fig.projectsveltos.io_clusterprofiles.yaml | 8 +++ ...ig.projectsveltos.io_clustersummaries.yaml | 8 +++ .../config.projectsveltos.io_profiles.yaml | 8 +++ controllers/handlers_kustomize.go | 66 +++++++++++++++++-- manifest/manifest.yaml | 24 +++++++ 7 files changed, 121 insertions(+), 4 deletions(-) diff --git a/api/v1beta1/spec.go b/api/v1beta1/spec.go index 93c94c21..65145020 100644 --- a/api/v1beta1/spec.go +++ b/api/v1beta1/spec.go @@ -460,6 +460,12 @@ type KustomizationRef struct { // +optional Path string `json:"path,omitempty"` + // Components is a list of paths to Kustomize components. These paths are relative to the + // `Path` field and are included in the Kustomize build to provide reusable configuration logic. + // The paths can be static or leverage Go templates for dynamic customization. + // +optional + Components []string `json:"components,omitempty"` + // Optional indicates that the referenced resource is not mandatory. // If set to true and the resource is not found, the error will be ignored, // and Sveltos will continue processing other ValueFroms. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 299db36b..a0913fa8 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -726,6 +726,11 @@ func (in *HelmUpgradeOptions) DeepCopy() *HelmUpgradeOptions { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KustomizationRef) DeepCopyInto(out *KustomizationRef) { *out = *in + if in.Components != nil { + in, out := &in.Components, &out.Components + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.Values != nil { in, out := &in.Values, &out.Values *out = make(map[string]string, len(*in)) diff --git a/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml b/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml index 6d687572..657e0527 100644 --- a/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml +++ b/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml @@ -633,6 +633,14 @@ spec: be run on those paths and the outcome will be deployed. items: properties: + components: + description: |- + Components is a list of paths to Kustomize components. These paths are relative to the + `Path` field and are included in the Kustomize build to provide reusable configuration logic. + The paths can be static or leverage Go templates for dynamic customization. + items: + type: string + type: array deploymentType: default: Remote description: |- diff --git a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml index 28e7e114..eb31d67a 100644 --- a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml +++ b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml @@ -671,6 +671,14 @@ spec: be run on those paths and the outcome will be deployed. items: properties: + components: + description: |- + Components is a list of paths to Kustomize components. These paths are relative to the + `Path` field and are included in the Kustomize build to provide reusable configuration logic. + The paths can be static or leverage Go templates for dynamic customization. + items: + type: string + type: array deploymentType: default: Remote description: |- diff --git a/config/crd/bases/config.projectsveltos.io_profiles.yaml b/config/crd/bases/config.projectsveltos.io_profiles.yaml index 22a87eb5..3009b7ac 100644 --- a/config/crd/bases/config.projectsveltos.io_profiles.yaml +++ b/config/crd/bases/config.projectsveltos.io_profiles.yaml @@ -633,6 +633,14 @@ spec: be run on those paths and the outcome will be deployed. items: properties: + components: + description: |- + Components is a list of paths to Kustomize components. These paths are relative to the + `Path` field and are included in the Kustomize build to provide reusable configuration logic. + The paths can be static or leverage Go templates for dynamic customization. + items: + type: string + type: array deploymentType: default: Remote description: |- diff --git a/controllers/handlers_kustomize.go b/controllers/handlers_kustomize.go index 5dd8eec3..e7c435ec 100644 --- a/controllers/handlers_kustomize.go +++ b/controllers/handlers_kustomize.go @@ -45,6 +45,7 @@ import ( "sigs.k8s.io/kustomize/api/resmap" kustomizetypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/filesys" + "sigs.k8s.io/yaml" configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1" "github.com/projectsveltos/addon-controller/controllers/clustercache" @@ -587,6 +588,19 @@ func deployKustomizeRef(ctx context.Context, c client.Client, remoteRestConfig * logger.V(logs.LogDebug).Info(fmt.Sprintf("using path %s", instantiatedPath)) + // New: Instantiate components paths + var instantiatedComponents []string + for _, comp := range kustomizationRef.Components { + instantiatedComp, err := instantiateTemplateValues(ctx, getManagementClusterConfig(), getManagementClusterClient(), + clusterSummary, clusterSummary.GetName(), comp, objects, nil, logger) + if err != nil { + return nil, nil, err + } + instantiatedComponents = append(instantiatedComponents, instantiatedComp) + } + + logger.V(logs.LogDebug).Info(fmt.Sprintf("using path %s with components %v", instantiatedPath, instantiatedComponents)) + // check build path exists dirPath := filepath.Join(tmpDir, instantiatedPath) _, err = os.Stat(dirPath) @@ -595,6 +609,49 @@ func deployKustomizeRef(ctx context.Context, c client.Client, remoteRestConfig * return nil, nil, err } + // Read the existing kustomization.yaml file + kustFilePath := filepath.Join(dirPath, "kustomization.yaml") + kustData, err := os.ReadFile(kustFilePath) + if err != nil { + return nil, nil, fmt.Errorf("failed to read kustomization.yaml: %w", err) + } + + // Unmarshal the YAML into a map + var existingKust map[string]interface{} + if err := yaml.Unmarshal(kustData, &existingKust); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal kustomization.yaml: %w", err) + } + + // Add the new components to the map + if len(instantiatedComponents) > 0 { + // The components field is a list, so we need to append to it + if components, ok := existingKust["components"].([]interface{}); ok { + for _, comp := range instantiatedComponents { + components = append(components, comp) + } + existingKust["components"] = components + } else { + // If the field doesn't exist or is not a list, create a new list + var newComponents []interface{} + for _, comp := range instantiatedComponents { + newComponents = append(newComponents, comp) + } + existingKust["components"] = newComponents + } + } + + // Marshal the modified map back to YAML + modifiedKustData, err := yaml.Marshal(&existingKust) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal modified kustomization: %w", err) + } + + // Write the modified content back to the file + err = os.WriteFile(kustFilePath, modifiedKustData, permission0600) + if err != nil { + return nil, nil, fmt.Errorf("failed to write modified kustomization.yaml: %w", err) + } + fs := filesys.MakeFsOnDisk() var resMap resmap.ResMap @@ -756,7 +813,7 @@ func getKustomizedResources(ctx context.Context, c client.Client, clusterSummary objectsToDeployRemotely = make([]*unstructured.Unstructured, 0, len(resources)) for i := range resources { resource := resources[i] - yaml, err := resource.AsYAML() + resourceYAML, err := resource.AsYAML() if err != nil { logger.V(logs.LogInfo).Info(fmt.Sprintf("failed to get resource YAML %v", err)) return nil, nil, nil, err @@ -767,8 +824,9 @@ func getKustomizedResources(ctx context.Context, c client.Client, clusterSummary // All objects coming from Kustomize output can be expressed as template. Those will be instantiated using // substitute values first, and the resource in the management cluster later. templateName := fmt.Sprintf("%s-substitutevalues", clusterSummary.Name) - yaml, err = instantiateResourceWithSubstituteValues(templateName, yaml, instantiatedSubstituteValues, - funcmap.HasTextTemplateAnnotation(clusterSummary.Annotations), logger) + resourceYAML, err = instantiateResourceWithSubstituteValues(templateName, resourceYAML, + instantiatedSubstituteValues, funcmap.HasTextTemplateAnnotation(clusterSummary.Annotations), + logger) if err != nil { msg := fmt.Sprintf("failed to instantiate resource with substitute values: %v", err) logger.V(logs.LogInfo).Info(msg) @@ -777,7 +835,7 @@ func getKustomizedResources(ctx context.Context, c client.Client, clusterSummary } var u *unstructured.Unstructured - u, err = k8s_utils.GetUnstructured(yaml) + u, err = k8s_utils.GetUnstructured(resourceYAML) if err != nil { logger.V(logs.LogInfo).Info(fmt.Sprintf("failed to get unstructured %v", err)) return nil, nil, nil, err diff --git a/manifest/manifest.yaml b/manifest/manifest.yaml index ede8e230..2feaddf3 100644 --- a/manifest/manifest.yaml +++ b/manifest/manifest.yaml @@ -942,6 +942,14 @@ spec: be run on those paths and the outcome will be deployed. items: properties: + components: + description: |- + Components is a list of paths to Kustomize components. These paths are relative to the + `Path` field and are included in the Kustomize build to provide reusable configuration logic. + The paths can be static or leverage Go templates for dynamic customization. + items: + type: string + type: array deploymentType: default: Remote description: |- @@ -2641,6 +2649,14 @@ spec: be run on those paths and the outcome will be deployed. items: properties: + components: + description: |- + Components is a list of paths to Kustomize components. These paths are relative to the + `Path` field and are included in the Kustomize build to provide reusable configuration logic. + The paths can be static or leverage Go templates for dynamic customization. + items: + type: string + type: array deploymentType: default: Remote description: |- @@ -3979,6 +3995,14 @@ spec: be run on those paths and the outcome will be deployed. items: properties: + components: + description: |- + Components is a list of paths to Kustomize components. These paths are relative to the + `Path` field and are included in the Kustomize build to provide reusable configuration logic. + The paths can be static or leverage Go templates for dynamic customization. + items: + type: string + type: array deploymentType: default: Remote description: |-