Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: run schema validation on create #2585

Merged
merged 5 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 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 {
AustinAbro321 marked this conversation as resolved.
Show resolved Hide resolved
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) {
AustinAbro321 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading