Skip to content
Draft
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
18 changes: 18 additions & 0 deletions src/pkg/packager/layout/assemble.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,31 @@ type AssembleSkeletonOptions struct {

// AssembleSkeleton creates a skeleton package and returns the path to the created package.
func AssembleSkeleton(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath string, opts AssembleSkeletonOptions) (*PackageLayout, error) {
l := logger.From(ctx)
pkg.Metadata.Architecture = v1alpha1.SkeletonArch

buildPath, err := utils.MakeTempDir(config.CommonOptions.TempDirectory)
if err != nil {
return nil, err
}

// Bundle package-level values the same way AssemblePackage does: merge into a single
// values.yaml at the build root, copy the schema flat, then rewrite the spec fields so
// the published zarf.yaml points at those paths for downstream importers.
if len(pkg.Values.Files) > 0 {
l.Debug("merging values files to package", "files", pkg.Values.Files)
if err = mergeAndWriteValuesFile(ctx, pkg.Values.Files, packagePath, buildPath); err != nil {
return nil, err
}
pkg.Values.Files = []string{ValuesYAML}
}
if pkg.Values.Schema != "" {
if err = copyValuesSchema(ctx, pkg.Values.Schema, packagePath, buildPath); err != nil {
return nil, err
}
pkg.Values.Schema = ValuesSchema
}

if err = createDocumentationTar(pkg, packagePath, buildPath); err != nil {
return nil, err
}
Expand Down
25 changes: 24 additions & 1 deletion src/pkg/packager/layout/layout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,25 @@ import (
goyaml "github.com/goccy/go-yaml"
"github.com/stretchr/testify/require"
"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/pkg/feature"
"github.com/zarf-dev/zarf/src/pkg/packager/layout"
"github.com/zarf-dev/zarf/src/pkg/packager/load"
"github.com/zarf-dev/zarf/src/test/testutil"
_ "modernc.org/sqlite"
)

// feature.Set is write-once across the test binary, so any feature this package's tests
// rely on is enabled here once before tests run.
func TestMain(m *testing.M) {
if err := feature.Set([]feature.Feature{
{Name: feature.Values, Enabled: true},
{Name: feature.BundleSignature, Enabled: true},
}); err != nil {
panic(err)
}
os.Exit(m.Run())
}

func TestAssembleSkeleton(t *testing.T) {
t.Parallel()

Expand All @@ -30,19 +43,29 @@ func TestAssembleSkeleton(t *testing.T) {
pkgLayout, err := layout.AssembleSkeleton(ctx, pkg, "./testdata/zarf-skeleton-package", opt)
require.NoError(t, err)

// Package-level values are merged to a single values.yaml at the skeleton root and the
// schema is copied flat — same storage format AssemblePackage uses. The published
// zarf.yaml's Values fields point at those rewritten paths.
require.Equal(t, []string{layout.ValuesYAML}, pkgLayout.Pkg.Values.Files)
require.Equal(t, layout.ValuesSchema, pkgLayout.Pkg.Values.Schema)
require.FileExists(t, filepath.Join(pkgLayout.DirPath(), layout.ValuesYAML))
require.FileExists(t, filepath.Join(pkgLayout.DirPath(), layout.ValuesSchema))

b, err := os.ReadFile(filepath.Join(pkgLayout.DirPath(), "checksums.txt"))
require.NoError(t, err)
expectedChecksum := `0fea7403536c0c0e2a2d9b235d4b3716e86eefd8e78e7b14412dd5a750b77474 components/kustomizations.tar
54f657b43323e1ebecb0758835b8d01a0113b61b7bab0f4a8156f031128d00f9 components/data-injections.tar
879bfe82d20f7bdcd60f9e876043cc4343af4177a6ee8b2660c304a5b6c70be7 components/files.tar
a94b27907f8c7f0945e81e29fd97c2c8574b80dd07ad619473cdf074686fdd31 values.yaml
bd82245bfc3c79abfa23dcf72c8099a2788c1b6073464f1ee0c6b64b9c8ef2f6 documentation.tar
c0d7fe697e6c07add12cd49a033e5803cec1f11bc311a820433bfa2df33ef539 values.schema.json
c497f1a56559ea0a9664160b32e4b377df630454ded6a3787924130c02f341a6 components/manifests.tar
fb7ebee94a4479bacddd71195030a483b0b0b96d4f73f7fcd2c2c8e0fce0c5c6 components/helm-charts.tar
`

require.Equal(t, expectedChecksum, string(b))
testutil.RequireNoBackslashInPackagePaths(t, pkgLayout.Pkg)
require.Equal(t, "20c2cf8bde902c8daad1ad9fb3cd9f06741550ac34401474500a24835cb36114", testutil.ChecksumZarfYAMLContent(t, pkgLayout.Pkg), "skeleton zarf.yaml checksum drift — package would differ across build hosts")
require.Equal(t, "a2b6fbb6f722d48f3385e3a0a090f48a6b30cf7f60af615a249862cc37301364", testutil.ChecksumZarfYAMLContent(t, pkgLayout.Pkg), "skeleton zarf.yaml checksum drift — package would differ across build hosts")
}

func writePackageToDisk(t *testing.T, pkg v1alpha1.ZarfPackage, dir string) {
Expand Down
11 changes: 2 additions & 9 deletions src/pkg/packager/layout/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"github.com/stretchr/testify/require"

"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/pkg/feature"
"github.com/zarf-dev/zarf/src/pkg/utils"
"github.com/zarf-dev/zarf/src/test/testutil"
)
Expand Down Expand Up @@ -1044,15 +1043,9 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) {
}

// TestSignPackageBundleSignatureEnabled tests signing behavior when the BundleSignature
// feature flag is enabled. This test uses feature.Set() which is write-once, so it must
// be the last signing-related test to run. It is intentionally not parallel.
// feature flag is enabled. The feature is enabled in TestMain (layout_test.go) for the
// whole package since feature.Set is write-once.
func TestSignPackageBundleSignatureEnabled(t *testing.T) {
// Enable the BundleSignature feature flag via feature.Set()
err := feature.Set([]feature.Feature{
{Name: feature.BundleSignature, Enabled: true},
})
require.NoError(t, err)

ctx := testutil.TestContext(t)

t.Run("signing produces both bundle and legacy formats", func(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"example": { "type": "string" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
example: value
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ metadata:
version: v0.0.1
documentation:
my-doc: doc.md
values:
files:
- pkg-values/values.yaml
schema: pkg-values/values.schema.json
components:
- name: helm-charts
required: true
Expand Down
85 changes: 84 additions & 1 deletion src/pkg/packager/load/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,13 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath,
"importStack", len(importStack),
)

var valuesFiles []string
variables := pkg.Variables
constants := pkg.Constants
components := []v1alpha1.ZarfComponent{}
// pkgValuesByURL memoizes per-skeleton package-scoped values dirs so multiple
// components importing the same OCI URL only materialize the top-level layers once.
pkgValuesByURL := map[string]string{}

for _, component := range pkg.Components {
if !compatibleComponent(component, arch, flavor) {
Expand All @@ -81,6 +85,9 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath,
}

var importedPkg v1alpha1.ZarfPackage
// Set when the URL branch materializes the imported skeleton's package-scoped
// values layer; used as the rebase anchor for Values.Files.
var pkgValuesPath string
if component.Import.Path != "" {
importPath := filepath.Join(pkgPath.BaseDir, component.Import.Path)
for _, sp := range importStack {
Expand Down Expand Up @@ -123,7 +130,7 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath,
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
_, err = remote.ResolveRoot(ctx)
rootDesc, err := remote.ResolveRoot(ctx)
if err != nil {
if strings.Contains(err.Error(), "no matching manifest was found in the manifest list") {
return v1alpha1.ZarfPackage{}, fmt.Errorf("package at %s exists but has not been published as a skeleton: %w", component.Import.URL, err)
Expand All @@ -140,6 +147,62 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath,
return v1alpha1.ZarfPackage{}, fmt.Errorf("package %s has unmet requirements: %w If you cannot upgrade Zarf you may skip this check with --skip-version-check. Unexpected behavior or errors may occur", component.Import.URL, err)
}
}

// Materialize the published package-scoped values layer (fixed name per
// AssembleSkeleton's contract) into a per-skeleton cache dir keyed by root
// manifest digest. Memoized per URL so multiple components importing the
// same skeleton only pull once. Schema propagation is intentionally left
// for a follow-up — see TestResolveImports/schema-parent-{empty,wins}.
if len(importedPkg.Values.Files) > 0 {
if cached, ok := pkgValuesByURL[component.Import.URL]; ok {
pkgValuesPath = cached
} else {
cache := filepath.Join(cachePath, "oci")
if err := helpers.CreateDirectory(cache, helpers.ReadWriteExecuteUser); err != nil {
return v1alpha1.ZarfPackage{}, err
}
manifest, err := remote.FetchRoot(ctx)
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
pkgDir := filepath.Join(cache, "pkgs", rootDesc.Digest.Encoded())
if err := helpers.CreateDirectory(pkgDir, helpers.ReadWriteExecuteUser); err != nil {
return v1alpha1.ZarfPackage{}, err
}
store, err := ocistore.New(cache)
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
desc := manifest.Locate(layout.ValuesYAML)
if oci.IsEmptyDescriptor(desc) {
return v1alpha1.ZarfPackage{}, fmt.Errorf("skeleton %s declares values but %q layer was not published", component.Import.URL, layout.ValuesYAML)
}
exists, err := store.Exists(ctx, desc)
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
if !exists {
if err := remote.CopyToTarget(ctx, []ocispec.Descriptor{desc}, store, remote.GetDefaultCopyOpts()); err != nil {
return v1alpha1.ZarfPackage{}, err
}
}
src := filepath.Join(cache, "blobs", "sha256", desc.Digest.Encoded())
dst := filepath.Join(pkgDir, layout.ValuesYAML)
if err := helpers.CreatePathAndCopy(src, dst); err != nil {
return v1alpha1.ZarfPackage{}, fmt.Errorf("unable to materialize %s for skeleton %s: %w", layout.ValuesYAML, component.Import.URL, err)
}
abs, err := filepath.Abs(pkgPath.BaseDir)
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
rel, err := filepath.Rel(abs, pkgDir)
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
pkgValuesPath = rel
pkgValuesByURL[component.Import.URL] = pkgValuesPath
}
}
}

name := getComponentToImportName(component)
Expand Down Expand Up @@ -182,8 +245,28 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath,
components = append(components, composed)
variables = append(variables, importedPkg.Variables...)
constants = append(constants, importedPkg.Constants...)

// pkg.Values.Files is package-scoped. URL imports rebase against the
// per-skeleton cache dir set up earlier in the URL branch; path imports
// continue to anchor at the per-component importPath as before.
valuesAnchorPath := importPath
if pkgValuesPath != "" {
valuesAnchorPath = pkgValuesPath
}
for _, v := range importedPkg.Values.Files {
valuesFiles = append(valuesFiles, makePathRelativeTo(v, valuesAnchorPath))
}
}

valuesFiles = append(valuesFiles, pkg.Values.Files...)
valuesFilesMap := map[string]bool{}
pkg.Values.Files = nil
for _, v := range valuesFiles {
if _, present := valuesFilesMap[v]; !present {
pkg.Values.Files = append(pkg.Values.Files, v)
valuesFilesMap[v] = true
}
}
pkg.Components = components

varMap := map[string]bool{}
Expand Down
114 changes: 114 additions & 0 deletions src/pkg/packager/load/import_oci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

package load_test

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/defenseunicorns/pkg/helpers/v2"
"github.com/stretchr/testify/require"
"oras.land/oras-go/v2/registry"

"github.com/zarf-dev/zarf/src/pkg/packager"
"github.com/zarf-dev/zarf/src/pkg/packager/load"
"github.com/zarf-dev/zarf/src/test/testutil"
"github.com/zarf-dev/zarf/src/types"
)

// TestResolveImportsOCISkeletonValues exercises the OCI skeleton import path: a parent
// package importing a values-bearing skeleton via oci:// must materialize the skeleton's
// merged values.yaml on disk and rewrite Values.Files to a path the consumer can read.
// The publisher-side merge happens in AssembleSkeleton; this test verifies the importer
// side picks the file up. Schema propagation is intentionally out of scope here — see
// the existing TestResolveImports/schema-parent-* fixtures and follow-up work.
func TestResolveImportsOCISkeletonValues(t *testing.T) {
ctx := testutil.TestContext(t)

tmp := t.TempDir()
cachePath := filepath.Join(tmp, "cache")
require.NoError(t, os.MkdirAll(cachePath, 0o755))

// Skeleton: declares package-level values + schema.
skeletonDir := filepath.Join(tmp, "skeleton")
require.NoError(t, os.MkdirAll(filepath.Join(skeletonDir, "vals"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(skeletonDir, "vals", "values.yaml"),
[]byte("greeting: hello\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(skeletonDir, "vals", "values.schema.json"),
[]byte(`{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"greeting":{"type":"string"}}}`+"\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(skeletonDir, "zarf.yaml"), []byte(
`kind: ZarfPackageConfig
metadata:
name: skeleton-with-values
version: 0.0.1
values:
files:
- vals/values.yaml
schema: vals/values.schema.json
components:
- name: noop
required: true
`), 0o644))

// Spin up a real in-memory OCI registry on a free port.
port, err := helpers.GetAvailablePort()
require.NoError(t, err)
registryURL := testutil.SetupInMemoryRegistry(ctx, t, port)
registryRef := registry.Reference{
Registry: registryURL,
Repository: "test",
}

publishedRef, err := packager.PublishSkeleton(ctx, skeletonDir, registryRef, packager.PublishSkeletonOptions{
CachePath: cachePath,
RemoteOptions: types.RemoteOptions{PlainHTTP: true},
})
require.NoError(t, err)

// Parent: imports the skeleton via oci://. Defines its own additional values file so we
// can also confirm parent + imported values end up in the resolved Values.Files list.
parentDir := filepath.Join(tmp, "parent")
require.NoError(t, os.MkdirAll(parentDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(parentDir, "parent-values.yaml"),
[]byte("override: parent\n"), 0o644))
parentYAML := fmt.Sprintf(`kind: ZarfPackageConfig
metadata:
name: parent
version: 0.0.1
values:
files:
- parent-values.yaml
components:
- name: noop
required: true
import:
url: oci://%s
`, publishedRef.String())
require.NoError(t, os.WriteFile(filepath.Join(parentDir, "zarf.yaml"), []byte(parentYAML), 0o644))

resolved, err := load.PackageDefinition(ctx, parentDir, load.DefinitionOptions{
CachePath: cachePath,
RemoteOptions: types.RemoteOptions{PlainHTTP: true},
})
require.NoError(t, err)

// The imported skeleton's merged values must come first (deepest-first), then the parent's.
require.Len(t, resolved.Values.Files, 2,
"expected one values file from imported skeleton plus one from parent, got %v", resolved.Values.Files)
require.Equal(t, "parent-values.yaml", resolved.Values.Files[1])

// Imported values path must resolve to a real file on disk relative to the parent's BaseDir.
importedRel := resolved.Values.Files[0]
importedAbs := filepath.Join(parentDir, importedRel)
require.FileExists(t, importedAbs,
"expected imported skeleton values to materialize on disk at %s", importedAbs)

// Schema propagation across imports is left for future work (path imports also
// don't propagate today — see TestResolveImports/schema-parent-empty). Parent
// declares none, so resolved schema stays empty.
require.Empty(t, resolved.Values.Schema,
"schema propagation is not part of this fix; parent declares none, so resolved schema must stay empty")
}
Loading
Loading