1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 07:57:07 +00:00

Merge pull request #1518 from kyverno/test_cli

test command for kyverno
This commit is contained in:
Jim Bugwadia 2021-02-05 12:44:07 -08:00 committed by GitHub
commit b91022d438
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 779 additions and 287 deletions

View file

@ -1,10 +1,7 @@
package apply
import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
@ -14,21 +11,17 @@ import (
v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
pkgCommon "github.com/kyverno/kyverno/pkg/common"
client "github.com/kyverno/kyverno/pkg/dclient"
"github.com/kyverno/kyverno/pkg/engine"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/kyverno/common"
sanitizederror "github.com/kyverno/kyverno/pkg/kyverno/sanitizedError"
"github.com/kyverno/kyverno/pkg/openapi"
policy2 "github.com/kyverno/kyverno/pkg/policy"
"github.com/kyverno/kyverno/pkg/utils"
"github.com/spf13/cobra"
yamlv2 "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/genericclioptions"
log "sigs.k8s.io/controller-runtime/pkg/log"
yaml1 "sigs.k8s.io/yaml"
"github.com/go-git/go-billy/v5/memfs"
)
type resultCounts struct {
@ -148,12 +141,13 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool,
variablesString string, valuesFile string, namespace string, policyPaths []string) (validateEngineResponses []*response.EngineResponse, rc *resultCounts, resources []*unstructured.Unstructured, skippedPolicies []SkippedPolicy, err error) {
kubernetesConfig := genericclioptions.NewConfigFlags(true)
fs := memfs.New()
if valuesFile != "" && variablesString != "" {
return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError("pass the values either using set flag or values_file flag", err)
}
variables, valuesMap, err := getVariable(variablesString, valuesFile)
variables, valuesMap, err := common.GetVariable(variablesString, valuesFile)
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError("failed to decode yaml", err)
@ -186,7 +180,7 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool,
return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError("a stdin pipe can be used for either policies or resources, not both", err)
}
policies, err := getPoliciesFromPaths(policyPaths)
policies, err := common.GetPoliciesFromPaths(fs,policyPaths, false)
if err != nil {
fmt.Printf("Error: failed to load policies\nCause: %s\n", err)
os.Exit(1)
@ -204,14 +198,14 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool,
return validateEngineResponses, rc, resources, skippedPolicies, err
}
mutatedPolicies, err := mutatePolices(policies)
mutatedPolicies, err := common.MutatePolices(policies)
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError("failed to mutate policy", err)
}
}
resources, err = getResourceAccordingToResourcePath(resourcePaths, cluster, mutatedPolicies, dClient, namespace, policyReport)
resources, err = common.GetResourceAccordingToResourcePath(fs, resourcePaths, cluster, mutatedPolicies, dClient, namespace, policyReport, false)
if err != nil {
fmt.Printf("Error: failed to load resources\nCause: %s\n", err)
os.Exit(1)
@ -245,7 +239,7 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool,
}
matches := common.PolicyHasVariables(*policy)
variable := removeDuplicatevariables(matches)
variable := common.RemoveDuplicateVariables(matches)
if len(matches) > 0 && variablesString == "" && valuesFile == "" {
rc.skip++
@ -274,11 +268,18 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool,
return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError(fmt.Sprintf("policy %s have variables. pass the values for the variables using set/values_file flag", policy.Name), err)
}
ers, validateErs, err := applyPolicyOnResource(policy, resource, mutateLogPath, mutateLogPathIsDir, thisPolicyResourceValues, rc, policyReport)
ers, validateErs, responseError, rcErs, err := common.ApplyPolicyOnResource(policy, resource, mutateLogPath, mutateLogPathIsDir, thisPolicyResourceValues, policyReport)
if err != nil {
return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.Name, resource.GetName()).Error(), err)
}
if responseError == true {
rc.fail++
} else {
rc.pass++
}
if rcErs == true {
rc.error++
}
engineResponses = append(engineResponses, ers...)
validateEngineResponses = append(validateEngineResponses, validateErs)
}
@ -287,44 +288,6 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool,
return validateEngineResponses, rc, resources, skippedPolicies, nil
}
// getVariable - get the variables from console/file
func getVariable(variablesString, valuesFile string) (variables map[string]string, valuesMap map[string]map[string]Resource, err error) {
if variablesString != "" {
kvpairs := strings.Split(strings.Trim(variablesString, " "), ",")
for _, kvpair := range kvpairs {
kvs := strings.Split(strings.Trim(kvpair, " "), "=")
variables[strings.Trim(kvs[0], " ")] = strings.Trim(kvs[1], " ")
}
}
if valuesFile != "" {
yamlFile, err := ioutil.ReadFile(valuesFile)
if err != nil {
return variables, valuesMap, sanitizederror.NewWithError("unable to read yaml", err)
}
valuesBytes, err := yaml.ToJSON(yamlFile)
if err != nil {
return variables, valuesMap, sanitizederror.NewWithError("failed to convert json", err)
}
values := &Values{}
if err := json.Unmarshal(valuesBytes, values); err != nil {
return variables, valuesMap, sanitizederror.NewWithError("failed to decode yaml", err)
}
for _, p := range values.Policies {
pmap := make(map[string]Resource)
for _, r := range p.Resources {
pmap[r.Name] = r
}
valuesMap[p.Name] = pmap
}
}
return variables, valuesMap, nil
}
// checkMutateLogPath - checking path for printing mutated resource (-o flag)
func checkMutateLogPath(mutateLogPath string) (mutateLogPathIsDir bool, err error) {
if mutateLogPath != "" {
@ -347,68 +310,6 @@ func checkMutateLogPath(mutateLogPath string) (mutateLogPathIsDir bool, err erro
return mutateLogPathIsDir, err
}
// getPoliciesFromPaths - get policies according to the resource path
func getPoliciesFromPaths(policyPaths []string) (policies []*v1.ClusterPolicy, err error) {
if len(policyPaths) > 0 && policyPaths[0] == "-" {
if common.IsInputFromPipe() {
policyStr := ""
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
policyStr = policyStr + scanner.Text() + "\n"
}
yamlBytes := []byte(policyStr)
policies, err = utils.GetPolicy(yamlBytes)
if err != nil {
return nil, sanitizederror.NewWithError("failed to extract the resources", err)
}
}
} else {
var errors []error
policies, errors = common.GetPolicies(policyPaths)
if len(policies) == 0 {
if len(errors) > 0 {
return nil, sanitizederror.NewWithErrors("failed to read policies", errors)
}
return nil, sanitizederror.New(fmt.Sprintf("no policies found in paths %v", policyPaths))
}
if len(errors) > 0 && log.Log.V(1).Enabled() {
fmt.Printf("ignoring errors: \n")
for _, e := range errors {
fmt.Printf(" %v \n", e.Error())
}
}
}
return
}
// getResourceAccordingToResourcePath - get resources according to the resource path
func getResourceAccordingToResourcePath(resourcePaths []string, cluster bool, policies []*v1.ClusterPolicy, dClient *client.Client, namespace string, policyReport bool) (resources []*unstructured.Unstructured, err error) {
if len(resourcePaths) > 0 && resourcePaths[0] == "-" {
if common.IsInputFromPipe() {
resourceStr := ""
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
resourceStr = resourceStr + scanner.Text() + "\n"
}
yamlBytes := []byte(resourceStr)
resources, err = common.GetResource(yamlBytes)
if err != nil {
return nil, sanitizederror.NewWithError("failed to extract the resources", err)
}
}
} else if (len(resourcePaths) > 0 && resourcePaths[0] != "-") || len(resourcePaths) < 0 || cluster {
resources, err = common.GetResources(policies, resourcePaths, dClient, cluster, namespace, policyReport)
if err != nil {
return resources, err
}
}
return resources, err
}
// printReportOrViolation - printing policy report/violations
func printReportOrViolation(policyReport bool, validateEngineResponses []*response.EngineResponse, rc *resultCounts, resourcePaths []string, resourcesLen int, skippedPolicies []SkippedPolicy) {
if policyReport {
@ -437,165 +338,6 @@ func printReportOrViolation(policyReport bool, validateEngineResponses []*respon
}
}
// applyPolicyOnResource - function to apply policy on resource
func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unstructured,
mutateLogPath string, mutateLogPathIsDir bool, variables map[string]string,
rc *resultCounts, policyReport bool) ([]*response.EngineResponse, *response.EngineResponse, error) {
responseError := false
engineResponses := make([]*response.EngineResponse, 0)
resPath := fmt.Sprintf("%s/%s/%s", resource.GetNamespace(), resource.GetKind(), resource.GetName())
log.Log.V(3).Info("applying policy on resource", "policy", policy.Name, "resource", resPath)
ctx := context.NewContext()
for key, value := range variables {
startString := ""
endString := ""
for _, k := range strings.Split(key, ".") {
startString += fmt.Sprintf(`{"%s":`, k)
endString += `}`
}
midString := fmt.Sprintf(`"%s"`, value)
finalString := startString + midString + endString
var jsonData = []byte(finalString)
ctx.AddJSON(jsonData)
}
mutateResponse := engine.Mutate(&engine.PolicyContext{Policy: *policy, NewResource: *resource, JSONContext: ctx})
engineResponses = append(engineResponses, mutateResponse)
if !mutateResponse.IsSuccessful() {
fmt.Printf("Failed to apply mutate policy %s -> resource %s", policy.Name, resPath)
for i, r := range mutateResponse.PolicyResponse.Rules {
fmt.Printf("\n%d. %s", i+1, r.Message)
}
responseError = true
} else {
if len(mutateResponse.PolicyResponse.Rules) > 0 {
yamlEncodedResource, err := yamlv2.Marshal(mutateResponse.PatchedResource.Object)
if err != nil {
rc.error++
}
if mutateLogPath == "" {
mutatedResource := string(yamlEncodedResource)
if len(strings.TrimSpace(mutatedResource)) > 0 {
fmt.Printf("\nmutate policy %s applied to %s:", policy.Name, resPath)
fmt.Printf("\n" + mutatedResource)
fmt.Printf("\n")
}
} else {
err := printMutatedOutput(mutateLogPath, mutateLogPathIsDir, string(yamlEncodedResource), resource.GetName()+"-mutated")
if err != nil {
return engineResponses, &response.EngineResponse{}, sanitizederror.NewWithError("failed to print mutated result", err)
}
fmt.Printf("\n\nMutation:\nMutation has been applied successfully. Check the files.")
}
}
}
if resource.GetKind() == "Pod" && len(resource.GetOwnerReferences()) > 0 {
if policy.HasAutoGenAnnotation() {
if _, ok := policy.GetAnnotations()[engine.PodControllersAnnotation]; ok {
delete(policy.Annotations, engine.PodControllersAnnotation)
}
}
}
policyCtx := &engine.PolicyContext{Policy: *policy, NewResource: mutateResponse.PatchedResource, JSONContext: ctx}
validateResponse := engine.Validate(policyCtx)
if !policyReport {
if !validateResponse.IsSuccessful() {
fmt.Printf("\npolicy %s -> resource %s failed: \n", policy.Name, resPath)
for i, r := range validateResponse.PolicyResponse.Rules {
if !r.Success {
fmt.Printf("%d. %s: %s \n", i+1, r.Name, r.Message)
}
}
responseError = true
}
}
var policyHasGenerate bool
for _, rule := range policy.Spec.Rules {
if rule.HasGenerate() {
policyHasGenerate = true
}
}
if policyHasGenerate {
ctx := &engine.PolicyContext{Policy: *policy, NewResource: *resource}
generateResponse := engine.Generate(ctx)
engineResponses = append(engineResponses, generateResponse)
if len(generateResponse.PolicyResponse.Rules) > 0 {
log.Log.V(3).Info("generate resource is valid", "policy", policy.Name, "resource", resPath)
} else {
fmt.Printf("generate policy %s resource %s is invalid \n", policy.Name, resPath)
for i, r := range generateResponse.PolicyResponse.Rules {
fmt.Printf("%d. %s \b", i+1, r.Message)
}
responseError = true
}
}
if responseError == true {
rc.fail++
} else {
rc.pass++
}
return engineResponses, validateResponse, nil
}
// mutatePolicies - function to apply mutation on policies
func mutatePolices(policies []*v1.ClusterPolicy) ([]*v1.ClusterPolicy, error) {
newPolicies := make([]*v1.ClusterPolicy, 0)
logger := log.Log.WithName("apply")
for _, policy := range policies {
p, err := common.MutatePolicy(policy, logger)
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return nil, sanitizederror.NewWithError("failed to mutate policy.", err)
}
return nil, err
}
newPolicies = append(newPolicies, p)
}
return newPolicies, nil
}
// printMutatedOutput - function to print output in provided file or directory
func printMutatedOutput(mutateLogPath string, mutateLogPathIsDir bool, yaml string, fileName string) error {
var f *os.File
var err error
yaml = yaml + ("\n---\n\n")
if !mutateLogPathIsDir {
f, err = os.OpenFile(mutateLogPath, os.O_APPEND|os.O_WRONLY, 0644)
} else {
f, err = os.OpenFile(mutateLogPath+"/"+fileName+".yaml", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
if err != nil {
return err
}
if _, err := f.Write([]byte(yaml)); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
return nil
}
// createFileOrFolder - creating file or folder according to path provided
func createFileOrFolder(mutateLogPath string, mutateLogPathIsDir bool) error {
mutateLogPath = filepath.Clean(mutateLogPath)
@ -644,16 +386,3 @@ func createFileOrFolder(mutateLogPath string, mutateLogPathIsDir bool) error {
return nil
}
// removeDuplicatevariables - remove duplicate variables
func removeDuplicatevariables(matches [][]string) string {
var variableStr string
for _, m := range matches {
for _, v := range m {
foundVariable := strings.Contains(variableStr, v)
if !foundVariable {
variableStr = variableStr + " " + v
}
}
}
return variableStr
}

View file

@ -9,6 +9,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"sigs.k8s.io/controller-runtime/pkg/log"
jsonpatch "github.com/evanphx/json-patch"
@ -20,9 +21,32 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
yaml_v2 "sigs.k8s.io/yaml"
"github.com/kyverno/kyverno/pkg/engine"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response"
yamlv2 "gopkg.in/yaml.v2"
"github.com/go-git/go-billy/v5"
ut "github.com/kyverno/kyverno/pkg/utils"
client "github.com/kyverno/kyverno/pkg/dclient"
)
// GetPolicies - Extracting the policies from multiple YAML
type Resource struct {
Name string `json:"name"`
Values map[string]string `json:"values"`
}
type Policy struct {
Name string `json:"name"`
Resources []Resource `json:"resources"`
}
type Values struct {
Policies []Policy `json:"policies"`
}
func GetPolicies(paths []string) (policies []*v1.ClusterPolicy, errors []error) {
for _, path := range paths {
log.Log.V(5).Info("reading policies", "path", path)
@ -222,3 +246,300 @@ func IsInputFromPipe() bool {
fileInfo, _ := os.Stdin.Stat()
return fileInfo.Mode()&os.ModeCharDevice == 0
}
// RemoveDuplicateVariables - remove duplicate variables
func RemoveDuplicateVariables(matches [][]string) string {
var variableStr string
for _, m := range matches {
for _, v := range m {
foundVariable := strings.Contains(variableStr, v)
if !foundVariable {
variableStr = variableStr + " " + v
}
}
}
return variableStr
}
// GetVariable - get the variables from console/file
func GetVariable(variablesString, valuesFile string) ( map[string]string, map[string]map[string]Resource, error) {
valuesMap := make(map[string]map[string]Resource)
variables := make(map[string]string)
if variablesString != "" {
kvpairs := strings.Split(strings.Trim(variablesString, " "), ",")
for _, kvpair := range kvpairs {
kvs := strings.Split(strings.Trim(kvpair, " "), "=")
variables[strings.Trim(kvs[0], " ")] = strings.Trim(kvs[1], " ")
}
}
if valuesFile != "" {
yamlFile, err := ioutil.ReadFile(valuesFile)
if err != nil {
return variables, valuesMap, sanitizederror.NewWithError("unable to read yaml", err)
}
valuesBytes, err := yaml.ToJSON(yamlFile)
if err != nil {
return variables, valuesMap, sanitizederror.NewWithError("failed to convert json", err)
}
values := &Values{}
if err := json.Unmarshal(valuesBytes, values); err != nil {
return variables, valuesMap, sanitizederror.NewWithError("failed to decode yaml", err)
}
for _, p := range values.Policies {
pmap := make(map[string]Resource)
for _, r := range p.Resources {
pmap[r.Name] = r
}
valuesMap[p.Name] = pmap
}
}
return variables, valuesMap, nil
}
// MutatePolices - function to apply mutation on policies
func MutatePolices(policies []*v1.ClusterPolicy) ([]*v1.ClusterPolicy, error) {
newPolicies := make([]*v1.ClusterPolicy, 0)
logger := log.Log.WithName("apply")
for _, policy := range policies {
p, err := MutatePolicy(policy, logger)
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return nil, sanitizederror.NewWithError("failed to mutate policy.", err)
}
return nil, err
}
newPolicies = append(newPolicies, p)
}
return newPolicies, nil
}
// ApplyPolicyOnResource - function to apply policy on resource
func ApplyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unstructured,
mutateLogPath string, mutateLogPathIsDir bool, variables map[string]string, policyReport bool) ([]*response.EngineResponse, *response.EngineResponse, bool, bool, error) {
responseError := false
rcError := false
engineResponses := make([]*response.EngineResponse, 0)
resPath := fmt.Sprintf("%s/%s/%s", resource.GetNamespace(), resource.GetKind(), resource.GetName())
log.Log.V(3).Info("applying policy on resource", "policy", policy.Name, "resource", resPath)
ctx := context.NewContext()
for key, value := range variables {
startString := ""
endString := ""
for _, k := range strings.Split(key, ".") {
startString += fmt.Sprintf(`{"%s":`, k)
endString += `}`
}
midString := fmt.Sprintf(`"%s"`, value)
finalString := startString + midString + endString
var jsonData = []byte(finalString)
ctx.AddJSON(jsonData)
}
mutateResponse := engine.Mutate(&engine.PolicyContext{Policy: *policy, NewResource: *resource, JSONContext: ctx})
engineResponses = append(engineResponses, mutateResponse)
if !mutateResponse.IsSuccessful() {
fmt.Printf("Failed to apply mutate policy %s -> resource %s", policy.Name, resPath)
for i, r := range mutateResponse.PolicyResponse.Rules {
fmt.Printf("\n%d. %s", i+1, r.Message)
}
responseError = true
} else {
if len(mutateResponse.PolicyResponse.Rules) > 0 {
yamlEncodedResource, err := yamlv2.Marshal(mutateResponse.PatchedResource.Object)
if err != nil {
rcError = true
}
if mutateLogPath == "" {
mutatedResource := string(yamlEncodedResource)
if len(strings.TrimSpace(mutatedResource)) > 0 {
fmt.Printf("\nmutate policy %s applied to %s:", policy.Name, resPath)
fmt.Printf("\n" + mutatedResource)
fmt.Printf("\n")
}
} else {
err := PrintMutatedOutput(mutateLogPath, mutateLogPathIsDir, string(yamlEncodedResource), resource.GetName()+"-mutated")
if err != nil {
return engineResponses, &response.EngineResponse{}, responseError, rcError, sanitizederror.NewWithError("failed to print mutated result", err)
}
fmt.Printf("\n\nMutation:\nMutation has been applied successfully. Check the files.")
}
}
}
if resource.GetKind() == "Pod" && len(resource.GetOwnerReferences()) > 0 {
if policy.HasAutoGenAnnotation() {
if _, ok := policy.GetAnnotations()[engine.PodControllersAnnotation]; ok {
delete(policy.Annotations, engine.PodControllersAnnotation)
}
}
}
policyCtx := &engine.PolicyContext{Policy: *policy, NewResource: mutateResponse.PatchedResource, JSONContext: ctx}
validateResponse := engine.Validate(policyCtx)
if !policyReport {
if !validateResponse.IsSuccessful() {
fmt.Printf("\npolicy %s -> resource %s failed: \n", policy.Name, resPath)
for i, r := range validateResponse.PolicyResponse.Rules {
if !r.Success {
fmt.Printf("%d. %s: %s \n", i+1, r.Name, r.Message)
}
}
responseError = true
}
}
var policyHasGenerate bool
for _, rule := range policy.Spec.Rules {
if rule.HasGenerate() {
policyHasGenerate = true
}
}
if policyHasGenerate {
generateResponse := engine.Generate(&engine.PolicyContext{Policy: *policy, NewResource: *resource})
engineResponses = append(engineResponses, generateResponse)
if len(generateResponse.PolicyResponse.Rules) > 0 {
log.Log.V(3).Info("generate resource is valid", "policy", policy.Name, "resource", resPath)
} else {
fmt.Printf("generate policy %s resource %s is invalid \n", policy.Name, resPath)
for i, r := range generateResponse.PolicyResponse.Rules {
fmt.Printf("%d. %s \b", i+1, r.Message)
}
responseError = true
}
}
return engineResponses, validateResponse, responseError, rcError, nil
}
// PrintMutatedOutput - function to print output in provided file or directory
func PrintMutatedOutput(mutateLogPath string, mutateLogPathIsDir bool, yaml string, fileName string) error {
var f *os.File
var err error
yaml = yaml + ("\n---\n\n")
if !mutateLogPathIsDir {
f, err = os.OpenFile(mutateLogPath, os.O_APPEND|os.O_WRONLY, 0644)
} else {
f, err = os.OpenFile(mutateLogPath+"/"+fileName+".yaml", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
if err != nil {
return err
}
if _, err := f.Write([]byte(yaml)); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
return nil
}
// GetPoliciesFromPaths - get policies according to the resource path
func GetPoliciesFromPaths(fs billy.Filesystem, dirPath []string, isGit bool) (policies []*v1.ClusterPolicy, err error) {
var errors []error
if isGit {
for _, pp := range dirPath {
filep, err := fs.Open(pp)
bytes, err := ioutil.ReadAll(filep)
if err != nil {
fmt.Printf("Error: failed to read file %s: %v", filep.Name(), err.Error())
}
policyBytes, err := yaml.ToJSON(bytes)
if err != nil {
fmt.Printf("failed to convert to JSON: %v", err)
continue
}
policiesFromFile, errFromFile := ut.GetPolicy(policyBytes)
if errFromFile != nil {
err := fmt.Errorf("failed to process : %v", errFromFile.Error())
errors = append(errors, err)
continue
}
policies = append(policies, policiesFromFile...)
}
} else {
if len(dirPath) > 0 && dirPath[0] == "-" {
if IsInputFromPipe() {
policyStr := ""
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
policyStr = policyStr + scanner.Text() + "\n"
}
yamlBytes := []byte(policyStr)
policies, err = ut.GetPolicy(yamlBytes)
if err != nil {
return nil, sanitizederror.NewWithError("failed to extract the resources", err)
}
}
} else {
var errors []error
policies, errors = GetPolicies(dirPath)
if len(policies) == 0 {
if len(errors) > 0 {
return nil, sanitizederror.NewWithErrors("failed to read file", errors)
}
return nil, sanitizederror.New(fmt.Sprintf("no file found in paths %v", dirPath))
}
if len(errors) > 0 && log.Log.V(1).Enabled() {
fmt.Printf("ignoring errors: \n")
for _, e := range errors {
fmt.Printf(" %v \n", e.Error())
}
}
}
}
return
}
// GetResourceAccordingToResourcePath - get resources according to the resource path
func GetResourceAccordingToResourcePath(fs billy.Filesystem, resourcePaths []string,
cluster bool, policies []*v1.ClusterPolicy, dClient *client.Client, namespace string, policyReport bool, isGit bool) (resources []*unstructured.Unstructured, err error) {
if isGit {
resources, err = GetResourcesWithTest(fs, policies, resourcePaths, isGit)
if err != nil {
return nil, sanitizederror.NewWithError("failed to extract the resources", err)
}
} else {
if len(resourcePaths) > 0 && resourcePaths[0] == "-" {
if IsInputFromPipe() {
resourceStr := ""
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
resourceStr = resourceStr + scanner.Text() + "\n"
}
yamlBytes := []byte(resourceStr)
resources, err = GetResource(yamlBytes)
if err != nil {
return nil, sanitizederror.NewWithError("failed to extract the resources", err)
}
}
} else if (len(resourcePaths) > 0 && resourcePaths[0] != "-") || len(resourcePaths) < 0 || cluster {
resources, err = GetResources(policies, resourcePaths, dClient, cluster, namespace, policyReport)
if err != nil {
return resources, err
}
}
}
return resources, err
}

View file

@ -15,6 +15,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
log "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/go-git/go-billy/v5"
)
// GetResources gets matched resources by the given policies
@ -96,6 +97,52 @@ func GetResources(policies []*v1.ClusterPolicy, resourcePaths []string, dClient
}
return resources, nil
}
// GetResourcesWithTest with gets matched resources by the given policies
func GetResourcesWithTest(fs billy.Filesystem,policies []*v1.ClusterPolicy, resourcePaths []string, isGit bool) ([]*unstructured.Unstructured, error) {
resources := make([]*unstructured.Unstructured, 0)
var resourceTypesMap = make(map[string]bool)
var resourceTypes []string
for _, policy := range policies {
for _, rule := range policy.Spec.Rules {
for _, kind := range rule.MatchResources.Kinds {
resourceTypesMap[kind] = true
}
}
}
for kind := range resourceTypesMap {
resourceTypes = append(resourceTypes, kind)
}
if len(resourcePaths) > 0 {
for _, resourcePath := range resourcePaths {
var resourceBytes []byte
var err error
if isGit {
filep, err := fs.Open(resourcePath)
if err != nil {
fmt.Printf("Unable to open resource file: %s. error: %s", resourcePath, err)
continue
}
resourceBytes, err = ioutil.ReadAll(filep)
} else {
resourceBytes, err = getFileBytes(resourcePath)
}
if err != nil {
fmt.Printf("\n----------------------------------------------------------------------\nfailed to load resources: %s. \nerror: %s\n----------------------------------------------------------------------\n", resourcePath, err)
continue
}
getResources, err := GetResource(resourceBytes)
if err != nil {
return nil, err
}
for _, resource := range getResources {
resources = append(resources, resource)
}
}
}
return resources, nil
}
// GetResource converts raw bytes to unstructured object
func GetResource(resourceBytes []byte) ([]*unstructured.Unstructured, error) {

View file

@ -7,6 +7,7 @@ import (
"github.com/kyverno/kyverno/pkg/kyverno/apply"
"github.com/kyverno/kyverno/pkg/kyverno/validate"
"github.com/kyverno/kyverno/pkg/kyverno/version"
"github.com/kyverno/kyverno/pkg/kyverno/test"
"github.com/spf13/cobra"
"k8s.io/klog"
"k8s.io/klog/klogr"
@ -26,6 +27,7 @@ func CLI() {
version.Command(),
apply.Command(),
validate.Command(),
test.Command(),
}
cli.AddCommand(commands...)

355
pkg/kyverno/test/command.go Normal file
View file

@ -0,0 +1,355 @@
package test
import (
"fmt"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"net/url"
"sort"
"reflect"
"strings"
"github.com/spf13/cobra"
log "sigs.k8s.io/controller-runtime/pkg/log"
sanitizederror "github.com/kyverno/kyverno/pkg/kyverno/sanitizedError"
v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/kyverno/common"
"k8s.io/apimachinery/pkg/util/yaml"
policy2 "github.com/kyverno/kyverno/pkg/policy"
"github.com/kyverno/kyverno/pkg/openapi"
"github.com/kyverno/kyverno/pkg/engine/response"
corev1 "k8s.io/api/core/v1"
"github.com/kyverno/kyverno/pkg/policyreport"
report "github.com/kyverno/kyverno/pkg/api/policyreport/v1alpha1"
"github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kataras/tablewriter"
"github.com/lensesio/tableprinter"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-billy/v5"
"github.com/fatih/color"
client "github.com/kyverno/kyverno/pkg/dclient"
)
// Command returns version command
func Command() *cobra.Command {
var valuesFile string
return &cobra.Command{
Use: "test",
Short: "Shows current test of kyverno",
RunE: func(cmd *cobra.Command, dirPath []string) (err error) {
defer func() {
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
log.Log.Error(err, "failed to sanitize")
err = fmt.Errorf("internal error")
}
}
}()
err = testCommandExecute(dirPath, valuesFile)
if err != nil {
log.Log.V(3).Info("a directory is required")
return err
}
return nil
},
}
}
type Test struct {
Name string `json:"name"`
Policies []string `json:"policies"`
Resources []string `json:"resources"`
Variables string `json:"variables"`
Results []TestResults `json:"results"`
}
type SkippedPolicy struct {
Name string `json:"name"`
Rules []v1.Rule `json:"rules"`
Variable string `json:"variable"`
}
type TestResults struct {
Policy string `json:"policy"`
Rule string `json:"rule"`
Status string `json:"status"`
Resource string `json:"resource"`
}
type ReportResult struct {
TestResults
Resources []*corev1.ObjectReference `json:"resources"`
}
type Resource struct {
Name string `json:"name"`
Values map[string]string `json:"values"`
}
type Table struct {
ID int `header:"#"`
Resource string `header:"test"`
Result string `header:"result"`
}
type Policy struct {
Name string `json:"name"`
Resources []Resource `json:"resources"`
}
type Values struct {
Policies []Policy `json:"policies"`
}
func testCommandExecute(dirPath []string, valuesFile string) (err error) {
var errors []error
fs := memfs.New()
if len(dirPath) == 0 {
return sanitizederror.NewWithError(fmt.Sprintf("a directory is required"), err)
}
if strings.Contains(string(dirPath[0]), "https://") {
gitUrl, err := url.Parse(dirPath[0])
if err != nil {
return sanitizederror.NewWithError("failed to parse URL", err)
}
pathElems := strings.Split(gitUrl.Path[1:], "/")
if len(pathElems) != 3 {
err := fmt.Errorf("invalid URL path %s - expected https://github.com/:owner/:repository/:branch", gitUrl.Path)
return sanitizederror.NewWithError("failed to parse URL", err)
}
gitUrl.Path = strings.Join([]string{"/", pathElems[0], pathElems[1]}, "/")
repoURL := gitUrl.String()
cloneRepo, err := clone(repoURL, fs)
if err != nil {
return sanitizederror.NewWithError("failed to clone repository ", err)
}
log.Log.V(3).Info(" clone repository", cloneRepo )
policyYamls, err := listYAMLs(fs, "/")
if err != nil {
return sanitizederror.NewWithError("failed to list YAMLs in repository", err)
}
sort.Strings(policyYamls)
for _, yamlFilePath := range policyYamls {
file, err := fs.Open(yamlFilePath)
bytes, err := ioutil.ReadAll(file)
if err != nil {
sanitizederror.NewWithError("Error: failed to read file", err)
}
policyBytes, err := yaml.ToJSON(bytes)
if err != nil {
sanitizederror.NewWithError("failed to convert to JSON", err)
continue
}
if err := applyPoliciesFromPath(fs, policyBytes, valuesFile, true); err != nil {
return sanitizederror.NewWithError("failed to apply test command", err)
}
}
} else {
path := filepath.Clean(dirPath[0])
fileDesc, err := os.Stat(path)
if err != nil {
errors = append(errors, err)
}
if fileDesc.IsDir() {
files, err := ioutil.ReadDir(path)
if err != nil {
errors = append(errors, fmt.Errorf("failed to read %v: %v", path, err.Error()))
}
for _, file := range files {
fmt.Printf("\napplying test on file %s...", file.Name())
yamlFile, err := ioutil.ReadFile(filepath.Join(path, file.Name()))
if err != nil {
return sanitizederror.NewWithError("unable to read yaml", err)
}
valuesBytes, err := yaml.ToJSON(yamlFile)
if err != nil {
return sanitizederror.NewWithError("failed to convert json", err)
}
if err := applyPoliciesFromPath(fs, valuesBytes, valuesFile, false); err != nil {
return sanitizederror.NewWithError("failed to apply test command", err)
}
}
}
if len(errors) > 0 && log.Log.V(1).Enabled() {
fmt.Printf("ignoring errors: \n")
for _, e := range errors {
fmt.Printf(" %v \n", e.Error())
}
}
}
return nil
}
func buildPolicyResults(resps []*response.EngineResponse) map[string][]interface{} {
results := make(map[string][]interface{})
infos := policyreport.GeneratePRsFromEngineResponse(resps, log.Log)
for _, info := range infos {
for _, infoResult := range info.Results {
for _, rule := range infoResult.Rules {
if rule.Type != utils.Validation.String() {
continue
}
result := report.PolicyReportResult{
Policy: info.PolicyName,
Resources: []*corev1.ObjectReference{
{
Name: infoResult.Resource.Name,
},
},
}
result.Rule = rule.Name
result.Status = report.PolicyStatus(rule.Check)
results[rule.Name] = append(results[rule.Name], result)
}
}
}
return results
}
func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, valuesFile string, isGit bool) (err error) {
openAPIController, err := openapi.NewOpenAPIController()
engineResponses := make([]*response.EngineResponse, 0)
validateEngineResponses := make([]*response.EngineResponse, 0)
skippedPolicies := make([]SkippedPolicy, 0)
var dClient *client.Client
values := &Test{}
var variablesString string
if err := json.Unmarshal(policyBytes, values); err != nil {
return sanitizederror.NewWithError("failed to decode yaml", err)
}
_, valuesMap, err := common.GetVariable(variablesString, values.Variables)
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return sanitizederror.NewWithError("failed to decode yaml", err)
}
return err
}
policies, err := common.GetPoliciesFromPaths(fs, values.Policies, isGit)
if err != nil {
fmt.Printf("Error: failed to load policies\nCause: %s\n", err)
os.Exit(1)
}
mutatedPolicies, err := common.MutatePolices(policies)
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return sanitizederror.NewWithError("failed to mutate policy", err)
}
}
resources, err := common.GetResourceAccordingToResourcePath(fs, values.Resources, false, mutatedPolicies, dClient, "", false, isGit)
if err != nil {
fmt.Printf("Error: failed to load resources\nCause: %s\n", err)
os.Exit(1)
}
if err != nil {
fmt.Printf("Error: failed to load resources\nCause: %s\n", err)
os.Exit(1)
}
msgPolicies := "1 policy"
if len(mutatedPolicies) > 1 {
msgPolicies = fmt.Sprintf("%d policies", len(policies))
}
msgResources := "1 resource"
if len(resources) > 1 {
msgResources = fmt.Sprintf("%d resources", len(resources))
}
if len(mutatedPolicies) > 0 && len(resources) > 0 {
fmt.Printf("\napplying %s to %s... \n", msgPolicies, msgResources)
}
for _, policy := range mutatedPolicies {
err := policy2.Validate(policy, nil, true, openAPIController)
if err != nil {
fmt.Println("valuesMap1")
log.Log.V(3).Info(fmt.Sprintf("skipping policy %v as it is not valid", policy.Name), "error", err)
continue
}
matches := common.PolicyHasVariables(*policy)
variable := common.RemoveDuplicateVariables(matches)
if len(matches) > 0 && variablesString == "" && values.Variables == "" {
skipPolicy := SkippedPolicy{
Name: policy.GetName(),
Rules: policy.Spec.Rules,
Variable: variable,
}
skippedPolicies = append(skippedPolicies, skipPolicy)
log.Log.V(3).Info(fmt.Sprintf("skipping policy %s", policy.Name), "error", fmt.Sprintf("policy have variable - %s", variable))
continue
}
for _, resource := range resources {
thisPolicyResourceValues := make(map[string]string)
if len(valuesMap[policy.GetName()]) != 0 && !reflect.DeepEqual(valuesMap[policy.GetName()][resource.GetName()], Resource{}) {
thisPolicyResourceValues = valuesMap[policy.GetName()][resource.GetName()].Values
}
if len(common.PolicyHasVariables(*policy)) > 0 && len(thisPolicyResourceValues) == 0 {
return sanitizederror.NewWithError(fmt.Sprintf("policy %s have variables. pass the values for the variables using set/values_file flag", policy.Name), err)
}
ers, validateErs, _, _, err := common.ApplyPolicyOnResource(policy, resource, "", false, thisPolicyResourceValues, true)
if err != nil {
return sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.Name, resource.GetName()).Error(), err)
}
engineResponses = append(engineResponses, ers...)
validateEngineResponses = append(validateEngineResponses, validateErs)
}
}
resultsMap := buildPolicyResults(validateEngineResponses)
resultErr := printTestResult(resultsMap, values.Results)
if resultErr != nil {
return sanitizederror.NewWithError("Unable to genrate result. Error:", resultErr)
os.Exit(1)
}
return
}
func printTestResult(resps map[string][]interface {}, testResults []TestResults) (error){
printer := tableprinter.New(os.Stdout)
table := []*Table{}
boldRed := color.New(color.FgRed).Add(color.Bold)
boldFgCyan := color.New(color.FgCyan).Add(color.Bold)
for i, v := range testResults {
res := new(Table)
res.ID = i+1
res.Resource = boldFgCyan.Sprintf(v.Resource) + " with " + boldFgCyan.Sprintf(v.Policy) + "/" + boldFgCyan.Sprintf(v.Rule)
n := resps[v.Rule]
data, _ := json.Marshal(n)
valuesBytes, err := yaml.ToJSON(data)
if err != nil {
return sanitizederror.NewWithError("failed to convert json", err)
}
var r []ReportResult
json.Unmarshal(valuesBytes, &r)
res.Result = boldRed.Sprintf("Fail")
if len(r) != 0 {
var resource TestResults
for _, testRes := range r {
if testRes.Resources[0].Name == v.Resource {
resource.Policy = testRes.Policy
resource.Rule = testRes.Rule
resource.Status = testRes.Status
resource.Resource = testRes.Resources[0].Name
if v == resource {
res.Result = "Pass"
}
}
}
}
table = append(table, res)
}
printer.BorderTop, printer.BorderBottom, printer.BorderLeft, printer.BorderRight = true, true, true, true
printer.CenterSeparator = "│"
printer.ColumnSeparator = "│"
printer.RowSeparator = "─"
printer.RowCharLimit = 300
printer.RowLengthTitle = func(rowsLength int) bool {
return rowsLength > 10
}
printer.HeaderBgColor = tablewriter.BgBlackColor
printer.HeaderFgColor = tablewriter.FgGreenColor
printer.Print(table)
return nil
}

38
pkg/kyverno/test/git.go Normal file
View file

@ -0,0 +1,38 @@
package test
import (
"os"
"path/filepath"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/storage/memory"
)
func clone(path string, fs billy.Filesystem) (*git.Repository, error) {
return git.Clone(memory.NewStorage(), fs, &git.CloneOptions{
URL: path,
Progress: os.Stdout,
})
}
func listYAMLs(fs billy.Filesystem, path string) ([]string, error) {
path = filepath.Clean(path)
fis, err := fs.ReadDir(path)
if err != nil {
return nil, err
}
yamls := make([]string, 0)
for _, fi := range fis {
name := filepath.Join(path, fi.Name())
if fi.IsDir() {
continue
}
ext := filepath.Ext(name)
if ext != ".yml" && ext != ".yaml" {
continue
}
yamls = append(yamls, name)
}
return yamls, nil
}