1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00
kyverno/cmd/cli/kubectl-kyverno/processor/policy_processor.go
Mariam Fahmy c391fba64c
fix: get ns labels in the cluster mode when using the CLI (#10348)
* fix: get ns labels in the cluster mode when using the CLI

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>

* chore: fix chainsaw test

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>

* Update .vscode/launch.json

Co-authored-by: shuting <shuting@nirmata.com>
Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

---------

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>
Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>
Co-authored-by: shuting <shuting@nirmata.com>
2024-06-04 10:44:44 +00:00

397 lines
14 KiB
Go

package processor
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
json_patch "github.com/evanphx/json-patch/v5"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apis/v1alpha1"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/store"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/common"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/variables"
"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/engine/mutate/patch"
"github.com/kyverno/kyverno/pkg/engine/policycontext"
"github.com/kyverno/kyverno/pkg/exceptions"
"github.com/kyverno/kyverno/pkg/imageverifycache"
"github.com/kyverno/kyverno/pkg/registryclient"
jsonutils "github.com/kyverno/kyverno/pkg/utils/json"
"gomodules.xyz/jsonpatch/v2"
yamlv2 "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type PolicyProcessor struct {
Store *store.Store
Policies []kyvernov1.PolicyInterface
Resource unstructured.Unstructured
PolicyExceptions []*kyvernov2beta1.PolicyException
MutateLogPath string
MutateLogPathIsDir bool
Variables *variables.Variables
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 []v1alpha1.Subresource
Out io.Writer
}
func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse, error) {
cfg := config.NewDefaultConfiguration(false)
jp := jmespath.New(cfg)
resource := p.Resource
namespaceLabels := p.NamespaceSelectorMap[p.Resource.GetNamespace()]
policyExceptionLister := &policyExceptionLister{
exceptions: p.PolicyExceptions,
}
var client engineapi.Client
if p.Client != nil {
client = adapters.Client(p.Client)
}
rclient := p.Store.GetRegistryClient()
if rclient == nil {
rclient = registryclient.NewOrDie()
}
eng := engine.NewEngine(
cfg,
config.NewDefaultMetricsConfiguration(),
jmespath.New(cfg),
client,
factories.DefaultRegistryClientFactory(adapters.RegistryClient(rclient), nil),
imageverifycache.DisabledImageVerifyCache(),
store.ContextLoaderFactory(p.Store, nil),
exceptions.New(policyExceptionLister),
)
gvk, subresource := resource.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.Subresource.Group,
Version: s.Subresource.Version,
Kind: s.Subresource.Kind,
}
if gvk == subgvk {
gvk = schema.GroupVersionKind{
Group: s.ParentResource.Group,
Version: s.ParentResource.Version,
Kind: s.ParentResource.Kind,
}
parts := strings.Split(s.Subresource.Name, "/")
subresource = parts[1]
}
}
}
resPath := fmt.Sprintf("%s/%s/%s", resource.GetNamespace(), resource.GetKind(), resource.GetName())
responses := make([]engineapi.EngineResponse, 0, len(p.Policies))
// mutate
for _, policy := range p.Policies {
if !policy.GetSpec().HasMutate() {
continue
}
policyContext, err := p.makePolicyContext(jp, cfg, resource, policy, namespaceLabels, gvk, subresource)
if err != nil {
return responses, err
}
mutateResponse := eng.Mutate(context.Background(), policyContext)
err = p.processMutateEngineResponse(mutateResponse, resPath)
if err != nil {
return responses, fmt.Errorf("failed to print mutated result (%w)", err)
}
responses = append(responses, mutateResponse)
resource = mutateResponse.PatchedResource
}
// verify images
for _, policy := range p.Policies {
if !policy.GetSpec().HasVerifyImages() {
continue
}
policyContext, err := p.makePolicyContext(jp, cfg, resource, policy, namespaceLabels, gvk, subresource)
if err != nil {
return responses, err
}
verifyImageResponse, verifiedImageData := eng.VerifyAndPatchImages(context.TODO(), policyContext)
// update annotation to reflect verified images
var patches []jsonpatch.JsonPatchOperation
if !verifiedImageData.IsEmpty() {
annotationPatches, err := verifiedImageData.Patches(len(verifyImageResponse.PatchedResource.GetAnnotations()) != 0, log.Log)
if err != nil {
return responses, err
}
// add annotation patches first
patches = append(annotationPatches, patches...)
}
if len(patches) != 0 {
patch := jsonutils.JoinPatches(patch.ConvertPatches(patches...)...)
decoded, err := json_patch.DecodePatch(patch)
if err != nil {
return responses, err
}
options := &json_patch.ApplyOptions{SupportNegativeIndices: true, AllowMissingPathOnRemove: true, EnsurePathExistsOnAdd: true}
resourceBytes, err := verifyImageResponse.PatchedResource.MarshalJSON()
if err != nil {
return responses, err
}
patchedResourceBytes, err := decoded.ApplyWithOptions(resourceBytes, options)
if err != nil {
return responses, err
}
if err := verifyImageResponse.PatchedResource.UnmarshalJSON(patchedResourceBytes); err != nil {
return responses, err
}
}
responses = append(responses, verifyImageResponse)
resource = verifyImageResponse.PatchedResource
}
// validate
for _, policy := range p.Policies {
if !policyHasValidateOrVerifyImageChecks(policy) {
continue
}
policyContext, err := p.makePolicyContext(jp, cfg, resource, policy, namespaceLabels, gvk, subresource)
if err != nil {
return responses, err
}
validateResponse := eng.Validate(context.TODO(), policyContext)
responses = append(responses, validateResponse)
resource = validateResponse.PatchedResource
}
// generate
for _, policy := range p.Policies {
if policy.GetSpec().HasGenerate() {
policyContext, err := p.makePolicyContext(jp, cfg, resource, policy, namespaceLabels, gvk, subresource)
if err != nil {
return responses, err
}
generateResponse := eng.ApplyBackgroundChecks(context.TODO(), policyContext)
if !generateResponse.IsEmpty() {
newRuleResponse, err := handleGeneratePolicy(p.Out, p.Store, &generateResponse, *policyContext, p.RuleToCloneSourceResource)
if err != nil {
log.Log.Error(err, "failed to apply generate policy")
} else {
generateResponse.PolicyResponse.Rules = newRuleResponse
}
responses = append(responses, generateResponse)
}
p.Rc.addGenerateResponse(p.AuditWarn, generateResponse)
}
}
p.Rc.addEngineResponses(p.AuditWarn, responses...)
return responses, nil
}
func (p *PolicyProcessor) makePolicyContext(
jp jmespath.Interface,
cfg config.Configuration,
resource unstructured.Unstructured,
policy kyvernov1.PolicyInterface,
namespaceLabels map[string]string,
gvk schema.GroupVersionKind,
subresource string,
) (*policycontext.PolicyContext, error) {
operation := kyvernov1.Create
var resourceValues map[string]interface{}
if p.Variables != nil {
kindOnwhichPolicyIsApplied := common.GetKindsFromPolicy(p.Out, policy, p.Variables.Subresources(), p.Client)
vals, err := p.Variables.ComputeVariables(p.Store, policy.GetName(), resource.GetName(), resource.GetKind(), kindOnwhichPolicyIsApplied /*matches...*/)
if err != nil {
return nil, fmt.Errorf("policy `%s` have variables. pass the values for the variables for resource `%s` using set/values_file flag (%w)",
policy.GetName(),
resource.GetName(),
err,
)
}
resourceValues = vals
}
// TODO: this is kind of buggy, we should read that from the json context
switch resourceValues["request.operation"] {
case "DELETE":
operation = kyvernov1.Delete
case "UPDATE":
operation = kyvernov1.Update
}
policyContext, err := engine.NewPolicyContext(
jp,
resource,
operation,
p.UserInfo,
cfg,
)
if err != nil {
log.Log.Error(err, "failed to create policy context")
return nil, fmt.Errorf("failed to create policy context (%w)", err)
}
if operation == kyvernov1.Update {
resource := resource.DeepCopy()
policyContext = policyContext.WithOldResource(*resource)
if err := policyContext.JSONContext().AddOldResource(resource.Object); err != nil {
return nil, fmt.Errorf("failed to update old resource in json context (%w)", err)
}
}
if operation == kyvernov1.Delete {
policyContext = policyContext.WithOldResource(resource)
if err := policyContext.JSONContext().AddOldResource(resource.Object); err != nil {
return nil, fmt.Errorf("failed to update old resource in json context (%w)", err)
}
}
if p.Client != nil && len(namespaceLabels) == 0 && resource.GetKind() != "Namespace" {
ns, err := p.Client.GetResource(context.TODO(), "v1", "Namespace", "", resource.GetNamespace())
if err != nil {
log.Log.Error(err, "failed to get the resource's namespace")
return nil, fmt.Errorf("failed to get the resource's namespace (%w)", err)
}
namespaceLabels = ns.GetLabels()
}
policyContext = policyContext.
WithPolicy(policy).
WithNamespaceLabels(namespaceLabels).
WithResourceKind(gvk, subresource)
for key, value := range resourceValues {
err = policyContext.JSONContext().AddVariable(key, value)
if err != nil {
log.Log.Error(err, "failed to add variable to context", "key", key, "value", value)
return nil, fmt.Errorf("failed to add variable to context %s (%w)", key, err)
}
}
// we need to get the resources back from the context to account for injected variables
switch operation {
case kyvernov1.Create:
ret, err := policyContext.JSONContext().Query("request.object")
if err != nil {
return nil, err
}
if ret == nil {
policyContext = policyContext.WithNewResource(unstructured.Unstructured{})
} else {
object, ok := ret.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("the object retrieved from the json context is not valid")
}
policyContext = policyContext.WithNewResource(unstructured.Unstructured{Object: object})
}
case kyvernov1.Update:
{
ret, err := policyContext.JSONContext().Query("request.object")
if err != nil {
return nil, err
}
if ret == nil {
policyContext = policyContext.WithNewResource(unstructured.Unstructured{})
} else {
object, ok := ret.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("the object retrieved from the json context is not valid")
}
policyContext = policyContext.WithNewResource(unstructured.Unstructured{Object: object})
}
}
{
ret, err := policyContext.JSONContext().Query("request.oldObject")
if err != nil {
return nil, err
}
if ret == nil {
policyContext = policyContext.WithOldResource(unstructured.Unstructured{})
} else {
object, ok := ret.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("the object retrieved from the json context is not valid")
}
policyContext = policyContext.WithOldResource(unstructured.Unstructured{Object: object})
}
}
case kyvernov1.Delete:
ret, err := policyContext.JSONContext().Query("request.oldObject")
if err != nil {
return nil, err
}
if ret == nil {
policyContext = policyContext.WithOldResource(unstructured.Unstructured{})
} else {
object, ok := ret.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("the object retrieved from the json context is not valid")
}
policyContext = policyContext.WithOldResource(unstructured.Unstructured{Object: object})
}
}
return policyContext, nil
}
func (p *PolicyProcessor) processMutateEngineResponse(response engineapi.EngineResponse, resourcePath string) error {
printMutatedRes := p.Rc.addMutateResponse(response)
if printMutatedRes && p.PrintPatchResource {
yamlEncodedResource, err := yamlv2.Marshal(response.PatchedResource.Object)
if err != nil {
return fmt.Errorf("failed to marshal (%w)", err)
}
if p.MutateLogPath == "" {
mutatedResource := string(yamlEncodedResource) + string("\n---")
if len(strings.TrimSpace(mutatedResource)) > 0 {
if !p.Stdin {
fmt.Fprintf(p.Out, "\nmutate policy %s applied to %s:", response.Policy().GetName(), resourcePath)
}
fmt.Fprintf(p.Out, "\n"+mutatedResource+"\n")
}
} else {
err := p.printMutatedOutput(string(yamlEncodedResource))
if err != nil {
return fmt.Errorf("failed to print mutated result (%w)", err)
}
fmt.Fprintf(p.Out, "\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
}