Skip to content

Commit

Permalink
SKS-1825: Set the default APIGroup and Kind for IPPool CR used in Elf…
Browse files Browse the repository at this point in the history
…MachineTemplate (#172)
  • Loading branch information
haijianyang authored Mar 1, 2024
1 parent 1ed0e13 commit 3036c0f
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 1 deletion.
4 changes: 4 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ resources:
kind: ElfMachineTemplate
path: github.com/smartxworks/cluster-api-provider-elf/api/v1beta1
version: v1beta1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
version: "3"
20 changes: 20 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,23 @@ webhooks:
resources:
- elfmachines
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /mutate-infrastructure-cluster-x-k8s-io-v1beta1-elfmachinetemplate
failurePolicy: Fail
name: mutation.elfmachinetemplate.infrastructure.x-k8s.io
rules:
- apiGroups:
- infrastructure.cluster.x-k8s.io
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- elfmachinetemplates
sideEffects: None
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ require (
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.3.0
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/protobuf v1.31.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ func main() {
}).SetupWebhookWithManager(mgr); err != nil {
return err
}

if err := (&webhooks.ElfMachineTemplateMutation{
Client: mgr.GetClient(),
Logger: mgr.GetLogger().WithName("ElfMachineTemplateMutation"),
}).SetupWebhookWithManager(mgr); err != nil {
return err
}
}

if err := controllers.AddClusterControllerToManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: elfClusterConcurrency}); err != nil {
Expand Down
7 changes: 7 additions & 0 deletions test/helpers/envtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ func NewTestEnvironment() *TestEnvironment {
return err
}

if err := (&webhooks.ElfMachineTemplateMutation{
Client: mgr.GetClient(),
Logger: mgr.GetLogger().WithName("ElfMachineTemplateMutation"),
}).SetupWebhookWithManager(mgr); err != nil {
return err
}

return nil
}

Expand Down
76 changes: 76 additions & 0 deletions webhooks/elfmachine_webhook_mutation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,79 @@ limitations under the License.
*/

package webhooks

import (
"context"
"encoding/json"
"testing"

. "github.com/onsi/gomega"
"gomodules.xyz/jsonpatch/v2"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1"
"github.com/smartxworks/cluster-api-provider-elf/pkg/version"
"github.com/smartxworks/cluster-api-provider-elf/test/fake"
)

func init() {
scheme = runtime.NewScheme()
_ = infrav1.AddToScheme(scheme)
_ = admissionv1.AddToScheme(scheme)
}

var (
scheme *runtime.Scheme
)

func TestElfMachineMutation(t *testing.T) {
g := NewWithT(t)
tests := []testCase{}

elfMachine := fake.NewElfMachine(nil)
elfMachine.Annotations = nil
raw, err := marshal(elfMachine)
g.Expect(err).NotTo(HaveOccurred())
tests = append(tests, testCase{
name: "should set CAPE version",
admissionRequest: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{Group: infrav1.GroupVersion.Group, Version: infrav1.GroupVersion.Version, Kind: "ElfMachine"},
Operation: admissionv1.Create,
Object: runtime.RawExtension{Raw: raw},
}},
expectRespAllowed: true,
expectPatchs: []jsonpatch.Operation{
{Operation: "add", Path: "/metadata/annotations", Value: map[string]interface{}{infrav1.CAPEVersionAnnotation: version.CAPEVersion()}},
},
})

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mutation := ElfMachineMutation{}
mutation.InjectDecoder(admission.NewDecoder(scheme))

resp := mutation.Handle(context.Background(), tc.admissionRequest)
g.Expect(resp.Allowed).Should(Equal(tc.expectRespAllowed))
g.Expect(resp.Patches).Should(Equal(tc.expectPatchs))
})
}
}

func marshal(obj client.Object) ([]byte, error) {
bs, err := json.Marshal(obj)
if err != nil {
return nil, err
}
return bs, nil
}

type testCase struct {
name string
admissionRequest admission.Request
expectRespAllowed bool
expectPatchs []jsonpatch.Operation
}
89 changes: 89 additions & 0 deletions webhooks/elfmachinetemplate_webhook_mutation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package webhooks

import (
goctx "context"
"encoding/json"
"net/http"

"github.com/go-logr/logr"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1"
)

const (
defaultIPPoolAPIGroup = "ipam.metal3.io"
defaultIPPoolKind = "IPPool"
)

func (m *ElfMachineTemplateMutation) SetupWebhookWithManager(mgr ctrl.Manager) error {
if m.decoder == nil {
m.decoder = admission.NewDecoder(mgr.GetScheme())
}

hookServer := mgr.GetWebhookServer()
hookServer.Register("/mutate-infrastructure-cluster-x-k8s-io-v1beta1-elfmachinetemplate", &webhook.Admission{Handler: m})
return ctrl.NewWebhookManagedBy(mgr).
For(&infrav1.ElfMachine{}).
Complete()
}

//+kubebuilder:object:generate=false
//+kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-elfmachinetemplate,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=elfmachinetemplates,versions=v1beta1,name=mutation.elfmachinetemplate.infrastructure.x-k8s.io,admissionReviewVersions=v1

type ElfMachineTemplateMutation struct {
client.Client
decoder *admission.Decoder
logr.Logger
}

func (m *ElfMachineTemplateMutation) Handle(ctx goctx.Context, request admission.Request) admission.Response {
var elfMachineTemplate infrav1.ElfMachineTemplate
if err := m.decoder.Decode(request, &elfMachineTemplate); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

devices := elfMachineTemplate.Spec.Template.Spec.Network.Devices
for i := 0; i < len(devices); i++ {
for j := 0; j < len(devices[i].AddressesFromPools); j++ {
if devices[i].AddressesFromPools[j].APIGroup == nil || *devices[i].AddressesFromPools[j].APIGroup == "" {
devices[i].AddressesFromPools[j].APIGroup = pointer.String(defaultIPPoolAPIGroup)
}
if devices[i].AddressesFromPools[j].Kind == "" {
devices[i].AddressesFromPools[j].Kind = defaultIPPoolKind
}
}
}

if marshaledElfMachineTemplate, err := json.Marshal(elfMachineTemplate); err != nil {
return admission.Errored(http.StatusInternalServerError, err)
} else {
return admission.PatchResponseFromRaw(request.Object.Raw, marshaledElfMachineTemplate)
}
}

// InjectDecoder injects the decoder.
func (m *ElfMachineTemplateMutation) InjectDecoder(d *admission.Decoder) error {
m.decoder = d
return nil
}
85 changes: 85 additions & 0 deletions webhooks/elfmachinetemplate_webhook_mutation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package webhooks

import (
"context"
"testing"

. "github.com/onsi/gomega"
"gomodules.xyz/jsonpatch/v2"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1"
)

func TestElfMachineMutationTemplate(t *testing.T) {
g := NewWithT(t)
tests := []testCase{}

elfMachineTemplate := &infrav1.ElfMachineTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "elfmachinetemplate",
},
Spec: infrav1.ElfMachineTemplateSpec{
Template: infrav1.ElfMachineTemplateResource{
Spec: infrav1.ElfMachineSpec{},
},
},
}
elfMachineTemplate.Spec.Template.Spec.Network.Devices = []infrav1.NetworkDeviceSpec{
{AddressesFromPools: []corev1.TypedLocalObjectReference{{Name: "test"}}},
{AddressesFromPools: []corev1.TypedLocalObjectReference{{Name: "test", APIGroup: pointer.String("")}}},
{AddressesFromPools: []corev1.TypedLocalObjectReference{{Name: "test", APIGroup: pointer.String("apiGroup")}}},
{AddressesFromPools: []corev1.TypedLocalObjectReference{{Name: "test", APIGroup: pointer.String("apiGroup"), Kind: "kind"}}},
}
raw, err := marshal(elfMachineTemplate)
g.Expect(err).NotTo(HaveOccurred())
tests = append(tests, testCase{
name: "should set default values for network devices",
admissionRequest: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{Group: infrav1.GroupVersion.Group, Version: infrav1.GroupVersion.Version, Kind: "ElfMachine"},
Operation: admissionv1.Create,
Object: runtime.RawExtension{Raw: raw},
}},
expectRespAllowed: true,
expectPatchs: []jsonpatch.Operation{
{Operation: "replace", Path: "/spec/template/spec/network/devices/0/addressesFromPools/0/apiGroup", Value: defaultIPPoolAPIGroup},
{Operation: "replace", Path: "/spec/template/spec/network/devices/0/addressesFromPools/0/kind", Value: defaultIPPoolKind},
{Operation: "replace", Path: "/spec/template/spec/network/devices/1/addressesFromPools/0/apiGroup", Value: defaultIPPoolAPIGroup},
{Operation: "replace", Path: "/spec/template/spec/network/devices/1/addressesFromPools/0/kind", Value: defaultIPPoolKind},
{Operation: "replace", Path: "/spec/template/spec/network/devices/2/addressesFromPools/0/kind", Value: defaultIPPoolKind},
},
})

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mutation := ElfMachineTemplateMutation{}
mutation.InjectDecoder(admission.NewDecoder(scheme))

resp := mutation.Handle(context.Background(), tc.admissionRequest)
g.Expect(resp.Allowed).Should(Equal(tc.expectRespAllowed))
g.Expect(resp.Patches).Should(HaveLen(len(tc.expectPatchs)))
g.Expect(resp.Patches).Should(ContainElements(tc.expectPatchs))
})
}
}

0 comments on commit 3036c0f

Please sign in to comment.