diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae9758c731..8c7700afb1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,3 +47,7 @@ repos: files: "zarf.yaml" types: [yaml] args: ["--schemafile", "zarf.schema.json"] + exclude: | + (?x)^( + src/test/packages/12-lint/.* + )$ diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_prepare.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_prepare.md index d0c4598e7c..837e4c03ba 100644 --- a/docs/2-the-zarf-cli/100-cli-commands/zarf_prepare.md +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_prepare.md @@ -27,6 +27,7 @@ Tools to help prepare assets for packaging * [zarf](zarf.md) - DevSecOps for Airgap * [zarf prepare find-images](zarf_prepare_find-images.md) - Evaluates components in a zarf file to identify images specified in their helm charts and manifests * [zarf prepare generate-config](zarf_prepare_generate-config.md) - Generates a config file for Zarf +* [zarf prepare lint](zarf_prepare_lint.md) - Verifies the package schema * [zarf prepare patch-git](zarf_prepare_patch-git.md) - Converts all .git URLs to the specified Zarf HOST and with the Zarf URL pattern in a given FILE. NOTE: This should only be used for manifests that are not mutated by the Zarf Agent Mutating Webhook. * [zarf prepare sha256sum](zarf_prepare_sha256sum.md) - Generates a SHA256SUM for the given file diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_prepare_lint.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_prepare_lint.md new file mode 100644 index 0000000000..bb36206a1d --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_prepare_lint.md @@ -0,0 +1,35 @@ +# zarf prepare lint + + +Verifies the package schema + +## Synopsis + +Verifies the package schema and warns the user if they have variables that won't be evaluated + +``` +zarf prepare lint [ DIRECTORY ] [flags] +``` + +## Options + +``` + -h, --help help for lint +``` + +## 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 prepare](zarf_prepare.md) - Tools to help prepare assets for packaging diff --git a/docs/3-create-a-zarf-package/4-zarf-schema.md b/docs/3-create-a-zarf-package/4-zarf-schema.md index 241382d6f6..3156977bce 100644 --- a/docs/3-create-a-zarf-package/4-zarf-schema.md +++ b/docs/3-create-a-zarf-package/4-zarf-schema.md @@ -783,10 +783,6 @@ Must be one of: | -------- | -------- | | **Type** | `string` | -| Restrictions | | -| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| **Must match regular expression** | ```^(?!.*###ZARF_PKG_TMPL_).*$``` [Test](https://regex101.com/?regex=%5E%28%3F%21.%2A%23%23%23ZARF_PKG_TMPL_%29.%2A%24) | - @@ -803,9 +799,9 @@ Must be one of: | -------- | -------- | | **Type** | `string` | -| Restrictions | | -| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| **Must match regular expression** | ```^oci://(?!.*###ZARF_PKG_TMPL_).*$``` [Test](https://regex101.com/?regex=%5Eoci%3A%2F%2F%28%3F%21.%2A%23%23%23ZARF_PKG_TMPL_%29.%2A%24) | +| Restrictions | | +| --------------------------------- | --------------------------------------------------------------------------- | +| **Must match regular expression** | ```^oci://.*$``` [Test](https://regex101.com/?regex=%5Eoci%3A%2F%2F.%2A%24) | diff --git a/docs/3-create-a-zarf-package/index.md b/docs/3-create-a-zarf-package/index.md index 21bc292bef..692f37a9db 100644 --- a/docs/3-create-a-zarf-package/index.md +++ b/docs/3-create-a-zarf-package/index.md @@ -21,6 +21,9 @@ To learn more about creating a Zarf package, you can check out the following res The general flow of a Zarf package deployment on an existing initialized cluster is as follows: ```shell +# Before creating your package you can lint your zarf.yaml +$ zarf prepare lint + # To create a package run the following: $ zarf package create # - Enter any package templates that have not yet been defined diff --git a/docs/5-zarf-tutorials/0-creating-a-zarf-package.md b/docs/5-zarf-tutorials/0-creating-a-zarf-package.md index d0b53f65dd..f311a4d387 100644 --- a/docs/5-zarf-tutorials/0-creating-a-zarf-package.md +++ b/docs/5-zarf-tutorials/0-creating-a-zarf-package.md @@ -37,6 +37,7 @@ metadata: :::tip If you are using an Integrated Development Environment (such as [VS Code](../3-create-a-zarf-package/8-vscode.md)) to create and edit the `zarf.yaml` file, you can install or reference the [`zarf.schema.json`](https://github.com/defenseunicorns/zarf/blob/main/zarf.schema.json) file to get error checking and autocomplete. +Additionally, you can run `zarf prepare lint ` to validate aginst the [`zarf.schema.json`](https://github.com/defenseunicorns/zarf/blob/main/zarf.schema.json) ::: diff --git a/go.mod b/go.mod index 3bb7598da1..2b7271b1c3 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ module github.com/defenseunicorns/zarf -go 1.21.0 +go 1.21.1 -toolchain go1.21.3 +toolchain go1.21.4 + +replace github.com/xeipuuv/gojsonschema => github.com/defenseunicorns/gojsonschema v0.0.0-20231116163348-e00f069122d6 require ( cuelang.org/go v0.6.0 @@ -41,6 +43,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.8.4 + github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/crypto v0.14.0 golang.org/x/sync v0.5.0 golang.org/x/term v0.13.0 @@ -413,7 +416,6 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect diff --git a/go.sum b/go.sum index 62903abe0d..79365b4de7 100644 --- a/go.sum +++ b/go.sum @@ -445,6 +445,8 @@ github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hR github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/defenseunicorns/gojsonschema v0.0.0-20231116163348-e00f069122d6 h1:gwevOZ0fxT2nzM9hrtdPbsiOHjFqDRIYMzJHba3/G6Q= +github.com/defenseunicorns/gojsonschema v0.0.0-20231116163348-e00f069122d6/go.mod h1:StKLYMmPj1R5yIs6CK49EkcW1TvUYuw5Vri+LRk7Dy8= github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M= github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk= github.com/depcheck-test/depcheck-test v0.0.0-20220607135614-199033aaa936 h1:foGzavPWwtoyBvjWyKJYDYsyzy+23iBV7NKTwdk+LRY= @@ -1526,13 +1528,10 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= diff --git a/main.go b/main.go index 67396e0381..b9f58f6e44 100644 --- a/main.go +++ b/main.go @@ -5,16 +5,21 @@ package main import ( - _ "embed" + "embed" "github.com/defenseunicorns/zarf/src/cmd" "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/pkg/packager/lint" ) //go:embed cosign.pub var cosignPublicKey string +//go:embed zarf.schema.json +var zarfSchema embed.FS + func main() { config.CosignPublicKey = cosignPublicKey + lint.ZarfSchema = zarfSchema cmd.Execute() } diff --git a/src/cmd/prepare.go b/src/cmd/prepare.go index 3d2f92baca..fcd5e113c8 100644 --- a/src/cmd/prepare.go +++ b/src/cmd/prepare.go @@ -17,6 +17,7 @@ import ( "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager" + "github.com/defenseunicorns/zarf/src/pkg/packager/lint" "github.com/defenseunicorns/zarf/src/pkg/transform" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" @@ -205,6 +206,34 @@ var prepareGenerateConfigFile = &cobra.Command{ }, } +var lintCmd = &cobra.Command{ + Use: "lint [ DIRECTORY ]", + Args: cobra.MaximumNArgs(1), + Aliases: []string{"l"}, + Short: lang.CmdPrepareLintShort, + Long: lang.CmdPrepareLintLong, + Run: func(cmd *cobra.Command, args []string) { + baseDir := "" + if len(args) > 0 { + baseDir = args[0] + } else { + var err error + baseDir, err = os.Getwd() + if err != nil { + message.Fatalf(err, lang.CmdPrepareLintErr, err.Error()) + } + } + validator, err := lint.ValidateZarfSchema(baseDir) + if err != nil { + message.Fatal(err, err.Error()) + } + validator.DisplayFormattedMessage() + if !validator.IsSuccess() { + os.Exit(1) + } + }, +} + func init() { v := common.InitViper() @@ -213,6 +242,7 @@ func init() { prepareCmd.AddCommand(prepareComputeFileSha256sum) prepareCmd.AddCommand(prepareFindImages) prepareCmd.AddCommand(prepareGenerateConfigFile) + prepareCmd.AddCommand(lintCmd) prepareComputeFileSha256sum.Flags().StringVarP(&extractPath, "extract-path", "e", "", lang.CmdPrepareFlagExtractPath) diff --git a/src/config/config.go b/src/config/config.go index e1a8d6f816..071d160fe8 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -6,6 +6,7 @@ package config import ( "crypto/tls" + "embed" "fmt" "net/http" "os" @@ -92,6 +93,7 @@ var ( NoColor bool CosignPublicKey string + ZarfSchema embed.FS // Timestamp of when the CLI was started operationStartTime = time.Now().Unix() diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 1957931fad..13f337682a 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -359,6 +359,10 @@ $ zarf package publish ./path/to/dir oci://my-registry.com/my-namespace CmdPrepareFlagGitAccount = "User or organization name for the git account that the repos are created under." CmdPrepareFlagKubeVersion = "Override the default helm template KubeVersion when performing a package chart template" + CmdPrepareLintShort = "Verifies the package schema" + CmdPrepareLintLong = "Verifies the package schema and warns the user if they have variables that won't be evaluated" + CmdPrepareLintErr = "Unable to lint package: %s" + // zarf tools CmdToolsShort = "Collection of additional tools to make airgap easier" diff --git a/src/pkg/message/connect.go b/src/pkg/message/connect.go index 0df6f08d38..22b125ed51 100644 --- a/src/pkg/message/connect.go +++ b/src/pkg/message/connect.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/defenseunicorns/zarf/src/types" - "github.com/pterm/pterm" ) // PrintConnectStringTable prints a table of connect strings. @@ -16,14 +15,15 @@ func PrintConnectStringTable(connectStrings types.ConnectStrings) { Debugf("message.PrintConnectStringTable(%#v)", connectStrings) if len(connectStrings) > 0 { - list := pterm.TableData{{" Connect Command", "Description"}} + connectData := [][]string{} // Loop over each connectStrings and convert to pterm.TableData for name, connect := range connectStrings { - name = fmt.Sprintf(" zarf connect %s", name) - list = append(list, []string{name, connect.Description}) + name = fmt.Sprintf("zarf connect %s", name) + connectData = append(connectData, []string{name, connect.Description}) } // Create the table output with the data - _ = pterm.DefaultTable.WithHasHeader().WithData(list).Render() + header := []string{"Connect Command", "Description"} + Table(header, connectData) } } diff --git a/src/pkg/message/credentials.go b/src/pkg/message/credentials.go index b89e15da74..15e1e87c71 100644 --- a/src/pkg/message/credentials.go +++ b/src/pkg/message/credentials.go @@ -34,37 +34,32 @@ func PrintCredentialTable(state *types.ZarfState, componentsToDeploy []types.Dep // Set output to os.Stderr to avoid creds being printed in logs pterm.SetDefaultOutput(os.Stderr) - pterm.Println() - loginTableHeader := pterm.TableData{ - {" Application", "Username", "Password", "Connect", "Get-Creds Key"}, - } - - loginTable := pterm.TableData{} + loginData := [][]string{} if state.RegistryInfo.InternalRegistry { - loginTable = append(loginTable, pterm.TableData{ - {" Registry", state.RegistryInfo.PushUsername, state.RegistryInfo.PushPassword, "zarf connect registry", RegistryKey}, - {" Registry (read-only)", state.RegistryInfo.PullUsername, state.RegistryInfo.PullPassword, "zarf connect registry", RegistryReadKey}, - }...) + loginData = append(loginData, + []string{"Registry", state.RegistryInfo.PushUsername, state.RegistryInfo.PushPassword, "zarf connect registry", RegistryKey}, + []string{"Registry (read-only)", state.RegistryInfo.PullUsername, state.RegistryInfo.PullPassword, "zarf connect registry", RegistryReadKey}, + ) } for _, component := range componentsToDeploy { // Show message if including logging stack if component.Name == "logging" { - loginTable = append(loginTable, pterm.TableData{{" Logging", config.ZarfLoggingUser, state.LoggingSecret, "zarf connect logging", LoggingKey}}...) + loginData = append(loginData, []string{"Logging", config.ZarfLoggingUser, state.LoggingSecret, "zarf connect logging", LoggingKey}) } // Show message if including git-server if component.Name == "git-server" { - loginTable = append(loginTable, pterm.TableData{ - {" Git", state.GitServer.PushUsername, state.GitServer.PushPassword, "zarf connect git", GitKey}, - {" Git (read-only)", state.GitServer.PullUsername, state.GitServer.PullPassword, "zarf connect git", GitReadKey}, - {" Artifact Token", state.ArtifactServer.PushUsername, state.ArtifactServer.PushToken, "zarf connect git", ArtifactKey}, - }...) + loginData = append(loginData, + []string{"Git", state.GitServer.PushUsername, state.GitServer.PushPassword, "zarf connect git", GitKey}, + []string{"Git (read-only)", state.GitServer.PullUsername, state.GitServer.PullPassword, "zarf connect git", GitReadKey}, + []string{"Artifact Token", state.ArtifactServer.PushUsername, state.ArtifactServer.PushToken, "zarf connect git", ArtifactKey}, + ) } } - if len(loginTable) > 0 { - loginTable = append(loginTableHeader, loginTable...) - _ = pterm.DefaultTable.WithHasHeader().WithData(loginTable).Render() + if len(loginData) > 0 { + header := []string{"Application", "Username", "Password", "Connect", "Get-Creds Key"} + Table(header, loginData) } // Restore the log file if it was specified diff --git a/src/pkg/message/message.go b/src/pkg/message/message.go index 07b3db9983..ef1d2e235f 100644 --- a/src/pkg/message/message.go +++ b/src/pkg/message/message.go @@ -318,6 +318,28 @@ func Truncate(text string, length int, invert bool) string { return textEscaped } +// Table prints a padded table containing the specified header and data +func Table(header []string, data [][]string) { + pterm.Println() + + if len(header) > 0 { + header[0] = fmt.Sprintf(" %s", header[0]) + } + + table := pterm.TableData{ + header, + } + + for _, row := range data { + if len(row) > 0 { + row[0] = fmt.Sprintf(" %s", row[0]) + } + table = append(table, pterm.TableData{row}...) + } + + pterm.DefaultTable.WithHasHeader().WithData(table).Render() +} + func debugPrinter(offset int, a ...any) { printer := pterm.Debug.WithShowLineNumber(logLevel > 2).WithLineNumberOffset(offset) now := time.Now().Format(time.RFC3339) diff --git a/src/pkg/packager/lint/lint.go b/src/pkg/packager/lint/lint.go new file mode 100644 index 0000000000..b9142cc3fb --- /dev/null +++ b/src/pkg/packager/lint/lint.go @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package lint contains functions for verifying zarf yaml files are valid +package lint + +import ( + "embed" + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/defenseunicorns/zarf/src/pkg/layout" + "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/types" + "github.com/xeipuuv/gojsonschema" +) + +// ZarfSchema is exported so main.go can embed the schema file +var ZarfSchema embed.FS + +func getSchemaFile() ([]byte, error) { + return ZarfSchema.ReadFile("zarf.schema.json") +} + +// ValidateZarfSchema validates a zarf file against the zarf schema, returns *validator with warnings or errors if they exist +// along with an error if the validation itself failed +func ValidateZarfSchema(path string) (*Validator, error) { + validator := Validator{} + var err error + if err := utils.ReadYaml(filepath.Join(path, layout.ZarfYAML), &validator.typedZarfPackage); err != nil { + return nil, err + } + + checkForVarInComponentImport(&validator) + + if validator.jsonSchema, err = getSchemaFile(); err != nil { + return nil, err + } + + if err := utils.ReadYaml(filepath.Join(path, layout.ZarfYAML), &validator.untypedZarfPackage); err != nil { + return nil, err + } + + if err = validateSchema(&validator); err != nil { + return nil, err + } + + return &validator, nil +} + +func checkForVarInComponentImport(validator *Validator) { + for i, component := range validator.typedZarfPackage.Components { + if strings.Contains(component.Import.Path, types.ZarfPackageTemplatePrefix) { + validator.addWarning(fmt.Sprintf(".components.[%d].import.path: Will not resolve ZARF_PKG_TMPL_* variables", i)) + } + if strings.Contains(component.Import.URL, types.ZarfPackageTemplatePrefix) { + validator.addWarning(fmt.Sprintf(".components.[%d].import.url: Will not resolve ZARF_PKG_TMPL_* variables", i)) + } + } + +} + +func makeFieldPathYqCompat(field string) string { + if field == "(root)" { + return field + } + // \b is a metacharacter that will stop at the next non-word character (including .) + // https://regex101.com/r/pIRPk0/1 + re := regexp.MustCompile(`(\b\d+\b)`) + + wrappedField := re.ReplaceAllString(field, "[$1]") + + return fmt.Sprintf(".%s", wrappedField) +} + +func validateSchema(validator *Validator) error { + schemaLoader := gojsonschema.NewBytesLoader(validator.jsonSchema) + documentLoader := gojsonschema.NewGoLoader(validator.untypedZarfPackage) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return err + } + + if !result.Valid() { + for _, desc := range result.Errors() { + err := fmt.Errorf( + "%s: %s", makeFieldPathYqCompat(desc.Field()), desc.Description()) + validator.addError(err) + } + } + + return err +} diff --git a/src/pkg/packager/lint/lint_test.go b/src/pkg/packager/lint/lint_test.go new file mode 100644 index 0000000000..ddc9907e0f --- /dev/null +++ b/src/pkg/packager/lint/lint_test.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package lint contains functions for verifying zarf yaml files are valid +package lint + +import ( + "os" + "testing" + + "github.com/defenseunicorns/zarf/src/types" + goyaml "github.com/goccy/go-yaml" + "github.com/stretchr/testify/require" +) + +const badZarfPackage = ` +kind: ZarfInitConfig +metadata: + name: init + description: Testing bad yaml + +components: +- name: first-test-component + import: + not-path: packages/distros/k3s +- name: import-test + import: + path: 123123 + +- name: import-test + import: + path: "###ZARF_PKG_TMPL_ZEBRA###" + +- name: import-url + import: + url: "oci://###ZARF_PKG_TMPL_ZEBRA###" +` + +const goodZarfPackage = ` +kind: ZarfPackageConfig +metadata: + name: good-zarf-package + +components: + - name: baseline + required: true +` + +func readAndUnmarshalYaml[T interface{}](t *testing.T, yamlString string) T { + t.Helper() + var unmarshalledYaml T + err := goyaml.Unmarshal([]byte(yamlString), &unmarshalledYaml) + if err != nil { + t.Errorf("error unmarshalling yaml: %v", err) + } + return unmarshalledYaml +} + +func TestValidateSchema(t *testing.T) { + getZarfSchema := func(t *testing.T) []byte { + t.Helper() + file, err := os.ReadFile("../../../../zarf.schema.json") + if err != nil { + t.Errorf("error reading file: %v", err) + } + return file + } + + t.Run("validate schema success", func(t *testing.T) { + unmarshalledYaml := readAndUnmarshalYaml[interface{}](t, goodZarfPackage) + validator := Validator{untypedZarfPackage: unmarshalledYaml, jsonSchema: getZarfSchema(t)} + err := validateSchema(&validator) + require.NoError(t, err) + require.Empty(t, validator.errors) + }) + + t.Run("validate schema fail", func(t *testing.T) { + unmarshalledYaml := readAndUnmarshalYaml[interface{}](t, badZarfPackage) + validator := Validator{untypedZarfPackage: unmarshalledYaml, jsonSchema: getZarfSchema(t)} + err := validateSchema(&validator) + require.NoError(t, err) + require.EqualError(t, validator.errors[0], ".components.[0].import: Additional property not-path is not allowed") + require.EqualError(t, validator.errors[1], ".components.[1].import.path: Invalid type. Expected: string, given: integer") + }) + + t.Run("Template in component import success", func(t *testing.T) { + unmarshalledYaml := readAndUnmarshalYaml[types.ZarfPackage](t, goodZarfPackage) + validator := Validator{typedZarfPackage: unmarshalledYaml} + checkForVarInComponentImport(&validator) + require.Empty(t, validator.warnings) + }) + + t.Run("Template in component import failure", func(t *testing.T) { + unmarshalledYaml := readAndUnmarshalYaml[types.ZarfPackage](t, badZarfPackage) + validator := Validator{typedZarfPackage: unmarshalledYaml} + checkForVarInComponentImport(&validator) + require.Equal(t, validator.warnings[0], ".components.[2].import.path: Will not resolve ZARF_PKG_TMPL_* variables") + require.Equal(t, validator.warnings[1], ".components.[3].import.url: Will not resolve ZARF_PKG_TMPL_* variables") + }) + + t.Run("Wrap standalone numbers in bracket", func(t *testing.T) { + input := "components12.12.import.path" + expected := ".components12.[12].import.path" + acutal := makeFieldPathYqCompat(input) + require.Equal(t, expected, acutal) + }) + + t.Run("root doesn't change", func(t *testing.T) { + input := "(root)" + acutal := makeFieldPathYqCompat(input) + require.Equal(t, input, acutal) + }) +} diff --git a/src/pkg/packager/lint/validator.go b/src/pkg/packager/lint/validator.go new file mode 100644 index 0000000000..f88c22de5f --- /dev/null +++ b/src/pkg/packager/lint/validator.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package lint contains functions for verifying zarf yaml files are valid +package lint + +import ( + "fmt" + + "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/types" + "github.com/fatih/color" +) + +// Validator holds the warnings/errors and messaging that we get from validation +type Validator struct { + warnings []string + errors []error + jsonSchema []byte + typedZarfPackage types.ZarfPackage + untypedZarfPackage interface{} +} + +// DisplayFormattedMessage message sent to user based on validator results +func (v Validator) DisplayFormattedMessage() { + if !v.hasWarnings() && !v.hasErrors() { + message.Successf("Schema validation successful for %q", v.typedZarfPackage.Metadata.Name) + } + v.printValidationTable() +} + +// IsSuccess returns true if there are not any errors +func (v Validator) IsSuccess() bool { + return !v.hasErrors() +} + +func (v Validator) printValidationTable() { + if v.hasWarnings() || v.hasErrors() { + header := []string{"Type", "Message"} + connectData := [][]string{} + for _, warning := range v.warnings { + connectData = append(connectData, []string{utils.ColorWrap("Warning", color.FgYellow), warning}) + } + for _, err := range v.errors { + connectData = append(connectData, []string{utils.ColorWrap("Error", color.FgRed), err.Error()}) + } + message.Table(header, connectData) + message.Info(fmt.Sprintf("%d warnings and %d errors in %q", + len(v.warnings), len(v.errors), v.typedZarfPackage.Metadata.Name)) + } +} + +func (v Validator) hasWarnings() bool { + return len(v.warnings) > 0 +} + +func (v Validator) hasErrors() bool { + return len(v.errors) > 0 +} + +func (v *Validator) addWarning(message string) { + v.warnings = append(v.warnings, message) +} + +func (v *Validator) addError(err error) { + v.errors = append(v.errors, err) +} diff --git a/src/pkg/packager/variables.go b/src/pkg/packager/variables.go index f8f9dd20b4..2caa135907 100644 --- a/src/pkg/packager/variables.go +++ b/src/pkg/packager/variables.go @@ -59,16 +59,16 @@ func (p *Packager) fillActiveTemplate() error { return err } - if err := promptAndSetTemplate("###ZARF_PKG_TMPL_", false); err != nil { + if err := promptAndSetTemplate(types.ZarfPackageTemplatePrefix, false); err != nil { return err } // [DEPRECATION] Set the Package Variable syntax as well for backward compatibility - if err := promptAndSetTemplate("###ZARF_PKG_VAR_", true); err != nil { + if err := promptAndSetTemplate(types.ZarfPackageVariablePrefix, true); err != nil { return err } // Add special variable for the current package architecture - templateMap["###ZARF_PKG_ARCH###"] = p.arch + templateMap[types.ZarfPackageArch] = p.arch return utils.ReloadYamlTemplate(&p.cfg.Pkg, templateMap) } @@ -131,7 +131,7 @@ func (p *Packager) findComponentTemplatesAndReload() error { // iterate through components to and find all ###ZARF_COMPONENT_NAME, assign to component Name and value for i, component := range p.cfg.Pkg.Components { mappings := map[string]string{} - mappings["###ZARF_COMPONENT_NAME###"] = component.Name + mappings[types.ZarfComponentName] = component.Name err := utils.ReloadYamlTemplate(&p.cfg.Pkg.Components[i], mappings) if err != nil { return err diff --git a/src/pkg/utils/random.go b/src/pkg/utils/random.go index 0c074580d2..7966cd765a 100644 --- a/src/pkg/utils/random.go +++ b/src/pkg/utils/random.go @@ -6,8 +6,10 @@ package utils import ( "crypto/rand" + "fmt" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/fatih/color" ) // Very limited special chars for git / basic auth @@ -37,3 +39,9 @@ func First30last30(s string) string { return s } + +// ColorWrap changes a string to an ansi color code and appends the default color to the end +// preventing future characters from taking on the given color +func ColorWrap(str string, attr color.Attribute) string { + return fmt.Sprintf("\x1b[%dm%s\x1b[0m", attr, str) +} diff --git a/src/test/e2e/12_lint_test.go b/src/test/e2e/12_lint_test.go new file mode 100644 index 0000000000..98257fdc89 --- /dev/null +++ b/src/test/e2e/12_lint_test.go @@ -0,0 +1,31 @@ +package test + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLint(t *testing.T) { + t.Log("E2E: Lint") + + t.Run("zarf test lint fail", func(t *testing.T) { + t.Log("E2E: Test lint on schema fail") + + path := filepath.Join("src", "test", "packages", "12-lint") + _, stderr, err := e2e.Zarf("prepare", "lint", path) + require.Error(t, err, "Require an exit code since there was warnings / errors") + require.Contains(t, stderr, ".components.[0].import: Additional property not-path is not allowed") + require.Contains(t, stderr, ".components.[2].import.path: Will not resolve ZARF_PKG_TMPL_* variables") + require.Contains(t, stderr, ".variables: Invalid type. Expected: array, given: null") + }) + + t.Run("zarf test lint success", func(t *testing.T) { + t.Log("E2E: Test lint on schema success") + + // This runs lint on the zarf.yaml in the base directory of the repo + _, _, err := e2e.Zarf("prepare", "lint") + require.NoError(t, err, "Expect no error here because the yaml file is following schema") + }) +} diff --git a/src/test/packages/12-lint/zarf.yaml b/src/test/packages/12-lint/zarf.yaml new file mode 100644 index 0000000000..7c026a15b7 --- /dev/null +++ b/src/test/packages/12-lint/zarf.yaml @@ -0,0 +1,24 @@ +kind: ZarfInitConfig +metadata: + name: init + description1: Testing bad yaml + + +variables: + +components: + - name: first-test-component + import: + not-path: packages/distros/k3s + + - name: import-test + import: + path: 123123 + + - name: import-test + import: + path: "###ZARF_PKG_TMPL_ZEBRA###" + + - name: import-url + import: + url: "oci://###ZARF_PKG_TMPL_ZEBRA###" diff --git a/src/types/component.go b/src/types/component.go index ae9c88b4b5..fea1cd14b1 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -218,9 +218,9 @@ type ZarfDataInjection struct { type ZarfComponentImport struct { ComponentName string `json:"name,omitempty" jsonschema:"description=The name of the component to import from the referenced zarf.yaml"` // For further explanation see https://regex101.com/r/nxX8vx/1 - Path string `json:"path,omitempty" jsonschema:"description=The relative path to a directory containing a zarf.yaml to import from,pattern=^(?!.*###ZARF_PKG_TMPL_).*$"` + Path string `json:"path,omitempty" jsonschema:"description=The relative path to a directory containing a zarf.yaml to import from"` // For further explanation see https://regex101.com/r/nxX8vx/1 - URL string `json:"url,omitempty" jsonschema:"description=[beta] The URL to a Zarf package to import via OCI,pattern=^oci://(?!.*###ZARF_PKG_TMPL_).*$"` + URL string `json:"url,omitempty" jsonschema:"description=[beta] The URL to a Zarf package to import via OCI,pattern=^oci://.*$"` } // IsEmpty returns if the components fields (other than the fields we were told to ignore) are empty or set to the types zero-value diff --git a/src/types/runtime.go b/src/types/runtime.go index b9546c1658..0cd53e18a0 100644 --- a/src/types/runtime.go +++ b/src/types/runtime.go @@ -11,6 +11,14 @@ const ( FileVariableType VariableType = "file" ) +// Zarf looks for these strings in zarf.yaml to make dynamic changes +const ( + ZarfPackageTemplatePrefix = "###ZARF_PKG_TMPL_" + ZarfPackageVariablePrefix = "###ZARF_PKG_VAR_" + ZarfPackageArch = "###ZARF_PKG_ARCH###" + ZarfComponentName = "###ZARF_COMPONENT_NAME###" +) + // VariableType represents a type of a Zarf package variable type VariableType string diff --git a/zarf.schema.json b/zarf.schema.json index 3491cda363..cbf8ae40b6 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -635,12 +635,11 @@ "description": "The name of the component to import from the referenced zarf.yaml" }, "path": { - "pattern": "^(?!.*###ZARF_PKG_TMPL_).*$", "type": "string", "description": "The relative path to a directory containing a zarf.yaml to import from" }, "url": { - "pattern": "^oci://(?!.*###ZARF_PKG_TMPL_).*$", + "pattern": "^oci://.*$", "type": "string", "description": "[beta] The URL to a Zarf package to import via OCI" }