From 46e17e700c23ae3ef3f51dc98cdd8c8e0e41e527 Mon Sep 17 00:00:00 2001 From: Davide Salerno Date: Fri, 2 Jun 2023 12:51:59 +0200 Subject: [PATCH] =?UTF-8?q?[KOGITO-8648]=20[KSW-Operator]=20Implement=20th?= =?UTF-8?q?e=20Knative=20Addressable=20interf=E2=80=A6=20(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [KOGITO-8648] [KSW-Operator] Implement the Knative Addressable interface in dev profile Signed-off-by: Davide Salerno Update controllers/profiles/status_enricher_dev_test.go Co-authored-by: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com> Fixing namespace into unit tests Signed-off-by: Davide Salerno Update utils/kubernetes/service.go Co-authored-by: Filippe Spolti Changes coming from code review Signed-off-by: Davide Salerno * fix generate-all --------- Signed-off-by: Davide Salerno Co-authored-by: radtriste --- ...a08_kogitoserverlessplatform_minikube.yaml | 7 ++ ...serverlessplatform_withCache_minikube.yaml | 1 + controllers/profiles/status_enricher_dev.go | 19 +++++ .../profiles/status_enricher_dev_test.go | 84 +++++++++++++++++++ hack/local/run-e2e.sh | 2 +- test/e2e/workflow_test.go | 29 ++++++- test/yaml.go | 1 + utils/kubernetes/service.go | 44 ++++++++++ utils/kubernetes/service_test.go | 75 +++++++++++++++++ 9 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_minikube.yaml create mode 100644 controllers/profiles/status_enricher_dev_test.go create mode 100644 utils/kubernetes/service.go create mode 100644 utils/kubernetes/service_test.go diff --git a/config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_minikube.yaml b/config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_minikube.yaml new file mode 100644 index 000000000..f67fd9abf --- /dev/null +++ b/config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_minikube.yaml @@ -0,0 +1,7 @@ +apiVersion: sw.kogito.kie.org/v1alpha08 +kind: KogitoServerlessPlatform +metadata: + name: kogito-workflow-platform +spec: + platform: + buildStrategy: operator \ No newline at end of file diff --git a/config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_withCache_minikube.yaml b/config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_withCache_minikube.yaml index 6dc3f4dff..1f898a017 100644 --- a/config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_withCache_minikube.yaml +++ b/config/samples/sw.kogito_v1alpha08_kogitoserverlessplatform_withCache_minikube.yaml @@ -4,6 +4,7 @@ metadata: name: kogito-workflow-platform spec: platform: + baseImage: quay.io/kiegroup/kogito-swf-builder-nightly:latest buildStrategyOptions: KanikoBuildCacheEnabled: "true" diff --git a/controllers/profiles/status_enricher_dev.go b/controllers/profiles/status_enricher_dev.go index b386dcd87..b38c020e3 100644 --- a/controllers/profiles/status_enricher_dev.go +++ b/controllers/profiles/status_enricher_dev.go @@ -20,6 +20,7 @@ import ( openshiftv1 "github.com/openshift/api/route/v1" "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" "github.com/kiegroup/kogito-serverless-operator/controllers/workflowdef" @@ -28,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" operatorapi "github.com/kiegroup/kogito-serverless-operator/api/v1alpha08" + "github.com/kiegroup/kogito-serverless-operator/utils/kubernetes" ) func defaultDevStatusEnricher(ctx context.Context, c client.Client, workflow *operatorapi.KogitoServerlessWorkflow) (client.Object, error) { @@ -36,10 +38,12 @@ func defaultDevStatusEnricher(ctx context.Context, c client.Client, workflow *op // - Address the service can be reached // - Node port used service := &v1.Service{} + err := c.Get(ctx, types.NamespacedName{Namespace: workflow.Namespace, Name: workflow.Name}, service) if err != nil { return nil, err } + //If the service has got a Port that is a nodePort we have to use it to create the workflow's NodePort Endpoint if service.Spec.Ports != nil && len(service.Spec.Ports) > 0 { if port := findNodePortFromPorts(service.Spec.Ports); port > 0 { @@ -66,6 +70,14 @@ func defaultDevStatusEnricher(ctx context.Context, c client.Client, workflow *op } workflow.Status.Endpoint = url } + + address, err := kubernetes.RetrieveServiceURL(service) + if err != nil { + return nil, err + } + workflow.Status.Address = duckv1.Addressable{ + URL: address, + } } return workflow, nil @@ -84,8 +96,15 @@ func devStatusEnricherForOpenShift(ctx context.Context, client client.Client, wo } else { url = apis.HTTP(route.Spec.Host) } + url.Path = workflow.Name workflow.Status.Endpoint = url + if err != nil { + return nil, err + } + workflow.Status.Address = duckv1.Addressable{ + URL: url, + } return workflow, nil } diff --git a/controllers/profiles/status_enricher_dev_test.go b/controllers/profiles/status_enricher_dev_test.go new file mode 100644 index 000000000..06076246d --- /dev/null +++ b/controllers/profiles/status_enricher_dev_test.go @@ -0,0 +1,84 @@ +// 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. + +package profiles + +import ( + "context" + "strings" + "testing" + + openshiftv1 "github.com/openshift/api/route/v1" + "github.com/stretchr/testify/assert" + "knative.dev/pkg/apis" + + apiv08 "github.com/kiegroup/kogito-serverless-operator/api/v1alpha08" + "github.com/kiegroup/kogito-serverless-operator/test" +) + +func Test_enrichmentStatusOnK8s(t *testing.T) { + t.Run("verify that the service URL is returned with the default cluster name on default namespace", func(t *testing.T) { + + workflow := test.GetKogitoServerlessWorkflow("../../config/samples/"+test.KogitoServerlessWorkflowSampleDevModeYamlCR, t.Name()) + workflow.Namespace = toK8SNamespace(t.Name()) + service, err := defaultServiceCreator(workflow) + client := test.NewKogitoClientBuilder().WithRuntimeObjects(workflow, service).Build() + obj, err := defaultDevStatusEnricher(context.TODO(), client, workflow) + + reflectWorkflow := obj.(*apiv08.KogitoServerlessWorkflow) + assert.NoError(t, err) + assert.NotNil(t, obj) + assert.NotNil(t, reflectWorkflow.Status.Address) + assert.Equal(t, reflectWorkflow.Status.Address.URL.String(), "http://"+workflow.Name+"."+workflow.Namespace+".svc.cluster.local/"+workflow.Name) + + }) + + t.Run("verify that the service URL won't be generated if an invalid namespace is used", func(t *testing.T) { + + workflow := test.GetKogitoServerlessWorkflow("../../config/samples/"+test.KogitoServerlessWorkflowSampleDevModeYamlCR, t.Name()) + workflow.Namespace = t.Name() + service, err := defaultServiceCreator(workflow) + client := test.NewKogitoClientBuilder().WithRuntimeObjects(workflow, service).Build() + _, err = defaultDevStatusEnricher(context.TODO(), client, workflow) + assert.Error(t, err) + + }) +} + +func Test_enrichmentStatusOnOCP(t *testing.T) { + t.Run("verify that the service URL is returned with the default cluster name on default namespace", func(t *testing.T) { + workflow := test.GetKogitoServerlessWorkflow("../../config/samples/"+test.KogitoServerlessWorkflowSampleDevModeYamlCR, t.Name()) + workflow.Namespace = toK8SNamespace(t.Name()) + service, err := defaultServiceCreator(workflow) + route := &openshiftv1.Route{} + route.Name = workflow.Name + route.Namespace = workflow.Namespace + route.Spec.Host = workflow.Name + "." + workflow.Namespace + ".apps-crc.testing" + client := test.NewKogitoClientBuilderWithOpenShift().WithRuntimeObjects(workflow, service, route).Build() + obj, err := devStatusEnricherForOpenShift(context.TODO(), client, workflow) + + reflectWorkflow := obj.(*apiv08.KogitoServerlessWorkflow) + assert.NoError(t, err) + assert.NotNil(t, obj) + assert.NotNil(t, reflectWorkflow.Status.Address) + expectedURL := apis.HTTP(route.Spec.Host) + expectedURL.Path = workflow.Name + assert.Equal(t, reflectWorkflow.Status.Address.URL.String(), expectedURL.String()) + + }) +} + +func toK8SNamespace(testName string) string { + return strings.ToLower(strings.Replace(strings.Split(testName, "/")[0], "_", "-", 1)) +} diff --git a/hack/local/run-e2e.sh b/hack/local/run-e2e.sh index 0d23d91c2..d3062b783 100755 --- a/hack/local/run-e2e.sh +++ b/hack/local/run-e2e.sh @@ -20,7 +20,7 @@ echo "Using minikube profile ${MINIKUBE_PROFILE}" export OPERATOR_IMAGE_NAME=localhost/kogito-serverless-operator:0.0.1 eval "$(minikube -p "${MINIKUBE_PROFILE}" docker-env)" -if ! make container-build BUILDER=docker IMG="${OPERATOR_IMAGE_NAME}"; then +if ! make docker-build IMG="${OPERATOR_IMAGE_NAME}"; then echo "Failure: Failed to build image, exiting " >&2 exit 1 fi diff --git a/test/e2e/workflow_test.go b/test/e2e/workflow_test.go index 46bdff3ad..314fbc526 100644 --- a/test/e2e/workflow_test.go +++ b/test/e2e/workflow_test.go @@ -16,6 +16,7 @@ package e2e import ( "fmt" + "net/url" "os/exec" "path/filepath" "strconv" @@ -169,6 +170,9 @@ var _ = Describe("Kogito Serverless Operator", Ordered, func() { By("removing manager namespace") cmd := exec.Command("make", "undeploy") _, _ = utils.Run(cmd) + By("uninstalling CRDs") + cmd = exec.Command("make", "uninstall") + _, _ = utils.Run(cmd) }) Describe("ensure that Operator and Operand(s) can run in restricted namespaces", func() { @@ -178,7 +182,7 @@ var _ = Describe("Kogito Serverless Operator", Ordered, func() { By("creating an instance of the Kogito Serverless Platform") EventuallyWithOffset(1, func() error { cmd := exec.Command("kubectl", "apply", "-f", filepath.Join(projectDir, - "config/samples/"+test.KogitoServerlessPlatformWithCacheMinikubeYamlCR), "-n", namespace) + "config/samples/"+test.KogitoServerlessPlatformMinikubeYamlCR), "-n", namespace) _, err := utils.Run(cmd) return err }, time.Minute, time.Second).Should(Succeed()) @@ -222,6 +226,9 @@ var _ = Describe("Kogito Serverless Operator", Ordered, func() { GinkgoWriter.Println(fmt.Sprintf("builder podlog %s", responseLog)) } + By("check that the workflow is addressable") + EventuallyWithOffset(1, verifyWorkflowIsAddressable, 5*time.Minute, 30*time.Second).Should(BeTrue()) + EventuallyWithOffset(1, func() error { cmd := exec.Command("kubectl", "delete", "-f", filepath.Join(projectDir, "config/samples/"+test.KogitoServerlessWorkflowSampleDevModeYamlCR), "-n", namespace) @@ -251,3 +258,23 @@ func verifyWorkflowIsInRunningState() bool { return false } } + +func verifyWorkflowIsAddressable() bool { + cmd := exec.Command("kubectl", "get", "workflow", "greeting", "-n", namespace, "-o", "jsonpath={.status.address.url}") + if response, err := utils.Run(cmd); err != nil { + GinkgoWriter.Println(fmt.Errorf("failed to check if greeting workflow is running: %v", err)) + return false + } else { + GinkgoWriter.Println(fmt.Sprintf("Got response %s", response)) + if len(strings.TrimSpace(string(response))) > 0 { + _, err := url.ParseRequestURI(string(response)) + if err != nil { + GinkgoWriter.Println(fmt.Errorf("failed to parse result %v", err)) + return false + } + // The response is a valid URL so the test is passed + return true + } + return false + } +} diff --git a/test/yaml.go b/test/yaml.go index 2dc8ea69c..4cad6eb60 100644 --- a/test/yaml.go +++ b/test/yaml.go @@ -33,6 +33,7 @@ const ( KogitoServerlessWorkflowSampleDevModeWithExternalResourceYamlCR = "sw.kogito_v1alpha08_kogitoserverlessworkflow_devmodeWithExternalResource.yaml" KogitoServerlessWorkflowProdProfileSampleYamlCR = "sw.kogito_v1alpha08_kogitoserverlessworkflow_withExplicitProdProfile.yaml" KogitoServerlessPlatformWithCacheYamlCR = "sw.kogito_v1alpha08_kogitoserverlessplatform_withCacheAndCustomization.yaml" + KogitoServerlessPlatformMinikubeYamlCR = "sw.kogito_v1alpha08_kogitoserverlessplatform_minikube.yaml" KogitoServerlessPlatformWithCacheMinikubeYamlCR = "sw.kogito_v1alpha08_kogitoserverlessplatform_withCache_minikube.yaml" KogitoServerlessPlatformYamlCR = "sw.kogito_v1alpha08_kogitoserverlessplatform.yaml" KogitoServerlessPlatformWithBaseImageYamlCR = "sw.kogito_v1alpha08_kogitoserverlessplatformWithBaseImage.yaml" diff --git a/utils/kubernetes/service.go b/utils/kubernetes/service.go new file mode 100644 index 000000000..7acd64894 --- /dev/null +++ b/utils/kubernetes/service.go @@ -0,0 +1,44 @@ +// 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. + +package kubernetes + +import ( + "net/url" + + v1 "k8s.io/api/core/v1" + "knative.dev/pkg/apis" +) + +// TODO: retrieve the cluster domain from the /etc/resolve inside the pod or from the Platform CRD - will be addressed by KOGITO-9198 +var defaultClusterDomain = "svc.cluster.local" + +// retrieveServiceHost function that based on the service name, namespace and eventually the nodeport, will provide the service URI +func retrieveServiceHost(service *v1.Service) string { + namespace := service.Namespace + if len(namespace) == 0 { + namespace = "default" + } + // TODO: Retrieve the cluster domain or use the default one + return service.Name + "." + namespace + "." + defaultClusterDomain +} + +// RetrieveServiceURL function that based on the service name, namespace and eventually the nodeport, will provide the service URI +func RetrieveServiceURL(service *v1.Service) (*apis.URL, error) { + url := url.URL{ + Scheme: "http", + Host: retrieveServiceHost(service), + Path: service.Name} + return apis.ParseURL(url.String()) +} diff --git a/utils/kubernetes/service_test.go b/utils/kubernetes/service_test.go new file mode 100644 index 000000000..e6df6404f --- /dev/null +++ b/utils/kubernetes/service_test.go @@ -0,0 +1,75 @@ +// 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. + +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func Test_retrievingKubernetesServiceHost(t *testing.T) { + t.Run("verify that the service host is returned with the default cluster name on default namespace", func(t *testing.T) { + svc := &v1.Service{} + svc.Name = "workflow" + host := retrieveServiceHost(svc) + + assert.NotNil(t, host) + assert.Equal(t, host, svc.Name+".default.svc.cluster.local") + + }) + + t.Run("verify that the service host is returned with the default cluster name on non-default namespace", func(t *testing.T) { + svc := &v1.Service{} + svc.Name = "workflow" + svc.Namespace = "ns" + host := retrieveServiceHost(svc) + + assert.NotNil(t, host) + assert.Equal(t, host, svc.Name+"."+svc.Namespace+".svc.cluster.local") + + }) +} + +func Test_retrievingKubernetesServiceURL(t *testing.T) { + t.Run("verify that the service URL is returned with the default cluster name on default namespace", func(t *testing.T) { + svc := &v1.Service{} + svc.Name = "workflow" + RetrieveServiceURL(svc) + + url, err := RetrieveServiceURL(svc) + + assert.NoError(t, err) + assert.NotNil(t, url) + assert.Equal(t, url.String(), "http://"+svc.Name+".default.svc.cluster.local/"+svc.Name) + + }) + + t.Run("verify that the service URL is returned with the default cluster name on non-default namespace", func(t *testing.T) { + svc := &v1.Service{} + svc.Name = "workflow" + svc.Namespace = "ns" + RetrieveServiceURL(svc) + + url, err := RetrieveServiceURL(svc) + + assert.NoError(t, err) + assert.NotNil(t, url) + assert.Equal(t, url.String(), "http://"+svc.Name+"."+svc.Namespace+".svc.cluster.local/"+svc.Name) + + }) + +}