-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[AddressBinding] Add admission webhook to validate CREATE/UPDATE request
Signed-off-by: gran <[email protected]>
- Loading branch information
Showing
6 changed files
with
252 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package subnetport | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"reflect" | ||
|
||
admissionv1 "k8s.io/api/admission/v1" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
logf "sigs.k8s.io/controller-runtime/pkg/log" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
|
||
"github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" | ||
"github.com/vmware-tanzu/nsx-operator/pkg/util" | ||
) | ||
|
||
// log is for logging in this package. | ||
var addressbindinglog = logf.Log.WithName("addressbinding-webhook") | ||
|
||
// Create validator instead of using the existing one in controller-runtime because the existing one can't | ||
// inspect admission.Request in Handle function. | ||
|
||
//+kubebuilder:webhook:path=/validate-crd-nsx-vmware-com-v1alpha1-addressbinding,mutating=false,failurePolicy=fail,sideEffects=None,groups=crd.nsx.vmware.com,resources=addressbindings,verbs=create;update,versions=v1alpha1,name=addressbinding.validating.crd.nsx.vmware.com,admissionReviewVersions=v1 | ||
|
||
type AddressBindingValidator struct { | ||
Client client.Client | ||
decoder admission.Decoder | ||
} | ||
|
||
// Handle handles admission requests. | ||
func (v *AddressBindingValidator) Handle(ctx context.Context, req admission.Request) admission.Response { | ||
ab := &v1alpha1.AddressBinding{} | ||
if req.Operation == admissionv1.Delete { | ||
return admission.Allowed("") | ||
} else { | ||
err := v.decoder.Decode(req, ab) | ||
if err != nil { | ||
addressbindinglog.Error(err, "error while decoding AddressBinding", "AddressBinding", req.Namespace+"/"+req.Name) | ||
return admission.Errored(http.StatusBadRequest, err) | ||
} | ||
} | ||
switch req.Operation { | ||
case admissionv1.Create: | ||
existingAddressBindingList := &v1alpha1.AddressBindingList{} | ||
abIndexValue := fmt.Sprintf("%s/%s", ab.Namespace, ab.Spec.VMName) | ||
err := v.Client.List(context.TODO(), existingAddressBindingList, client.MatchingFields{util.AddressBindingNamespaceVMIndexKey: abIndexValue}) | ||
if err != nil { | ||
addressbindinglog.Error(err, "failed to list AddressBinding from cache", "indexValue", abIndexValue) | ||
return admission.Errored(http.StatusInternalServerError, err) | ||
} | ||
for _, existingAddressBinding := range existingAddressBindingList.Items { | ||
if ab.Name != existingAddressBinding.Name && ab.Spec.InterfaceName == existingAddressBinding.Spec.InterfaceName { | ||
return admission.Denied("interface already has AddressBinding") | ||
} | ||
} | ||
case admissionv1.Update: | ||
oldAddressBinding := &v1alpha1.AddressBinding{} | ||
if err := v.decoder.DecodeRaw(req.OldObject, oldAddressBinding); err != nil { | ||
addressbindinglog.Error(err, "error while decoding AddressBinding", "AddressBinding", req.Namespace+"/"+req.Name) | ||
return admission.Errored(http.StatusBadRequest, err) | ||
} | ||
if !reflect.DeepEqual(ab.Spec, oldAddressBinding.Spec) { | ||
return admission.Denied("update AddressBinding is not allowed") | ||
} | ||
} | ||
return admission.Allowed("") | ||
} |
134 changes: 134 additions & 0 deletions
134
pkg/controllers/subnetport/addressbinding_webhook_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package subnetport | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/agiledragon/gomonkey/v2" | ||
"github.com/stretchr/testify/assert" | ||
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" | ||
clientgoscheme "k8s.io/client-go/kubernetes/scheme" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
|
||
"github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" | ||
"github.com/vmware-tanzu/nsx-operator/pkg/util" | ||
) | ||
|
||
func TestAddressBindingValidator_Handle(t *testing.T) { | ||
req1, _ := json.Marshal(&v1alpha1.AddressBinding{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Namespace: "ns1", | ||
Name: "ab1", | ||
}, | ||
Spec: v1alpha1.AddressBindingSpec{ | ||
VMName: "vm1", | ||
InterfaceName: "inf1", | ||
}, | ||
}) | ||
req1New, _ := json.Marshal(&v1alpha1.AddressBinding{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Namespace: "ns1", | ||
Name: "ab1", | ||
}, | ||
Spec: v1alpha1.AddressBindingSpec{ | ||
VMName: "vm1", | ||
InterfaceName: "inf1new", | ||
}, | ||
}) | ||
req2, _ := json.Marshal(&v1alpha1.AddressBinding{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Namespace: "ns1", | ||
Name: "ab2", | ||
}, | ||
Spec: v1alpha1.AddressBindingSpec{ | ||
VMName: "vm1", | ||
InterfaceName: "inf2", | ||
}, | ||
}) | ||
type args struct { | ||
req admission.Request | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
prepareFunc func(*testing.T, client.Client, context.Context) *gomonkey.Patches | ||
want admission.Response | ||
}{ | ||
{ | ||
name: "delete", | ||
args: args{req: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Delete}}}, | ||
want: admission.Allowed(""), | ||
}, | ||
{ | ||
name: "create decode error", | ||
args: args{req: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}}}, | ||
want: admission.Errored(http.StatusBadRequest, fmt.Errorf("there is no content to decode")), | ||
}, | ||
{ | ||
name: "create", | ||
args: args{req: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create, Object: runtime.RawExtension{Raw: req1}}}}, | ||
want: admission.Allowed(""), | ||
}, | ||
{ | ||
name: "create list error", | ||
args: args{req: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create, Object: runtime.RawExtension{Raw: req1}}}}, | ||
prepareFunc: func(t *testing.T, client client.Client, ctx context.Context) *gomonkey.Patches { | ||
return gomonkey.ApplyMethodSeq(client, "List", []gomonkey.OutputCell{{ | ||
Values: gomonkey.Params{fmt.Errorf("mock error")}, | ||
Times: 1, | ||
}}) | ||
}, | ||
want: admission.Errored(http.StatusInternalServerError, fmt.Errorf("mock error")), | ||
}, | ||
{ | ||
name: "create dup", | ||
args: args{req: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create, Object: runtime.RawExtension{Raw: req2}}}}, | ||
want: admission.Denied("interface already has AddressBinding"), | ||
}, | ||
{ | ||
name: "update decode error", | ||
args: args{req: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Update, Object: runtime.RawExtension{Raw: req1}}}}, | ||
want: admission.Errored(http.StatusBadRequest, fmt.Errorf("there is no content to decode")), | ||
}, | ||
{ | ||
name: "update changed", | ||
args: args{req: admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Update, Object: runtime.RawExtension{Raw: req1New}, OldObject: runtime.RawExtension{Raw: req1}}}}, | ||
want: admission.Denied("update AddressBinding is not allowed"), | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
scheme := clientgoscheme.Scheme | ||
v1alpha1.AddToScheme(scheme) | ||
client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&v1alpha1.AddressBinding{}).WithIndex(&v1alpha1.AddressBinding{}, util.AddressBindingNamespaceVMIndexKey, addressBindingNamespaceVMIndexFunc).Build() | ||
decoder := admission.NewDecoder(scheme) | ||
ctx := context.TODO() | ||
client.Create(ctx, &v1alpha1.AddressBinding{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Namespace: "ns1", | ||
Name: "ab2a", | ||
}, | ||
Spec: v1alpha1.AddressBindingSpec{ | ||
VMName: "vm1", | ||
InterfaceName: "inf2", | ||
}, | ||
}) | ||
if tt.prepareFunc != nil { | ||
patches := tt.prepareFunc(t, client, ctx) | ||
defer patches.Reset() | ||
} | ||
v := &AddressBindingValidator{ | ||
Client: client, | ||
decoder: decoder, | ||
} | ||
assert.Equalf(t, tt.want, v.Handle(ctx, tt.args.req), "Handle()") | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters