mirror of
https://github.com/kyverno/kyverno.git
synced 2024-12-14 11:57:48 +00:00
refactor: webhooks package (#3516)
* refactor: use more policy interface Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com> * refactor: migrate to policy interface Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com> * refactor: webhooks package Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
This commit is contained in:
parent
9fc65fa5a7
commit
6e813a6b9e
13 changed files with 547 additions and 571 deletions
|
@ -62,3 +62,28 @@ func ResponseWithMessageAndPatch(allowed bool, msg string, patch []byte) *v1beta
|
|||
r.Patch = patch
|
||||
return r
|
||||
}
|
||||
|
||||
func ResponseStatus(allowed bool, status, msg string) *v1beta1.AdmissionResponse {
|
||||
r := Response(allowed)
|
||||
r.Result = &metav1.Status{
|
||||
Status: status,
|
||||
Message: msg,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func ResponseFailure(allowed bool, msg string) *v1beta1.AdmissionResponse {
|
||||
return ResponseStatus(allowed, metav1.StatusFailure, msg)
|
||||
}
|
||||
|
||||
func ResponseSuccess(allowed bool, msg string) *v1beta1.AdmissionResponse {
|
||||
return ResponseStatus(allowed, metav1.StatusSuccess, msg)
|
||||
}
|
||||
|
||||
func ResponseSuccessWithPatch(allowed bool, msg string, patch []byte) *v1beta1.AdmissionResponse {
|
||||
r := ResponseSuccess(allowed, msg)
|
||||
if len(patch) > 0 {
|
||||
r.Patch = patch
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
func (ws *WebhookServer) verifyHandler(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := ws.log.WithName("verifyHandler").WithValues("action", "verify", "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation, "gvk", request.Kind.String())
|
||||
logger.V(3).Info("incoming request", "last admission request timestamp", ws.webhookMonitor.Time())
|
||||
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
|
@ -7,9 +7,12 @@ import (
|
|||
"github.com/go-logr/logr"
|
||||
wildcard "github.com/kyverno/go-wildcard"
|
||||
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
"github.com/kyverno/kyverno/pkg/autogen"
|
||||
enginectx "github.com/kyverno/kyverno/pkg/engine/context"
|
||||
"github.com/kyverno/kyverno/pkg/engine/response"
|
||||
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
|
||||
"github.com/pkg/errors"
|
||||
yamlv2 "gopkg.in/yaml.v2"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
@ -125,6 +128,14 @@ func (i *ArrayFlags) Set(value string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// patchRequest applies patches to the request.Object and returns a new copy of the request
|
||||
func patchRequest(patches []byte, request *v1beta1.AdmissionRequest, logger logr.Logger) *v1beta1.AdmissionRequest {
|
||||
patchedResource := processResourceWithPatches(patches, request.Object.Raw, logger)
|
||||
newRequest := request.DeepCopy()
|
||||
newRequest.Object.Raw = patchedResource
|
||||
return newRequest
|
||||
}
|
||||
|
||||
func processResourceWithPatches(patch []byte, resource []byte, log logr.Logger) []byte {
|
||||
if patch == nil {
|
||||
return resource
|
||||
|
@ -246,3 +257,17 @@ func excludeKyvernoResources(kind string) bool {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func newVariablesContext(request *v1beta1.AdmissionRequest, userRequestInfo *v1.RequestInfo) (*enginectx.Context, error) {
|
||||
ctx := enginectx.NewContext()
|
||||
if err := ctx.AddRequest(request); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load incoming request in context")
|
||||
}
|
||||
if err := ctx.AddUserInfo(*userRequestInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load userInfo in context")
|
||||
}
|
||||
if err := ctx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load service account in context")
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
|
225
pkg/webhooks/handlers.go
Normal file
225
pkg/webhooks/handlers.go
Normal file
|
@ -0,0 +1,225 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
"github.com/kyverno/kyverno/pkg/common"
|
||||
"github.com/kyverno/kyverno/pkg/engine"
|
||||
enginectx "github.com/kyverno/kyverno/pkg/engine/context"
|
||||
policyvalidate "github.com/kyverno/kyverno/pkg/policy"
|
||||
"github.com/kyverno/kyverno/pkg/policycache"
|
||||
"github.com/kyverno/kyverno/pkg/policymutation"
|
||||
"github.com/kyverno/kyverno/pkg/userinfo"
|
||||
"github.com/kyverno/kyverno/pkg/utils"
|
||||
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
|
||||
"github.com/kyverno/kyverno/pkg/webhooks/handlers"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
// TODO: use admission review sub resource ?
|
||||
func isStatusUpdate(old, new kyverno.PolicyInterface) bool {
|
||||
if !reflect.DeepEqual(old.GetAnnotations(), new.GetAnnotations()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(old.GetLabels(), new.GetLabels()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(old.GetSpec(), new.GetSpec()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func errorResponse(logger logr.Logger, err error, message string) *v1beta1.AdmissionResponse {
|
||||
logger.Error(err, message)
|
||||
return admissionutils.ResponseFailure(false, message+": "+err.Error())
|
||||
}
|
||||
|
||||
func setupLogger(logger logr.Logger, name string, request *v1beta1.AdmissionRequest) logr.Logger {
|
||||
return logger.WithName("MutateWebhook").WithValues(
|
||||
"uid", request.UID,
|
||||
"kind", request.Kind,
|
||||
"namespace", request.Namespace,
|
||||
"name", request.Name,
|
||||
"operation", request.Operation,
|
||||
"gvk", request.Kind.String(),
|
||||
)
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) admissionHandler(filter bool, inner handlers.AdmissionHandler) http.HandlerFunc {
|
||||
if filter {
|
||||
inner = handlers.Filter(ws.configHandler, inner)
|
||||
}
|
||||
return handlers.Monitor(ws.webhookMonitor, handlers.Admission(ws.log, inner))
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) policyMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := setupLogger(ws.log, "policy mutation", request)
|
||||
policy, oldPolicy, err := admissionutils.GetPolicies(request)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to unmarshal policies from admission request")
|
||||
return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to default value, check kyverno controller logs for details: %v", err))
|
||||
}
|
||||
if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
|
||||
logger.V(4).Info("skip policy mutation on status update")
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
startTime := time.Now()
|
||||
logger.V(3).Info("start policy change mutation")
|
||||
defer logger.V(3).Info("finished policy change mutation", "time", time.Since(startTime).String())
|
||||
// Generate JSON Patches for defaults
|
||||
if patches, updateMsgs := policymutation.GenerateJSONPatchesForDefaults(policy, logger); len(patches) != 0 {
|
||||
return admissionutils.ResponseWithMessageAndPatch(true, strings.Join(updateMsgs, "'"), patches)
|
||||
}
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
|
||||
//policyValidation performs the validation check on policy resource
|
||||
func (ws *WebhookServer) policyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := setupLogger(ws.log, "policy validation", request)
|
||||
policy, oldPolicy, err := admissionutils.GetPolicies(request)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to unmarshal policies from admission request")
|
||||
return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to validate policy, check kyverno controller logs for details: %v", err))
|
||||
}
|
||||
if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
|
||||
logger.V(4).Info("skip policy validation on status update")
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
startTime := time.Now()
|
||||
logger.V(3).Info("start policy change validation")
|
||||
defer logger.V(3).Info("finished policy change validation", "time", time.Since(startTime).String())
|
||||
response, err := policyvalidate.Validate(policy, ws.client, false, ws.openAPIController)
|
||||
if err != nil {
|
||||
logger.Error(err, "policy validation errors")
|
||||
return admissionutils.ResponseWithMessage(true, err.Error())
|
||||
}
|
||||
if response != nil && len(response.Warnings) != 0 {
|
||||
return response
|
||||
}
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
|
||||
// resourceMutation mutates resource
|
||||
func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := setupLogger(ws.log, "resource mutation", request)
|
||||
if excludeKyvernoResources(request.Kind.Kind) {
|
||||
return admissionutils.ResponseSuccess(true, "")
|
||||
}
|
||||
if request.Operation == v1beta1.Delete {
|
||||
resource, err := utils.ConvertResource(request.OldObject.Raw, request.Kind.Group, request.Kind.Version, request.Kind.Kind, request.Namespace)
|
||||
if err == nil {
|
||||
ws.prGenerator.Add(buildDeletionPrInfo(resource))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Converting oldObject failed: %v", err))
|
||||
}
|
||||
return admissionutils.ResponseSuccess(true, "")
|
||||
}
|
||||
logger.V(4).Info("received an admission request in mutating webhook")
|
||||
requestTime := time.Now().Unix()
|
||||
mutatePolicies := ws.pCache.GetPolicies(policycache.Mutate, request.Kind.Kind, request.Namespace)
|
||||
verifyImagesPolicies := ws.pCache.GetPolicies(policycache.VerifyImages, request.Kind.Kind, request.Namespace)
|
||||
if len(mutatePolicies) == 0 && len(verifyImagesPolicies) == 0 {
|
||||
logger.V(4).Info("no policies matched admission request")
|
||||
return admissionutils.ResponseSuccess(true, "")
|
||||
}
|
||||
addRoles := containsRBACInfo(mutatePolicies)
|
||||
policyContext, err := ws.buildPolicyContext(request, addRoles)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to build policy context")
|
||||
return admissionutils.ResponseFailure(false, err.Error())
|
||||
}
|
||||
// update container images to a canonical form
|
||||
if err := enginectx.MutateResourceWithImageInfo(request.Object.Raw, policyContext.JSONContext); err != nil {
|
||||
ws.log.Error(err, "failed to patch images info to resource, policies that mutate images may be impacted")
|
||||
}
|
||||
mutatePatches := ws.applyMutatePolicies(request, policyContext, mutatePolicies, requestTime, logger)
|
||||
newRequest := patchRequest(mutatePatches, request, logger)
|
||||
imagePatches, err := ws.applyImageVerifyPolicies(newRequest, policyContext, verifyImagesPolicies, logger)
|
||||
if err != nil {
|
||||
logger.Error(err, "image verification failed")
|
||||
return admissionutils.ResponseFailure(false, err.Error())
|
||||
}
|
||||
var patches = append(mutatePatches, imagePatches...)
|
||||
return admissionutils.ResponseSuccessWithPatch(true, "", patches)
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := setupLogger(ws.log, "resource validation", request)
|
||||
if request.Operation == v1beta1.Delete {
|
||||
ws.handleDelete(request)
|
||||
}
|
||||
if excludeKyvernoResources(request.Kind.Kind) {
|
||||
return admissionutils.ResponseSuccess(true, "")
|
||||
}
|
||||
logger.V(6).Info("received an admission request in validating webhook")
|
||||
// timestamp at which this admission request got triggered
|
||||
requestTime := time.Now().Unix()
|
||||
policies := ws.pCache.GetPolicies(policycache.ValidateEnforce, request.Kind.Kind, "")
|
||||
// Get namespace policies from the cache for the requested resource namespace
|
||||
nsPolicies := ws.pCache.GetPolicies(policycache.ValidateEnforce, request.Kind.Kind, request.Namespace)
|
||||
policies = append(policies, nsPolicies...)
|
||||
generatePolicies := ws.pCache.GetPolicies(policycache.Generate, request.Kind.Kind, request.Namespace)
|
||||
if len(generatePolicies) == 0 && request.Operation == v1beta1.Update {
|
||||
// handle generate source resource updates
|
||||
go ws.handleUpdatesForGenerateRules(request, []kyverno.PolicyInterface{})
|
||||
}
|
||||
var roles, clusterRoles []string
|
||||
if containsRBACInfo(policies, generatePolicies) {
|
||||
var err error
|
||||
roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request, ws.configHandler)
|
||||
if err != nil {
|
||||
return errorResponse(logger, err, "failed to fetch RBAC data")
|
||||
}
|
||||
}
|
||||
userRequestInfo := kyverno.RequestInfo{
|
||||
Roles: roles,
|
||||
ClusterRoles: clusterRoles,
|
||||
AdmissionUserInfo: *request.UserInfo.DeepCopy(),
|
||||
}
|
||||
ctx, err := newVariablesContext(request, &userRequestInfo)
|
||||
if err != nil {
|
||||
return errorResponse(logger, err, "failed create policy rule context")
|
||||
}
|
||||
namespaceLabels := make(map[string]string)
|
||||
if request.Kind.Kind != "Namespace" && request.Namespace != "" {
|
||||
namespaceLabels = common.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, ws.nsLister, logger)
|
||||
}
|
||||
newResource, oldResource, err := utils.ExtractResources(nil, request)
|
||||
if err != nil {
|
||||
return errorResponse(logger, err, "failed create parse resource")
|
||||
}
|
||||
if err := ctx.AddImageInfo(&newResource); err != nil {
|
||||
return errorResponse(logger, err, "failed add image information to policy rule context")
|
||||
}
|
||||
policyContext := &engine.PolicyContext{
|
||||
NewResource: newResource,
|
||||
OldResource: oldResource,
|
||||
AdmissionInfo: userRequestInfo,
|
||||
ExcludeGroupRole: ws.configHandler.GetExcludeGroupRole(),
|
||||
ExcludeResourceFunc: ws.configHandler.ToFilter,
|
||||
JSONContext: ctx,
|
||||
Client: ws.client,
|
||||
}
|
||||
vh := &validationHandler{
|
||||
log: ws.log,
|
||||
eventGen: ws.eventGen,
|
||||
prGenerator: ws.prGenerator,
|
||||
}
|
||||
ok, msg := vh.handleValidation(ws.promConfig, request, policies, policyContext, namespaceLabels, requestTime)
|
||||
if !ok {
|
||||
logger.Info("admission request denied")
|
||||
return admissionutils.ResponseFailure(false, msg)
|
||||
}
|
||||
// push admission request to audit handler, this won't block the admission request
|
||||
ws.auditHandler.Add(request.DeepCopy())
|
||||
// process generate policies
|
||||
ws.applyGeneratePolicies(request, policyContext, generatePolicies, requestTime, logger)
|
||||
return admissionutils.ResponseSuccess(true, "")
|
||||
}
|
96
pkg/webhooks/handlers/admission.go
Normal file
96
pkg/webhooks/handlers/admission.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/kyverno/kyverno/pkg/config"
|
||||
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
|
||||
"github.com/kyverno/kyverno/pkg/webhookconfig"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
type AdmissionHandler func(*v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse
|
||||
|
||||
func Admission(logger logr.Logger, inner AdmissionHandler) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
startTime := time.Now()
|
||||
if request.Body == nil {
|
||||
logger.Info("empty body", "req", request.URL.String())
|
||||
http.Error(writer, "empty body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer request.Body.Close()
|
||||
body, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
logger.Info("failed to read HTTP body", "req", request.URL.String())
|
||||
http.Error(writer, "failed to read HTTP body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
contentType := request.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
logger.Info("invalid Content-Type", "contextType", contentType)
|
||||
http.Error(writer, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
admissionReview := &v1beta1.AdmissionReview{}
|
||||
if err := json.Unmarshal(body, &admissionReview); err != nil {
|
||||
logger.Error(err, "failed to decode request body to type 'AdmissionReview")
|
||||
http.Error(writer, "Can't decode body as AdmissionReview", http.StatusExpectationFailed)
|
||||
return
|
||||
}
|
||||
logger = logger.WithName("handlerFunc").WithValues(
|
||||
"kind", admissionReview.Request.Kind,
|
||||
"namespace", admissionReview.Request.Namespace,
|
||||
"name", admissionReview.Request.Name,
|
||||
"operation", admissionReview.Request.Operation,
|
||||
"uid", admissionReview.Request.UID,
|
||||
)
|
||||
admissionReview.Response = &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
UID: admissionReview.Request.UID,
|
||||
}
|
||||
adminssionResponse := inner(admissionReview.Request)
|
||||
if adminssionResponse != nil {
|
||||
admissionReview.Response = adminssionResponse
|
||||
}
|
||||
responseJSON, err := json.Marshal(admissionReview)
|
||||
if err != nil {
|
||||
http.Error(writer, fmt.Sprintf("Could not encode response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := writer.Write(responseJSON); err != nil {
|
||||
http.Error(writer, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
logger.V(4).Info("admission review request processed", "time", time.Since(startTime).String())
|
||||
}
|
||||
}
|
||||
|
||||
func Filter(c config.Interface, inner AdmissionHandler) AdmissionHandler {
|
||||
return func(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
if c.ToFilter(request.Kind.Kind, request.Namespace, request.Name) {
|
||||
return nil
|
||||
}
|
||||
return inner(request)
|
||||
}
|
||||
}
|
||||
|
||||
func Verify(m *webhookconfig.Monitor, logger logr.Logger) AdmissionHandler {
|
||||
return func(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger = logger.WithName("verifyHandler").WithValues(
|
||||
"action", "verify",
|
||||
"kind", request.Kind,
|
||||
"namespace", request.Namespace,
|
||||
"name", request.Name,
|
||||
"operation", request.Operation,
|
||||
"gvk", request.Kind.String(),
|
||||
)
|
||||
logger.V(3).Info("incoming request", "last admission request timestamp", m.Time())
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
}
|
15
pkg/webhooks/handlers/monitor.go
Normal file
15
pkg/webhooks/handlers/monitor.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/kyverno/kyverno/pkg/webhookconfig"
|
||||
)
|
||||
|
||||
func Monitor(m *webhookconfig.Monitor, inner http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
m.SetTime(time.Now())
|
||||
inner(w, r)
|
||||
}
|
||||
}
|
14
pkg/webhooks/handlers/probe.go
Normal file
14
pkg/webhooks/handlers/probe.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package handlers
|
||||
|
||||
import "net/http"
|
||||
|
||||
func Probe(check func() error) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if check != nil {
|
||||
if err := check(); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
120
pkg/webhooks/metrics.go
Normal file
120
pkg/webhooks/metrics.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/go-logr/logr"
|
||||
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
"github.com/kyverno/kyverno/pkg/engine/response"
|
||||
"github.com/kyverno/kyverno/pkg/metrics"
|
||||
admissionRequests "github.com/kyverno/kyverno/pkg/metrics/admissionrequests"
|
||||
admissionReviewDuration "github.com/kyverno/kyverno/pkg/metrics/admissionreviewduration"
|
||||
policyExecutionDuration "github.com/kyverno/kyverno/pkg/metrics/policyexecutionduration"
|
||||
policyResults "github.com/kyverno/kyverno/pkg/metrics/policyresults"
|
||||
)
|
||||
|
||||
func registerAdmissionReviewDurationMetricMutate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, engineResponses []*response.EngineResponse, admissionReviewLatencyDuration int64) {
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
if err := admissionReviewDuration.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, admissionReviewLatencyDuration, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionRequestsMetricMutate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, engineResponses []*response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
if err := admissionRequests.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionReviewDurationMetricGenerate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, latencyReceiver *chan int64, engineResponsesReceiver *chan []*response.EngineResponse) {
|
||||
defer close(*latencyReceiver)
|
||||
defer close(*engineResponsesReceiver)
|
||||
engineResponses := <-(*engineResponsesReceiver)
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
// this goroutine will keep on waiting here till it doesn't receive the admission review latency int64 from the other goroutine i.e. ws.HandleGenerate
|
||||
admissionReviewLatencyDuration := <-(*latencyReceiver)
|
||||
if err := admissionReviewDuration.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, admissionReviewLatencyDuration, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionRequestsMetricGenerate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, engineResponsesReceiver *chan []*response.EngineResponse) {
|
||||
defer close(*engineResponsesReceiver)
|
||||
engineResponses := <-(*engineResponsesReceiver)
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
if err := admissionRequests.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerPolicyResultsMetricValidation(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, policy v1.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyResults.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyResults.ParsePromConfig(*promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func registerPolicyExecutionDurationMetricValidate(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, policy v1.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyExecutionDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyExecutionDuration.ParsePromConfig(*promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, "", resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionReviewDurationMetricValidate(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, engineResponses []*response.EngineResponse, admissionReviewLatencyDuration int64) {
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
if err := admissionReviewDuration.ParsePromConfig(*promConfig).ProcessEngineResponses(engineResponses, admissionReviewLatencyDuration, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionRequestsMetricValidate(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, engineResponses []*response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := admissionRequests.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
if err := admissionRequests.ParsePromConfig(*promConfig).ProcessEngineResponses(engineResponses, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) registerPolicyResultsMetricMutation(logger logr.Logger, resourceRequestOperation string, policy kyverno.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyResults.ParseResourceRequestOperation(resourceRequestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyResults.ParsePromConfig(*ws.promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) registerPolicyExecutionDurationMetricMutate(logger logr.Logger, resourceRequestOperation string, policy kyverno.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyExecutionDuration.ParseResourceRequestOperation(resourceRequestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyExecutionDuration.ParsePromConfig(*ws.promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, "", resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
|
@ -12,9 +12,6 @@ import (
|
|||
"github.com/kyverno/kyverno/pkg/engine"
|
||||
"github.com/kyverno/kyverno/pkg/engine/response"
|
||||
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
|
||||
"github.com/kyverno/kyverno/pkg/metrics"
|
||||
policyExecutionDuration "github.com/kyverno/kyverno/pkg/metrics/policyexecutionduration"
|
||||
policyResults "github.com/kyverno/kyverno/pkg/metrics/policyresults"
|
||||
"github.com/kyverno/kyverno/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
|
@ -159,23 +156,3 @@ func (ws *WebhookServer) applyMutation(request *v1beta1.AdmissionRequest, policy
|
|||
|
||||
return engineResponse, policyPatches, nil
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) registerPolicyResultsMetricMutation(logger logr.Logger, resourceRequestOperation string, policy kyverno.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyResults.ParseResourceRequestOperation(resourceRequestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyResults.ParsePromConfig(*ws.promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) registerPolicyExecutionDurationMetricMutate(logger logr.Logger, resourceRequestOperation string, policy kyverno.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyExecutionDuration.ParseResourceRequestOperation(resourceRequestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyExecutionDuration.ParsePromConfig(*ws.promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, "", resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
"github.com/kyverno/kyverno/pkg/policymutation"
|
||||
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
func (ws *WebhookServer) policyMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := ws.log.WithValues("action", "policy mutation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation, "gvk", request.Kind.String())
|
||||
policy, oldPolicy, err := admissionutils.GetPolicies(request)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to unmarshal policies from admission request")
|
||||
return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to default value, check kyverno controller logs for details: %v", err))
|
||||
}
|
||||
if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
|
||||
logger.V(4).Info("skip policy mutation on status update")
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
startTime := time.Now()
|
||||
logger.V(3).Info("start policy change mutation")
|
||||
defer logger.V(3).Info("finished policy change mutation", "time", time.Since(startTime).String())
|
||||
// Generate JSON Patches for defaults
|
||||
if patches, updateMsgs := policymutation.GenerateJSONPatchesForDefaults(policy, logger); len(patches) != 0 {
|
||||
return admissionutils.ResponseWithMessageAndPatch(true, strings.Join(updateMsgs, "'"), patches)
|
||||
}
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
|
||||
func isStatusUpdate(old, new kyverno.PolicyInterface) bool {
|
||||
if !reflect.DeepEqual(old.GetAnnotations(), new.GetAnnotations()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(old.GetLabels(), new.GetLabels()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(old.GetSpec(), new.GetSpec()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
policyvalidate "github.com/kyverno/kyverno/pkg/policy"
|
||||
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
//policyValidation performs the validation check on policy resource
|
||||
func (ws *WebhookServer) policyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := ws.log.WithValues("action", "policy validation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation, "gvk", request.Kind.String())
|
||||
policy, oldPolicy, err := admissionutils.GetPolicies(request)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to unmarshal policies from admission request")
|
||||
return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to validate policy, check kyverno controller logs for details: %v", err))
|
||||
}
|
||||
if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
|
||||
logger.V(4).Info("skip policy validation on status update")
|
||||
return admissionutils.Response(true)
|
||||
}
|
||||
startTime := time.Now()
|
||||
logger.V(3).Info("start policy change validation")
|
||||
defer logger.V(3).Info("finished policy change validation", "time", time.Since(startTime).String())
|
||||
response, err := policyvalidate.Validate(policy, ws.client, false, ws.openAPIController)
|
||||
if err != nil {
|
||||
logger.Error(err, "policy validation errors")
|
||||
return admissionutils.ResponseWithMessage(true, err.Error())
|
||||
}
|
||||
if response != nil && len(response.Warnings) != 0 {
|
||||
return response
|
||||
}
|
||||
return admissionutils.Response(true)
|
||||
}
|
|
@ -3,9 +3,6 @@ package webhooks
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -16,17 +13,12 @@ import (
|
|||
kyvernoclient "github.com/kyverno/kyverno/pkg/client/clientset/versioned"
|
||||
kyvernoinformer "github.com/kyverno/kyverno/pkg/client/informers/externalversions/kyverno/v1"
|
||||
kyvernolister "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v1"
|
||||
"github.com/kyverno/kyverno/pkg/common"
|
||||
"github.com/kyverno/kyverno/pkg/config"
|
||||
client "github.com/kyverno/kyverno/pkg/dclient"
|
||||
"github.com/kyverno/kyverno/pkg/engine"
|
||||
enginectx "github.com/kyverno/kyverno/pkg/engine/context"
|
||||
"github.com/kyverno/kyverno/pkg/engine/response"
|
||||
"github.com/kyverno/kyverno/pkg/event"
|
||||
"github.com/kyverno/kyverno/pkg/generate"
|
||||
"github.com/kyverno/kyverno/pkg/metrics"
|
||||
admissionRequests "github.com/kyverno/kyverno/pkg/metrics/admissionrequests"
|
||||
admissionReviewDuration "github.com/kyverno/kyverno/pkg/metrics/admissionreviewduration"
|
||||
"github.com/kyverno/kyverno/pkg/openapi"
|
||||
"github.com/kyverno/kyverno/pkg/policycache"
|
||||
"github.com/kyverno/kyverno/pkg/policyreport"
|
||||
|
@ -35,9 +27,9 @@ import (
|
|||
"github.com/kyverno/kyverno/pkg/utils"
|
||||
"github.com/kyverno/kyverno/pkg/webhookconfig"
|
||||
webhookgenerate "github.com/kyverno/kyverno/pkg/webhooks/generate"
|
||||
"github.com/kyverno/kyverno/pkg/webhooks/handlers"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
informers "k8s.io/client-go/informers/core/v1"
|
||||
rbacinformer "k8s.io/client-go/informers/rbac/v1"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
|
@ -156,32 +148,26 @@ func NewWebhookServer(
|
|||
grc *generate.Controller,
|
||||
promConfig *metrics.PromConfig,
|
||||
) (*WebhookServer, error) {
|
||||
|
||||
if tlsPair == nil {
|
||||
return nil, errors.New("NewWebhookServer is not initialized properly")
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
pair, err := tls.X509KeyPair(tlsPair.Certificate, tlsPair.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{pair}
|
||||
|
||||
ws := &WebhookServer{
|
||||
client: client,
|
||||
kyvernoClient: kyvernoClient,
|
||||
grLister: grInformer.Lister().GenerateRequests(config.KyvernoNamespace),
|
||||
grSynced: grInformer.Informer().HasSynced,
|
||||
pLister: pInformer.Lister(),
|
||||
pSynced: pInformer.Informer().HasSynced,
|
||||
rbLister: rbInformer.Lister(),
|
||||
rbSynced: rbInformer.Informer().HasSynced,
|
||||
rLister: rInformer.Lister(),
|
||||
rSynced: rInformer.Informer().HasSynced,
|
||||
nsLister: namespace.Lister(),
|
||||
nsListerSynced: namespace.Informer().HasSynced,
|
||||
|
||||
client: client,
|
||||
kyvernoClient: kyvernoClient,
|
||||
grLister: grInformer.Lister().GenerateRequests(config.KyvernoNamespace),
|
||||
grSynced: grInformer.Informer().HasSynced,
|
||||
pLister: pInformer.Lister(),
|
||||
pSynced: pInformer.Informer().HasSynced,
|
||||
rbLister: rbInformer.Lister(),
|
||||
rbSynced: rbInformer.Informer().HasSynced,
|
||||
rLister: rInformer.Lister(),
|
||||
rSynced: rInformer.Informer().HasSynced,
|
||||
nsLister: namespace.Lister(),
|
||||
nsListerSynced: namespace.Informer().HasSynced,
|
||||
crbLister: crbInformer.Lister(),
|
||||
crLister: crInformer.Lister(),
|
||||
crbSynced: crbInformer.Informer().HasSynced,
|
||||
|
@ -200,153 +186,24 @@ func NewWebhookServer(
|
|||
openAPIController: openAPIController,
|
||||
promConfig: promConfig,
|
||||
}
|
||||
|
||||
mux := httprouter.New()
|
||||
mux.HandlerFunc("POST", config.MutatingWebhookServicePath, ws.handlerFunc(ws.resourceMutation, true))
|
||||
mux.HandlerFunc("POST", config.ValidatingWebhookServicePath, ws.handlerFunc(ws.resourceValidation, true))
|
||||
mux.HandlerFunc("POST", config.PolicyMutatingWebhookServicePath, ws.handlerFunc(ws.policyMutation, true))
|
||||
mux.HandlerFunc("POST", config.PolicyValidatingWebhookServicePath, ws.handlerFunc(ws.policyValidation, true))
|
||||
mux.HandlerFunc("POST", config.VerifyMutatingWebhookServicePath, ws.handlerFunc(ws.verifyHandler, false))
|
||||
|
||||
// Patch Liveness responds to a Kubernetes Liveness probe
|
||||
// Fail this request if Kubernetes should restart this instance
|
||||
mux.HandlerFunc("GET", config.LivenessServicePath, func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
if err := ws.webhookRegister.Check(); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
// Patch Readiness responds to a Kubernetes Readiness probe
|
||||
// Fail this request if this instance can't accept traffic, but Kubernetes shouldn't restart it
|
||||
mux.HandlerFunc("GET", config.ReadinessServicePath, func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mux.HandlerFunc("POST", config.MutatingWebhookServicePath, ws.admissionHandler(true, ws.resourceMutation))
|
||||
mux.HandlerFunc("POST", config.ValidatingWebhookServicePath, ws.admissionHandler(true, ws.resourceValidation))
|
||||
mux.HandlerFunc("POST", config.PolicyMutatingWebhookServicePath, ws.admissionHandler(true, ws.policyMutation))
|
||||
mux.HandlerFunc("POST", config.PolicyValidatingWebhookServicePath, ws.admissionHandler(true, ws.policyValidation))
|
||||
mux.HandlerFunc("POST", config.VerifyMutatingWebhookServicePath, ws.admissionHandler(false, handlers.Verify(ws.webhookMonitor, ws.log)))
|
||||
mux.HandlerFunc("GET", config.LivenessServicePath, handlers.Probe(ws.webhookRegister.Check))
|
||||
mux.HandlerFunc("GET", config.ReadinessServicePath, handlers.Probe(nil))
|
||||
ws.server = &http.Server{
|
||||
Addr: ":9443", // Listen on port for HTTPS requests
|
||||
TLSConfig: &tlsConfig,
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{pair}},
|
||||
Handler: mux,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) handlerFunc(handler func(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse, filter bool) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
ws.webhookMonitor.SetTime(startTime)
|
||||
|
||||
admissionReview := ws.bodyToAdmissionReview(r, rw)
|
||||
if admissionReview == nil {
|
||||
ws.log.Info("failed to parse admission review request", "request", r)
|
||||
return
|
||||
}
|
||||
|
||||
logger := ws.log.WithName("handlerFunc").WithValues("kind", admissionReview.Request.Kind, "namespace", admissionReview.Request.Namespace,
|
||||
"name", admissionReview.Request.Name, "operation", admissionReview.Request.Operation, "uid", admissionReview.Request.UID)
|
||||
|
||||
admissionReview.Response = &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
UID: admissionReview.Request.UID,
|
||||
}
|
||||
|
||||
// Do not process the admission requests for kinds that are in filterKinds for filtering
|
||||
request := admissionReview.Request
|
||||
if filter && ws.configHandler.ToFilter(request.Kind.Kind, request.Namespace, request.Name) {
|
||||
writeResponse(rw, admissionReview)
|
||||
return
|
||||
}
|
||||
|
||||
admissionReview.Response = handler(request)
|
||||
writeResponse(rw, admissionReview)
|
||||
logger.V(4).Info("admission review request processed", "time", time.Since(startTime).String())
|
||||
}
|
||||
}
|
||||
|
||||
func writeResponse(rw http.ResponseWriter, admissionReview *v1beta1.AdmissionReview) {
|
||||
responseJSON, err := json.Marshal(admissionReview)
|
||||
if err != nil {
|
||||
http.Error(rw, fmt.Sprintf("Could not encode response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := rw.Write(responseJSON); err != nil {
|
||||
http.Error(rw, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// resourceMutation mutates resource
|
||||
func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := ws.log.WithName("MutateWebhook").WithValues("uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation, "gvk", request.Kind.String())
|
||||
|
||||
if excludeKyvernoResources(request.Kind.Kind) {
|
||||
return successResponse(nil)
|
||||
}
|
||||
|
||||
if request.Operation == v1beta1.Delete {
|
||||
resource, err := utils.ConvertResource(request.OldObject.Raw, request.Kind.Group, request.Kind.Version, request.Kind.Kind, request.Namespace)
|
||||
|
||||
if err == nil {
|
||||
ws.prGenerator.Add(buildDeletionPrInfo(resource))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Converting oldObject failed: %v", err))
|
||||
}
|
||||
|
||||
return successResponse(nil)
|
||||
}
|
||||
|
||||
logger.V(4).Info("received an admission request in mutating webhook")
|
||||
requestTime := time.Now().Unix()
|
||||
kind := request.Kind.Kind
|
||||
mutatePolicies := ws.pCache.GetPolicies(policycache.Mutate, kind, request.Namespace)
|
||||
verifyImagesPolicies := ws.pCache.GetPolicies(policycache.VerifyImages, kind, request.Namespace)
|
||||
|
||||
if len(mutatePolicies) == 0 && len(verifyImagesPolicies) == 0 {
|
||||
logger.V(4).Info("no policies matched admission request")
|
||||
return successResponse(nil)
|
||||
}
|
||||
|
||||
addRoles := containsRBACInfo(mutatePolicies)
|
||||
policyContext, err := ws.buildPolicyContext(request, addRoles)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to build policy context")
|
||||
return failureResponse(err.Error())
|
||||
}
|
||||
|
||||
// update container images to a canonical form
|
||||
if err := enginectx.MutateResourceWithImageInfo(request.Object.Raw, policyContext.JSONContext); err != nil {
|
||||
ws.log.Error(err, "failed to patch images info to resource, policies that mutate images may be impacted")
|
||||
}
|
||||
|
||||
mutatePatches := ws.applyMutatePolicies(request, policyContext, mutatePolicies, requestTime, logger)
|
||||
|
||||
newRequest := patchRequest(mutatePatches, request, logger)
|
||||
imagePatches, err := ws.applyImageVerifyPolicies(newRequest, policyContext, verifyImagesPolicies, logger)
|
||||
if err != nil {
|
||||
logger.Error(err, "image verification failed")
|
||||
return failureResponse(err.Error())
|
||||
}
|
||||
|
||||
var patches = append(mutatePatches, imagePatches...)
|
||||
return successResponse(patches)
|
||||
}
|
||||
|
||||
// patchRequest applies patches to the request.Object and returns a new copy of the request
|
||||
func patchRequest(patches []byte, request *v1beta1.AdmissionRequest, logger logr.Logger) *v1beta1.AdmissionRequest {
|
||||
patchedResource := processResourceWithPatches(patches, request.Object.Raw, logger)
|
||||
newRequest := request.DeepCopy()
|
||||
newRequest.Object.Raw = patchedResource
|
||||
return newRequest
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) buildPolicyContext(request *v1beta1.AdmissionRequest, addRoles bool) (*engine.PolicyContext, error) {
|
||||
userRequestInfo := v1.RequestInfo{
|
||||
AdmissionUserInfo: *request.UserInfo.DeepCopy(),
|
||||
|
@ -398,269 +255,32 @@ func (ws *WebhookServer) buildPolicyContext(request *v1beta1.AdmissionRequest, a
|
|||
return policyContext, nil
|
||||
}
|
||||
|
||||
func successResponse(patch []byte) *v1beta1.AdmissionResponse {
|
||||
r := &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Result: &metav1.Status{
|
||||
Status: "Success",
|
||||
},
|
||||
}
|
||||
|
||||
if len(patch) > 0 {
|
||||
patchType := v1beta1.PatchTypeJSONPatch
|
||||
r.PatchType = &patchType
|
||||
r.Patch = patch
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func errorResponse(logger logr.Logger, err error, message string) *v1beta1.AdmissionResponse {
|
||||
logger.Error(err, message)
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: "Failure",
|
||||
Message: message + ": " + err.Error(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func failureResponse(message string) *v1beta1.AdmissionResponse {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: "Failure",
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionReviewDurationMetricMutate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, engineResponses []*response.EngineResponse, admissionReviewLatencyDuration int64) {
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
if err := admissionReviewDuration.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, admissionReviewLatencyDuration, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionRequestsMetricMutate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, engineResponses []*response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
if err := admissionRequests.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionReviewDurationMetricGenerate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, latencyReceiver *chan int64, engineResponsesReceiver *chan []*response.EngineResponse) {
|
||||
defer close(*latencyReceiver)
|
||||
defer close(*engineResponsesReceiver)
|
||||
|
||||
engineResponses := <-(*engineResponsesReceiver)
|
||||
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
// this goroutine will keep on waiting here till it doesn't receive the admission review latency int64 from the other goroutine i.e. ws.HandleGenerate
|
||||
admissionReviewLatencyDuration := <-(*latencyReceiver)
|
||||
if err := admissionReviewDuration.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, admissionReviewLatencyDuration, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionRequestsMetricGenerate(logger logr.Logger, promConfig metrics.PromConfig, requestOperation string, engineResponsesReceiver *chan []*response.EngineResponse) {
|
||||
defer close(*engineResponsesReceiver)
|
||||
engineResponses := <-(*engineResponsesReceiver)
|
||||
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
if err := admissionRequests.ParsePromConfig(promConfig).ProcessEngineResponses(engineResponses, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
||||
logger := ws.log.WithName("ValidateWebhook").WithValues("uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation)
|
||||
|
||||
if request.Operation == v1beta1.Delete {
|
||||
ws.handleDelete(request)
|
||||
}
|
||||
|
||||
if excludeKyvernoResources(request.Kind.Kind) {
|
||||
return successResponse(nil)
|
||||
}
|
||||
|
||||
logger.V(6).Info("received an admission request in validating webhook")
|
||||
// timestamp at which this admission request got triggered
|
||||
admissionRequestTimestamp := time.Now().Unix()
|
||||
kind := request.Kind.Kind
|
||||
policies := ws.pCache.GetPolicies(policycache.ValidateEnforce, kind, "")
|
||||
// Get namespace policies from the cache for the requested resource namespace
|
||||
nsPolicies := ws.pCache.GetPolicies(policycache.ValidateEnforce, kind, request.Namespace)
|
||||
policies = append(policies, nsPolicies...)
|
||||
generatePolicies := ws.pCache.GetPolicies(policycache.Generate, kind, request.Namespace)
|
||||
|
||||
if len(generatePolicies) == 0 && request.Operation == v1beta1.Update {
|
||||
// handle generate source resource updates
|
||||
go ws.handleUpdatesForGenerateRules(request, []v1.PolicyInterface{})
|
||||
}
|
||||
|
||||
var roles, clusterRoles []string
|
||||
if containsRBACInfo(policies, generatePolicies) {
|
||||
var err error
|
||||
roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request, ws.configHandler)
|
||||
if err != nil {
|
||||
return errorResponse(logger, err, "failed to fetch RBAC data")
|
||||
}
|
||||
}
|
||||
|
||||
userRequestInfo := v1.RequestInfo{
|
||||
Roles: roles,
|
||||
ClusterRoles: clusterRoles,
|
||||
AdmissionUserInfo: *request.UserInfo.DeepCopy(),
|
||||
}
|
||||
|
||||
ctx, err := newVariablesContext(request, &userRequestInfo)
|
||||
if err != nil {
|
||||
return errorResponse(logger, err, "failed create policy rule context")
|
||||
}
|
||||
|
||||
namespaceLabels := make(map[string]string)
|
||||
if request.Kind.Kind != "Namespace" && request.Namespace != "" {
|
||||
namespaceLabels = common.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, ws.nsLister, logger)
|
||||
}
|
||||
|
||||
newResource, oldResource, err := utils.ExtractResources(nil, request)
|
||||
if err != nil {
|
||||
return errorResponse(logger, err, "failed create parse resource")
|
||||
}
|
||||
|
||||
if err := ctx.AddImageInfo(&newResource); err != nil {
|
||||
return errorResponse(logger, err, "failed add image information to policy rule context")
|
||||
}
|
||||
|
||||
policyContext := &engine.PolicyContext{
|
||||
NewResource: newResource,
|
||||
OldResource: oldResource,
|
||||
AdmissionInfo: userRequestInfo,
|
||||
ExcludeGroupRole: ws.configHandler.GetExcludeGroupRole(),
|
||||
ExcludeResourceFunc: ws.configHandler.ToFilter,
|
||||
JSONContext: ctx,
|
||||
Client: ws.client,
|
||||
}
|
||||
|
||||
vh := &validationHandler{
|
||||
log: ws.log,
|
||||
eventGen: ws.eventGen,
|
||||
prGenerator: ws.prGenerator,
|
||||
}
|
||||
|
||||
ok, msg := vh.handleValidation(ws.promConfig, request, policies, policyContext, namespaceLabels, admissionRequestTimestamp)
|
||||
if !ok {
|
||||
logger.Info("admission request denied")
|
||||
return failureResponse(msg)
|
||||
}
|
||||
|
||||
// push admission request to audit handler, this won't block the admission request
|
||||
ws.auditHandler.Add(request.DeepCopy())
|
||||
|
||||
// process generate policies
|
||||
ws.applyGeneratePolicies(request, policyContext, generatePolicies, admissionRequestTimestamp, logger)
|
||||
|
||||
return successResponse(nil)
|
||||
}
|
||||
|
||||
// RunAsync TLS server in separate thread and returns control immediately
|
||||
func (ws *WebhookServer) RunAsync(stopCh <-chan struct{}) {
|
||||
logger := ws.log
|
||||
if !cache.WaitForCacheSync(stopCh, ws.grSynced, ws.pSynced, ws.rbSynced, ws.crbSynced, ws.rSynced, ws.crSynced) {
|
||||
logger.Info("failed to sync informer cache")
|
||||
ws.log.Info("failed to sync informer cache")
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.V(3).Info("started serving requests", "addr", ws.server.Addr)
|
||||
ws.log.V(3).Info("started serving requests", "addr", ws.server.Addr)
|
||||
if err := ws.server.ListenAndServeTLS("", ""); err != http.ErrServerClosed {
|
||||
logger.Error(err, "failed to listen to requests")
|
||||
ws.log.Error(err, "failed to listen to requests")
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info("starting service")
|
||||
|
||||
ws.log.Info("starting service")
|
||||
}
|
||||
|
||||
// Stop TLS server and returns control after the server is shut down
|
||||
func (ws *WebhookServer) Stop(ctx context.Context) {
|
||||
logger := ws.log
|
||||
|
||||
// remove the static webhook configurations
|
||||
go ws.webhookRegister.Remove(ws.cleanUp)
|
||||
|
||||
// shutdown http.Server with context timeout
|
||||
err := ws.server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
// Error from closing listeners, or context timeout:
|
||||
logger.Error(err, "shutting down server")
|
||||
ws.log.Error(err, "shutting down server")
|
||||
err = ws.server.Close()
|
||||
if err != nil {
|
||||
logger.Error(err, "server shut down failed")
|
||||
ws.log.Error(err, "server shut down failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bodyToAdmissionReview creates AdmissionReview object from request body
|
||||
// Answers to the http.ResponseWriter if request is not valid
|
||||
func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview {
|
||||
logger := ws.log
|
||||
if request.Body == nil {
|
||||
logger.Info("empty body", "req", request.URL.String())
|
||||
http.Error(writer, "empty body", http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
defer request.Body.Close()
|
||||
body, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
logger.Info("failed to read HTTP body", "req", request.URL.String())
|
||||
http.Error(writer, "failed to read HTTP body", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
contentType := request.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
logger.Info("invalid Content-Type", "contextType", contentType)
|
||||
http.Error(writer, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
|
||||
return nil
|
||||
}
|
||||
|
||||
admissionReview := &v1beta1.AdmissionReview{}
|
||||
if err := json.Unmarshal(body, &admissionReview); err != nil {
|
||||
logger.Error(err, "failed to decode request body to type 'AdmissionReview")
|
||||
http.Error(writer, "Can't decode body as AdmissionReview", http.StatusExpectationFailed)
|
||||
return nil
|
||||
}
|
||||
|
||||
return admissionReview
|
||||
}
|
||||
|
||||
func newVariablesContext(request *v1beta1.AdmissionRequest, userRequestInfo *v1.RequestInfo) (*enginectx.Context, error) {
|
||||
ctx := enginectx.NewContext()
|
||||
if err := ctx.AddRequest(request); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load incoming request in context")
|
||||
}
|
||||
|
||||
if err := ctx.AddUserInfo(*userRequestInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load userInfo in context")
|
||||
}
|
||||
|
||||
if err := ctx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load service account in context")
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@ import (
|
|||
"github.com/kyverno/kyverno/pkg/engine"
|
||||
"github.com/kyverno/kyverno/pkg/engine/response"
|
||||
"github.com/kyverno/kyverno/pkg/metrics"
|
||||
admissionRequests "github.com/kyverno/kyverno/pkg/metrics/admissionrequests"
|
||||
admissionReviewDuration "github.com/kyverno/kyverno/pkg/metrics/admissionreviewduration"
|
||||
policyExecutionDuration "github.com/kyverno/kyverno/pkg/metrics/policyexecutionduration"
|
||||
policyResults "github.com/kyverno/kyverno/pkg/metrics/policyresults"
|
||||
"github.com/kyverno/kyverno/pkg/policyreport"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -150,46 +146,6 @@ func getResourceName(request *v1beta1.AdmissionRequest) string {
|
|||
return resourceName
|
||||
}
|
||||
|
||||
func registerPolicyResultsMetricValidation(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, policy v1.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyResults.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyResults.ParsePromConfig(*promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_results_total metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func registerPolicyExecutionDurationMetricValidate(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, policy v1.PolicyInterface, engineResponse response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := policyExecutionDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
if err := policyExecutionDuration.ParsePromConfig(*promConfig).ProcessEngineResponse(policy, engineResponse, metrics.AdmissionRequest, "", resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_policy_execution_duration_seconds metrics for the above policy", "name", policy.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionReviewDurationMetricValidate(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, engineResponses []*response.EngineResponse, admissionReviewLatencyDuration int64) {
|
||||
resourceRequestOperationPromAlias, err := admissionReviewDuration.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
if err := admissionReviewDuration.ParsePromConfig(*promConfig).ProcessEngineResponses(engineResponses, admissionReviewLatencyDuration, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_review_duration_seconds metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdmissionRequestsMetricValidate(promConfig *metrics.PromConfig, logger logr.Logger, requestOperation string, engineResponses []*response.EngineResponse) {
|
||||
resourceRequestOperationPromAlias, err := admissionRequests.ParseResourceRequestOperation(requestOperation)
|
||||
if err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
if err := admissionRequests.ParsePromConfig(*promConfig).ProcessEngineResponses(engineResponses, resourceRequestOperationPromAlias); err != nil {
|
||||
logger.Error(err, "error occurred while registering kyverno_admission_requests_total metrics")
|
||||
}
|
||||
}
|
||||
|
||||
func buildDeletionPrInfo(oldR unstructured.Unstructured) policyreport.Info {
|
||||
return policyreport.Info{
|
||||
Namespace: oldR.GetNamespace(),
|
||||
|
|
Loading…
Reference in a new issue