From 9008e789474d98741dc2f424b51c646e6967f2af Mon Sep 17 00:00:00 2001 From: Pasquale Congiusti Date: Sat, 14 Dec 2024 14:56:16 +0100 Subject: [PATCH] feat(cmd): kamel promote --export-gitops-dir Introduced a `--export-gitops-dir` flag that will create a simple opinionated Kustomize based GitOps overlay directory. The result can be used to be stored in Git and be used as a source for any GitOps pipeline. ``` kamel promote my-it --to my-dest-namespace --export-gitops-dir /path/to/export/dir ``` Closes #5456 --- e2e/common/cli/promote_test.go | 65 ++++++++ pkg/cmd/promote.go | 266 +++++++++++++++++++++++++++++- pkg/cmd/promote_test.go | 288 +++++++++++++++++++++++++++++++-- 3 files changed, 603 insertions(+), 16 deletions(-) create mode 100644 e2e/common/cli/promote_test.go diff --git a/e2e/common/cli/promote_test.go b/e2e/common/cli/promote_test.go new file mode 100644 index 0000000000..a8662da789 --- /dev/null +++ b/e2e/common/cli/promote_test.go @@ -0,0 +1,65 @@ +//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 common + +import ( + "context" + "os" + "testing" + + corev1 "k8s.io/api/core/v1" + + . "github.com/onsi/gomega" + + . "github.com/apache/camel-k/v2/e2e/support" + v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" +) + +func TestKamelPromoteGitOps(t *testing.T) { + t.Parallel() + WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { + t.Run("build and run gitops", func(t *testing.T) { + g.Expect(KamelRun(t, ctx, ns, "files/yaml.yaml").Execute()).To(Succeed()) + g.Eventually(IntegrationConditionStatus(t, ctx, ns, "yaml", v1.IntegrationConditionReady)). + Should(Equal(corev1.ConditionTrue)) + g.Eventually(IntegrationPodPhase(t, ctx, ns, "yaml"), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + g.Eventually(IntegrationLogs(t, ctx, ns, "yaml"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + }) + WithNewTestNamespace(t, func(ctx context.Context, g *WithT, nsTarget string) { + // Export to GitOps directory structure + tmpDir, err := os.MkdirTemp("", "ck-promote-it-*") + if err != nil { + t.Error(err) + } + g.Expect(Kamel(t, ctx, "promote", "yaml", "-n", ns, "--to", nsTarget, "--export-gitops-dir", tmpDir).Execute()).To(Succeed()) + // Run the exported Integration as it would be any CICD + ExpectExecSucceed(t, g, Kubectl("apply", "-k", tmpDir+"/yaml/overlays/"+nsTarget)) + g.Eventually(IntegrationPodPhase(t, ctx, nsTarget, "yaml"), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + g.Eventually(IntegrationConditionStatus(t, ctx, nsTarget, "yaml", v1.IntegrationConditionReady), TestTimeoutShort). + Should(Equal(corev1.ConditionTrue)) + g.Eventually(IntegrationLogs(t, ctx, nsTarget, "yaml"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + // Make sure that no IntegrationKit was ever built for this Integration + g.Eventually(IntegrationKit(t, ctx, nsTarget, "yaml")).Should(Equal("")) + }) + }) +} diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go index 258569a726..270ce84405 100644 --- a/pkg/cmd/promote.go +++ b/pkg/cmd/promote.go @@ -21,12 +21,15 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" "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/io" "github.com/apache/camel-k/v2/pkg/util/kubernetes" "github.com/apache/camel-k/v2/pkg/util/sets" "github.com/spf13/cobra" @@ -54,6 +57,7 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO cmd.Flags().StringP("to-operator", "x", "", "The operator id which will reconcile the promoted Integration/Pipe") cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml") cmd.Flags().BoolP("image", "i", false, "Output the container image only") + cmd.Flags().String("export-gitops-dir", "", "Export to a Kustomize GitOps overlay structure") return &cmd, &options } @@ -64,6 +68,7 @@ type promoteCmdOptions struct { ToOperator string `mapstructure:"to-operator" yaml:",omitempty"` OutputFormat string `mapstructure:"output" yaml:",omitempty"` Image bool `mapstructure:"image" yaml:",omitempty"` + ToGitOpsDir string `mapstructure:"export-gitops-dir" yaml:",omitempty"` } func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error { @@ -74,7 +79,8 @@ func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error { return errors.New("promote requires a destination namespace as --to argument") } if o.To == o.Namespace { - return errors.New("source and destination namespaces must be different in order to avoid promoted Integration/Pipe clashes with the source Integration/Pipe") + return errors.New("source and destination namespaces must be different in order to avoid promoted Integration/Pipe " + + "clashes with the source Integration/Pipe") } return nil } @@ -142,6 +148,14 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { if o.OutputFormat != "" { return showPipeOutput(cmd, destPipe, o.OutputFormat, c.GetScheme()) } + if o.ToGitOpsDir != "" { + err = appendKustomizePipe(destPipe, o.ToGitOpsDir) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), `Exported a Kustomize based Gitops directory to `+o.ToGitOpsDir+` for "`+name+`" Pipe`) + return nil + } // Ensure the destination namespace has access to the source namespace images err = addSystemPullerRoleBinding(o.Context, c, sourceIntegration.Namespace, destPipe.Namespace) if err != nil { @@ -164,6 +178,14 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { if o.OutputFormat != "" { return showIntegrationOutput(cmd, destIntegration, o.OutputFormat) } + if o.ToGitOpsDir != "" { + err = appendKustomizeIntegration(destIntegration, o.ToGitOpsDir) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), `Exported a Kustomize based Gitops directory to `+o.ToGitOpsDir+` for "`+name+`" Integration`) + return nil + } // Ensure the destination namespace has access to the source namespace images err = addSystemPullerRoleBinding(o.Context, c, sourceIntegration.Namespace, destIntegration.Namespace) if err != nil { @@ -421,3 +443,245 @@ func addSystemPullerRoleBinding(ctx context.Context, c client.Client, sourceNS s func showImageOnly(cmd *cobra.Command, integration *v1.Integration) { fmt.Fprintln(cmd.OutOrStdout(), integration.Status.Image) } + +const kustomizationContent = `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +` + +// appendKustomizeIntegration creates a Kustomize GitOps based directory structure for the chosen Integration. +func appendKustomizeIntegration(dstIt *v1.Integration, destinationDir string) error { + namespaceDest := dstIt.Namespace + if _, err := os.Stat(destinationDir); err != nil { + return err + } + + baseIt := dstIt.DeepCopy() + baseIt.Namespace = "" + if baseIt.Annotations != nil { + delete(baseIt.Annotations, v1.OperatorIDAnnotation) + } + appFolderName := strings.ToLower(baseIt.Name) + + newpath := filepath.Join(destinationDir, appFolderName, "routes") + err := os.MkdirAll(newpath, io.FilePerm755) + if err != nil { + return err + } + for _, src := range baseIt.OriginalSourcesOnly() { + srcName := filepath.Join(newpath, src.Name) + cnt := []byte(src.Content) + if err := os.WriteFile(srcName, cnt, io.FilePerm755); err != nil { + return err + } + } + + newpath = filepath.Join(destinationDir, appFolderName, "base") + err = os.MkdirAll(newpath, io.FilePerm755) + if err != nil { + return err + } + marshalledIt, err := kubernetes.ToYAML(baseIt) + if err != nil { + return err + } + filename := "integration.yaml" + itName := filepath.Join(newpath, filename) + if err := os.WriteFile(itName, marshalledIt, io.FilePerm755); err != nil { + return err + } + baseKustCnt := kustomizationContent + `- ` + filename + kustName := filepath.Join(newpath, "kustomization.yaml") + if err := os.WriteFile(kustName, []byte(baseKustCnt), io.FilePerm755); err != nil { + return err + } + + newpath = filepath.Join(destinationDir, appFolderName, "overlays", namespaceDest) + err = os.MkdirAll(newpath, io.FilePerm755) + if err != nil { + return err + } + patchName := "patch-integration.yaml" + patchedIt := getIntegrationPatch(baseIt) + marshalledPatchIt, err := kubernetes.ToYAML(patchedIt) + if err != nil { + return err + } + patchFileName := filepath.Join(newpath, patchName) + if err := os.WriteFile(patchFileName, marshalledPatchIt, io.FilePerm755); err != nil { + return err + } + nsKustCnt := kustomizationContent + `- ../../base` + nsKustCnt += ` +namespace: ` + namespaceDest + ` +patches: +- path: patch-integration.yaml +` + kustName = filepath.Join(newpath, "kustomization.yaml") + if err := os.WriteFile(kustName, []byte(nsKustCnt), io.FilePerm755); err != nil { + return err + } + + return err +} + +// getIntegrationPatch will filter those traits/configuration we want to include in the Integration patch. +func getIntegrationPatch(baseIt *v1.Integration) *v1.Integration { + patchedTraits := v1.Traits{} + baseTraits := baseIt.Spec.Traits + if baseTraits.Affinity != nil { + patchedTraits.Affinity = baseIt.Spec.Traits.Affinity + } + if baseTraits.Camel != nil && baseTraits.Camel.Properties != nil { + patchedTraits.Camel = &traitv1.CamelTrait{ + Properties: baseTraits.Camel.Properties, + } + } + if baseTraits.Container != nil && (baseTraits.Container.RequestCPU != "" || baseTraits.Container.RequestMemory != "" || + baseTraits.Container.LimitCPU != "" || baseTraits.Container.LimitMemory != "") { + patchedTraits.Container = &traitv1.ContainerTrait{ + RequestCPU: baseTraits.Container.RequestCPU, + RequestMemory: baseTraits.Container.RequestMemory, + LimitCPU: baseTraits.Container.LimitCPU, + LimitMemory: baseTraits.Container.LimitMemory, + } + } + if baseTraits.Environment != nil && baseTraits.Environment.Vars != nil { + patchedTraits.Environment = &traitv1.EnvironmentTrait{ + Vars: baseTraits.Environment.Vars, + } + } + if baseTraits.JVM != nil && baseTraits.JVM.Options != nil { + patchedTraits.JVM = &traitv1.JVMTrait{ + Options: baseTraits.JVM.Options, + } + } + if baseTraits.Mount != nil && (baseTraits.Mount.Configs != nil || baseTraits.Mount.Resources != nil || + baseTraits.Mount.Volumes != nil || baseTraits.Mount.EmptyDirs != nil) { + patchedTraits.Mount = &traitv1.MountTrait{ + Configs: baseTraits.Mount.Configs, + Resources: baseTraits.Mount.Resources, + Volumes: baseTraits.Mount.Volumes, + EmptyDirs: baseTraits.Mount.EmptyDirs, + } + } + if baseTraits.Toleration != nil { + patchedTraits.Toleration = baseIt.Spec.Traits.Toleration + } + + patchedIt := v1.NewIntegration("", baseIt.Name) + patchedIt.Spec = v1.IntegrationSpec{ + Traits: patchedTraits, + } + + return &patchedIt +} + +// appendKustomizePipe creates a Kustomize GitOps based directory structure for the chosen Pipe. +func appendKustomizePipe(dstPipe *v1.Pipe, destinationDir string) error { + namespaceDest := dstPipe.Namespace + if _, err := os.Stat(destinationDir); err != nil { + return err + } + + basePipe := dstPipe.DeepCopy() + basePipe.Namespace = "" + if basePipe.Annotations != nil { + delete(basePipe.Annotations, v1.OperatorIDAnnotation) + } + appFolderName := strings.ToLower(basePipe.Name) + + newpath := filepath.Join(destinationDir, appFolderName, "base") + err := os.MkdirAll(newpath, io.FilePerm755) + if err != nil { + return err + } + marshalledPipe, err := kubernetes.ToYAML(basePipe) + if err != nil { + return err + } + filename := "pipe.yaml" + itName := filepath.Join(newpath, filename) + if err := os.WriteFile(itName, marshalledPipe, io.FilePerm755); err != nil { + return err + } + baseKustCnt := kustomizationContent + `- ` + filename + kustName := filepath.Join(newpath, "kustomization.yaml") + if err := os.WriteFile(kustName, []byte(baseKustCnt), io.FilePerm755); err != nil { + return err + } + + newpath = filepath.Join(destinationDir, appFolderName, "overlays", namespaceDest) + err = os.MkdirAll(newpath, io.FilePerm755) + if err != nil { + return err + } + patchName := "patch-pipe.yaml" + patchedPipe := getPipePatch(basePipe) + marshalledPatchPipe, err := kubernetes.ToYAML(patchedPipe) + if err != nil { + return err + } + patchFileName := filepath.Join(newpath, patchName) + if err := os.WriteFile(patchFileName, marshalledPatchPipe, io.FilePerm755); err != nil { + return err + } + nsKustCnt := kustomizationContent + `- ../../base` + nsKustCnt += ` +namespace: ` + namespaceDest + ` +patches: +- path: patch-pipe.yaml +` + kustName = filepath.Join(newpath, "kustomization.yaml") + if err := os.WriteFile(kustName, []byte(nsKustCnt), io.FilePerm755); err != nil { + return err + } + + return err +} + +// getPipePatch will filter those traits/configuration we want to include in the Pipe patch. +func getPipePatch(basePipe *v1.Pipe) *v1.Pipe { + patchedPipe := v1.NewPipe("", basePipe.Name) + patchedPipe.Annotations = basePipe.Annotations + // Only keep those traits we want to include in the patch + for kAnn := range basePipe.Annotations { + if strings.HasPrefix(kAnn, v1.TraitAnnotationPrefix) { + if !isPipeTraitPatch(kAnn) { + delete(basePipe.Annotations, kAnn) + } + } + } + return &patchedPipe +} + +// isPipeTraitPatch returns true if it belongs to the list of the opinionated traits we want to keep in the patch. +func isPipeTraitPatch(keyAnnotation string) bool { + if strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"affinity") { + return true + } + if keyAnnotation == v1.TraitAnnotationPrefix+"camel.properties" { + return true + } + if strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"container.request") || + strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"container.limit") { + return true + } + if keyAnnotation == v1.TraitAnnotationPrefix+"environment.vars" { + return true + } + if keyAnnotation == v1.TraitAnnotationPrefix+"jvm.options" { + return true + } + if strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"mount.configs") || + strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"mount.resources") || + strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"mount.volumes") || + strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"mount.empty-dirs") { + return true + } + if strings.HasPrefix(keyAnnotation, v1.TraitAnnotationPrefix+"toleration") { + return true + } + + return false +} diff --git a/pkg/cmd/promote_test.go b/pkg/cmd/promote_test.go index 03a942f994..ef163b7316 100644 --- a/pkg/cmd/promote_test.go +++ b/pkg/cmd/promote_test.go @@ -19,6 +19,8 @@ package cmd import ( "fmt" + "os" + "path/filepath" "testing" v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" @@ -142,13 +144,13 @@ func TestPipeDryRun(t *testing.T) { dstPlatform.Status.Version = defaults.Version dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady - defaultKB := nominalPipe("my-kb-test") - defaultIntegration, defaultKit := nominalIntegration("my-kb-test") + defaultKB := nominalPipe("my-pipe-test") + defaultIntegration, defaultKit := nominalIntegration("my-pipe-test") srcCatalog := createTestCamelCatalog(srcPlatform) dstCatalog := createTestCamelCatalog(dstPlatform) promoteCmdOptions, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultKB, &defaultIntegration, &defaultKit, &srcCatalog, &dstCatalog) - output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-kb-test", "--to", "prod-namespace", "-o", "yaml", "-n", "default") + output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-pipe-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 @@ -159,7 +161,7 @@ metadata: 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 + name: my-pipe-test namespace: prod-namespace spec: sink: {} @@ -235,7 +237,7 @@ func TestPipeWithMetadataDryRun(t *testing.T) { dstPlatform.Status.Version = defaults.Version dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady - defaultKB := nominalPipe("my-kb-test") + defaultKB := nominalPipe("my-pipe-test") defaultKB.Annotations = map[string]string{ "camel.apache.org/operator.id": "camel-k", "my-annotation": "my-value", @@ -243,12 +245,12 @@ func TestPipeWithMetadataDryRun(t *testing.T) { defaultKB.Labels = map[string]string{ "my-label": "my-value", } - defaultIntegration, defaultKit := nominalIntegration("my-kb-test") + defaultIntegration, defaultKit := nominalIntegration("my-pipe-test") srcCatalog := createTestCamelCatalog(srcPlatform) dstCatalog := createTestCamelCatalog(dstPlatform) promoteCmdOptions, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultKB, &defaultIntegration, &defaultKit, &srcCatalog, &dstCatalog) - output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-kb-test", "--to", "prod-namespace", "-o", "yaml", "-n", "default") + output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-pipe-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 @@ -262,7 +264,7 @@ metadata: creationTimestamp: null labels: my-label: my-value - name: my-kb-test + name: my-pipe-test namespace: prod-namespace spec: sink: {} @@ -299,13 +301,13 @@ func TestPipeImageOnly(t *testing.T) { dstPlatform.Status.Version = defaults.Version dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady - defaultKB := nominalPipe("my-kb-test") - defaultIntegration, defaultKit := nominalIntegration("my-kb-test") + defaultKB := nominalPipe("my-pipe-test") + defaultIntegration, defaultKit := nominalIntegration("my-pipe-test") srcCatalog := createTestCamelCatalog(srcPlatform) dstCatalog := createTestCamelCatalog(dstPlatform) _, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultKB, &defaultIntegration, &defaultKit, &srcCatalog, &dstCatalog) - output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-kb-test", "--to", "prod-namespace", "-i", "-n", "default") + output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-pipe-test", "--to", "prod-namespace", "-i", "-n", "default") require.NoError(t, err) assert.Equal(t, "my-special-image\n", output) } @@ -427,7 +429,7 @@ func TestPipeWithSavedTraitsDryRun(t *testing.T) { dstPlatform.Status.Version = defaults.Version dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady - defaultKB := nominalPipe("my-kb-test") + defaultKB := nominalPipe("my-pipe-test") defaultKB.Annotations = map[string]string{ "camel.apache.org/operator.id": "camel-k", "my-annotation": "my-value", @@ -435,12 +437,12 @@ func TestPipeWithSavedTraitsDryRun(t *testing.T) { defaultKB.Labels = map[string]string{ "my-label": "my-value", } - defaultIntegration, defaultKit := nominalIntegration("my-kb-test") + defaultIntegration, defaultKit := nominalIntegration("my-pipe-test") srcCatalog := createTestCamelCatalog(srcPlatform) dstCatalog := createTestCamelCatalog(dstPlatform) promoteCmdOptions, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultKB, &defaultIntegration, &defaultKit, &srcCatalog, &dstCatalog) - output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-kb-test", "--to", "prod-namespace", "-o", "yaml", "-n", "default") + output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-pipe-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 @@ -454,7 +456,7 @@ metadata: creationTimestamp: null labels: my-label: my-value - name: my-kb-test + name: my-pipe-test namespace: prod-namespace spec: sink: {} @@ -462,3 +464,259 @@ spec: status: {} `, output) } + +const expectedGitOpsIt = `apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + creationTimestamp: null + name: my-it-test +spec: + traits: + affinity: + nodeAffinityLabels: + - my-node + camel: + properties: + - my.property=val + runtimeVersion: 1.2.3 + container: + image: my-special-image + imagePullPolicy: Always + limitCPU: "1" + limitMemory: 1024Mi + port: 2000 + requestCPU: "0.5" + requestMemory: 512Mi + environment: + vars: + - MY_VAR=val + jvm: + classpath: /path/to/artifact-1/*:/path/to/artifact-2/* + jar: my.jar + options: + - -XMX 123 + mount: + configs: + - configmap:my-cm + - secret:my-sec + service: + annotations: + my-annotation: "123" + auto: false + enabled: true + toleration: + taints: + - taint1:true +status: {} +` + +const expectedGitOpsItPatch = `apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + creationTimestamp: null + name: my-it-test +spec: + traits: + affinity: + nodeAffinityLabels: + - my-node + camel: + properties: + - my.property=val + container: + limitCPU: "1" + limitMemory: 1024Mi + requestCPU: "0.5" + requestMemory: 512Mi + environment: + vars: + - MY_VAR=val + jvm: + options: + - -XMX 123 + mount: + configs: + - configmap:my-cm + - secret:my-sec + toleration: + taints: + - taint1:true +status: {} +` + +func TestIntegrationGitOps(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") + defaultIntegration.Status.Traits = &v1.Traits{ + Affinity: &trait.AffinityTrait{ + NodeAffinityLabels: []string{"my-node"}, + }, + Camel: &trait.CamelTrait{ + Properties: []string{"my.property=val"}, + }, + Container: &trait.ContainerTrait{ + LimitCPU: "1", + LimitMemory: "1024Mi", + RequestCPU: "0.5", + RequestMemory: "512Mi", + Port: 2000, + ImagePullPolicy: corev1.PullAlways, + }, + Environment: &trait.EnvironmentTrait{ + Vars: []string{"MY_VAR=val"}, + }, + JVM: &trait.JVMTrait{ + Jar: "my.jar", + Options: []string{"-XMX 123"}, + }, + Mount: &trait.MountTrait{ + Configs: []string{"configmap:my-cm", "secret:my-sec"}, + }, + Service: &trait.ServiceTrait{ + Trait: trait.Trait{ + Enabled: ptr.To(true), + }, + Auto: ptr.To(false), + Annotations: map[string]string{ + "my-annotation": "123", + }, + }, + Toleration: &trait.TolerationTrait{ + Taints: []string{"taint1:true"}, + }, + } + srcCatalog := createTestCamelCatalog(srcPlatform) + dstCatalog := createTestCamelCatalog(dstPlatform) + + tmpDir, err := os.MkdirTemp("", "ck-promote-it-*") + if err != nil { + t.Error(err) + } + + _, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultIntegration, &defaultKit, &srcCatalog, &dstCatalog) + output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-it-test", "--to", "prod-namespace", "--export-gitops-dir", tmpDir, "-n", "default") + require.NoError(t, err) + assert.Contains(t, output, `Exported a Kustomize based Gitops directory`) + + baseIt, err := os.ReadFile(filepath.Join(tmpDir, "my-it-test", "base", "integration.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedGitOpsIt, string(baseIt)) + + patchIt, err := os.ReadFile(filepath.Join(tmpDir, "my-it-test", "overlays", "prod-namespace", "patch-integration.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedGitOpsItPatch, string(patchIt)) +} + +const expectedGitOpsPipe = `apiVersion: camel.apache.org/v1 +kind: Pipe +metadata: + annotations: + my-annotation: my-value + trait.camel.apache.org/affinity.node-affinity-labels: '[node1,node2]' + trait.camel.apache.org/camel.properties: '[a=1]' + trait.camel.apache.org/camel.runtime-version: 1.2.3 + trait.camel.apache.org/container.image: my-special-image + trait.camel.apache.org/container.image-pull-policy: Always + trait.camel.apache.org/container.limit-cpu: "2" + trait.camel.apache.org/container.limit-memory: 1024Mi + trait.camel.apache.org/container.request-cpu: "1" + trait.camel.apache.org/container.request-memory: 2048Mi + trait.camel.apache.org/environment.vars: '[MYVAR=1]' + trait.camel.apache.org/jvm.classpath: /path/to/artifact-1/*:/path/to/artifact-2/* + trait.camel.apache.org/jvm.jar: my.jar + trait.camel.apache.org/jvm.options: '[-XMX 123]' + trait.camel.apache.org/mount.resources: '[configmap:my-cm,secret:my-sec/my-key@/tmp/file.txt]' + trait.camel.apache.org/service.auto: "false" + trait.camel.apache.org/toleration.taints: '[mytaints:true]' + creationTimestamp: null + labels: + my-label: my-value + name: my-pipe-test +spec: + sink: {} + source: {} +status: {} +` + +const expectedGitOpsPipePatch = `apiVersion: camel.apache.org/v1 +kind: Pipe +metadata: + annotations: + my-annotation: my-value + trait.camel.apache.org/affinity.node-affinity-labels: '[node1,node2]' + trait.camel.apache.org/camel.properties: '[a=1]' + trait.camel.apache.org/container.limit-cpu: "2" + trait.camel.apache.org/container.limit-memory: 1024Mi + trait.camel.apache.org/container.request-cpu: "1" + trait.camel.apache.org/container.request-memory: 2048Mi + trait.camel.apache.org/environment.vars: '[MYVAR=1]' + trait.camel.apache.org/jvm.options: '[-XMX 123]' + trait.camel.apache.org/mount.resources: '[configmap:my-cm,secret:my-sec/my-key@/tmp/file.txt]' + trait.camel.apache.org/toleration.taints: '[mytaints:true]' + creationTimestamp: null + name: my-pipe-test +spec: + sink: {} + source: {} +status: {} +` + +func TestPipeGitOps(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 + defaultKB := nominalPipe("my-pipe-test") + defaultKB.Annotations = map[string]string{ + "camel.apache.org/operator.id": "camel-k", + "my-annotation": "my-value", + v1.TraitAnnotationPrefix + "affinity.node-affinity-labels": "[node1,node2]", + v1.TraitAnnotationPrefix + "camel.properties": "[a=1]", + v1.TraitAnnotationPrefix + "container.limit-cpu": "2", + v1.TraitAnnotationPrefix + "container.limit-memory": "1024Mi", + v1.TraitAnnotationPrefix + "container.request-cpu": "1", + v1.TraitAnnotationPrefix + "container.request-memory": "2048Mi", + v1.TraitAnnotationPrefix + "container.image-pull-policy": "Always", + v1.TraitAnnotationPrefix + "environment.vars": "[MYVAR=1]", + v1.TraitAnnotationPrefix + "jvm.options": "[-XMX 123]", + v1.TraitAnnotationPrefix + "jvm.jar": "my.jar", + v1.TraitAnnotationPrefix + "mount.resources": "[configmap:my-cm,secret:my-sec/my-key@/tmp/file.txt]", + v1.TraitAnnotationPrefix + "service.auto": "false", + v1.TraitAnnotationPrefix + "toleration.taints": "[mytaints:true]", + } + defaultKB.Labels = map[string]string{ + "my-label": "my-value", + } + defaultIntegration, defaultKit := nominalIntegration("my-pipe-test") + srcCatalog := createTestCamelCatalog(srcPlatform) + dstCatalog := createTestCamelCatalog(dstPlatform) + + tmpDir, err := os.MkdirTemp("", "ck-promote-pipe-*") + if err != nil { + t.Error(err) + } + + _, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultKB, &defaultIntegration, &defaultKit, &srcCatalog, &dstCatalog) + output, err := ExecuteCommand(promoteCmd, cmdPromote, "my-pipe-test", "--to", "prod-namespace", "--export-gitops-dir", tmpDir, "-n", "default") + require.NoError(t, err) + assert.Contains(t, output, `Exported a Kustomize based Gitops directory`) + + baseIt, err := os.ReadFile(filepath.Join(tmpDir, "my-pipe-test", "base", "pipe.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedGitOpsPipe, string(baseIt)) + + patchPipe, err := os.ReadFile(filepath.Join(tmpDir, "my-pipe-test", "overlays", "prod-namespace", "patch-pipe.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedGitOpsPipePatch, string(patchPipe)) +}