Skip to content

Commit

Permalink
feat: run schema validation on create (zarf-dev#2585)
Browse files Browse the repository at this point in the history
Signed-off-by: Austin Abro <[email protected]>
Signed-off-by: Tim Seagren <[email protected]>
  • Loading branch information
AustinAbro321 authored and chaospuppy committed Aug 5, 2024
1 parent 39108d6 commit b835d34
Show file tree
Hide file tree
Showing 23 changed files with 825 additions and 778 deletions.
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

"github.com/zarf-dev/zarf/src/cmd"
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/pkg/packager/lint"
"github.com/zarf-dev/zarf/src/pkg/lint"
)

//go:embed cosign.pub
Expand Down
4 changes: 3 additions & 1 deletion src/cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/zarf-dev/zarf/src/cmd/common"
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/config/lang"
"github.com/zarf-dev/zarf/src/pkg/lint"
"github.com/zarf-dev/zarf/src/pkg/message"
"github.com/zarf-dev/zarf/src/pkg/packager"
"github.com/zarf-dev/zarf/src/pkg/transform"
Expand Down Expand Up @@ -269,6 +270,7 @@ var devLintCmd = &cobra.Command{
Short: lang.CmdDevLintShort,
Long: lang.CmdDevLintLong,
RunE: func(cmd *cobra.Command, args []string) error {
config.CommonOptions.Confirm = true
pkgConfig.CreateOpts.BaseDir = common.SetBaseDirectory(args)
v := common.GetViper()
pkgConfig.CreateOpts.SetVariables = helpers.TransformAndMergeMap(
Expand All @@ -280,7 +282,7 @@ var devLintCmd = &cobra.Command{
}
defer pkgClient.ClearTempPaths()

return pkgClient.Lint(cmd.Context())
return lint.Validate(cmd.Context(), pkgConfig.CreateOpts)
},
}

Expand Down
7 changes: 0 additions & 7 deletions src/config/lang/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,19 +630,15 @@ const (
// Package validate
const (
PkgValidateTemplateDeprecation = "Package template %q is using the deprecated syntax ###ZARF_PKG_VAR_%s###. This will be removed in Zarf v1.0.0. Please update to ###ZARF_PKG_TMPL_%s###."
PkgValidateMustBeUppercase = "variable name %q must be all uppercase and contain no special characters except _"
PkgValidateErrAction = "invalid action: %w"
PkgValidateErrActionCmdWait = "action %q cannot be both a command and wait action"
PkgValidateErrActionClusterNetwork = "a single wait action must contain only one of cluster or network"
PkgValidateErrChart = "invalid chart definition: %w"
PkgValidateErrChartName = "chart %q exceed the maximum length of %d characters"
PkgValidateErrChartNameMissing = "chart must include a name"
PkgValidateErrChartNameNotUnique = "chart name %q is not unique"
PkgValidateErrChartNamespaceMissing = "chart %q must include a namespace"
PkgValidateErrChartURLOrPath = "chart %q must have either a url or localPath"
PkgValidateErrChartVersion = "chart %q must include a chart version"
PkgValidateErrComponentName = "component name %q must be all lowercase and contain no special characters except '-' and cannot start with a '-'"
PkgValidateErrComponentLocalOS = "component %q contains a localOS value that is not supported: %s (supported: %s)"
PkgValidateErrComponentNameNotUnique = "component name %q is not unique"
PkgValidateErrComponentReqDefault = "component %q cannot be both required and default"
PkgValidateErrComponentReqGrouped = "component %q cannot be both required and grouped"
Expand All @@ -654,11 +650,8 @@ const (
PkgValidateErrManifest = "invalid manifest definition: %w"
PkgValidateErrManifestFileOrKustomize = "manifest %q must have at least one file or kustomization"
PkgValidateErrManifestNameLength = "manifest %q exceed the maximum length of %d characters"
PkgValidateErrManifestNameMissing = "manifest must include a name"
PkgValidateErrManifestNameNotUnique = "manifest name %q is not unique"
PkgValidateErrPkgConstantName = "constant name %q must be all uppercase and contain no special characters except _"
PkgValidateErrPkgConstantPattern = "provided value for constant %q does not match pattern %q"
PkgValidateErrPkgName = "package name %q must be all lowercase and contain no special characters except '-' and cannot start with a '-'"
PkgValidateErrVariable = "invalid package variable: %w"
PkgValidateErrYOLONoArch = "cluster architecture not allowed in YOLO"
PkgValidateErrYOLONoDistro = "cluster distros not allowed in YOLO"
Expand Down
115 changes: 115 additions & 0 deletions src/pkg/lint/findings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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"
"path/filepath"

"github.com/defenseunicorns/pkg/helpers/v2"
"github.com/fatih/color"
"github.com/zarf-dev/zarf/src/pkg/message"
)

// PackageFinding is a struct that contains a finding about something wrong with a package
type PackageFinding struct {
// YqPath is the path to the key where the error originated from, this is sometimes empty in the case of a general error
YqPath string
Description string
// Item is the value of a key that is causing an error, for example a bad image name
Item string
// PackageNameOverride shows the name of the package that the error originated from
// If it is not set the base package will be used when displaying the error
PackageNameOverride string
// PackagePathOverride shows the path to the package that the error originated from
// If it is not set the base package will be used when displaying the error
PackagePathOverride string
Severity Severity
}

// Severity is the type of finding
type Severity int

// different severities of package errors
const (
SevErr Severity = iota + 1
SevWarn
)

func (f PackageFinding) itemizedDescription() string {
if f.Item == "" {
return f.Description
}
return fmt.Sprintf("%s - %s", f.Description, f.Item)
}

func colorWrapSev(s Severity) string {
if s == SevErr {
return message.ColorWrap("Error", color.FgRed)
} else if s == SevWarn {
return message.ColorWrap("Warning", color.FgYellow)
}
return "unknown"
}

func filterLowerSeverity(findings []PackageFinding, severity Severity) []PackageFinding {
findings = helpers.RemoveMatches(findings, func(finding PackageFinding) bool {
return finding.Severity > severity
})
return findings
}

// PrintFindings prints the findings of the given severity in a table
func PrintFindings(findings []PackageFinding, severity Severity, baseDir string, packageName string) {
findings = filterLowerSeverity(findings, severity)
if len(findings) == 0 {
return
}
mapOfFindingsByPath := GroupFindingsByPath(findings, packageName)

header := []string{"Type", "Path", "Message"}

for _, findings := range mapOfFindingsByPath {
lintData := [][]string{}
for _, finding := range findings {
lintData = append(lintData, []string{
colorWrapSev(finding.Severity),
message.ColorWrap(finding.YqPath, color.FgCyan),
finding.itemizedDescription(),
})
}
var packagePathFromUser string
if helpers.IsOCIURL(findings[0].PackagePathOverride) {
packagePathFromUser = findings[0].PackagePathOverride
} else {
packagePathFromUser = filepath.Join(baseDir, findings[0].PackagePathOverride)
}
message.Notef("Linting package %q at %s", findings[0].PackageNameOverride, packagePathFromUser)
message.Table(header, lintData)
}
}

// GroupFindingsByPath groups findings by their package path
func GroupFindingsByPath(findings []PackageFinding, packageName string) map[string][]PackageFinding {
for i := range findings {
if findings[i].PackageNameOverride == "" {
findings[i].PackageNameOverride = packageName
}
if findings[i].PackagePathOverride == "" {
findings[i].PackagePathOverride = "."
}
}

mapOfFindingsByPath := make(map[string][]PackageFinding)
for _, finding := range findings {
mapOfFindingsByPath[finding.PackagePathOverride] = append(mapOfFindingsByPath[finding.PackagePathOverride], finding)
}
return mapOfFindingsByPath
}

// HasSevOrHigher returns true if the findings contain a severity equal to or greater than the given severity
func HasSevOrHigher(findings []PackageFinding, severity Severity) bool {
return len(filterLowerSeverity(findings, severity)) > 0
}
105 changes: 105 additions & 0 deletions src/pkg/lint/findings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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 (
"testing"

"github.com/stretchr/testify/require"
)

func TestGroupFindingsByPath(t *testing.T) {
t.Parallel()
tests := []struct {
name string
findings []PackageFinding
severity Severity
packageName string
want map[string][]PackageFinding
}{
{
name: "same package multiple findings",
findings: []PackageFinding{
{Severity: SevWarn, PackageNameOverride: "import", PackagePathOverride: "path"},
{Severity: SevWarn, PackageNameOverride: "import", PackagePathOverride: "path"},
},
packageName: "testPackage",
want: map[string][]PackageFinding{
"path": {
{Severity: SevWarn, PackageNameOverride: "import", PackagePathOverride: "path"},
{Severity: SevWarn, PackageNameOverride: "import", PackagePathOverride: "path"},
},
},
},
{
name: "different packages single finding",
findings: []PackageFinding{
{Severity: SevWarn, PackageNameOverride: "import", PackagePathOverride: "path"},
{Severity: SevErr, PackageNameOverride: "", PackagePathOverride: ""},
},
packageName: "testPackage",
want: map[string][]PackageFinding{
"path": {{Severity: SevWarn, PackageNameOverride: "import", PackagePathOverride: "path"}},
".": {{Severity: SevErr, PackageNameOverride: "testPackage", PackagePathOverride: "."}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, GroupFindingsByPath(tt.findings, tt.packageName))
})
}
}

func TestHasSeverity(t *testing.T) {
t.Parallel()
tests := []struct {
name string
severity Severity
expected bool
findings []PackageFinding
}{
{
name: "error severity present",
findings: []PackageFinding{
{
Severity: SevErr,
},
},
severity: SevErr,
expected: true,
},
{
name: "error severity not present",
findings: []PackageFinding{
{
Severity: SevWarn,
},
},
severity: SevErr,
expected: false,
},
{
name: "err and warning severity present",
findings: []PackageFinding{
{
Severity: SevWarn,
},
{
Severity: SevErr,
},
},
severity: SevErr,
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, HasSevOrHigher(tt.findings, tt.severity))
})
}
}
Loading

0 comments on commit b835d34

Please sign in to comment.