diff --git a/Makefile b/Makefile index 60d315c30..4f40f9c99 100644 --- a/Makefile +++ b/Makefile @@ -455,3 +455,5 @@ delete-cluster: install-kind workflow_test_image_build-and-push: docker build -t localhost:5001/testimage/sonataflow-minimal-example:0.1 ./test/testdata/workflow/docker-image/ docker push localhost:5001/testimage/sonataflow-minimal-example:0.1 + docker build --build-arg WORKFLOW_FILE=broken-workflow.sw.json -t localhost:5001/testimage/sonataflow-minimal-example-broken:0.1 ./test/testdata/workflow/docker-image/ + docker push localhost:5001/testimage/sonataflow-minimal-example-broken:0.1 diff --git a/internal/controller/validation/common.go b/internal/controller/validation/common.go index d3860ec0f..d91c579c9 100644 --- a/internal/controller/validation/common.go +++ b/internal/controller/validation/common.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func KindRegistryName(ctx context.Context) (string, error) { +var kindRegistryName = func(ctx context.Context) (string, error) { config := corev1.ConfigMap{} err := utils.GetClient().Get(ctx, client.ObjectKey{Namespace: "kube-public", Name: "local-registry-hosting"}, &config) if err == nil { @@ -43,6 +43,7 @@ func KindRegistryName(ctx context.Context) (string, error) { } func checkUrlHasPrefix(input string) bool { + input = strings.ToLower(input) prefixes := []string{"http://", "https://", "docker://"} for _, prefix := range prefixes { if strings.HasPrefix(input, prefix) { @@ -69,7 +70,7 @@ func hostAndPortFromUri(input string) (string, string, error) { // check if host is ip address if net.ParseIP(host) == nil { - hosts, err := net.LookupIP(host) + hosts, err := resolve(host) if err != nil { return "", "", fmt.Errorf("Failed to resolve domain: %v\n", err) } @@ -82,8 +83,8 @@ func hostAndPortFromUri(input string) (string, string, error) { return host, port, nil } -func ImageStoredInKindRegistry(ctx context.Context, image string) (bool, string, error) { - kindRegistryHostAndPort, err := KindRegistryName(ctx) +func imageStoredInKindRegistry(ctx context.Context, image string) (bool, string, error) { + kindRegistryHostAndPort, err := kindRegistryName(ctx) if err != nil { return false, "", fmt.Errorf("Failed to get kind registry name: %v\n", err) } @@ -110,3 +111,7 @@ func getipv4(ips []net.IP) (string, error) { } return "", fmt.Errorf("No ipv4 address found") } + +var resolve = func(host string) ([]net.IP, error) { + return net.LookupIP(host) +} diff --git a/internal/controller/validation/common_test.go b/internal/controller/validation/common_test.go new file mode 100644 index 000000000..f24d25ef6 --- /dev/null +++ b/internal/controller/validation/common_test.go @@ -0,0 +1,118 @@ +// Copyright 2024 Apache Software Foundation (ASF) +// +// 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 validation + +import ( + "context" + "net" + "testing" +) + +func TestCheckUrlHasPrefix(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"http://example.com", true}, + {"https://example.com", true}, + {"docker://my-image", true}, + + {"example.com", false}, + {"127.0.0.1", false}, + {"my-image", false}, + + {"HTTP://example.com", true}, + {"HTTPS://example.com", true}, + {"DOCKER://my-image", true}, + } + + for _, test := range tests { + result := checkUrlHasPrefix(test.input) + if result != test.expected { + t.Errorf("checkUrlHasPrefix(%q) = %v; expected %v", test.input, result, test.expected) + } + } +} + +func TestHostAndPortFromUri(t *testing.T) { + tests := []struct { + input string + expectedHost string + expectedPort string + expectingError bool + }{ + {"http://192.168.1.1:8080", "192.168.1.1", "8080", false}, + {"https://192.168.1.1:8080", "192.168.1.1", "8080", false}, + {"docker://192.168.1.1:8080", "192.168.1.1", "8080", false}, + {"http://localhost:5000", "127.0.0.1", "5000", false}, + {"https://localhost:5000", "127.0.0.1", "5000", false}, + {"docker://localhost:5000", "127.0.0.1", "5000", false}, + {"localhost:5000", "127.0.0.1", "5000", false}, + {"ftp://example.com", "", "", true}, + {"invalid_url", "", "", true}, + } + + for _, test := range tests { + host, port, err := hostAndPortFromUri(test.input) + + if (err != nil) != test.expectingError { + t.Errorf("hostAndPortFromUri(%q) error = %v, expected error = %v", test.input, err, test.expectingError) + } + if host != test.expectedHost || port != test.expectedPort { + t.Errorf("hostAndPortFromUri(%q) = (%q, %q), expected (%q, %q)", test.input, host, port, test.expectedHost, test.expectedPort) + } + } +} + +func TestImageStoredInKindRegistry(t *testing.T) { + originalKindRegistryNameFunc := kindRegistryName + originalResolveFunc := resolve + defer func() { + kindRegistryName = originalKindRegistryNameFunc + resolve = originalResolveFunc + }() + + tests := []struct { + image string + expectedResult bool + expectedHost string + resolvedIp string + expectingError bool + }{ + {"kind-registry:5000/my-image", true, "172.18.0.4:5000", "172.18.0.4", false}, + {"172.18.0.4:5000/my-image", true, "172.18.0.4:5000", "0.0.0.0", false}, + {"docker.io/my-image", false, "", "1.1.1.1", false}, + {"172.18.0.4:6000/my-image", false, "", "0.0.0.0", false}, + } + + for _, test := range tests { + kindRegistryName = func(ctx context.Context) (string, error) { + return "172.18.0.4:5000", nil + } + resolve = func(host string) ([]net.IP, error) { + return []net.IP{net.ParseIP(test.resolvedIp)}, nil + } + + result, hostAndPort, err := imageStoredInKindRegistry(context.Background(), test.image) + + if (err != nil) != test.expectingError { + t.Errorf("ImageStoredInKindRegistry(%q) error = %v, expected error = %v", test.image, err, test.expectingError) + } + + if result != test.expectedResult || hostAndPort != test.expectedHost { + t.Errorf("ImageStoredInKindRegistry(%q) = (%v, %q), expected (%v, %q)", test.image, result, hostAndPort, test.expectedResult, test.expectedHost) + } + } +} diff --git a/internal/controller/validation/image_url_sanitizer.go b/internal/controller/validation/image_url_sanitizer.go index ec5ccfc30..364ecdc90 100644 --- a/internal/controller/validation/image_url_sanitizer.go +++ b/internal/controller/validation/image_url_sanitizer.go @@ -28,7 +28,7 @@ import ( type imageUrlSanitizer struct{} func (v *imageUrlSanitizer) Validate(ctx context.Context, client client.Client, sonataflow *operatorapi.SonataFlow, req ctrl.Request) error { - isInKindRegistry, kindRegistryUrl, err := ImageStoredInKindRegistry(ctx, sonataflow.Spec.PodTemplate.Container.Image) + isInKindRegistry, kindRegistryUrl, err := imageStoredInKindRegistry(ctx, sonataflow.Spec.PodTemplate.Container.Image) if err != nil { return err } diff --git a/internal/controller/validation/image_validator.go b/internal/controller/validation/image_validator.go index dad1991ff..fac4d73ee 100644 --- a/internal/controller/validation/image_validator.go +++ b/internal/controller/validation/image_validator.go @@ -52,7 +52,7 @@ func NewImageValidator() Validator { } func validateImage(ctx context.Context, sonataflow *operatorapi.SonataFlow) (bool, error) { - isInKindRegistry, _, err := ImageStoredInKindRegistry(ctx, sonataflow.Spec.PodTemplate.Container.Image) + isInKindRegistry, _, err := imageStoredInKindRegistry(ctx, sonataflow.Spec.PodTemplate.Container.Image) if err != nil { return false, err } @@ -82,8 +82,6 @@ func validateImage(ctx context.Context, sonataflow *operatorapi.SonataFlow) (boo } func remoteImage(sonataflow *operatorapi.SonataFlow) (v1.Image, error) { - fmt.Println("remoteImage") - imageRef, err := name.ParseReference(sonataflow.Spec.PodTemplate.Container.Image) if err != nil { return nil, err @@ -97,8 +95,6 @@ func remoteImage(sonataflow *operatorapi.SonataFlow) (v1.Image, error) { } func kindRegistryImage(sonataflow *operatorapi.SonataFlow) (v1.Image, error) { - fmt.Println("kindRegistryImage") - transportOptions := []remote.Option{ remote.WithTransport(&http.Transport{ Proxy: http.ProxyFromEnvironment, diff --git a/test/e2e/workflow_test.go b/test/e2e/workflow_test.go index 9d7b5c93b..1ee3cb9ce 100644 --- a/test/e2e/workflow_test.go +++ b/test/e2e/workflow_test.go @@ -145,6 +145,49 @@ var _ = Describe("Workflow Non-Persistence Use Cases :: ", Label("flows-non-pers }, 3*time.Minute, time.Second).Should(Succeed()) }) + It("should not deploy the Simple Workflow because image contains no workflow definition", func() { + By("creating an instance of the SonataFlow Operand(CR)") + EventuallyWithOffset(1, func() error { + cmd := exec.Command("kubectl", "apply", "-f", filepath.Join(projectDir, + "test/testdata/"+test.SonataFlowSimpleOpsYamlCRImageContainsNoWorkflow), "-n", targetNamespace) + _, err := utils.Run(cmd) + return err + }, 3*time.Minute, time.Second).Should(Succeed()) + + By("verifying that the workflow is not in a running state within one minute") + ConsistentlyWithOffset(1, func() bool { + return verifyWorkflowIsInRunningState("simple", targetNamespace) + }, 1*time.Minute, 10*time.Second).Should(BeFalse(), "Workflow unexpectedly reached the running state") + + EventuallyWithOffset(1, func() error { + cmd := exec.Command("kubectl", "delete", "-f", filepath.Join(projectDir, + "test/testdata/"+test.SonataFlowSimpleOpsYamlCRImageContainsNoWorkflow), "-n", targetNamespace) + _, err := utils.Run(cmd) + return err + }, 3*time.Minute, time.Second).Should(Succeed()) + }) + + It("should not deploy the Simple Workflow because image contains broken (not equals to flow from yaml) workflow definition", func() { + By("creating an instance of the SonataFlow Operand(CR)") + EventuallyWithOffset(1, func() error { + cmd := exec.Command("kubectl", "apply", "-f", filepath.Join(projectDir, + "test/testdata/"+test.SonataFlowSimpleOpsYamlCRImageContainsBrokenWorkflow), "-n", targetNamespace) + _, err := utils.Run(cmd) + return err + }, 3*time.Minute, time.Second).Should(Succeed()) + + By("verifying that the workflow is not in a running state within one minute") + ConsistentlyWithOffset(1, func() bool { + return verifyWorkflowIsInRunningState("simple", targetNamespace) + }, 1*time.Minute, 10*time.Second).Should(BeFalse(), "Workflow unexpectedly reached the running state") + + EventuallyWithOffset(1, func() error { + cmd := exec.Command("kubectl", "delete", "-f", filepath.Join(projectDir, + "test/testdata/"+test.SonataFlowSimpleOpsYamlCRImageContainsBrokenWorkflow), "-n", targetNamespace) + _, err := utils.Run(cmd) + return err + }, 3*time.Minute, time.Second).Should(Succeed()) + }) }) }) diff --git a/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops-broken-workflow-in-image.yaml b/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops-broken-workflow-in-image.yaml new file mode 100644 index 000000000..a0d6dcc6a --- /dev/null +++ b/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops-broken-workflow-in-image.yaml @@ -0,0 +1,35 @@ +# Copyright 2023 Red Hat, Inc. and/or its affiliates +# +# 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. + +apiVersion: sonataflow.org/v1alpha08 +kind: SonataFlow +metadata: + name: simple + annotations: + sonataflow.org/description: Simple example on k8s! + sonataflow.org/version: 0.0.1 + labels: + test: test +spec: + podTemplate: + container: + image: testimage/sonataflow-minimal-example-broken:0.1 + flow: + start: HelloWorld + states: + - name: HelloWorld + type: inject + data: + message: Hello World + end: true diff --git a/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops-no-workflow-in-image.yaml b/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops-no-workflow-in-image.yaml new file mode 100644 index 000000000..b1a11d631 --- /dev/null +++ b/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops-no-workflow-in-image.yaml @@ -0,0 +1,35 @@ +# Copyright 2023 Red Hat, Inc. and/or its affiliates +# +# 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. + +apiVersion: sonataflow.org/v1alpha08 +kind: SonataFlow +metadata: + name: simple + annotations: + sonataflow.org/description: Simple example on k8s! + sonataflow.org/version: 0.0.1 + labels: + test: test +spec: + podTemplate: + container: + image: quay.io/kiegroup/sonataflow-minimal-example:latest + flow: + start: HelloWorld + states: + - name: HelloWorld + type: inject + data: + message: Hello World + end: true diff --git a/test/testdata/workflow/docker-image/broken-workflow.sw.json b/test/testdata/workflow/docker-image/broken-workflow.sw.json new file mode 100644 index 000000000..3ef8543c5 --- /dev/null +++ b/test/testdata/workflow/docker-image/broken-workflow.sw.json @@ -0,0 +1,18 @@ +{ + "id": "hello", + "version": "1.0", + "specVersion": "0.8.0", + "name": "Hello World", + "description": "Description", + "start": "HelloWorld", + "states": [ + { + "name": "HelloWorld", + "type": "inject", + "data": { + "message": "Hello World" + }, + "end": true + } + ] +} diff --git a/test/yaml.go b/test/yaml.go index a0fbd8bb6..c93f69a4c 100644 --- a/test/yaml.go +++ b/test/yaml.go @@ -59,8 +59,11 @@ const ( sonataFlowBuilderConfig = "sonataflow-operator-builder-config_v1_configmap.yaml" sonataFlowBuildSucceed = "sonataflow.org_v1alpha08_sonataflowbuild.yaml" knativeDefaultBrokerCR = "knative_default_broker.yaml" - e2eSamples = "test/testdata/" - manifestsPath = "bundle/manifests/" + + SonataFlowSimpleOpsYamlCRImageContainsNoWorkflow = "sonataflow.org_v1alpha08_sonataflow-simpleops-no-workflow-in-image.yaml" + SonataFlowSimpleOpsYamlCRImageContainsBrokenWorkflow = "sonataflow.org_v1alpha08_sonataflow-simpleops-broken-workflow-in-image.yaml" + e2eSamples = "test/testdata/" + manifestsPath = "bundle/manifests/" ) var projectDir = ""