diff --git a/src/api/convert/convert.go b/src/api/convert/convert.go new file mode 100644 index 0000000000..5d49c2ed8a --- /dev/null +++ b/src/api/convert/convert.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package convert provides functions for converting between Zarf package API versions. +package convert + +import ( + "github.com/zarf-dev/zarf/src/api/v1alpha1" + "github.com/zarf-dev/zarf/src/api/v1beta1" + internalv1alpha1 "github.com/zarf-dev/zarf/src/internal/api/v1alpha1" + internalv1beta1 "github.com/zarf-dev/zarf/src/internal/api/v1beta1" +) + +// V1Alpha1PkgToV1Beta1 converts a v1alpha1 ZarfPackage to a v1beta1 Package. +func V1Alpha1PkgToV1Beta1(pkg v1alpha1.ZarfPackage) v1beta1.Package { + generic := internalv1alpha1.ConvertToGeneric(pkg) + v1beta1Pkg := internalv1beta1.ConvertFromGeneric(generic) + return v1beta1.SetDeprecatedFromGeneric(generic, v1beta1Pkg) +} + +// V1Beta1PkgToV1Alpha1 converts a v1beta1 Package to a v1alpha1 ZarfPackage. +func V1Beta1PkgToV1Alpha1(pkg v1beta1.Package) v1alpha1.ZarfPackage { + generic := internalv1beta1.ConvertToGeneric(pkg) + return internalv1alpha1.ConvertFromGeneric(generic) +} diff --git a/src/api/convert/convert_test.go b/src/api/convert/convert_test.go new file mode 100644 index 0000000000..73b22da4f4 --- /dev/null +++ b/src/api/convert/convert_test.go @@ -0,0 +1,1183 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package convert + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/api/v1alpha1" + "github.com/zarf-dev/zarf/src/api/v1beta1" + "github.com/zarf-dev/zarf/src/internal/api/types" +) + +func TestV1Alpha1PkgToV1Beta1_Metadata(t *testing.T) { + t.Parallel() + allowOverride := true + pkg := v1alpha1.ZarfPackage{ + APIVersion: v1alpha1.APIVersion, + Kind: v1alpha1.ZarfPackageConfig, + Metadata: v1alpha1.ZarfMetadata{ + Name: "test-pkg", + Description: "A test package", + Version: "1.0.0", + URL: "https://example.com", + Image: "https://example.com/image.png", + Authors: "Test Author", + Documentation: "https://docs.example.com", + Source: "https://github.com/example", + Vendor: "Example Corp", + AggregateChecksum: "abc123", + Architecture: "amd64", + Uncompressed: true, + AllowNamespaceOverride: &allowOverride, + Annotations: map[string]string{ + "existing": "annotation", + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Equal(t, v1beta1.APIVersion, result.APIVersion) + require.Equal(t, v1beta1.ZarfPackageConfig, result.Kind) + require.Equal(t, "test-pkg", result.Metadata.Name) + require.Equal(t, "A test package", result.Metadata.Description) + require.Equal(t, "1.0.0", result.Metadata.Version) + require.Equal(t, "amd64", result.Metadata.Architecture) + require.True(t, result.Metadata.Uncompressed) + // AllowNamespaceOverride=true → PreventNamespaceOverride=false. + require.False(t, result.Metadata.PreventNamespaceOverride) + + // v1alpha1-only metadata fields should be migrated to annotations. + require.Equal(t, "https://example.com", result.Metadata.Annotations["metadata.url"]) + require.Equal(t, "https://example.com/image.png", result.Metadata.Annotations["metadata.image"]) + require.Equal(t, "Test Author", result.Metadata.Annotations["metadata.authors"]) + require.Equal(t, "https://docs.example.com", result.Metadata.Annotations["metadata.documentation"]) + require.Equal(t, "https://github.com/example", result.Metadata.Annotations["metadata.source"]) + require.Equal(t, "Example Corp", result.Metadata.Annotations["metadata.vendor"]) + // Existing annotation should be preserved. + require.Equal(t, "annotation", result.Metadata.Annotations["existing"]) + + // AggregateChecksum should move from metadata to build. + require.Equal(t, "abc123", result.Build.AggregateChecksum) +} + +func TestV1Alpha1PkgToV1Beta1_Build(t *testing.T) { + t.Parallel() + signed := true + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Build: v1alpha1.ZarfBuildData{ + Terminal: "my-machine", + User: "test-user", + Architecture: "arm64", + Timestamp: "Mon, 02 Jan 2006 15:04:05 -0700", + Version: "v0.30.0", + Migrations: []string{"migration1"}, + RegistryOverrides: map[string]string{"docker.io": "internal.registry"}, + Differential: true, + DifferentialPackageVersion: "0.29.0", + DifferentialMissing: []string{"comp-a"}, + Flavor: "vanilla", + Signed: &signed, + VersionRequirements: []v1alpha1.VersionRequirement{ + {Version: "v0.28.0", Reason: "needs feature X"}, + }, + ProvenanceFiles: []string{"sig.json"}, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Equal(t, v1beta1.ZarfPackageConfig, result.Kind) + require.Equal(t, "my-machine", result.Build.Hostname) + require.Equal(t, "test-user", result.Build.User) + require.Equal(t, "arm64", result.Build.Architecture) + require.Equal(t, "v0.30.0", result.Build.Version) + require.True(t, result.Build.Differential) + require.Equal(t, "0.29.0", result.Build.DifferentialPackageVersion) + require.Equal(t, "vanilla", result.Build.Flavor) + require.NotNil(t, result.Build.Signed) + require.True(t, *result.Build.Signed) + require.Len(t, result.Build.VersionRequirements, 1) + require.Equal(t, "v0.28.0", result.Build.VersionRequirements[0].Version) + require.Equal(t, "needs feature X", result.Build.VersionRequirements[0].Reason) + require.Equal(t, []string{"sig.json"}, result.Build.ProvenanceFiles) +} + +func TestV1Alpha1PkgToV1Beta1_VariablesAndConstantsShim(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Variables: []v1alpha1.InteractiveVariable{ + { + Variable: v1alpha1.Variable{ + Name: "MY_VAR", + Sensitive: true, + AutoIndent: true, + Pattern: "^[a-z]+$", + Type: v1alpha1.FileVariableType, + }, + Description: "A variable", + Default: "default-val", + Prompt: true, + }, + }, + Constants: []v1alpha1.Constant{ + { + Name: "MY_CONST", + Value: "const-val", + Description: "A constant", + AutoIndent: true, + Pattern: ".*", + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + vars := result.GetDeprecatedVariables() + require.Len(t, vars, 1) + require.Equal(t, "MY_VAR", vars[0].Name) + require.True(t, vars[0].Sensitive) + require.True(t, vars[0].AutoIndent) + require.Equal(t, "^[a-z]+$", vars[0].Pattern) + require.Equal(t, v1beta1.FileVariableType, vars[0].Type) + require.Equal(t, "A variable", vars[0].Description) + require.Equal(t, "default-val", vars[0].Default) + require.True(t, vars[0].Prompt) + + consts := result.GetDeprecatedConstants() + require.Len(t, consts, 1) + require.Equal(t, "MY_CONST", consts[0].Name) + require.Equal(t, "const-val", consts[0].Value) + require.Equal(t, "A constant", consts[0].Description) + require.True(t, consts[0].AutoIndent) +} + +func TestV1Alpha1PkgToV1Beta1_YOLOAndGroupShim(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Metadata: v1alpha1.ZarfMetadata{ + Name: "yolo-pkg", + YOLO: true, + }, + Components: []v1alpha1.ZarfComponent{ + { + Name: "comp", + DeprecatedGroup: "my-group", + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.True(t, result.Metadata.GetDeprecatedYOLO()) + require.Len(t, result.Components, 1) + require.Equal(t, "my-group", result.Components[0].GetDeprecatedGroup()) +} + +func TestV1Alpha1PkgToV1Beta1_ComponentBasics(t *testing.T) { + t.Parallel() + required := true + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Components: []v1alpha1.ZarfComponent{ + { + Name: "my-component", + Description: "test component", + Required: &required, + Only: v1alpha1.ZarfComponentOnlyTarget{ + LocalOS: "linux", + Cluster: v1alpha1.ZarfComponentOnlyCluster{ + Architecture: "amd64", + }, + Flavor: "vanilla", + }, + Import: v1alpha1.ZarfComponentImport{ + Path: "./path", + URL: "oci://example.com/pkg", + }, + Images: []string{"nginx:latest", "redis:7"}, + Repos: []string{"https://github.com/example/repo"}, + DataInjections: []v1alpha1.ZarfDataInjection{ + {Source: "/data", Target: v1alpha1.ZarfContainerTarget{Namespace: "default", Selector: "app=test", Container: "main", Path: "/inject"}}, + }, + HealthChecks: []v1alpha1.NamespacedObjectKindReference{ + {APIVersion: "apps/v1", Kind: "Deployment", Namespace: "default", Name: "my-deploy"}, + {APIVersion: "v1", Kind: "Pod", Namespace: "default", Name: "my-pod"}, + }, + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Len(t, result.Components, 1) + comp := result.Components[0] + require.Equal(t, "my-component", comp.Name) + require.Equal(t, "test component", comp.Description) + + // Required=true → Optional=false. + require.False(t, comp.Optional) + + require.Equal(t, "linux", comp.Target.OS) + require.Equal(t, "amd64", comp.Target.Architecture) + require.Equal(t, "vanilla", comp.Target.Flavor) + + // v1alpha1 Import.Path/URL get promoted into the v1beta1 Local/Remote lists. + require.Len(t, comp.Import.Local, 1) + require.Equal(t, "./path", comp.Import.Local[0].Path) + require.Len(t, comp.Import.Remote, 1) + require.Equal(t, "oci://example.com/pkg", comp.Import.Remote[0].URL) + + // Images are converted from []string to []Image. + require.Len(t, comp.Images, 2) + require.Equal(t, "nginx:latest", comp.Images[0].Name) + require.Equal(t, "redis:7", comp.Images[1].Name) + + require.Equal(t, []string{"https://github.com/example/repo"}, comp.Repositories) + + // DataInjections should be preserved via the private shim. + di := comp.GetDeprecatedDataInjections() + require.Len(t, di, 1) + require.Equal(t, "/data", di[0].Source) + + // HealthChecks should become onDeploy.onSuccess wait actions with kind in .. format. + require.Len(t, comp.Actions.OnDeploy.OnSuccess, 2) + require.NotNil(t, comp.Actions.OnDeploy.OnSuccess[0].Wait) + require.NotNil(t, comp.Actions.OnDeploy.OnSuccess[0].Wait.Cluster) + require.Equal(t, "Deployment.v1.apps", comp.Actions.OnDeploy.OnSuccess[0].Wait.Cluster.Kind) + require.Equal(t, "my-deploy", comp.Actions.OnDeploy.OnSuccess[0].Wait.Cluster.Name) + require.Equal(t, "default", comp.Actions.OnDeploy.OnSuccess[0].Wait.Cluster.Namespace) + + // Core API resources (no group) keep kind as-is. + require.NotNil(t, comp.Actions.OnDeploy.OnSuccess[1].Wait) + require.NotNil(t, comp.Actions.OnDeploy.OnSuccess[1].Wait.Cluster) + require.Equal(t, "Pod", comp.Actions.OnDeploy.OnSuccess[1].Wait.Cluster.Kind) + require.Equal(t, "my-pod", comp.Actions.OnDeploy.OnSuccess[1].Wait.Cluster.Name) + require.Equal(t, "default", comp.Actions.OnDeploy.OnSuccess[1].Wait.Cluster.Namespace) +} + +func TestV1Alpha1PkgToV1Beta1_ServiceInference(t *testing.T) { + t.Parallel() + tests := []struct { + name string + compName string + service v1beta1.Service + }{ + {name: "registry", compName: "zarf-registry", service: v1beta1.ServiceRegistry}, + {name: "seed registry", compName: "zarf-seed-registry", service: v1beta1.ServiceSeedRegistry}, + {name: "injector", compName: "zarf-injector", service: v1beta1.ServiceInjector}, + {name: "agent", compName: "zarf-agent", service: v1beta1.ServiceAgent}, + {name: "git server", compName: "git-server", service: v1beta1.ServiceGitServer}, + {name: "no service", compName: "my-app", service: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Components: []v1alpha1.ZarfComponent{ + {Name: tt.compName}, + }, + } + result := V1Alpha1PkgToV1Beta1(pkg) + require.Len(t, result.Components, 1) + require.Equal(t, tt.service, result.Components[0].Service) + }) + } +} + +func TestV1Alpha1PkgToV1Beta1_ChartSources(t *testing.T) { + t.Parallel() + tests := []struct { + name string + chart v1alpha1.ZarfChart + validate func(t *testing.T, c v1beta1.Chart) + }{ + { + name: "helm repo", + chart: v1alpha1.ZarfChart{ + Name: "podinfo", + URL: "https://stefanprodan.github.io/podinfo", + RepoName: "podinfo", + Version: "6.4.0", + }, + validate: func(t *testing.T, c v1beta1.Chart) { + require.NotNil(t, c.HelmRepository) + require.Equal(t, "https://stefanprodan.github.io/podinfo", c.HelmRepository.URL) + require.Equal(t, "podinfo", c.HelmRepository.Name) + require.Equal(t, "6.4.0", c.HelmRepository.Version) + }, + }, + { + name: "oci registry", + chart: v1alpha1.ZarfChart{ + Name: "podinfo", + URL: "oci://ghcr.io/stefanprodan/charts/podinfo", + Version: "6.4.0", + }, + validate: func(t *testing.T, c v1beta1.Chart) { + require.NotNil(t, c.OCI) + require.Equal(t, "oci://ghcr.io/stefanprodan/charts/podinfo", c.OCI.URL) + require.Equal(t, "6.4.0", c.OCI.Version) + }, + }, + { + name: "git repo with version", + chart: v1alpha1.ZarfChart{ + Name: "my-chart", + URL: "https://github.com/example/repo", + GitPath: "charts/my-chart", + Version: "6.4.0", + }, + validate: func(t *testing.T, c v1beta1.Chart) { + require.NotNil(t, c.Git) + require.Equal(t, "https://github.com/example/repo@6.4.0", c.Git.URL) + require.Equal(t, "charts/my-chart", c.Git.Path) + }, + }, + { + name: "git repo without version", + chart: v1alpha1.ZarfChart{ + Name: "my-chart", + URL: "https://github.com/example/repo", + GitPath: "charts/my-chart", + }, + validate: func(t *testing.T, c v1beta1.Chart) { + require.NotNil(t, c.Git) + require.Equal(t, "https://github.com/example/repo", c.Git.URL) + require.Equal(t, "charts/my-chart", c.Git.Path) + }, + }, + { + name: "git repo with ref already in url", + chart: v1alpha1.ZarfChart{ + Name: "my-chart", + URL: "https://github.com/example/repo.git@v2.0.0", + GitPath: "charts/my-chart", + Version: "6.4.0", + }, + validate: func(t *testing.T, c v1beta1.Chart) { + require.NotNil(t, c.Git) + // URL already has @ref, should not double-append version. + require.Equal(t, "https://github.com/example/repo.git@v2.0.0", c.Git.URL) + require.Equal(t, "charts/my-chart", c.Git.Path) + }, + }, + { + name: "local path", + chart: v1alpha1.ZarfChart{ + Name: "local-chart", + LocalPath: "./charts/my-chart", + }, + validate: func(t *testing.T, c v1beta1.Chart) { + require.NotNil(t, c.Local) + require.Equal(t, "./charts/my-chart", c.Local.Path) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Components: []v1alpha1.ZarfComponent{ + { + Name: "chart-comp", + Charts: []v1alpha1.ZarfChart{tt.chart}, + }, + }, + } + result := V1Alpha1PkgToV1Beta1(pkg) + require.Len(t, result.Components, 1) + require.Len(t, result.Components[0].Charts, 1) + tt.validate(t, result.Components[0].Charts[0]) + }) + } +} + +func TestV1Alpha1PkgToV1Beta1_ManifestSkipWait(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Components: []v1alpha1.ZarfComponent{ + { + Name: "manifest-comp", + Manifests: []v1alpha1.ZarfManifest{ + { + Name: "with-no-wait", + NoWait: true, + }, + { + Name: "default-wait", + }, + }, + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Len(t, result.Components[0].Manifests, 2) + + // NoWait=true → SkipWait=true. + require.True(t, result.Components[0].Manifests[0].SkipWait) + // NoWait=false → SkipWait=false. + require.False(t, result.Components[0].Manifests[1].SkipWait) +} + +func TestV1Alpha1PkgToV1Beta1_Actions(t *testing.T) { + t.Parallel() + mute := true + maxSeconds := 30 + maxRetries := 3 + dir := "/tmp" + + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Components: []v1alpha1.ZarfComponent{ + { + Name: "action-comp", + Actions: v1alpha1.ZarfComponentActions{ + OnDeploy: v1alpha1.ZarfComponentActionSet{ + Defaults: v1alpha1.ZarfComponentActionDefaults{ + Mute: true, + MaxTotalSeconds: 60, + MaxRetries: 2, + Dir: "/work", + Env: []string{"FOO=bar"}, + Shell: v1alpha1.Shell{ + Linux: "bash", + }, + }, + Before: []v1alpha1.ZarfComponentAction{ + { + Cmd: "echo before", + Mute: &mute, + MaxTotalSeconds: &maxSeconds, + MaxRetries: &maxRetries, + Dir: &dir, + Description: "run before", + SetVariables: []v1alpha1.Variable{ + {Name: "OUT_VAR", Sensitive: true}, + }, + DeprecatedSetVariable: "OLD_VAR", + }, + }, + After: []v1alpha1.ZarfComponentAction{ + {Cmd: "echo after"}, + }, + OnSuccess: []v1alpha1.ZarfComponentAction{ + {Cmd: "echo success"}, + }, + OnFailure: []v1alpha1.ZarfComponentAction{ + {Cmd: "echo failure"}, + }, + }, + }, + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Len(t, result.Components, 1) + actions := result.Components[0].Actions + + // Defaults + require.True(t, actions.OnDeploy.Defaults.Silent) + require.Equal(t, int32(60), actions.OnDeploy.Defaults.MaxTotalSeconds) + require.Equal(t, int32(2), actions.OnDeploy.Defaults.Retries) + require.Equal(t, "/work", actions.OnDeploy.Defaults.Dir) + require.Equal(t, []string{"FOO=bar"}, actions.OnDeploy.Defaults.Env) + require.Equal(t, "bash", actions.OnDeploy.Defaults.Shell.Linux) + + // Before action + require.Len(t, actions.OnDeploy.Before, 1) + before := actions.OnDeploy.Before[0] + require.Equal(t, "echo before", before.Cmd) + require.NotNil(t, before.Silent) + require.True(t, *before.Silent) + require.NotNil(t, before.MaxTotalSeconds) + require.Equal(t, int32(30), *before.MaxTotalSeconds) + require.NotNil(t, before.Retries) + require.Equal(t, int32(3), *before.Retries) + require.Equal(t, "run before", before.Description) + // SetVariables should include both the explicit one and the deprecated one, surfaced via the shim. + setVars := before.GetDeprecatedSetVariables() + require.Len(t, setVars, 2) + require.Equal(t, "OUT_VAR", setVars[0].Name) + require.True(t, setVars[0].Sensitive) + require.Equal(t, "OLD_VAR", setVars[1].Name) + + // OnSuccess should be the merge of v1alpha1 After + OnSuccess. + require.Len(t, actions.OnDeploy.OnSuccess, 2) + require.Equal(t, "echo after", actions.OnDeploy.OnSuccess[0].Cmd) + require.Equal(t, "echo success", actions.OnDeploy.OnSuccess[1].Cmd) + + // OnFailure + require.Len(t, actions.OnDeploy.OnFailure, 1) + require.Equal(t, "echo failure", actions.OnDeploy.OnFailure[0].Cmd) +} + +func TestV1Alpha1PkgToV1Beta1_Files(t *testing.T) { + t.Parallel() + tmpl := true + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Components: []v1alpha1.ZarfComponent{ + { + Name: "file-comp", + Files: []v1alpha1.ZarfFile{ + { + Source: "https://example.com/file.tar.gz", + Shasum: "deadbeef", + Target: "/opt/file.tar.gz", + Executable: true, + Symlinks: []string{"/usr/local/bin/file"}, + ExtractPath: "bin/file", + Template: &tmpl, + }, + }, + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Len(t, result.Components[0].Files, 1) + f := result.Components[0].Files[0] + require.Equal(t, "https://example.com/file.tar.gz", f.Source) + require.Equal(t, "deadbeef", f.Checksum) + require.Equal(t, "/opt/file.tar.gz", f.Destination) + require.True(t, f.Executable) + require.Equal(t, []string{"/usr/local/bin/file"}, f.Symlinks) + require.Equal(t, "bin/file", f.ExtractPath) + require.True(t, f.EnableValues) +} + +func TestV1Alpha1PkgToV1Beta1_ValuesAndDocumentation(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Values: v1alpha1.ZarfValues{ + Files: []string{"values.yaml"}, + Schema: "values.schema.json", + }, + Documentation: map[string]string{ + "readme": "# Hello", + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Equal(t, []string{"values.yaml"}, result.Values.Files) + require.Equal(t, "values.schema.json", result.Values.Schema) + require.Equal(t, "# Hello", result.Documentation["readme"]) +} + +func TestV1Alpha1PkgToV1Beta1_DeprecatedVersionShim(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Components: []v1alpha1.ZarfComponent{ + { + Name: "chart-comp", + Charts: []v1alpha1.ZarfChart{ + { + Name: "my-chart", + URL: "https://charts.example.com", + Version: "1.2.3", + }, + }, + }, + }, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Len(t, result.Components[0].Charts, 1) + chart := result.Components[0].Charts[0] + require.Equal(t, "1.2.3", chart.GetDeprecatedVersion()) +} + +func TestV1Alpha1PkgToV1Beta1_OriginalAPIVersion(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + APIVersion: v1alpha1.APIVersion, + Kind: v1alpha1.ZarfPackageConfig, + } + + result := V1Alpha1PkgToV1Beta1(pkg) + + require.Equal(t, v1alpha1.APIVersion, result.Build.GetOriginalAPIVersion()) +} + +// --- v1beta1 → v1alpha1 tests --- + +func TestV1Beta1PkgToV1Alpha1_Metadata(t *testing.T) { + t.Parallel() + pkg := v1beta1.Package{ + APIVersion: v1beta1.APIVersion, + Kind: v1beta1.ZarfPackageConfig, + Metadata: v1beta1.PackageMetadata{ + Name: "test-pkg", + Description: "A test package", + Version: "1.0.0", + Architecture: "amd64", + Uncompressed: true, + PreventNamespaceOverride: false, + Annotations: map[string]string{ + "existing": "annotation", + "metadata.url": "https://example.com", + "metadata.image": "https://example.com/image.png", + "metadata.authors": "Test Author", + "metadata.documentation": "https://docs.example.com", + "metadata.source": "https://github.com/example", + "metadata.vendor": "Example Corp", + }, + }, + Build: v1beta1.BuildData{ + AggregateChecksum: "abc123", + }, + } + + result := V1Beta1PkgToV1Alpha1(pkg) + + require.Equal(t, v1alpha1.APIVersion, result.APIVersion) + require.Equal(t, v1alpha1.ZarfPackageConfig, result.Kind) + require.Equal(t, "test-pkg", result.Metadata.Name) + require.Equal(t, "A test package", result.Metadata.Description) + require.Equal(t, "1.0.0", result.Metadata.Version) + require.Equal(t, "amd64", result.Metadata.Architecture) + require.True(t, result.Metadata.Uncompressed) + // PreventNamespaceOverride=false → AllowNamespaceOverride=true. + require.NotNil(t, result.Metadata.AllowNamespaceOverride) + require.True(t, *result.Metadata.AllowNamespaceOverride) + + // v1alpha1-only metadata fields should be restored from annotations. + require.Equal(t, "https://example.com", result.Metadata.URL) + require.Equal(t, "https://example.com/image.png", result.Metadata.Image) + require.Equal(t, "Test Author", result.Metadata.Authors) + require.Equal(t, "https://docs.example.com", result.Metadata.Documentation) + require.Equal(t, "https://github.com/example", result.Metadata.Source) + require.Equal(t, "Example Corp", result.Metadata.Vendor) + + // Metadata-specific annotations should be consumed, regular annotations preserved. + require.Equal(t, "annotation", result.Metadata.Annotations["existing"]) + require.Empty(t, result.Metadata.Annotations["metadata.url"]) + + // AggregateChecksum should move from build to metadata. + require.Equal(t, "abc123", result.Metadata.AggregateChecksum) +} + +func TestV1Beta1PkgToV1Alpha1_Build(t *testing.T) { + t.Parallel() + signed := true + pkg := v1beta1.Package{ + Kind: v1beta1.ZarfPackageConfig, + Build: v1beta1.BuildData{ + Hostname: "my-machine", + User: "test-user", + Architecture: "arm64", + Timestamp: "Mon, 02 Jan 2006 15:04:05 -0700", + Version: "v0.30.0", + Migrations: []string{"migration1"}, + RegistryOverrides: map[string]string{"docker.io": "internal.registry"}, + Differential: true, + DifferentialPackageVersion: "0.29.0", + Flavor: "vanilla", + Signed: &signed, + VersionRequirements: []v1beta1.VersionRequirement{ + {Version: "v0.28.0", Reason: "needs feature X"}, + }, + ProvenanceFiles: []string{"sig.json"}, + }, + } + + result := V1Beta1PkgToV1Alpha1(pkg) + + require.Equal(t, v1alpha1.ZarfPackageConfig, result.Kind) + require.Equal(t, "my-machine", result.Build.Terminal) + require.Equal(t, "test-user", result.Build.User) + require.Equal(t, "arm64", result.Build.Architecture) + require.Equal(t, "v0.30.0", result.Build.Version) + require.True(t, result.Build.Differential) + require.Equal(t, "0.29.0", result.Build.DifferentialPackageVersion) + require.Equal(t, "vanilla", result.Build.Flavor) + require.NotNil(t, result.Build.Signed) + require.True(t, *result.Build.Signed) + require.Len(t, result.Build.VersionRequirements, 1) + require.Equal(t, "v0.28.0", result.Build.VersionRequirements[0].Version) +} + +func TestV1Beta1PkgToV1Alpha1_ComponentBasics(t *testing.T) { + t.Parallel() + pkg := v1beta1.Package{ + Kind: v1beta1.ZarfPackageConfig, + Components: []v1beta1.Component{ + { + Name: "my-component", + Description: "test component", + Optional: true, + ComponentSpec: v1beta1.ComponentSpec{ + Target: v1beta1.ComponentTarget{ + OS: "linux", + Architecture: "amd64", + Flavor: "vanilla", + }, + Import: v1beta1.ComponentImport{ + Local: []v1beta1.ComponentImportLocal{{Path: "./path"}}, + Remote: []v1beta1.ComponentImportRemote{{URL: "oci://example.com/pkg"}}, + }, + Images: []v1beta1.Image{ + {Name: "nginx:latest"}, + {Name: "redis:7", Source: "daemon"}, + }, + Repositories: []string{"https://github.com/example/repo"}, + }, + }, + }, + } + + result := V1Beta1PkgToV1Alpha1(pkg) + + require.Len(t, result.Components, 1) + comp := result.Components[0] + require.Equal(t, "my-component", comp.Name) + require.Equal(t, "test component", comp.Description) + + // Optional=true → Required=false. + require.NotNil(t, comp.Required) + require.False(t, *comp.Required) + + require.Equal(t, "linux", comp.Only.LocalOS) + require.Equal(t, "amd64", comp.Only.Cluster.Architecture) + require.Equal(t, "vanilla", comp.Only.Flavor) + + // v1beta1 Local[0] / Remote[0] project back onto v1alpha1 Import.Path/URL. + require.Equal(t, "./path", comp.Import.Path) + require.Equal(t, "oci://example.com/pkg", comp.Import.URL) + + // Images are converted from []Image back to []string. + require.Len(t, comp.Images, 2) + require.Equal(t, "nginx:latest", comp.Images[0]) + require.Equal(t, "redis:7", comp.Images[1]) + + require.Equal(t, []string{"https://github.com/example/repo"}, comp.Repos) +} + +func TestV1Beta1PkgToV1Alpha1_ChartSources(t *testing.T) { + t.Parallel() + tests := []struct { + name string + chart v1beta1.Chart + validate func(t *testing.T, c v1alpha1.ZarfChart) + }{ + { + name: "helm repo", + chart: v1beta1.Chart{ + Name: "podinfo", + HelmRepository: &v1beta1.HelmRepositorySource{ + URL: "https://stefanprodan.github.io/podinfo", + Name: "podinfo", + Version: "6.4.0", + }, + }, + validate: func(t *testing.T, c v1alpha1.ZarfChart) { + require.Equal(t, "https://stefanprodan.github.io/podinfo", c.URL) + require.Equal(t, "podinfo", c.RepoName) + require.Equal(t, "6.4.0", c.Version) + }, + }, + { + name: "oci registry", + chart: v1beta1.Chart{ + Name: "podinfo", + OCI: &v1beta1.OCISource{ + URL: "oci://ghcr.io/stefanprodan/charts/podinfo", + Version: "6.4.0", + }, + }, + validate: func(t *testing.T, c v1alpha1.ZarfChart) { + require.Equal(t, "oci://ghcr.io/stefanprodan/charts/podinfo", c.URL) + require.Equal(t, "6.4.0", c.Version) + }, + }, + { + name: "git repo with version in url", + chart: v1beta1.Chart{ + Name: "my-chart", + Git: &v1beta1.GitSource{ + URL: "https://github.com/example/repo@6.4.0", + Path: "charts/my-chart", + }, + }, + validate: func(t *testing.T, c v1alpha1.ZarfChart) { + require.Equal(t, "https://github.com/example/repo", c.URL) + require.Equal(t, "charts/my-chart", c.GitPath) + require.Equal(t, "6.4.0", c.Version) + }, + }, + { + name: "git repo without version", + chart: v1beta1.Chart{ + Name: "my-chart", + Git: &v1beta1.GitSource{ + URL: "https://github.com/example/repo", + Path: "charts/my-chart", + }, + }, + validate: func(t *testing.T, c v1alpha1.ZarfChart) { + require.Equal(t, "https://github.com/example/repo", c.URL) + require.Equal(t, "charts/my-chart", c.GitPath) + }, + }, + { + name: "local path", + chart: v1beta1.Chart{ + Name: "local-chart", + Local: &v1beta1.LocalSource{Path: "./charts/my-chart"}, + }, + validate: func(t *testing.T, c v1alpha1.ZarfChart) { + require.Equal(t, "./charts/my-chart", c.LocalPath) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + pkg := v1beta1.Package{ + Kind: v1beta1.ZarfPackageConfig, + Components: []v1beta1.Component{ + { + Name: "chart-comp", + ComponentSpec: v1beta1.ComponentSpec{Charts: []v1beta1.Chart{tt.chart}}, + }, + }, + } + result := V1Beta1PkgToV1Alpha1(pkg) + require.Len(t, result.Components, 1) + require.Len(t, result.Components[0].Charts, 1) + tt.validate(t, result.Components[0].Charts[0]) + }) + } +} + +func TestV1Beta1PkgToV1Alpha1_ManifestSkipWaitInversion(t *testing.T) { + t.Parallel() + pkg := v1beta1.Package{ + Kind: v1beta1.ZarfPackageConfig, + Components: []v1beta1.Component{ + { + Name: "manifest-comp", + ComponentSpec: v1beta1.ComponentSpec{ + Manifests: []v1beta1.Manifest{ + { + Name: "skip-wait", + SkipWait: true, + }, + { + Name: "default-wait", + }, + }, + }, + }, + }, + } + + result := V1Beta1PkgToV1Alpha1(pkg) + + require.Len(t, result.Components[0].Manifests, 2) + + // SkipWait=true → NoWait=true. + require.True(t, result.Components[0].Manifests[0].NoWait) + // SkipWait=false → NoWait=false. + require.False(t, result.Components[0].Manifests[1].NoWait) +} + +func TestV1Beta1PkgToV1Alpha1_Actions(t *testing.T) { + t.Parallel() + mute := true + dir := "/tmp" + maxSec := int32(30) + retries := int32(3) + maxSecDef := int32(60) + retriesDef := int32(2) + + pkg := v1beta1.Package{ + Kind: v1beta1.ZarfPackageConfig, + Components: []v1beta1.Component{ + { + Name: "action-comp", + ComponentSpec: v1beta1.ComponentSpec{ + Actions: v1beta1.ComponentActions{ + OnDeploy: v1beta1.ComponentActionSet{ + Defaults: v1beta1.ComponentActionDefaults{ + Silent: true, + MaxTotalSeconds: maxSecDef, + Retries: retriesDef, + Dir: "/work", + Env: []string{"FOO=bar"}, + Shell: v1beta1.Shell{ + Linux: "bash", + }, + }, + Before: []v1beta1.ComponentAction{ + { + Cmd: "echo before", + Silent: &mute, + MaxTotalSeconds: &maxSec, + Retries: &retries, + Dir: &dir, + }, + }, + OnSuccess: []v1beta1.ComponentAction{ + {Cmd: "echo after"}, + }, + OnFailure: []v1beta1.ComponentAction{ + {Cmd: "echo failure"}, + }, + }, + }, + }, + }, + }, + } + + result := V1Beta1PkgToV1Alpha1(pkg) + + require.Len(t, result.Components, 1) + actions := result.Components[0].Actions + + // Defaults + require.True(t, actions.OnDeploy.Defaults.Mute) + require.Equal(t, 60, actions.OnDeploy.Defaults.MaxTotalSeconds) + require.Equal(t, 2, actions.OnDeploy.Defaults.MaxRetries) + require.Equal(t, "/work", actions.OnDeploy.Defaults.Dir) + require.Equal(t, []string{"FOO=bar"}, actions.OnDeploy.Defaults.Env) + require.Equal(t, "bash", actions.OnDeploy.Defaults.Shell.Linux) + + // Before action + require.Len(t, actions.OnDeploy.Before, 1) + before := actions.OnDeploy.Before[0] + require.Equal(t, "echo before", before.Cmd) + require.NotNil(t, before.Mute) + require.True(t, *before.Mute) + require.NotNil(t, before.MaxTotalSeconds) + require.Equal(t, 30, *before.MaxTotalSeconds) + require.NotNil(t, before.MaxRetries) + require.Equal(t, 3, *before.MaxRetries) + + // OnSuccess + require.Len(t, actions.OnDeploy.OnSuccess, 1) + require.Equal(t, "echo after", actions.OnDeploy.OnSuccess[0].Cmd) + + // OnFailure + require.Len(t, actions.OnDeploy.OnFailure, 1) + require.Equal(t, "echo failure", actions.OnDeploy.OnFailure[0].Cmd) +} + +func TestV1Beta1PkgToV1Alpha1_VariablesShim(t *testing.T) { + t.Parallel() + pkg := v1beta1.SetDeprecatedFromGeneric(types.Package{ + Variables: []types.InteractiveVariable{ + { + Variable: types.Variable{ + Name: "MY_VAR", + Sensitive: true, + AutoIndent: true, + Pattern: "^[a-z]+$", + Type: types.FileVariableType, + }, + Description: "A variable", + Default: "default-val", + Prompt: true, + }, + }, + Constants: []types.Constant{ + {Name: "MY_CONST", Value: "const-val"}, + }, + }, v1beta1.Package{Kind: v1beta1.ZarfPackageConfig}) + + result := V1Beta1PkgToV1Alpha1(pkg) + + require.Len(t, result.Variables, 1) + v := result.Variables[0] + require.Equal(t, "MY_VAR", v.Name) + require.True(t, v.Sensitive) + require.Equal(t, v1alpha1.FileVariableType, v.Type) + require.Equal(t, "A variable", v.Description) + require.Equal(t, "default-val", v.Default) + require.True(t, v.Prompt) + + require.Len(t, result.Constants, 1) + require.Equal(t, "MY_CONST", result.Constants[0].Name) + require.Equal(t, "const-val", result.Constants[0].Value) +} + +func TestV1Beta1PkgToV1Alpha1_OriginalAPIVersion(t *testing.T) { + t.Parallel() + pkg := v1beta1.Package{ + APIVersion: v1beta1.APIVersion, + Kind: v1beta1.ZarfPackageConfig, + } + + result := V1Beta1PkgToV1Alpha1(pkg) + + require.Equal(t, v1beta1.APIVersion, result.Build.OriginalAPIVersion()) +} + +func TestOriginalAPIVersion_SurvivesRoundTrip(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + APIVersion: v1alpha1.APIVersion, + Kind: v1alpha1.ZarfPackageConfig, + } + + beta := V1Alpha1PkgToV1Beta1(pkg) + require.Equal(t, v1alpha1.APIVersion, beta.Build.GetOriginalAPIVersion()) + + // Converting back must preserve the true original, not report v1beta1. + result := V1Beta1PkgToV1Alpha1(beta) + require.Equal(t, v1alpha1.APIVersion, result.Build.OriginalAPIVersion()) +} + +func TestRoundTrip_V1Alpha1_To_V1Beta1_And_Back(t *testing.T) { + t.Parallel() + required := true + allowOverride := true + mute := true + maxSeconds := 30 + maxRetries := 3 + dir := "/tmp" + + original := v1alpha1.ZarfPackage{ + APIVersion: v1alpha1.APIVersion, + Kind: v1alpha1.ZarfPackageConfig, + Metadata: v1alpha1.ZarfMetadata{ + Name: "round-trip-pkg", + Description: "round trip test", + Version: "2.0.0", + Architecture: "arm64", + URL: "https://example.com", + Authors: "Test Author", + AllowNamespaceOverride: &allowOverride, + }, + Build: v1alpha1.ZarfBuildData{ + Terminal: "my-machine", + Architecture: "arm64", + Timestamp: "Mon, 02 Jan 2006 15:04:05 -0700", + Version: "v0.30.0", + }, + Components: []v1alpha1.ZarfComponent{ + { + Name: "test-comp", + Required: &required, + Images: []string{"nginx:latest"}, + Repos: []string{"https://github.com/example/repo"}, + Charts: []v1alpha1.ZarfChart{ + { + Name: "my-chart", + URL: "https://charts.example.com", + RepoName: "my-chart", + Version: "1.2.3", + Namespace: "default", + }, + }, + Manifests: []v1alpha1.ZarfManifest{ + { + Name: "my-manifest", + Namespace: "default", + Files: []string{"manifest.yaml"}, + NoWait: true, + }, + }, + Actions: v1alpha1.ZarfComponentActions{ + OnDeploy: v1alpha1.ZarfComponentActionSet{ + Defaults: v1alpha1.ZarfComponentActionDefaults{ + MaxTotalSeconds: 60, + MaxRetries: 2, + }, + Before: []v1alpha1.ZarfComponentAction{ + { + Cmd: "echo before", + Mute: &mute, + MaxTotalSeconds: &maxSeconds, + MaxRetries: &maxRetries, + Dir: &dir, + }, + }, + }, + }, + DataInjections: []v1alpha1.ZarfDataInjection{ + {Source: "/data", Target: v1alpha1.ZarfContainerTarget{Namespace: "default", Selector: "app=test", Container: "main", Path: "/inject"}}, + }, + }, + }, + Constants: []v1alpha1.Constant{ + {Name: "MY_CONST", Value: "val"}, + }, + Variables: []v1alpha1.InteractiveVariable{ + { + Variable: v1alpha1.Variable{Name: "MY_VAR"}, + Description: "a var", + Default: "default", + }, + }, + } + + // Round-trip: v1alpha1 → v1beta1 → v1alpha1. + beta := V1Alpha1PkgToV1Beta1(original) + result := V1Beta1PkgToV1Alpha1(beta) + + require.Equal(t, v1alpha1.APIVersion, result.APIVersion) + require.Equal(t, original.Kind, result.Kind) + require.Equal(t, original.Metadata.Name, result.Metadata.Name) + require.Equal(t, original.Metadata.Description, result.Metadata.Description) + require.Equal(t, original.Metadata.Version, result.Metadata.Version) + require.Equal(t, original.Metadata.Architecture, result.Metadata.Architecture) + require.Equal(t, original.Metadata.URL, result.Metadata.URL) + require.Equal(t, original.Metadata.Authors, result.Metadata.Authors) + + require.Len(t, result.Components, 1) + comp := result.Components[0] + require.Equal(t, "test-comp", comp.Name) + require.NotNil(t, comp.Required) + require.True(t, *comp.Required) + require.Equal(t, []string{"nginx:latest"}, comp.Images) + + // Chart should round-trip via structured source → flat fields. + require.Len(t, comp.Charts, 1) + require.Equal(t, "https://charts.example.com", comp.Charts[0].URL) + require.Equal(t, "my-chart", comp.Charts[0].RepoName) + require.Equal(t, "1.2.3", comp.Charts[0].Version) + + // Manifest NoWait should survive round-trip. + require.Len(t, comp.Manifests, 1) + require.True(t, comp.Manifests[0].NoWait) + + // Action timeouts should round-trip through int32 conversion. + require.Equal(t, 60, comp.Actions.OnDeploy.Defaults.MaxTotalSeconds) + require.Equal(t, 2, comp.Actions.OnDeploy.Defaults.MaxRetries) + require.Len(t, comp.Actions.OnDeploy.Before, 1) + require.NotNil(t, comp.Actions.OnDeploy.Before[0].MaxTotalSeconds) + require.Equal(t, 30, *comp.Actions.OnDeploy.Before[0].MaxTotalSeconds) + + // DataInjections should survive round-trip via private shim. + require.Len(t, comp.DataInjections, 1) + require.Equal(t, "/data", comp.DataInjections[0].Source) + + // Constants and variables. + require.Len(t, result.Constants, 1) + require.Equal(t, "MY_CONST", result.Constants[0].Name) + require.Len(t, result.Variables, 1) + require.Equal(t, "MY_VAR", result.Variables[0].Name) +} diff --git a/src/api/v1beta1/convert.go b/src/api/v1beta1/convert.go new file mode 100644 index 0000000000..20021b96c2 --- /dev/null +++ b/src/api/v1beta1/convert.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package v1beta1 + +import "github.com/zarf-dev/zarf/src/internal/api/types" + +// SetDeprecatedFromGeneric populates the unexported v1alpha1 backwards-compatibility +// shim fields on an already-converted package. +// This is intentionally not reachable outside Zarf as types.Package is internal. +func SetDeprecatedFromGeneric(g types.Package, pkg Package) Package { + pkg.variables = interactiveVarsFromGeneric(g.Variables) + pkg.constants = constantsFromGeneric(g.Constants) + pkg.Metadata.yolo = g.Metadata.YOLO + + for i := range pkg.Components { + gc := g.Components[i] + pkg.Components[i].dataInjections = dataInjectionsFromGeneric(gc.DataInjections) + pkg.Components[i].group = gc.Group + + for j := range pkg.Components[i].Charts { + gch := gc.Charts[j] + pkg.Components[i].Charts[j].version = gch.Version + pkg.Components[i].Charts[j].variables = chartVarsFromGeneric(gch.Variables) + } + + applyActionSetSetVariables(&pkg.Components[i].Actions.OnCreate, gc.Actions.OnCreate) + applyActionSetSetVariables(&pkg.Components[i].Actions.OnDeploy, gc.Actions.OnDeploy) + applyActionSetSetVariables(&pkg.Components[i].Actions.OnRemove, gc.Actions.OnRemove) + } + + return pkg +} + +func applyActionSetSetVariables(set *ComponentActionSet, g types.ComponentActionSet) { + applyActionSliceSetVariables(set.Before, g.Before) + applyActionSliceSetVariables(set.OnSuccess, g.OnSuccess) + applyActionSliceSetVariables(set.OnFailure, g.OnFailure) +} + +func applyActionSliceSetVariables(actions []ComponentAction, g []types.ComponentAction) { + for k := range g { + setVars := setVarsFromGeneric(g[k].SetVariables) + if g[k].DeprecatedSetVariable != "" { + setVars = append(setVars, Variable{Name: g[k].DeprecatedSetVariable}) + } + if len(setVars) > 0 { + actions[k].setVariables = setVars + } + } +} + +func variableFromGeneric(v types.Variable) Variable { + return Variable{ + Name: v.Name, + Sensitive: v.Sensitive, + AutoIndent: v.AutoIndent, + Pattern: v.Pattern, + Type: VariableType(v.Type), + } +} + +func setVarsFromGeneric(in []types.Variable) []Variable { + var out []Variable + for _, v := range in { + out = append(out, variableFromGeneric(v)) + } + return out +} + +func interactiveVarsFromGeneric(in []types.InteractiveVariable) []InteractiveVariable { + var out []InteractiveVariable + for _, v := range in { + out = append(out, InteractiveVariable{ + Variable: variableFromGeneric(v.Variable), + Description: v.Description, + Default: v.Default, + Prompt: v.Prompt, + }) + } + return out +} + +func constantsFromGeneric(in []types.Constant) []Constant { + var out []Constant + for _, c := range in { + out = append(out, Constant{ + Name: c.Name, + Value: c.Value, + Description: c.Description, + AutoIndent: c.AutoIndent, + Pattern: c.Pattern, + }) + } + return out +} + +func chartVarsFromGeneric(in []types.ZarfChartVariable) []ZarfChartVariable { + var out []ZarfChartVariable + for _, v := range in { + out = append(out, ZarfChartVariable{Name: v.Name, Description: v.Description, Path: v.Path}) + } + return out +} + +func dataInjectionsFromGeneric(in []types.ZarfDataInjection) []ZarfDataInjection { + var out []ZarfDataInjection + for _, d := range in { + out = append(out, ZarfDataInjection{ + Source: d.Source, + Target: ZarfContainerTarget{ + Namespace: d.Target.Namespace, + Selector: d.Target.Selector, + Container: d.Target.Container, + Path: d.Target.Path, + }, + Compress: d.Compress, + }) + } + return out +} diff --git a/src/cmd/internal.go b/src/cmd/internal.go index 0cbed491c2..748b7ab03b 100644 --- a/src/cmd/internal.go +++ b/src/cmd/internal.go @@ -39,6 +39,7 @@ func newInternalCommand(rootCmd *cobra.Command) *cobra.Command { cmd.AddCommand(newInternalUpdateGiteaPVCCommand()) cmd.AddCommand(newInternalIsValidHostnameCommand()) cmd.AddCommand(newInternalCrc32Command()) + cmd.AddCommand(newInternalConvertCommand()) return cmd } diff --git a/src/cmd/internal_convert.go b/src/cmd/internal_convert.go new file mode 100644 index 0000000000..cbb3e77154 --- /dev/null +++ b/src/cmd/internal_convert.go @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package cmd + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + goyaml "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/zarf-dev/zarf/src/api/convert" + "github.com/zarf-dev/zarf/src/api/v1alpha1" + "github.com/zarf-dev/zarf/src/pkg/logger" +) + +type internalConvertOptions struct{} + +// This command will be unhidden and moved to dev once v1beta1 is ready for use +func newInternalConvertCommand() *cobra.Command { + o := &internalConvertOptions{} + + cmd := &cobra.Command{ + Use: "convert [directory]", + Short: "Convert zarf.yaml to the latest API version (V1beta1)", + Hidden: true, + Args: cobra.MaximumNArgs(1), + RunE: o.run, + } + + return cmd +} + +func (o *internalConvertOptions) run(cmd *cobra.Command, args []string) error { + l := logger.From(cmd.Context()) + dir := "." + if len(args) > 0 { + dir = args[0] + } + + inputPath := filepath.Join(dir, "zarf.yaml") + b, err := os.ReadFile(inputPath) + if err != nil { + return fmt.Errorf("reading %s: %w", inputPath, err) + } + + var pkg v1alpha1.ZarfPackage + if err := goyaml.Unmarshal(b, &pkg); err != nil { + return fmt.Errorf("parsing %s: %w", inputPath, err) + } + + if err := checkRemovedFields(pkg); err != nil { + return err + } + + result := convert.V1Alpha1PkgToV1Beta1(pkg) + + out, err := goyaml.Marshal(result) + if err != nil { + return fmt.Errorf("marshaling v1beta1 package: %w", err) + } + + outputPath := filepath.Join(dir, "zarf-v1beta1.yaml") + if err := os.WriteFile(outputPath, out, 0644); err != nil { + return fmt.Errorf("writing %s: %w", outputPath, err) + } + + l.Info("converted", "input", inputPath, "output", outputPath) + return nil +} + +func checkRemovedFields(pkg v1alpha1.ZarfPackage) error { + var errs []error + if pkg.Metadata.YOLO { + // TODO, add link to connected docs when available + errs = append(errs, fmt.Errorf(".metadata.yolo is removed without replacement in v1beta1 — replace it with connected deployments")) + } + // TODO link to values docs + if len(pkg.Variables) > 0 { + errs = append(errs, fmt.Errorf(".variables is removed in v1beta1 — consider using Zarf values instead")) + } + if len(pkg.Constants) > 0 { + errs = append(errs, fmt.Errorf(".constants is removed in v1beta1 — consider using Zarf values instead")) + } + for _, c := range pkg.Components { + if c.DeprecatedGroup != "" { + errs = append(errs, fmt.Errorf("can't convert component %s, .components.group is removed without replacement in v1beta1 — consider using .components[x].only.flavor instead", c.Name)) + } + if c.Default { + errs = append(errs, fmt.Errorf("can't convert component %s, .components.default is removed without replacement in v1beta1", c.Name)) + } + if len(c.DataInjections) > 0 { + errs = append(errs, fmt.Errorf("can't convert component %s, .components.dataInjections is removed without replacement in v1beta1 — see https://docs.zarf.dev/best-practices/data-injections-migration/ for alternatives", c.Name)) + } + if len(c.Only.Cluster.Distros) > 0 { + errs = append(errs, fmt.Errorf("can't convert component %s, .components.only.cluster.distro is removed without replacement in v1beta1", c.Name)) + } + // TODO add link to example of newer import system + if c.Import.Name != "" { + errs = append(errs, fmt.Errorf("can't convert component %s, .components.import.name is removed without replacement in v1beta1", c.Name)) + } + for _, ch := range c.Charts { + // TODO link to values docs + if len(ch.Variables) > 0 { + errs = append(errs, fmt.Errorf("can't convert chart %s in component %s, .components.charts.variables is removed without replacement in v1beta1 — consider using Zarf values instead", ch.Name, c.Name)) + } + } + errs = append(errs, checkRemovedActionFields(c)...) + } + return errors.Join(errs...) +} + +// checkRemovedActionFields reports actions using setVariable/setVariables, which are removed in v1beta1 in favor of setValues. +func checkRemovedActionFields(c v1alpha1.ZarfComponent) []error { + var errs []error + actionSets := []struct { + onAny string + set v1alpha1.ZarfComponentActionSet + }{ + {"onCreate", c.Actions.OnCreate}, + {"onDeploy", c.Actions.OnDeploy}, + {"onRemove", c.Actions.OnRemove}, + } + for _, as := range actionSets { + set := as.set + for _, actions := range [][]v1alpha1.ZarfComponentAction{set.Before, set.After, set.OnSuccess, set.OnFailure} { + for _, a := range actions { + if a.DeprecatedSetVariable != "" || len(a.SetVariables) > 0 { + errs = append(errs, fmt.Errorf("can't convert component %s, .components.actions.%s setVariable/setVariables is removed in v1beta1 — use setValues instead", c.Name, as.onAny)) + break + } + } + } + } + return errs +} diff --git a/src/cmd/internal_convert_test.go b/src/cmd/internal_convert_test.go new file mode 100644 index 0000000000..dbd933e9be --- /dev/null +++ b/src/cmd/internal_convert_test.go @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/api/v1alpha1" +) + +func TestCheckRemovedFields(t *testing.T) { + t.Parallel() + tests := []struct { + name string + pkg v1alpha1.ZarfPackage + wantErr string + }{ + { + name: "yolo", + pkg: v1alpha1.ZarfPackage{Metadata: v1alpha1.ZarfMetadata{YOLO: true}}, + wantErr: ".metadata.yolo", + }, + { + name: "package variables", + pkg: v1alpha1.ZarfPackage{Variables: []v1alpha1.InteractiveVariable{{Variable: v1alpha1.Variable{Name: "FOO"}}}}, + wantErr: ".variables is removed", + }, + { + name: "package constants", + pkg: v1alpha1.ZarfPackage{Constants: []v1alpha1.Constant{{Name: "FOO"}}}, + wantErr: ".constants is removed", + }, + { + name: "component group", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{Name: "c", DeprecatedGroup: "g"}}}, + wantErr: ".components.group", + }, + { + name: "component default", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{Name: "c", Default: true}}}, + wantErr: ".components.default", + }, + { + name: "data injections", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{Name: "c", DataInjections: []v1alpha1.ZarfDataInjection{{Source: "/data"}}}}}, + wantErr: ".components.dataInjections", + }, + { + name: "cluster distro", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{ + Name: "c", + Only: v1alpha1.ZarfComponentOnlyTarget{Cluster: v1alpha1.ZarfComponentOnlyCluster{Distros: []string{"k3s"}}}, + }}}, + wantErr: ".components.only.cluster.distro", + }, + { + name: "import name", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{Name: "c", Import: v1alpha1.ZarfComponentImport{Name: "n"}}}}, + wantErr: ".components.import.name", + }, + { + name: "chart variables", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{Name: "c", Charts: []v1alpha1.ZarfChart{{Name: "ch", Variables: []v1alpha1.ZarfChartVariable{{Name: "V"}}}}}}}, + wantErr: ".components.charts.variables", + }, + { + name: "action setVariables", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{ + Name: "c", + Actions: v1alpha1.ZarfComponentActions{ + OnDeploy: v1alpha1.ZarfComponentActionSet{ + Before: []v1alpha1.ZarfComponentAction{{Cmd: "echo", SetVariables: []v1alpha1.Variable{{Name: "V"}}}}, + }, + }, + }}}, + wantErr: ".components.actions.onDeploy", + }, + { + name: "action deprecated setVariable", + pkg: v1alpha1.ZarfPackage{Components: []v1alpha1.ZarfComponent{{ + Name: "c", + Actions: v1alpha1.ZarfComponentActions{ + OnCreate: v1alpha1.ZarfComponentActionSet{ + OnSuccess: []v1alpha1.ZarfComponentAction{{Cmd: "echo", DeprecatedSetVariable: "V"}}, + }, + }, + }}}, + wantErr: ".components.actions.onCreate", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := checkRemovedFields(tt.pkg) + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr) + }) + } +} + +func TestCheckRemovedFieldsConvertible(t *testing.T) { + t.Parallel() + pkg := v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Metadata: v1alpha1.ZarfMetadata{ + Name: "convertible", + }, + Components: []v1alpha1.ZarfComponent{ + { + Name: "web", + Images: []string{"nginx:latest"}, + Charts: []v1alpha1.ZarfChart{{Name: "ch", LocalPath: "./chart"}}, + Actions: v1alpha1.ZarfComponentActions{ + OnDeploy: v1alpha1.ZarfComponentActionSet{ + Before: []v1alpha1.ZarfComponentAction{{Cmd: "echo hi", SetValues: []v1alpha1.SetValue{{Key: "k"}}}}, + }, + }, + }, + }, + } + require.NoError(t, checkRemovedFields(pkg)) +} diff --git a/src/internal/api/types/package.go b/src/internal/api/types/package.go new file mode 100644 index 0000000000..d67a8b2ff4 --- /dev/null +++ b/src/internal/api/types/package.go @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package types holds the internal generic representation of a Zarf package used for lossless conversions between API versions. +// This type is never exposed publicly. Each API version converts to/from this type, giving N conversion functions instead of N². +// The shape mirrors the latest schema (v1beta1) with extra fields appended where earlier versions carry data that does not survive +// untouched on the latest schema. +package types + +// Package is the internal superset representation used for conversions between API versions. +type Package struct { + APIVersion string + Kind string + Metadata PackageMetadata + Build BuildData + Components []Component + Values Values + Documentation map[string]string + + // v1alpha1-only fields preserved for lossless round-trip. + Variables []InteractiveVariable + Constants []Constant +} + +// PackageMetadata is the superset of metadata fields across API versions. +type PackageMetadata struct { + Name string + Description string + Version string + Uncompressed bool + Architecture string + Annotations map[string]string + // PreventNamespaceOverride is the v1beta1 form. v1alpha1 stores AllowNamespaceOverride *bool; + // only one of these should be populated by the converter. + PreventNamespaceOverride bool + AllowNamespaceOverride *bool + + // v1alpha1-only metadata fields. v1beta1 migrates these to Annotations. + URL string + Image string + YOLO bool + Authors string + Documentation string + Source string + Vendor string + // AggregateChecksum lives in Metadata on v1alpha1 and in Build on v1beta1. + AggregateChecksum string +} + +// BuildData is the superset of build fields across API versions. +type BuildData struct { + // Hostname is the v1beta1 name (v1alpha1: Terminal). + Hostname string + User string + Architecture string + Timestamp string + Version string + Migrations []string + RegistryOverrides map[string]string + Differential bool + DifferentialPackageVersion string + Flavor string + Signed *bool + VersionRequirements []VersionRequirement + ProvenanceFiles []string + AggregateChecksum string + // OriginalAPIVersion tracks the apiVersion the package was read from before any conversion. + OriginalAPIVersion string + + // v1alpha1-only build fields. + DifferentialMissing []string +} + +// VersionRequirement specifies a minimum Zarf version needed. +type VersionRequirement struct { + Version string + Reason string +} + +// Values defines values files and schema. +type Values struct { + Files []string + Schema string +} + +// Component is the superset of component fields across API versions. +type Component struct { + Name string + Description string + Optional bool + Target ComponentTarget + Import ComponentImport + Service string + Manifests []Manifest + Charts []Chart + Files []File + Images []Image + ImageArchives []ImageArchive + Repositories []string + Actions ComponentActions + + // v1alpha1-only fields preserved for lossless round-trip. + Default bool + Required *bool + Group string + DataInjections []ZarfDataInjection + HealthChecks []NamespacedObjectKindReference + Distros []string +} + +// ComponentTarget filters a component to a target OS/arch/flavor. +type ComponentTarget struct { + OS string + Architecture string + Flavor string +} + +// ComponentImport carries imports from any API version. +type ComponentImport struct { + // v1beta1 form: separate lists of local and remote component config references. + Local []ComponentImportLocal + Remote []ComponentImportRemote + + // v1alpha1-only single-import fields. + Name string + Path string + URL string +} + +// ComponentImportLocal references a local component config file. +type ComponentImportLocal struct { + Path string +} + +// ComponentImportRemote references a remote (OCI) component config. +type ComponentImportRemote struct { + URL string +} + +// KustomizeManifest holds kustomization settings for a manifest. +type KustomizeManifest struct { + Files []string + AllowAnyDirectory bool + EnablePlugins bool +} + +// Manifest is the superset of manifest fields across API versions. +type Manifest struct { + Name string + Namespace string + Files []string + Kustomize *KustomizeManifest + SkipWait bool + ServerSideApply string + EnableValues bool + + // v1alpha1-only round-trip fields. + Template *bool +} + +// Chart is the superset of chart fields across API versions. +type Chart struct { + Name string + Namespace string + ReleaseName string + ValuesFiles []string + Values []ChartValue + SkipSchemaValidation bool + ServerSideApply string + SkipWait bool + + // v1beta1 structured sources. + HelmRepository *HelmRepositorySource + Git *GitSource + Local *LocalSource + OCI *OCISource + + // v1alpha1-only flat source fields. Used during conversion to populate structured sources. + URL string + RepoName string + GitPath string + LocalPath string + Version string + SchemaValidation *bool + Variables []ZarfChartVariable +} + +// ChartValue maps a source path to a target path. +type ChartValue struct { + SourcePath string + TargetPath string +} + +// HelmRepositorySource represents a chart stored in a Helm repository. +type HelmRepositorySource struct { + Name string + URL string + Version string +} + +// GitSource represents a chart stored in a Git repository. +type GitSource struct { + URL string + Path string +} + +// LocalSource represents a chart stored locally. +type LocalSource struct { + Path string +} + +// OCISource represents a chart stored in an OCI registry. +type OCISource struct { + URL string + Version string +} + +// File is the superset of file fields across API versions. +type File struct { + Source string + Checksum string + Destination string + Executable bool + Symlinks []string + ExtractPath string + EnableValues bool +} + +// Image represents an OCI image in the package. +type Image struct { + Name string + Source string +} + +// ImageArchive defines a tar archive of images to include in the package. +type ImageArchive struct { + Path string + Images []string +} + +// ComponentActions are ActionSets mapped to package lifecycle operations. +type ComponentActions struct { + OnCreate ComponentActionSet + OnDeploy ComponentActionSet + OnRemove ComponentActionSet +} + +// ComponentActionSet is a set of actions for one lifecycle operation. +type ComponentActionSet struct { + Defaults ComponentActionDefaults + Before []ComponentAction + OnSuccess []ComponentAction + OnFailure []ComponentAction +} + +// ComponentActionDefaults sets defaults for child actions. +type ComponentActionDefaults struct { + Silent bool + MaxTotalSeconds int32 + Retries int32 + Dir string + Env []string + Shell Shell +} + +// ComponentAction is the superset of action fields across API versions. +type ComponentAction struct { + Silent *bool + MaxTotalSeconds *int32 + Retries *int32 + Dir *string + Env []string + Cmd string + Shell *Shell + SetValues []SetValue + Description string + Wait *ComponentActionWait + EnableValues bool + + // v1alpha1-only round-trip fields. + SetVariables []Variable + DeprecatedSetVariable string +} + +// SetValue declares a value that can be set during a deploy. +type SetValue struct { + Key string + Value any + Type string +} + +// ComponentActionWait specifies a condition to wait for before continuing. +type ComponentActionWait struct { + Cluster *ComponentActionWaitCluster + Network *ComponentActionWaitNetwork +} + +// ComponentActionWaitCluster specifies a cluster-level wait condition. +type ComponentActionWaitCluster struct { + Kind string + Name string + Namespace string + Condition string +} + +// ComponentActionWaitNetwork specifies a network-level wait condition. +type ComponentActionWaitNetwork struct { + Protocol string + Address string + Code int32 +} + +// Shell represents shell preferences per OS. +type Shell struct { + Windows string + Linux string + Darwin string +} + +// VariableType represents a type of a Zarf package variable. +type VariableType string + +const ( + // RawVariableType is the default type for a Zarf package variable. + RawVariableType VariableType = "raw" + // FileVariableType loads a variable's contents from a file. + FileVariableType VariableType = "file" +) + +// Variable represents a variable that has a value set programmatically. +type Variable struct { + Name string + Sensitive bool + AutoIndent bool + Pattern string + Type VariableType +} + +// InteractiveVariable is a variable that can prompt a user for more information. +type InteractiveVariable struct { + Variable + Description string + Default string + Prompt bool +} + +// Constant is a value that can be used to dynamically template resources or run in actions. +type Constant struct { + Name string + Value string + Description string + AutoIndent bool + Pattern string +} + +// ZarfChartVariable represents a variable that can be set for Helm chart overrides. +type ZarfChartVariable struct { + Name string + Description string + Path string +} + +// ZarfContainerTarget defines the destination info for a ZarfDataInjection target. +type ZarfContainerTarget struct { + Namespace string + Selector string + Container string + Path string +} + +// ZarfDataInjection is a data-injection definition. +type ZarfDataInjection struct { + Source string + Target ZarfContainerTarget + Compress bool +} + +// NamespacedObjectKindReference references a cluster resource targeted by a health check. +type NamespacedObjectKindReference struct { + APIVersion string + Kind string + Namespace string + Name string +} diff --git a/src/internal/api/v1alpha1/convert.go b/src/internal/api/v1alpha1/convert.go new file mode 100644 index 0000000000..cde1466068 --- /dev/null +++ b/src/internal/api/v1alpha1/convert.go @@ -0,0 +1,850 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package v1alpha1 contains functions for converting between the public v1alpha1 Zarf package and the internal generic representation. +package v1alpha1 + +import ( + "strings" + + "github.com/zarf-dev/zarf/src/api/v1alpha1" + "github.com/zarf-dev/zarf/src/internal/api/types" +) + +// ConvertToGeneric converts a v1alpha1 ZarfPackage to the internal generic representation. +func ConvertToGeneric(pkg v1alpha1.ZarfPackage) types.Package { + // Preserve an already-recorded original across multi-hop conversions; otherwise this is the original. + originalAPIVersion := pkg.Build.OriginalAPIVersion() + if originalAPIVersion == "" { + originalAPIVersion = pkg.APIVersion + } + g := types.Package{ + APIVersion: pkg.APIVersion, + Kind: string(pkg.Kind), + Metadata: types.PackageMetadata{ + Name: pkg.Metadata.Name, + Description: pkg.Metadata.Description, + Version: pkg.Metadata.Version, + Uncompressed: pkg.Metadata.Uncompressed, + Architecture: pkg.Metadata.Architecture, + Annotations: pkg.Metadata.Annotations, + AllowNamespaceOverride: pkg.Metadata.AllowNamespaceOverride, + URL: pkg.Metadata.URL, + Image: pkg.Metadata.Image, + YOLO: pkg.Metadata.YOLO, + Authors: pkg.Metadata.Authors, + Documentation: pkg.Metadata.Documentation, + Source: pkg.Metadata.Source, + Vendor: pkg.Metadata.Vendor, + AggregateChecksum: pkg.Metadata.AggregateChecksum, + }, + Build: types.BuildData{ + Hostname: pkg.Build.Terminal, + User: pkg.Build.User, + Architecture: pkg.Build.Architecture, + Timestamp: pkg.Build.Timestamp, + Version: pkg.Build.Version, + Migrations: pkg.Build.Migrations, + RegistryOverrides: pkg.Build.RegistryOverrides, + Differential: pkg.Build.Differential, + DifferentialPackageVersion: pkg.Build.DifferentialPackageVersion, + Flavor: pkg.Build.Flavor, + Signed: pkg.Build.Signed, + DifferentialMissing: pkg.Build.DifferentialMissing, + ProvenanceFiles: pkg.Build.ProvenanceFiles, + OriginalAPIVersion: originalAPIVersion, + }, + Values: types.Values{ + Files: pkg.Values.Files, + Schema: pkg.Values.Schema, + }, + Documentation: pkg.Documentation, + Variables: interactiveVarsToGeneric(pkg.Variables), + Constants: constantsToGeneric(pkg.Constants), + } + + for _, vr := range pkg.Build.VersionRequirements { + g.Build.VersionRequirements = append(g.Build.VersionRequirements, types.VersionRequirement{ + Version: vr.Version, + Reason: vr.Reason, + }) + } + + for _, c := range pkg.Components { + g.Components = append(g.Components, componentToGeneric(c)) + } + + return g +} + +func componentToGeneric(c v1alpha1.ZarfComponent) types.Component { + gc := types.Component{ + Name: c.Name, + Description: c.Description, + Default: c.Default, + Required: c.Required, + Group: c.DeprecatedGroup, + DataInjections: dataInjectionsToGeneric(c.DataInjections), + HealthChecks: healthChecksToGeneric(c.HealthChecks), + Repositories: c.Repos, + Target: types.ComponentTarget{ + OS: c.Only.LocalOS, + Architecture: c.Only.Cluster.Architecture, + Flavor: c.Only.Flavor, + }, + Distros: c.Only.Cluster.Distros, + Import: types.ComponentImport{ + Name: c.Import.Name, + Path: c.Import.Path, + URL: c.Import.URL, + }, + Actions: actionsToGeneric(c.Actions), + } + + for _, m := range c.Manifests { + gc.Manifests = append(gc.Manifests, manifestToGeneric(m)) + } + + for _, ch := range c.Charts { + gc.Charts = append(gc.Charts, chartToGeneric(ch)) + } + + for _, f := range c.Files { + gc.Files = append(gc.Files, types.File{ + Source: f.Source, + Checksum: f.Shasum, + Destination: f.Target, + Executable: f.Executable, + Symlinks: f.Symlinks, + ExtractPath: f.ExtractPath, + EnableValues: derefBool(f.Template), + }) + } + + for _, img := range c.Images { + gc.Images = append(gc.Images, types.Image{Name: img}) + } + + for _, ia := range c.ImageArchives { + gc.ImageArchives = append(gc.ImageArchives, types.ImageArchive{ + Path: ia.Path, + Images: ia.Images, + }) + } + + return gc +} + +func manifestToGeneric(m v1alpha1.ZarfManifest) types.Manifest { + gm := types.Manifest{ + Name: m.Name, + Namespace: m.Namespace, + Files: m.Files, + SkipWait: m.NoWait, + ServerSideApply: m.ServerSideApply, + EnableValues: derefBool(m.Template), + Template: m.Template, + } + if len(m.Kustomizations) > 0 || m.KustomizeAllowAnyDirectory || m.EnableKustomizePlugins { + gm.Kustomize = &types.KustomizeManifest{ + Files: m.Kustomizations, + AllowAnyDirectory: m.KustomizeAllowAnyDirectory, + EnablePlugins: m.EnableKustomizePlugins, + } + } + return gm +} + +func chartToGeneric(ch v1alpha1.ZarfChart) types.Chart { + gc := types.Chart{ + Name: ch.Name, + Namespace: ch.Namespace, + ReleaseName: ch.ReleaseName, + ValuesFiles: ch.ValuesFiles, + SkipSchemaValidation: ch.SchemaValidation != nil && !*ch.SchemaValidation, + ServerSideApply: ch.ServerSideApply, + SkipWait: ch.NoWait, + URL: ch.URL, + RepoName: ch.RepoName, + GitPath: ch.GitPath, + LocalPath: ch.LocalPath, + Version: ch.Version, + SchemaValidation: ch.SchemaValidation, + Variables: chartVarsToGeneric(ch.Variables), + Values: chartValuesToGeneric(ch.Values), + } + return gc +} + +func chartValuesToGeneric(vals []v1alpha1.ZarfChartValue) []types.ChartValue { + var out []types.ChartValue + for _, v := range vals { + out = append(out, types.ChartValue{ + SourcePath: v.SourcePath, + TargetPath: v.TargetPath, + }) + } + return out +} + +func actionsToGeneric(a v1alpha1.ZarfComponentActions) types.ComponentActions { + return types.ComponentActions{ + OnCreate: actionSetToGeneric(a.OnCreate), + OnDeploy: actionSetToGeneric(a.OnDeploy), + OnRemove: actionSetToGeneric(a.OnRemove), + } +} + +func actionSetToGeneric(s v1alpha1.ZarfComponentActionSet) types.ComponentActionSet { + defaults := types.ComponentActionDefaults{ + Silent: s.Defaults.Mute, + MaxTotalSeconds: int32(s.Defaults.MaxTotalSeconds), + Retries: int32(s.Defaults.MaxRetries), + Dir: s.Defaults.Dir, + Env: s.Defaults.Env, + Shell: types.Shell{ + Windows: s.Defaults.Shell.Windows, + Linux: s.Defaults.Shell.Linux, + Darwin: s.Defaults.Shell.Darwin, + }, + } + + onSuccess := actionSliceToGeneric(s.After) + onSuccess = append(onSuccess, actionSliceToGeneric(s.OnSuccess)...) + + return types.ComponentActionSet{ + Defaults: defaults, + Before: actionSliceToGeneric(s.Before), + OnSuccess: onSuccess, + OnFailure: actionSliceToGeneric(s.OnFailure), + } +} + +func actionSliceToGeneric(actions []v1alpha1.ZarfComponentAction) []types.ComponentAction { + var out []types.ComponentAction + for _, a := range actions { + out = append(out, actionToGeneric(a)) + } + return out +} + +func actionToGeneric(a v1alpha1.ZarfComponentAction) types.ComponentAction { + ga := types.ComponentAction{ + Silent: a.Mute, + Dir: a.Dir, + Env: a.Env, + Cmd: a.Cmd, + Description: a.Description, + Wait: waitToGeneric(a.Wait), + EnableValues: derefBool(a.Template), + SetVariables: varsToGeneric(a.SetVariables), + DeprecatedSetVariable: a.DeprecatedSetVariable, + } + + if a.MaxTotalSeconds != nil { + v := int32(*a.MaxTotalSeconds) + ga.MaxTotalSeconds = &v + } + if a.MaxRetries != nil { + v := int32(*a.MaxRetries) + ga.Retries = &v + } + + for _, sv := range a.SetValues { + ga.SetValues = append(ga.SetValues, types.SetValue{ + Key: sv.Key, + Value: sv.Value, + Type: string(sv.Type), + }) + } + + if a.Shell != nil { + ga.Shell = &types.Shell{ + Windows: a.Shell.Windows, + Linux: a.Shell.Linux, + Darwin: a.Shell.Darwin, + } + } + + return ga +} + +func waitToGeneric(w *v1alpha1.ZarfComponentActionWait) *types.ComponentActionWait { + if w == nil { + return nil + } + gw := &types.ComponentActionWait{} + if w.Cluster != nil { + gw.Cluster = &types.ComponentActionWaitCluster{ + Kind: w.Cluster.Kind, + Name: w.Cluster.Name, + Namespace: w.Cluster.Namespace, + Condition: w.Cluster.Condition, + } + } + if w.Network != nil { + gw.Network = &types.ComponentActionWaitNetwork{ + Protocol: w.Network.Protocol, + Address: w.Network.Address, + Code: int32(w.Network.Code), + } + } + return gw +} + +// ConvertFromGeneric converts the internal generic representation to a v1alpha1 ZarfPackage. +func ConvertFromGeneric(g types.Package) v1alpha1.ZarfPackage { + pkg := v1alpha1.ZarfPackage{ + APIVersion: v1alpha1.APIVersion, + Kind: v1alpha1.ZarfPackageKind(g.Kind), + Metadata: metadataFromGeneric(g.Metadata, g.Build), + Build: buildFromGeneric(g.Build), + Values: v1alpha1.ZarfValues{Files: g.Values.Files, Schema: g.Values.Schema}, + Documentation: g.Documentation, + Variables: interactiveVarsFromGeneric(g.Variables), + Constants: constantsFromGeneric(g.Constants), + } + + if pkg.Kind == "" { + pkg.Kind = v1alpha1.ZarfPackageConfig + } + + for _, c := range g.Components { + pkg.Components = append(pkg.Components, componentFromGeneric(c)) + } + + return pkg +} + +func metadataFromGeneric(m types.PackageMetadata, b types.BuildData) v1alpha1.ZarfMetadata { + meta := v1alpha1.ZarfMetadata{ + Name: m.Name, + Description: m.Description, + Version: m.Version, + Uncompressed: m.Uncompressed, + Architecture: m.Architecture, + AllowNamespaceOverride: m.AllowNamespaceOverride, + URL: m.URL, + Image: m.Image, + YOLO: m.YOLO, + Authors: m.Authors, + Documentation: m.Documentation, + Source: m.Source, + Vendor: m.Vendor, + } + + // If we only have the v1beta1 form of namespace override, project it back to v1alpha1. + if meta.AllowNamespaceOverride == nil { + allow := !m.PreventNamespaceOverride + meta.AllowNamespaceOverride = &allow + } + + // AggregateChecksum: prefer the v1alpha1 native location, fall back to build (v1beta1 location). + switch { + case m.AggregateChecksum != "": + meta.AggregateChecksum = m.AggregateChecksum + case b.AggregateChecksum != "": + meta.AggregateChecksum = b.AggregateChecksum + } + + // Restore v1alpha1-only metadata fields from annotations if the generic fields are empty. + // This handles the case where data originated from v1beta1 and the fields were stored as annotations. + if m.Annotations != nil { + restore := map[string]*string{ + "metadata.url": &meta.URL, + "metadata.image": &meta.Image, + "metadata.authors": &meta.Authors, + "metadata.documentation": &meta.Documentation, + "metadata.source": &meta.Source, + "metadata.vendor": &meta.Vendor, + } + annotations := make(map[string]string) + for k, v := range m.Annotations { + if target, ok := restore[k]; ok { + if *target == "" { + *target = v + } + continue + } + annotations[k] = v + } + if len(annotations) > 0 { + meta.Annotations = annotations + } + } + + return meta +} + +func buildFromGeneric(b types.BuildData) v1alpha1.ZarfBuildData { + out := v1alpha1.ZarfBuildData{ + Terminal: b.Hostname, + User: b.User, + Architecture: b.Architecture, + Timestamp: b.Timestamp, + Version: b.Version, + Migrations: b.Migrations, + RegistryOverrides: b.RegistryOverrides, + Differential: b.Differential, + DifferentialPackageVersion: b.DifferentialPackageVersion, + DifferentialMissing: b.DifferentialMissing, + Flavor: b.Flavor, + Signed: b.Signed, + ProvenanceFiles: b.ProvenanceFiles, + } + + for _, vr := range b.VersionRequirements { + out.VersionRequirements = append(out.VersionRequirements, v1alpha1.VersionRequirement{ + Version: vr.Version, + Reason: vr.Reason, + }) + } + + out.SetOriginalAPIVersion(b.OriginalAPIVersion) + + return out +} + +func componentFromGeneric(c types.Component) v1alpha1.ZarfComponent { + ac := v1alpha1.ZarfComponent{ + Name: c.Name, + Description: c.Description, + Default: c.Default, + Required: requiredFromGeneric(c.Optional, c.Required), + DeprecatedGroup: c.Group, + DataInjections: dataInjectionsFromGeneric(c.DataInjections), + HealthChecks: healthChecksFromGeneric(c.HealthChecks), + Repos: c.Repositories, + Only: v1alpha1.ZarfComponentOnlyTarget{ + LocalOS: c.Target.OS, + Cluster: v1alpha1.ZarfComponentOnlyCluster{ + Architecture: c.Target.Architecture, + Distros: c.Distros, + }, + Flavor: c.Target.Flavor, + }, + Import: v1alpha1.ZarfComponentImport{ + Name: c.Import.Name, + Path: c.Import.Path, + URL: c.Import.URL, + }, + Actions: actionsFromGeneric(c.Actions), + } + + // If the v1alpha1 single-path import fields are empty but the v1beta1 lists have one entry, project it back. + if ac.Import.Path == "" && len(c.Import.Local) > 0 { + ac.Import.Path = c.Import.Local[0].Path + } + if ac.Import.URL == "" && len(c.Import.Remote) > 0 { + ac.Import.URL = c.Import.Remote[0].URL + } + + for _, m := range c.Manifests { + ac.Manifests = append(ac.Manifests, manifestFromGeneric(m)) + } + + for _, ch := range c.Charts { + ac.Charts = append(ac.Charts, chartFromGeneric(ch)) + } + + for _, f := range c.Files { + af := v1alpha1.ZarfFile{ + Source: f.Source, + Shasum: f.Checksum, + Target: f.Destination, + Executable: f.Executable, + Symlinks: f.Symlinks, + ExtractPath: f.ExtractPath, + } + if f.EnableValues { + t := true + af.Template = &t + } + ac.Files = append(ac.Files, af) + } + + for _, img := range c.Images { + ac.Images = append(ac.Images, img.Name) + } + + for _, ia := range c.ImageArchives { + ac.ImageArchives = append(ac.ImageArchives, v1alpha1.ImageArchive{ + Path: ia.Path, + Images: ia.Images, + }) + } + + return ac +} + +// requiredFromGeneric maps the v1beta1 Optional and v1alpha1 Required back to v1alpha1 Required. +// Prefers preserved v1alpha1 Required; otherwise inverts Optional. +func requiredFromGeneric(optional bool, required *bool) *bool { + if required != nil { + return required + } + v := !optional + return &v +} + +func manifestFromGeneric(m types.Manifest) v1alpha1.ZarfManifest { + am := v1alpha1.ZarfManifest{ + Name: m.Name, + Namespace: m.Namespace, + Files: m.Files, + ServerSideApply: m.ServerSideApply, + NoWait: m.SkipWait, + Template: m.Template, + } + if m.Kustomize != nil { + am.Kustomizations = m.Kustomize.Files + am.KustomizeAllowAnyDirectory = m.Kustomize.AllowAnyDirectory + am.EnableKustomizePlugins = m.Kustomize.EnablePlugins + } + if am.Template == nil && m.EnableValues { + t := true + am.Template = &t + } + return am +} + +func chartFromGeneric(ch types.Chart) v1alpha1.ZarfChart { + ac := v1alpha1.ZarfChart{ + Name: ch.Name, + Namespace: ch.Namespace, + ReleaseName: ch.ReleaseName, + ValuesFiles: ch.ValuesFiles, + ServerSideApply: ch.ServerSideApply, + NoWait: ch.SkipWait, + URL: ch.URL, + RepoName: ch.RepoName, + GitPath: ch.GitPath, + LocalPath: ch.LocalPath, + Version: ch.Version, + Variables: chartVarsFromGeneric(ch.Variables), + } + + // Prefer preserved v1alpha1 SchemaValidation; otherwise derive from SkipSchemaValidation. + if ch.SchemaValidation != nil { + ac.SchemaValidation = ch.SchemaValidation + } else if ch.SkipSchemaValidation { + f := false + ac.SchemaValidation = &f + } + + // If flat fields are empty but structured sources are populated, project them onto the flat fields. + if ac.URL == "" && ac.LocalPath == "" { + switch { + case ch.HelmRepository != nil && ch.HelmRepository.URL != "": + ac.URL = ch.HelmRepository.URL + ac.RepoName = ch.HelmRepository.Name + if ac.Version == "" { + ac.Version = ch.HelmRepository.Version + } + case ch.OCI != nil && ch.OCI.URL != "": + ac.URL = ch.OCI.URL + if ac.Version == "" { + ac.Version = ch.OCI.Version + } + case ch.Git != nil && ch.Git.URL != "": + gitURL := ch.Git.URL + if idx := strings.LastIndex(gitURL, "@"); idx > 0 { + if ac.Version == "" { + ac.Version = gitURL[idx+1:] + } + gitURL = gitURL[:idx] + } + ac.URL = gitURL + ac.GitPath = ch.Git.Path + case ch.Local != nil && ch.Local.Path != "": + ac.LocalPath = ch.Local.Path + } + } + + for _, v := range ch.Values { + ac.Values = append(ac.Values, v1alpha1.ZarfChartValue{ + SourcePath: v.SourcePath, + TargetPath: v.TargetPath, + }) + } + + return ac +} + +func actionsFromGeneric(a types.ComponentActions) v1alpha1.ZarfComponentActions { + return v1alpha1.ZarfComponentActions{ + OnCreate: actionSetFromGeneric(a.OnCreate), + OnDeploy: actionSetFromGeneric(a.OnDeploy), + OnRemove: actionSetFromGeneric(a.OnRemove), + } +} + +func actionSetFromGeneric(s types.ComponentActionSet) v1alpha1.ZarfComponentActionSet { + defaults := v1alpha1.ZarfComponentActionDefaults{ + Mute: s.Defaults.Silent, + MaxTotalSeconds: int(s.Defaults.MaxTotalSeconds), + MaxRetries: int(s.Defaults.Retries), + Dir: s.Defaults.Dir, + Env: s.Defaults.Env, + Shell: v1alpha1.Shell{ + Windows: s.Defaults.Shell.Windows, + Linux: s.Defaults.Shell.Linux, + Darwin: s.Defaults.Shell.Darwin, + }, + } + + return v1alpha1.ZarfComponentActionSet{ + Defaults: defaults, + Before: actionSliceFromGeneric(s.Before), + OnSuccess: actionSliceFromGeneric(s.OnSuccess), + OnFailure: actionSliceFromGeneric(s.OnFailure), + } +} + +func actionSliceFromGeneric(actions []types.ComponentAction) []v1alpha1.ZarfComponentAction { + var out []v1alpha1.ZarfComponentAction + for _, a := range actions { + out = append(out, actionFromGeneric(a)) + } + return out +} + +func actionFromGeneric(a types.ComponentAction) v1alpha1.ZarfComponentAction { + aa := v1alpha1.ZarfComponentAction{ + Mute: a.Silent, + Dir: a.Dir, + Env: a.Env, + Cmd: a.Cmd, + Description: a.Description, + Wait: waitFromGeneric(a.Wait), + SetVariables: varsFromGeneric(a.SetVariables), + DeprecatedSetVariable: a.DeprecatedSetVariable, + } + + if a.MaxTotalSeconds != nil { + v := int(*a.MaxTotalSeconds) + aa.MaxTotalSeconds = &v + } + if a.Retries != nil { + v := int(*a.Retries) + aa.MaxRetries = &v + } + if a.EnableValues { + t := true + aa.Template = &t + } + + for _, sv := range a.SetValues { + aa.SetValues = append(aa.SetValues, v1alpha1.SetValue{ + Key: sv.Key, + Value: sv.Value, + Type: v1alpha1.SetValueType(sv.Type), + }) + } + + if a.Shell != nil { + aa.Shell = &v1alpha1.Shell{ + Windows: a.Shell.Windows, + Linux: a.Shell.Linux, + Darwin: a.Shell.Darwin, + } + } + + return aa +} + +func waitFromGeneric(w *types.ComponentActionWait) *v1alpha1.ZarfComponentActionWait { + if w == nil { + return nil + } + aw := &v1alpha1.ZarfComponentActionWait{} + if w.Cluster != nil { + aw.Cluster = &v1alpha1.ZarfComponentActionWaitCluster{ + Kind: w.Cluster.Kind, + Name: w.Cluster.Name, + Namespace: w.Cluster.Namespace, + Condition: w.Cluster.Condition, + } + } + if w.Network != nil { + aw.Network = &v1alpha1.ZarfComponentActionWaitNetwork{ + Protocol: w.Network.Protocol, + Address: w.Network.Address, + Code: int(w.Network.Code), + } + } + return aw +} + +func derefBool(p *bool) bool { + if p == nil { + return false + } + return *p +} + +func variableToGeneric(v v1alpha1.Variable) types.Variable { + return types.Variable{ + Name: v.Name, + Sensitive: v.Sensitive, + AutoIndent: v.AutoIndent, + Pattern: v.Pattern, + Type: types.VariableType(v.Type), + } +} + +func variableFromGeneric(v types.Variable) v1alpha1.Variable { + return v1alpha1.Variable{ + Name: v.Name, + Sensitive: v.Sensitive, + AutoIndent: v.AutoIndent, + Pattern: v.Pattern, + Type: v1alpha1.VariableType(v.Type), + } +} + +func varsToGeneric(in []v1alpha1.Variable) []types.Variable { + var out []types.Variable + for _, v := range in { + out = append(out, variableToGeneric(v)) + } + return out +} + +func varsFromGeneric(in []types.Variable) []v1alpha1.Variable { + var out []v1alpha1.Variable + for _, v := range in { + out = append(out, variableFromGeneric(v)) + } + return out +} + +func interactiveVarsToGeneric(in []v1alpha1.InteractiveVariable) []types.InteractiveVariable { + var out []types.InteractiveVariable + for _, v := range in { + out = append(out, types.InteractiveVariable{ + Variable: variableToGeneric(v.Variable), + Description: v.Description, + Default: v.Default, + Prompt: v.Prompt, + }) + } + return out +} + +func interactiveVarsFromGeneric(in []types.InteractiveVariable) []v1alpha1.InteractiveVariable { + var out []v1alpha1.InteractiveVariable + for _, v := range in { + out = append(out, v1alpha1.InteractiveVariable{ + Variable: variableFromGeneric(v.Variable), + Description: v.Description, + Default: v.Default, + Prompt: v.Prompt, + }) + } + return out +} + +func constantsToGeneric(in []v1alpha1.Constant) []types.Constant { + var out []types.Constant + for _, c := range in { + out = append(out, types.Constant{ + Name: c.Name, + Value: c.Value, + Description: c.Description, + AutoIndent: c.AutoIndent, + Pattern: c.Pattern, + }) + } + return out +} + +func constantsFromGeneric(in []types.Constant) []v1alpha1.Constant { + var out []v1alpha1.Constant + for _, c := range in { + out = append(out, v1alpha1.Constant{ + Name: c.Name, + Value: c.Value, + Description: c.Description, + AutoIndent: c.AutoIndent, + Pattern: c.Pattern, + }) + } + return out +} + +func chartVarsToGeneric(in []v1alpha1.ZarfChartVariable) []types.ZarfChartVariable { + var out []types.ZarfChartVariable + for _, v := range in { + out = append(out, types.ZarfChartVariable{Name: v.Name, Description: v.Description, Path: v.Path}) + } + return out +} + +func chartVarsFromGeneric(in []types.ZarfChartVariable) []v1alpha1.ZarfChartVariable { + var out []v1alpha1.ZarfChartVariable + for _, v := range in { + out = append(out, v1alpha1.ZarfChartVariable{Name: v.Name, Description: v.Description, Path: v.Path}) + } + return out +} + +func dataInjectionsToGeneric(in []v1alpha1.ZarfDataInjection) []types.ZarfDataInjection { + var out []types.ZarfDataInjection + for _, d := range in { + out = append(out, types.ZarfDataInjection{ + Source: d.Source, + Target: types.ZarfContainerTarget{ + Namespace: d.Target.Namespace, + Selector: d.Target.Selector, + Container: d.Target.Container, + Path: d.Target.Path, + }, + Compress: d.Compress, + }) + } + return out +} + +func dataInjectionsFromGeneric(in []types.ZarfDataInjection) []v1alpha1.ZarfDataInjection { + var out []v1alpha1.ZarfDataInjection + for _, d := range in { + out = append(out, v1alpha1.ZarfDataInjection{ + Source: d.Source, + Target: v1alpha1.ZarfContainerTarget{ + Namespace: d.Target.Namespace, + Selector: d.Target.Selector, + Container: d.Target.Container, + Path: d.Target.Path, + }, + Compress: d.Compress, + }) + } + return out +} + +func healthChecksToGeneric(in []v1alpha1.NamespacedObjectKindReference) []types.NamespacedObjectKindReference { + var out []types.NamespacedObjectKindReference + for _, h := range in { + out = append(out, types.NamespacedObjectKindReference{ + APIVersion: h.APIVersion, + Kind: h.Kind, + Namespace: h.Namespace, + Name: h.Name, + }) + } + return out +} + +func healthChecksFromGeneric(in []types.NamespacedObjectKindReference) []v1alpha1.NamespacedObjectKindReference { + var out []v1alpha1.NamespacedObjectKindReference + for _, h := range in { + out = append(out, v1alpha1.NamespacedObjectKindReference{ + APIVersion: h.APIVersion, + Kind: h.Kind, + Namespace: h.Namespace, + Name: h.Name, + }) + } + return out +} diff --git a/src/internal/api/v1beta1/convert.go b/src/internal/api/v1beta1/convert.go new file mode 100644 index 0000000000..541857e8c2 --- /dev/null +++ b/src/internal/api/v1beta1/convert.go @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package v1beta1 contains functions for converting between the public v1beta1 Zarf package and the internal generic representation. +package v1beta1 + +import ( + "strings" + + "github.com/zarf-dev/zarf/src/api/v1beta1" + "github.com/zarf-dev/zarf/src/internal/api/types" +) + +// ConvertToGeneric converts a v1beta1 Package to the internal generic representation. +func ConvertToGeneric(pkg v1beta1.Package) types.Package { + // Preserve an already-recorded original across multi-hop conversions; otherwise this is the original. + originalAPIVersion := pkg.Build.GetOriginalAPIVersion() + if originalAPIVersion == "" { + originalAPIVersion = pkg.APIVersion + } + g := types.Package{ + APIVersion: pkg.APIVersion, + Kind: string(pkg.Kind), + Metadata: types.PackageMetadata{ + Name: pkg.Metadata.Name, + Description: pkg.Metadata.Description, + Version: pkg.Metadata.Version, + Uncompressed: pkg.Metadata.Uncompressed, + Architecture: pkg.Metadata.Architecture, + Annotations: pkg.Metadata.Annotations, + PreventNamespaceOverride: pkg.Metadata.PreventNamespaceOverride, + }, + Build: types.BuildData{ + Hostname: pkg.Build.Hostname, + User: pkg.Build.User, + Architecture: pkg.Build.Architecture, + Timestamp: pkg.Build.Timestamp, + Version: pkg.Build.Version, + Migrations: pkg.Build.Migrations, + RegistryOverrides: pkg.Build.RegistryOverrides, + Differential: pkg.Build.Differential, + DifferentialPackageVersion: pkg.Build.DifferentialPackageVersion, + Flavor: pkg.Build.Flavor, + Signed: pkg.Build.Signed, + ProvenanceFiles: pkg.Build.ProvenanceFiles, + AggregateChecksum: pkg.Build.AggregateChecksum, + OriginalAPIVersion: originalAPIVersion, + }, + Values: types.Values{ + Files: pkg.Values.Files, + Schema: pkg.Values.Schema, + }, + Documentation: pkg.Documentation, + Variables: deprecatedVarsToGeneric(pkg.GetDeprecatedVariables()), + Constants: deprecatedConstantsToGeneric(pkg.GetDeprecatedConstants()), + } + + for _, vr := range pkg.Build.VersionRequirements { + g.Build.VersionRequirements = append(g.Build.VersionRequirements, types.VersionRequirement{ + Version: vr.Version, + Reason: vr.Reason, + }) + } + + for _, c := range pkg.Components { + g.Components = append(g.Components, componentToGeneric(c)) + } + + return g +} + +func componentToGeneric(c v1beta1.Component) types.Component { + gc := types.Component{ + Name: c.Name, + Description: c.Description, + Optional: c.Optional, + Service: string(c.Service), + Repositories: c.Repositories, + Target: types.ComponentTarget{ + OS: c.Target.OS, + Architecture: c.Target.Architecture, + Flavor: c.Target.Flavor, + }, + Import: importToGeneric(c.Import), + Actions: actionsToGeneric(c.Actions), + DataInjections: deprecatedDataInjectionsToGeneric(c.GetDeprecatedDataInjections()), + } + + for _, m := range c.Manifests { + gc.Manifests = append(gc.Manifests, manifestToGeneric(m)) + } + + for _, ch := range c.Charts { + gc.Charts = append(gc.Charts, chartToGeneric(ch)) + } + + for _, f := range c.Files { + gc.Files = append(gc.Files, types.File{ + Source: f.Source, + Checksum: f.Checksum, + Destination: f.Destination, + Executable: f.Executable, + Symlinks: f.Symlinks, + ExtractPath: f.ExtractPath, + EnableValues: f.EnableValues, + }) + } + + for _, img := range c.Images { + gc.Images = append(gc.Images, types.Image{ + Name: img.Name, + Source: img.Source, + }) + } + + for _, ia := range c.ImageArchives { + gc.ImageArchives = append(gc.ImageArchives, types.ImageArchive{ + Path: ia.Path, + Images: ia.Images, + }) + } + + return gc +} + +func importToGeneric(imp v1beta1.ComponentImport) types.ComponentImport { + out := types.ComponentImport{} + for _, l := range imp.Local { + out.Local = append(out.Local, types.ComponentImportLocal{Path: l.Path}) + } + for _, r := range imp.Remote { + out.Remote = append(out.Remote, types.ComponentImportRemote{URL: r.URL}) + } + return out +} + +func manifestToGeneric(m v1beta1.Manifest) types.Manifest { + gm := types.Manifest{ + Name: m.Name, + Namespace: m.Namespace, + Files: m.Files, + SkipWait: m.SkipWait, + ServerSideApply: string(m.ServerSideApply), + EnableValues: m.EnableValues, + } + if m.Kustomize != nil { + gm.Kustomize = &types.KustomizeManifest{ + Files: m.Kustomize.Files, + AllowAnyDirectory: m.Kustomize.AllowAnyDirectory, + EnablePlugins: m.Kustomize.EnablePlugins, + } + } + return gm +} + +func chartToGeneric(ch v1beta1.Chart) types.Chart { + gc := types.Chart{ + Name: ch.Name, + Namespace: ch.Namespace, + ReleaseName: ch.ReleaseName, + ValuesFiles: ch.ValuesFiles, + SkipSchemaValidation: ch.SkipSchemaValidation, + ServerSideApply: string(ch.ServerSideApply), + SkipWait: ch.SkipWait, + Version: ch.GetDeprecatedVersion(), + Variables: deprecatedChartVarsToGeneric(ch.GetDeprecatedVariables()), + } + + if ch.HelmRepository != nil { + gc.HelmRepository = &types.HelmRepositorySource{ + Name: ch.HelmRepository.Name, + URL: ch.HelmRepository.URL, + Version: ch.HelmRepository.Version, + } + } + if ch.Git != nil { + gc.Git = &types.GitSource{ + URL: ch.Git.URL, + Path: ch.Git.Path, + } + } + if ch.Local != nil { + gc.Local = &types.LocalSource{Path: ch.Local.Path} + } + if ch.OCI != nil { + gc.OCI = &types.OCISource{ + URL: ch.OCI.URL, + Version: ch.OCI.Version, + } + } + + for _, v := range ch.Values { + gc.Values = append(gc.Values, types.ChartValue{ + SourcePath: v.SourcePath, + TargetPath: v.TargetPath, + }) + } + + return gc +} + +func actionsToGeneric(a v1beta1.ComponentActions) types.ComponentActions { + return types.ComponentActions{ + OnCreate: actionSetToGeneric(a.OnCreate), + OnDeploy: actionSetToGeneric(a.OnDeploy), + OnRemove: actionSetToGeneric(a.OnRemove), + } +} + +func actionSetToGeneric(s v1beta1.ComponentActionSet) types.ComponentActionSet { + return types.ComponentActionSet{ + Defaults: types.ComponentActionDefaults{ + Silent: s.Defaults.Silent, + MaxTotalSeconds: s.Defaults.MaxTotalSeconds, + Retries: s.Defaults.Retries, + Dir: s.Defaults.Dir, + Env: s.Defaults.Env, + Shell: types.Shell{ + Windows: s.Defaults.Shell.Windows, + Linux: s.Defaults.Shell.Linux, + Darwin: s.Defaults.Shell.Darwin, + }, + }, + Before: actionSliceToGeneric(s.Before), + OnSuccess: actionSliceToGeneric(s.OnSuccess), + OnFailure: actionSliceToGeneric(s.OnFailure), + } +} + +func actionSliceToGeneric(actions []v1beta1.ComponentAction) []types.ComponentAction { + var out []types.ComponentAction + for _, a := range actions { + out = append(out, actionToGeneric(a)) + } + return out +} + +func actionToGeneric(a v1beta1.ComponentAction) types.ComponentAction { + ga := types.ComponentAction{ + Silent: a.Silent, + MaxTotalSeconds: a.MaxTotalSeconds, + Retries: a.Retries, + Dir: a.Dir, + Env: a.Env, + Cmd: a.Cmd, + Description: a.Description, + Wait: waitToGeneric(a.Wait), + EnableValues: a.EnableValues, + SetVariables: deprecatedSetVarsToGeneric(a.GetDeprecatedSetVariables()), + } + + for _, sv := range a.SetValues { + ga.SetValues = append(ga.SetValues, types.SetValue{ + Key: sv.Key, + Type: string(sv.Type), + }) + } + + if a.Shell != nil { + ga.Shell = &types.Shell{ + Windows: a.Shell.Windows, + Linux: a.Shell.Linux, + Darwin: a.Shell.Darwin, + } + } + + return ga +} + +func waitToGeneric(w *v1beta1.ComponentActionWait) *types.ComponentActionWait { + if w == nil { + return nil + } + gw := &types.ComponentActionWait{} + if w.Cluster != nil { + gw.Cluster = &types.ComponentActionWaitCluster{ + Kind: w.Cluster.Kind, + Name: w.Cluster.Name, + Namespace: w.Cluster.Namespace, + Condition: w.Cluster.Condition, + } + } + if w.Network != nil { + gw.Network = &types.ComponentActionWaitNetwork{ + Protocol: w.Network.Protocol, + Address: w.Network.Address, + Code: w.Network.Code, + } + } + return gw +} + +// ConvertFromGeneric converts the internal generic representation to a v1beta1 Package. +func ConvertFromGeneric(g types.Package) v1beta1.Package { + pkg := v1beta1.Package{ + APIVersion: v1beta1.APIVersion, + Kind: v1beta1.PackageKind(g.Kind), + Metadata: metadataFromGeneric(g.Metadata), + Build: buildFromGeneric(g.Build, g.Metadata), + Values: v1beta1.Values{Files: g.Values.Files, Schema: g.Values.Schema}, + Documentation: g.Documentation, + } + + if pkg.Kind == "" { + pkg.Kind = v1beta1.ZarfPackageConfig + } + + // v1beta1 has no Kind ZarfInitConfig; collapse the v1alpha1 init kind into the normal package kind. + if string(pkg.Kind) == "ZarfInitConfig" { + pkg.Kind = v1beta1.ZarfPackageConfig + } + + for _, c := range g.Components { + pkg.Components = append(pkg.Components, componentFromGeneric(c)) + } + + return pkg +} + +func metadataFromGeneric(m types.PackageMetadata) v1beta1.PackageMetadata { + meta := v1beta1.PackageMetadata{ + Name: m.Name, + Description: m.Description, + Version: m.Version, + Uncompressed: m.Uncompressed, + Architecture: m.Architecture, + Annotations: m.Annotations, + } + + // Map v1alpha1 AllowNamespaceOverride (*bool, default allow) onto v1beta1 PreventNamespaceOverride (bool, default allow). + if m.AllowNamespaceOverride != nil { + meta.PreventNamespaceOverride = !*m.AllowNamespaceOverride + } else { + meta.PreventNamespaceOverride = m.PreventNamespaceOverride + } + + // Migrate v1alpha1-only metadata fields into annotations. + extras := map[string]string{ + "metadata.url": m.URL, + "metadata.image": m.Image, + "metadata.authors": m.Authors, + "metadata.documentation": m.Documentation, + "metadata.source": m.Source, + "metadata.vendor": m.Vendor, + } + for k, v := range extras { + if v == "" { + continue + } + if meta.Annotations == nil { + meta.Annotations = make(map[string]string) + } + meta.Annotations[k] = v + } + + return meta +} + +func buildFromGeneric(b types.BuildData, m types.PackageMetadata) v1beta1.BuildData { + out := v1beta1.BuildData{ + Hostname: b.Hostname, + User: b.User, + Architecture: b.Architecture, + Timestamp: b.Timestamp, + Version: b.Version, + Migrations: b.Migrations, + RegistryOverrides: b.RegistryOverrides, + Differential: b.Differential, + DifferentialPackageVersion: b.DifferentialPackageVersion, + Flavor: b.Flavor, + Signed: b.Signed, + ProvenanceFiles: b.ProvenanceFiles, + } + + // AggregateChecksum lives in metadata in v1alpha1, build in v1beta1. + switch { + case b.AggregateChecksum != "": + out.AggregateChecksum = b.AggregateChecksum + case m.AggregateChecksum != "": + out.AggregateChecksum = m.AggregateChecksum + } + + for _, vr := range b.VersionRequirements { + out.VersionRequirements = append(out.VersionRequirements, v1beta1.VersionRequirement{ + Version: vr.Version, + Reason: vr.Reason, + }) + } + + out.SetOriginalAPIVersion(b.OriginalAPIVersion) + + return out +} + +func componentFromGeneric(c types.Component) v1beta1.Component { + bc := v1beta1.Component{ + Name: c.Name, + Description: c.Description, + Optional: optionalFromGeneric(c.Optional, c.Required), + ComponentSpec: v1beta1.ComponentSpec{ + Repositories: c.Repositories, + Target: v1beta1.ComponentTarget{ + OS: c.Target.OS, + Architecture: c.Target.Architecture, + Flavor: c.Target.Flavor, + }, + Import: importFromGeneric(c.Import), + Service: serviceFromGeneric(c), + Actions: actionsFromGeneric(c.Actions), + }, + } + + for _, m := range c.Manifests { + bc.Manifests = append(bc.Manifests, manifestFromGeneric(m)) + } + + for _, ch := range c.Charts { + bc.Charts = append(bc.Charts, chartFromGeneric(ch)) + } + + for _, f := range c.Files { + bc.Files = append(bc.Files, v1beta1.File{ + Source: f.Source, + Checksum: f.Checksum, + Destination: f.Destination, + Executable: f.Executable, + Symlinks: f.Symlinks, + ExtractPath: f.ExtractPath, + EnableValues: f.EnableValues, + }) + } + + for _, img := range c.Images { + bc.Images = append(bc.Images, v1beta1.Image{ + Name: img.Name, + Source: img.Source, + }) + } + + for _, ia := range c.ImageArchives { + bc.ImageArchives = append(bc.ImageArchives, v1beta1.ImageArchive{ + Path: ia.Path, + Images: ia.Images, + }) + } + + // Convert v1alpha1 HealthChecks into onDeploy onSuccess wait actions. + for _, hc := range c.HealthChecks { + bc.Actions.OnDeploy.OnSuccess = append(bc.Actions.OnDeploy.OnSuccess, v1beta1.ComponentAction{ + Wait: &v1beta1.ComponentActionWait{ + Cluster: &v1beta1.ComponentActionWaitCluster{ + Kind: healthCheckKind(hc.Kind, hc.APIVersion), + Name: hc.Name, + Namespace: hc.Namespace, + }, + }, + }) + } + + return bc +} + +// optionalFromGeneric maps the v1alpha1 Required *bool and v1beta1 Optional bool onto a single v1beta1 Optional bool. +// v1alpha1: Required=nil/false → Optional=true; Required=true → Optional=false. +// v1beta1: Optional flows through directly when Required is nil. +func optionalFromGeneric(optional bool, required *bool) bool { + if required != nil { + return !*required + } + return optional +} + +func serviceFromGeneric(c types.Component) v1beta1.Service { + if c.Service != "" { + return v1beta1.Service(c.Service) + } + // Infer the v1beta1 Service from well-known v1alpha1 component names. + switch c.Name { + case "zarf-registry": + return v1beta1.ServiceRegistry + case "zarf-seed-registry": + return v1beta1.ServiceSeedRegistry + case "zarf-injector": + return v1beta1.ServiceInjector + case "zarf-agent": + return v1beta1.ServiceAgent + case "git-server": + return v1beta1.ServiceGitServer + } + return "" +} + +func importFromGeneric(imp types.ComponentImport) v1beta1.ComponentImport { + out := v1beta1.ComponentImport{} + for _, l := range imp.Local { + out.Local = append(out.Local, v1beta1.ComponentImportLocal{Path: l.Path}) + } + for _, r := range imp.Remote { + out.Remote = append(out.Remote, v1beta1.ComponentImportRemote{URL: r.URL}) + } + // Promote v1alpha1 single-import fields when no structured imports are present. + if len(out.Local) == 0 && imp.Path != "" { + out.Local = append(out.Local, v1beta1.ComponentImportLocal{Path: imp.Path}) + } + if len(out.Remote) == 0 && imp.URL != "" { + out.Remote = append(out.Remote, v1beta1.ComponentImportRemote{URL: imp.URL}) + } + return out +} + +func manifestFromGeneric(m types.Manifest) v1beta1.Manifest { + bm := v1beta1.Manifest{ + Name: m.Name, + Namespace: m.Namespace, + Files: m.Files, + SkipWait: m.SkipWait, + ServerSideApply: v1beta1.ServerSideApplyMode(m.ServerSideApply), + EnableValues: m.EnableValues, + } + if m.Kustomize != nil { + bm.Kustomize = &v1beta1.KustomizeManifest{ + Files: m.Kustomize.Files, + AllowAnyDirectory: m.Kustomize.AllowAnyDirectory, + EnablePlugins: m.Kustomize.EnablePlugins, + } + } + // v1alpha1 Template *bool maps onto EnableValues when it is explicitly true. + if !bm.EnableValues && m.Template != nil && *m.Template { + bm.EnableValues = true + } + return bm +} + +func chartFromGeneric(ch types.Chart) v1beta1.Chart { + bc := v1beta1.Chart{ + Name: ch.Name, + Namespace: ch.Namespace, + ReleaseName: ch.ReleaseName, + ValuesFiles: ch.ValuesFiles, + SkipSchemaValidation: ch.SkipSchemaValidation, + ServerSideApply: v1beta1.ServerSideApplyMode(ch.ServerSideApply), + SkipWait: ch.SkipWait, + Values: chartValuesFromGeneric(ch.Values), + } + + // Use the structured sources if present; otherwise infer from v1alpha1 flat fields. + switch { + case ch.HelmRepository != nil: + bc.HelmRepository = &v1beta1.HelmRepositorySource{ + Name: ch.HelmRepository.Name, + URL: ch.HelmRepository.URL, + Version: ch.HelmRepository.Version, + } + case ch.Git != nil: + bc.Git = &v1beta1.GitSource{URL: ch.Git.URL, Path: ch.Git.Path} + case ch.Local != nil: + bc.Local = &v1beta1.LocalSource{Path: ch.Local.Path} + case ch.OCI != nil: + bc.OCI = &v1beta1.OCISource{URL: ch.OCI.URL, Version: ch.OCI.Version} + case ch.URL != "": + switch { + case strings.HasPrefix(ch.URL, "oci://"): + bc.OCI = &v1beta1.OCISource{URL: ch.URL, Version: ch.Version} + case ch.GitPath != "" || isGitURL(ch.URL): + gitURL := ch.URL + if ch.Version != "" && !strings.Contains(ch.URL, "@") { + gitURL += "@" + ch.Version + } + bc.Git = &v1beta1.GitSource{URL: gitURL, Path: ch.GitPath} + default: + bc.HelmRepository = &v1beta1.HelmRepositorySource{ + Name: ch.RepoName, + URL: ch.URL, + Version: ch.Version, + } + } + case ch.LocalPath != "": + bc.Local = &v1beta1.LocalSource{Path: ch.LocalPath} + } + + // v1alpha1 SchemaValidation *bool: nil/true → SkipSchemaValidation=false; explicit false → true. + if !bc.SkipSchemaValidation && ch.SchemaValidation != nil && !*ch.SchemaValidation { + bc.SkipSchemaValidation = true + } + + return bc +} + +func chartValuesFromGeneric(vals []types.ChartValue) []v1beta1.ChartValue { + var out []v1beta1.ChartValue + for _, v := range vals { + out = append(out, v1beta1.ChartValue{ + SourcePath: v.SourcePath, + TargetPath: v.TargetPath, + }) + } + return out +} + +func actionsFromGeneric(a types.ComponentActions) v1beta1.ComponentActions { + return v1beta1.ComponentActions{ + OnCreate: actionSetFromGeneric(a.OnCreate), + OnDeploy: actionSetFromGeneric(a.OnDeploy), + OnRemove: actionSetFromGeneric(a.OnRemove), + } +} + +func actionSetFromGeneric(s types.ComponentActionSet) v1beta1.ComponentActionSet { + return v1beta1.ComponentActionSet{ + Defaults: v1beta1.ComponentActionDefaults{ + Silent: s.Defaults.Silent, + MaxTotalSeconds: s.Defaults.MaxTotalSeconds, + Retries: s.Defaults.Retries, + Dir: s.Defaults.Dir, + Env: s.Defaults.Env, + Shell: v1beta1.Shell{ + Windows: s.Defaults.Shell.Windows, + Linux: s.Defaults.Shell.Linux, + Darwin: s.Defaults.Shell.Darwin, + }, + }, + Before: actionSliceFromGeneric(s.Before), + OnSuccess: actionSliceFromGeneric(s.OnSuccess), + OnFailure: actionSliceFromGeneric(s.OnFailure), + } +} + +func actionSliceFromGeneric(actions []types.ComponentAction) []v1beta1.ComponentAction { + var out []v1beta1.ComponentAction + for _, a := range actions { + out = append(out, actionFromGeneric(a)) + } + return out +} + +func actionFromGeneric(a types.ComponentAction) v1beta1.ComponentAction { + ba := v1beta1.ComponentAction{ + Silent: a.Silent, + MaxTotalSeconds: a.MaxTotalSeconds, + Retries: a.Retries, + Dir: a.Dir, + Env: a.Env, + Cmd: a.Cmd, + Description: a.Description, + Wait: waitFromGeneric(a.Wait), + EnableValues: a.EnableValues, + } + + for _, sv := range a.SetValues { + ba.SetValues = append(ba.SetValues, v1beta1.SetValue{ + Key: sv.Key, + Type: v1beta1.SetValueType(sv.Type), + }) + } + + if a.Shell != nil { + ba.Shell = &v1beta1.Shell{ + Windows: a.Shell.Windows, + Linux: a.Shell.Linux, + Darwin: a.Shell.Darwin, + } + } + + return ba +} + +func waitFromGeneric(w *types.ComponentActionWait) *v1beta1.ComponentActionWait { + if w == nil { + return nil + } + bw := &v1beta1.ComponentActionWait{} + if w.Cluster != nil { + bw.Cluster = &v1beta1.ComponentActionWaitCluster{ + Kind: w.Cluster.Kind, + Name: w.Cluster.Name, + Namespace: w.Cluster.Namespace, + Condition: w.Cluster.Condition, + } + } + if w.Network != nil { + bw.Network = &v1beta1.ComponentActionWaitNetwork{ + Protocol: w.Network.Protocol, + Address: w.Network.Address, + Code: w.Network.Code, + } + } + return bw +} + +// healthCheckKind returns the wait-for kind string for a v1alpha1 health check. +// For resources with a group (e.g. APIVersion "apps/v1"), the format is ... +// For core resources with no group (e.g. APIVersion "v1"), the kind is returned as-is. +func healthCheckKind(kind, apiVersion string) string { + group, version, found := strings.Cut(apiVersion, "/") + if !found { + return kind + } + return kind + "." + version + "." + group +} + +func isGitURL(url string) bool { + if idx := strings.LastIndex(url, "@"); idx > 0 { + url = url[:idx] + } + return strings.HasSuffix(url, ".git") +} + +func deprecatedVarToGeneric(v v1beta1.Variable) types.Variable { + return types.Variable{ + Name: v.Name, + Sensitive: v.Sensitive, + AutoIndent: v.AutoIndent, + Pattern: v.Pattern, + Type: types.VariableType(v.Type), + } +} + +func deprecatedVarsToGeneric(in []v1beta1.InteractiveVariable) []types.InteractiveVariable { + var out []types.InteractiveVariable + for _, v := range in { + out = append(out, types.InteractiveVariable{ + Variable: deprecatedVarToGeneric(v.Variable), + Description: v.Description, + Default: v.Default, + Prompt: v.Prompt, + }) + } + return out +} + +func deprecatedConstantsToGeneric(in []v1beta1.Constant) []types.Constant { + var out []types.Constant + for _, c := range in { + out = append(out, types.Constant{ + Name: c.Name, + Value: c.Value, + Description: c.Description, + AutoIndent: c.AutoIndent, + Pattern: c.Pattern, + }) + } + return out +} + +func deprecatedChartVarsToGeneric(in []v1beta1.ZarfChartVariable) []types.ZarfChartVariable { + var out []types.ZarfChartVariable + for _, v := range in { + out = append(out, types.ZarfChartVariable{Name: v.Name, Description: v.Description, Path: v.Path}) + } + return out +} + +func deprecatedSetVarsToGeneric(in []v1beta1.Variable) []types.Variable { + var out []types.Variable + for _, v := range in { + out = append(out, deprecatedVarToGeneric(v)) + } + return out +} + +func deprecatedDataInjectionsToGeneric(in []v1beta1.ZarfDataInjection) []types.ZarfDataInjection { + var out []types.ZarfDataInjection + for _, d := range in { + out = append(out, types.ZarfDataInjection{ + Source: d.Source, + Target: types.ZarfContainerTarget{ + Namespace: d.Target.Namespace, + Selector: d.Target.Selector, + Container: d.Target.Container, + Path: d.Target.Path, + }, + Compress: d.Compress, + }) + } + return out +}