mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-06 16:06:56 +00:00
* refactor: engine rule response creation Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * private fields Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * more private Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fix Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fix Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fix unit tests Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> --------- Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
262 lines
9.6 KiB
Go
262 lines
9.6 KiB
Go
package engine
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/go-logr/logr"
|
||
gojmespath "github.com/jmespath/go-jmespath"
|
||
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
||
"github.com/kyverno/kyverno/pkg/clients/dclient"
|
||
"github.com/kyverno/kyverno/pkg/config"
|
||
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
|
||
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
|
||
"github.com/kyverno/kyverno/pkg/engine/handlers"
|
||
"github.com/kyverno/kyverno/pkg/engine/internal"
|
||
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
|
||
"github.com/kyverno/kyverno/pkg/logging"
|
||
"github.com/kyverno/kyverno/pkg/metrics"
|
||
"github.com/kyverno/kyverno/pkg/registryclient"
|
||
"github.com/kyverno/kyverno/pkg/tracing"
|
||
"go.opentelemetry.io/otel/metric/global"
|
||
"go.opentelemetry.io/otel/metric/instrument"
|
||
"go.opentelemetry.io/otel/trace"
|
||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||
)
|
||
|
||
type engine struct {
|
||
configuration config.Configuration
|
||
metricsConfiguration config.MetricsConfiguration
|
||
client dclient.Interface
|
||
rclient registryclient.Client
|
||
contextLoader engineapi.ContextLoaderFactory
|
||
exceptionSelector engineapi.PolicyExceptionSelector
|
||
// metrics
|
||
resultCounter instrument.Int64Counter
|
||
durationHistogram instrument.Float64Histogram
|
||
}
|
||
|
||
type handlerFactory = func() (handlers.Handler, error)
|
||
|
||
func NewEngine(
|
||
configuration config.Configuration,
|
||
metricsConfiguration config.MetricsConfiguration,
|
||
client dclient.Interface,
|
||
rclient registryclient.Client,
|
||
contextLoader engineapi.ContextLoaderFactory,
|
||
exceptionSelector engineapi.PolicyExceptionSelector,
|
||
) engineapi.Engine {
|
||
meter := global.MeterProvider().Meter(metrics.MeterName)
|
||
resultCounter, err := meter.Int64Counter(
|
||
"kyverno_policy_results",
|
||
instrument.WithDescription("can be used to track the results associated with the policies applied in the user’s cluster, at the level from rule to policy to admission requests"),
|
||
)
|
||
if err != nil {
|
||
logging.Error(err, "failed to register metric kyverno_policy_results")
|
||
}
|
||
durationHistogram, err := meter.Float64Histogram(
|
||
"kyverno_policy_execution_duration_seconds",
|
||
instrument.WithDescription("can be used to track the latencies (in seconds) associated with the execution/processing of the individual rules under Kyverno policies whenever they evaluate incoming resource requests"),
|
||
)
|
||
if err != nil {
|
||
logging.Error(err, "failed to register metric kyverno_policy_execution_duration_seconds")
|
||
}
|
||
return &engine{
|
||
configuration: configuration,
|
||
metricsConfiguration: metricsConfiguration,
|
||
client: client,
|
||
rclient: rclient,
|
||
contextLoader: contextLoader,
|
||
exceptionSelector: exceptionSelector,
|
||
resultCounter: resultCounter,
|
||
durationHistogram: durationHistogram,
|
||
}
|
||
}
|
||
|
||
func (e *engine) Validate(
|
||
ctx context.Context,
|
||
policyContext engineapi.PolicyContext,
|
||
) engineapi.EngineResponse {
|
||
response := engineapi.NewEngineResponseFromPolicyContext(policyContext, time.Now())
|
||
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.validate"), policyContext)
|
||
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
|
||
policyResponse := e.validate(ctx, logger, policyContext)
|
||
response = response.WithPolicyResponse(policyResponse)
|
||
}
|
||
response = response.Done(time.Now())
|
||
e.reportMetrics(ctx, logger, policyContext.Operation(), policyContext.AdmissionOperation(), response)
|
||
return response
|
||
}
|
||
|
||
func (e *engine) Mutate(
|
||
ctx context.Context,
|
||
policyContext engineapi.PolicyContext,
|
||
) engineapi.EngineResponse {
|
||
response := engineapi.NewEngineResponseFromPolicyContext(policyContext, time.Now())
|
||
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.mutate"), policyContext)
|
||
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
|
||
policyResponse, patchedResource := e.mutate(ctx, logger, policyContext)
|
||
response = response.
|
||
WithPolicyResponse(policyResponse).
|
||
WithPatchedResource(patchedResource)
|
||
}
|
||
response = response.Done(time.Now())
|
||
e.reportMetrics(ctx, logger, policyContext.Operation(), policyContext.AdmissionOperation(), response)
|
||
return response
|
||
}
|
||
|
||
func (e *engine) Generate(
|
||
ctx context.Context,
|
||
policyContext engineapi.PolicyContext,
|
||
) engineapi.EngineResponse {
|
||
response := engineapi.NewEngineResponseFromPolicyContext(policyContext, time.Now())
|
||
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.generate"), policyContext)
|
||
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
|
||
policyResponse := e.generateResponse(ctx, logger, policyContext)
|
||
response = response.WithPolicyResponse(policyResponse)
|
||
}
|
||
response = response.Done(time.Now())
|
||
e.reportMetrics(ctx, logger, policyContext.Operation(), policyContext.AdmissionOperation(), response)
|
||
return response
|
||
}
|
||
|
||
func (e *engine) VerifyAndPatchImages(
|
||
ctx context.Context,
|
||
policyContext engineapi.PolicyContext,
|
||
) (engineapi.EngineResponse, engineapi.ImageVerificationMetadata) {
|
||
response := engineapi.NewEngineResponseFromPolicyContext(policyContext, time.Now())
|
||
ivm := engineapi.ImageVerificationMetadata{}
|
||
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.verify"), policyContext)
|
||
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
|
||
policyResponse, innerIvm := e.verifyAndPatchImages(ctx, logger, policyContext)
|
||
response, ivm = response.WithPolicyResponse(policyResponse), innerIvm
|
||
}
|
||
response = response.Done(time.Now())
|
||
e.reportMetrics(ctx, logger, policyContext.Operation(), policyContext.AdmissionOperation(), response)
|
||
return response, ivm
|
||
}
|
||
|
||
func (e *engine) ApplyBackgroundChecks(
|
||
ctx context.Context,
|
||
policyContext engineapi.PolicyContext,
|
||
) engineapi.EngineResponse {
|
||
response := engineapi.NewEngineResponseFromPolicyContext(policyContext, time.Now())
|
||
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.background"), policyContext)
|
||
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
|
||
policyResponse := e.applyBackgroundChecks(ctx, logger, policyContext)
|
||
response = response.WithPolicyResponse(policyResponse)
|
||
}
|
||
response = response.Done(time.Now())
|
||
e.reportMetrics(ctx, logger, policyContext.Operation(), policyContext.AdmissionOperation(), response)
|
||
return response
|
||
}
|
||
|
||
func (e *engine) ContextLoader(
|
||
policy kyvernov1.PolicyInterface,
|
||
rule kyvernov1.Rule,
|
||
) engineapi.EngineContextLoader {
|
||
loader := e.contextLoader(policy, rule)
|
||
return func(ctx context.Context, contextEntries []kyvernov1.ContextEntry, jsonContext enginecontext.Interface) error {
|
||
return loader.Load(
|
||
ctx,
|
||
e.client,
|
||
e.rclient,
|
||
contextEntries,
|
||
jsonContext,
|
||
)
|
||
}
|
||
}
|
||
|
||
// matches checks if either the new or old resource satisfies the filter conditions defined in the rule
|
||
func matches(
|
||
rule kyvernov1.Rule,
|
||
policyContext engineapi.PolicyContext,
|
||
resource unstructured.Unstructured,
|
||
) error {
|
||
gvk, subresource := policyContext.ResourceKind()
|
||
err := engineutils.MatchesResourceDescription(
|
||
resource,
|
||
rule,
|
||
policyContext.AdmissionInfo(),
|
||
policyContext.NamespaceLabels(),
|
||
policyContext.Policy().GetNamespace(),
|
||
gvk,
|
||
subresource,
|
||
policyContext.Operation(),
|
||
)
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
oldResource := policyContext.OldResource()
|
||
if oldResource.Object != nil {
|
||
err := engineutils.MatchesResourceDescription(
|
||
policyContext.OldResource(),
|
||
rule,
|
||
policyContext.AdmissionInfo(),
|
||
policyContext.NamespaceLabels(),
|
||
policyContext.Policy().GetNamespace(),
|
||
gvk,
|
||
subresource,
|
||
policyContext.Operation(),
|
||
)
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
|
||
func (e *engine) invokeRuleHandler(
|
||
ctx context.Context,
|
||
logger logr.Logger,
|
||
handlerFactory handlerFactory,
|
||
policyContext engineapi.PolicyContext,
|
||
resource unstructured.Unstructured,
|
||
rule kyvernov1.Rule,
|
||
ruleType engineapi.RuleType,
|
||
) (unstructured.Unstructured, []engineapi.RuleResponse) {
|
||
return tracing.ChildSpan2(
|
||
ctx,
|
||
"pkg/engine",
|
||
fmt.Sprintf("RULE %s", rule.Name),
|
||
func(ctx context.Context, span trace.Span) (unstructured.Unstructured, []engineapi.RuleResponse) {
|
||
// check if resource and rule match
|
||
if err := matches(rule, policyContext, resource); err != nil {
|
||
logger.V(4).Info("rule not matched", "reason", err.Error())
|
||
return resource, nil
|
||
}
|
||
if handlerFactory == nil {
|
||
return resource, handlers.WithError(rule, ruleType, "failed to instantiate handler", nil)
|
||
} else if handler, err := handlerFactory(); err != nil {
|
||
return resource, handlers.WithError(rule, ruleType, "failed to instantiate handler", err)
|
||
} else if handler != nil {
|
||
// check if there's an exception
|
||
if ruleResp := e.hasPolicyExceptions(logger, ruleType, policyContext, rule); ruleResp != nil {
|
||
return resource, handlers.WithResponses(ruleResp)
|
||
}
|
||
// load rule context
|
||
contextLoader := e.ContextLoader(policyContext.Policy(), rule)
|
||
if err := contextLoader(ctx, rule.Context, policyContext.JSONContext()); err != nil {
|
||
if _, ok := err.(gojmespath.NotFoundError); ok {
|
||
logger.V(3).Info("failed to load context", "reason", err.Error())
|
||
} else {
|
||
logger.Error(err, "failed to load context")
|
||
}
|
||
return resource, handlers.WithError(rule, ruleType, "failed to load context", err)
|
||
}
|
||
// check preconditions
|
||
preconditionsPassed, err := internal.CheckPreconditions(logger, policyContext.JSONContext(), rule.GetAnyAllConditions())
|
||
if err != nil {
|
||
return resource, handlers.WithError(rule, ruleType, "failed to evaluate preconditions", err)
|
||
}
|
||
if !preconditionsPassed {
|
||
return resource, handlers.WithSkip(rule, ruleType, "preconditions not met")
|
||
}
|
||
// process handler
|
||
return handler.Process(ctx, logger, policyContext, resource, rule, contextLoader)
|
||
}
|
||
return resource, nil
|
||
},
|
||
)
|
||
}
|