From d248cfe005306b16d027839f49d013c0ae1e9c55 Mon Sep 17 00:00:00 2001 From: Pasquale Congiusti Date: Thu, 29 Aug 2024 14:39:21 +0200 Subject: [PATCH] feat(cli): promote kitless --- .../ROOT/pages/running/camel-runtimes.adoc | 8 +- .../modules/ROOT/pages/running/promoting.adoc | 10 +- e2e/advanced/tekton_test.go | 5 - e2e/install/upgrade/upgrade_test.go | 10 +- e2e/native/native_test.go | 3 - e2e/support/test_support.go | 22 +- .../camel/v1/integration_types_support.go | 7 +- .../v1/integration_types_support_test.go | 24 ++ .../camel/v1/integrationkit_types_support.go | 7 +- .../v1/integrationkit_types_support_test.go | 13 +- pkg/cmd/promote.go | 159 +++++-------- pkg/cmd/promote_test.go | 221 ++---------------- pkg/cmd/run.go | 8 - pkg/controller/integration/build_kit.go | 30 +-- pkg/controller/integration/monitor.go | 1 + pkg/controller/integrationkit/error.go | 1 + .../integrationkit_controller.go | 1 + pkg/controller/integrationkit/monitor.go | 1 + pkg/trait/builder_test.go | 7 +- pkg/trait/camel.go | 2 + pkg/trait/container_probes_test.go | 1 + pkg/trait/container_test.go | 27 ++- pkg/trait/istio_test.go | 7 +- pkg/trait/jvm.go | 102 ++++---- pkg/trait/jvm_test.go | 2 +- pkg/trait/quarkus.go | 49 ++-- pkg/trait/quarkus_test.go | 20 ++ pkg/trait/service_test.go | 21 +- 28 files changed, 313 insertions(+), 456 deletions(-) diff --git a/docs/modules/ROOT/pages/running/camel-runtimes.adoc b/docs/modules/ROOT/pages/running/camel-runtimes.adoc index 34da12edf7..ebe5834729 100644 --- a/docs/modules/ROOT/pages/running/camel-runtimes.adoc +++ b/docs/modules/ROOT/pages/running/camel-runtimes.adoc @@ -1,6 +1,8 @@ = Camel Runtimes (aka "sourceless" Integrations) -Camel K can run any runtime available in Apache Camel. However, this is possible only when the Camel application was previously built and packaged into a container image. Also, if you run through this option, some of the features offered by the operator may not be available. For example, you won't be able to discover Camel capabilities because the source is not available to the operator but embedded in the container image. +Camel K operator is traditionally in charge to perform a build from a Camel DSL source. The resulting Integration depends directly on an IntegrationKit, which is a reusable custom resource backing the final container image that your application will run. In the last versions, the only runtime the operator can build is Camel Quarkus (via Camel K Runtime project). + +However Camel K can run any runtime available in Apache Camel. This is possible only when the Camel application was previously built and packaged into a container image externally. Mind that if you run through this option, some of the features offered by the operator may not be available. For example, you won't be able to discover Camel capabilities because the source is not available to the operator but embedded in the container image. This option is quite interesting if in general you're building your applications externally, ie, via a CICD technology, and you want to delegate the operator only the "operational" part, taking care on your own of the building and publishing part. @@ -15,7 +17,7 @@ You can have your own Camel application or just create a basic one for the purpo The step above is a very quick way to create a basic Camel application in any of the available runtime. Let's imagine we've done this for Camel Main or we have already a Camel application as a Maven project. As the build part is something we want to take care on our own, we create a pipeline to build, containerize and push the container to a registry (see as a reference https://github.com/tektoncd/catalog/blob/main/task/kamel-run/0.1/samples/run-external-build.yaml[Camel K Tekton example]). -At this stage we do have a container image with our Camel application. We can use the `kamel` CLI to run our Camel application via `kamel run --image docker.io/my-org/my-app:1.0.0` tuning, if it's the case, with any of the trait or configuration required. Mind that, when you run an Integration with this option, the operator will create a **synthetic** IntegrationKit. +At this stage we do have a container image with our Camel application. We can use the `kamel` CLI to run our Camel application via `kamel run --image docker.io/my-org/my-app:1.0.0` tuning, if it's the case, with any of the trait or configuration required. As there is no creation of an IntegrationKit, this is also known as 'kit-less' Integration. If all is good, in a few seconds (there is no build involved) you should have your application up and running and you can monitor and operate with Camel K as usual. @@ -27,4 +29,4 @@ Every Camel application requires a `CamelCatalog` object to know how to perform [[traits]] == Trait configuration -Certain Camel K operational aspect may be driven by traits. When you're building the application outside the operator, some of those traits may not be executed as they are executed during the building phase that we are skipping when running **sourceless Integrations**. There is also no possible way to auto-tune certain traits that require the presence of the source. In this case, you should instead provide a trait configuration with the values that are required by your Integration (for example, Knative, Service and other deployment traits). \ No newline at end of file +Certain Camel K operational aspect may be driven by traits. When you're building the application outside the operator, some of those traits may not be executed as they are executed during the building phase that we are skipping when running **sourceless Integrations**. There is also no possible way to auto-tune certain traits that require the presence of the source. In this case, you should instead provide a trait configuration with the values that are required by your Integration (for example, Knative, Service and other deployment traits). diff --git a/docs/modules/ROOT/pages/running/promoting.adoc b/docs/modules/ROOT/pages/running/promoting.adoc index f7f5f4c355..308c87ae36 100644 --- a/docs/modules/ROOT/pages/running/promoting.adoc +++ b/docs/modules/ROOT/pages/running/promoting.adoc @@ -3,19 +3,11 @@ As soon as you have an Integration running in your cluster, you will be challenged to move that Integration to an higher environment. Ie, you can test your Integration in a **development** environment, and, as soon as you're happy with the result, you will need to move it into a **production** environment. -== External IntegrationKits - -When you create an Integration, the operator takes care to use an existing IntegrationKit or creating one from scratch. When you're moving your Integration across environments you'll therefore need to create an IntegrationKit accordingly. The creation of the IntegrationKit, in this case, is a simple copy of the original IntegrationKit with the `.spec.image` coming from the `.status.image` of the original IntegrationKit. Additionally, it has to be labelled as `camel.apache.org/kit.type: external`. - -NOTE: these two configuration are required to avoid the new IntegrationKit to kick off a new build operation. - -The copy is something you can do with any external tooling or using the `kamel promote` command as provided below: - == CLI `promote` command Camel K has an opinionated way to achieve the promotion goal through the usage of `kamel promote` command. With this command you will be able to easily move an Integration from one namespace to another without worrying about any low level detail such as resources needed by the Integration. You only need to make sure that both the source operator and the destination operator are using the same container registry and that the destination namespace provides the required Configmaps, Secrets or Kamelets required by the Integration. -NOTE: in order to use the same container registry, you can use the `--registry` option during installation phase or change the IntegrationPlatform to reflect that accordingly. +NOTE: use dry run option (`-o yaml`) and export the result to any separated cluster or Git repository to perform a GitOps strategy. Let's see a simple Integration that uses a Configmap to expose some message on an HTTP endpoint. We can start creating such an Integration and testing in a namespace called `development`: diff --git a/e2e/advanced/tekton_test.go b/e2e/advanced/tekton_test.go index 56e4e2fcce..86f8e0557d 100644 --- a/e2e/advanced/tekton_test.go +++ b/e2e/advanced/tekton_test.go @@ -73,11 +73,6 @@ func TestTektonLikeBehavior(t *testing.T) { g.Eventually(IntegrationPodPhase(t, ctx, ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning)) g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) g.Eventually(IntegrationLogs(t, ctx, ns, name), TestTimeoutShort).Should(ContainSubstring("Echo")) - tektonIntegrationKitName := IntegrationKit(t, ctx, ns, name)() - g.Expect(tektonIntegrationKitName).ToNot(BeEmpty()) - tektonIntegrationKitImage := KitImage(t, ctx, ns, tektonIntegrationKitName)() - g.Expect(tektonIntegrationKitImage).ToNot(BeEmpty()) - g.Eventually(Kit(t, ctx, ns, tektonIntegrationKitName)().Status.Image, TestTimeoutShort).Should(Equal(externalImage)) }) }) } diff --git a/e2e/install/upgrade/upgrade_test.go b/e2e/install/upgrade/upgrade_test.go index 50903ccc04..fe4ff1cabe 100644 --- a/e2e/install/upgrade/upgrade_test.go +++ b/e2e/install/upgrade/upgrade_test.go @@ -87,6 +87,7 @@ func TestUpgrade(t *testing.T) { // Check the IntegrationPlatform has been reconciled g.Eventually(PlatformPhase(t, ctx, ns)).Should(Equal(v1.IntegrationPlatformPhaseReady)) g.Eventually(PlatformVersion(t, ctx, ns)).Should(Equal(lastVersion)) + lastRuntimeVersion := PlatformRuntimeVersion(t, ctx, ns)() // We need a different namespace from the global operator WithNewTestNamespace(t, func(ctx context.Context, g *WithT, nsIntegration string) { @@ -117,6 +118,7 @@ func TestUpgrade(t *testing.T) { // Check the IntegrationPlatform has been reconciled g.Eventually(PlatformPhase(t, ctx, ns), TestTimeoutMedium).Should(Equal(v1.IntegrationPlatformPhaseReady)) g.Eventually(PlatformVersion(t, ctx, ns), TestTimeoutMedium).Should(Equal(defaults.Version)) + g.Eventually(PlatformRuntimeVersion(t, ctx, ns), TestTimeoutMedium).Should(Equal(defaults.DefaultRuntimeVersion)) // Check the Integration hasn't been upgraded g.Consistently(IntegrationVersion(t, ctx, nsIntegration, name), 15*time.Second, 3*time.Second). @@ -135,14 +137,14 @@ func TestUpgrade(t *testing.T) { g.Eventually(IntegrationVersion(t, ctx, nsIntegration, name), TestTimeoutMedium).Should(Equal(defaults.Version)) // Check the previous kit is not garbage collected - g.Eventually(Kits(t, ctx, ns, KitWithVersion(lastVersion))).Should(HaveLen(1)) + g.Eventually(Kits(t, ctx, ns, KitWithRuntimeVersion(lastRuntimeVersion))).Should(HaveLen(1)) // Check a new kit is created with the current version - g.Eventually(Kits(t, ctx, ns, KitWithVersion(defaults.Version))).Should(HaveLen(1)) + g.Eventually(Kits(t, ctx, ns, KitWithRuntimeVersion(defaults.DefaultRuntimeVersion))).Should(HaveLen(1)) // Check the new kit is ready - g.Eventually(Kits(t, ctx, ns, KitWithVersion(defaults.Version), KitWithPhase(v1.IntegrationKitPhaseReady)), + g.Eventually(Kits(t, ctx, ns, KitWithRuntimeVersion(defaults.DefaultRuntimeVersion), KitWithPhase(v1.IntegrationKitPhaseReady)), TestTimeoutMedium).Should(HaveLen(1)) - kit := Kits(t, ctx, ns, KitWithVersion(defaults.Version))()[0] + kit := Kits(t, ctx, ns, KitWithRuntimeVersion(defaults.DefaultRuntimeVersion))()[0] // Check the Integration uses the new image g.Eventually(IntegrationKit(t, ctx, nsIntegration, name), TestTimeoutMedium).Should(Equal(kit.Name)) diff --git a/e2e/native/native_test.go b/e2e/native/native_test.go index 893f96daac..29d93579f7 100644 --- a/e2e/native/native_test.go +++ b/e2e/native/native_test.go @@ -27,7 +27,6 @@ import ( "testing" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" . "github.com/apache/camel-k/v2/e2e/support" @@ -41,8 +40,6 @@ func TestNativeIntegrations(t *testing.T) { g.Expect(KamelRun(t, ctx, ns, "files/JavaScript.js", "--name", name, "-t", "quarkus.build-mode=native", "-t", "builder.tasks-limit-memory=quarkus-native:6.5Gi").Execute()).To(Succeed()) g.Eventually(IntegrationPhase(t, ctx, ns, name)).Should(Equal(v1.IntegrationPhaseError)) - g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionKitAvailable)). - Should(Equal(corev1.ConditionFalse)) }) t.Run("xml native support", func(t *testing.T) { diff --git a/e2e/support/test_support.go b/e2e/support/test_support.go index 48cab1b691..485379fcf8 100644 --- a/e2e/support/test_support.go +++ b/e2e/support/test_support.go @@ -1291,18 +1291,10 @@ func KitWithPhase(phase v1.IntegrationKitPhase) KitFilter { } } -func KitWithVersion(version string) KitFilter { +func KitWithRuntimeVersion(version string) KitFilter { return &kitFilter{ filter: func(kit *v1.IntegrationKit) bool { - return kit.Status.Version == version - }, - } -} - -func KitWithVersionPrefix(versionPrefix string) KitFilter { - return &kitFilter{ - filter: func(kit *v1.IntegrationKit) bool { - return strings.HasPrefix(kit.Status.Version, versionPrefix) + return kit.Status.RuntimeVersion == version }, } } @@ -2124,6 +2116,16 @@ func PlatformVersion(t *testing.T, ctx context.Context, ns string) func() string } } +func PlatformRuntimeVersion(t *testing.T, ctx context.Context, ns string) func() string { + return func() string { + p := Platform(t, ctx, ns)() + if p == nil { + return "" + } + return p.Status.Build.RuntimeVersion + } +} + func PlatformPhase(t *testing.T, ctx context.Context, ns string) func() v1.IntegrationPlatformPhase { return func() v1.IntegrationPlatformPhase { p := Platform(t, ctx, ns)() diff --git a/pkg/apis/camel/v1/integration_types_support.go b/pkg/apis/camel/v1/integration_types_support.go index dec40102f9..9683fef31b 100644 --- a/pkg/apis/camel/v1/integration_types_support.go +++ b/pkg/apis/camel/v1/integration_types_support.go @@ -19,6 +19,7 @@ package v1 import ( "fmt" + "regexp" "strings" corev1 "k8s.io/api/core/v1" @@ -90,7 +91,11 @@ func (in *Integration) UserDefinedSources() []SourceSpec { // IsManagedBuild returns true when the Integration requires to be built by the operator. func (in *Integration) IsManagedBuild() bool { - return in.Spec.Traits.Container == nil || in.Spec.Traits.Container.Image == "" + if in.Spec.Traits.Container == nil || in.Spec.Traits.Container.Image == "" { + return true + } + isManagedBuild, err := regexp.MatchString("(.*)/(.*)/camel-k-kit-(.*)@sha256:(.*)", in.Spec.Traits.Container.Image) + return err == nil && isManagedBuild } func (in *IntegrationSpec) AddSource(name string, content string, language Language) { diff --git a/pkg/apis/camel/v1/integration_types_support_test.go b/pkg/apis/camel/v1/integration_types_support_test.go index d2bbb87819..fda60c1b29 100644 --- a/pkg/apis/camel/v1/integration_types_support_test.go +++ b/pkg/apis/camel/v1/integration_types_support_test.go @@ -21,6 +21,7 @@ import ( "fmt" "testing" + "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" "github.com/stretchr/testify/assert" ) @@ -101,3 +102,26 @@ func TestGetConfigurationProperty(t *testing.T) { v6 := integration.GetConfigurationProperty("key6") assert.Equal(t, "", v6) } + +func TestManagedBuild(t *testing.T) { + integration := Integration{ + Spec: IntegrationSpec{}, + } + assert.True(t, integration.IsManagedBuild()) + integration.Spec.Traits = Traits{ + Container: &trait.ContainerTrait{}, + } + assert.True(t, integration.IsManagedBuild()) + integration.Spec.Traits = Traits{ + Container: &trait.ContainerTrait{ + Image: "registry.io/my-org/my-image", + }, + } + assert.False(t, integration.IsManagedBuild()) + integration.Spec.Traits = Traits{ + Container: &trait.ContainerTrait{ + Image: "10.100.107.57/camel-k/camel-k-kit-cr82ehho23os73cgua70@sha256:13e5a67d37665710c0bdd89701c7ae10aee393b00f5e4e09dc8ecc234763e7c2", + }, + } + assert.True(t, integration.IsManagedBuild()) +} diff --git a/pkg/apis/camel/v1/integrationkit_types_support.go b/pkg/apis/camel/v1/integrationkit_types_support.go index ed8595c3dd..ec8e92d5a3 100644 --- a/pkg/apis/camel/v1/integrationkit_types_support.go +++ b/pkg/apis/camel/v1/integrationkit_types_support.go @@ -20,7 +20,6 @@ package v1 import ( "fmt" "path/filepath" - "sort" "strconv" "github.com/apache/camel-k/v2/pkg/util/sets" @@ -211,16 +210,14 @@ func (in *IntegrationKitStatus) GetConditions() []ResourceCondition { } // GetDependenciesPaths returns the set of dependency paths. -func (in *IntegrationKitStatus) GetDependenciesPaths() []string { +func (in *IntegrationKitStatus) GetDependenciesPaths() *sets.Set { s := sets.NewSet() for _, dep := range in.Artifacts { path := filepath.Dir(dep.Target) s.Add(fmt.Sprintf("%s/*", path)) } - values := s.List() - sort.Strings(values) - return values + return s } func (c IntegrationKitCondition) GetType() string { diff --git a/pkg/apis/camel/v1/integrationkit_types_support_test.go b/pkg/apis/camel/v1/integrationkit_types_support_test.go index 78a741f81d..c75498ecf5 100644 --- a/pkg/apis/camel/v1/integrationkit_types_support_test.go +++ b/pkg/apis/camel/v1/integrationkit_types_support_test.go @@ -18,6 +18,7 @@ limitations under the License. package v1 import ( + "sort" "testing" "github.com/stretchr/testify/assert" @@ -39,9 +40,11 @@ func TestGetKitDependenciesDirectories(t *testing.T) { }, } paths := kit.Status.GetDependenciesPaths() - assert.Len(t, paths, 4) - assert.Equal(t, "my-dir/*", paths[0]) - assert.Equal(t, "my-dir1/lib/*", paths[1]) - assert.Equal(t, "my-dir1/lib2/*", paths[2]) - assert.Equal(t, "my-dir2/lib/*", paths[3]) + pathsArray := paths.List() + sort.Strings(pathsArray) + assert.Len(t, pathsArray, 4) + assert.Equal(t, "my-dir/*", pathsArray[0]) + assert.Equal(t, "my-dir1/lib/*", pathsArray[1]) + assert.Equal(t, "my-dir1/lib2/*", pathsArray[2]) + assert.Equal(t, "my-dir2/lib/*", pathsArray[3]) } diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go index 0a368b28de..339afac7a3 100644 --- a/pkg/cmd/promote.go +++ b/pkg/cmd/promote.go @@ -21,12 +21,14 @@ import ( "context" "errors" "fmt" + "sort" "strings" v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" "github.com/apache/camel-k/v2/pkg/client" "github.com/apache/camel-k/v2/pkg/util/kubernetes" + "github.com/apache/camel-k/v2/pkg/util/sets" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -133,12 +135,8 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { // Pipe promotion if promotePipe { - destPipe, destKit := o.editPipe(sourcePipe, sourceIntegration, sourceKit) + destPipe := o.editPipe(sourcePipe, sourceIntegration, sourceKit) if o.OutputFormat != "" { - if err := showIntegrationKitOutput(cmd, destKit, o.OutputFormat); err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), `---`) return showPipeOutput(cmd, destPipe, o.OutputFormat, c.GetScheme()) } // Ensure the destination namespace has access to the source namespace images @@ -146,10 +144,6 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { if err != nil { return err } - _, err = o.replaceResource(destKit) - if err != nil { - return err - } replaced, err := o.replaceResource(destPipe) if err != nil { return err @@ -163,12 +157,8 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { } // Plain Integration promotion - destIntegration, destKit := o.editIntegration(sourceIntegration, sourceKit) + destIntegration := o.editIntegration(sourceIntegration, sourceKit) if o.OutputFormat != "" { - if err := showIntegrationKitOutput(cmd, destKit, o.OutputFormat); err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), `---`) return showIntegrationOutput(cmd, destIntegration, o.OutputFormat) } // Ensure the destination namespace has access to the source namespace images @@ -176,10 +166,6 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { if err != nil { return err } - _, err = o.replaceResource(destKit) - if err != nil { - return err - } replaced, err := o.replaceResource(destIntegration) if err != nil { return err @@ -233,6 +219,9 @@ func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.In } func (o *promoteCmdOptions) getIntegrationKit(c client.Client, ref *corev1.ObjectReference) (*v1.IntegrationKit, error) { + if ref == nil { + return nil, nil + } ik := v1.NewIntegrationKit(ref.Namespace, ref.Name) key := k8sclient.ObjectKey{ Name: ref.Name, @@ -245,41 +234,63 @@ func (o *promoteCmdOptions) getIntegrationKit(c client.Client, ref *corev1.Objec return ik, nil } -func (o *promoteCmdOptions) editIntegration(it *v1.Integration, kit *v1.IntegrationKit) (*v1.Integration, *v1.IntegrationKit) { +func (o *promoteCmdOptions) editIntegration(it *v1.Integration, kit *v1.IntegrationKit) *v1.Integration { contImage := it.Status.Image - // IntegrationKit - dstKit := v1.NewIntegrationKit(o.To, kit.Name) - dstKit.Spec = *kit.Spec.DeepCopy() - dstKit.Annotations = cloneAnnotations(kit.Annotations, o.ToOperator) - dstKit.Labels = cloneLabels(kit.Labels) - dstKit.Labels = alterKitLabels(dstKit.Labels, kit) - dstKit.Spec.Image = contImage // Integration dstIt := v1.NewIntegration(o.To, it.Name) dstIt.Spec = *it.Spec.DeepCopy() dstIt.Annotations = cloneAnnotations(it.Annotations, o.ToOperator) dstIt.Labels = cloneLabels(it.Labels) - dstIt.Spec.IntegrationKit = &corev1.ObjectReference{ - Namespace: dstKit.Namespace, - Name: dstKit.Name, - Kind: dstKit.Kind, - } - // We must provide the classpath expected for the IntegrationKit. This is calculated dynamically and - // would get lost when creating the promoted IntegrationKit (which is in .status.artifacts). For this reason - // we must report it in the promoted Integration. - kitClasspath := kit.Status.GetDependenciesPaths() - if len(kitClasspath) > 0 { + dstIt.Spec.IntegrationKit = nil + if dstIt.Spec.Traits.Container == nil { + dstIt.Spec.Traits.Container = &traitv1.ContainerTrait{} + } + dstIt.Spec.Traits.Container.Image = contImage + if kit != nil { + // We must provide the classpath expected for the IntegrationKit. This is calculated dynamically and + // would get lost when creating the non managed build Integration. For this reason + // we must report it in the promoted Integration. if dstIt.Spec.Traits.JVM == nil { dstIt.Spec.Traits.JVM = &traitv1.JVMTrait{} } jvmTrait := dstIt.Spec.Traits.JVM - if jvmTrait.Classpath != "" { - jvmTrait.Classpath += ":" + mergedClasspath := getClasspath(kit, jvmTrait.Classpath) + jvmTrait.Classpath = mergedClasspath + // We must also set the runtime version so we pin it to the given catalog on which + // the container image was built + if dstIt.Spec.Traits.Camel == nil { + dstIt.Spec.Traits.Camel = &traitv1.CamelTrait{} + } + dstIt.Spec.Traits.Camel.RuntimeVersion = kit.Status.RuntimeVersion + } + + return &dstIt +} + +// getClasspath merges the classpath required by the kit with any value provided in the trait. +func getClasspath(kit *v1.IntegrationKit, jvmTraitClasspath string) string { + kitClasspathSet := kit.Status.GetDependenciesPaths() + if !kitClasspathSet.IsEmpty() { + if jvmTraitClasspath != "" { + jvmTraitClasspathSet := getClasspathSet(jvmTraitClasspath) + kitClasspathSet = sets.Union(kitClasspathSet, jvmTraitClasspathSet) } - jvmTrait.Classpath += strings.Join(kitClasspath, ":") + classPaths := kitClasspathSet.List() + sort.Strings(classPaths) + + return strings.Join(classPaths, ":") } - return &dstIt, dstKit + return jvmTraitClasspath +} + +func getClasspathSet(cps string) *sets.Set { + s := sets.NewSet() + for _, cp := range strings.Split(cps, ":") { + s.Add(cp) + } + + return s } // Return all annotations overriding the operator Id if provided. @@ -311,54 +322,14 @@ func cloneLabels(lbs map[string]string) map[string]string { return newMap } -// Change labels expected by Integration Kit replacing the creator to reflect the -// fact the new kit was cloned by another one instead. -func alterKitLabels(lbs map[string]string, kit *v1.IntegrationKit) map[string]string { - lbs[v1.IntegrationKitTypeLabel] = v1.IntegrationKitTypeExternal - if lbs[kubernetes.CamelCreatorLabelKind] != "" { - delete(lbs, kubernetes.CamelCreatorLabelKind) - } - if lbs[kubernetes.CamelCreatorLabelName] != "" { - delete(lbs, kubernetes.CamelCreatorLabelName) - } - if lbs[kubernetes.CamelCreatorLabelNamespace] != "" { - delete(lbs, kubernetes.CamelCreatorLabelNamespace) - } - if lbs[kubernetes.CamelCreatorLabelVersion] != "" { - delete(lbs, kubernetes.CamelCreatorLabelVersion) - } - - lbs[kubernetes.CamelClonedLabelKind] = v1.IntegrationKitKind - lbs[kubernetes.CamelClonedLabelName] = kit.Name - lbs[kubernetes.CamelClonedLabelNamespace] = kit.Namespace - lbs[kubernetes.CamelClonedLabelVersion] = kit.ResourceVersion - - return lbs -} - -func (o *promoteCmdOptions) editPipe(kb *v1.Pipe, it *v1.Integration, kit *v1.IntegrationKit) (*v1.Pipe, *v1.IntegrationKit) { +func (o *promoteCmdOptions) editPipe(kb *v1.Pipe, it *v1.Integration, kit *v1.IntegrationKit) *v1.Pipe { contImage := it.Status.Image - // IntegrationKit - dstKit := v1.NewIntegrationKit(o.To, kit.Name) - dstKit.Spec = *kit.Spec.DeepCopy() - dstKit.Annotations = cloneAnnotations(kit.Annotations, o.ToOperator) - dstKit.Labels = cloneLabels(kit.Labels) - dstKit.Labels = alterKitLabels(dstKit.Labels, kit) - dstKit.Spec.Image = contImage // Pipe dst := v1.NewPipe(o.To, kb.Name) dst.Spec = *kb.Spec.DeepCopy() dst.Annotations = cloneAnnotations(kb.Annotations, o.ToOperator) dst.Labels = cloneLabels(kb.Labels) - if dst.Spec.Integration == nil { - dst.Spec.Integration = &v1.IntegrationSpec{} - } - dst.Spec.Integration.IntegrationKit = &corev1.ObjectReference{ - Namespace: dstKit.Namespace, - Name: dstKit.Name, - Kind: dstKit.Kind, - } - + dst.Annotations[fmt.Sprintf("%scontainer.image", v1.TraitAnnotationPrefix)] = contImage if dst.Spec.Source.Ref != nil { dst.Spec.Source.Ref.Namespace = o.To } @@ -373,22 +344,18 @@ func (o *promoteCmdOptions) editPipe(kb *v1.Pipe, it *v1.Integration, kit *v1.In } } - // We must provide the classpath expected for the IntegrationKit. This is calculated dynamically and - // would get lost when creating the promoted IntegrationKit (which is in .status.artifacts). For this reason - // we must report it in the promoted Integration. - kitClasspath := kit.Status.GetDependenciesPaths() - if len(kitClasspath) > 0 { - if dst.Spec.Integration.Traits.JVM == nil { - dst.Spec.Integration.Traits.JVM = &traitv1.JVMTrait{} - } - jvmTrait := dst.Spec.Integration.Traits.JVM - if jvmTrait.Classpath != "" { - jvmTrait.Classpath += ":" - } - jvmTrait.Classpath += strings.Join(kitClasspath, ":") + if kit != nil { + // We must provide the classpath expected for the IntegrationKit. This is calculated dynamically and + // would get lost when creating the non managed build Integration. For this reason + // we must report it in the promoted Integration. + mergedClasspath := getClasspath(kit, dst.Annotations[fmt.Sprintf("%sjvm.classpath", v1.TraitAnnotationPrefix)]) + dst.Annotations[fmt.Sprintf("%sjvm.classpath", v1.TraitAnnotationPrefix)] = mergedClasspath + // We must also set the runtime version so we pin it to the given catalog on which + // the container image was built + dst.Annotations[fmt.Sprintf("%scamel.runtime-version", v1.TraitAnnotationPrefix)] = kit.Status.RuntimeVersion } - return &dst, dstKit + return &dst } func (o *promoteCmdOptions) replaceResource(res k8sclient.Object) (bool, error) { diff --git a/pkg/cmd/promote_test.go b/pkg/cmd/promote_test.go index 72b87d4956..0d92f40897 100644 --- a/pkg/cmd/promote_test.go +++ b/pkg/cmd/promote_test.go @@ -24,7 +24,6 @@ import ( v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" "github.com/apache/camel-k/v2/pkg/platform" "github.com/apache/camel-k/v2/pkg/util/defaults" - "github.com/apache/camel-k/v2/pkg/util/kubernetes" "github.com/apache/camel-k/v2/pkg/util/test" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" @@ -95,34 +94,17 @@ func TestIntegrationDryRun(t *testing.T) { assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat) require.NoError(t, err) assert.Equal(t, `apiVersion: camel.apache.org/v1 -kind: IntegrationKit -metadata: - creationTimestamp: null - labels: - camel.apache.org/cloned.from.kind: IntegrationKit - camel.apache.org/cloned.from.name: my-it-test-kit - camel.apache.org/cloned.from.namespace: default - camel.apache.org/cloned.from.version: "999" - camel.apache.org/kit.type: external - name: my-it-test-kit - namespace: prod-namespace -spec: - image: my-special-image - traits: {} -status: {} ---- -apiVersion: camel.apache.org/v1 kind: Integration metadata: creationTimestamp: null name: my-it-test namespace: prod-namespace spec: - integrationKit: - kind: IntegrationKit - name: my-it-test-kit - namespace: prod-namespace traits: + camel: + runtimeVersion: 1.2.3 + container: + image: my-special-image jvm: classpath: /path/to/artifact-1/*:/path/to/artifact-2/* status: {} @@ -139,6 +121,7 @@ func nominalIntegration(name string) (v1.Integration, v1.IntegrationKit) { {Target: "/path/to/artifact-1/a-1.jar"}, {Target: "/path/to/artifact-2/a-2.jar"}, }, + RuntimeVersion: "1.2.3", } it.Status.IntegrationKit = &corev1.ObjectReference{ Namespace: ik.Namespace, @@ -167,37 +150,16 @@ func TestPipeDryRun(t *testing.T) { assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat) require.NoError(t, err) assert.Equal(t, `apiVersion: camel.apache.org/v1 -kind: IntegrationKit -metadata: - creationTimestamp: null - labels: - camel.apache.org/cloned.from.kind: IntegrationKit - camel.apache.org/cloned.from.name: my-kb-test-kit - camel.apache.org/cloned.from.namespace: default - camel.apache.org/cloned.from.version: "999" - camel.apache.org/kit.type: external - name: my-kb-test-kit - namespace: prod-namespace -spec: - image: my-special-image - traits: {} -status: {} ---- -apiVersion: camel.apache.org/v1 kind: Pipe metadata: + annotations: + trait.camel.apache.org/camel.runtime-version: 1.2.3 + trait.camel.apache.org/container.image: my-special-image + trait.camel.apache.org/jvm.classpath: /path/to/artifact-1/*:/path/to/artifact-2/* creationTimestamp: null name: my-kb-test namespace: prod-namespace spec: - integration: - integrationKit: - kind: IntegrationKit - name: my-kb-test-kit - namespace: prod-namespace - traits: - jvm: - classpath: /path/to/artifact-1/*:/path/to/artifact-2/* sink: {} source: {} status: {} @@ -241,23 +203,6 @@ func TestIntegrationWithMetadataDryRun(t *testing.T) { assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat) require.NoError(t, err) assert.Equal(t, `apiVersion: camel.apache.org/v1 -kind: IntegrationKit -metadata: - creationTimestamp: null - labels: - camel.apache.org/cloned.from.kind: IntegrationKit - camel.apache.org/cloned.from.name: my-it-test-kit - camel.apache.org/cloned.from.namespace: default - camel.apache.org/cloned.from.version: "999" - camel.apache.org/kit.type: external - name: my-it-test-kit - namespace: prod-namespace -spec: - image: my-special-image - traits: {} -status: {} ---- -apiVersion: camel.apache.org/v1 kind: Integration metadata: annotations: @@ -268,11 +213,11 @@ metadata: name: my-it-test namespace: prod-namespace spec: - integrationKit: - kind: IntegrationKit - name: my-it-test-kit - namespace: prod-namespace traits: + camel: + runtimeVersion: 1.2.3 + container: + image: my-special-image jvm: classpath: /path/to/artifact-1/*:/path/to/artifact-2/* status: {} @@ -305,41 +250,19 @@ func TestPipeWithMetadataDryRun(t *testing.T) { assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat) require.NoError(t, err) assert.Equal(t, `apiVersion: camel.apache.org/v1 -kind: IntegrationKit -metadata: - creationTimestamp: null - labels: - camel.apache.org/cloned.from.kind: IntegrationKit - camel.apache.org/cloned.from.name: my-kb-test-kit - camel.apache.org/cloned.from.namespace: default - camel.apache.org/cloned.from.version: "999" - camel.apache.org/kit.type: external - name: my-kb-test-kit - namespace: prod-namespace -spec: - image: my-special-image - traits: {} -status: {} ---- -apiVersion: camel.apache.org/v1 kind: Pipe metadata: annotations: my-annotation: my-value + trait.camel.apache.org/camel.runtime-version: 1.2.3 + trait.camel.apache.org/container.image: my-special-image + trait.camel.apache.org/jvm.classpath: /path/to/artifact-1/*:/path/to/artifact-2/* creationTimestamp: null labels: my-label: my-value name: my-kb-test namespace: prod-namespace spec: - integration: - integrationKit: - kind: IntegrationKit - name: my-kb-test-kit - namespace: prod-namespace - traits: - jvm: - classpath: /path/to/artifact-1/*:/path/to/artifact-2/* sink: {} source: {} status: {} @@ -404,25 +327,6 @@ func TestIntegrationToOperatorId(t *testing.T) { assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat) require.NoError(t, err) assert.Equal(t, `apiVersion: camel.apache.org/v1 -kind: IntegrationKit -metadata: - annotations: - camel.apache.org/operator.id: my-prod-operator - creationTimestamp: null - labels: - camel.apache.org/cloned.from.kind: IntegrationKit - camel.apache.org/cloned.from.name: my-it-test-kit - camel.apache.org/cloned.from.namespace: default - camel.apache.org/cloned.from.version: "999" - camel.apache.org/kit.type: external - name: my-it-test-kit - namespace: prod -spec: - image: my-special-image - traits: {} -status: {} ---- -apiVersion: camel.apache.org/v1 kind: Integration metadata: annotations: @@ -431,11 +335,11 @@ metadata: name: my-it-test namespace: prod spec: - integrationKit: - kind: IntegrationKit - name: my-it-test-kit - namespace: prod traits: + camel: + runtimeVersion: 1.2.3 + container: + image: my-special-image jvm: classpath: /path/to/artifact-1/*:/path/to/artifact-2/* status: {} @@ -449,25 +353,6 @@ status: {} assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat) require.NoError(t, err) assert.Equal(t, `apiVersion: camel.apache.org/v1 -kind: IntegrationKit -metadata: - annotations: - camel.apache.org/operator.id: my-prod-operator - creationTimestamp: null - labels: - camel.apache.org/cloned.from.kind: IntegrationKit - camel.apache.org/cloned.from.name: my-it-test-kit - camel.apache.org/cloned.from.namespace: default - camel.apache.org/cloned.from.version: "999" - camel.apache.org/kit.type: external - name: my-it-test-kit - namespace: prod -spec: - image: my-special-image - traits: {} -status: {} ---- -apiVersion: camel.apache.org/v1 kind: Integration metadata: annotations: @@ -476,69 +361,11 @@ metadata: name: my-it-test namespace: prod spec: - integrationKit: - kind: IntegrationKit - name: my-it-test-kit - namespace: prod - traits: - jvm: - classpath: /path/to/artifact-1/*:/path/to/artifact-2/* -status: {} -`, output) -} - -func TestIntegrationKitWithLabels(t *testing.T) { - srcPlatform := v1.NewIntegrationPlatform("default", platform.DefaultPlatformName) - srcPlatform.Status.Version = defaults.Version - srcPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion - srcPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady - dstPlatform := v1.NewIntegrationPlatform("prod-namespace", platform.DefaultPlatformName) - dstPlatform.Status.Version = defaults.Version - dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion - dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady - defaultIntegration, defaultKit := nominalIntegration("my-it-test") - defaultKit.Labels = map[string]string{ - kubernetes.CamelCreatorLabelKind: "Integration", - kubernetes.CamelCreatorLabelName: "my-original-it-name", - kubernetes.CamelCreatorLabelNamespace: "my-original-it-namespace", - kubernetes.CamelCreatorLabelVersion: "my-original-it-version", - } - srcCatalog := createTestCamelCatalog(srcPlatform) - dstCatalog := createTestCamelCatalog(dstPlatform) - - promoteCmdOptions, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultIntegration, &defaultKit, &srcCatalog, &dstCatalog) - output, err := test.ExecuteCommand(promoteCmd, cmdPromote, "my-it-test", "--to", "prod-namespace", "-o", "yaml", "-n", "default") - assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat) - require.NoError(t, err) - assert.Equal(t, `apiVersion: camel.apache.org/v1 -kind: IntegrationKit -metadata: - creationTimestamp: null - labels: - camel.apache.org/cloned.from.kind: IntegrationKit - camel.apache.org/cloned.from.name: my-it-test-kit - camel.apache.org/cloned.from.namespace: default - camel.apache.org/cloned.from.version: "999" - camel.apache.org/kit.type: external - name: my-it-test-kit - namespace: prod-namespace -spec: - image: my-special-image - traits: {} -status: {} ---- -apiVersion: camel.apache.org/v1 -kind: Integration -metadata: - creationTimestamp: null - name: my-it-test - namespace: prod-namespace -spec: - integrationKit: - kind: IntegrationKit - name: my-it-test-kit - namespace: prod-namespace traits: + camel: + runtimeVersion: 1.2.3 + container: + image: my-special-image jvm: classpath: /path/to/artifact-1/*:/path/to/artifact-2/* status: {} diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 52419bdfdd..8f458b33de 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -594,14 +594,6 @@ func showIntegrationOutput(cmd *cobra.Command, integration *v1.Integration, outp return printer.PrintObj(integration, cmd.OutOrStdout()) } -func showIntegrationKitOutput(cmd *cobra.Command, integrationKit *v1.IntegrationKit, outputFormat string) error { - printer := printers.NewTypeSetter(scheme.Scheme) - printer.Delegate = &kubernetes.CLIPrinter{ - Format: outputFormat, - } - return printer.PrintObj(integrationKit, cmd.OutOrStdout()) -} - func (o *runCmdOptions) getIntegration(cmd *cobra.Command, c client.Client, namespace, name string) (*v1.Integration, *v1.Integration, error) { it := &v1.Integration{ TypeMeta: metav1.TypeMeta{ diff --git a/pkg/controller/integration/build_kit.go b/pkg/controller/integration/build_kit.go index 1ec95bac61..2ac2d4fd08 100644 --- a/pkg/controller/integration/build_kit.go +++ b/pkg/controller/integration/build_kit.go @@ -45,7 +45,6 @@ func (action *buildKitAction) CanHandle(integration *v1.Integration) bool { func (action *buildKitAction) Handle(ctx context.Context, integration *v1.Integration) (*v1.Integration, error) { // TODO: we may need to add a timeout strategy, i.e give up after some time in case of an unrecoverable error. - secrets, configmaps := getIntegrationSecretAndConfigmapResourceVersions(ctx, action.client, integration) hash, err := digest.ComputeForIntegration(integration, configmaps, secrets) if err != nil { @@ -73,9 +72,10 @@ func (action *buildKitAction) Handle(ctx context.Context, integration *v1.Integr action.L.Debug("No kit specified in integration status so looking up", "integration", integration.Name, "namespace", integration.Namespace) existingKits, err := lookupKitsForIntegration(ctx, action.client, integration) if err != nil { - return nil, fmt.Errorf("failed to lookup kits for integration %s/%s: %w", - integration.Namespace, integration.Name, err) - + err = fmt.Errorf("failed to lookup kits for integration %s/%s: %w", integration.Namespace, integration.Name, err) + integration.Status.Phase = v1.IntegrationPhaseError + integration.SetReadyConditionError(err.Error()) + return integration, err } action.L.Debug("Applying traits to integration", @@ -83,9 +83,10 @@ func (action *buildKitAction) Handle(ctx context.Context, integration *v1.Integr "namespace", integration.Namespace) env, err := trait.Apply(ctx, action.client, integration, nil) if err != nil { - return nil, fmt.Errorf("failed to apply traits to integration %s/%s: %w", - integration.Namespace, integration.Name, err) - + err = fmt.Errorf("failed to apply traits to integration %s/%s: %w", integration.Namespace, integration.Name, err) + integration.Status.Phase = v1.IntegrationPhaseError + integration.SetReadyConditionError(err.Error()) + return integration, err } action.L.Debug("Searching integration kits to assign to integration", "integration", @@ -100,9 +101,10 @@ kits: action.L.Debug("Comparing existing kit with environment", "env kit", kit.Name, "existing kit", k.Name) match, err := kitMatches(action.client, &kit, k) if err != nil { - return nil, fmt.Errorf("error occurred matches integration kits with environment for integration %s/%s: %w", - integration.Namespace, integration.Name, err) - + err = fmt.Errorf("error occurred matches integration kits with environment for integration %s/%s: %w", integration.Namespace, integration.Name, err) + integration.Status.Phase = v1.IntegrationPhaseError + integration.SetReadyConditionError(err.Error()) + return integration, err } if match { if integrationKit == nil || @@ -123,9 +125,10 @@ kits: "namespace", integration.Namespace, "integration kit", kit.Name) if err := action.client.Create(ctx, &kit); err != nil { - return nil, fmt.Errorf("failed to create new integration kit for integration %s/%s: %w", - integration.Namespace, integration.Name, err) - + err = fmt.Errorf("failed to create new integration kit for integration %s/%s: %w", integration.Namespace, integration.Name, err) + integration.Status.Phase = v1.IntegrationPhaseError + integration.SetReadyConditionError(err.Error()) + return integration, err } if integrationKit == nil { integrationKit = &kit @@ -133,7 +136,6 @@ kits: } if integrationKit != nil { - action.L.Debug("Setting integration kit for integration", "integration", integration.Name, "namespace", integration.Namespace, "integration kit", integrationKit.Name) // Set the kit name so the next handle loop, will fall through the // same path as integration with a user defined kit diff --git a/pkg/controller/integration/monitor.go b/pkg/controller/integration/monitor.go index f82e8a13e6..af90cc3db7 100644 --- a/pkg/controller/integration/monitor.go +++ b/pkg/controller/integration/monitor.go @@ -65,6 +65,7 @@ func (action *monitorAction) CanHandle(integration *v1.Integration) bool { integration.Status.Phase == v1.IntegrationPhaseError } +//nolint:nestif func (action *monitorAction) Handle(ctx context.Context, integration *v1.Integration) (*v1.Integration, error) { // When in InitializationFailed condition a kit is not available for the integration // so handle it differently from the rest diff --git a/pkg/controller/integrationkit/error.go b/pkg/controller/integrationkit/error.go index 9f245f0efc..596690ee91 100644 --- a/pkg/controller/integrationkit/error.go +++ b/pkg/controller/integrationkit/error.go @@ -42,6 +42,7 @@ func (action *errorAction) CanHandle(kit *v1.IntegrationKit) bool { } func (action *errorAction) Handle(ctx context.Context, kit *v1.IntegrationKit) (*v1.IntegrationKit, error) { + //nolint: staticcheck if kit.IsExternal() || kit.IsSynthetic() { // do nothing, it's not a managed kit return nil, nil diff --git a/pkg/controller/integrationkit/integrationkit_controller.go b/pkg/controller/integrationkit/integrationkit_controller.go index 4dde58bb4c..6705907e82 100644 --- a/pkg/controller/integrationkit/integrationkit_controller.go +++ b/pkg/controller/integrationkit/integrationkit_controller.go @@ -242,6 +242,7 @@ func (r *reconcileIntegrationKit) Reconcile(ctx context.Context, request reconci //nolint:nestif if target.Status.Phase == v1.IntegrationKitPhaseNone || target.Status.Phase == v1.IntegrationKitPhaseWaitingForPlatform { rlog.Debug("Preparing to shift integration kit phase") + //nolint: staticcheck if target.IsExternal() || target.IsSynthetic() { target.Status.Phase = v1.IntegrationKitPhaseInitialization return r.update(ctx, &instance, target) diff --git a/pkg/controller/integrationkit/monitor.go b/pkg/controller/integrationkit/monitor.go index a574bb9448..82e958ffcc 100644 --- a/pkg/controller/integrationkit/monitor.go +++ b/pkg/controller/integrationkit/monitor.go @@ -44,6 +44,7 @@ func (action *monitorAction) CanHandle(kit *v1.IntegrationKit) bool { } func (action *monitorAction) Handle(ctx context.Context, kit *v1.IntegrationKit) (*v1.IntegrationKit, error) { + //nolint: staticcheck if kit.IsExternal() || kit.IsSynthetic() { // do nothing, it's not a managed kit // if it's a syntetic Kit add a condition to warn this is a diff --git a/pkg/trait/builder_test.go b/pkg/trait/builder_test.go index bc1adede58..a096c6e7c8 100644 --- a/pkg/trait/builder_test.go +++ b/pkg/trait/builder_test.go @@ -45,11 +45,12 @@ func TestBuilderTraitNotAppliedBecauseOfNilKit(t *testing.T) { e.IntegrationKit = nil t.Run(string(e.Platform.Status.Cluster), func(t *testing.T) { - conditions, err := NewBuilderTestCatalog().apply(e) + trait, _ := newBuilderTrait().(*builderTrait) + configure, conditions, err := trait.Configure(e) + assert.False(t, configure) + assert.Empty(t, conditions) require.NoError(t, err) - assert.NotEmpty(t, conditions) - assert.NotEmpty(t, e.ExecutedTraits) assert.Nil(t, e.GetTrait("builder")) assert.Empty(t, e.Pipeline) }) diff --git a/pkg/trait/camel.go b/pkg/trait/camel.go index 0d585a3e6b..698f360514 100644 --- a/pkg/trait/camel.go +++ b/pkg/trait/camel.go @@ -78,6 +78,7 @@ func (t *camelTrait) Configure(e *Environment) (bool, *TraitCondition, error) { } var cond *TraitCondition + //nolint: staticcheck if (e.Integration != nil && !e.Integration.IsManagedBuild()) || (e.IntegrationKit != nil && e.IntegrationKit.IsSynthetic()) { // We set a condition to warn the user the catalog used to run the Integration // may differ from the runtime version which we don't control @@ -118,6 +119,7 @@ func (t *camelTrait) Apply(e *Environment) error { } } if e.IntegrationKit != nil { + //nolint: staticcheck if !e.IntegrationKit.IsSynthetic() { e.IntegrationKit.Status.RuntimeVersion = e.CamelCatalog.Runtime.Version e.IntegrationKit.Status.RuntimeProvider = e.CamelCatalog.Runtime.Provider diff --git a/pkg/trait/container_probes_test.go b/pkg/trait/container_probes_test.go index 5a60f73522..4b128e9c2a 100644 --- a/pkg/trait/container_probes_test.go +++ b/pkg/trait/container_probes_test.go @@ -57,6 +57,7 @@ func newTestProbesEnv(t *testing.T, integration *v1.Integration) Environment { }, }, }, + IntegrationKit: &v1.IntegrationKit{}, Integration: integration, Resources: kubernetes.NewCollection(), ApplicationProperties: make(map[string]string), diff --git a/pkg/trait/container_test.go b/pkg/trait/container_test.go index 80a60efcf4..db41467a84 100644 --- a/pkg/trait/container_test.go +++ b/pkg/trait/container_test.go @@ -384,10 +384,11 @@ func TestContainerWithImagePullPolicy(t *testing.T) { traitCatalog := NewCatalog(nil) environment := Environment{ - Ctx: context.TODO(), - Client: client, - CamelCatalog: catalog, - Catalog: traitCatalog, + Ctx: context.TODO(), + Client: client, + CamelCatalog: catalog, + Catalog: traitCatalog, + IntegrationKit: &v1.IntegrationKit{}, Integration: &v1.Integration{ Spec: v1.IntegrationSpec{ Profile: v1.TraitProfileKubernetes, @@ -431,10 +432,11 @@ func TestDeploymentContainerPorts(t *testing.T) { traitCatalog := NewCatalog(nil) environment := Environment{ - Ctx: context.TODO(), - Client: client, - CamelCatalog: catalog, - Catalog: traitCatalog, + Ctx: context.TODO(), + Client: client, + CamelCatalog: catalog, + Catalog: traitCatalog, + IntegrationKit: &v1.IntegrationKit{}, Integration: &v1.Integration{ Spec: v1.IntegrationSpec{ Profile: v1.TraitProfileKubernetes, @@ -496,10 +498,11 @@ func TestKnativeServiceContainerPorts(t *testing.T) { traitCatalog := NewCatalog(nil) environment := Environment{ - Ctx: context.TODO(), - Client: client, - CamelCatalog: catalog, - Catalog: traitCatalog, + Ctx: context.TODO(), + Client: client, + CamelCatalog: catalog, + Catalog: traitCatalog, + IntegrationKit: &v1.IntegrationKit{}, Integration: &v1.Integration{ Spec: v1.IntegrationSpec{ Profile: v1.TraitProfileKnative, diff --git a/pkg/trait/istio_test.go b/pkg/trait/istio_test.go index 7be2a62917..e6f63df4c5 100644 --- a/pkg/trait/istio_test.go +++ b/pkg/trait/istio_test.go @@ -45,9 +45,10 @@ func NewIstioTestEnv(t *testing.T, d *appsv1.Deployment, s *serving.Service, ena require.NoError(t, err) env := Environment{ - Catalog: NewEnvironmentTestCatalog(), - CamelCatalog: catalog, - Client: client, + Catalog: NewEnvironmentTestCatalog(), + CamelCatalog: catalog, + Client: client, + IntegrationKit: &v1.IntegrationKit{}, Integration: &v1.Integration{ Status: v1.IntegrationStatus{ Phase: v1.IntegrationPhaseDeploying, diff --git a/pkg/trait/jvm.go b/pkg/trait/jvm.go index a123f00daf..0a28ba036d 100644 --- a/pkg/trait/jvm.go +++ b/pkg/trait/jvm.go @@ -69,17 +69,12 @@ func (t *jvmTrait) Configure(e *Environment) (bool, *TraitCondition, error) { notice := userDisabledMessage + "; this configuration is deprecated and may be removed within next releases" return false, NewIntegrationCondition("JVM", v1.IntegrationConditionTraitInfo, corev1.ConditionTrue, traitConfigurationReason, notice), nil } - if !e.IntegrationKitInPhase(v1.IntegrationKitPhaseReady) || !e.IntegrationInRunningPhases() { - return false, nil, nil - } - // The JVM trait must be disabled in case the current IntegrationKit corresponds to a native build - if qt := e.Catalog.GetTrait(quarkusTraitID); qt != nil { - if quarkus, ok := qt.(*quarkusTrait); ok && quarkus.isNativeIntegration(e) { - return false, NewIntegrationConditionPlatformDisabledWithMessage("JVM", "quarkus native build"), nil - } + if (e.IntegrationKit != nil && !e.IntegrationKitInPhase(v1.IntegrationKitPhaseReady)) || !e.IntegrationInRunningPhases() { + return false, nil, nil } + //nolint: staticcheck if ((e.Integration != nil && !e.Integration.IsManagedBuild()) || (e.IntegrationKit != nil && e.IntegrationKit.IsSynthetic())) && t.Jar == "" { // We skip this trait since we cannot make any assumption on the container Java tooling running @@ -90,6 +85,13 @@ func (t *jvmTrait) Configure(e *Environment) (bool, *TraitCondition, error) { ), nil } + // The JVM trait must be disabled in case the current IntegrationKit corresponds to a native build + if qt := e.Catalog.GetTrait(quarkusTraitID); qt != nil { + if quarkus, ok := qt.(*quarkusTrait); ok && quarkus.isNativeIntegration(e) { + return false, NewIntegrationConditionPlatformDisabledWithMessage("JVM", "quarkus native build"), nil + } + } + return true, nil, nil } @@ -139,6 +141,11 @@ func (t *jvmTrait) Apply(e *Environment) error { args = append(args, httpProxyArgs...) } + return t.feedContainer(container, args, e) +} + +//nolint:nestif +func (t *jvmTrait) feedContainer(container *corev1.Container, args []string, e *Environment) error { // If user provided the jar, we will execute on the container something like // java -Dxyx ... -cp ... -jar my-app.jar // For this reason it's important that the container is a java based container able to run a Camel (hence Java) application @@ -147,20 +154,23 @@ func (t *jvmTrait) Apply(e *Environment) error { classpathItems := t.prepareClasspathItems(container) if t.Jar != "" { // User is providing the Jar to execute explicitly - args = append(args, "-cp", strings.Join(classpathItems, ":")) + args = append(args, "-cp", classpathItems) args = append(args, "-jar", t.Jar) } else { kit, err := t.getIntegrationKit(e) if err != nil { return err } - kitDepsDirs := kit.Status.GetDependenciesPaths() - if len(kitDepsDirs) == 0 { - // Use legacy Camel Quarkus expected structure - kitDepsDirs = getLegacyCamelQuarkusDependenciesPaths() + if kit != nil { + // managed Integrations + kitDepsDirs := kit.Status.GetDependenciesPaths() + if kitDepsDirs.IsEmpty() { + // Use legacy Camel Quarkus expected structure + kitDepsDirs = getLegacyCamelQuarkusDependenciesPaths() + } + classpathItems = getClasspath(kitDepsDirs, classpathItems) } - classpathItems = append(classpathItems, kitDepsDirs...) - args = append(args, "-cp", strings.Join(classpathItems, ":")) + args = append(args, "-cp", classpathItems) args = append(args, e.CamelCatalog.Runtime.ApplicationClass) } container.Args = args @@ -168,28 +178,41 @@ func (t *jvmTrait) Apply(e *Environment) error { return nil } +// getClasspath merges the classpath required by the kit with any value provided in the trait. +func getClasspath(depsDirs *sets.Set, jvmTraitClasspath string) string { + if !depsDirs.IsEmpty() { + if jvmTraitClasspath != "" { + jvmTraitClasspathSet := getClasspathSet(jvmTraitClasspath) + depsDirs = sets.Union(depsDirs, jvmTraitClasspathSet) + } + classPaths := depsDirs.List() + sort.Strings(classPaths) + + return strings.Join(classPaths, ":") + } + + return jvmTraitClasspath +} + +func getClasspathSet(cps string) *sets.Set { + s := sets.NewSet() + for _, cp := range strings.Split(cps, ":") { + s.Add(cp) + } + + return s +} + func (t *jvmTrait) getIntegrationKit(e *Environment) (*v1.IntegrationKit, error) { kit := e.IntegrationKit if kit == nil && e.Integration.Status.IntegrationKit != nil { name := e.Integration.Status.IntegrationKit.Name ns := e.Integration.GetIntegrationKitNamespace(e.Platform) - k := v1.NewIntegrationKit(ns, name) - if err := t.Client.Get(e.Ctx, ctrl.ObjectKeyFromObject(k), k); err != nil { + kit = v1.NewIntegrationKit(ns, name) + if err := t.Client.Get(e.Ctx, ctrl.ObjectKeyFromObject(kit), kit); err != nil { return nil, fmt.Errorf("unable to find integration kit %s/%s: %w", ns, name, err) } - kit = k - } - - if kit == nil { - if e.Integration.Status.IntegrationKit != nil { - return nil, fmt.Errorf( - "unable to find integration kit %s/%s", - e.Integration.GetIntegrationKitNamespace(e.Platform), - e.Integration.Status.IntegrationKit.Name, - ) - } - return nil, fmt.Errorf("unable to find integration kit for integration %s", e.Integration.Name) } return kit, nil @@ -212,7 +235,7 @@ func (t *jvmTrait) enableDebug(e *Environment) string { suspend, t.DebugAddress) } -func (t *jvmTrait) prepareClasspathItems(container *corev1.Container) []string { +func (t *jvmTrait) prepareClasspathItems(container *corev1.Container) string { existingClasspaths := extractExistingClasspathItems(container) classpath := sets.NewSet() // Deprecated: replaced by /etc/camel/resources.d/[_configmaps/_secrets] (camel.ResourcesConfigmapsMountPath/camel.ResourcesSecretsMountPath). @@ -235,10 +258,10 @@ func (t *jvmTrait) prepareClasspathItems(container *corev1.Container) []string { if existingClasspaths != nil { existingClasspaths = append(existingClasspaths, items...) - return existingClasspaths + return strings.Join(existingClasspaths, ":") } - return items + return strings.Join(items, ":") } // extractExistingClasspathItems returns any container classpath option (if exists). @@ -317,11 +340,12 @@ func (t *jvmTrait) prepareHTTPProxy(container *corev1.Container) ([]string, erro } // Deprecated: to be removed as soon as version 2.3.x is no longer supported. -func getLegacyCamelQuarkusDependenciesPaths() []string { - return []string{ - "dependencies/*", - "dependencies/lib/boot/*", - "dependencies/lib/main/*", - "dependencies/quarkus/*", - } +func getLegacyCamelQuarkusDependenciesPaths() *sets.Set { + s := sets.NewSet() + s.Add("dependencies/*") + s.Add("dependencies/lib/boot/*") + s.Add("dependencies/lib/main/*") + s.Add("dependencies/quarkus/*") + + return s } diff --git a/pkg/trait/jvm_test.go b/pkg/trait/jvm_test.go index dff67c63ed..0eea06c2f7 100644 --- a/pkg/trait/jvm_test.go +++ b/pkg/trait/jvm_test.go @@ -493,7 +493,7 @@ func TestApplyJvmTraitWithClasspathAndExistingContainerCPArg(t *testing.T) { "-cp", "my-precious-lib.jar", "-cp", - fmt.Sprintf("my-precious-lib.jar:./resources:%s:%s:%s:%s:%s:dependencies/*", + fmt.Sprintf("./resources:%s:%s:%s:%s:%s:dependencies/*:my-precious-lib.jar", rdMountPath, cmrMountPath, scrMountPath, "/path/to/another/dep.jar", "/path/to/my-dep.jar"), "io.quarkus.bootstrap.runner.QuarkusEntryPoint", diff --git a/pkg/trait/quarkus.go b/pkg/trait/quarkus.go index 748edaac5c..b2193b1ec0 100644 --- a/pkg/trait/quarkus.go +++ b/pkg/trait/quarkus.go @@ -142,6 +142,15 @@ func (t *quarkusTrait) Matches(trait Trait) bool { func (t *quarkusTrait) Configure(e *Environment) (bool, *TraitCondition, error) { condition := t.adaptDeprecatedFields() + if t.containsMode(traitv1.NativeQuarkusMode) && e.IntegrationInPhase(v1.IntegrationPhaseBuildingKit) { + // Native compilation is only supported for a subset of languages, + // so let's check for compatibility, and fail-fast the Integration, + // to save compute resources and user time. + if err := t.validateNativeSupport(e); err != nil { + return false, nil, err + } + } + return e.IntegrationInPhase(v1.IntegrationPhaseBuildingKit) || e.IntegrationKitInPhase(v1.IntegrationKitPhaseBuildSubmitted) || e.IntegrationKitInPhase(v1.IntegrationKitPhaseReady) && e.IntegrationInRunningPhases(), @@ -167,10 +176,20 @@ func (t *quarkusTrait) adaptDeprecatedFields() *TraitCondition { return nil } +func (t *quarkusTrait) validateNativeSupport(e *Environment) error { + for _, source := range e.Integration.AllSources() { + if language := source.InferLanguage(); !getLanguageSettings(e, language).native { + return fmt.Errorf("invalid native support: Integration %s/%s contains a %s source that cannot be compiled to native executable", + e.Integration.Namespace, e.Integration.Name, language) + } + } + + return nil +} + func (t *quarkusTrait) Apply(e *Environment) error { if e.IntegrationInPhase(v1.IntegrationPhaseBuildingKit) { t.applyWhileBuildingKit(e) - return nil } @@ -190,16 +209,6 @@ func (t *quarkusTrait) Apply(e *Environment) error { } func (t *quarkusTrait) applyWhileBuildingKit(e *Environment) { - if t.containsMode(traitv1.NativeQuarkusMode) { - // Native compilation is only supported for a subset of languages, - // so let's check for compatibility, and fail-fast the Integration, - // to save compute resources and user time. - if !t.validateNativeSupport(e) { - // Let the calling controller handle the Integration update - return - } - } - switch len(t.Modes) { case 0: // Default behavior @@ -224,24 +233,6 @@ func (t *quarkusTrait) applyWhileBuildingKit(e *Environment) { } } -func (t *quarkusTrait) validateNativeSupport(e *Environment) bool { - for _, source := range e.Integration.AllSources() { - if language := source.InferLanguage(); !getLanguageSettings(e, language).native { - t.L.ForIntegration(e.Integration).Infof("Integration %s/%s contains a %s source that cannot be compiled to native executable", e.Integration.Namespace, e.Integration.Name, language) - e.Integration.Status.Phase = v1.IntegrationPhaseError - e.Integration.Status.SetCondition( - v1.IntegrationConditionKitAvailable, - corev1.ConditionFalse, - v1.IntegrationConditionUnsupportedLanguageReason, - fmt.Sprintf("native compilation for language %q is not supported", language)) - - return false - } - } - - return true -} - func (t *quarkusTrait) newIntegrationKit(e *Environment, packageType quarkusPackageType) *v1.IntegrationKit { integration := e.Integration kit := v1.NewIntegrationKit(integration.GetIntegrationKitNamespace(e.Platform), fmt.Sprintf("kit-%s", xid.New())) diff --git a/pkg/trait/quarkus_test.go b/pkg/trait/quarkus_test.go index 6c0ae919d5..107eecfaa0 100644 --- a/pkg/trait/quarkus_test.go +++ b/pkg/trait/quarkus_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/apache/camel-k/v2/pkg/util/boolean" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" @@ -55,6 +56,21 @@ func TestConfigureQuarkusTraitBuildSubmitted(t *testing.T) { assert.Len(t, packageTask.Steps, 4) } +func TestConfigureQuarkusTraitNativeNotSupported(t *testing.T) { + quarkusTrait, environment := createNominalQuarkusTest() + // Set a source not supporting Quarkus native + environment.Integration.Spec.Sources[0].Language = v1.LanguageJavaScript + environment.Integration.Status.Phase = v1.IntegrationPhaseBuildingKit + quarkusTrait.Modes = []traitv1.QuarkusMode{traitv1.NativeQuarkusMode} + + configured, condition, err := quarkusTrait.Configure(environment) + + assert.False(t, configured) + require.Error(t, err) + assert.Equal(t, "invalid native support: Integration default/my-it contains a js source that cannot be compiled to native executable", err.Error()) + assert.Nil(t, condition) +} + func TestApplyQuarkusTraitDefaultKitLayout(t *testing.T) { quarkusTrait, environment := createNominalQuarkusTest() environment.Integration.Status.Phase = v1.IntegrationPhaseBuildingKit @@ -96,6 +112,10 @@ func createNominalQuarkusTest() (*quarkusTrait, *Environment) { Catalog: NewCatalog(client), CamelCatalog: &camel.RuntimeCatalog{}, Integration: &v1.Integration{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "my-it", + }, Spec: v1.IntegrationSpec{ Sources: []v1.SourceSpec{ { diff --git a/pkg/trait/service_test.go b/pkg/trait/service_test.go index 58063df29c..b61149115f 100644 --- a/pkg/trait/service_test.go +++ b/pkg/trait/service_test.go @@ -431,9 +431,10 @@ func TestServiceWithKnativeServiceEnabled(t *testing.T) { require.NoError(t, err) environment := Environment{ - CamelCatalog: catalog, - Catalog: traitCatalog, - Client: client, + CamelCatalog: catalog, + Catalog: traitCatalog, + Client: client, + IntegrationKit: &v1.IntegrationKit{}, Integration: &v1.Integration{ ObjectMeta: metav1.ObjectMeta{ Name: ServiceTestName, @@ -523,9 +524,10 @@ func TestServicesWithKnativeProfile(t *testing.T) { require.NoError(t, err) environment := Environment{ - CamelCatalog: catalog, - Catalog: traitCatalog, - Client: client, + CamelCatalog: catalog, + Catalog: traitCatalog, + Client: client, + IntegrationKit: &v1.IntegrationKit{}, Integration: &v1.Integration{ ObjectMeta: metav1.ObjectMeta{ Name: ServiceTestName, @@ -603,9 +605,10 @@ func TestServiceWithKnativeServiceDisabledInIntegrationPlatform(t *testing.T) { require.NoError(t, err) environment := Environment{ - CamelCatalog: catalog, - Catalog: traitCatalog, - Client: client, + CamelCatalog: catalog, + Catalog: traitCatalog, + Client: client, + IntegrationKit: &v1.IntegrationKit{}, Integration: &v1.Integration{ ObjectMeta: metav1.ObjectMeta{ Name: ServiceTestName,