From dd347e508afa1d7be1fdb147cc4aacf296443a1f Mon Sep 17 00:00:00 2001 From: Gianluca Mardente Date: Sat, 18 Jul 2026 16:32:32 +0200 Subject: [PATCH] feat: expose RemoteURL on KustomizationRefs This PR adds `KustomizationRef.RemoteURL` letting a `KustomizationRef` fetch its content directly from an HTTP/HTTPS endpoint or an OCI registry, without a Flux `GitRepository`/`OCIRepository`/`Bucket` or a `ConfigMap`/`Secret`. Mirrors `PolicyRef.RemoteURL` Unlike `PolicyRef.RemoteURL`'s OCI path (which flattens all `.yaml`/`.yml`/`.json` files across layers into one concatenated blob), the new path preserves the fetched content's directory structure. - `Namespace`/`Name`/`Kind` are now optional on `KustomizationRef` (required only when `RemoteURL` is unset), matching the `PolicyRef` precedent. - Hashing and periodic re-fetch interval both extended to cover `RemoteURL`-based refs, same as they already do for `PolicyRef`. ```yaml apiVersion: config.projectsveltos.io/v1beta1 kind: ClusterProfile metadata: name: kustomize-remoteurl-example spec: clusterSelector: matchLabels: env: fv kustomizationRefs: - remoteURL: url: oci://ghcr.io/gianlucam76/sveltos-oci-kustomize-test:head path: overlays/production targetNamespace: production ``` Same content also works over plain HTTPS, pointing at any tarball URL (e.g. a GitHub archive): ```yaml kustomizationRefs: - remoteURL: url: https://github.com/gianlucam76/kustomize/archive/refs/heads/main.tar.gz path: kustomize-main/helloWorldWithOverlays/overlays/production targetNamespace: production ``` --- api/v1beta1/spec.go | 55 +++- api/v1beta1/zz_generated.deepcopy.go | 30 +++ ...fig.projectsveltos.io_clusterprofiles.yaml | 59 ++++- ...g.projectsveltos.io_clusterpromotions.yaml | 59 ++++- ...ig.projectsveltos.io_clustersummaries.yaml | 59 ++++- .../config.projectsveltos.io_profiles.yaml | 59 ++++- controllers/clustersummary_controller.go | 13 +- controllers/handlers_kustomize.go | 53 +++- controllers/url_source.go | 102 +++++++- manifest/manifest.yaml | 236 ++++++++++++++++-- test/fv/kustomize_remoteurl_test.go | 154 ++++++++++++ 11 files changed, 824 insertions(+), 55 deletions(-) create mode 100644 test/fv/kustomize_remoteurl_test.go diff --git a/api/v1beta1/spec.go b/api/v1beta1/spec.go index 9e4e3478..9afbfa7a 100644 --- a/api/v1beta1/spec.go +++ b/api/v1beta1/spec.go @@ -546,24 +546,30 @@ type ProvenanceVerification struct { KeyringSecretRef corev1.SecretReference `json:"keyringSecretRef"` } +// +kubebuilder:validation:XValidation:rule="has(self.remoteURL) != has(self.kind)",message="either remoteURL or kind must be set, but not both" type KustomizationRef struct { // Namespace of the referenced resource. // For ClusterProfile namespace can be left empty. In such a case, namespace will // be implicit set to cluster's namespace. // For Profile namespace must be left empty. The Profile namespace will be used. // Namespace can be expressed as a template and instantiate using any cluster field. - Namespace string `json:"namespace"` + // Not used when RemoteURL is set. + // +optional + Namespace string `json:"namespace,omitempty"` // Name of the referenced resource. // Name can be expressed as a template and instantiate using any cluster field. - // +kubebuilder:validation:MinLength=1 - Name string `json:"name"` + // Required when RemoteURL is not set. + // +optional + Name string `json:"name,omitempty"` // Kind of the resource. Supported kinds are: // - flux GitRepository;OCIRepository;Bucket // - ConfigMap/Secret + // Required when RemoteURL is not set. // +kubebuilder:validation:Enum=GitRepository;OCIRepository;Bucket;ConfigMap;Secret - Kind string `json:"kind"` + // +optional + Kind string `json:"kind,omitempty"` // Path to the directory containing the kustomization.yaml file, or the // set of plain YAMLs a kustomization.yaml should be generated for. @@ -668,6 +674,47 @@ type KustomizationRef struct { // +kubebuilder:default:=false // +optional Force bool `json:"force,omitempty"` + + // RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + // endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + // or a ConfigMap/Secret. + // When set, Kind/Name/Namespace must be omitted. + // +optional + RemoteURL *RemoteKustomizeURL `json:"remoteURL,omitempty"` +} + +// RemoteKustomizeURL groups all fields related to fetching a Kustomize directory from a +// remote source. Unlike PolicyRef's RemoteURL, which serves a single raw YAML/JSON document, +// the content here must preserve a directory tree, since kustomize build resolves files +// referenced by relative path (bases, resources, patches, generator files, and so on). +// Supports HTTP/HTTPS endpoints and OCI registries. +type RemoteKustomizeURL struct { + // URL is the remote source serving the Kustomize directory content. + // Sveltos fetches the content on every reconciliation and redeploys if the + // content hash has changed. + // Supported schemes: + // "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + // (.tar.gz) of the Kustomize directory + // "oci://" — OCI registry artifact whose layers are extracted + // the same way, preserving the directory tree + // +kubebuilder:validation:Pattern=`^(https?|oci)://` + URL string `json:"url"` + + // Interval defines how often Sveltos re-fetches the source to detect changes. + // Defaults to 5 minutes. + // +optional + Interval *metav1.Duration `json:"interval,omitempty"` + + // SecretRef references a Secret in the management cluster containing optional + // credentials for fetching the source. Both Name and Namespace must be set, + // allowing the Secret to live in any namespace (e.g. projectsveltos) so that + // a single Secret can be shared across clusters without replication. + // Supported Secret keys: + // "token" — Bearer token (Authorization: Bearer ) + // "username"+"password" — HTTP Basic Auth or OCI registry basic auth + // "caFile" — PEM-encoded CA certificate for TLS verification + // +optional + SecretRef *corev1.SecretReference `json:"secretRef,omitempty"` } // StopMatchingBehavior indicates what will happen when Cluster stops matching diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index dd08190d..7367cd4f 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -984,6 +984,11 @@ func (in *KustomizationRef) DeepCopyInto(out *KustomizationRef) { *out = make([]ValueFrom, len(*in)) copy(*out, *in) } + if in.RemoteURL != nil { + in, out := &in.RemoteURL, &out.RemoteURL + *out = new(RemoteKustomizeURL) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KustomizationRef. @@ -1327,6 +1332,31 @@ func (in *ReleaseReport) DeepCopy() *ReleaseReport { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteKustomizeURL) DeepCopyInto(out *RemoteKustomizeURL) { + *out = *in + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(corev1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteKustomizeURL. +func (in *RemoteKustomizeURL) DeepCopy() *RemoteKustomizeURL { + if in == nil { + return nil + } + out := new(RemoteKustomizeURL) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RemoteURL) DeepCopyInto(out *RemoteURL) { *out = *in diff --git a/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml b/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml index f9e056e0..9d549c14 100644 --- a/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml +++ b/config/crd/bases/config.projectsveltos.io_clusterprofiles.yaml @@ -780,6 +780,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -791,7 +792,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -800,6 +801,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -817,6 +819,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which + the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -919,11 +969,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: diff --git a/config/crd/bases/config.projectsveltos.io_clusterpromotions.yaml b/config/crd/bases/config.projectsveltos.io_clusterpromotions.yaml index 5196a03a..7b1f7b41 100644 --- a/config/crd/bases/config.projectsveltos.io_clusterpromotions.yaml +++ b/config/crd/bases/config.projectsveltos.io_clusterpromotions.yaml @@ -682,6 +682,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -693,7 +694,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -702,6 +703,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -719,6 +721,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to + reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -821,11 +871,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: diff --git a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml index 3fa1dbc6..eefd82a9 100644 --- a/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml +++ b/config/crd/bases/config.projectsveltos.io_clustersummaries.yaml @@ -819,6 +819,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -830,7 +831,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -839,6 +840,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -856,6 +858,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to + reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -958,11 +1008,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: diff --git a/config/crd/bases/config.projectsveltos.io_profiles.yaml b/config/crd/bases/config.projectsveltos.io_profiles.yaml index 84cb4643..21b45e0a 100644 --- a/config/crd/bases/config.projectsveltos.io_profiles.yaml +++ b/config/crd/bases/config.projectsveltos.io_profiles.yaml @@ -780,6 +780,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -791,7 +792,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -800,6 +801,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -817,6 +819,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which + the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -919,11 +969,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: diff --git a/controllers/clustersummary_controller.go b/controllers/clustersummary_controller.go index 99f9db6d..27840fe2 100644 --- a/controllers/clustersummary_controller.go +++ b/controllers/clustersummary_controller.go @@ -616,12 +616,19 @@ func (r *ClusterSummaryReconciler) proceedDeployingClusterSummary(ctx context.Co return reconcile.Result{Requeue: true, RequeueAfter: dryRunRequeueAfter}, nil } - // If any PolicyRef uses a URL source, schedule a periodic re-fetch so that - // remote content changes are detected even without a Kubernetes watch event. + // If any PolicyRef or KustomizationRef uses a URL source, schedule a periodic + // re-fetch so that remote content changes are detected even without a Kubernetes + // watch event. // We deliberately do NOT call setNextReconcileTime here: the interval is a // polling floor, not a cooldown — external events should still trigger // immediate reconciliation. - if interval := minURLInterval(clusterSummaryScope.ClusterSummary.Spec.ClusterProfileSpec.PolicyRefs); interval > 0 { + policyRefInterval := minURLInterval(clusterSummaryScope.ClusterSummary.Spec.ClusterProfileSpec.PolicyRefs) + kustomizeRefInterval := minKustomizeURLInterval(clusterSummaryScope.ClusterSummary.Spec.ClusterProfileSpec.KustomizationRefs) + interval := policyRefInterval + if interval == 0 || (kustomizeRefInterval > 0 && kustomizeRefInterval < interval) { + interval = kustomizeRefInterval + } + if interval > 0 { r.setNextReconcileTime(clusterSummaryScope, interval) return reconcile.Result{RequeueAfter: interval}, nil } diff --git a/controllers/handlers_kustomize.go b/controllers/handlers_kustomize.go index cf571460..c2198901 100644 --- a/controllers/handlers_kustomize.go +++ b/controllers/handlers_kustomize.go @@ -414,6 +414,10 @@ func kustomizationHash(ctx context.Context, c client.Client, clusterSummary *con func getHashFromKustomizationRef(ctx context.Context, c client.Client, clusterSummary *configv1beta1.ClusterSummary, kustomizationRef *configv1beta1.KustomizationRef, logger logr.Logger) ([]byte, error) { + if kustomizationRef.RemoteURL != nil { + return getHashFromRemoteKustomizeURL(ctx, kustomizationRef.RemoteURL, clusterSummary, logger) + } + var result string namespace, err := libsveltostemplate.GetReferenceResourceNamespace(ctx, c, clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, kustomizationRef.Namespace, clusterSummary.Spec.ClusterType) @@ -474,6 +478,27 @@ func getHashFromKustomizationRef(ctx context.Context, c client.Client, clusterSu return []byte(result), nil } +// getHashFromRemoteKustomizeURL fetches the content referenced by a KustomizationRef's +// RemoteURL and returns a hash covering it. It reuses fetchContent (rather than +// fetchContentToDir) since only a byte stream that changes when the content changes is +// needed here; the directory structure is only relevant when the content is actually +// extracted for a kustomize build. +func getHashFromRemoteKustomizeURL(ctx context.Context, remoteURL *configv1beta1.RemoteKustomizeURL, + clusterSummary *configv1beta1.ClusterSummary, logger logr.Logger) ([]byte, error) { + + body, err := fetchContent(ctx, remoteURL.URL, remoteURL.SecretRef, + clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, + clusterSummary.Spec.ClusterType, logger) + if err != nil { + logger.V(logs.LogInfo).Info(fmt.Sprintf("failed to fetch remote Kustomize URL %s for hashing: %v", + remoteURL.URL, err)) + return nil, err + } + + hash := sha256.Sum256(body) + return []byte(hex.EncodeToString(hash[:])), nil +} + // instantiateKustomizeSubstituteValues gets all substitute values for a KustomizationRef and // instantiate those using resources in the management cluster. func instantiateKustomizeSubstituteValues(ctx context.Context, dCtx *deploymentContext, @@ -788,7 +813,9 @@ func prepareFileSystem(ctx context.Context, c client.Client, kustomizationRef *configv1beta1.KustomizationRef, clusterSummary *configv1beta1.ClusterSummary, logger logr.Logger) (string, error) { - if kustomizationRef.Kind == string(libsveltosv1beta1.ConfigMapReferencedResourceKind) { + if kustomizationRef.RemoteURL != nil { + return prepareFileSystemWithRemoteURL(ctx, kustomizationRef, clusterSummary, logger) + } else if kustomizationRef.Kind == string(libsveltosv1beta1.ConfigMapReferencedResourceKind) { return prepareFileSystemWithConfigMap(ctx, c, kustomizationRef, clusterSummary, logger) } else if kustomizationRef.Kind == string(libsveltosv1beta1.SecretReferencedResourceKind) { return prepareFileSystemWithSecret(ctx, c, kustomizationRef, clusterSummary, logger) @@ -819,6 +846,30 @@ func prepareFileSystem(ctx context.Context, c client.Client, return prepareFileSystemWithFluxSource(s, logger) } +// prepareFileSystemWithRemoteURL fetches the Kustomize directory content referenced by +// kustomizationRef.RemoteURL and extracts it to a temporary directory, preserving the +// archive's directory structure so kustomize build can resolve relative file references. +func prepareFileSystemWithRemoteURL(ctx context.Context, kustomizationRef *configv1beta1.KustomizationRef, + clusterSummary *configv1beta1.ClusterSummary, logger logr.Logger) (string, error) { + + tmpDir, err := os.MkdirTemp("", fmt.Sprintf("kustomization-%s-%s", + clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName)) + if err != nil { + return "", fmt.Errorf("tmp dir error: %w", err) + } + + err = fetchContentToDir(ctx, kustomizationRef.RemoteURL.URL, kustomizationRef.RemoteURL.SecretRef, + clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, clusterSummary.Spec.ClusterType, + tmpDir, logger) + if err != nil { + os.RemoveAll(tmpDir) + return "", err + } + + logger.V(logs.LogDebug).Info(fmt.Sprintf("fetched remote Kustomize content from %s", kustomizationRef.RemoteURL.URL)) + return tmpDir, nil +} + func prepareFileSystemWithConfigMap(ctx context.Context, c client.Client, kustomizationRef *configv1beta1.KustomizationRef, clusterSummary *configv1beta1.ClusterSummary, logger logr.Logger) (string, error) { diff --git a/controllers/url_source.go b/controllers/url_source.go index 82e79775..121d551b 100644 --- a/controllers/url_source.go +++ b/controllers/url_source.go @@ -26,6 +26,7 @@ import ( "fmt" "io" "net/http" + "os" "path/filepath" "strings" "time" @@ -157,6 +158,33 @@ func fetchOCI(ctx context.Context, rawURL string, secretRef *corev1.SecretRefere clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, logger logr.Logger) ([]byte, error) { + layers, err := pullOCIArtifactLayers(ctx, rawURL, secretRef, clusterNamespace, clusterName, clusterType, logger) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + for _, raw := range layers { + content, err := extractYAMLFromLayer(raw, rawURL, logger) + if err != nil { + return nil, err + } + buf.Write(content) + } + + return buf.Bytes(), nil +} + +// pullOCIArtifactLayers connects to the OCI registry referenced by rawURL (scheme "oci://") +// and returns the raw bytes of every layer in the artifact's manifest, in order. +// Supported Secret keys: "token" (pre-obtained bearer token), "username"+"password" (basic auth +// exchanged for a registry token), "caFile" (PEM CA for TLS verification). +// The secretRef Name and Namespace support Go templating against the target cluster. +// When Namespace is empty it defaults to clusterNamespace. +func pullOCIArtifactLayers(ctx context.Context, rawURL string, secretRef *corev1.SecretReference, + clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, + logger logr.Logger) ([][]byte, error) { + ref := strings.TrimPrefix(rawURL, "oci://") repo, err := orasremote.NewRepository(ref) @@ -232,7 +260,7 @@ func fetchOCI(ctx context.Context, rawURL string, secretRef *corev1.SecretRefere return nil, fmt.Errorf("failed to parse OCI manifest %s: %w", rawURL, err) } - var buf bytes.Buffer + layers := make([][]byte, 0, len(manifest.Layers)) for _, layer := range manifest.Layers { rc, err := repo.Fetch(ctx, layer) if err != nil { @@ -243,14 +271,10 @@ func fetchOCI(ctx context.Context, rawURL string, secretRef *corev1.SecretRefere if err != nil { return nil, fmt.Errorf("failed to read OCI layer from %s: %w", rawURL, err) } - content, err := extractYAMLFromLayer(raw, rawURL, logger) - if err != nil { - return nil, err - } - buf.Write(content) + layers = append(layers, raw) } - return buf.Bytes(), nil + return layers, nil } // extractYAMLFromLayer extracts YAML/JSON manifest bytes from a single OCI layer. @@ -354,3 +378,67 @@ func minURLInterval(refs []configv1beta1.PolicyRef) time.Duration { } return result } + +// minKustomizeURLInterval returns the shortest polling interval across all URL-based +// KustomizationRefs, using defaultURLInterval for any entry that does not specify an +// explicit interval. Returns 0 if no URL-based KustomizationRefs are present. +func minKustomizeURLInterval(refs []configv1beta1.KustomizationRef) time.Duration { + result := time.Duration(0) + for i := range refs { + if refs[i].RemoteURL == nil { + continue + } + interval := defaultURLInterval + if refs[i].RemoteURL.Interval != nil && refs[i].RemoteURL.Interval.Duration > 0 { + interval = refs[i].RemoteURL.Interval.Duration + } + if result == 0 || interval < result { + result = interval + } + } + return result +} + +// fetchContentToDir retrieves a Kustomize directory tree from a remote source and +// extracts it into destDir, preserving the relative paths of every file so that +// kustomize build can resolve files referenced by relative path (bases, resources, +// patches, generator files, and so on). +// It dispatches on the URL scheme: "oci://" routes to fetchOCIToDir; everything else +// is fetched via fetchURL and treated as a gzipped tarball (.tar.gz). +func fetchContentToDir(ctx context.Context, rawURL string, secretRef *corev1.SecretReference, + clusterNamespace, clusterName string, clusterType libsveltosv1beta1.ClusterType, + destDir string, logger logr.Logger) error { + + if strings.HasPrefix(rawURL, "oci://") { + layers, err := pullOCIArtifactLayers(ctx, rawURL, secretRef, clusterNamespace, clusterName, clusterType, logger) + if err != nil { + return err + } + for i, raw := range layers { + if err := extractTarGzBytes(raw, destDir, i); err != nil { + return fmt.Errorf("failed to extract OCI layer from %s: %w", rawURL, err) + } + } + return nil + } + + body, err := fetchURL(ctx, rawURL, secretRef, clusterNamespace, clusterName, clusterType, logger) + if err != nil { + return err + } + return extractTarGzBytes(body, destDir, 0) +} + +// extractTarGzBytes writes a gzipped tarball's raw bytes to a temporary file inside +// destDir and extracts it in place using extractTarGz, preserving the archive's +// directory structure. index disambiguates the temp file name when extracting +// multiple archives (e.g. multiple OCI layers) into the same destDir. +func extractTarGzBytes(raw []byte, destDir string, index int) error { + filePath := filepath.Join(destDir, fmt.Sprintf("remote-%d.tar.gz", index)) + if err := os.WriteFile(filePath, raw, permission0600); err != nil { + return fmt.Errorf("failed to write archive to %s: %w", filePath, err) + } + defer os.Remove(filePath) + + return extractTarGz(filePath, destDir) +} diff --git a/manifest/manifest.yaml b/manifest/manifest.yaml index 1d6ecf20..7e7d4eee 100644 --- a/manifest/manifest.yaml +++ b/manifest/manifest.yaml @@ -1089,6 +1089,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -1100,7 +1101,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -1109,6 +1110,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -1126,6 +1128,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which + the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -1228,11 +1278,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: @@ -3300,6 +3349,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -3311,7 +3361,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -3320,6 +3370,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -3337,6 +3388,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to + reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -3439,11 +3538,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: @@ -6545,6 +6643,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -6556,7 +6655,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -6565,6 +6664,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -6582,6 +6682,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to + reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -6684,11 +6832,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: @@ -8787,6 +8934,7 @@ spec: Kind of the resource. Supported kinds are: - flux GitRepository;OCIRepository;Bucket - ConfigMap/Secret + Required when RemoteURL is not set. enum: - GitRepository - OCIRepository @@ -8798,7 +8946,7 @@ spec: description: |- Name of the referenced resource. Name can be expressed as a template and instantiate using any cluster field. - minLength: 1 + Required when RemoteURL is not set. type: string namespace: description: |- @@ -8807,6 +8955,7 @@ spec: be implicit set to cluster's namespace. For Profile namespace must be left empty. The Profile namespace will be used. Namespace can be expressed as a template and instantiate using any cluster field. + Not used when RemoteURL is set. type: string optional: default: false @@ -8824,6 +8973,54 @@ spec: When expressed as templates, the values are filled in using information from resources within the management cluster before deployment (Cluster and TemplateResourceRefs) type: string + remoteURL: + description: |- + RemoteURL configures fetching the Kustomize directory content from an HTTP/HTTPS + endpoint or an OCI registry, without requiring a Flux GitRepository/OCIRepository/Bucket + or a ConfigMap/Secret. + When set, Kind/Name/Namespace must be omitted. + properties: + interval: + description: |- + Interval defines how often Sveltos re-fetches the source to detect changes. + Defaults to 5 minutes. + type: string + secretRef: + description: |- + SecretRef references a Secret in the management cluster containing optional + credentials for fetching the source. Both Name and Namespace must be set, + allowing the Secret to live in any namespace (e.g. projectsveltos) so that + a single Secret can be shared across clusters without replication. + Supported Secret keys: + "token" — Bearer token (Authorization: Bearer ) + "username"+"password" — HTTP Basic Auth or OCI registry basic auth + "caFile" — PEM-encoded CA certificate for TLS verification + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which + the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + url: + description: |- + URL is the remote source serving the Kustomize directory content. + Sveltos fetches the content on every reconciliation and redeploys if the + content hash has changed. + Supported schemes: + "http://" or "https://" — HTTP/HTTPS endpoint serving a gzipped tarball + (.tar.gz) of the Kustomize directory + "oci://" — OCI registry artifact whose layers are extracted + the same way, preserving the directory tree + pattern: ^(https?|oci):// + type: string + required: + - url + type: object skipNamespaceCreation: default: false description: |- @@ -8926,11 +9123,10 @@ spec: - name type: object type: array - required: - - kind - - name - - namespace type: object + x-kubernetes-validations: + - message: either remoteURL or kind must be set, but not both + rule: has(self.remoteURL) != has(self.kind) type: array x-kubernetes-list-type: atomic maxConsecutiveFailures: diff --git a/test/fv/kustomize_remoteurl_test.go b/test/fv/kustomize_remoteurl_test.go new file mode 100644 index 00000000..01edbd57 --- /dev/null +++ b/test/fv/kustomize_remoteurl_test.go @@ -0,0 +1,154 @@ +/* +Copyright 2026. projectsveltos.io. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fv_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + + configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1" + "github.com/projectsveltos/addon-controller/lib/clusterops" + libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1" +) + +// Both specs below deploy the same content: helloWorldWithOverlays/overlays/production from +// https://github.com/gianlucam76/kustomize, which references helloWorldWithOverlays/base via a +// relative path (../../base). That only works if the fetched archive's directory structure is +// preserved rather than flattened, which is what these two specs are here to verify. +var _ = Describe("Kustomize RemoteURL", func() { + const ( + namePrefix = "kustomize-remoteurl-" + ociTestURL = "oci://ghcr.io/gianlucam76/sveltos-oci-kustomize-test:head" + httpsTestURL = "https://github.com/gianlucam76/kustomize/archive/refs/heads/main.tar.gz" + ) + + verifyKustomizeRemoteURLDeployment := func(remoteURL *configv1beta1.RemoteKustomizeURL, path string) { + Byf("Create a ClusterProfile matching Cluster %s/%s", kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName()) + clusterProfile := getClusterProfile(namePrefix, map[string]string{key: value}) + clusterProfile.Spec.SyncMode = configv1beta1.SyncModeContinuous + Expect(k8sClient.Create(context.TODO(), clusterProfile)).To(Succeed()) + + verifyClusterProfileMatches(clusterProfile) + + verifyClusterSummary(clusterops.ClusterProfileLabelName, clusterProfile.Name, &clusterProfile.Spec, + kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName(), getClusterType()) + + targetNamespace := randomString() + + Byf("Update ClusterProfile %s to reference RemoteURL %s", clusterProfile.Name, remoteURL.URL) + currentClusterProfile := &configv1beta1.ClusterProfile{} + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + Expect(k8sClient.Get(context.TODO(), + types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed()) + currentClusterProfile.Spec.KustomizationRefs = []configv1beta1.KustomizationRef{ + { + RemoteURL: remoteURL, + Path: path, + TargetNamespace: targetNamespace, + }, + } + return k8sClient.Update(context.TODO(), currentClusterProfile) + }) + Expect(err).To(BeNil()) + + Expect(k8sClient.Get(context.TODO(), + types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed()) + + clusterSummary := verifyClusterSummary(clusterops.ClusterProfileLabelName, + currentClusterProfile.Name, ¤tClusterProfile.Spec, + kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName(), getClusterType()) + + Byf("Getting client to access the workload cluster") + workloadClient, err := getKindWorkloadClusterKubeconfig() + Expect(err).To(BeNil()) + Expect(workloadClient).ToNot(BeNil()) + + Byf("Verifying Service %s is created in namespace %s", kustomizeProdServiceName, targetNamespace) + Eventually(func() bool { + currentService := &corev1.Service{} + err = workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: targetNamespace, Name: kustomizeProdServiceName}, currentService) + return err == nil + }, timeout, pollingInterval).Should(BeTrue()) + + Byf("Verifying Deployment %s is created in namespace %s", kustomizeProdDeployName, targetNamespace) + Eventually(func() bool { + currentDeployment := &appsv1.Deployment{} + err = workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: targetNamespace, Name: kustomizeProdDeployName}, currentDeployment) + return err == nil + }, timeout, pollingInterval).Should(BeTrue()) + + Byf("Verifying ConfigMap %s is created in namespace %s", kustomizeProdMapName, targetNamespace) + Eventually(func() bool { + currentConfigMap := &corev1.ConfigMap{} + err = workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: targetNamespace, Name: kustomizeProdMapName}, currentConfigMap) + return err == nil + }, timeout, pollingInterval).Should(BeTrue()) + + Byf("Verifying ClusterSummary %s status is set to Provisioned for kustomize", clusterSummary.Name) + verifyFeatureStatusIsProvisioned(kindWorkloadCluster.GetNamespace(), clusterSummary.Name, libsveltosv1beta1.FeatureKustomize) + + policies := []policy{ + {kind: kindService, name: kustomizeProdServiceName, namespace: targetNamespace, group: ""}, + {kind: kindConfigMap, name: kustomizeProdMapName, namespace: targetNamespace, group: ""}, + {kind: kindDeployment, name: kustomizeProdDeployName, namespace: targetNamespace, group: appsGroupName}, + } + verifyClusterConfiguration(configv1beta1.ClusterProfileKind, clusterProfile.Name, + clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, libsveltosv1beta1.FeatureKustomize, + policies, nil) + + Byf("Removing RemoteURL reference from ClusterProfile %s", clusterProfile.Name) + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + Expect(k8sClient.Get(context.TODO(), + types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed()) + currentClusterProfile.Spec.KustomizationRefs = []configv1beta1.KustomizationRef{} + return k8sClient.Update(context.TODO(), currentClusterProfile) + }) + Expect(err).To(BeNil()) + + Byf("Verifying Deployment %s is removed from namespace %s", kustomizeProdDeployName, targetNamespace) + Eventually(func() bool { + currentDeployment := &appsv1.Deployment{} + err = workloadClient.Get(context.TODO(), + types.NamespacedName{Namespace: targetNamespace, Name: kustomizeProdDeployName}, currentDeployment) + return err != nil && apierrors.IsNotFound(err) + }, timeout, pollingInterval).Should(BeTrue()) + + deleteClusterProfile(clusterProfile) + } + + It("Deploys Kustomize resources from an OCI artifact, preserving directory structure", + Label("FV", "PULLMODE", "EXTENDED"), func() { + verifyKustomizeRemoteURLDeployment(&configv1beta1.RemoteKustomizeURL{URL: ociTestURL}, "overlays/production") + }) + + It("Deploys Kustomize resources from an HTTPS tarball, preserving directory structure", + Label("FV", "PULLMODE", "EXTENDED"), func() { + verifyKustomizeRemoteURLDeployment(&configv1beta1.RemoteKustomizeURL{URL: httpsTestURL}, + "kustomize-main/helloWorldWithOverlays/overlays/production") + }) +})