From 9fbdf44520bbefd7c8b3d55eefb703bfd75f9e76 Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Thu, 26 Mar 2026 10:21:27 +0100 Subject: [PATCH 1/2] Update kogito-examples URL and use docker as default --- bddframework/pkg/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bddframework/pkg/config/config.go b/bddframework/pkg/config/config.go index 49c0f2d..6747e56 100644 --- a/bddframework/pkg/config/config.go +++ b/bddframework/pkg/config/config.go @@ -109,12 +109,12 @@ const ( defaultOperatorProfilingDataAccessYamlURI = "../profiling/kogito-operator-profiling-data-access.yaml" defaultOperatorProfilingOutputFileURI = "./bdd-cover.out" - defaultKogitoExamplesURI = "https://github.com/apache/incubator-kie-kogito-examples" + defaultKogitoExamplesURI = "https://github.com/kiegroup/kogito-examples" defaultLoadFactor = 1 defaultHTTPRetryNumber = 3 - defaultContainerEngine = "podman" + defaultContainerEngine = "docker" installationSourceOlm = "olm" installationSourceYaml = "yaml" From 7132e43c6a9acc11b1144a19985d3bd68c85a2d6 Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Thu, 23 Apr 2026 09:35:18 +0200 Subject: [PATCH 2/2] YAML Installation testing implementation --- bddframework/pkg/config/config.go | 40 ++++++- run_custom_operator_test.sh | 37 ++++++ testbdd/README.md | 1 + .../install_logic_operator_yaml.feature | 25 +++++ testbdd/framework/operator.go | 3 +- testbdd/installers/sonataflow_installer.go | 105 +++++++++++++++--- testbdd/steps/data.go | 1 + testbdd/steps/operator.go | 84 +++++++++++++- testbdd/steps/sonataflow.go | 78 ++++++++++++- 9 files changed, 353 insertions(+), 21 deletions(-) create mode 100755 run_custom_operator_test.sh create mode 100644 testbdd/features/installation/install_logic_operator_yaml.feature diff --git a/bddframework/pkg/config/config.go b/bddframework/pkg/config/config.go index 6747e56..e5f4426 100644 --- a/bddframework/pkg/config/config.go +++ b/bddframework/pkg/config/config.go @@ -22,6 +22,7 @@ package config import ( "fmt" "path/filepath" + "strings" flag "github.com/spf13/pflag" ) @@ -45,6 +46,7 @@ type TestConfig struct { operatorInstallationSource string operatorCatalogImage string useProductOperator bool + relatedImages map[string]string // profiling operatorProfiling bool @@ -102,7 +104,7 @@ type TestConfig struct { } const ( - defaultOperatorYamlURI = "../operator.yaml" + defaultOperatorYamlURI = "http://raw.githubusercontent.com/kubesmarts/kie-tools/refs/heads/9.105.x-prod/packages/sonataflow-operator/operator.yaml" defaultRhpamOperatorYamlURI = "../rhpam-operator.yaml" defaultCliPath = "../build/_output/bin/kogito" @@ -122,6 +124,9 @@ const ( var ( env = TestConfig{} + + // Map at the package level to hold the flag pointers + relatedImagePointers = make(map[string]*string) ) // BindFlags binds BDD tests env flags to given flag set @@ -147,6 +152,16 @@ func BindFlags(set *flag.FlagSet) { set.StringVar(&env.operatorInstallationSource, prefix+"operator_installation_source", installationSourceYaml, "Operator installation source") set.StringVar(&env.operatorCatalogImage, prefix+"operator_catalog_image", "", "Operator catalog image") set.BoolVar(&env.useProductOperator, prefix+"use_product_operator", false, "Set to true to deploy RHPAM Kogito operator, false for using Kogito operator. Default is false.") + // operator related images + for _, v := range GetRelatedImageVars() { + flagName := strings.ToLower(strings.TrimPrefix(v, "RELATED_IMAGE_")) + // set.String automatically returns a *string linked to the CLI flag! + relatedImagePointers[v] = set.String( + "operator.related_image."+flagName, + "", + fmt.Sprintf("Override for %s", v), + ) + } // operator profiling set.BoolVar(&env.operatorProfiling, prefix+"operator_profiling_enabled", false, "Enable the profiling of the operator. If enabled, operator will be automatically deployed with yaml files.") @@ -154,7 +169,7 @@ func BindFlags(set *flag.FlagSet) { set.StringVar(&env.operatorProfilingOutputFileURI, prefix+"operator_profiling_output_file_uri", defaultOperatorProfilingOutputFileURI, "Url or Path where to store the profiling outputs.") // files/binaries - set.StringVar(&env.operatorYamlURI, prefix+"operator_yaml_uri", defaultOperatorYamlURI, "Url or Path to kogito-operator.yaml file") + set.StringVar(&env.operatorYamlURI, prefix+"operator_yaml_uri", defaultOperatorYamlURI, "Url or Path to operator.yaml file") set.StringVar(&env.rhpamOperatorYamlURI, prefix+"rhpam_operator_yaml_uri", defaultRhpamOperatorYamlURI, "Url or Path to kogito-operator.yaml file") set.StringVar(&env.cliPath, prefix+"cli_path", defaultCliPath, "Path to built CLI to test") @@ -222,6 +237,27 @@ func addPersistenceTypeImageTagFlags(set *flag.FlagSet, imageTags *imageTags, im set.StringVar(imageTags.GetImageTagPointerFromPersistenceType(imageType, persistenceType), key, "", description) } +// Get Array of string identifiers for all operator RELATED_IMAGES keys +func GetRelatedImageVars() []string { + return []string{ + "RELATED_IMAGE_JOBS_SERVICE_POSTGRESQL", + "RELATED_IMAGE_JOBS_SERVICE_EPHEMERAL", + "RELATED_IMAGE_DATA_INDEX_POSTGRESQL", + "RELATED_IMAGE_DATA_INDEX_EPHEMERAL", + "RELATED_IMAGE_DB_MIGRATOR_TOOL", + "RELATED_IMAGE_BASE_BUILDER", + "RELATED_IMAGE_DEVMODE", + } +} + +// GetRelatedImage returns the override for a specific related image variable +func GetRelatedImage(varName string) string { + if ptr, ok := relatedImagePointers[varName]; ok && ptr != nil { + return *ptr + } + return "" +} + // tests configuration // IsSmokeTests return whether smoke tests should be executed diff --git a/run_custom_operator_test.sh b/run_custom_operator_test.sh new file mode 100755 index 0000000..4b71ebb --- /dev/null +++ b/run_custom_operator_test.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +export DEBUG=true + +rm -rf testbdd/logs/ + +go run testbdd/scripts/prune_namespaces.go + +./hack/clean-cluster-operators.sh +./hack/clean-crds.sh + +# Configuration variables (change these to match your actual test registry/tags) +REGISTRY="registry-proxy.engineering.redhat.com/rh-osbs" +OPERATOR_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-69005-20260325162017" +JOBSSERVICE_POSTGESQL_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-18542-20260325161846" +JOBSSERVICE_EPHEMERAL_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-45012-20260325161758" +DATAINDEX_POSTGESQL_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-71465-20260325161709" +DATAINDEX_EPHEMERAL_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-58554-20260325161807" +DBMIGRATOR_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-65986-20260325161934" +BUIDLER_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-71914-20260325161933" +DEVMODE_TAG="openshift-serverless-nightly-rhel-9-containers-candidate-95367-20260325162027" + +echo "🚀 Running Logic Operator BDD tests with custom related NIGHTLY images..." + +# Execute the Godog test suite and pass the CLI arguments +go test ./testbdd/... -v --godog.tags="installation" --godog.format=junit --godog.format=pretty -args \ + --tests.operator_image_tag="${REGISTRY}/openshift-serverless-1-logic-rhel9-operator:${OPERATOR_TAG}" \ + --operator.related_image.jobs_service_postgresql="${REGISTRY}/openshift-serverless-1-logic-jobs-service-postgresql-rhel9:${JOBSSERVICE_POSTGESQL_TAG}" \ + --operator.related_image.jobs_service_ephemeral="${REGISTRY}/openshift-serverless-1-logic-jobs-service-ephemeral-rhel9:${JOBSSERVICE_EPHEMERAL_TAG}" \ + --operator.related_image.data_index_postgresql="${REGISTRY}/openshift-serverless-1-logic-data-index-postgresql-rhel9:${DATAINDEX_POSTGESQL_TAG}" \ + --operator.related_image.data_index_ephemeral="${REGISTRY}/openshift-serverless-1-logic-data-index-ephemeral-rhel9:${DATAINDEX_EPHEMERAL_TAG}" \ + --operator.related_image.db_migrator_tool="${REGISTRY}/openshift-serverless-1-logic-db-migrator-tool-rhel9:${DBMIGRATOR_TAG}" \ + --operator.related_image.base_builder="${REGISTRY}/openshift-serverless-1-logic-swf-builder-rhel9:${BUIDLER_TAG}" \ + --operator.related_image.devmode="${REGISTRY}/openshift-serverless-1-logic-swf-devmode-rhel9:${DEVMODE_TAG}" \ + --tests.cr_deployment_only + + diff --git a/testbdd/README.md b/testbdd/README.md index 0b6a68f..5ec185e 100644 --- a/testbdd/README.md +++ b/testbdd/README.md @@ -74,6 +74,7 @@ You can set these optional keys: *Default is false.* - `local_execution` to be set to true if running tests in local using either a local or remote cluster. *Default is false.* +- `operator.related_image.jobs_service_postgresql` etc. Logs will be shown on the Terminal. diff --git a/testbdd/features/installation/install_logic_operator_yaml.feature b/testbdd/features/installation/install_logic_operator_yaml.feature new file mode 100644 index 0000000..126f69a --- /dev/null +++ b/testbdd/features/installation/install_logic_operator_yaml.feature @@ -0,0 +1,25 @@ +Feature: Deploy Logic Operator with using YAML + As a developer + I want to install the Logic Operator using YAML + So that I can verify the installation behaves expectedly + + @installation + Scenario: Deploy the operator and verify it is running + Given Namespace is created + When SonataFlow Operator is deployed + Then SonataFlow Operator has 1 pod running + And Service "logic-operator-controller-manager-metrics-service" exists + And ConfigMap "logic-operator-builder-config" exists + And ConfigMap "logic-operator-controllers-config" exists + And ConfigMap "logic-operator-builder-config" contains following strings: + | FROM ${RELATED_IMAGE_BASE_BUILDER} AS builder | + | FROM registry.access.redhat.com/ubi9/openjdk-17-runtime:1.23 | + When Postgres is deployed + When SonataFlowPlatform with DataIndexAndJobsService using Postgres is deployed + When SonataFlow callbackstatetimeouts example is deployed + Then SonataFlow "callbackstatetimeouts" has the condition "Running" set to "True" within 5 minutes + Then HTTP POST request on non-dev SonataFlow "callbackstatetimeouts" is successful within 1 minute with path "callbackstatetimeouts", expectedResponseContains '"workflowdata":{"message":"Hello"}"' and body: + """json + {"message": "Hello" + } + """ \ No newline at end of file diff --git a/testbdd/framework/operator.go b/testbdd/framework/operator.go index 0eb1018..b30cdc4 100644 --- a/testbdd/framework/operator.go +++ b/testbdd/framework/operator.go @@ -32,7 +32,8 @@ import ( const ( sonataFlowOperatorTimeoutInMin = 5 - sonataFlowOperatorName = "logic-operator-rhel8" + // TODO - this needs to use the correct value based on the STREAM ( CR/Nightly ) + sonataFlowOperatorName = "logic-operator" sonataFlowOperatorDeploymentName = sonataFlowOperatorName + "-controller-manager" sonataFlowOperatorPullImageSecretPrefix = sonataFlowOperatorName + "-dockercfg" ) diff --git a/testbdd/installers/sonataflow_installer.go b/testbdd/installers/sonataflow_installer.go index 97baba6..f0e1286 100644 --- a/testbdd/installers/sonataflow_installer.go +++ b/testbdd/installers/sonataflow_installer.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "regexp" + "strings" "sigs.k8s.io/controller-runtime/pkg/client" @@ -32,13 +33,13 @@ import ( srvframework "github.com/kubesmarts/operator-bdd-test/testbdd/framework" ) -const defaultOperatorImage = "quay.io/kubesmarts/incubator-kie-sonataflow-operator" +const defaultOperatorImage = "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-rhel9-operator:latest" var ( // sonataFlowYamlClusterInstaller installs SonataFlow operator cluster wide using YAMLs sonataFlowYamlClusterInstaller = installers.YamlClusterWideServiceInstaller{ InstallClusterYaml: installSonataFlowUsingYaml, - InstallationNamespace: SonataFlowNamespace, + InstallationNamespace: LogicOperatorNamespace, WaitForClusterYamlServiceRunning: waitForSonataFlowOperatorUsingYamlRunning, GetAllClusterYamlCrsInNamespace: getSonataFlowCrsInNamespace, UninstallClusterYaml: uninstallSonataFlowUsingYaml, @@ -71,7 +72,25 @@ var ( sonataFlowServiceName = "SonataFlow operator" sonataFlowOperatorSubscriptionName = "sonataflow-operator" - sonataFlowOperatorSubscriptionChannel = "alpha" + sonataFlowOperatorSubscriptionChannel = "stable" + + sonataFlowOperatorControllerConfigName = sonataFlowOperatorSubscriptionName + "-controllers-config" + sonataFlowOperatorBuilderConfigName = sonataFlowOperatorSubscriptionName + "-builder-config" + sonataFlowOperatorControllerManagerServiceAccountName = sonataFlowOperatorSubscriptionName + "-controller-manager" + sonataFlowOperatorMetricsReaderName = sonataFlowOperatorSubscriptionName + "-metrics-reader" + sonataFlowOperatorLeaderElectionRoleName = sonataFlowOperatorSubscriptionName + "-leader-election-role" + sonataFlowOperatorBuilderManagerRoleName = sonataFlowOperatorSubscriptionName + "-builder-manager-role" + + // Openshift Serverless Logic naming constants + LogicOperatorNamespace = "openshift-serverless-logic" + logicOperatorSubscriptionName = "logic-operator" + + logicOperatorControllerConfigName = logicOperatorSubscriptionName + "-controllers-config" + logicOperatorBuilderConfigName = logicOperatorSubscriptionName + "-builder-config" + logicOperatorControllerManagerServiceAccountName = logicOperatorSubscriptionName + "-controller-manager" + logicOperatorMetricsReaderName = logicOperatorSubscriptionName + "-metrics-reader" + logicOperatorLeaderElectionRoleName = logicOperatorSubscriptionName + "-leader-election-role" + logicOperatorBuilderManagerRoleName = logicOperatorSubscriptionName + "-builder-manager-role" ) // GetSonataFlowInstaller returns SonataFlow installer @@ -96,24 +115,84 @@ func GetSonataFlowInstaller() (installers.ServiceInstaller, error) { func installSonataFlowUsingYaml() error { framework.GetMainLogger().Info("Installing SonataFlow operator") - yamlContent, err := framework.ReadFromURI(config.GetOperatorYamlURI()) + operatorImage := config.GetOperatorImageTag() + manifestURI := config.GetOperatorYamlURI() + + // Read the content of operator.yaml from configured URL + yamlContent, err := framework.ReadFromURI(manifestURI) if err != nil { - framework.GetMainLogger().Error(err, "Error while reading the operator YAML file") + framework.GetMainLogger().Error(err, "Error while reading the operator YAML file at %s", manifestURI) return err } - regexp, err := regexp.Compile("main") - if err != nil { - return err + // Patch the image reference of operator + if len(operatorImage) > 0 { + imageRegex := regexp.MustCompile(`image:\s*["']?.*?incubator-kie-sonataflow-operator[^"'\s]*["']?`) + if !imageRegex.MatchString(yamlContent) { + // Fallback: search for a generic placeholder if the specific one isn't found + imageRegex = regexp.MustCompile(`image:\s*["']?placeholder["']?|image:\s*["']?main["']?`) + } + yamlContent = imageRegex.ReplaceAllString(yamlContent, fmt.Sprintf("image: %s", operatorImage)) + } + + // 2. Patch Related Images Environment Variables + relatedImageVars := []string{ + "RELATED_IMAGE_JOBS_SERVICE_POSTGRESQL", + "RELATED_IMAGE_JOBS_SERVICE_EPHEMERAL", + "RELATED_IMAGE_DATA_INDEX_POSTGRESQL", + "RELATED_IMAGE_DATA_INDEX_EPHEMERAL", + "RELATED_IMAGE_DB_MIGRATOR_TOOL", + "RELATED_IMAGE_BASE_BUILDER", + "RELATED_IMAGE_DEVMODE", + } + + for _, envVar := range relatedImageVars { + overrideValue := config.GetRelatedImage(envVar) + if len(overrideValue) > 0 { + // (?s) allows the regex to read across newlines. + // It finds "name: " and the subsequent "value: " string, keeping them intact (${1}) + // and replaces the actual image tag. + re := regexp.MustCompile(`(?s)(name:\s*` + envVar + `\s+value:\s*)([^\s"']+)`) + yamlContent = re.ReplaceAllString(yamlContent, "${1}"+overrideValue) + + framework.GetMainLogger().Info(fmt.Sprintf("Patched %s with %s", envVar, overrideValue)) + } } - yamlContent = regexp.ReplaceAllString(yamlContent, config.GetOperatorImageTag()) - tempFilePath, err := framework.CreateTemporaryFile("kogito-serverless-operator*.yaml", yamlContent) + // Replace builder image reference in sonataflow-operator-builder-config + builderImageUrl := config.GetRelatedImage("RELATED_IMAGE_BASE_BUILDER") + builderRegex := regexp.MustCompile(`docker\.io/apache/incubator-kie-sonataflow-builder[^\s"']*`) + yamlContent = builderRegex.ReplaceAllString(yamlContent, builderImageUrl) + + // Replace sonataflow-operator-system with openshift-serverless-logic + yamlContent = strings.ReplaceAll(yamlContent, SonataFlowNamespace, LogicOperatorNamespace) + // Replace remaining community prefixes + yamlContent = strings.ReplaceAll(yamlContent, "sonataflow-operator-", "logic-operator-") + + /** + // Replace sonataflow-operator-controllers-config with logic-operator-controllers-config + yamlContent = strings.ReplaceAll(yamlContent, sonataFlowOperatorControllerConfigName, logicOperatorControllerConfigName) + // Replace sonataflow-operator-builder-config with logic-operator-builder-config + yamlContent = strings.ReplaceAll(yamlContent, sonataFlowOperatorBuilderConfigName, logicOperatorBuilderConfigName) + // Replace sonataflow-operator-controller-manager with logic-operator-controller-manager + yamlContent = strings.ReplaceAll(yamlContent, sonataFlowOperatorControllerManagerServiceAccountName, + logicOperatorControllerManagerServiceAccountName) + // Replace sonataflow-operator-metrics-reader with logic-operator-metrics-reader + yamlContent = strings.ReplaceAll(yamlContent, sonataFlowOperatorMetricsReaderName, logicOperatorMetricsReaderName) + // Replace sonataflow-operator-leader-election-role with logic-operator-leader-election-role + yamlContent = strings.ReplaceAll(yamlContent, sonataFlowOperatorLeaderElectionRoleName, logicOperatorLeaderElectionRoleName) + // Replace sonataflow-operator-builder-manager-role with logic-operator-builder-manager-role + yamlContent = strings.ReplaceAll(yamlContent, sonataFlowOperatorBuilderManagerRoleName, logicOperatorBuilderManagerRoleName) +*/ + // Create also one file to be able to inspect the YAML if needed + framework.CreateFile("./", "operator.yaml", yamlContent) + tempFilePath, err := framework.CreateTemporaryFile("logic-operator*.yaml", yamlContent) if err != nil { framework.GetMainLogger().Error(err, "Error while storing adjusted YAML content to temporary file") return err } + // TODO: Make this differentiate between different CLIs _, err = framework.CreateCommand("oc", "create", "-f", tempFilePath).Execute() if err != nil { framework.GetMainLogger().Error(err, "Error while installing SonataFlow operator from YAML file") @@ -124,15 +203,15 @@ func installSonataFlowUsingYaml() error { } func waitForSonataFlowOperatorUsingYamlRunning() error { - return srvframework.WaitForSonataFlowOperatorRunning(SonataFlowNamespace) + return srvframework.WaitForSonataFlowOperatorRunning(LogicOperatorNamespace) } func uninstallSonataFlowUsingYaml() error { framework.GetMainLogger().Info("Uninstalling SonataFlow operator") - output, err := framework.CreateCommand("oc", "delete", "-f", config.GetOperatorYamlURI(), "--timeout=30s", "--ignore-not-found=true").Execute() + output, err := framework.CreateCommand("oc", "delete", "-f", "./operator.yaml", "--timeout=60s", "--ignore-not-found=true").Execute() if err != nil { - framework.GetMainLogger().Error(err, fmt.Sprintf("Deleting SonataFlow operator failed, output: %s", output)) + framework.GetMainLogger().Error(err, fmt.Sprintf("Deleting SonataFlow operator failed, output:\n %s", output)) return err } diff --git a/testbdd/steps/data.go b/testbdd/steps/data.go index d081e3e..645798c 100644 --- a/testbdd/steps/data.go +++ b/testbdd/steps/data.go @@ -32,6 +32,7 @@ import ( // Data contains all data needed by Gherkin steps to run type Data struct { *sonataFlowSteps.Data + OperatorNamespace string } // RegisterAllSteps register all steps available to the test suite diff --git a/testbdd/steps/operator.go b/testbdd/steps/operator.go index 8905b3d..2a261a4 100644 --- a/testbdd/steps/operator.go +++ b/testbdd/steps/operator.go @@ -21,6 +21,7 @@ package steps import ( "fmt" + "strings" "github.com/cucumber/godog" @@ -29,11 +30,17 @@ import ( "github.com/kubesmarts/operator-bdd-test/bddframework/pkg/config" "github.com/kubesmarts/operator-bdd-test/bddframework/pkg/framework" kogitoInstallers "github.com/kubesmarts/operator-bdd-test/bddframework/pkg/installers" + corev1 "k8s.io/api/core/v1" + + "k8s.io/apimachinery/pkg/types" ) func registerOperatorSteps(ctx *godog.ScenarioContext, data *Data) { ctx.Step(`^SonataFlow Operator is deployed$`, data.sonataFlowOperatorIsDeployed) - ctx.Step(`^SonataFlow Operator has (\d+) (?:pod|pods) running"$`, data.sonataFlowOperatorHasPodsRunning) + ctx.Step(`^SonataFlow Operator has (\d+) (?:pod|pods) running$`, data.sonataFlowOperatorHasPodsRunning) + ctx.Step(`^Service "([^"]*)" exists$`, data.serviceExists) + ctx.Step(`^ConfigMap "([^"]*)" exists$`, data.configMapExists) + ctx.Step(`^ConfigMap "([^"]*)" contains following strings:$`, data.configMapContainsStrings) // Not migrated yet //ctx.Step(`^Kogito operator should be installed$`, data.kogitoOperatorShouldBeInstalled) //ctx.Step(`^CLI install Kogito operator$`, data.cliInstallKogitoOperator) @@ -41,6 +48,8 @@ func registerOperatorSteps(ctx *godog.ScenarioContext, data *Data) { func (data *Data) sonataFlowOperatorIsDeployed() (err error) { var installer kogitoInstallers.ServiceInstaller + // Always use OSL namespace + data.OperatorNamespace = installers.LogicOperatorNamespace if config.UseProductOperator() { installer, err = &kogitoInstallers.YamlClusterWideServiceInstaller{}, fmt.Errorf("OLM is not supported by the steps yet") } else { @@ -49,11 +58,78 @@ func (data *Data) sonataFlowOperatorIsDeployed() (err error) { if err != nil { return err } - return installer.Install(data.Namespace) + return installer.Install(data.OperatorNamespace) +} + +func (data *Data) sonataFlowOperatorHasPodsRunning(numberOfPods int) error { + return framework.WaitForPodsWithLabel(data.OperatorNamespace, "app.kubernetes.io/name", "sonataflow-operator", numberOfPods, 1) +} + +func (data *Data) serviceExists(serviceName string) error { + framework.GetLogger(data.OperatorNamespace).Info("Checking if Service exists", "service", serviceName) + + _, err := framework.GetService(data.OperatorNamespace, serviceName) + if err != nil { + return fmt.Errorf("Service %s does not exist in namespace %s: %v", serviceName, data.OperatorNamespace, err) + } + return nil +} + +func (data *Data) configMapExists(cmName string) error { + framework.GetLogger(data.OperatorNamespace).Info("Checking if ConfigMap exists", "configMap", cmName) + + exists, err := framework.IsConfigMapExist(types.NamespacedName{Name: cmName, Namespace: data.OperatorNamespace}) + if err != nil { + return fmt.Errorf("error while checking if ConfigMap %s exists: %v", cmName, err) + } + if !exists { + return fmt.Errorf("ConfigMap %s does not exist in namespace %s", cmName, data.OperatorNamespace) + } + return nil } -func (data *Data) sonataFlowOperatorHasPodsRunning(numberOfPods int, name, phase string) error { - return framework.WaitForPodsWithLabel(name, "control-plane", "sonataflow-operator", numberOfPods, 1) +func (data *Data) configMapContainsStrings(cmName string, table *godog.Table) error { + framework.GetLogger(data.OperatorNamespace).Info("Validating ConfigMap contains strings", "configMap", cmName) + + cm := &corev1.ConfigMap{} + exists, err := framework.GetObjectWithKey(types.NamespacedName{Name: cmName, Namespace: data.OperatorNamespace}, cm) + if err != nil { + return fmt.Errorf("error fetching ConfigMap %s: %v", cmName, err) + } + if !exists { + return fmt.Errorf("ConfigMap %s does not exist in namespace %s", cmName, data.OperatorNamespace) + } + + // Concatenate all ConfigMap values into one large string for easy searching + var allDataValues strings.Builder + for _, value := range cm.Data { + allDataValues.WriteString(value) + } + + // Iterate over the Gherkin data table + for _, row := range table.Rows { + if len(row.Cells) == 0 { + continue + } + + expectedString := row.Cells[0].Value + + // Placeholder Resolution Logic - allows to check for string influenced by the stream + if strings.Contains(expectedString, "${RELATED_IMAGE_BASE_BUILDER}") { + builderImage := config.GetRelatedImage("RELATED_IMAGE_BASE_BUILDER") + if builderImage == "" { + // Fallback to the default if the property wasn't provided during the test run + builderImage = "docker.io/apache/incubator-kie-sonataflow-builder:main" + } + expectedString = strings.ReplaceAll(expectedString, "${RELATED_IMAGE_BASE_BUILDER}", builderImage) + } + + if !strings.Contains(allDataValues.String(), expectedString) { + return fmt.Errorf("ConfigMap '%s' does not contain the expected string:\n'%s'", cmName, expectedString) + } + } + + return nil } // diff --git a/testbdd/steps/sonataflow.go b/testbdd/steps/sonataflow.go index 3c433f3..7141e39 100644 --- a/testbdd/steps/sonataflow.go +++ b/testbdd/steps/sonataflow.go @@ -46,6 +46,7 @@ func registerSonataFlowSteps(ctx *godog.ScenarioContext, data *Data) { ctx.Step(`^HTTP POST request as Cloud Event on SonataFlow "([^"]*)" is successful within (\d+) minutes? with path "([^"]*)", headers "([^"]*)" and body:$`, data.httpPostRequestAsCloudEventOnSonataFlowIsSuccessfulWithinMinutesWithPathHeadersAndBody) ctx.Step(`^HTTP GET request on SonataFlow "([^"]*)" is successful within (\d+) minutes? with path "([^"]*)", expectedResponseContains '([^']*)'$`, data.httpGetRequestOnSonataFlowIsSuccessfulWithinMinutesWithResponseContains) ctx.Step(`^HTTP POST request on SonataFlow "([^"]*)" is successful within (\d+) minutes? with path "([^"]*)", expectedResponseContains '([^']*)' and body:$`, data.httpPostRequestOnSonataFlowIsSuccessfulWithinMinutesWithResponseAndBody) + ctx.Step(`^HTTP POST request on non-dev SonataFlow "([^"]*)" is successful within (\d+) minutes? with path "([^"]*)", expectedResponseContains '([^']*)' and body:$`, data.httpPostRequestWithinPodOnSonataFlowIsSuccessfulWithinMinutesWithResponseAndBody) ctx.Step(`^SonataFlow "([^"]*)" pods log contains text '([^']*)' within (\d+) minutes$`, data.sonataFlowLogContainsTextWithinMinutes) ctx.Step(`^SonataFlow "([^"]*)" pods log does not contain text '([^']*)' within (\d+) minutes$`, data.sonataFlowLogDoesNotContainTextWithinMinutes) } @@ -132,8 +133,16 @@ func (data *Data) sonataFlowIsAddressableWithinMinutes(name string, timeoutInMin return false, fmt.Errorf("no SonataFlow found with name %s in namespace %s", name, data.Namespace) } + if sonataFlow.ObjectMeta.Annotations["sonataflow.org/profile"] == "gitops" { + return false, fmt.Errorf("SonataFlow %s does NOT have an address, because it uses gitops profile", name) + } + + if sonataFlow.ObjectMeta.Annotations["sonataflow.org/profile"] == "preview" { + return false, fmt.Errorf("SonataFlow %s does NOT have an address, because it uses preview profile", name) + } + if sonataFlow.Status.Address.URL == nil { - return false, fmt.Errorf("SonataFlow %s does NOT have an address", name) + return false, fmt.Errorf("SonataFlow %s does NOT have an address.", name) } if _, err := url.ParseRequestURI(sonataFlow.Status.Address.URL.String()); err != nil { @@ -249,6 +258,27 @@ func (data *Data) httpPostRequestOnSonataFlowIsSuccessfulWithinMinutesWithRespon } } +func (data *Data) httpPostRequestWithinPodOnSonataFlowIsSuccessfulWithinMinutesWithResponseAndBody(name string, timeoutInMin int, path, expectedResponseContains string, body *godog.DocString) error { + podName, err := getWorkflowPodName(data.Namespace, name) + if err != nil { + return err + } + + framework.GetLogger(data.Namespace).Info("Executing POST request inside pod", "pod", podName, "path", path) + + // 2. Execute the curl command + output, err := executePostInPod(data.Namespace, podName, path, body.Content) + if err != nil { + return err + } + framework.GetLogger(data.Namespace).Info("POST request successful", "response", output) + + // Note: If you have subsequent steps that need to validate the response, + // you can store 'output' in your 'Data' struct! (e.g., data.LastHttpResponse = output) + + return nil +} + func parseHeaders(headersContent string) (map[string]string, error) { headers := make(map[string]string) @@ -267,3 +297,49 @@ func parseHeaders(headersContent string) (map[string]string, error) { return headers, nil } + +// Helper: Get the running pod name for a specific workflow +func getWorkflowPodName(namespace, workflowName string) (string, error) { + // SonataFlow workflows generate Deployments with the same name as the workflow + pods, err := framework.GetPodsByDeployment(namespace, workflowName) + if err != nil { + return "", fmt.Errorf("error fetching pods for deployment %s: %v", workflowName, err) + } + + if len(pods) == 0 { + return "", fmt.Errorf("no pods found for workflow deployment %s", workflowName) + } + + // Iterate through the list and return the first pod that is actively running + for _, pod := range pods { + if framework.IsPodRunning(&pod) { + return pod.Name, nil + } + } + + return "", fmt.Errorf("no running pods found for workflow %s", workflowName) +} + +// Helper: Execute the curl command inside the pod +func executePostInPod(namespace, podName, path, jsonData string) (string, error) { + cli := "kubectl" + if framework.IsOpenshift() { + cli = "oc" + } + + cmd := framework.CreateCommand( + cli, "exec", podName, "-n", namespace, "--", + "curl", "-s", "-X", "POST", + "-H", "Content-Type: application/json", + "-d", jsonData, + fmt.Sprintf("http://localhost:8080/%s", path), + ) + + // Execute returns the standard output (your HTTP response body) + output, err := cmd.Execute() + if err != nil { + return "", fmt.Errorf("failed to execute curl in pod: %v. Output: %s", err, output) + } + + return output, nil +}