diff --git a/e2e/common/runtimes/default.go b/e2e/common/runtimes/default.go new file mode 100644 index 0000000000..1cc41c3f9a --- /dev/null +++ b/e2e/common/runtimes/default.go @@ -0,0 +1,26 @@ +//go:build integration +// +build integration + +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 runtimes + +import "github.com/apache/camel-k/v2/e2e/support" + +var ns = support.GetEnvOrDefault("CAMEL_K_TEST_NAMESPACE", support.GetCIProcessID()) +var operatorID = support.GetEnvOrDefault("CAMEL_K_OPERATOR_ID", support.GetCIProcessID()) diff --git a/e2e/common/runtimes/runtimes_test.go b/e2e/common/runtimes/runtimes_test.go new file mode 100644 index 0000000000..d95c6820c8 --- /dev/null +++ b/e2e/common/runtimes/runtimes_test.go @@ -0,0 +1,70 @@ +//go:build integration +// +build integration + +// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration" + +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 runtimes + +import ( + "testing" + + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + + . "github.com/apache/camel-k/v2/e2e/support" + v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" +) + +func TestSourceLessIntegrations(t *testing.T) { + RegisterTestingT(t) + var cmData = make(map[string]string) + cmData["my-file.txt"] = "Hello World!" + CreatePlainTextConfigmap(ns, "my-cm-sourceless", cmData) + + t.Run("Camel Main", func(t *testing.T) { + itName := "my-camel-main-v1" + Expect(KamelRunWithID(operatorID, ns, "--image", "docker.io/squakez/my-camel-main:1.0.0", "--resource", "configmap:my-cm-sourceless@/tmp/app/data").Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(ns, itName), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationConditionStatus(ns, itName, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + Eventually(IntegrationLogs(ns, itName), TestTimeoutShort).Should(ContainSubstring(cmData["my-file.txt"])) + Eventually(IntegrationLogs(ns, itName), TestTimeoutShort).Should(ContainSubstring("Apache Camel (Main)")) + }) + + t.Run("Camel Spring Boot", func(t *testing.T) { + itName := "my-camel-sb-v1" + Expect(KamelRunWithID(operatorID, ns, "--image", "docker.io/squakez/my-camel-sb:1.0.0", "--resource", "configmap:my-cm-sourceless@/tmp/app/data").Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(ns, itName), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationConditionStatus(ns, itName, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + Eventually(IntegrationLogs(ns, itName), TestTimeoutShort).Should(ContainSubstring(cmData["my-file.txt"])) + Eventually(IntegrationLogs(ns, itName), TestTimeoutShort).Should(ContainSubstring("Spring Boot")) + }) + + t.Run("Camel Quarkus", func(t *testing.T) { + itName := "my-camel-quarkus-v1" + Expect(KamelRunWithID(operatorID, ns, "--image", "docker.io/squakez/my-camel-quarkus:1.0.0", "--resource", "configmap:my-cm-sourceless@/tmp/app/data").Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(ns, itName), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationConditionStatus(ns, itName, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + Eventually(IntegrationLogs(ns, itName), TestTimeoutShort).Should(ContainSubstring(cmData["my-file.txt"])) + Eventually(IntegrationLogs(ns, itName), TestTimeoutShort).Should(ContainSubstring("powered by Quarkus")) + }) + + Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed()) +} diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index f117f8138e..ea8ba47105 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -98,6 +98,7 @@ func newCmdRun(rootCmdOptions *RootCmdOptions) (*cobra.Command, *runCmdOptions) } cmd.Flags().String("name", "", "The integration name") + cmd.Flags().String("image", "", "An image built externally (ie, via CICD). Enabling it will skip the Integration build phase.") cmd.Flags().StringArrayP("connect", "c", nil, "A Service that the integration should bind to, specified as [[apigroup/]version:]kind:[namespace/]name") cmd.Flags().StringArrayP("dependency", "d", nil, usageDependency) cmd.Flags().BoolP("wait", "w", false, "Wait for the integration to be running") @@ -145,6 +146,7 @@ type runCmdOptions struct { Save bool `mapstructure:"save" yaml:",omitempty" kamel:"omitsave"` IntegrationKit string `mapstructure:"kit" yaml:",omitempty"` IntegrationName string `mapstructure:"name" yaml:",omitempty"` + ContainerImage string `mapstructure:"image" yaml:",omitempty"` Profile string `mapstructure:"profile" yaml:",omitempty"` OperatorID string `mapstructure:"operator-id" yaml:",omitempty"` OutputFormat string `mapstructure:"output" yaml:",omitempty"` @@ -236,10 +238,6 @@ func (o *runCmdOptions) decode(cmd *cobra.Command, args []string) error { } func (o *runCmdOptions) validateArgs(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return errors.New("run expects at least 1 argument, received 0") - } - if _, err := source.Resolve(context.Background(), args, false, cmd); err != nil { return fmt.Errorf("one of the provided sources is not reachable: %w", err) } @@ -324,6 +322,11 @@ func (o *runCmdOptions) run(cmd *cobra.Command, args []string) error { } } + // We need to make this check at this point, in order to have sources filled during decoding + if len(args) < 1 && o.ContainerImage == "" { + return errors.New("run command expects either an Integration source or the container image (via --image argument)") + } + integration, err := o.createOrUpdateIntegration(cmd, c, args) if err != nil { return err @@ -536,8 +539,15 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd *cobra.Command, c client.C return nil, err } - if err := o.resolveSources(cmd, sources, integration); err != nil { - return nil, err + if o.ContainerImage == "" { + // Resolve resources + if err := o.resolveSources(cmd, sources, integration); err != nil { + return nil, err + } + } else { + // Source-less Integration as the user provided a container image built externally + o.Traits = append(o.Traits, fmt.Sprintf("container.image=%s", o.ContainerImage)) + o.Traits = append(o.Traits, "jvm.enabled=false") } if err := resolvePodTemplate(context.Background(), cmd, o.PodTemplate, &integration.Spec); err != nil { @@ -868,11 +878,15 @@ func (o *runCmdOptions) getPlatform(cmd *cobra.Command, c client.Client, it *v1. func (o *runCmdOptions) GetIntegrationName(sources []string) string { name := "" - if o.IntegrationName != "" { + switch { + case o.IntegrationName != "": name = o.IntegrationName name = kubernetes.SanitizeName(name) - } else if len(sources) == 1 { + case len(sources) == 1: name = kubernetes.SanitizeName(sources[0]) + case o.ContainerImage != "": + // source-less execution + name = kubernetes.SanitizeName(strings.ReplaceAll(o.ContainerImage, ":", "-v")) } return name } diff --git a/pkg/cmd/run_test.go b/pkg/cmd/run_test.go index becac009b4..6ca17a838b 100644 --- a/pkg/cmd/run_test.go +++ b/pkg/cmd/run_test.go @@ -511,13 +511,9 @@ func TestRunBuildPropertyFlag(t *testing.T) { func TestRunValidateArgs(t *testing.T) { runCmdOptions, rootCmd, _ := initializeRunCmdOptions(t) - args := []string{} - err := runCmdOptions.validateArgs(rootCmd, args) - assert.NotNil(t, err) - assert.Equal(t, "run expects at least 1 argument, received 0", err.Error()) - args = []string{"run_test.go"} - err = runCmdOptions.validateArgs(rootCmd, args) + args := []string{"run_test.go"} + err := runCmdOptions.validateArgs(rootCmd, args) assert.Nil(t, err) args = []string{"missing_file"} @@ -826,3 +822,29 @@ func TestRunOutputWithoutKubernetesCluster(t *testing.T) { _, err = test.ExecuteCommand(rootCmd, cmdRun, "-o", "yaml", integrationSource) require.NoError(t, err) } + +func TestSourceLessIntegration(t *testing.T) { + runCmdOptions, runCmd, _ := initializeRunCmdOptionsWithOutput(t) + output, err := test.ExecuteCommand(runCmd, cmdRun, "--image", "docker.io/my-org/my-app:1.0.0", "-o", "yaml", "-t", "mount.configs=configmap:my-cm") + assert.Equal(t, "yaml", runCmdOptions.OutputFormat) + + assert.Nil(t, err) + assert.Equal(t, `apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + annotations: + camel.apache.org/operator.id: camel-k + creationTimestamp: null + name: my-app-v1 +spec: + traits: + container: + image: docker.io/my-org/my-app:1.0.0 + jvm: + enabled: false + mount: + configs: + - configmap:my-cm +status: {} +`, output) +} diff --git a/pkg/util/kubernetes/sanitize_test.go b/pkg/util/kubernetes/sanitize_test.go index 31b422e20e..93bcac937b 100644 --- a/pkg/util/kubernetes/sanitize_test.go +++ b/pkg/util/kubernetes/sanitize_test.go @@ -36,6 +36,8 @@ func TestSanitizeName(t *testing.T) { {"input": "-foo-bar-", "expect": "foo-bar"}, {"input": "1foo-bar2", "expect": "1foo-bar2"}, {"input": "foo-bar-1", "expect": "foo-bar-1"}, + {"input": "docker.io/squakez/my-camel-sb:1.0.0", "expect": "my-camel-sb1"}, + {"input": "docker.io/squakez/my-camel-sb:2.0.0", "expect": "my-camel-sb2"}, } for _, c := range cases { diff --git a/script/Makefile b/script/Makefile index 3b7dbea47a..840c98430c 100644 --- a/script/Makefile +++ b/script/Makefile @@ -260,6 +260,7 @@ test-common: do-build go test -timeout 30m -v ./e2e/common/config -tags=integration $(TEST_INTEGRATION_COMMON_LANG_RUN) $(GOTESTFMT) || FAILED=1; \ go test -timeout 30m -v ./e2e/common/misc -tags=integration $(TEST_INTEGRATION_COMMON_LANG_RUN) $(GOTESTFMT) || FAILED=1; \ go test -timeout 60m -v ./e2e/common/traits -tags=integration $(TEST_INTEGRATION_COMMON_LANG_RUN) $(GOTESTFMT) || FAILED=1; \ + go test -timeout 20m -v ./e2e/common/runtimes -tags=integration $(TEST_INTEGRATION_COMMON_LANG_RUN) $(GOTESTFMT) || FAILED=1; \ go test -timeout 10m -v ./e2e/common/support/teardown_test.go -tags=integration $(TEST_INTEGRATION_COMMON_LANG_RUN) $(GOTESTFMT) || FAILED=1; \ exit $${FAILED}