mirror of
https://github.com/kyverno/kyverno.git
synced 2024-12-14 11:57:48 +00:00
c391fba64c
* 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>
397 lines
14 KiB
Go
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
|
|
}
|