2020-03-06 03:00:18 +05:30
package apply
import (
"fmt"
2020-06-08 17:01:56 +05:30
"os"
2020-07-21 00:41:30 +05:30
"path/filepath"
2020-10-15 17:29:07 -07:00
"reflect"
2020-07-21 00:41:30 +05:30
"strings"
2020-04-03 10:30:52 +05:30
"time"
2021-02-07 20:26:56 -08:00
"github.com/go-git/go-billy/v5/memfs"
2020-10-15 17:29:07 -07:00
v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
2020-11-13 13:40:28 +05:30
pkgCommon "github.com/kyverno/kyverno/pkg/common"
2020-10-07 11:12:31 -07:00
client "github.com/kyverno/kyverno/pkg/dclient"
2020-11-10 10:49:29 +05:30
"github.com/kyverno/kyverno/pkg/engine/response"
2020-10-07 11:12:31 -07:00
"github.com/kyverno/kyverno/pkg/kyverno/common"
2020-12-10 16:07:56 +05:30
sanitizederror "github.com/kyverno/kyverno/pkg/kyverno/sanitizedError"
2021-04-29 22:39:44 +05:30
"github.com/kyverno/kyverno/pkg/kyverno/store"
2020-10-15 17:29:07 -07:00
"github.com/kyverno/kyverno/pkg/openapi"
2020-10-07 11:12:31 -07:00
policy2 "github.com/kyverno/kyverno/pkg/policy"
2020-03-06 03:00:18 +05:30
"github.com/spf13/cobra"
2020-10-15 17:29:07 -07:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2020-03-06 03:00:18 +05:30
"k8s.io/cli-runtime/pkg/genericclioptions"
2020-03-20 11:43:21 -07:00
log "sigs.k8s.io/controller-runtime/pkg/log"
2020-11-10 10:49:29 +05:30
yaml1 "sigs.k8s.io/yaml"
2020-03-06 03:00:18 +05:30
)
2020-08-18 21:03:00 -07:00
type resultCounts struct {
2020-09-01 09:11:20 -07:00
pass int
fail int
warn int
2020-08-18 21:03:00 -07:00
error int
2020-09-01 09:11:20 -07:00
skip int
2020-08-18 21:03:00 -07:00
}
2020-10-30 16:38:19 +05:30
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" `
}
2020-11-19 15:03:15 +05:30
type SkippedPolicy struct {
2020-11-20 12:27:02 +05:30
Name string ` json:"name" `
2020-11-20 12:40:45 +05:30
Rules [ ] v1 . Rule ` json:"rules" `
2020-11-20 12:27:02 +05:30
Variable string ` json:"variable" `
2020-11-19 15:03:15 +05:30
}
2020-12-07 11:26:04 -08:00
var applyHelp = `
2020-11-20 12:27:02 +05:30
To apply on a resource :
kyverno apply / path / to / policy . yaml / path / to / folderOfPolicies -- resource = / path / to / resource1 -- resource = / path / to / resource2
To apply on a cluster :
kyverno apply / path / to / policy . yaml / path / to / folderOfPolicies -- cluster
To apply policy with variables :
1. To apply single policy with variable on single resource use flag "set" .
Example :
kyverno apply / path / to / policy . yaml -- resource / path / to / resource . yaml -- set < variable1 >= < value1 > , < variable2 >= < value2 >
2. To apply multiple policy with variable on multiple resource use flag "values_file" .
Example :
kyverno apply / path / to / policy1 . yaml / path / to / policy2 . yaml -- resource / path / to / resource1 . yaml -- resource / path / to / resource2 . yaml - f / path / to / value . yaml
Format of value . yaml :
policies :
- name : < policy1 name >
resources :
- name : < resource1 name >
values :
< variable1 in policy1 > : < value >
< variable2 in policy1 > : < value >
- name : < resource2 name >
values :
< variable1 in policy1 > : < value >
< variable2 in policy1 > : < value >
- name : < policy2 name >
resources :
- name : < resource1 name >
values :
< variable1 in policy2 > : < value >
< variable2 in policy2 > : < value >
- name : < resource2 name >
values :
< variable1 in policy2 > : < value >
< variable2 in policy2 > : < value >
2021-03-10 02:15:45 +05:30
namespaceSelector :
- name : < namespace1 name >
labels :
< label key > : < label value >
- name : < namespace2 name >
labels :
< label key > : < label value >
2020-11-20 12:27:02 +05:30
2020-12-07 11:26:04 -08:00
More info : https : //kyverno.io/docs/kyverno-cli/
`
func Command ( ) * cobra . Command {
var cmd * cobra . Command
var resourcePaths [ ] string
2021-03-26 23:33:45 +05:30
var cluster , policyReport , stdin bool
2020-12-07 11:26:04 -08:00
var mutateLogPath , variablesString , valuesFile , namespace string
cmd = & cobra . Command {
Use : "apply" ,
Short : "applies policies on resources" ,
Example : applyHelp ,
2020-03-06 03:00:18 +05:30
RunE : func ( cmd * cobra . Command , policyPaths [ ] string ) ( err error ) {
defer func ( ) {
if err != nil {
2020-12-07 11:26:04 -08:00
if ! sanitizederror . IsErrorSanitized ( err ) {
2020-03-20 11:43:21 -07:00
log . Log . Error ( err , "failed to sanitize" )
2020-08-05 23:53:27 +05:30
err = fmt . Errorf ( "internal error" )
2020-03-06 03:00:18 +05:30
}
}
} ( )
2021-03-26 23:33:45 +05:30
validateEngineResponses , rc , resources , skippedPolicies , err := applyCommandHelper ( resourcePaths , cluster , policyReport , mutateLogPath , variablesString , valuesFile , namespace , policyPaths , stdin )
2020-10-30 16:38:19 +05:30
if err != nil {
return err
2020-08-11 22:59:50 +05:30
}
2021-03-26 23:33:45 +05:30
printReportOrViolation ( policyReport , validateEngineResponses , rc , resourcePaths , len ( resources ) , skippedPolicies , stdin )
2020-12-20 01:21:31 +05:30
return nil
} ,
}
2020-10-16 19:56:32 +05:30
2020-12-20 01:21:31 +05:30
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" )
2021-03-26 23:33:45 +05:30
cmd . Flags ( ) . BoolVarP ( & stdin , "stdin" , "i" , false , "Optional mutate policy parameter to pipe directly through to kubectl" )
2020-12-20 01:21:31 +05:30
return cmd
}
2020-08-05 23:53:27 +05:30
2020-12-23 15:10:07 -08:00
func applyCommandHelper ( resourcePaths [ ] string , cluster bool , policyReport bool , mutateLogPath string ,
2021-03-26 23:33:45 +05:30
variablesString string , valuesFile string , namespace string , policyPaths [ ] string , stdin bool ) ( validateEngineResponses [ ] * response . EngineResponse , rc * resultCounts , resources [ ] * unstructured . Unstructured , skippedPolicies [ ] SkippedPolicy , err error ) {
2020-12-23 15:10:07 -08:00
2021-04-29 22:39:44 +05:30
store . SetMock ( true )
2020-12-20 01:21:31 +05:30
kubernetesConfig := genericclioptions . NewConfigFlags ( true )
2021-02-02 18:43:19 +05:30
fs := memfs . New ( )
2020-10-16 19:56:32 +05:30
2020-12-20 01:21:31 +05:30
if valuesFile != "" && variablesString != "" {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( "pass the values either using set flag or values_file flag" , err )
}
2020-12-07 11:26:04 -08:00
2021-03-10 02:15:45 +05:30
variables , valuesMap , namespaceSelectorMap , err := common . GetVariable ( variablesString , valuesFile , fs , false , "" )
2020-12-20 01:21:31 +05:30
if err != nil {
if ! sanitizederror . IsErrorSanitized ( err ) {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( "failed to decode yaml" , err )
}
return validateEngineResponses , rc , resources , skippedPolicies , err
}
2020-10-16 19:56:32 +05:30
2020-12-20 01:21:31 +05:30
openAPIController , err := openapi . NewOpenAPIController ( )
if err != nil {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( "failed to initialize openAPIController" , err )
}
2020-07-21 00:41:30 +05:30
2020-12-20 01:21:31 +05:30
var dClient * client . Client
if cluster {
restConfig , err := kubernetesConfig . ToRESTConfig ( )
if err != nil {
return validateEngineResponses , rc , resources , skippedPolicies , err
}
dClient , err = client . NewClient ( restConfig , 15 * time . Minute , make ( chan struct { } ) , log . Log )
if err != nil {
return validateEngineResponses , rc , resources , skippedPolicies , err
}
}
2020-03-06 03:00:18 +05:30
2020-12-20 01:21:31 +05:30
if len ( policyPaths ) == 0 {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( fmt . Sprintf ( "require policy" ) , err )
}
2020-11-11 17:10:38 +05:30
2020-12-20 01:21:31 +05:30
if ( len ( policyPaths ) > 0 && policyPaths [ 0 ] == "-" ) && len ( resourcePaths ) > 0 && resourcePaths [ 0 ] == "-" {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( "a stdin pipe can be used for either policies or resources, not both" , err )
}
2020-11-11 11:57:23 +05:30
2021-02-18 01:00:41 +05:30
policies , err := common . GetPoliciesFromPaths ( fs , policyPaths , false , "" )
2020-12-20 01:21:31 +05:30
if err != nil {
fmt . Printf ( "Error: failed to load policies\nCause: %s\n" , err )
os . Exit ( 1 )
}
2020-07-10 14:56:07 +05:30
2020-12-20 01:21:31 +05:30
if len ( resourcePaths ) == 0 && ! cluster {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( fmt . Sprintf ( "resource file(s) or cluster required" ) , err )
}
2020-08-18 21:03:00 -07:00
2020-12-20 01:21:31 +05:30
mutateLogPathIsDir , err := checkMutateLogPath ( mutateLogPath )
if err != nil {
if ! sanitizederror . IsErrorSanitized ( err ) {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( "failed to create file/folder" , err )
}
return validateEngineResponses , rc , resources , skippedPolicies , err
}
2020-08-18 21:03:00 -07:00
2021-03-12 06:00:37 +05:30
// empty the previous contents of the file just in case if the file already existed before with some content(so as to perform overwrites)
// the truncation of files for the case when mutateLogPath is dir, is handled under pkg/kyverno/apply/common.go
2021-03-17 01:27:31 +05:30
if ! mutateLogPathIsDir && mutateLogPath != "" {
2021-03-12 06:00:37 +05:30
_ , err := os . OpenFile ( mutateLogPath , os . O_TRUNC | os . O_WRONLY , 0644 )
if err != nil {
if ! sanitizederror . IsErrorSanitized ( err ) {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( "failed to truncate the existing file at " + mutateLogPath , err )
}
return validateEngineResponses , rc , resources , skippedPolicies , err
}
}
2021-02-02 18:43:19 +05:30
mutatedPolicies , err := common . MutatePolices ( policies )
2020-12-20 01:21:31 +05:30
if err != nil {
if ! sanitizederror . IsErrorSanitized ( err ) {
return validateEngineResponses , rc , resources , skippedPolicies , sanitizederror . NewWithError ( "failed to mutate policy" , err )
}
}
2020-11-19 15:03:15 +05:30
2021-02-18 01:00:41 +05:30
resources , err = common . GetResourceAccordingToResourcePath ( fs , resourcePaths , cluster , mutatedPolicies , dClient , namespace , policyReport , false , "" )
2020-12-20 01:21:31 +05:30
if err != nil {
fmt . Printf ( "Error: failed to load resources\nCause: %s\n" , err )
os . Exit ( 1 )
}
2020-03-06 03:00:18 +05:30
2020-12-20 01:21:31 +05:30
msgPolicies := "1 policy"
if len ( mutatedPolicies ) > 1 {
msgPolicies = fmt . Sprintf ( "%d policies" , len ( policies ) )
}
2020-11-19 15:03:15 +05:30
2020-12-20 01:21:31 +05:30
msgResources := "1 resource"
if len ( resources ) > 1 {
msgResources = fmt . Sprintf ( "%d resources" , len ( resources ) )
}
2020-08-22 01:21:29 +05:30
2020-12-20 01:21:31 +05:30
if len ( mutatedPolicies ) > 0 && len ( resources ) > 0 {
2021-03-26 23:33:45 +05:30
if ! stdin {
fmt . Printf ( "\napplying %s to %s... \n" , msgPolicies , msgResources )
}
2020-12-20 01:21:31 +05:30
}
2020-08-11 22:59:50 +05:30
2020-12-20 01:21:31 +05:30
rc = & resultCounts { }
2020-12-23 15:10:07 -08:00
engineResponses := make ( [ ] * response . EngineResponse , 0 )
validateEngineResponses = make ( [ ] * response . EngineResponse , 0 )
2020-12-20 01:21:31 +05:30
skippedPolicies = make ( [ ] SkippedPolicy , 0 )
2020-08-11 22:59:50 +05:30
2020-12-20 01:21:31 +05:30
for _ , policy := range mutatedPolicies {
2021-01-06 16:32:02 -08:00
err := policy2 . Validate ( policy , nil , true , openAPIController )
2020-12-20 01:21:31 +05:30
if err != nil {
rc . skip += len ( resources )
log . Log . V ( 3 ) . Info ( fmt . Sprintf ( "skipping policy %v as it is not valid" , policy . Name ) , "error" , err )
continue
}
2020-08-12 13:39:06 +05:30
2020-12-20 01:21:31 +05:30
matches := common . PolicyHasVariables ( * policy )
2021-02-02 18:43:19 +05:30
variable := common . RemoveDuplicateVariables ( matches )
2020-12-20 01:21:31 +05:30
if len ( matches ) > 0 && variablesString == "" && valuesFile == "" {
rc . skip ++
skipPolicy := SkippedPolicy {
Name : policy . GetName ( ) ,
Rules : policy . Spec . Rules ,
Variable : variable ,
2020-03-06 03:00:18 +05:30
}
2020-12-20 01:21:31 +05:30
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
}
2020-03-06 03:00:18 +05:30
2020-12-20 01:21:31 +05:30
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
}
2020-08-18 21:03:00 -07:00
2020-12-20 01:21:31 +05:30
for k , v := range variables {
thisPolicyResourceValues [ k ] = v
}
2021-04-29 22:39:44 +05:30
if len ( common . PolicyHasVariables ( * policy ) ) > 0 && len ( thisPolicyResourceValues ) == 0 && len ( store . GetContext ( ) . Policies ) == 0 {
2020-12-20 01:21:31 +05:30
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 )
}
2021-03-26 23:33:45 +05:30
ers , validateErs , responseError , rcErs , err := common . ApplyPolicyOnResource ( policy , resource , mutateLogPath , mutateLogPathIsDir , thisPolicyResourceValues , policyReport , namespaceSelectorMap , stdin )
2020-12-20 01:21:31 +05:30
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 )
}
2021-02-02 18:43:19 +05:30
if responseError == true {
rc . fail ++
} else {
rc . pass ++
}
if rcErs == true {
rc . error ++
}
2020-12-20 01:21:31 +05:30
engineResponses = append ( engineResponses , ers ... )
validateEngineResponses = append ( validateEngineResponses , validateErs )
}
2020-03-06 03:00:18 +05:30
}
2020-12-20 01:21:31 +05:30
return validateEngineResponses , rc , resources , skippedPolicies , nil
2020-03-06 03:00:18 +05:30
}
2020-10-30 16:38:19 +05:30
// checkMutateLogPath - checking path for printing mutated resource (-o flag)
2020-11-03 01:25:32 +05:30
func checkMutateLogPath ( mutateLogPath string ) ( mutateLogPathIsDir bool , err error ) {
2020-10-30 16:38:19 +05:30
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 {
2020-12-07 11:26:04 -08:00
if ! sanitizederror . IsErrorSanitized ( err ) {
return mutateLogPathIsDir , sanitizederror . NewWithError ( "failed to create file/folder." , err )
2020-10-30 16:38:19 +05:30
}
return mutateLogPathIsDir , err
}
}
return mutateLogPathIsDir , err
}
// printReportOrViolation - printing policy report/violations
2021-03-26 23:33:45 +05:30
func printReportOrViolation ( policyReport bool , validateEngineResponses [ ] * response . EngineResponse , rc * resultCounts , resourcePaths [ ] string , resourcesLen int , skippedPolicies [ ] SkippedPolicy , stdin bool ) {
2020-10-30 16:38:19 +05:30
if policyReport {
2020-11-13 00:48:40 +05:30
os . Setenv ( "POLICY-TYPE" , pkgCommon . PolicyReport )
2020-11-19 15:03:15 +05:30
resps := buildPolicyReports ( validateEngineResponses , skippedPolicies )
2020-11-19 11:52:31 +05:30
if len ( resps ) > 0 || resourcesLen == 0 {
2020-11-03 02:01:20 +05:30
fmt . Println ( "----------------------------------------------------------------------\nPOLICY REPORT:\n----------------------------------------------------------------------" )
2020-11-01 22:32:12 +05:30
report , _ := generateCLIraw ( resps )
yamlReport , _ := yaml1 . Marshal ( report )
fmt . Println ( string ( yamlReport ) )
2020-10-30 16:38:19 +05:30
} else {
2020-11-13 13:40:28 +05:30
fmt . Println ( "----------------------------------------------------------------------\nPOLICY REPORT: skip generating policy report (no validate policy found/resource skipped)" )
2020-10-30 16:38:19 +05:30
}
} else {
rcCount := rc . pass + rc . fail + rc . warn + rc . error + rc . skip
if rcCount < len ( resourcePaths ) {
rc . skip += len ( resourcePaths ) - rcCount
}
2021-03-26 23:33:45 +05:30
if ! stdin {
fmt . Printf ( "\npass: %d, fail: %d, warn: %d, error: %d, skip: %d \n" ,
rc . pass , rc . fail , rc . warn , rc . error , rc . skip )
}
2020-10-30 16:38:19 +05:30
if rc . fail > 0 || rc . error > 0 {
os . Exit ( 1 )
}
}
}
2020-08-18 21:03:00 -07:00
// createFileOrFolder - creating file or folder according to path provided
2020-08-22 01:21:29 +05:30
func createFileOrFolder ( mutateLogPath string , mutateLogPathIsDir bool ) error {
mutateLogPath = filepath . Clean ( mutateLogPath )
_ , err := os . Stat ( mutateLogPath )
2020-07-21 00:41:30 +05:30
if err != nil {
if os . IsNotExist ( err ) {
2020-08-22 01:21:29 +05:30
if ! mutateLogPathIsDir {
2020-11-17 12:01:01 -08:00
// check the folder existence, then create the file
2020-07-21 10:33:38 +05:30
var folderPath string
2020-08-22 01:21:29 +05:30
s := strings . Split ( mutateLogPath , "/" )
2020-07-21 10:33:38 +05:30
if len ( s ) > 1 {
2020-08-22 01:21:29 +05:30
folderPath = mutateLogPath [ : len ( mutateLogPath ) - len ( s [ len ( s ) - 1 ] ) - 1 ]
2020-07-21 10:33:38 +05:30
_ , err := os . Stat ( folderPath )
if os . IsNotExist ( err ) {
errDir := os . MkdirAll ( folderPath , 0755 )
if errDir != nil {
2020-12-07 11:26:04 -08:00
return sanitizederror . NewWithError ( fmt . Sprintf ( "failed to create directory" ) , err )
2020-07-21 10:33:38 +05:30
}
2020-07-21 00:41:30 +05:30
}
}
2020-08-22 01:21:29 +05:30
file , err := os . OpenFile ( mutateLogPath , os . O_RDONLY | os . O_CREATE , 0644 )
2020-07-21 00:41:30 +05:30
if err != nil {
2020-12-07 11:26:04 -08:00
return sanitizederror . NewWithError ( fmt . Sprintf ( "failed to create file" ) , err )
2020-07-21 00:41:30 +05:30
}
err = file . Close ( )
if err != nil {
2020-12-07 11:26:04 -08:00
return sanitizederror . NewWithError ( fmt . Sprintf ( "failed to close file" ) , err )
2020-07-21 00:41:30 +05:30
}
} else {
2020-08-22 01:21:29 +05:30
errDir := os . MkdirAll ( mutateLogPath , 0755 )
2020-07-21 00:41:30 +05:30
if errDir != nil {
2020-12-07 11:26:04 -08:00
return sanitizederror . NewWithError ( fmt . Sprintf ( "failed to create directory" ) , err )
2020-07-21 00:41:30 +05:30
}
}
} else {
2020-12-07 11:26:04 -08:00
return sanitizederror . NewWithError ( fmt . Sprintf ( "failed to describe file" ) , err )
2020-07-21 00:41:30 +05:30
}
}
return nil
}