From f00ab1b379f276a2dc4b84540054d820afd77b21 Mon Sep 17 00:00:00 2001 From: Jiri Podivin Date: Wed, 3 Apr 2024 14:47:17 +0200 Subject: [PATCH] Moving CRD checks into webhook Tests for webhook are included. Signed-off-by: Jiri Podivin --- api/go.mod | 9 +- api/go.sum | 8 +- api/v1beta1/openstackansibleee_webhook.go | 96 ++++++++++++++++- controllers/openstack_ansibleee_controller.go | 50 +-------- go.mod | 2 +- tests/functional/ansibleee_controller_test.go | 17 --- tests/functional/base_test.go | 9 ++ .../openstackansibleee_webhook_test.go | 100 ++++++++++++++++++ tests/functional/suite_test.go | 15 +++ 9 files changed, 233 insertions(+), 73 deletions(-) create mode 100644 tests/functional/openstackansibleee_webhook_test.go diff --git a/api/go.mod b/api/go.mod index 29c34ca0..52352472 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,10 +3,12 @@ module github.com/openstack-k8s-operators/openstack-ansibleee-operator/api go 1.20 require ( + github.com/go-playground/validator/v10 v10.19.0 github.com/onsi/ginkgo/v2 v2.17.1 github.com/onsi/gomega v1.32.0 github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20240326081751-56015b1ae3f6 github.com/openstack-k8s-operators/lib-common/modules/storage v0.3.1-0.20240326081751-56015b1ae3f6 + gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.28.8 k8s.io/apimachinery v0.28.8 k8s.io/client-go v0.28.8 @@ -20,11 +22,14 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -37,6 +42,7 @@ require ( github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -48,9 +54,9 @@ require ( github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect @@ -63,7 +69,6 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.28.8 // indirect k8s.io/component-base v0.28.8 // indirect diff --git a/api/go.sum b/api/go.sum index 21f9e736..ea58598a 100644 --- a/api/go.sum +++ b/api/go.sum @@ -17,6 +17,7 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -29,6 +30,10 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -68,6 +73,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= @@ -113,7 +119,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -129,6 +134,7 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/api/v1beta1/openstackansibleee_webhook.go b/api/v1beta1/openstackansibleee_webhook.go index 17708676..9723c0b5 100644 --- a/api/v1beta1/openstackansibleee_webhook.go +++ b/api/v1beta1/openstackansibleee_webhook.go @@ -24,8 +24,14 @@ package v1beta1 import ( "fmt" + "regexp" + "github.com/go-playground/validator/v10" + "gopkg.in/yaml.v2" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -81,13 +87,78 @@ var _ webhook.Validator = &OpenStackAnsibleEE{} // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (r *OpenStackAnsibleEE) ValidateCreate() (admission.Warnings, error) { openstackansibleeelog.Info("validate create", "name", r.Name) + var errors field.ErrorList + + errors = append(errors, r.Spec.ValidateCreate()...) + if len(errors) != 0 { + openstackansibleeelog.Info("validation failed", "name", r.Name) + return nil, apierrors.NewInvalid( + schema.GroupKind{Group: "ansibleee.openstack.org", Kind: "OpenStakAnsibleEE"}, + r.Name, + errors, + ) + } + return nil, nil +} + +func (spec *OpenStackAnsibleEESpec) ValidateCreate() field.ErrorList { + // Setting up input validation, including custom validators + validate := validator.New() + var errors field.ErrorList + if valErr := validate.RegisterValidation("play", isPlay); valErr != nil { + errors = append(errors, + field.InternalError( + field.NewPath("spec"), + fmt.Errorf("error registering input validation"))) + } + if valErr := validate.RegisterValidation("fqcn", isFQCN); valErr != nil { + errors = append(errors, + field.InternalError( + field.NewPath("spec"), + fmt.Errorf("error registering input validation"))) + } + if len(spec.Play) > 0 { + valErr := validate.Var(spec.Play, "play") + if valErr != nil { + errors = append(errors, field.Invalid( + field.NewPath("spec.play"), + spec.Play, + fmt.Sprintf( + "error checking sanity of an inline play: %s %s", + spec.Play, valErr), + )) + } + } else if len(spec.Playbook) > 0 { + // As we set "playbook.yaml" as default + // we need to ensure that Play and Role are empty before addPlaybook + if validate.Var(spec.Playbook, "fqcn") != nil { + // First we check if the playbook isn't imported from a collection + // if it is not we assume the variable holds a path. + valErr := validate.Var(spec.Playbook, "filepath") + if valErr != nil { + errors = append(errors, field.Invalid( + field.NewPath("spec.playbook"), + spec.Playbook, + fmt.Sprintf( + "error checking sanity of playbook name/path: %s %s", + spec.Playbook, valErr), + )) + } + } - for _, value := range r.Spec.Env { + } + + for _, value := range spec.Env { if value.Name == "ANSIBLE_ENABLE_TASK_DEBUGGER" { - return nil, fmt.Errorf("ansible-runner does not support ANSIBLE_ENABLE_TASK_DEBUGGER") + errors = append(errors, field.Invalid( + field.NewPath("spec.env"), + spec.Env, + fmt.Sprintf("ansible-runner does not support ANSIBLE_ENABLE_TASK_DEBUGGER"), + )) } } - return nil, nil + + return errors } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type @@ -105,3 +176,22 @@ func (r *OpenStackAnsibleEE) ValidateDelete() (admission.Warnings, error) { // TODO(user): fill in your validation logic upon object deletion. return nil, nil } + +// isPlay checks if the free form document has attributes of ansible play +// Specifically if it is a parsable yaml with list as a root element +func isPlay(document validator.FieldLevel) bool { + var play []map[string]interface{} + err := yaml.Unmarshal([]byte(document.Field().String()), &play) + return err == nil +} + +// isFQCN checks if the string matches regular expression of ansible FQCN +// The function doesn't check if the collection exists or is accessible +// function only accepts FQCNs as defined by +// https://old-galaxy.ansible.com/docs/contributing/namespaces.html#namespace-limitations +// Regex derived from ansible-lint rule +func isFQCN(name validator.FieldLevel) bool { + pattern, compileErr := regexp.Compile(`^\w+(\.\w+){2,}$`) + match := pattern.Match([]byte(name.Field().String())) + return match && compileErr == nil +} diff --git a/controllers/openstack_ansibleee_controller.go b/controllers/openstack_ansibleee_controller.go index e9b59e3d..6137c121 100644 --- a/controllers/openstack_ansibleee_controller.go +++ b/controllers/openstack_ansibleee_controller.go @@ -18,7 +18,6 @@ package controllers import ( "fmt" - "regexp" "sort" "strings" "time" @@ -33,7 +32,6 @@ import ( "context" "github.com/go-logr/logr" - "github.com/go-playground/validator/v10" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" job "github.com/openstack-k8s-operators/lib-common/modules/common/job" @@ -271,16 +269,6 @@ func (r *OpenStackAnsibleEEReconciler) jobForOpenStackAnsibleEE(ctx context.Cont Log := r.GetLogger(ctx) labels := instance.GetObjectMeta().GetLabels() - // Setting up input validation, including custom validators - validate := validator.New() - - if valErr := validate.RegisterValidation("play", isPlay); valErr != nil { - return nil, fmt.Errorf("error registering input validation") - } - if valErr := validate.RegisterValidation("fqcn", isFQCN); valErr != nil { - return nil, fmt.Errorf("error registering input validation") - } - ls := labelsForOpenStackAnsibleEE(instance.Name, labels) args := instance.Spec.Args @@ -373,27 +361,10 @@ func (r *OpenStackAnsibleEEReconciler) jobForOpenStackAnsibleEE(ctx context.Cont } if len(instance.Spec.Play) > 0 { - valErr := validate.Var(instance.Spec.Play, "play") - if valErr != nil { - return nil, fmt.Errorf( - "error checking sanity of an inline play: %s %w", - instance.Spec.Play, valErr) - } setRunnerEnvVar(h, "RUNNER_PLAYBOOK", instance.Spec.Play, "play", job, hashes) } else if len(playbook) > 0 { // As we set "playbook.yaml" as default - // we need to ensure that Play and Role are empty before addPlaybook - if validate.Var(playbook, "fqcn") != nil { - // First we check if the playbook isn't imported from a collection - // if it is not we assume the variable holds a path. - valErr := validate.Var(playbook, "filepath") - if valErr != nil { - return nil, fmt.Errorf( - "error checking sanity of playbook name/path: %s %w", - playbook, valErr) - } - } - + // we need to ensure that Play is empty before adding playbook setRunnerEnvVar(h, "RUNNER_PLAYBOOK", playbook, "playbooks", job, hashes) } @@ -556,22 +527,3 @@ func (r *OpenStackAnsibleEEReconciler) SetupWithManager(mgr ctrl.Manager) error Owns(&batchv1.Job{}). Complete(r) } - -// isPlay checks if the free form document has attributes of ansible play -// Specifically if it is a parsable yaml with list as a root element -func isPlay(document validator.FieldLevel) bool { - var play []map[string]interface{} - err := yaml.Unmarshal([]byte(document.Field().String()), &play) - return err == nil -} - -// isFQCN checks if the string matches regular expression of ansible FQCN -// The function doesn't check if the collection exists or is accessible -// function only accepts FQCNs as defined by -// https://galaxy.ansible.com/docs/contributing/namespaces.html#namespace-limitations -// Regex derive -func isFQCN(name validator.FieldLevel) bool { - pattern, compileErr := regexp.Compile(`^\w+(\.\w+){2,}$`) - match := pattern.Match([]byte(name.Field().String())) - return match && compileErr == nil -} diff --git a/go.mod b/go.mod index cd2c84e5..4a6d166c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ replace github.com/openstack-k8s-operators/openstack-ansibleee-operator/api => . require ( github.com/go-logr/logr v1.4.1 - github.com/go-playground/validator/v10 v10.19.0 github.com/google/uuid v1.6.0 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 github.com/onsi/ginkgo/v2 v2.17.1 @@ -35,6 +34,7 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/tests/functional/ansibleee_controller_test.go b/tests/functional/ansibleee_controller_test.go index 937eaea5..007d2485 100644 --- a/tests/functional/ansibleee_controller_test.go +++ b/tests/functional/ansibleee_controller_test.go @@ -188,23 +188,6 @@ var _ = Describe("Ansibleee controller", func() { }) - Context("with invalid playbook name/path", func() { - BeforeEach(func() { - DeferCleanup(th.DeleteInstance, CreateAnsibleeeWithParams( - ansibleeeName, "/", "test-image", "", "", map[string]interface{}{}, []map[string]interface{}{})) - }) - It("runs a job and reports when it succeeds", func() { - th.ExpectConditionWithDetails( - ansibleeeName, - ConditionGetterFunc(AnsibleeeConditionGetter), - v1beta1.AnsibleExecutionJobReadyCondition, - corev1.ConditionUnknown, - condition.InitReason, - "AnsibleExecutionJob not started", - ) - }) - }) - Context("with an inline play", func() { BeforeEach(func() { DeferCleanup(th.DeleteInstance, CreateAnsibleeeWithParams( diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index 843a8418..25553351 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -34,6 +34,15 @@ const ( - name: Using debug statement ansible.builtin.debug: msg: "Hello, world this is ansibleee-play.yaml"` + + // This constant is meant to represent malformed inline play + malformedPlay = ` + - name: Print hello world + hosts: all + tasks: + - name: Using debug statement + ansible.builtin.debug: + msg: "Hello, world this is ansibleee-play.yaml"` ) func GetAnsibleee(name types.NamespacedName) *v1beta1.OpenStackAnsibleEE { diff --git a/tests/functional/openstackansibleee_webhook_test.go b/tests/functional/openstackansibleee_webhook_test.go new file mode 100644 index 00000000..8dc7c864 --- /dev/null +++ b/tests/functional/openstackansibleee_webhook_test.go @@ -0,0 +1,100 @@ +package functional_test + +import ( + "fmt" + + . "github.com/onsi/gomega" //revive:disable:dot-imports + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var _ = Describe("OpenStackAnsibleEE Webhook", func() { + + When("User creates a valid ansibleee resource", func() { + + It("should be accepted", func() { + DeferCleanup(th.DeleteInstance, CreateAnsibleee(ansibleeeName)) + + }) + }) + When("User creates ansibleee resource referencing valid playbook", func() { + It("should be accepted", func() { + DeferCleanup(th.DeleteInstance, CreateAnsibleeeWithParams( + ansibleeeName, + "some.valid.fqcn", + "test-image", + "", + "", + map[string]interface{}{}, + []map[string]interface{}{})) + }) + }) + When("User creates ansibleee resource with a valid inline play", func() { + It("should be accepted", func() { + DeferCleanup(th.DeleteInstance, CreateAnsibleeeWithParams( + ansibleeeName, + "", + "test-image", + play, + "", + map[string]interface{}{}, + []map[string]interface{}{}, + )) + }) + }) + When("User creates ansibleee resource with invalid playbook reference", func() { + It("is rejected", func() { + Eventually( + func(_ Gomega) string { + newInstance := map[string]interface{}{ + "apiVersion": "ansibleee.openstack.org/v1beta1", + "kind": "OpenStackAnsibleEE", + "metadata": map[string]interface{}{ + "name": ansibleeeName.Name, + "namespace": ansibleeeName.Namespace, + }, + "spec": map[string]interface{}{ + // this can be removed as soon as webhook is enabled in the + // test env + "image": "test-image", + "playbook": "/", + "play": "", + "cmdline": "", + }, + } + unstructuredObj := &unstructured.Unstructured{Object: newInstance} + _, err := controllerutil.CreateOrPatch(th.Ctx, th.K8sClient, unstructuredObj, func() error { return nil }) + return fmt.Sprintf("%s", err) + }).Should(ContainSubstring("error checking sanity of playbook name/path")) + + }) + }) + When("User creates ansibleee resource with invalid inline playbook", func() { + It("is rejected", func() { + Eventually( + func(_ Gomega) string { + newInstance := map[string]interface{}{ + "apiVersion": "ansibleee.openstack.org/v1beta1", + "kind": "OpenStackAnsibleEE", + "metadata": map[string]interface{}{ + "name": ansibleeeName.Name, + "namespace": ansibleeeName.Namespace, + }, + "spec": map[string]interface{}{ + // this can be removed as soon as webhook is enabled in the + // test env + "image": "test-image", + "playbook": "", + "play": malformedPlay, + "cmdline": "", + }, + } + unstructuredObj := &unstructured.Unstructured{Object: newInstance} + _, err := controllerutil.CreateOrPatch(th.Ctx, th.K8sClient, unstructuredObj, func() error { return nil }) + return fmt.Sprintf("%s", err) + }).Should(ContainSubstring("error checking sanity of an inline play")) + }) + }) +}) diff --git a/tests/functional/suite_test.go b/tests/functional/suite_test.go index 45c0559e..992240da 100644 --- a/tests/functional/suite_test.go +++ b/tests/functional/suite_test.go @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/webhook" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -78,6 +79,10 @@ var _ = BeforeSuite(func() { testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + LocalServingHost: "127.0.0.1", + }, } ctx, cancel = context.WithCancel(context.TODO()) @@ -101,6 +106,7 @@ var _ = BeforeSuite(func() { th = helpers.NewTestHelper(ctx, k8sClient, timeout, interval, logger) Expect(th).NotTo(BeNil()) + webhookInstallOptions := &testEnv.WebhookInstallOptions // Start the controller-manager in a goroutine k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ @@ -112,9 +118,18 @@ var _ = BeforeSuite(func() { BindAddress: "0", }, LeaderElection: false, + WebhookServer: webhook.NewServer( + webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), }) Expect(err).ToNot(HaveOccurred()) + err = (&ansibleeev1.OpenStackAnsibleEE{}).SetupWebhookWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + kclient, err := kubernetes.NewForConfig(cfg) Expect(err).ToNot(HaveOccurred(), "failed to create kclient")