Skip to content

Commit 77a784f

Browse files
Update VM webhooks for 4.18 (codeready-toolchain#643)
Co-authored-by: Rajiv Senthilnathan <rsenthil@redhat.com>
1 parent a0b201c commit 77a784f

File tree

6 files changed

+89
-43
lines changed

6 files changed

+89
-43
lines changed

deploy/webhook/member-operator-webhook.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ objects:
293293
port: 443
294294
matchPolicy: Equivalent
295295
rules:
296-
- operations: ["CREATE", "UPDATE"]
296+
- operations: ["UPDATE"]
297297
apiGroups: ["kubevirt.io"]
298298
apiVersions: ["*"]
299299
resources: ["virtualmachines"]

pkg/webhook/deploy/deployment_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func mutatingWebhookConfig(namespace, caBundle string) string {
227227
}
228228

229229
func validatingWebhookConfig(namespace, caBundle string) string {
230-
return fmt.Sprintf(`{"apiVersion": "admissionregistration.k8s.io/v1","kind": "ValidatingWebhookConfiguration","metadata": {"labels": {"app": "member-operator-webhook","toolchain.dev.openshift.com/provider": "codeready-toolchain"},"name": "member-operator-validating-webhook-%[2]s"},"webhooks": [{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-users-rolebindings","port": 443}},"failurePolicy": "Ignore","matchPolicy": "Equivalent","name": "users.rolebindings.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["rbac.authorization.k8s.io","authorization.openshift.io"],"apiVersions": ["v1"],"operations": ["CREATE","UPDATE"],"resources": ["rolebindings"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5},{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-spacebindingrequests","port": 443}},"failurePolicy": "Fail","matchPolicy": "Equivalent","name": "users.spacebindingrequests.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["toolchain.dev.openshift.com"],"apiVersions": ["v1alpha1"],"operations": ["CREATE","UPDATE"],"resources": ["spacebindingrequests"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5},{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-ssprequests","port": 443}},"failurePolicy": "Fail","matchPolicy": "Equivalent","name": "users.virtualmachines.ssp.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["ssp.kubevirt.io"],"apiVersions": ["*"],"operations": ["CREATE","UPDATE"],"resources": ["ssps"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5},{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-vmrequests","port": 443}},"failurePolicy": "Fail","matchPolicy": "Equivalent","name": "users.virtualmachines.validating.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["kubevirt.io"],"apiVersions": ["*"],"operations": ["CREATE","UPDATE"],"resources": ["virtualmachines"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5}]}`, caBundle, namespace)
230+
return fmt.Sprintf(`{"apiVersion": "admissionregistration.k8s.io/v1","kind": "ValidatingWebhookConfiguration","metadata": {"labels": {"app": "member-operator-webhook","toolchain.dev.openshift.com/provider": "codeready-toolchain"},"name": "member-operator-validating-webhook-%[2]s"},"webhooks": [{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-users-rolebindings","port": 443}},"failurePolicy": "Ignore","matchPolicy": "Equivalent","name": "users.rolebindings.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["rbac.authorization.k8s.io","authorization.openshift.io"],"apiVersions": ["v1"],"operations": ["CREATE","UPDATE"],"resources": ["rolebindings"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5},{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-spacebindingrequests","port": 443}},"failurePolicy": "Fail","matchPolicy": "Equivalent","name": "users.spacebindingrequests.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["toolchain.dev.openshift.com"],"apiVersions": ["v1alpha1"],"operations": ["CREATE","UPDATE"],"resources": ["spacebindingrequests"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5},{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-ssprequests","port": 443}},"failurePolicy": "Fail","matchPolicy": "Equivalent","name": "users.virtualmachines.ssp.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["ssp.kubevirt.io"],"apiVersions": ["*"],"operations": ["CREATE","UPDATE"],"resources": ["ssps"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5},{"admissionReviewVersions": ["v1"],"clientConfig": {"caBundle": "%[1]s","service": {"name": "member-operator-webhook","namespace": "%[2]s","path": "/validate-vmrequests","port": 443}},"failurePolicy": "Fail","matchPolicy": "Equivalent","name": "users.virtualmachines.validating.webhook.sandbox","namespaceSelector": {"matchLabels": {"toolchain.dev.openshift.com/provider": "codeready-toolchain"}},"reinvocationPolicy": "Never","rules": [{"apiGroups": ["kubevirt.io"],"apiVersions": ["*"],"operations": ["UPDATE"],"resources": ["virtualmachines"],"scope": "Namespaced"}],"sideEffects": "None","timeoutSeconds": 5}]}`, caBundle, namespace)
231231
}
232232

233233
func serviceAccount(namespace string) string {

pkg/webhook/mutatingwebhook/vm_mutate.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ var sandboxToleration = map[string]interface{}{
2626
"operator": "Exists",
2727
}
2828

29+
const manualRunStrategy = "Manual"
30+
2931
func HandleMutateVirtualMachines(w http.ResponseWriter, r *http.Request) {
3032
handleMutate(vmLogger, w, r, vmMutator)
3133
}
@@ -50,6 +52,9 @@ func vmMutator(admReview admissionv1.AdmissionReview) *admissionv1.AdmissionResp
5052
// instead of changing the object we need to tell K8s how to change the object via patch
5153
vmPatchItems := []map[string]interface{}{}
5254

55+
// ensure runStrategy is set to Manual, see https://docs.redhat.com/en/documentation/openshift_container_platform/4.11/html/virtualization/virtual-machines#virt-about-runstrategies-vms_virt-create-vms
56+
vmPatchItems = ensureManualRunStrategy(unstructuredRequestObj, vmPatchItems)
57+
5358
// ensure limits are set in a best effort approach, if the limits are not set for any reason the request will still be allowed
5459
vmPatchItems = ensureLimits(unstructuredRequestObj, vmPatchItems)
5560

@@ -254,6 +259,22 @@ func ensureLimits(unstructuredObj *unstructured.Unstructured, patchItems []map[s
254259
return patchItems
255260
}
256261

262+
// ensureManualRunStrategy ensures the VM's RunStrategy is set to Manual
263+
func ensureManualRunStrategy(unstructuredRequestObj *unstructured.Unstructured, patchItems []map[string]interface{}) []map[string]interface{} {
264+
// get existing runStrategy, if any
265+
runStrategy, _, err := unstructured.NestedString(unstructuredRequestObj.Object, "spec", "runStrategy")
266+
if err != nil {
267+
vmLogger.Error(err, "unable to get runStrategy from VirtualMachine", "VirtualMachine", unstructuredRequestObj)
268+
return patchItems
269+
}
270+
271+
if runStrategy != manualRunStrategy {
272+
patchItems = append(patchItems, addManualRunStrategy())
273+
}
274+
275+
return patchItems
276+
}
277+
257278
// ensureTolerations ensures tolerations are set on the VirtualMachine
258279
func ensureTolerations(unstructuredRequestObj *unstructured.Unstructured, patchItems []map[string]interface{}) []map[string]interface{} {
259280
// get existing tolerations, if any
@@ -309,3 +330,11 @@ func addTolerations(tolerations []interface{}) map[string]interface{} {
309330
"value": tolerations,
310331
}
311332
}
333+
334+
func addManualRunStrategy() map[string]interface{} {
335+
return map[string]interface{}{
336+
"op": "add",
337+
"path": "/spec/runStrategy",
338+
"value": manualRunStrategy,
339+
}
340+
}

pkg/webhook/mutatingwebhook/vm_mutate_test.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ func TestVMMutator(t *testing.T) {
3838
actualResponse := vmMutator(admReview)
3939

4040
// then
41-
expectedPatches := []map[string]interface{}{expectedTolerationsPatch, expectedVolumesPatch}
41+
expectedPatches := []map[string]interface{}{addManualRunStrategy(), expectedTolerationsPatch, expectedVolumesPatch}
4242
actualPatchContent := actualResponse.Patch
4343
expectedPatchContent, err := json.Marshal(expectedPatches)
4444
require.NoError(t, err)
4545
require.Equal(t, string(expectedPatchContent), string(actualPatchContent))
46-
assert.Equal(t, expectedVMMutateRespSuccess(t, expectedTolerationsPatch, expectedVolumesPatch), *actualResponse)
46+
assert.Equal(t, expectedVMMutateRespSuccess(t, addManualRunStrategy(), expectedTolerationsPatch, expectedVolumesPatch), *actualResponse)
4747
})
4848

4949
t.Run("fail", func(t *testing.T) {
@@ -454,6 +454,36 @@ func TestEnsureTolerations(t *testing.T) {
454454
})
455455
}
456456

457+
func TestEnsureManualRunStrategy(t *testing.T) {
458+
459+
t.Run("success", func(t *testing.T) {
460+
t.Run("no existing runStrategy", func(t *testing.T) {
461+
// given
462+
vmAdmReviewRequestObj := vmAdmReviewRequestObject(t)
463+
// expect only sandbox toleration
464+
expectedPatchItems := []map[string]interface{}{addManualRunStrategy()}
465+
466+
// when
467+
actualPatchItems := ensureManualRunStrategy(vmAdmReviewRequestObj, []map[string]interface{}{})
468+
469+
// then
470+
assertPatchesEqual(t, expectedPatchItems, actualPatchItems)
471+
})
472+
473+
t.Run("runStrategy already set", func(t *testing.T) {
474+
// given
475+
vmAdmReviewRequestObj := vmAdmReviewRequestObject(t, setRunStrategy("Always")) // it doesn't matter what runStrategy is set, the patch is always applied since it's an 'add' JSON patch
476+
expectedPatchItems := []map[string]interface{}{addManualRunStrategy()}
477+
478+
// when
479+
actualPatchItems := ensureManualRunStrategy(vmAdmReviewRequestObj, []map[string]interface{}{})
480+
481+
// then
482+
assertPatchesEqual(t, expectedPatchItems, actualPatchItems)
483+
})
484+
})
485+
}
486+
457487
type admissionReviewOption func(t *testing.T, unstructuredAdmReview *unstructured.Unstructured)
458488

459489
func setDomainResourcesRequests(requests map[string]string) admissionReviewOption {
@@ -548,6 +578,13 @@ func cloudInitVolume(cicType cloudInitConfigType, userData string) map[string]in
548578
return volume
549579
}
550580

581+
func setRunStrategy(runStrategy string) admissionReviewOption {
582+
return func(t *testing.T, unstructuredAdmReview *unstructured.Unstructured) {
583+
err := unstructured.SetNestedField(unstructuredAdmReview.Object, runStrategy, "request", "object", "spec", "runStrategy")
584+
require.NoError(t, err)
585+
}
586+
}
587+
551588
func vmAdmReviewRequestObject(t *testing.T, options ...admissionReviewOption) *unstructured.Unstructured {
552589
return admReviewRequestObject(t, vmRawAdmissionReviewJSONTemplate, options...)
553590
}

pkg/webhook/validatingwebhook/validate_vm_request.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
runtimeClient "sigs.k8s.io/controller-runtime/pkg/client"
1212
)
1313

14+
const manualRunStrategy = "Manual"
15+
1416
type VMRequestValidator struct {
1517
Client runtimeClient.Client
1618
}
@@ -53,24 +55,15 @@ func (v VMRequestValidator) validate(body []byte) []byte {
5355
return denyAdmissionRequest(admReview, errors.New("failed to validate VirtualMachine request"))
5456
}
5557

56-
hasRunStrategy, err := hasRunningStrategy(unstructuredRequestObj)
58+
runStrategy, hasRunStrategy, err := unstructured.NestedString(unstructuredRequestObj.Object, "spec", "runStrategy")
5759
if err != nil {
5860
log.Error(err, "failed to unmarshal VirtualMachine json object", "AdmissionReview", admReview)
5961
return denyAdmissionRequest(admReview, errors.New("failed to validate VirtualMachine request"))
6062
}
61-
if hasRunStrategy {
62-
log.Info("sandbox user is trying to create a VM with RunStrategy configured", "AdmissionReview", admReview) // not allowed because it interferes with the Dev Sandbox Idler
63-
return denyAdmissionRequest(admReview, errors.New("this is a Dev Sandbox enforced restriction. Configuring RunStrategy is not allowed"))
63+
if hasRunStrategy && runStrategy != manualRunStrategy {
64+
log.Info("sandbox user is trying to configure a VM with RunStrategy set to a value other than Manual", "AdmissionReview", admReview) // other run strategies are not allowed because it conflicts with the Dev Sandbox Idler
65+
return denyAdmissionRequest(admReview, errors.New("this is a Dev Sandbox enforced restriction. Only 'Manual' RunStrategy is permitted"))
6466
}
65-
// the user is not creating a VM with the 'runStrategy' configured, allowing the request.
67+
// the user is configuring a VM without the 'runStrategy' configured or with it configured to Manual, allowing the request.
6668
return allowAdmissionRequest(admReview)
6769
}
68-
69-
func hasRunningStrategy(unstructuredObj *unstructured.Unstructured) (bool, error) {
70-
_, runStrategyFound, err := unstructured.NestedString(unstructuredObj.Object, "spec", "runStrategy")
71-
if err != nil {
72-
return runStrategyFound, err
73-
}
74-
75-
return runStrategyFound, nil
76-
}

pkg/webhook/validatingwebhook/validate_vm_request_test.go

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package validatingwebhook
22

33
import (
44
"bytes"
5+
"fmt"
56
"io"
67
"net/http"
78
"net/http/httptest"
@@ -23,9 +24,9 @@ func TestHandleValidateVMAdmissionRequestBlocked(t *testing.T) {
2324
ts := httptest.NewServer(http.HandlerFunc(v.HandleValidate))
2425
defer ts.Close()
2526

26-
t.Run("sandbox user trying to create a VM resource with RunStrategy is denied", func(t *testing.T) {
27+
t.Run("sandbox user trying to update a VM resource with RunStrategy other than 'Manual' is denied", func(t *testing.T) {
2728
// when
28-
resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(newCreateVMAdmissionRequest(t, VMAdmReviewTmplParams{"CREATE", "johnsmith"}, createVMWithRunStrategyJSONTmpl)))
29+
resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(newCreateVMAdmissionRequest(t, VMAdmReviewTmplParams{"UPDATE", "johnsmith"}, createVMWithRunStrategyJSONTmpl("Always"))))
2930

3031
// then
3132
require.NoError(t, err)
@@ -34,12 +35,12 @@ func TestHandleValidateVMAdmissionRequestBlocked(t *testing.T) {
3435
require.NoError(t, resp.Body.Close())
3536
}()
3637
require.NoError(t, err)
37-
test.VerifyRequestBlocked(t, body, "this is a Dev Sandbox enforced restriction. Configuring RunStrategy is not allowed", "b6ae2ab4-782b-11ee-b962-0242ac120002")
38+
test.VerifyRequestBlocked(t, body, "this is a Dev Sandbox enforced restriction. Only 'Manual' RunStrategy is permitted", "b6ae2ab4-782b-11ee-b962-0242ac120002")
3839
})
3940

40-
t.Run("sandbox user trying to update a VM resource with RunStrategy is denied", func(t *testing.T) {
41+
t.Run("sandbox user trying to update a VM resource with RunStrategy 'Manual' is allowed", func(t *testing.T) {
4142
// when
42-
resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(newCreateVMAdmissionRequest(t, VMAdmReviewTmplParams{"UPDATE", "johnsmith"}, createVMWithRunStrategyJSONTmpl)))
43+
resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(newCreateVMAdmissionRequest(t, VMAdmReviewTmplParams{"UPDATE", "johnsmith"}, createVMWithRunStrategyJSONTmpl("Manual"))))
4344

4445
// then
4546
require.NoError(t, err)
@@ -48,25 +49,10 @@ func TestHandleValidateVMAdmissionRequestBlocked(t *testing.T) {
4849
require.NoError(t, resp.Body.Close())
4950
}()
5051
require.NoError(t, err)
51-
test.VerifyRequestBlocked(t, body, "this is a Dev Sandbox enforced restriction. Configuring RunStrategy is not allowed", "b6ae2ab4-782b-11ee-b962-0242ac120002")
52-
})
53-
54-
t.Run("sandbox user trying to create a VM resource without RunStrategy is allowed", func(t *testing.T) {
55-
// when
56-
resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(newCreateVMAdmissionRequest(t, VMAdmReviewTmplParams{"CREATE", "johnsmith"}, createVMWithoutRunStrategyJSONTmpl)))
57-
58-
// then
59-
require.NoError(t, err)
60-
body, err := io.ReadAll(resp.Body)
61-
defer func() {
62-
require.NoError(t, resp.Body.Close())
63-
}()
64-
require.NoError(t, err)
65-
6652
test.VerifyRequestAllowed(t, body, "b6ae2ab4-782b-11ee-b962-0242ac120002")
6753
})
6854

69-
t.Run("sandbox user trying to update a VM resource without RunStrategy is allowed", func(t *testing.T) {
55+
t.Run("sandbox user trying to update a VM resource without RunStrategy is allowed", func(t *testing.T) { // note: RunStrategy can be removed as it's not needed (it's automatically set upon creation)
7056
// when
7157
resp, err := http.Post(ts.URL, "application/json", bytes.NewBuffer(newCreateVMAdmissionRequest(t, VMAdmReviewTmplParams{"UPDATE", "johnsmith"}, createVMWithoutRunStrategyJSONTmpl)))
7258

@@ -80,7 +66,6 @@ func TestHandleValidateVMAdmissionRequestBlocked(t *testing.T) {
8066

8167
test.VerifyRequestAllowed(t, body, "b6ae2ab4-782b-11ee-b962-0242ac120002")
8268
})
83-
8469
}
8570

8671
func newVMRequestValidator(t *testing.T) *VMRequestValidator {
@@ -114,7 +99,8 @@ type VMAdmReviewTmplParams struct {
11499
Username string
115100
}
116101

117-
var createVMWithRunStrategyJSONTmpl = `{
102+
func createVMWithRunStrategyJSONTmpl(runStrategy string) string {
103+
return fmt.Sprintf(`{
118104
"kind": "AdmissionReview",
119105
"apiVersion": "admission.k8s.io/v1",
120106
"request": {
@@ -156,7 +142,7 @@ var createVMWithRunStrategyJSONTmpl = `{
156142
"namespace": "{{.Username}}-dev"
157143
},
158144
"spec": {
159-
"runStrategy": "Always"
145+
"runStrategy": "%s"
160146
}
161147
},
162148
"oldObject": null,
@@ -168,7 +154,8 @@ var createVMWithRunStrategyJSONTmpl = `{
168154
"fieldValidation": "Ignore"
169155
}
170156
}
171-
}`
157+
}`, runStrategy)
158+
}
172159

173160
var createVMWithoutRunStrategyJSONTmpl = `{
174161
"kind": "AdmissionReview",

0 commit comments

Comments
 (0)