1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

fix: validate the YAML test file syntactically and schematically (#8145)

* fix: validate the YAML test file syntactically

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* schema validation

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* unit tests

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2023-08-29 00:04:00 +02:00 committed by GitHub
parent ecc7b87df6
commit bb3df218ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 116 additions and 40 deletions

View file

@ -12,14 +12,15 @@ import (
"github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-billy/v5/memfs"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test/api"
sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError" sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError"
gitutils "github.com/kyverno/kyverno/pkg/utils/git" gitutils "github.com/kyverno/kyverno/pkg/utils/git"
"k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log"
) )
type policy struct { type testCase struct {
bytes []byte test *api.Test
resourcePath string resourcePath string
} }
@ -27,8 +28,8 @@ func loadTests(
dirPath []string, dirPath []string,
fileName string, fileName string,
gitBranch string, gitBranch string,
) (billy.Filesystem, []policy, []error) { ) (billy.Filesystem, []testCase, []error) {
var policies []policy var tests []testCase
var errors []error var errors []error
if strings.Contains(dirPath[0], "https://") { if strings.Contains(dirPath[0], "https://") {
fs := memfs.New() fs := memfs.New()
@ -69,49 +70,49 @@ func loadTests(
log.Log.V(3).Info(fmt.Sprintf("failed to clone repository %v as it is not valid", repoURL), "error", cloneErr) log.Log.V(3).Info(fmt.Sprintf("failed to clone repository %v as it is not valid", repoURL), "error", cloneErr)
os.Exit(1) os.Exit(1)
} }
if policyYamls, err := gitutils.ListYamls(fs, gitPathToYamls); err != nil { if yamlFiles, err := gitutils.ListYamls(fs, gitPathToYamls); err != nil {
errors = append(errors, sanitizederror.NewWithError("failed to list YAMLs in repository", err)) errors = append(errors, sanitizederror.NewWithError("failed to list YAMLs in repository", err))
} else { } else {
sort.Strings(policyYamls) sort.Strings(yamlFiles)
for _, yamlFilePath := range policyYamls { for _, yamlFilePath := range yamlFiles {
file, err := fs.Open(yamlFilePath) file, err := fs.Open(yamlFilePath)
if err != nil { if err != nil {
errors = append(errors, sanitizederror.NewWithError("Error: failed to open file", err)) errors = append(errors, sanitizederror.NewWithError("Error: failed to open file", err))
continue continue
} }
if path.Base(file.Name()) == fileName { if path.Base(file.Name()) == fileName {
policyresoucePath := strings.Trim(yamlFilePath, fileName) resoucePath := strings.Trim(yamlFilePath, fileName)
bytes, err := io.ReadAll(file) yamlBytes, err := io.ReadAll(file)
if err != nil { if err != nil {
errors = append(errors, sanitizederror.NewWithError("Error: failed to read file", err)) errors = append(errors, fmt.Errorf("failed to read file (%s)", err))
continue continue
} }
policyBytes, err := yaml.ToJSON(bytes) test, err := loadTest(yamlBytes)
if err != nil { if err != nil {
errors = append(errors, sanitizederror.NewWithError("failed to convert to JSON", err)) errors = append(errors, fmt.Errorf("failed to load test file (%s)", err))
continue continue
} }
policies = append(policies, policy{ tests = append(tests, testCase{
bytes: policyBytes, test: test,
resourcePath: policyresoucePath, resourcePath: resoucePath,
}) })
} }
} }
} }
} }
return fs, policies, errors return fs, tests, errors
} else { } else {
path := filepath.Clean(dirPath[0]) path := filepath.Clean(dirPath[0])
policies, errors = loadLocalTest(path, fileName) tests, errors = loadLocalTest(path, fileName)
return nil, policies, errors return nil, tests, errors
} }
} }
func loadLocalTest( func loadLocalTest(
path string, path string,
fileName string, fileName string,
) ([]policy, []error) { ) ([]testCase, []error) {
var policies []policy var policies []testCase
var errors []error var errors []error
files, err := os.ReadDir(path) files, err := os.ReadDir(path)
if err != nil { if err != nil {
@ -124,18 +125,18 @@ func loadLocalTest(
errors = append(errors, errs...) errors = append(errors, errs...)
} else if file.Name() == fileName { } else if file.Name() == fileName {
// We accept the risk of including files here as we read the test dir only. // We accept the risk of including files here as we read the test dir only.
yamlFile, err := os.ReadFile(filepath.Join(path, file.Name())) // #nosec G304 yamlBytes, err := os.ReadFile(filepath.Join(path, file.Name())) // #nosec G304
if err != nil { if err != nil {
errors = append(errors, sanitizederror.NewWithError("unable to read yaml", err)) errors = append(errors, fmt.Errorf("unable to read yaml (%s)", err))
continue continue
} }
valuesBytes, err := yaml.ToJSON(yamlFile) test, err := loadTest(yamlBytes)
if err != nil { if err != nil {
errors = append(errors, sanitizederror.NewWithError("failed to convert json", err)) errors = append(errors, fmt.Errorf("failed to load test file (%s)", err))
continue continue
} }
policies = append(policies, policy{ policies = append(policies, testCase{
bytes: valuesBytes, test: test,
resourcePath: path, resourcePath: path,
}) })
} }
@ -143,3 +144,11 @@ func loadLocalTest(
} }
return policies, errors return policies, errors
} }
func loadTest(data []byte) (*api.Test, error) {
var test api.Test
if err := yaml.UnmarshalStrict(data, &test); err != nil {
return nil, err
}
return &test, nil
}

View file

@ -0,0 +1,63 @@
package test
import (
"reflect"
"testing"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test/api"
)
func Test_loadTest(t *testing.T) {
tests := []struct {
name string
data []byte
want *api.Test
wantErr bool
}{{
name: "invalid schema",
data: []byte(`
- name: mytest
policies:
- pol.yaml
resources:
- pod.yaml
results:
- policy: evil-policy-match-foreign-pods
rule: evil-validation
resource: nginx
status: pass
`),
want: nil,
wantErr: true,
}, {
name: "unknown field",
data: []byte(`
name: mytest
policies:
- pol.yaml
resources:
- pod.yaml
results:
- policy: evil-policy-match-foreign-pods
rule: evil-validation
resource: nginx
foo: bar
result: pass
`),
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := loadTest(tt.data)
if (err != nil) != tt.wantErr {
t.Errorf("loadTest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("loadTest() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,7 +1,6 @@
package test package test
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -33,7 +32,7 @@ import (
func applyPoliciesFromPath( func applyPoliciesFromPath(
fs billy.Filesystem, fs billy.Filesystem,
policyBytes []byte, values *api.Test,
isGit bool, isGit bool,
policyResourcePath string, policyResourcePath string,
rc *resultCounts, rc *resultCounts,
@ -43,13 +42,9 @@ func applyPoliciesFromPath(
) (map[string]policyreportv1alpha2.PolicyReportResult, []api.TestResults, error) { ) (map[string]policyreportv1alpha2.PolicyReportResult, []api.TestResults, error) {
engineResponses := make([]engineapi.EngineResponse, 0) engineResponses := make([]engineapi.EngineResponse, 0)
var dClient dclient.Interface var dClient dclient.Interface
values := &api.Test{}
var resultCounts common.ResultCounts var resultCounts common.ResultCounts
store.SetLocal(true) store.SetLocal(true)
if err := json.Unmarshal(policyBytes, values); err != nil {
return nil, nil, sanitizederror.NewWithError("failed to decode yaml", err)
}
var filteredResults []api.TestResults var filteredResults []api.TestResults
for _, res := range values.Results { for _, res := range values.Results {

View file

@ -94,14 +94,29 @@ func testCommandExecute(
// load tests // load tests
fs, policies, errors := loadTests(dirPath, fileName, gitBranch) fs, policies, errors := loadTests(dirPath, fileName, gitBranch)
if len(policies) == 0 { if len(policies) == 0 {
fmt.Printf("\n No test yamls available \n") fmt.Println()
fmt.Println("No test yamls available")
}
if len(errors) > 0 {
fmt.Println()
fmt.Println("Test errors:")
for _, e := range errors {
fmt.Printf(" %v \n", e.Error())
}
}
if len(policies) == 0 {
if len(errors) == 0 {
os.Exit(0)
} else {
os.Exit(1)
}
} }
rc = &resultCounts{} rc = &resultCounts{}
var table table.Table var table table.Table
for _, p := range policies { for _, p := range policies {
if reports, tests, err := applyPoliciesFromPath( if reports, tests, err := applyPoliciesFromPath(
fs, fs,
p.bytes, p.test,
fs != nil, fs != nil,
p.resourcePath, p.resourcePath,
rc, rc,
@ -116,12 +131,6 @@ func testCommandExecute(
table.AddFailed(t.RawRows...) table.AddFailed(t.RawRows...)
} }
} }
if len(errors) > 0 && log.Log.V(1).Enabled() {
fmt.Println("test errors:")
for _, e := range errors {
fmt.Printf(" %v \n", e.Error())
}
}
if !failOnly { if !failOnly {
fmt.Printf("\nTest Summary: %d tests passed and %d tests failed\n", rc.Pass+rc.Skip, rc.Fail) fmt.Printf("\nTest Summary: %d tests passed and %d tests failed\n", rc.Pass+rc.Skip, rc.Fail)
} else { } else {