mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-09 09:26:54 +00:00
* refactor: introduce cli processor package Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * tests Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * counts Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> --------- Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
298 lines
9.4 KiB
Go
298 lines
9.4 KiB
Go
package processor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
|
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
|
|
valuesapi "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apis/values"
|
|
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log"
|
|
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/pkg/autogen"
|
|
"github.com/kyverno/kyverno/pkg/clients/dclient"
|
|
"github.com/kyverno/kyverno/pkg/config"
|
|
"github.com/kyverno/kyverno/pkg/engine"
|
|
"github.com/kyverno/kyverno/pkg/engine/adapters"
|
|
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
|
|
"github.com/kyverno/kyverno/pkg/engine/factories"
|
|
"github.com/kyverno/kyverno/pkg/engine/jmespath"
|
|
"github.com/kyverno/kyverno/pkg/imageverifycache"
|
|
"github.com/kyverno/kyverno/pkg/registryclient"
|
|
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
|
|
yamlv2 "gopkg.in/yaml.v2"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
)
|
|
|
|
type PolicyProcessor struct {
|
|
Policy kyvernov1.PolicyInterface
|
|
Resource *unstructured.Unstructured
|
|
MutateLogPath string
|
|
MutateLogPathIsDir bool
|
|
Variables map[string]interface{}
|
|
UserInfo *kyvernov1beta1.RequestInfo
|
|
PolicyReport bool
|
|
NamespaceSelectorMap map[string]map[string]string
|
|
Stdin bool
|
|
Rc *ResultCounts
|
|
PrintPatchResource bool
|
|
RuleToCloneSourceResource map[string]string
|
|
Client dclient.Interface
|
|
AuditWarn bool
|
|
Subresources []valuesapi.Subresource
|
|
}
|
|
|
|
func (p *PolicyProcessor) ApplyPolicyOnResource() ([]engineapi.EngineResponse, error) {
|
|
jp := jmespath.New(config.NewDefaultConfiguration(false))
|
|
|
|
var engineResponses []engineapi.EngineResponse
|
|
namespaceLabels := make(map[string]string)
|
|
operation := kyvernov1.Create
|
|
|
|
if p.Variables["request.operation"] == "DELETE" {
|
|
operation = kyvernov1.Delete
|
|
}
|
|
|
|
policyWithNamespaceSelector := false
|
|
OuterLoop:
|
|
for _, p := range autogen.ComputeRules(p.Policy) {
|
|
if p.MatchResources.ResourceDescription.NamespaceSelector != nil ||
|
|
p.ExcludeResources.ResourceDescription.NamespaceSelector != nil {
|
|
policyWithNamespaceSelector = true
|
|
break
|
|
}
|
|
for _, m := range p.MatchResources.Any {
|
|
if m.ResourceDescription.NamespaceSelector != nil {
|
|
policyWithNamespaceSelector = true
|
|
break OuterLoop
|
|
}
|
|
}
|
|
for _, m := range p.MatchResources.All {
|
|
if m.ResourceDescription.NamespaceSelector != nil {
|
|
policyWithNamespaceSelector = true
|
|
break OuterLoop
|
|
}
|
|
}
|
|
for _, e := range p.ExcludeResources.Any {
|
|
if e.ResourceDescription.NamespaceSelector != nil {
|
|
policyWithNamespaceSelector = true
|
|
break OuterLoop
|
|
}
|
|
}
|
|
for _, e := range p.ExcludeResources.All {
|
|
if e.ResourceDescription.NamespaceSelector != nil {
|
|
policyWithNamespaceSelector = true
|
|
break OuterLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
if policyWithNamespaceSelector {
|
|
resourceNamespace := p.Resource.GetNamespace()
|
|
namespaceLabels = p.NamespaceSelectorMap[p.Resource.GetNamespace()]
|
|
if resourceNamespace != "default" && len(namespaceLabels) < 1 {
|
|
return engineResponses, sanitizederror.NewWithError(fmt.Sprintf("failed to get namespace labels for resource %s. use --values-file flag to pass the namespace labels", p.Resource.GetName()), nil)
|
|
}
|
|
}
|
|
|
|
resPath := fmt.Sprintf("%s/%s/%s", p.Resource.GetNamespace(), p.Resource.GetKind(), p.Resource.GetName())
|
|
log.Log.V(3).Info("applying policy on resource", "policy", p.Policy.GetName(), "resource", resPath)
|
|
|
|
resourceRaw, err := p.Resource.MarshalJSON()
|
|
if err != nil {
|
|
log.Log.Error(err, "failed to marshal resource")
|
|
}
|
|
|
|
updatedResource, err := kubeutils.BytesToUnstructured(resourceRaw)
|
|
if err != nil {
|
|
log.Log.Error(err, "unable to convert raw resource to unstructured")
|
|
}
|
|
|
|
if err != nil {
|
|
log.Log.Error(err, "failed to load resource in context")
|
|
}
|
|
|
|
cfg := config.NewDefaultConfiguration(false)
|
|
gvk, subresource := updatedResource.GroupVersionKind(), ""
|
|
// If --cluster flag is not set, then we need to find the top level resource GVK and subresource
|
|
if p.Client == nil {
|
|
for _, s := range p.Subresources {
|
|
subgvk := schema.GroupVersionKind{
|
|
Group: s.APIResource.Group,
|
|
Version: s.APIResource.Version,
|
|
Kind: s.APIResource.Kind,
|
|
}
|
|
if gvk == subgvk {
|
|
gvk = schema.GroupVersionKind{
|
|
Group: s.ParentResource.Group,
|
|
Version: s.ParentResource.Version,
|
|
Kind: s.ParentResource.Kind,
|
|
}
|
|
parts := strings.Split(s.APIResource.Name, "/")
|
|
subresource = parts[1]
|
|
}
|
|
}
|
|
}
|
|
var client engineapi.Client
|
|
if p.Client != nil {
|
|
client = adapters.Client(p.Client)
|
|
}
|
|
rclient := registryclient.NewOrDie()
|
|
eng := engine.NewEngine(
|
|
cfg,
|
|
config.NewDefaultMetricsConfiguration(),
|
|
jmespath.New(cfg),
|
|
client,
|
|
factories.DefaultRegistryClientFactory(adapters.RegistryClient(rclient), nil),
|
|
imageverifycache.DisabledImageVerifyCache(),
|
|
store.ContextLoaderFactory(nil),
|
|
nil,
|
|
"",
|
|
)
|
|
policyContext, err := engine.NewPolicyContext(
|
|
jp,
|
|
*updatedResource,
|
|
operation,
|
|
p.UserInfo,
|
|
cfg,
|
|
)
|
|
if err != nil {
|
|
log.Log.Error(err, "failed to create policy context")
|
|
}
|
|
|
|
policyContext = policyContext.
|
|
WithPolicy(p.Policy).
|
|
WithNamespaceLabels(namespaceLabels).
|
|
WithResourceKind(gvk, subresource)
|
|
|
|
for key, value := range p.Variables {
|
|
err = policyContext.JSONContext().AddVariable(key, value)
|
|
if err != nil {
|
|
log.Log.Error(err, "failed to add variable to context")
|
|
}
|
|
}
|
|
|
|
mutateResponse := eng.Mutate(context.Background(), policyContext)
|
|
combineRuleResponses(mutateResponse)
|
|
engineResponses = append(engineResponses, mutateResponse)
|
|
|
|
err = p.processMutateEngineResponse(mutateResponse, resPath)
|
|
if err != nil {
|
|
if !sanitizederror.IsErrorSanitized(err) {
|
|
return engineResponses, sanitizederror.NewWithError("failed to print mutated result", err)
|
|
}
|
|
}
|
|
|
|
verifyImageResponse, _ := eng.VerifyAndPatchImages(context.TODO(), policyContext)
|
|
if !verifyImageResponse.IsEmpty() {
|
|
verifyImageResponse = combineRuleResponses(verifyImageResponse)
|
|
engineResponses = append(engineResponses, verifyImageResponse)
|
|
}
|
|
|
|
var policyHasValidate bool
|
|
for _, rule := range autogen.ComputeRules(p.Policy) {
|
|
if rule.HasValidate() || rule.HasVerifyImageChecks() {
|
|
policyHasValidate = true
|
|
}
|
|
}
|
|
|
|
policyContext = policyContext.WithNewResource(mutateResponse.PatchedResource)
|
|
|
|
var validateResponse engineapi.EngineResponse
|
|
if policyHasValidate {
|
|
validateResponse = eng.Validate(context.Background(), policyContext)
|
|
validateResponse = combineRuleResponses(validateResponse)
|
|
}
|
|
|
|
if !validateResponse.IsEmpty() {
|
|
engineResponses = append(engineResponses, validateResponse)
|
|
}
|
|
|
|
var policyHasGenerate bool
|
|
for _, rule := range autogen.ComputeRules(p.Policy) {
|
|
if rule.HasGenerate() {
|
|
policyHasGenerate = true
|
|
}
|
|
}
|
|
|
|
if policyHasGenerate {
|
|
generateResponse := eng.ApplyBackgroundChecks(context.TODO(), policyContext)
|
|
if !generateResponse.IsEmpty() {
|
|
newRuleResponse, err := handleGeneratePolicy(&generateResponse, *policyContext, p.RuleToCloneSourceResource)
|
|
if err != nil {
|
|
log.Log.Error(err, "failed to apply generate policy")
|
|
} else {
|
|
generateResponse.PolicyResponse.Rules = newRuleResponse
|
|
}
|
|
combineRuleResponses(generateResponse)
|
|
engineResponses = append(engineResponses, generateResponse)
|
|
}
|
|
p.Rc.addGenerateResponse(p.AuditWarn, resPath, generateResponse)
|
|
}
|
|
|
|
p.Rc.addEngineResponses(p.AuditWarn, engineResponses...)
|
|
|
|
return engineResponses, nil
|
|
}
|
|
|
|
func (p *PolicyProcessor) processMutateEngineResponse(response engineapi.EngineResponse, resourcePath string) error {
|
|
printMutatedRes := p.Rc.addMutateResponse(resourcePath, response)
|
|
if printMutatedRes && p.PrintPatchResource {
|
|
yamlEncodedResource, err := yamlv2.Marshal(response.PatchedResource.Object)
|
|
if err != nil {
|
|
return sanitizederror.NewWithError("failed to marshal", err)
|
|
}
|
|
|
|
if p.MutateLogPath == "" {
|
|
mutatedResource := string(yamlEncodedResource) + string("\n---")
|
|
if len(strings.TrimSpace(mutatedResource)) > 0 {
|
|
if !p.Stdin {
|
|
fmt.Printf("\nmutate policy %s applied to %s:", p.Policy.GetName(), resourcePath)
|
|
}
|
|
fmt.Printf("\n" + mutatedResource + "\n")
|
|
}
|
|
} else {
|
|
err := p.printMutatedOutput(string(yamlEncodedResource))
|
|
if err != nil {
|
|
return sanitizederror.NewWithError("failed to print mutated result", err)
|
|
}
|
|
fmt.Printf("\n\nMutation:\nMutation has been applied successfully. Check the files.")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *PolicyProcessor) printMutatedOutput(yaml string) error {
|
|
var file *os.File
|
|
mutateLogPath := filepath.Clean(p.MutateLogPath)
|
|
filename := p.Resource.GetName() + "-mutated"
|
|
if !p.MutateLogPathIsDir {
|
|
// truncation for the case when mutateLogPath is a file (not a directory) is handled under pkg/kyverno/apply/test_command.go
|
|
f, err := os.OpenFile(mutateLogPath, os.O_APPEND|os.O_WRONLY, 0o600) // #nosec G304
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file = f
|
|
} else {
|
|
f, err := os.OpenFile(filepath.Join(mutateLogPath, filename+".yaml"), os.O_CREATE|os.O_WRONLY, 0o600) // #nosec G304
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file = f
|
|
}
|
|
if _, err := file.Write([]byte(yaml + "\n---\n\n")); err != nil {
|
|
if err := file.Close(); err != nil {
|
|
log.Log.Error(err, "failed to close file")
|
|
}
|
|
return err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|