mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-10 01:46:55 +00:00
520 lines
17 KiB
Go
520 lines
17 KiB
Go
package apply
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
|
|
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"
|
|
"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"
|
|
)
|
|
|
|
type resultCounts struct {
|
|
pass int
|
|
fail int
|
|
warn int
|
|
error int
|
|
skip int
|
|
}
|
|
|
|
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 Command() *cobra.Command {
|
|
var cmd *cobra.Command
|
|
var resourcePaths []string
|
|
var cluster, policyReport bool
|
|
var mutateLogPath, variablesString, valuesFile, namespace string
|
|
|
|
kubernetesConfig := genericclioptions.NewConfigFlags(true)
|
|
|
|
cmd = &cobra.Command{
|
|
Use: "apply",
|
|
Short: "applies policies on resources",
|
|
Example: fmt.Sprintf("To apply on a resource:\nkyverno apply /path/to/policy.yaml /path/to/folderOfPolicies --resource=/path/to/resource1 --resource=/path/to/resource2\n\nTo apply on a cluster\nkyverno apply /path/to/policy.yaml /path/to/folderOfPolicies --cluster"),
|
|
RunE: func(cmd *cobra.Command, policyPaths []string) (err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
if !sanitizedError.IsErrorSanitized(err) {
|
|
log.Log.Error(err, "failed to sanitize")
|
|
err = fmt.Errorf("internal error")
|
|
}
|
|
}
|
|
}()
|
|
|
|
if valuesFile != "" && variablesString != "" {
|
|
return sanitizedError.NewWithError("pass the values either using set flag or values_file flag", err)
|
|
}
|
|
|
|
variables, valuesMap, err := getVariable(variablesString, valuesFile)
|
|
if err != nil {
|
|
if !sanitizedError.IsErrorSanitized(err) {
|
|
return sanitizedError.NewWithError("failed to decode yaml", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
openAPIController, err := openapi.NewOpenAPIController()
|
|
if err != nil {
|
|
return sanitizedError.NewWithError("failed to initialize openAPIController", err)
|
|
}
|
|
|
|
var dClient *client.Client
|
|
if cluster {
|
|
restConfig, err := kubernetesConfig.ToRESTConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dClient, err = client.NewClient(restConfig, 5*time.Minute, make(chan struct{}), log.Log)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(policyPaths) == 0 {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("require policy"), err)
|
|
}
|
|
|
|
policies, err := common.ValidateAndGetPolicies(policyPaths)
|
|
if err != nil {
|
|
if !sanitizedError.IsErrorSanitized(err) {
|
|
return sanitizedError.NewWithError("failed to mutate policies.", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if len(resourcePaths) == 0 && !cluster {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("resource file(s) or cluster required"), err)
|
|
}
|
|
|
|
mutateLogPathIsDir, err := checkMutateLogPath(mutateLogPath)
|
|
if err != nil {
|
|
if !sanitizedError.IsErrorSanitized(err) {
|
|
return sanitizedError.NewWithError("failed to create file/folder", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
resources, err := getResourceAccordingToResourcePath(resourcePaths, cluster, policies, dClient, namespace)
|
|
if err != nil {
|
|
if !sanitizedError.IsErrorSanitized(err) {
|
|
return sanitizedError.NewWithError("failed to load resources", err)
|
|
}
|
|
}
|
|
|
|
mutatedPolicies, err := mutatePolices(policies)
|
|
|
|
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)
|
|
}
|
|
|
|
rc := &resultCounts{}
|
|
engineResponses := make([]response.EngineResponse, 0)
|
|
for _, policy := range mutatedPolicies {
|
|
err := policy2.Validate(utils.MarshalPolicy(*policy), nil, true, openAPIController)
|
|
if err != nil {
|
|
rc.skip += len(resources)
|
|
fmt.Printf("\nskipping policy %v as it is not valid: %v\n", policy.Name, err)
|
|
continue
|
|
}
|
|
|
|
if common.PolicyHasVariables(*policy) && variablesString == "" && valuesFile == "" {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("policy %s have variables. pass the values for the variables using set/values_file flag", policy.Name), err)
|
|
}
|
|
|
|
for _, resource := range resources {
|
|
// get values from file for this policy resource combination
|
|
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
|
|
}
|
|
|
|
for k, v := range variables {
|
|
thisPolicyResourceValues[k] = v
|
|
}
|
|
|
|
if common.PolicyHasVariables(*policy) && 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, err := applyPolicyOnResource(policy, resource, mutateLogPath, mutateLogPathIsDir, thisPolicyResourceValues, rc, policyReport)
|
|
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...)
|
|
}
|
|
}
|
|
|
|
printReportOrViolation(policyReport, engineResponses, rc, resourcePaths)
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringArrayVarP(&resourcePaths, "resource", "r", []string{}, "Path to resource files")
|
|
cmd.Flags().BoolVarP(&cluster, "cluster", "c", false, "Checks if policies should be applied to cluster in the current context")
|
|
cmd.Flags().StringVarP(&mutateLogPath, "output", "o", "", "Prints the mutated resources in provided file/directory")
|
|
cmd.Flags().StringVarP(&variablesString, "set", "s", "", "Variables that are required")
|
|
cmd.Flags().StringVarP(&valuesFile, "values-file", "f", "", "File containing values for policy variables")
|
|
cmd.Flags().BoolVarP(&policyReport, "policy-report", "", false, "Generates policy report when passed (default policyviolation r")
|
|
cmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Optional Policy parameter passed with cluster flag")
|
|
return cmd
|
|
}
|
|
|
|
// 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 != "" {
|
|
spath := strings.Split(mutateLogPath, "/")
|
|
sfileName := strings.Split(spath[len(spath)-1], ".")
|
|
if sfileName[len(sfileName)-1] == "yml" || sfileName[len(sfileName)-1] == "yaml" {
|
|
mutateLogPathIsDir = false
|
|
} else {
|
|
mutateLogPathIsDir = true
|
|
}
|
|
|
|
err := createFileOrFolder(mutateLogPath, mutateLogPathIsDir)
|
|
if err != nil {
|
|
if !sanitizedError.IsErrorSanitized(err) {
|
|
return mutateLogPathIsDir, sanitizedError.NewWithError("failed to create file/folder.", err)
|
|
}
|
|
return mutateLogPathIsDir, err
|
|
}
|
|
}
|
|
return mutateLogPathIsDir, err
|
|
}
|
|
|
|
// getResourceAccordingToResourcePath - get resources according to the resource path
|
|
func getResourceAccordingToResourcePath(resourcePaths []string, cluster bool, policies []*v1.ClusterPolicy, dClient *client.Client, namespace string) (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 resources, 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)
|
|
if err != nil {
|
|
return resources, sanitizedError.NewWithError("failed to load resources", err)
|
|
}
|
|
}
|
|
return resources, err
|
|
}
|
|
|
|
// printReportOrViolation - printing policy report/violations
|
|
func printReportOrViolation(policyReport bool, engineResponses []response.EngineResponse, rc *resultCounts, resourcePaths []string) {
|
|
if policyReport {
|
|
resps := buildPolicyReports(engineResponses)
|
|
if len(resps) > 0 {
|
|
fmt.Println("----------------------------------------------------------------------\nPOLICY REPORT:\n----------------------------------------------------------------------")
|
|
report, _ := generateCLIraw(resps)
|
|
yamlReport, _ := yaml1.Marshal(report)
|
|
fmt.Println(string(yamlReport))
|
|
} else {
|
|
fmt.Println("----------------------------------------------------------------------\nPOLICY REPORT: not generated (no validation failure/resource skipped)")
|
|
}
|
|
|
|
} else {
|
|
rcCount := rc.pass + rc.fail + rc.warn + rc.error + rc.skip
|
|
if rcCount < len(resourcePaths) {
|
|
rc.skip += len(resourcePaths) - rcCount
|
|
}
|
|
|
|
fmt.Printf("\npass: %d, fail: %d, warn: %d, error: %d, skip: %d \n",
|
|
rc.pass, rc.fail, rc.warn, rc.error, rc.skip)
|
|
|
|
if rc.fail > 0 || rc.error > 0 {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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, 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, Context: 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, sanitizedError.NewWithError("failed to print mutated result", err)
|
|
}
|
|
fmt.Printf("\n\nMutation:\nMutation has been applied succesfully. 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
validateResponse := engine.Validate(engine.PolicyContext{Policy: *policy, NewResource: mutateResponse.PatchedResource, Context: ctx})
|
|
engineResponses = append(engineResponses, validateResponse)
|
|
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
|
|
}
|
|
}
|
|
|
|
if responseError == true {
|
|
rc.fail++
|
|
} else {
|
|
rc.pass++
|
|
}
|
|
|
|
return engineResponses, 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)
|
|
_, err := os.Stat(mutateLogPath)
|
|
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
if !mutateLogPathIsDir {
|
|
// check the folder existance, then create the file
|
|
var folderPath string
|
|
s := strings.Split(mutateLogPath, "/")
|
|
|
|
if len(s) > 1 {
|
|
folderPath = mutateLogPath[:len(mutateLogPath)-len(s[len(s)-1])-1]
|
|
_, err := os.Stat(folderPath)
|
|
if os.IsNotExist(err) {
|
|
errDir := os.MkdirAll(folderPath, 0755)
|
|
if errDir != nil {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("failed to create directory"), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
file, err := os.OpenFile(mutateLogPath, os.O_RDONLY|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("failed to create file"), err)
|
|
}
|
|
|
|
err = file.Close()
|
|
if err != nil {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("failed to close file"), err)
|
|
}
|
|
|
|
} else {
|
|
errDir := os.MkdirAll(mutateLogPath, 0755)
|
|
if errDir != nil {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("failed to create directory"), err)
|
|
}
|
|
}
|
|
|
|
} else {
|
|
return sanitizedError.NewWithError(fmt.Sprintf("failed to describe file"), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|