From c8238c7914cc7d97827f6d649068f1961815913b Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:14:07 +0100 Subject: [PATCH] K8s: Add admission mutation support to builder (#95526) --- pkg/services/apiserver/builder/admission.go | 18 ++++- .../apiserver/builder/admission_test.go | 78 ++++++++++++++++++- pkg/services/apiserver/builder/common.go | 8 ++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/pkg/services/apiserver/builder/admission.go b/pkg/services/apiserver/builder/admission.go index 2bdfcea467e..dbd39130572 100644 --- a/pkg/services/apiserver/builder/admission.go +++ b/pkg/services/apiserver/builder/admission.go @@ -8,11 +8,20 @@ import ( ) type builderAdmission struct { + mutators map[schema.GroupVersion]APIGroupMutation validators map[schema.GroupVersion]APIGroupValidation } +var _ admission.MutationInterface = (*builderAdmission)(nil) var _ admission.ValidationInterface = (*builderAdmission)(nil) +func (b *builderAdmission) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + if m, ok := b.mutators[a.GetResource().GroupVersion()]; ok { + return m.Mutate(ctx, a, o) + } + return nil +} + func (b *builderAdmission) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { if v, ok := b.validators[a.GetResource().GroupVersion()]; ok { return v.Validate(ctx, a, o) @@ -25,17 +34,22 @@ func (b *builderAdmission) Handles(operation admission.Operation) bool { } func NewAdmissionFromBuilders(builders []APIGroupBuilder) *builderAdmission { + mutators := make(map[schema.GroupVersion]APIGroupMutation) validators := make(map[schema.GroupVersion]APIGroupValidation) for _, builder := range builders { + if m, ok := builder.(APIGroupMutation); ok { + mutators[builder.GetGroupVersion()] = m + } if v, ok := builder.(APIGroupValidation); ok { validators[builder.GetGroupVersion()] = v } } - return NewAdmission(validators) + return NewAdmission(mutators, validators) } -func NewAdmission(validators map[schema.GroupVersion]APIGroupValidation) *builderAdmission { +func NewAdmission(mutators map[schema.GroupVersion]APIGroupMutation, validators map[schema.GroupVersion]APIGroupValidation) *builderAdmission { return &builderAdmission{ + mutators: mutators, validators: validators, } } diff --git a/pkg/services/apiserver/builder/admission_test.go b/pkg/services/apiserver/builder/admission_test.go index 67eda367683..361825fd732 100644 --- a/pkg/services/apiserver/builder/admission_test.go +++ b/pkg/services/apiserver/builder/admission_test.go @@ -7,8 +7,10 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/apis/example" ) func TestBuilderAdmission_Validate(t *testing.T) { @@ -81,7 +83,7 @@ func TestBuilderAdmission_Validate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := builder.NewAdmission(tt.validators) + b := builder.NewAdmission(nil, tt.validators) err := b.Validate(context.Background(), tt.attributes, nil) if tt.wantErr { require.Error(t, err) @@ -133,6 +135,72 @@ func TestNewAdmissionFromBuilders(t *testing.T) { require.Error(t, err) } +func TestBuilderAdmission_Admit(t *testing.T) { + gvk := schema.GroupVersionKind{ + Group: "test.grafana.app", + Version: "v1", + Kind: "Foo", + } + gvr := gvk.GroupVersion().WithResource("foos") + exampleObj := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: example.PodSpec{}} + tests := []struct { + name string + mutators map[schema.GroupVersion]builder.APIGroupMutation + attributes admission.Attributes + wantErr bool + wantSpec example.PodSpec + }{ + { + name: "mutator exists - error", + mutators: map[schema.GroupVersion]builder.APIGroupMutation{ + gvk.GroupVersion(): &mockMutator{ + mutateFunc: func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return errors.New("test error") + }, + }, + }, + attributes: admission.NewAttributesRecord(exampleObj.DeepCopy(), nil, gvk, "default", "foo", gvr, "", admission.Create, nil, false, nil), + wantErr: true, + wantSpec: exampleObj.Spec, + }, + { + name: "mutator exists - add hostname", + mutators: map[schema.GroupVersion]builder.APIGroupMutation{ + gvk.GroupVersion(): &mockMutator{ + mutateFunc: func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + obj := a.GetObject().(*example.Pod) + obj.Spec.Hostname = "test" + return nil + }, + }, + }, + attributes: admission.NewAttributesRecord(exampleObj.DeepCopy(), nil, gvk, "default", "foo", gvr, "", admission.Create, nil, false, nil), + wantErr: false, + wantSpec: example.PodSpec{Hostname: "test"}, + }, + { + name: "mutator does not exist", + mutators: map[schema.GroupVersion]builder.APIGroupMutation{}, + attributes: admission.NewAttributesRecord(exampleObj.DeepCopy(), nil, gvk, "default", "foo", gvr, "", admission.Create, nil, false, nil), + wantErr: false, + wantSpec: exampleObj.Spec, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := builder.NewAdmission(tt.mutators, nil) + err := b.Admit(context.Background(), tt.attributes, nil) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.wantSpec, tt.attributes.GetObject().(*example.Pod).Spec) + }) + } +} + type mockBuilder struct { builder.APIGroupBuilder groupVersion schema.GroupVersion @@ -147,6 +215,14 @@ func (m *mockBuilder) Validate(ctx context.Context, a admission.Attributes, o ad return m.validator.Validate(ctx, a, o) } +type mockMutator struct { + mutateFunc func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error +} + +func (m *mockMutator) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return m.mutateFunc(ctx, a, o) +} + type mockValidator struct { validateFunc func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error } diff --git a/pkg/services/apiserver/builder/common.go b/pkg/services/apiserver/builder/common.go index 965ef403a66..7356a648f6f 100644 --- a/pkg/services/apiserver/builder/common.go +++ b/pkg/services/apiserver/builder/common.go @@ -47,7 +47,15 @@ type APIGroupBuilder interface { GetAuthorizer() authorizer.Authorizer } +type APIGroupMutation interface { + // Mutate allows the builder to make changes to the object before it is persisted. + // Context is used only for timeout/deadline/cancellation and tracing information. + Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) +} + type APIGroupValidation interface { + // Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate + // Context is used only for timeout/deadline/cancellation and tracing information. Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) }