1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-04-18 02:06:52 +00:00

feat: add global context entry validation webhook ()

* feat: add global context entry validation webhook

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: use `k8s.io/apimachinery/pkg/util/json` instead of `encoding/json`

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: lint

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

---------

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
This commit is contained in:
Vishal Choudhary 2024-02-02 22:34:50 +05:30 committed by GitHub
parent 2b712107d2
commit 3142af64a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 420 additions and 14 deletions

View file

@ -38,8 +38,10 @@ import (
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
runtimeutils "github.com/kyverno/kyverno/pkg/utils/runtime"
"github.com/kyverno/kyverno/pkg/validation/exception"
"github.com/kyverno/kyverno/pkg/validation/globalcontext"
"github.com/kyverno/kyverno/pkg/webhooks"
webhooksexception "github.com/kyverno/kyverno/pkg/webhooks/exception"
webhooksglobalcontext "github.com/kyverno/kyverno/pkg/webhooks/globalcontext"
webhookspolicy "github.com/kyverno/kyverno/pkg/webhooks/policy"
webhooksresource "github.com/kyverno/kyverno/pkg/webhooks/resource"
webhookgenerate "github.com/kyverno/kyverno/pkg/webhooks/updaterequest"
@ -56,6 +58,7 @@ import (
const (
resyncPeriod = 15 * time.Minute
exceptionWebhookControllerName = "exception-webhook-controller"
gctxWebhookControllerName = "global-context-webhook-controller"
)
var (
@ -183,9 +186,37 @@ func createrLeaderControllers(
configuration,
caSecretName,
)
gctxWebhookController := genericwebhookcontroller.NewController(
gctxWebhookControllerName,
kubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations(),
kubeInformer.Admissionregistration().V1().ValidatingWebhookConfigurations(),
caInformer,
config.GlobalContextValidatingWebhookConfigurationName,
config.GlobalContextValidatingWebhookServicePath,
serverIP,
servicePort,
webhookServerPort,
nil,
[]admissionregistrationv1.RuleWithOperations{{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"kyverno.io"},
APIVersions: []string{"v2alpha1"},
Resources: []string{"globalcontextentries"},
},
Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
admissionregistrationv1.Update,
},
}},
genericwebhookcontroller.Fail,
genericwebhookcontroller.None,
configuration,
caSecretName,
)
leaderControllers = append(leaderControllers, internal.NewController(certmanager.ControllerName, certManager, certmanager.Workers))
leaderControllers = append(leaderControllers, internal.NewController(webhookcontroller.ControllerName, webhookController, webhookcontroller.Workers))
leaderControllers = append(leaderControllers, internal.NewController(exceptionWebhookControllerName, exceptionWebhookController, 1))
leaderControllers = append(leaderControllers, internal.NewController(gctxWebhookControllerName, gctxWebhookController, 1))
if generateVAPs {
checker := checker.NewSelfChecker(kubeClient.AuthorizationV1().SelfSubjectAccessReviews())
@ -498,11 +529,15 @@ func main() {
Enabled: internal.PolicyExceptionEnabled(),
Namespace: internal.ExceptionNamespace(),
})
globalContextHandlers := webhooksglobalcontext.NewHandlers(globalcontext.ValidationOptions{
Enabled: internal.PolicyExceptionEnabled(),
})
server := webhooks.NewServer(
signalCtx,
policyHandlers,
resourceHandlers,
exceptionHandlers,
globalContextHandlers,
setup.Configuration,
setup.MetricsManager,
webhooks.DebugModeOptions{

View file

@ -24,6 +24,8 @@ const (
ValidatingWebhookConfigurationName = "kyverno-resource-validating-webhook-cfg"
// ExceptionValidatingWebhookConfigurationName ...
ExceptionValidatingWebhookConfigurationName = "kyverno-exception-validating-webhook-cfg"
// GlobalContextValidatingWebhookConfigurationName ...
GlobalContextValidatingWebhookConfigurationName = "kyverno-global-context-validating-webhook-cfg"
// CleanupValidatingWebhookConfigurationName ...
CleanupValidatingWebhookConfigurationName = "kyverno-cleanup-validating-webhook-cfg"
// PolicyMutatingWebhookConfigurationName default policy mutating webhook configuration name
@ -58,6 +60,8 @@ const (
ValidatingWebhookServicePath = "/validate"
// ExceptionValidatingWebhookServicePath is the path for policy exception validation webhook(used to validate policy exception resource)
ExceptionValidatingWebhookServicePath = "/exceptionvalidate"
// GlobalContextValidatingWebhookServicePath is the path for global context validation webhook(used to validate global context entries)
GlobalContextValidatingWebhookServicePath = "/globalcontextvalidate"
// CleanupValidatingWebhookServicePath is the path for cleanup policy validation webhook(used to validate cleanup policy resource)
CleanupValidatingWebhookServicePath = "/validate"
// TtlValidatingWebhookServicePath is the path for validation of cleanup.kyverno.io/ttl label value

View file

@ -89,10 +89,6 @@ func (c *controller) getEntry(name string) (*kyvernov2alpha1.GlobalContextEntry,
}
func (c *controller) makeStoreEntry(ctx context.Context, gce *kyvernov2alpha1.GlobalContextEntry) (store.Entry, error) {
// TODO: should be done at validation time
if err := gce.Validate(); err != nil {
return nil, err.ToAggregate()
}
if gce.Spec.KubernetesResource != nil {
gvr := schema.GroupVersionResource{
Group: gce.Spec.KubernetesResource.Group,

View file

@ -1,11 +1,11 @@
package admission
import (
"encoding/json"
"fmt"
kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/util/json"
)
func UnmarshalCleanupPolicy(kind string, raw []byte) (kyvernov2alpha1.CleanupPolicyInterface, error) {

View file

@ -1,7 +1,6 @@
package admission
import (
"encoding/json"
"reflect"
"testing"
@ -9,6 +8,7 @@ import (
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
)
func TestUnmarshalCleanupPolicy(t *testing.T) {

View file

@ -1,10 +1,9 @@
package admission
import (
"encoding/json"
kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/util/json"
)
func UnmarshalPolicyException(raw []byte) (*kyvernov2beta1.PolicyException, error) {

View file

@ -1,10 +1,11 @@
package admission
import (
"encoding/json"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/util/json"
kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"

View file

@ -0,0 +1,28 @@
package admission
import (
kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/util/json"
)
func UnmarshalGlobalContextEntry(raw []byte) (*kyvernov2alpha1.GlobalContextEntry, error) {
var exception *kyvernov2alpha1.GlobalContextEntry
if err := json.Unmarshal(raw, &exception); err != nil {
return nil, err
}
return exception, nil
}
func GetGlobalContextEntry(request admissionv1.AdmissionRequest) (*kyvernov2alpha1.GlobalContextEntry, *kyvernov2alpha1.GlobalContextEntry, error) {
var empty *kyvernov2alpha1.GlobalContextEntry
gctx, err := UnmarshalGlobalContextEntry(request.Object.Raw)
if err != nil {
return gctx, empty, err
}
if request.Operation == admissionv1.Update {
old, err := UnmarshalGlobalContextEntry(request.OldObject.Raw)
return gctx, old, err
}
return gctx, empty, nil
}

View file

@ -0,0 +1,183 @@
package admission
import (
"reflect"
"testing"
kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
)
func TestUnmarshalGlobalContext(t *testing.T) {
testCases := []struct {
name string
raw []byte
expectErr bool
}{
{
name: "Valid JSON",
raw: []byte(`{"field": "value"}`),
},
{
name: "Invalid JSON data",
raw: []byte(`invalid JSON data`),
expectErr: true,
},
{
name: "Empty JSON",
raw: []byte(`{}`),
},
{
name: "Missing Field",
raw: []byte(``),
expectErr: true,
},
{
name: "Nested Array",
raw: []byte(`{"nested": [{"field": "value"}]}`),
},
{
name: "Invalid Type",
raw: []byte(`123`),
expectErr: true,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
result, err := UnmarshalGlobalContextEntry(test.raw)
if test.expectErr {
if err == nil {
t.Error("Expected an error, but got none")
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
var gctx *kyvernov2alpha1.GlobalContextEntry
json.Unmarshal(test.raw, &gctx)
if !reflect.DeepEqual(result, gctx) {
t.Errorf("Expected %+v, got %+v", gctx, result)
}
}
})
}
}
func TestGetGlobalContext(t *testing.T) {
type args struct {
request admissionv1.AdmissionRequest
}
testCases := []struct {
name string
args args
}{{
name: "Valid JSON",
args: args{
request: admissionv1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: []byte(`{"field":"value"}`),
},
OldObject: runtime.RawExtension{
Raw: []byte(`{"field":"value"}`),
},
Operation: "CREATE",
},
},
}, {
name: "Invalid JSON data",
args: args{
request: admissionv1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: []byte(`invalid JSON data`),
},
OldObject: runtime.RawExtension{
Raw: []byte(`{"field":"value"}`),
},
Operation: "UPDATE",
},
},
}, {
name: "Empty JSON",
args: args{
request: admissionv1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: []byte(`{}`),
},
OldObject: runtime.RawExtension{
Raw: []byte(`{"field":"value"}`),
},
Operation: "DELETE",
},
},
}, {
name: "Missing Field",
args: args{
request: admissionv1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: []byte(``),
},
OldObject: runtime.RawExtension{
Raw: []byte(`{"field":"value"}`),
},
Operation: "CONNECT",
},
},
}, {
name: "Nested Array",
args: args{
request: admissionv1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: []byte(`{"nested": [{"field": "value"}]}`),
},
OldObject: runtime.RawExtension{
Raw: []byte(`{"field":"value"}`),
},
Operation: "DELETE",
},
},
}, {
name: "Invalid Type",
args: args{
request: admissionv1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: []byte(`123`),
},
OldObject: runtime.RawExtension{
Raw: []byte(`{"field":"value"}`),
},
Operation: "UPDATE",
},
},
}}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
g1, g2, _ := GetGlobalContextEntry(test.args.request)
var empty *kyvernov2alpha1.GlobalContextEntry
expectedG1, err := UnmarshalGlobalContextEntry(test.args.request.Object.Raw)
if err != nil {
expectedG2 := empty
if !reflect.DeepEqual(expectedG1, g1) || !reflect.DeepEqual(expectedG2, g2) {
t.Errorf("Expected policies %+v and %+v , got %+v and %+v ", expectedG1, expectedG2, g1, g2)
}
} else if test.args.request.Operation == admissionv1.Update {
expectedG2, err := UnmarshalGlobalContextEntry(test.args.request.OldObject.Raw)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(expectedG1, g1) || !reflect.DeepEqual(expectedG2, g2) {
t.Errorf("Expected policies %+v and %+v , got %+v and %+v ", expectedG1, expectedG2, g1, g2)
}
} else {
expectedG2 := empty
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(expectedG1, g1) || !reflect.DeepEqual(expectedG2, g2) {
t.Errorf("Expected policies %+v and %+v , got %+v and %+v ", expectedG1, expectedG2, g1, g2)
}
}
})
}
}

View file

@ -1,10 +1,9 @@
package admission
import (
"encoding/json"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/json"
)
func UnmarshalPartialObjectMetadata(raw []byte) (*metav1.PartialObjectMetadata, error) {

View file

@ -1,10 +1,11 @@
package admission
import (
"encoding/json"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/util/json"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

View file

@ -1,11 +1,11 @@
package admission
import (
"encoding/json"
"fmt"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/util/json"
)
func UnmarshalPolicy(kind string, raw []byte) (kyvernov1.PolicyInterface, error) {

View file

@ -1,7 +1,6 @@
package admission
import (
"encoding/json"
"reflect"
"testing"
@ -9,6 +8,7 @@ import (
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
)
func TestUnmarshalPolicy(t *testing.T) {

View file

@ -0,0 +1,26 @@
package globalcontext
import (
"context"
"github.com/go-logr/logr"
kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
)
const (
disabledGctx = "Global context entry would not be processed until it is enabled."
)
type ValidationOptions struct {
Enabled bool
}
// Validate checks global context entry is valid
func Validate(ctx context.Context, logger logr.Logger, gctx *kyvernov2alpha1.GlobalContextEntry, opts ValidationOptions) ([]string, error) {
var warnings []string
if !opts.Enabled {
warnings = append(warnings, disabledGctx)
}
errs := gctx.Validate()
return warnings, errs.ToAggregate()
}

View file

@ -0,0 +1,81 @@
package globalcontext
import (
"context"
"testing"
"github.com/kyverno/kyverno/pkg/logging"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
"gotest.tools/assert"
)
func Test_Validate(t *testing.T) {
type args struct {
opts ValidationOptions
resource []byte
}
tc := []struct {
name string
args args
want int
wantErr bool
}{
{
name: "GlobalContextEntry disabled.",
args: args{
opts: ValidationOptions{
Enabled: false,
},
resource: []byte(`{"apiVersion":"kyverno.io/v2alpha1","kind":"GlobalContextEntry","metadata":{"name":"ingress"},"spec":{"apiCall":{"service":{"url":"https://svc.kyverno/example","caBundle":"-----BEGIN CERTIFICATE-----\n-----REDACTED-----\n-----END CERTIFICATE-----"},"refreshInterval":"10ns"}}}`),
},
want: 1,
wantErr: false,
},
{
name: "GlobalContextEntry enabled, both KubernetesResource and APICall present",
args: args{
opts: ValidationOptions{
Enabled: true,
},
resource: []byte(`{"apiVersion":"kyverno.io/v2alpha1","kind":"GlobalContextEntry","metadata":{"name":"ingress"},"spec":{"apiCall":{"service":{"url":"https://svc.kyverno/example","caBundle":"-----BEGIN CERTIFICATE-----\n-----REDACTED-----\n-----END CERTIFICATE-----"},"refreshInterval":"10ns"},"kubernetesResource":{"group":"apis/networking.k8s.io","version":"v1","resource":"ingresses","namespace":"apps"}}}`),
},
want: 0,
wantErr: true,
},
{
name: "GlobalContextEntry enabled, neither KubernetesResource nor APICall present",
args: args{
opts: ValidationOptions{
Enabled: true,
},
resource: []byte(`{"apiVersion":"kyverno.io/v2alpha1","kind":"GlobalContextEntry","metadata":{"name":"ingress"},"spec":{}}`),
},
want: 0,
wantErr: true,
},
{
name: "GlobalContextEntry enabled.",
args: args{
opts: ValidationOptions{
Enabled: true,
},
resource: []byte(`{"apiVersion":"kyverno.io/v2alpha1","kind":"GlobalContextEntry","metadata":{"name":"ingress"},"spec":{"apiCall":{"service":{"url":"https://svc.kyverno/example","caBundle":"-----BEGIN CERTIFICATE-----\n-----REDACTED-----\n-----END CERTIFICATE-----"},"refreshInterval":"10ns"}}}`),
},
want: 0,
wantErr: false,
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
gctx, err := admissionutils.UnmarshalGlobalContextEntry(c.args.resource)
assert.NilError(t, err)
warnings, err := Validate(context.Background(), logging.GlobalLogger(), gctx, c.args.opts)
if c.wantErr {
assert.Assert(t, err != nil)
} else {
assert.NilError(t, err)
}
assert.Assert(t, len(warnings) == c.want)
})
}
}

View file

@ -0,0 +1,36 @@
package globalcontext
import (
"context"
"time"
"github.com/go-logr/logr"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
validation "github.com/kyverno/kyverno/pkg/validation/globalcontext"
"github.com/kyverno/kyverno/pkg/webhooks"
"github.com/kyverno/kyverno/pkg/webhooks/handlers"
)
type gctxHandlers struct {
validationOptions validation.ValidationOptions
}
func NewHandlers(validationOptions validation.ValidationOptions) webhooks.GlobalContextHandlers {
return &gctxHandlers{
validationOptions: validationOptions,
}
}
// Validate performs the validation check on global context entries
func (h *gctxHandlers) Validate(ctx context.Context, logger logr.Logger, request handlers.AdmissionRequest, startTime time.Time) handlers.AdmissionResponse {
gctx, _, err := admissionutils.GetGlobalContextEntry(request.AdmissionRequest)
if err != nil {
logger.Error(err, "failed to unmarshal global context entry from admission request")
return admissionutils.Response(request.UID, err)
}
warnings, err := validation.Validate(ctx, logger, gctx, h.validationOptions)
if err != nil {
logger.Error(err, "global context entry validation errors")
}
return admissionutils.Response(request.UID, err, warnings...)
}

View file

@ -42,6 +42,11 @@ type ExceptionHandlers interface {
Validate(context.Context, logr.Logger, handlers.AdmissionRequest, time.Time) admissionv1.AdmissionResponse
}
type GlobalContextHandlers interface {
// Validate performs the validation check on global context entries
Validate(context.Context, logr.Logger, handlers.AdmissionRequest, time.Time) admissionv1.AdmissionResponse
}
type PolicyHandlers interface {
// Mutate performs the mutation of policy resources
Mutate(context.Context, logr.Logger, handlers.AdmissionRequest, time.Time) admissionv1.AdmissionResponse
@ -72,6 +77,7 @@ func NewServer(
policyHandlers PolicyHandlers,
resourceHandlers ResourceHandlers,
exceptionHandlers ExceptionHandlers,
globalContextHandlers GlobalContextHandlers,
configuration config.Configuration,
metricsConfig metrics.MetricsConfigManager,
debugModeOpts DebugModeOptions,
@ -89,6 +95,7 @@ func NewServer(
resourceLogger := logger.WithName("resource")
policyLogger := logger.WithName("policy")
exceptionLogger := logger.WithName("exception")
globalContextLogger := logger.WithName("globalcontext")
verifyLogger := logger.WithName("verify")
registerWebhookHandlers(
mux,
@ -152,6 +159,16 @@ func NewServer(
WithAdmission(exceptionLogger.WithName("validate")).
ToHandlerFunc("VALIDATE"),
)
mux.HandlerFunc(
"POST",
config.GlobalContextValidatingWebhookServicePath,
handlers.FromAdmissionFunc("VALIDATE", globalContextHandlers.Validate).
WithDump(debugModeOpts.DumpPayload).
WithSubResourceFilter().
WithMetrics(globalContextLogger, metricsConfig.Config(), metrics.WebhookValidating).
WithAdmission(globalContextLogger.WithName("validate")).
ToHandlerFunc("VALIDATE"),
)
mux.HandlerFunc(
"POST",
config.VerifyMutatingWebhookServicePath,