Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ provider "uds" {
- `force_helm_ssa_conflicts` (Boolean) Force Helm to take ownership of conflicting fields during Server-Side Apply operations during package deployment. Use when external tools (kubectl, HPAs, etc.) have modified resources. Defaults to `false`. Can also be configured with the `UDS_FORCE_HELM_SSA_CONFLICTS` environment variable.
- `insecure_force_http` (Boolean) Allow plain HTTP for OCI package sources and force plain HTTP for external Zarf registry pushes. Package sources continue to use HTTPS when available, while Zarf-managed registries use the transport recorded in cluster state. Defaults to `false`. Can also be configured with the `UDS_INSECURE_FORCE_HTTP` environment variable.
- `insecure_skip_tls_verification` (Boolean) Skip TLS certificate verification for HTTPS package sources and external Zarf registry pushes. Zarf-managed mTLS registries continue to use their managed trust configuration. Defaults to `false`. Can also be configured with the `UDS_INSECURE_SKIP_TLS_VERIFICATION` environment variable.
- `validate_packages_on_plan` (Boolean) Whether to validate UDS packages during planning. When enabled, the provider may load packages during plan to catch package-dependent configuration errors early, such as invalid optional component names or signature verification failures. Disable this to avoid plan-time package downloads or expensive validation. These checks are still enforced during apply. Defaults to `true`. Can also be configured with the `UDS_VALIDATE_PACKAGES_ON_PLAN` environment variable.
- `validate_packages_on_plan` (Boolean) Whether to validate UDS packages and resolve source digests during planning. When enabled, the provider detects changes behind mutable source references and may load packages to catch package-dependent configuration errors early, such as invalid optional component names or signature verification failures. Disabling this avoids plan-time source access, but changes behind an unchanged source reference cannot independently trigger an update. Validation and digest recording still occur when another change causes apply. Defaults to `true`. Can also be configured with the `UDS_VALIDATE_PACKAGES_ON_PLAN` environment variable.
- `zarf_cache_path` (String) Filesystem path to the local Zarf cache directory. Defaults to `~/.zarf-cache`. Can also be configured with the `UDS_ZARF_CACHE_PATH` environment variable.
24 changes: 24 additions & 0 deletions docs/resources/package.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,29 @@ resource "uds_package" "init_with_git_server" {
}
```

### Mutable OCI Tags

All OCI tags are treated as mutable. During planning, the provider resolves the tag with the same registry credentials and transport settings used for package loading. A changed manifest digest produces an in-place redeployment even when the source string and package version have not changed. Apply loads the immutable digest selected by the plan and rejects a package whose content changed after planning.

```terraform
resource "uds_package" "dos_games" {
source = "oci://ghcr.io/zarf-dev/packages/dos-games:1.3.0"
}

output "dos_games_source_digest" {
description = "Immutable package digest resolved from the mutable OCI tag."
value = uds_package.dos_games.source_digest
}
```

## Source Digest Lifecycle

`source_digest` is the resolved identity of the desired package source during planning and the package actually deployed after apply or refresh. `metadata.digest` is the digest recorded in the deployed Zarf package state in the cluster. The values agree after a successful, refreshed deployment, but they have distinct lifecycle roles so the configured `source` remains exactly as authored.

By default, planning resolves every OCI reference, including a digest-pinned reference, by contacting its registry. Tagged OCI resolution reads manifest metadata rather than downloading the complete package and uses the same registry authentication, plain-HTTP negotiation, and TLS settings as package loading. Setting `validate_packages_on_plan = false` skips both package validation and source digest resolution during planning. In that mode, a change behind an unchanged source reference cannot independently trigger an update; when another configuration change causes apply, the provider loads the current source and records its digest without plan-time pinning.

When plan validation is enabled, local tarball and split-package sources are hashed during planning with Zarf's package digest implementation. They can be planned offline, but the referenced files must be available and unchanged through apply. OCI planning requires registry access; if the registry is unavailable, planning fails on `source` instead of retaining a stale digest. A new plan is required if any source changes between plan and apply.

<!-- schema generated by tfplugindocs -->
## Schema

Expand Down Expand Up @@ -169,6 +192,7 @@ resource "uds_package" "init_with_git_server" {
- `metadata` (Attributes) Metadata retrieved from the UDS package (zarf.yaml). (see [below for nested schema](#nestedatt--metadata))
- `name` (String) Name of the UDS Package.
- `set_variables` (Map of String, Sensitive) Computed map of zarf variables set for this package.
- `source_digest` (String) Resolved digest of the desired package source during planning and the package actually deployed after apply or refresh. A digest change triggers an in-place redeployment without changing `source`.
- `version` (String) Version of the deployed UDS package.

<a id="nestedblock--component"></a>
Expand Down
11 changes: 11 additions & 0 deletions examples/resources/uds_package/resource-mutable-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2026 Defense Unicorns
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial

resource "uds_package" "dos_games" {
source = "oci://ghcr.io/zarf-dev/packages/dos-games:1.3.0"
}

output "dos_games_source_digest" {
description = "Immutable package digest resolved from the mutable OCI tag."
value = uds_package.dos_games.source_digest
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ tool (
require (
github.com/defenseunicorns/pkg/helpers/v2 v2.0.4
github.com/goccy/go-yaml v1.19.2
github.com/google/go-containerregistry v0.22.0
github.com/hashicorp/terraform-plugin-framework v1.19.0
github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0
Expand Down Expand Up @@ -306,7 +307,6 @@ require (
github.com/google/certificate-transparency-go v1.3.3 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.22.0 // indirect
github.com/google/go-github/v88 v88.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/licensecheck v0.3.1 // indirect
Expand Down
14 changes: 14 additions & 0 deletions internal/packager/zarf_packager.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,19 @@ type Packager interface {
Deploy(ctx context.Context, pkgLayout *layout.PackageLayout, opts zPackager.DeployOptions) (zPackager.DeployResult, error)
Remove(ctx context.Context, pkg zarfAPI.PackageDefinition, opts zPackager.RemoveOptions) error
LoadPackage(ctx context.Context, source string, opts zPackager.LoadOptions) (_ *layout.PackageLayout, err error)
PackageDigest(ctx context.Context, source string, opts zPackager.PackageDigestOptions) (string, error)
GetPackageFromSourceOrCluster(ctx context.Context, cluster *cluster.Cluster, src string, namespaceOverride string, opts zPackager.LoadOptions) (_ zarfAPI.PackageDefinition, err error)
}

type loadPackageFunc func(context.Context, string, zPackager.LoadOptions) (*layout.PackageLayout, error)
type packageDigestFunc func(context.Context, string, zPackager.PackageDigestOptions) (string, error)
type deployFunc func(context.Context, *layout.PackageLayout, zPackager.DeployOptions) (zPackager.DeployResult, error)
type getPackageFunc func(context.Context, *cluster.Cluster, string, string, zPackager.LoadOptions) (zarfAPI.PackageDefinition, error)
type removeFunc func(context.Context, zarfAPI.PackageDefinition, zPackager.RemoveOptions) error

type zarfPackager struct {
loadPackage loadPackageFunc
packageDigest packageDigestFunc
deployPackage deployFunc
getPackage getPackageFunc
removePackage removeFunc
Expand All @@ -47,12 +50,23 @@ var zarfConfigOnce sync.Once
func NewPackager() Packager {
return &zarfPackager{
loadPackage: zPackager.LoadPackage,
packageDigest: zPackager.PackageDigest,
deployPackage: zPackager.Deploy,
getPackage: zPackager.GetPackageFromSourceOrCluster,
removePackage: zPackager.Remove,
}
}

func (p *zarfPackager) PackageDigest(ctx context.Context, source string, opts zPackager.PackageDigestOptions) (string, error) {
p.ensureZarfConfigured()
zarfCtx := logging.WithZarfLogger(ctx)
digest, err := p.packageDigest(zarfCtx, source, opts)
if err != nil {
return "", logging.WrapZarfError(zarfCtx, err)
}
return digest, nil
}

func (p *zarfPackager) Deploy(ctx context.Context, pkgLayout *layout.PackageLayout, opts zPackager.DeployOptions) (zPackager.DeployResult, error) {
p.ensureZarfConfigured()
zarfCtx := logging.WithZarfLogger(ctx)
Expand Down
40 changes: 40 additions & 0 deletions internal/packager/zarf_packager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,43 @@ func TestZarfPackagerLoadPackageWrapsCapturedOutputOnError(t *testing.T) {
require.ErrorContains(t, err, "load Zarf command output")
}

func TestZarfPackagerPackageDigestDelegatesWithLoggerContextAndReturnsDigest(t *testing.T) {
const source = "sentinel-source"
const want = "sha256:sentinel"
options := zPackager.PackageDigestOptions{Architecture: "arm64"}
called := false
p := newTestZarfPackager(testZarfPackagerOptions{
packageDigest: func(ctx context.Context, gotSource string, gotOptions zPackager.PackageDigestOptions) (string, error) {
called = true
require.True(t, logger.From(ctx).Enabled(ctx, slog.LevelInfo))
require.Equal(t, source, gotSource)
require.Equal(t, options, gotOptions)
return want, nil
},
})

digest, err := p.PackageDigest(context.Background(), source, options)
require.NoError(t, err)
require.Equal(t, want, digest)
require.True(t, called)
}

func TestZarfPackagerPackageDigestWrapsCapturedOutputOnError(t *testing.T) {
sentinel := errors.New("digest failed")
p := newTestZarfPackager(testZarfPackagerOptions{
packageDigest: func(ctx context.Context, _ string, _ zPackager.PackageDigestOptions) (string, error) {
logger.From(ctx).Error("digest Zarf command output")
return "", sentinel
},
})

_, err := p.PackageDigest(context.Background(), "source", zPackager.PackageDigestOptions{})

require.ErrorIs(t, err, sentinel)
require.ErrorContains(t, err, "captured Zarf output:")
require.ErrorContains(t, err, "digest Zarf command output")
}

func TestZarfPackagerGetPackageDelegatesWithLoggerContextAndReturnsPackage(t *testing.T) {
const source = "sentinel-source"
const namespace = "sentinel-namespace"
Expand Down Expand Up @@ -159,6 +196,7 @@ func TestZarfPackagerGetPackageDelegatesWithLoggerContextAndReturnsPackage(t *te
func TestNewPackagerInitializesDelegates(t *testing.T) {
p := NewPackager().(*zarfPackager)
require.NotNil(t, p.loadPackage)
require.NotNil(t, p.packageDigest)
require.NotNil(t, p.deployPackage)
require.NotNil(t, p.getPackage)
require.NotNil(t, p.removePackage)
Expand Down Expand Up @@ -210,6 +248,7 @@ func TestZarfPackagerConfiguresConcurrently(t *testing.T) {

type testZarfPackagerOptions struct {
loadPackage loadPackageFunc
packageDigest packageDigestFunc
deployPackage deployFunc
getPackage getPackageFunc
removePackage removeFunc
Expand All @@ -218,6 +257,7 @@ type testZarfPackagerOptions struct {
func newTestZarfPackager(options testZarfPackagerOptions) *zarfPackager {
return &zarfPackager{
loadPackage: options.loadPackage,
packageDigest: options.packageDigest,
deployPackage: options.deployPackage,
getPackage: options.getPackage,
removePackage: options.removePackage,
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (p *udsProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *
},
"validate_packages_on_plan": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Whether to validate UDS packages during planning. When enabled, the provider may load packages during plan to catch package-dependent configuration errors early, such as invalid optional component names or signature verification failures. Disable this to avoid plan-time package downloads or expensive validation. These checks are still enforced during apply. Defaults to `true`. Can also be configured with the `UDS_VALIDATE_PACKAGES_ON_PLAN` environment variable.",
MarkdownDescription: "Whether to validate UDS packages and resolve source digests during planning. When enabled, the provider detects changes behind mutable source references and may load packages to catch package-dependent configuration errors early, such as invalid optional component names or signature verification failures. Disabling this avoids plan-time source access, but changes behind an unchanged source reference cannot independently trigger an update. Validation and digest recording still occur when another change causes apply. Defaults to `true`. Can also be configured with the `UDS_VALIDATE_PACKAGES_ON_PLAN` environment variable.",
},
},
}
Expand Down
Loading