Skip to content

Commit

Permalink
feat: package generation ALPHA (#2269)
Browse files Browse the repository at this point in the history
## Description

Introduce Zarf package generation from a source

## Related Issue

Fixes #821

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [x] Test, docs, adr added or updated as needed
- [x] [Contributor Guide
Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow)
followed

---------

Signed-off-by: razzle <[email protected]>
Co-authored-by: corang <[email protected]>
Co-authored-by: razzle <[email protected]>
  • Loading branch information
3 people authored Mar 13, 2024
1 parent cc7b6fc commit 9608731
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 19 deletions.
1 change: 1 addition & 0 deletions docs/2-the-zarf-cli/100-cli-commands/zarf_dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Commands useful for developing packages
* [zarf](zarf.md) - DevSecOps for Airgap
* [zarf dev deploy](zarf_dev_deploy.md) - [beta] Creates and deploys a Zarf package from a given directory
* [zarf dev find-images](zarf_dev_find-images.md) - Evaluates components in a Zarf file to identify images specified in their helm charts and manifests
* [zarf dev generate](zarf_dev_generate.md) - [alpha] Creates a zarf.yaml automatically from a given remote (git) Helm chart
* [zarf dev generate-config](zarf_dev_generate-config.md) - Generates a config file for Zarf
* [zarf dev lint](zarf_dev_lint.md) - Lints the given package for valid schema and recommended practices
* [zarf dev patch-git](zarf_dev_patch-git.md) - Converts all .git URLs to the specified Zarf HOST and with the Zarf URL pattern in a given FILE. NOTE:
Expand Down
42 changes: 42 additions & 0 deletions docs/2-the-zarf-cli/100-cli-commands/zarf_dev_generate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# zarf dev generate
<!-- Auto-generated by hack/gen-cli-docs.sh -->

[alpha] Creates a zarf.yaml automatically from a given remote (git) Helm chart

```
zarf dev generate NAME [flags]
```

## Examples

```
zarf dev generate podinfo --url https://github.com/stefanprodan/podinfo.git --version 6.4.0 --gitPath charts/podinfo
```

## Options

```
--gitPath string Relative path to the chart in the git repository
-h, --help help for generate
--kube-version string Override the default helm template KubeVersion when performing a package chart template
--output-directory string Output directory for the generated zarf.yaml
--url string URL to the source git repository
--version string The Version of the chart to use
```

## Options inherited from parent commands

```
-a, --architecture string Architecture for OCI images and Zarf packages
--insecure Allow access to insecure registries and disable other recommended security enforcements such as package checksum and signature validation. This flag should only be used if you have a specific reason and accept the reduced security posture.
-l, --log-level string Log level when running Zarf. Valid options are: warn, info, debug, trace (default "info")
--no-color Disable colors in output
--no-log-file Disable log file creation
--no-progress Disable fancy UI progress bars, spinners, logos, etc
--tmpdir string Specify the temporary directory to use for intermediate files
--zarf-cache string Specify the location of the Zarf cache directory (default "~/.zarf-cache")
```

## SEE ALSO

* [zarf dev](zarf_dev.md) - Commands useful for developing packages
38 changes: 37 additions & 1 deletion src/cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ var devDeployCmd = &cobra.Command{
},
}

var devGenerateCmd = &cobra.Command{
Use: "generate NAME",
Aliases: []string{"g"},
Args: cobra.ExactArgs(1),
Short: lang.CmdDevGenerateShort,
Example: lang.CmdDevGenerateExample,
Run: func(_ *cobra.Command, args []string) {
pkgConfig.GenerateOpts.Name = args[0]

pkgConfig.CreateOpts.BaseDir = "."
pkgConfig.FindImagesOpts.RepoHelmChartPath = pkgConfig.GenerateOpts.GitPath

pkgClient := packager.NewOrDie(&pkgConfig)
defer pkgClient.ClearTempPaths()

if err := pkgClient.Generate(); err != nil {
message.Fatalf(err, err.Error())
}
},
}

var devTransformGitLinksCmd = &cobra.Command{
Use: "patch-git HOST FILE",
Aliases: []string{"p"},
Expand Down Expand Up @@ -186,7 +207,6 @@ var devFindImagesCmd = &cobra.Command{
Short: lang.CmdDevFindImagesShort,
Long: lang.CmdDevFindImagesLong,
Run: func(_ *cobra.Command, args []string) {
// If a directory was provided, use that as the base directory
common.SetBaseDirectory(args, &pkgConfig)

// Ensure uppercase keys from viper
Expand Down Expand Up @@ -256,13 +276,15 @@ func init() {
rootCmd.AddCommand(devCmd)

devCmd.AddCommand(devDeployCmd)
devCmd.AddCommand(devGenerateCmd)
devCmd.AddCommand(devTransformGitLinksCmd)
devCmd.AddCommand(devSha256SumCmd)
devCmd.AddCommand(devFindImagesCmd)
devCmd.AddCommand(devGenConfigFileCmd)
devCmd.AddCommand(devLintCmd)

bindDevDeployFlags(v)
bindDevGenerateFlags(v)

devSha256SumCmd.Flags().StringVarP(&extractPath, "extract-path", "e", "", lang.CmdDevFlagExtractPath)

Expand Down Expand Up @@ -307,3 +329,17 @@ func bindDevDeployFlags(v *viper.Viper) {

devDeployFlags.BoolVar(&pkgConfig.CreateOpts.NoYOLO, "no-yolo", v.GetBool(common.VDevDeployNoYolo), lang.CmdDevDeployFlagNoYolo)
}

func bindDevGenerateFlags(_ *viper.Viper) {
generateFlags := devGenerateCmd.Flags()

generateFlags.StringVar(&pkgConfig.GenerateOpts.URL, "url", "", "URL to the source git repository")
generateFlags.StringVar(&pkgConfig.GenerateOpts.Version, "version", "", "The Version of the chart to use")
generateFlags.StringVar(&pkgConfig.GenerateOpts.GitPath, "gitPath", "", "Relative path to the chart in the git repository")
generateFlags.StringVar(&pkgConfig.GenerateOpts.Output, "output-directory", "", "Output directory for the generated zarf.yaml")
generateFlags.StringVar(&pkgConfig.FindImagesOpts.KubeVersionOverride, "kube-version", "", lang.CmdDevFlagKubeVersion)

devGenerateCmd.MarkFlagRequired("url")
devGenerateCmd.MarkFlagRequired("version")
devGenerateCmd.MarkFlagRequired("output-directory")
}
7 changes: 6 additions & 1 deletion src/config/lang/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
// Alternative languages can be created by duplicating this file and changing the build tag to "//go:build alt_language && <language>".
package lang

import "errors"
import (
"errors"
)

// All language strings should be in the form of a constant
// The constants should be grouped by the top level package they are used in (or common)
Expand Down Expand Up @@ -357,6 +359,9 @@ $ zarf package pull oci://ghcr.io/defenseunicorns/packages/dos-games:1.0.0 -a sk
CmdDevDeployFlagNoYolo = "Disable the YOLO mode default override and create / deploy the package as-defined"
CmdDevDeployErr = "Failed to dev deploy: %s"

CmdDevGenerateShort = "[alpha] Creates a zarf.yaml automatically from a given remote (git) Helm chart"
CmdDevGenerateExample = "zarf dev generate podinfo --url https://github.com/stefanprodan/podinfo.git --version 6.4.0 --gitPath charts/podinfo"

CmdDevPatchGitShort = "Converts all .git URLs to the specified Zarf HOST and with the Zarf URL pattern in a given FILE. NOTE:\n" +
"This should only be used for manifests that are not mutated by the Zarf Agent Mutating Webhook."
CmdDevPatchGitOverwritePrompt = "Overwrite the file %s with these changes?"
Expand Down
102 changes: 102 additions & 0 deletions src/pkg/packager/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package packager contains functions for interacting with, managing and deploying Zarf packages.
package packager

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

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/internal/packager/validate"
"github.com/defenseunicorns/zarf/src/pkg/layout"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/pkg/utils/helpers"
"github.com/defenseunicorns/zarf/src/types"
goyaml "github.com/goccy/go-yaml"
)

// Generate generates a Zarf package definition.
func (p *Packager) Generate() (err error) {
generatedZarfYAMLPath := filepath.Join(p.cfg.GenerateOpts.Output, layout.ZarfYAML)
spinner := message.NewProgressSpinner("Generating package for %q at %s", p.cfg.GenerateOpts.Name, generatedZarfYAMLPath)

if !utils.InvalidPath(generatedZarfYAMLPath) {
prefixed := filepath.Join(p.cfg.GenerateOpts.Output, fmt.Sprintf("%s-%s", p.cfg.GenerateOpts.Name, layout.ZarfYAML))

message.Warnf("%s already exists, writing to %s", generatedZarfYAMLPath, prefixed)

generatedZarfYAMLPath = prefixed

if !utils.InvalidPath(generatedZarfYAMLPath) {
return fmt.Errorf("unable to generate package, %s already exists", generatedZarfYAMLPath)
}
}

generatedComponent := types.ZarfComponent{
Name: p.cfg.GenerateOpts.Name,
Required: true,
Charts: []types.ZarfChart{
{
Name: p.cfg.GenerateOpts.Name,
Version: p.cfg.GenerateOpts.Version,
Namespace: p.cfg.GenerateOpts.Name,
URL: p.cfg.GenerateOpts.URL,
GitPath: p.cfg.GenerateOpts.GitPath,
},
},
}

p.cfg.Pkg = types.ZarfPackage{
Kind: types.ZarfPackageConfig,
Metadata: types.ZarfMetadata{
Name: p.cfg.GenerateOpts.Name,
Version: p.cfg.GenerateOpts.Version,
Description: "auto-generated using `zarf dev generate`",
},
Components: []types.ZarfComponent{
generatedComponent,
},
}
p.arch = config.GetArch()

images, err := p.findImages()
if err != nil {
// purposefully not returning error here, as we can still generate the package without images
message.Warnf("Unable to find images: %s", err.Error())
}

for i := range p.cfg.Pkg.Components {
name := p.cfg.Pkg.Components[i].Name
p.cfg.Pkg.Components[i].Images = images[name]
}

if err := validate.Run(p.cfg.Pkg); err != nil {
return err
}

if err := utils.CreateDirectory(p.cfg.GenerateOpts.Output, helpers.ReadExecuteAllWriteUser); err != nil {
return err
}

b, err := goyaml.MarshalWithOptions(p.cfg.Pkg, goyaml.IndentSequence(true), goyaml.UseSingleQuote(false))
if err != nil {
return err
}

schemaComment := fmt.Sprintf("# yaml-language-server: $schema=https://raw.githubusercontent.com/%s/%s/zarf.schema.json", config.GithubProject, config.CLIVersion)
content := schemaComment + "\n" + string(b)

// lets space things out a bit
content = strings.Replace(content, "kind:\n", "\nkind:\n", 1)
content = strings.Replace(content, "metadata:\n", "\nmetadata:\n", 1)
content = strings.Replace(content, "components:\n", "\ncomponents:\n", 1)

spinner.Successf("Generated package for %q at %s", p.cfg.GenerateOpts.Name, generatedZarfYAMLPath)

return os.WriteFile(generatedZarfYAMLPath, []byte(content), helpers.ReadAllWriteUser)
}
37 changes: 20 additions & 17 deletions src/pkg/packager/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,38 @@ type imageMap map[string]bool

// FindImages iterates over a Zarf.yaml and attempts to parse any images.
func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
repoHelmChartPath := p.cfg.FindImagesOpts.RepoHelmChartPath
kubeVersionOverride := p.cfg.FindImagesOpts.KubeVersionOverride
whyImage := p.cfg.FindImagesOpts.Why

imagesMap := make(map[string][]string)
erroredCharts := []string{}
erroredCosignLookups := []string{}
whyResources := []string{}

cwd, err := os.Getwd()
if err != nil {
return nil, err
}

defer func() {
// Return to the original working directory
if err := os.Chdir(cwd); err != nil {
message.Warnf("Unable to return to the original working directory: %s", err.Error())
}
}()
if err := os.Chdir(p.cfg.CreateOpts.BaseDir); err != nil {
return nil, fmt.Errorf("unable to access directory '%s': %w", p.cfg.CreateOpts.BaseDir, err)
return nil, fmt.Errorf("unable to access directory %q: %w", p.cfg.CreateOpts.BaseDir, err)
}
message.Note(fmt.Sprintf("Using build directory %s", p.cfg.CreateOpts.BaseDir))

if err = p.readZarfYAML(layout.ZarfYAML); err != nil {
return nil, fmt.Errorf("unable to read the zarf.yaml file: %w", err)
}

return p.findImages()
}

func (p *Packager) findImages() (imgMap map[string][]string, err error) {
repoHelmChartPath := p.cfg.FindImagesOpts.RepoHelmChartPath
kubeVersionOverride := p.cfg.FindImagesOpts.KubeVersionOverride
whyImage := p.cfg.FindImagesOpts.Why

imagesMap := make(map[string][]string)
erroredCharts := []string{}
erroredCosignLookups := []string{}
whyResources := []string{}

if err := p.composeComponents(); err != nil {
return nil, err
}
Expand Down Expand Up @@ -90,7 +99,6 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
}

for _, component := range p.cfg.Pkg.Components {

if len(component.Charts)+len(component.Manifests)+len(component.Repos) < 1 {
// Skip if it doesn't have what we need
continue
Expand Down Expand Up @@ -342,11 +350,6 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {

fmt.Println(componentDefinition)

// Return to the original working directory
if err := os.Chdir(cwd); err != nil {
return nil, err
}

if len(erroredCharts) > 0 || len(erroredCosignLookups) > 0 {
errMsg := ""
if len(erroredCharts) > 0 {
Expand Down
39 changes: 39 additions & 0 deletions src/test/e2e/13_zarf_package_generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package test provides e2e tests for Zarf.
package test

import (
"path/filepath"
"testing"

"github.com/defenseunicorns/zarf/src/pkg/layout"
"github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/types"
"github.com/stretchr/testify/require"
)

func TestZarfDevGenerate(t *testing.T) {
t.Log("E2E: Zarf Dev Generate")

t.Run("Test generate podinfo", func(t *testing.T) {
tmpDir := t.TempDir()

url := "https://github.com/stefanprodan/podinfo.git"
version := "6.4.0"
gitPath := "charts/podinfo"

stdOut, stdErr, err := e2e.Zarf("dev", "generate", "podinfo", "--url", url, "--version", version, "--gitPath", gitPath, "--output-directory", tmpDir)
require.NoError(t, err, stdOut, stdErr)

zarfPackage := types.ZarfPackage{}
packageLocation := filepath.Join(tmpDir, layout.ZarfYAML)
err = utils.ReadYaml(packageLocation, &zarfPackage)
require.NoError(t, err)
require.Equal(t, zarfPackage.Components[0].Charts[0].URL, url)
require.Equal(t, zarfPackage.Components[0].Charts[0].Version, version)
require.Equal(t, zarfPackage.Components[0].Charts[0].GitPath, gitPath)
require.NotEmpty(t, zarfPackage.Components[0].Images)
})
}
3 changes: 3 additions & 0 deletions src/types/packager.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type PackagerConfig struct {
// FindImagesOpts tracks user-defined options used to find images
FindImagesOpts ZarfFindImagesOptions

// GenerateOpts tracks user-defined values for package generation.
GenerateOpts ZarfGenerateOptions

// The package data
Pkg ZarfPackage

Expand Down
9 changes: 9 additions & 0 deletions src/types/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ type ZarfPullOptions struct {
OutputDirectory string `json:"outputDirectory" jsonschema:"description=Location where the pulled Zarf package will be placed"`
}

// ZarfGenerateOptions tracks the user-defined options during package generation.
type ZarfGenerateOptions struct {
Name string `json:"name" jsonschema:"description=Name of the package being generated"`
URL string `json:"url" jsonschema:"description=URL to the source git repository"`
Version string `json:"version" jsonschema:"description=Version of the chart to use"`
GitPath string `json:"gitPath" jsonschema:"description=Relative path to the chart in the git repository"`
Output string `json:"output" jsonschema:"description=Location where the finalized zarf.yaml will be placed"`
}

// ZarfInitOptions tracks the user-defined options during cluster initialization.
type ZarfInitOptions struct {
// Zarf init is installing the k3s component
Expand Down

0 comments on commit 9608731

Please sign in to comment.