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

feat: Extend CLI to cover generate policies (#3456)

- Change in namespace for test-generate example
- Change cloneResource to cloneSourceResource
- Add support for namespaced Policy and fix log messages
- Add test-generate in Makefile and an example of namespaced Policy
- Fix namespaced policy issue and add comments
- Refactor according to new generate controller
- Add json tag to GeneratedResource field of RuleResponse struct

Signed-off-by: Shubham Nazare <shubham4443@gmail.com>

Co-authored-by: Prateek Pandey <prateek.pandey@nirmata.com>
Co-authored-by: Vyankatesh Kudtarkar <vyankateshkd@gmail.com>
This commit is contained in:
Shubham Nazare 2022-05-25 19:56:22 +05:30 committed by GitHub
parent fbbe57f5e1
commit 165c5d9fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 559 additions and 44 deletions

View file

@ -267,7 +267,7 @@ test-clean: ## Clean tests cache
go clean -testcache ./... go clean -testcache ./...
.PHONY: test-cli .PHONY: test-cli
test-cli: test-cli-policies test-cli-local test-cli-local-mutate test-cli-test-case-selector-flag test-cli-registry test-cli: test-cli-policies test-cli-local test-cli-local-mutate test-cli-local-generate test-cli-test-case-selector-flag test-cli-registry
.PHONY: test-cli-policies .PHONY: test-cli-policies
test-cli-policies: cli test-cli-policies: cli
@ -281,6 +281,10 @@ test-cli-local: cli
test-cli-local-mutate: cli test-cli-local-mutate: cli
cmd/cli/kubectl-kyverno/kyverno test ./test/cli/test-mutate cmd/cli/kubectl-kyverno/kyverno test ./test/cli/test-mutate
.PHONY: test-cli-local-generate
test-cli-local-generate: cli
cmd/cli/kubectl-kyverno/kyverno test ./test/cli/test-generate
.PHONY: test-cli-test-case-selector-flag .PHONY: test-cli-test-case-selector-flag
test-cli-test-case-selector-flag: cli test-cli-test-case-selector-flag: cli
cmd/cli/kubectl-kyverno/kyverno test ./test/cli/test --test-case-selector "policy=disallow-latest-tag, rule=require-image-tag, resource=test-require-image-tag-pass" cmd/cli/kubectl-kyverno/kyverno test ./test/cli/test --test-case-selector "policy=disallow-latest-tag, rule=require-image-tag, resource=test-require-image-tag-pass"

View file

@ -313,7 +313,7 @@ func applyCommandHelper(resourcePaths []string, userInfoPath string, cluster boo
return rc, resources, skipInvalidPolicies, pvInfos, sanitizederror.NewWithError(fmt.Sprintf("policy `%s` have variables. pass the values for the variables for resource `%s` using set/values_file flag", policy.GetName(), resource.GetName()), err) return rc, resources, skipInvalidPolicies, pvInfos, sanitizederror.NewWithError(fmt.Sprintf("policy `%s` have variables. pass the values for the variables for resource `%s` using set/values_file flag", policy.GetName(), resource.GetName()), err)
} }
_, info, err := common.ApplyPolicyOnResource(policy, resource, mutateLogPath, mutateLogPathIsDir, thisPolicyResourceValues, userInfo, policyReport, namespaceSelectorMap, stdin, rc, true) _, info, err := common.ApplyPolicyOnResource(policy, resource, mutateLogPath, mutateLogPathIsDir, thisPolicyResourceValues, userInfo, policyReport, namespaceSelectorMap, stdin, rc, true, nil)
if err != nil { if err != nil {
return rc, resources, skipInvalidPolicies, pvInfos, sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.GetName(), resource.GetName()).Error(), err) return rc, resources, skipInvalidPolicies, pvInfos, sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.GetName(), resource.GetName()).Error(), err)
} }

View file

@ -244,15 +244,34 @@ type Test struct {
} }
type TestResults struct { type TestResults struct {
Policy string `json:"policy"` // Policy mentions the name of the policy.
Rule string `json:"rule"` Policy string `json:"policy"`
Result policyreportv1alpha2.PolicyResult `json:"result"` // Rule mentions the name of the rule in the policy.
Status policyreportv1alpha2.PolicyResult `json:"status"` Rule string `json:"rule"`
Resource string `json:"resource"` // Result mentions the result that the user is expecting.
Kind string `json:"kind"` // Possible values are pass, fail and skip.
Namespace string `json:"namespace"` Result policyreportv1alpha2.PolicyResult `json:"result"`
PatchedResource string `json:"patchedResource"` // Status mentions the status that the user is expecting.
AutoGeneratedRule string `json:"auto_generated_rule"` // Possible values are pass, fail and skip.
Status policyreportv1alpha2.PolicyResult `json:"status"`
// Resource mentions the name of the resource on which the policy is to be applied.
Resource string `json:"resource"`
// Kind mentions the kind of the resource on which the policy is to be applied.
Kind string `json:"kind"`
// Namespace mentions the namespace of the policy which has namespace scope.
Namespace string `json:"namespace"`
// PatchedResource takes a resource configuration file in yaml format from
// the user to compare it against the Kyverno mutated resource configuration.
PatchedResource string `json:"patchedResource"`
// AutoGeneratedRule is internally set by the CLI command. It takes values either
// autogen or autogen-cronjob.
AutoGeneratedRule string `json:"auto_generated_rule"`
// GeneratedResource takes a resource configuration file in yaml format from
// the user to compare it against the Kyverno generated resource configuration.
GeneratedResource string `json:"generatedResource"`
// CloneSourceResource takes the resource configuration file in yaml format
// from the user which is meant to be cloned by the generate rule.
CloneSourceResource string `json:"cloneSourceResource"`
} }
type ReportResult struct { type ReportResult struct {
@ -553,6 +572,39 @@ func buildPolicyResults(engineResponses []*response.EngineResponse, testResults
results[resultsKey] = result results[resultsKey] = result
} }
} }
for _, rule := range resp.PolicyResponse.Rules {
if rule.Type != response.Generation || test.Rule != rule.Name {
continue
}
var resultsKey []string
var resultKey string
var result policyreportv1alpha2.PolicyReportResult
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name, resourceNamespace, resourceKind, resourceName)
for _, key := range resultsKey {
if val, ok := results[key]; ok {
result = val
resultKey = key
} else {
continue
}
if rule.Status == response.RuleStatusSkip {
result.Result = policyreportv1alpha2.StatusSkip
} else if rule.Status == response.RuleStatusError {
result.Result = policyreportv1alpha2.StatusError
} else {
var x string
result.Result = policyreportv1alpha2.StatusFail
x = getAndCompareResource(test.GeneratedResource, rule.GeneratedResource, isGit, policyResourcePath, fs, true)
if x == "pass" {
result.Result = policyreportv1alpha2.StatusPass
}
}
results[resultKey] = result
}
}
} }
for _, rule := range resp.PolicyResponse.Rules { for _, rule := range resp.PolicyResponse.Rules {
@ -580,7 +632,7 @@ func buildPolicyResults(engineResponses []*response.EngineResponse, testResults
var x string var x string
for _, path := range patchedResourcePath { for _, path := range patchedResourcePath {
result.Result = policyreportv1alpha2.StatusFail result.Result = policyreportv1alpha2.StatusFail
x = getAndComparePatchedResource(path, resp.PatchedResource, isGit, policyResourcePath, fs) x = getAndCompareResource(path, resp.PatchedResource, isGit, policyResourcePath, fs, false)
if x == "pass" { if x == "pass" {
result.Result = policyreportv1alpha2.StatusPass result.Result = policyreportv1alpha2.StatusPass
break break
@ -663,17 +715,22 @@ func getUserDefinedPolicyNameAndNamespace(policyName string) (string, string) {
return "", policyName return "", policyName
} }
// getAndComparePatchedResource --> Get the patchedResource from the path provided by user // getAndCompareResource --> Get the patchedResource or generatedResource from the path provided by user
// And compare this patchedResource with engine generated patcheResource. // And compare this resource with engine generated resource.
func getAndComparePatchedResource(path string, enginePatchedResource unstructured.Unstructured, isGit bool, policyResourcePath string, fs billy.Filesystem) string { func getAndCompareResource(path string, engineResource unstructured.Unstructured, isGit bool, policyResourcePath string, fs billy.Filesystem, isGenerate bool) string {
var status string var status string
patchedResources, err := common.GetPatchedResourceFromPath(fs, path, isGit, policyResourcePath) resourceType := "patchedResource"
if isGenerate {
resourceType = "generatedResource"
}
userResource, err := common.GetResourceFromPath(fs, path, isGit, policyResourcePath, resourceType)
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
matched, err := generate.ValidateResourceWithPattern(log.Log, enginePatchedResource.UnstructuredContent(), patchedResources.UnstructuredContent()) matched, err := generate.ValidateResourceWithPattern(log.Log, engineResource.UnstructuredContent(), userResource.UnstructuredContent())
if err != nil { if err != nil {
log.Log.V(3).Info("patched resource mismatch", "error", err.Error()) log.Log.V(3).Info(resourceType+" mismatch", "error", err.Error())
status = "fail" status = "fail"
} }
@ -763,8 +820,16 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, isGit bool,
for i, result := range values.Results { for i, result := range values.Results {
arrPatchedResource := []string{result.PatchedResource} arrPatchedResource := []string{result.PatchedResource}
arrGeneratedResource := []string{result.GeneratedResource}
arrCloneSourceResource := []string{result.CloneSourceResource}
patchedResourceFullPath := getFullPath(arrPatchedResource, policyResourcePath, isGit) patchedResourceFullPath := getFullPath(arrPatchedResource, policyResourcePath, isGit)
generatedResourceFullPath := getFullPath(arrGeneratedResource, policyResourcePath, isGit)
CloneSourceResourceFullPath := getFullPath(arrCloneSourceResource, policyResourcePath, isGit)
values.Results[i].PatchedResource = patchedResourceFullPath[0] values.Results[i].PatchedResource = patchedResourceFullPath[0]
values.Results[i].GeneratedResource = generatedResourceFullPath[0]
values.Results[i].CloneSourceResource = CloneSourceResourceFullPath[0]
} }
policies, err := common.GetPoliciesFromPaths(fs, policyFullPath, isGit, policyResourcePath) policies, err := common.GetPoliciesFromPaths(fs, policyFullPath, isGit, policyResourcePath)
@ -783,6 +848,7 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, isGit bool,
} }
} }
var ruleToCloneSourceResource = map[string]string{}
for _, p := range filteredPolicies { for _, p := range filteredPolicies {
filteredRules := []kyvernov1.Rule{} filteredRules := []kyvernov1.Rule{}
@ -790,6 +856,23 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, isGit bool,
for _, res := range values.Results { for _, res := range values.Results {
if rule.Name == res.Rule { if rule.Name == res.Rule {
filteredRules = append(filteredRules, rule) filteredRules = append(filteredRules, rule)
if rule.HasGenerate() {
ruleUnstr, err := generate.GetUnstrRule(rule.Generation.DeepCopy())
if err != nil {
fmt.Printf("Error: failed to get unstructured rule\nCause: %s\n", err)
break
}
genClone, _, err := unstructured.NestedMap(ruleUnstr.Object, "clone")
if err != nil {
fmt.Printf("Error: failed to read data\nCause: %s\n", err)
break
}
if len(genClone) != 0 {
ruleToCloneSourceResource[rule.Name] = res.CloneSourceResource
}
}
break break
} }
} }
@ -868,7 +951,7 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, isGit bool,
return sanitizederror.NewWithError(fmt.Sprintf("policy `%s` have variables. pass the values for the variables for resource `%s` using set/values_file flag", policy.GetName(), resource.GetName()), err) return sanitizederror.NewWithError(fmt.Sprintf("policy `%s` have variables. pass the values for the variables for resource `%s` using set/values_file flag", policy.GetName(), resource.GetName()), err)
} }
ers, info, err := common.ApplyPolicyOnResource(policy, resource, "", false, thisPolicyResourceValues, userInfo, true, namespaceSelectorMap, false, &resultCounts, false) ers, info, err := common.ApplyPolicyOnResource(policy, resource, "", false, thisPolicyResourceValues, userInfo, true, namespaceSelectorMap, false, &resultCounts, false, ruleToCloneSourceResource)
if err != nil { if err != nil {
return sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.GetName(), resource.GetName()).Error(), err) return sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.GetName(), resource.GetName()).Error(), err)
} }

View file

@ -21,6 +21,7 @@ import (
sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError" sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store"
"github.com/kyverno/kyverno/pkg/autogen" "github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/background/generate"
"github.com/kyverno/kyverno/pkg/dclient" "github.com/kyverno/kyverno/pkg/dclient"
"github.com/kyverno/kyverno/pkg/engine" "github.com/kyverno/kyverno/pkg/engine"
engineContext "github.com/kyverno/kyverno/pkg/engine/context" engineContext "github.com/kyverno/kyverno/pkg/engine/context"
@ -32,6 +33,7 @@ import (
"github.com/kyverno/kyverno/pkg/utils" "github.com/kyverno/kyverno/pkg/utils"
yamlv2 "gopkg.in/yaml.v2" yamlv2 "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"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"
) )
@ -397,7 +399,7 @@ func MutatePolicies(policies []kyvernov1.PolicyInterface) ([]kyvernov1.PolicyInt
func ApplyPolicyOnResource(policy kyvernov1.PolicyInterface, resource *unstructured.Unstructured, func ApplyPolicyOnResource(policy kyvernov1.PolicyInterface, resource *unstructured.Unstructured,
mutateLogPath string, mutateLogPathIsDir bool, variables map[string]interface{}, userInfo kyvernov1beta1.RequestInfo, policyReport bool, mutateLogPath string, mutateLogPathIsDir bool, variables map[string]interface{}, userInfo kyvernov1beta1.RequestInfo, policyReport bool,
namespaceSelectorMap map[string]map[string]string, stdin bool, rc *ResultCounts, namespaceSelectorMap map[string]map[string]string, stdin bool, rc *ResultCounts,
printPatchResource bool, printPatchResource bool, ruleToCloneSourceResource map[string]string,
) ([]*response.EngineResponse, policyreport.Info, error) { ) ([]*response.EngineResponse, policyreport.Info, error) {
var engineResponses []*response.EngineResponse var engineResponses []*response.EngineResponse
namespaceLabels := make(map[string]string) namespaceLabels := make(map[string]string)
@ -561,11 +563,17 @@ OuterLoop:
ExcludeResourceFunc: func(s1, s2, s3 string) bool { ExcludeResourceFunc: func(s1, s2, s3 string) bool {
return false return false
}, },
JSONContext: engineContext.NewContext(), JSONContext: ctx,
NamespaceLabels: namespaceLabels, NamespaceLabels: namespaceLabels,
} }
generateResponse := engine.ApplyBackgroundChecks(policyContext) generateResponse := engine.ApplyBackgroundChecks(policyContext)
if generateResponse != nil && !generateResponse.IsEmpty() { if generateResponse != nil && !generateResponse.IsEmpty() {
newRuleResponse, err := handleGeneratePolicy(generateResponse, *policyContext, ruleToCloneSourceResource)
if err != nil {
log.Log.Error(err, "failed to apply generate policy")
} else {
generateResponse.PolicyResponse.Rules = newRuleResponse
}
engineResponses = append(engineResponses, generateResponse) engineResponses = append(engineResponses, generateResponse)
} }
updateResultCounts(policy, generateResponse, resPath, rc) updateResultCounts(policy, generateResponse, resPath, rc)
@ -995,35 +1003,110 @@ func GetKindsFromPolicy(policy kyvernov1.PolicyInterface) map[string]struct{} {
return kindOnwhichPolicyIsApplied return kindOnwhichPolicyIsApplied
} }
// GetPatchedResourceFromPath - get patchedResource from given path //GetResourceFromPath - get patchedResource and generatedResource from given path
func GetPatchedResourceFromPath(fs billy.Filesystem, path string, isGit bool, policyResourcePath string) (unstructured.Unstructured, error) { func GetResourceFromPath(fs billy.Filesystem, path string, isGit bool, policyResourcePath string, resourceType string) (unstructured.Unstructured, error) {
var patchedResourceBytes []byte var resourceBytes []byte
var patchedResource unstructured.Unstructured var resource unstructured.Unstructured
var err error var err error
if isGit { if isGit {
if len(path) > 0 { if len(path) > 0 {
filep, fileErr := fs.Open(filepath.Join(policyResourcePath, path)) filep, fileErr := fs.Open(filepath.Join(policyResourcePath, path))
if fileErr != nil { if fileErr != nil {
fmt.Printf("Unable to open patchedResource file: %s. \nerror: %s", path, fileErr) fmt.Printf("Unable to open %s file: %s. \nerror: %s", resourceType, path, err)
} }
patchedResourceBytes, err = ioutil.ReadAll(filep) resourceBytes, err = ioutil.ReadAll(filep)
} }
} else { } else {
patchedResourceBytes, err = getFileBytes(path) resourceBytes, err = getFileBytes(path)
} }
if err != nil { if err != nil {
fmt.Printf("\n----------------------------------------------------------------------\nfailed to load patchedResource: %s. \nerror: %s\n----------------------------------------------------------------------\n", path, err) fmt.Printf("\n----------------------------------------------------------------------\nfailed to load %s: %s. \nerror: %s\n----------------------------------------------------------------------\n", resourceType, path, err)
return patchedResource, err return resource, err
} }
patchedResource, err = GetPatchedResource(patchedResourceBytes) resource, err = GetPatchedAndGeneratedResource(resourceBytes)
if err != nil { if err != nil {
return patchedResource, err return resource, err
} }
return patchedResource, nil return resource, nil
}
// initializeMockController initializes a basic Generate Controller with a fake dynamic client.
func initializeMockController(objects []runtime.Object) (*generate.GenerateController, error) {
client, err := dclient.NewMockClient(runtime.NewScheme(), nil, objects...)
if err != nil {
fmt.Printf("Failed to mock dynamic client")
return nil, err
}
client.SetDiscovery(dclient.NewFakeDiscoveryClient(nil))
c := generate.NewGenerateControllerWithOnlyClient(client)
return c, nil
}
// handleGeneratePolicy returns a new RuleResponse with the Kyverno generated resource configuration by applying the generate rule.
func handleGeneratePolicy(generateResponse *response.EngineResponse, policyContext engine.PolicyContext, ruleToCloneSourceResource map[string]string) ([]response.RuleResponse, error) {
objects := []runtime.Object{&policyContext.NewResource}
var resources = []*unstructured.Unstructured{}
for _, rule := range generateResponse.PolicyResponse.Rules {
if path, ok := ruleToCloneSourceResource[rule.Name]; ok {
resourceBytes, err := getFileBytes(path)
if err != nil {
fmt.Printf("failed to get resource bytes\n")
} else {
resources, err = GetResource(resourceBytes)
if err != nil {
fmt.Printf("failed to convert resource bytes to unstructured format\n")
}
}
}
}
for _, res := range resources {
objects = append(objects, res)
}
c, err := initializeMockController(objects)
if err != nil {
fmt.Println("error at controller")
return nil, err
}
gr := kyvernov1beta1.UpdateRequest{
Spec: kyvernov1beta1.UpdateRequestSpec{
Type: kyvernov1beta1.Generate,
Policy: generateResponse.PolicyResponse.Policy.Name,
Resource: kyvernov1.ResourceSpec{
Kind: generateResponse.PolicyResponse.Resource.Kind,
Namespace: generateResponse.PolicyResponse.Resource.Namespace,
Name: generateResponse.PolicyResponse.Resource.Name,
APIVersion: generateResponse.PolicyResponse.Resource.APIVersion,
},
},
}
var newRuleResponse []response.RuleResponse
for _, rule := range generateResponse.PolicyResponse.Rules {
genResource, _, err := c.ApplyGeneratePolicy(log.Log, &policyContext, gr, []string{rule.Name})
if err != nil {
rule.Status = response.RuleStatusError
return nil, err
}
unstrGenResource, err := c.GetUnstrResource(genResource[0])
if err != nil {
rule.Status = response.RuleStatusError
return nil, err
}
rule.GeneratedResource = *unstrGenResource
newRuleResponse = append(newRuleResponse, rule)
}
return newRuleResponse, nil
} }
// GetUserInfoFromPath - get the request info as user info from a given path // GetUserInfoFromPath - get the request info as user info from a given path

View file

@ -100,7 +100,7 @@ func Test_NamespaceSelector(t *testing.T) {
for _, tc := range testcases { for _, tc := range testcases {
policyArray, _ := ut.GetPolicy(tc.policy) policyArray, _ := ut.GetPolicy(tc.policy)
resourceArray, _ := GetResource(tc.resource) resourceArray, _ := GetResource(tc.resource)
ApplyPolicyOnResource(policyArray[0], resourceArray[0], "", false, nil, v1beta1.RequestInfo{}, false, tc.namespaceSelectorMap, false, rc, false) ApplyPolicyOnResource(policyArray[0], resourceArray[0], "", false, nil, v1beta1.RequestInfo{}, false, tc.namespaceSelectorMap, false, rc, false, nil)
assert.Equal(t, int64(rc.Pass), int64(tc.result.Pass)) assert.Equal(t, int64(rc.Pass), int64(tc.result.Pass))
assert.Equal(t, int64(rc.Fail), int64(tc.result.Fail)) assert.Equal(t, int64(rc.Fail), int64(tc.result.Fail))
// TODO: autogen rules seem to not be present when autogen internals is disabled // TODO: autogen rules seem to not be present when autogen internals is disabled

View file

@ -278,13 +278,13 @@ func convertResourceToUnstructured(resourceYaml []byte) (*unstructured.Unstructu
} }
// GetPatchedResource converts raw bytes to unstructured object // GetPatchedResource converts raw bytes to unstructured object
func GetPatchedResource(patchResourceBytes []byte) (unstructured.Unstructured, error) { func GetPatchedAndGeneratedResource(resourceBytes []byte) (unstructured.Unstructured, error) {
getPatchedResource, err := GetResource(patchResourceBytes) getResource, err := GetResource(resourceBytes)
if err != nil { if err != nil {
return unstructured.Unstructured{}, err return unstructured.Unstructured{}, err
} }
patchedResource := *getPatchedResource[0] resource := *getResource[0]
return patchedResource, nil return resource, nil
} }
// GetKindsFromRule will return the kinds from policy match block // GetKindsFromRule will return the kinds from policy match block

View file

@ -227,7 +227,7 @@ func (c *GenerateController) applyGenerate(resource unstructured.Unstructured, u
} }
// Apply the generate rule on resource // Apply the generate rule on resource
return c.applyGeneratePolicy(logger, policyContext, ur, applicableRules) return c.ApplyGeneratePolicy(logger, policyContext, ur, applicableRules)
} }
// cleanupClonedResource deletes cloned resource if sync is not enabled for the clone policy // cleanupClonedResource deletes cloned resource if sync is not enabled for the clone policy
@ -301,7 +301,7 @@ func updateStatus(statusControl common.StatusControlInterface, ur kyvernov1beta1
return nil return nil
} }
func (c *GenerateController) applyGeneratePolicy(log logr.Logger, policyContext *engine.PolicyContext, ur kyvernov1beta1.UpdateRequest, applicableRules []string) (genResources []kyvernov1.ResourceSpec, processExisting bool, err error) { func (c *GenerateController) ApplyGeneratePolicy(log logr.Logger, policyContext *engine.PolicyContext, ur kyvernov1beta1.UpdateRequest, applicableRules []string) (genResources []kyvernov1.ResourceSpec, processExisting bool, err error) {
// Get the response as the actions to be performed on the resource // Get the response as the actions to be performed on the resource
// - - substitute values // - - substitute values
policy := policyContext.Policy policy := policyContext.Policy
@ -388,7 +388,7 @@ func applyRule(log logr.Logger, client dclient.Interface, rule kyvernov1.Rule, r
var err error var err error
var mode ResourceMode var mode ResourceMode
var noGenResource kyvernov1.ResourceSpec var noGenResource kyvernov1.ResourceSpec
genUnst, err := getUnstrRule(rule.Generation.DeepCopy()) genUnst, err := GetUnstrRule(rule.Generation.DeepCopy())
if err != nil { if err != nil {
return noGenResource, err return noGenResource, err
} }
@ -614,7 +614,7 @@ const (
Update = "UPDATE" Update = "UPDATE"
) )
func getUnstrRule(rule *kyvernov1.Generation) (*unstructured.Unstructured, error) { func GetUnstrRule(rule *kyvernov1.Generation) (*unstructured.Unstructured, error) {
ruleData, err := json.Marshal(rule) ruleData, err := json.Marshal(rule)
if err != nil { if err != nil {
return nil, err return nil, err
@ -622,6 +622,36 @@ func getUnstrRule(rule *kyvernov1.Generation) (*unstructured.Unstructured, error
return utils.ConvertToUnstructured(ruleData) return utils.ConvertToUnstructured(ruleData)
} }
func (c *GenerateController) ApplyResource(resource *unstructured.Unstructured) error {
kind, _, namespace, apiVersion, err := getResourceInfo(resource.Object)
if err != nil {
return err
}
_, err = c.client.CreateResource(apiVersion, kind, namespace, resource, false)
if err != nil {
return err
}
return nil
}
// NewGenerateControllerWithOnlyClient returns an instance of Controller with only the client.
func NewGenerateControllerWithOnlyClient(client dclient.Interface) *GenerateController {
c := GenerateController{
client: client,
}
return &c
}
// GetUnstrResource converts ResourceSpec object to type Unstructured
func (c *GenerateController) GetUnstrResource(genResourceSpec kyvernov1.ResourceSpec) (*unstructured.Unstructured, error) {
resource, err := c.client.GetResource(genResourceSpec.APIVersion, genResourceSpec.Kind, genResourceSpec.Namespace, genResourceSpec.Name)
if err != nil {
return nil, err
}
return resource, nil
}
func deleteGeneratedResources(log logr.Logger, client dclient.Interface, ur kyvernov1beta1.UpdateRequest) error { func deleteGeneratedResources(log logr.Logger, client dclient.Interface, ur kyvernov1beta1.UpdateRequest) error {
for _, genResource := range ur.Status.GeneratedResources { for _, genResource := range ur.Status.GeneratedResources {
err := client.DeleteResource("", genResource.Kind, genResource.Namespace, genResource.Name, false) err := client.DeleteResource("", genResource.Kind, genResource.Namespace, genResource.Name, false)

View file

@ -96,7 +96,7 @@ func filterRule(rule kyvernov1.Rule, policyContext *PolicyContext) *response.Rul
} }
} }
} }
logger.V(4).Info("rule not matched", "reason", err.Error())
return nil return nil
} }

View file

@ -102,6 +102,9 @@ type RuleResponse struct {
// JSON patches, for mutation rules // JSON patches, for mutation rules
Patches [][]byte `json:"patches,omitempty"` Patches [][]byte `json:"patches,omitempty"`
// Resource generated by the generate rules of a policy
GeneratedResource unstructured.Unstructured `json:"generatedResource,omitempty"`
// rule status // rule status
Status RuleStatus `json:"status"` Status RuleStatus `json:"status"`

View file

@ -0,0 +1,10 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: hello-world-namespace
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress

View file

@ -0,0 +1,12 @@
name: deny-all-traffic
policies:
- policy.yaml
resources:
- resource.yaml
results:
- policy: add-networkpolicy
rule: default-deny
resource: hello-world-namespace
generatedResource: generatedResource.yaml
kind: Namespace
result: pass

View file

@ -0,0 +1,37 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-networkpolicy
annotations:
policies.kyverno.io/title: Add Network Policy
policies.kyverno.io/category: Multi-Tenancy
policies.kyverno.io/subject: NetworkPolicy
policies.kyverno.io/description: >-
By default, Kubernetes allows communications across all Pods within a cluster.
The NetworkPolicy resource and a CNI plug-in that supports NetworkPolicy must be used to restrict
communications. A default NetworkPolicy should be configured for each Namespace to
default deny all ingress and egress traffic to the Pods in the Namespace. Application
teams can then configure additional NetworkPolicy resources to allow desired traffic
to application Pods from select sources. This policy will create a new NetworkPolicy resource
named `default-deny` which will deny all traffic anytime a new Namespace is created.
spec:
rules:
- name: default-deny
match:
resources:
kinds:
- Namespace
generate:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
name: default-deny
namespace: "{{request.object.metadata.name}}"
synchronize: true
data:
spec:
# select all pods in the namespace
podSelector: {}
# deny all traffic
policyTypes:
- Ingress
- Egress

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: hello-world-namespace

View file

@ -0,0 +1,14 @@
apiVersion: v1
kind: LimitRange
metadata:
name: default-limitrange
namespace: hello-world-namespace
spec:
limits:
- default:
cpu: 500m
memory: 1Gi
defaultRequest:
cpu: 200m
memory: 256Mi
type: Container

View file

@ -0,0 +1,11 @@
apiVersion: v1
kind: ResourceQuota
metadata:
name: default-resourcequota
namespace: hello-world-namespace
spec:
hard:
requests.cpu: '4'
requests.memory: '16Gi'
limits.cpu: '4'
limits.memory: '16Gi'

View file

@ -0,0 +1,18 @@
name: add-quota
policies:
- policy.yaml
resources:
- resource.yaml
results:
- policy: add-ns-quota
rule: generate-resourcequota
resource: hello-world-namespace
generatedResource: generatedResourceQuota.yaml
kind: Namespace
result: pass
- policy: add-ns-quota
rule: generate-limitrange
resource: hello-world-namespace
generatedResource: generatedLimitRange.yaml
kind: Namespace
result: pass

View file

@ -0,0 +1,55 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-ns-quota
annotations:
policies.kyverno.io/title: Add Quota
policies.kyverno.io/category: Multi-Tenancy
policies.kyverno.io/subject: ResourceQuota, LimitRange
policies.kyverno.io/description: >-
To better control the number of resources that can be created in a given
Namespace and provide default resource consumption limits for Pods,
ResourceQuota and LimitRange resources are recommended.
This policy will generate ResourceQuota and LimitRange resources when
a new Namespace is created.
spec:
rules:
- name: generate-resourcequota
match:
resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: ResourceQuota
name: default-resourcequota
synchronize: true
namespace: "{{request.object.metadata.name}}"
data:
spec:
hard:
requests.cpu: '4'
requests.memory: '16Gi'
limits.cpu: '4'
limits.memory: '16Gi'
- name: generate-limitrange
match:
resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: LimitRange
name: default-limitrange
synchronize: true
namespace: "{{request.object.metadata.name}}"
data:
spec:
limits:
- default:
cpu: 500m
memory: 1Gi
defaultRequest:
cpu: 200m
memory: 256Mi
type: Container

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: hello-world-namespace

View file

@ -0,0 +1,10 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: nginx-deployment-default-pdb
namespace: hello-world
spec:
minAvailable: 1
selector:
matchLabels:
app: nginx

View file

@ -0,0 +1,13 @@
name: pdb-test
policies:
- policy.yaml
resources:
- resource.yaml
results:
- policy: create-default-pdb
rule: create-default-pdb
resource: nginx-deployment
generatedResource: generatedResource.yaml
kind: Deployment
result: pass
namespace: hello-world

View file

@ -0,0 +1,23 @@
apiVersion: kyverno.io/v1
kind: Policy
metadata:
name: create-default-pdb
namespace: hello-world
spec:
rules:
- name: create-default-pdb
match:
resources:
kinds:
- Deployment
generate:
apiVersion: policy/v1
kind: PodDisruptionBudget
name: "{{request.object.metadata.name}}-default-pdb"
namespace: "{{request.object.metadata.namespace}}"
data:
spec:
minAvailable: 1
selector:
matchLabels:
"{{request.object.metadata.labels}}"

View file

@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: hello-world
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: regcred
namespace: default
type: Opaque
data:
password: MWYyZDFlMmU2N2Rm

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: regcred
namespace: hello-world-namespace
type: Opaque
data:
password: MWYyZDFlMmU2N2Rm

View file

@ -0,0 +1,13 @@
name: sync-secrets
policies:
- policy.yaml
resources:
- resource.yaml
results:
- policy: sync-secrets
rule: sync-image-pull-secret
resource: hello-world-namespace
generatedResource: generatedResource.yaml
cloneSourceResource: cloneSourceResource.yaml
kind: Namespace
result: pass

View file

@ -0,0 +1,31 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: sync-secrets
annotations:
policies.kyverno.io/title: Sync Secrets
policies.kyverno.io/category: Sample
policies.kyverno.io/subject: Secret
policies.kyverno.io/description: >-
Secrets like registry credentials often need to exist in multiple
Namespaces so Pods there have access. Manually duplicating those Secrets
is time consuming and error prone. This policy will copy a
Secret called `regcred` which exists in the `default` Namespace to
new Namespaces when they are created. It will also push updates to
the copied Secrets should the source Secret be changed.
spec:
rules:
- name: sync-image-pull-secret
match:
resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: Secret
name: regcred
namespace: "{{request.object.metadata.name}}"
synchronize: true
clone:
namespace: default
name: regcred

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: hello-world-namespace

View file

@ -8,9 +8,11 @@ results:
rule: check-digest rule: check-digest
resource: no-digest resource: no-digest
kind: Pod kind: Pod
namespace: test
status: fail status: fail
- policy: require-image-digest - policy: require-image-digest
rule: check-digest rule: check-digest
resource: with-digest resource: with-digest
kind: Pod kind: Pod
namespace: test
status: pass status: pass

View file

@ -10,6 +10,7 @@ results:
rule: limit-configmap-for-sa-developer rule: limit-configmap-for-sa-developer
resource: any-configmap-name-good resource: any-configmap-name-good
kind: ConfigMap kind: ConfigMap
namespace: any-namespace
result: fail result: fail
- policy: limit-configmap-for-sa - policy: limit-configmap-for-sa
rule: limit-configmap-for-sa-developer rule: limit-configmap-for-sa-developer

View file

@ -9,11 +9,13 @@ results:
rule: require-image-tag rule: require-image-tag
resource: test-require-image-tag-pass resource: test-require-image-tag-pass
kind: Pod kind: Pod
namespace: test
status: pass status: pass
- policy: disallow-latest-tag - policy: disallow-latest-tag
rule: require-image-tag rule: require-image-tag
resource: test-require-image-tag-fail resource: test-require-image-tag-fail
kind: Pod kind: Pod
namespace: test
status: fail status: fail
- policy: disallow-latest-tag - policy: disallow-latest-tag
rule: validate-image-tag rule: validate-image-tag
@ -23,32 +25,38 @@ results:
- policy: disallow-latest-tag - policy: disallow-latest-tag
rule: validate-image-tag rule: validate-image-tag
resource: test-validate-image-tag-fail resource: test-validate-image-tag-fail
namespace: test
kind: Pod kind: Pod
status: fail status: fail
- policy: disallow-latest-tag - policy: disallow-latest-tag
rule: validate-image-tag rule: validate-image-tag
resource: test-validate-image-tag-pass resource: test-validate-image-tag-pass
kind: Pod kind: Pod
namespace: test
status: pass status: pass
- policy: duration-test - policy: duration-test
rule: greater-than rule: greater-than
resource: test-lifetime-fail resource: test-lifetime-fail
kind: Pod kind: Pod
namespace: test
status: fail status: fail
- policy: duration-test - policy: duration-test
rule: less-than rule: less-than
resource: test-lifetime-fail resource: test-lifetime-fail
kind: Pod kind: Pod
namespace: test
status: pass status: pass
- policy: duration-test - policy: duration-test
rule: greater-equal-than rule: greater-equal-than
resource: test-lifetime-fail resource: test-lifetime-fail
kind: Pod kind: Pod
namespace: test
status: fail status: fail
- policy: duration-test - policy: duration-test
rule: less-equal-than rule: less-equal-than
resource: test-lifetime-fail resource: test-lifetime-fail
kind: Pod kind: Pod
namespace: test
status: pass status: pass
- policy: restrict-pod-counts - policy: restrict-pod-counts
@ -60,11 +68,13 @@ results:
rule: restrict-pod-count rule: restrict-pod-count
resource: test-require-image-tag-pass resource: test-require-image-tag-pass
kind: Pod kind: Pod
namespace: test
status: fail status: fail
- policy: restrict-pod-counts - policy: restrict-pod-counts
rule: restrict-pod-count rule: restrict-pod-count
resource: test-require-image-tag-fail resource: test-require-image-tag-fail
kind: Pod kind: Pod
namespace: test
status: fail status: fail
- policy: restrict-pod-counts - policy: restrict-pod-counts
rule: restrict-pod-count rule: restrict-pod-count
@ -75,9 +85,11 @@ results:
rule: restrict-pod-count rule: restrict-pod-count
resource: test-validate-image-tag-fail resource: test-validate-image-tag-fail
kind: Pod kind: Pod
namespace: test
status: fail status: fail
- policy: restrict-pod-counts - policy: restrict-pod-counts
rule: restrict-pod-count rule: restrict-pod-count
resource: test-validate-image-tag-pass resource: test-validate-image-tag-pass
kind: Pod kind: Pod
namespace: test
status: fail status: fail