Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions api/v1beta1/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -861,8 +861,13 @@ type RemoteURL struct {
// Sveltos fetches the content on every reconciliation and redeploys if the
// content hash has changed.
// Supported schemes:
// "http://" or "https://" — HTTP/HTTPS endpoint returning raw YAML/JSON
// "oci://" — OCI registry artifact containing YAML manifests
// "http://" or "https://" — HTTP/HTTPS endpoint returning a raw YAML/JSON
// document, a gzip-compressed document, an
// uncompressed tar, or a gzip-compressed tar of
// .yaml/.yml/.json files
// "oci://" — OCI registry artifact whose layers are accepted
// in the same shapes: raw YAML/JSON, gzip-compressed
// YAML/JSON, uncompressed tar, or gzip-compressed tar
// +kubebuilder:validation:Pattern=`^(https?|oci)://`
URL string `json:"url"`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1223,8 +1223,13 @@ spec:
Sveltos fetches the content on every reconciliation and redeploys if the
content hash has changed.
Supported schemes:
"http://" or "https://" — HTTP/HTTPS endpoint returning raw YAML/JSON
"oci://" — OCI registry artifact containing YAML manifests
"http://" or "https://" — HTTP/HTTPS endpoint returning a raw YAML/JSON
document, a gzip-compressed document, an
uncompressed tar, or a gzip-compressed tar of
.yaml/.yml/.json files
"oci://" — OCI registry artifact whose layers are accepted
in the same shapes: raw YAML/JSON, gzip-compressed
YAML/JSON, uncompressed tar, or gzip-compressed tar
pattern: ^(https?|oci)://
type: string
required:
Expand Down
27 changes: 21 additions & 6 deletions config/crd/bases/config.projectsveltos.io_clusterpromotions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1125,8 +1125,13 @@ spec:
Sveltos fetches the content on every reconciliation and redeploys if the
content hash has changed.
Supported schemes:
"http://" or "https://" — HTTP/HTTPS endpoint returning raw YAML/JSON
"oci://" — OCI registry artifact containing YAML manifests
"http://" or "https://" — HTTP/HTTPS endpoint returning a raw YAML/JSON
document, a gzip-compressed document, an
uncompressed tar, or a gzip-compressed tar of
.yaml/.yml/.json files
"oci://" — OCI registry artifact whose layers are accepted
in the same shapes: raw YAML/JSON, gzip-compressed
YAML/JSON, uncompressed tar, or gzip-compressed tar
pattern: ^(https?|oci)://
type: string
required:
Expand Down Expand Up @@ -2571,8 +2576,13 @@ spec:
Sveltos fetches the content on every reconciliation and redeploys if the
content hash has changed.
Supported schemes:
"http://" or "https://" — HTTP/HTTPS endpoint returning raw YAML/JSON
"oci://" — OCI registry artifact containing YAML manifests
"http://" or "https://" — HTTP/HTTPS endpoint returning a raw YAML/JSON
document, a gzip-compressed document, an
uncompressed tar, or a gzip-compressed tar of
.yaml/.yml/.json files
"oci://" — OCI registry artifact whose layers are accepted
in the same shapes: raw YAML/JSON, gzip-compressed
YAML/JSON, uncompressed tar, or gzip-compressed tar
pattern: ^(https?|oci)://
type: string
required:
Expand Down Expand Up @@ -2989,8 +2999,13 @@ spec:
Sveltos fetches the content on every reconciliation and redeploys if the
content hash has changed.
Supported schemes:
"http://" or "https://" — HTTP/HTTPS endpoint returning raw YAML/JSON
"oci://" — OCI registry artifact containing YAML manifests
"http://" or "https://" — HTTP/HTTPS endpoint returning a raw YAML/JSON
document, a gzip-compressed document, an
uncompressed tar, or a gzip-compressed tar of
.yaml/.yml/.json files
"oci://" — OCI registry artifact whose layers are accepted
in the same shapes: raw YAML/JSON, gzip-compressed
YAML/JSON, uncompressed tar, or gzip-compressed tar
pattern: ^(https?|oci)://
type: string
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1262,8 +1262,13 @@ spec:
Sveltos fetches the content on every reconciliation and redeploys if the
content hash has changed.
Supported schemes:
"http://" or "https://" — HTTP/HTTPS endpoint returning raw YAML/JSON
"oci://" — OCI registry artifact containing YAML manifests
"http://" or "https://" — HTTP/HTTPS endpoint returning a raw YAML/JSON
document, a gzip-compressed document, an
uncompressed tar, or a gzip-compressed tar of
.yaml/.yml/.json files
"oci://" — OCI registry artifact whose layers are accepted
in the same shapes: raw YAML/JSON, gzip-compressed
YAML/JSON, uncompressed tar, or gzip-compressed tar
pattern: ^(https?|oci)://
type: string
required:
Expand Down
9 changes: 7 additions & 2 deletions config/crd/bases/config.projectsveltos.io_profiles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1223,8 +1223,13 @@ spec:
Sveltos fetches the content on every reconciliation and redeploys if the
content hash has changed.
Supported schemes:
"http://" or "https://" — HTTP/HTTPS endpoint returning raw YAML/JSON
"oci://" — OCI registry artifact containing YAML manifests
"http://" or "https://" — HTTP/HTTPS endpoint returning a raw YAML/JSON
document, a gzip-compressed document, an
uncompressed tar, or a gzip-compressed tar of
.yaml/.yml/.json files
"oci://" — OCI registry artifact whose layers are accepted
in the same shapes: raw YAML/JSON, gzip-compressed
YAML/JSON, uncompressed tar, or gzip-compressed tar
pattern: ^(https?|oci)://
type: string
required:
Expand Down
43 changes: 32 additions & 11 deletions controllers/handlers_kustomize.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers

import (
archivetar "archive/tar"
"bufio"
"bytes"
"compress/gzip"
"context"
Expand Down Expand Up @@ -479,14 +480,15 @@ func getHashFromKustomizationRef(ctx context.Context, c client.Client, clusterSu
}

// 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.
// RemoteURL and returns a hash covering it. It uses fetchContentForHash, which returns
// the same raw bytes fetchContentToDir extracts to disk for deployment (rather than
// fetchContent's flattened .yaml/.yml/.json-only view), so the hash changes whenever
// anything fetchContentToDir would actually deploy changes — including non-manifest
// files a kustomize build depends on (patches, generators, etc.) and directory structure.
func getHashFromRemoteKustomizeURL(ctx context.Context, remoteURL *configv1beta1.RemoteKustomizeURL,
clusterSummary *configv1beta1.ClusterSummary, logger logr.Logger) ([]byte, error) {

body, err := fetchContent(ctx, remoteURL.URL, remoteURL.SecretRef,
body, err := fetchContentForHash(ctx, remoteURL.URL, remoteURL.SecretRef,
clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName,
clusterSummary.Spec.ClusterType, logger)
if err != nil {
Expand Down Expand Up @@ -1247,6 +1249,11 @@ func deployEachKustomizeRefs(ctx context.Context, c client.Client, remoteRestCon
return localResourceReports, remoteResourceReports, err
}

// extractTarGz extracts the tar archive at src into dest, preserving directory
// structure. The archive may be gzip-compressed (the layout produced by tools
// such as `flux push artifact`) or an uncompressed tar; the gzip magic bytes
// are sniffed to tell which. macOS AppleDouble sidecar entries and PAX header
// entries are skipped (see isSkippableTarEntry) rather than extracted.
func extractTarGz(src, dest string) error {
// Open the tarball for reading
tarball, err := os.Open(src)
Expand All @@ -1255,15 +1262,25 @@ func extractTarGz(src, dest string) error {
}
defer tarball.Close()

// Create a gzip reader to decompress the tarball
gzipReader, err := gzip.NewReader(io.LimitReader(tarball, maxSize))
if err != nil {
bufReader := bufio.NewReader(io.LimitReader(tarball, maxSize))
magic, err := bufReader.Peek(len(gzipMagicBytes))
if err != nil && !errors.Is(err, io.EOF) {
return err
}
defer gzipReader.Close()

// Create a tar reader to read the uncompressed tarball
tarReader := archivetar.NewReader(gzipReader)
// tarSource yields the uncompressed tar stream: the gzip-decompressed
// reader if the archive is gzip-compressed, or bufReader itself otherwise.
tarSource := io.Reader(bufReader)
if bytes.Equal(magic, gzipMagicBytes) {
gzipReader, err := gzip.NewReader(bufReader)
if err != nil {
return err
}
defer gzipReader.Close()
tarSource = gzipReader
}

tarReader := archivetar.NewReader(tarSource)

// Iterate over each file in the tarball and extract it to the destination
for {
Expand All @@ -1275,6 +1292,10 @@ func extractTarGz(src, dest string) error {
return err
}

if isSkippableTarEntry(header.Name) {
continue
}

target := filepath.Join(dest, filepath.Clean(header.Name))
if !strings.HasPrefix(target, dest) {
return fmt.Errorf("tar archive entry %q is outside of destination directory", header.Name)
Expand Down
81 changes: 81 additions & 0 deletions controllers/handlers_kustomize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers_test

import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"fmt"
Expand Down Expand Up @@ -247,6 +248,55 @@ var _ = Describe("KustomizeRefs", func() {
_, err = os.Stat(extraFilePath)
Expect(os.IsNotExist(err)).To(BeTrue())
})

It("extractTarGz extracts an uncompressed tar archive", func() {
srcDir, err := os.MkdirTemp("", "test")
Expect(err).To(BeNil())
defer os.RemoveAll(srcDir)

src := filepath.Join(srcDir, "plain.tar")
writeTarArchive(src, false, map[string]string{
"overlay.yaml": "namePrefix: uncompressed-\n",
})

dest, err := os.MkdirTemp("", "extract")
Expect(err).To(BeNil())
defer os.RemoveAll(dest)

Expect(controllers.ExtractTarGz(src, dest)).To(Succeed())

contents, err := os.ReadFile(filepath.Join(dest, "overlay.yaml"))
Expect(err).To(BeNil())
Expect(string(contents)).To(ContainSubstring("namePrefix"))
})

It("extractTarGz skips AppleDouble and PAX header entries", func() {
srcDir, err := os.MkdirTemp("", "test")
Expect(err).To(BeNil())
defer os.RemoveAll(srcDir)

src := filepath.Join(srcDir, "with-junk.tar.gz")
writeTarArchive(src, true, map[string]string{
"kustomization.yaml": "resources:\n- deploy.yaml\n",
"._kustomization.yaml": "\x00\x05\x16\x07AppleDouble binary junk",
"PaxHeader/deploy.yaml": "not a real manifest",
})

dest, err := os.MkdirTemp("", "extract")
Expect(err).To(BeNil())
defer os.RemoveAll(dest)

Expect(controllers.ExtractTarGz(src, dest)).To(Succeed())

_, err = os.ReadFile(filepath.Join(dest, "kustomization.yaml"))
Expect(err).To(BeNil())

_, err = os.Stat(filepath.Join(dest, "._kustomization.yaml"))
Expect(os.IsNotExist(err)).To(BeTrue())

_, err = os.Stat(filepath.Join(dest, "PaxHeader", "deploy.yaml"))
Expect(os.IsNotExist(err)).To(BeTrue())
})
})

var _ = Describe("Hash methods", func() {
Expand Down Expand Up @@ -611,6 +661,37 @@ var _ = Describe("Hash methods", func() {
})
})

// writeTarArchive builds a tar archive from entries (name -> content) and writes it to
// dest. When gz is true the archive is gzip-compressed first.
func writeTarArchive(dest string, gz bool, entries map[string]string) {
var tarBuf bytes.Buffer
tw := tar.NewWriter(&tarBuf)
for name, content := range entries {
hdr := &tar.Header{
Name: name,
Typeflag: tar.TypeReg,
Mode: 0600,
Size: int64(len(content)),
}
Expect(tw.WriteHeader(hdr)).To(Succeed())
_, err := tw.Write([]byte(content))
Expect(err).To(BeNil())
}
Expect(tw.Close()).To(Succeed())

raw := tarBuf.Bytes()
if gz {
var gzBuf bytes.Buffer
gw := gzip.NewWriter(&gzBuf)
_, err := gw.Write(raw)
Expect(err).To(BeNil())
Expect(gw.Close()).To(Succeed())
raw = gzBuf.Bytes()
}

Expect(os.WriteFile(dest, raw, 0600)).To(Succeed())
}

func createTarGz(dest string) {
// Create the test directory and some test files.
err := os.MkdirAll("testdata/testdir", 0755)
Expand Down
Loading