diff --git a/src/api/internal/v1beta1/component.go b/src/api/internal/v1beta1/component.go index 75f69cfded..ca055e1e9e 100644 --- a/src/api/internal/v1beta1/component.go +++ b/src/api/internal/v1beta1/component.go @@ -315,6 +315,8 @@ type ZarfDataInjection struct { Target ZarfContainerTarget `json:"target"` // Compress the data before transmitting using gzip. Note: this requires support for tar/gzip locally and in the target image. Compress bool `json:"compress,omitempty"` + // [alpha] The type of data injection (default 'embedded' which bundles the data at create time). + Type string `json:"type,omitempty" jsonschema:"enum=embedded,enum=external"` } // ZarfComponentImport structure for including imported Zarf components. diff --git a/src/api/v1alpha1/component.go b/src/api/v1alpha1/component.go index ff24de1b7b..180ed99d9f 100644 --- a/src/api/v1alpha1/component.go +++ b/src/api/v1alpha1/component.go @@ -4,6 +4,13 @@ // Package v1alpha1 holds the definition of the v1alpha1 Zarf Package package v1alpha1 +const ( + // DataInjectionEmbedded is the default data injection type + DataInjectionEmbedded = "embedded" + // DataInjectionExternal is for external data injection + DataInjectionExternal = "external" +) + // ZarfComponent is the primary functional grouping of assets to deploy by Zarf. type ZarfComponent struct { // The name of the component. @@ -373,6 +380,8 @@ type ZarfDataInjection struct { Target ZarfContainerTarget `json:"target"` // Compress the data before transmitting using gzip. Note: this requires support for tar/gzip locally and in the target image. Compress bool `json:"compress,omitempty"` + // [alpha] The type of data injection (default 'embedded' which bundles the data at create time). + Type string `json:"type,omitempty" jsonschema:"enum=embedded,enum=external"` } // ZarfComponentImport structure for including imported Zarf components. diff --git a/src/cmd/crane.go b/src/cmd/crane.go index 9b3ad95ff7..f9d56ac673 100644 --- a/src/cmd/crane.go +++ b/src/cmd/crane.go @@ -371,7 +371,8 @@ func doPruneImagesForPackages(ctx context.Context, options []crane.Option, s *st digest, err := crane.Digest(transformedImageNoCheck, options...) if err != nil { - return err + l.Warn("unable to get digest for image, skipping prune check", "image", transformedImageNoCheck, "error", err.Error()) + continue } pkgImages[digest] = true } diff --git a/src/pkg/cluster/data.go b/src/pkg/cluster/data.go index ef64531831..c99280afbe 100644 --- a/src/pkg/cluster/data.go +++ b/src/pkg/cluster/data.go @@ -63,7 +63,14 @@ func (c *Cluster) HandleDataInjection(ctx context.Context, data v1alpha1.ZarfDat l.Debug("performing data injection", "target", data.Target) source := filepath.Join(dataInjectionPath, filepath.Base(data.Target.Path)) + if data.Type == v1alpha1.DataInjectionExternal { + source = dataInjectionPath + } + if helpers.InvalidPath(source) { + if data.Type == v1alpha1.DataInjectionExternal { + return fmt.Errorf("could not find the external data injection source path %s", source) + } // The path is likely invalid because of how we compose OCI components, add an index suffix to the filename source = filepath.Join(dataInjectionPath, strconv.Itoa(dataIdx), filepath.Base(data.Target.Path)) if helpers.InvalidPath(source) { diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 87fe8cc22d..a46e087fc2 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -486,17 +486,27 @@ func (d *deployer) deployComponent(ctx context.Context, pkgLayout *layout.Packag g, gCtx := errgroup.WithContext(ctx) for idx, data := range component.DataInjections { - tmpDir, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) - if err != nil { - return nil, err - } - defer func() { - err = errors.Join(err, os.RemoveAll(tmpDir)) - }() - dataInjectionsPath, err := pkgLayout.GetComponentDir(ctx, tmpDir, component.Name, layout.DataComponentDir) - if err != nil { - return nil, err + var dataInjectionsPath string + if data.Type == v1alpha1.DataInjectionExternal { + source := d.vc.ReplaceString(data.Source) + if source == "" { + return nil, fmt.Errorf("data injection source cannot be empty for external type") + } + dataInjectionsPath = source + } else { + tmpDir, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) + if err != nil { + return nil, err + } + defer func() { + err = errors.Join(err, os.RemoveAll(tmpDir)) + }() + dataInjectionsPath, err = pkgLayout.GetComponentDir(ctx, tmpDir, component.Name, layout.DataComponentDir) + if err != nil { + return nil, err + } } + g.Go(func() error { return d.c.HandleDataInjection(gCtx, data, dataInjectionsPath, idx) }) diff --git a/src/pkg/packager/layout/assemble.go b/src/pkg/packager/layout/assemble.go index afa1077d8a..bd139e0054 100644 --- a/src/pkg/packager/layout/assemble.go +++ b/src/pkg/packager/layout/assemble.go @@ -393,6 +393,10 @@ func assemblePackageComponent(ctx context.Context, component v1alpha1.ZarfCompon } for dataIdx, data := range component.DataInjections { + if data.Type == v1alpha1.DataInjectionExternal { + continue + } + rel := filepath.Join(string(DataComponentDir), strconv.Itoa(dataIdx), filepath.Base(data.Target.Path)) dst := filepath.Join(compBuildPath, rel) @@ -626,6 +630,10 @@ func assembleSkeletonComponent(ctx context.Context, component v1alpha1.ZarfCompo } for dataIdx, data := range component.DataInjections { + if data.Type == v1alpha1.DataInjectionExternal { + continue + } + rel := filepath.Join(string(DataComponentDir), strconv.Itoa(dataIdx), filepath.Base(data.Target.Path)) dst := filepath.Join(compBuildPath, rel) diff --git a/src/pkg/packager/layout/sbom.go b/src/pkg/packager/layout/sbom.go index 6918fa5eae..8b40cc85bf 100644 --- a/src/pkg/packager/layout/sbom.go +++ b/src/pkg/packager/layout/sbom.go @@ -156,6 +156,9 @@ func createFileSBOM(ctx context.Context, component v1alpha1.ZarfComponent, outpu err = errors.Join(err, os.RemoveAll(tmpDir)) }() tarPath := filepath.Join(buildPath, ComponentsDir, component.Name) + ".tar" + if _, err := os.Stat(tarPath); os.IsNotExist(err) { + return nil, nil + } err = archive.Decompress(ctx, tarPath, tmpDir, archive.DecompressOpts{}) if err != nil { return nil, err diff --git a/src/pkg/variables/templates.go b/src/pkg/variables/templates.go index 5f8c9b940c..586e7cf494 100644 --- a/src/pkg/variables/templates.go +++ b/src/pkg/variables/templates.go @@ -49,6 +49,17 @@ func (vc *VariableConfig) GetAllTemplates() map[string]*TextTemplate { return templateMap } +// ReplaceString replaces text templates in a string. +func (vc *VariableConfig) ReplaceString(text string) string { + templateMap := vc.GetAllTemplates() + for key, template := range templateMap { + if template != nil { + text = strings.ReplaceAll(text, key, template.Value) + } + } + return text +} + // ReplaceTextTemplate loads a file from a given path, replaces text in it and writes it back in place. func (vc *VariableConfig) ReplaceTextTemplate(path string) (err error) { templateRegex := fmt.Sprintf("###%s_[A-Z0-9_]+###", strings.ToUpper(vc.templatePrefix)) diff --git a/src/test/e2e/23_data_injection_external_test.go b/src/test/e2e/23_data_injection_external_test.go new file mode 100644 index 0000000000..e2bf336a74 --- /dev/null +++ b/src/test/e2e/23_data_injection_external_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExternalDataInjection(t *testing.T) { + t.Log("E2E: External Data injection") + + tmpdir := t.TempDir() + + image := "alpine:latest" + + podYaml := fmt.Sprintf(`apiVersion: v1 +kind: Pod +metadata: + name: external-data-pod + namespace: external-data-test + labels: + app: external-data-test +spec: + containers: + - name: alpine + image: %s + command: ["/bin/sh", "-c", "while true; do sleep 3600; done"] + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + emptyDir: {} +`, image) + + err := os.WriteFile(filepath.Join(tmpdir, "pod.yaml"), []byte(podYaml), 0644) + require.NoError(t, err) + + zarfYaml := fmt.Sprintf(`kind: ZarfPackageConfig +metadata: + name: external-data + version: 0.0.1 + +components: + - name: data-pod + required: true + manifests: + - name: pod + namespace: external-data-test + files: + - pod.yaml + images: + - %s + + - name: inject-data + required: true + dataInjections: + - source: "###ZARF_VAR_EXT_DATA###" + type: external + target: + namespace: external-data-test + selector: app=external-data-test + container: alpine + path: /data +`, image) + + err = os.WriteFile(filepath.Join(tmpdir, "zarf.yaml"), []byte(zarfYaml), 0644) + require.NoError(t, err) + + // Create data to inject + dataDir := filepath.Join(tmpdir, "my-data") + err = os.Mkdir(dataDir, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(dataDir, "test.txt"), []byte("hello external world"), 0644) + require.NoError(t, err) + + // Create package + stdOut, stdErr, err := e2e.Zarf(t, "package", "create", tmpdir, "-o", tmpdir, "--confirm") + require.NoError(t, err, stdOut, stdErr) + + packageName := fmt.Sprintf("zarf-package-external-data-%s-0.0.1.tar.zst", e2e.Arch) + packagePath := filepath.Join(tmpdir, packageName) + + // Deploy package with variable + stdOut, stdErr, err = e2e.Zarf(t, "package", "deploy", packagePath, "--confirm", "--set", fmt.Sprintf("EXT_DATA=%s", dataDir)) + require.NoError(t, err, stdOut, stdErr) + + // Verify injection + stdOut, stdErr, err = e2e.Kubectl(t, "-n", "external-data-test", "exec", "external-data-pod", "--", "cat", "/data/test.txt") + require.NoError(t, err, stdOut, stdErr) + require.Contains(t, stdOut, "hello external world") + + // Cleanup + stdOut, stdErr, err = e2e.Zarf(t, "package", "remove", packagePath, "--confirm") + require.NoError(t, err, stdOut, stdErr) +} + diff --git a/zarf.schema.json b/zarf.schema.json index 0d6fbade1b..144c35d3c4 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -996,6 +996,14 @@ "target": { "$ref": "#/$defs/ZarfContainerTarget", "description": "The target pod + container to inject the data into." + }, + "type": { + "description": "[alpha] The type of data injection (default 'embedded' which bundles the data at create time).", + "enum": [ + "embedded", + "external" + ], + "type": "string" } }, "required": [