diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 0231efd01d..8e6418947c 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -23,9 +23,10 @@ * xref:running/running.adoc[Run an Integration] ** xref:running/running-cli.adoc[kamel run CLI] ** xref:running/build-from-git.adoc[Git hosted Integrations] +** xref:running/gitops.adoc[GitOps] ** xref:running/self-managed.adoc[Self managed Integrations] ** xref:running/synthetic.adoc[Synthetic Integrations] -** xref:running/promoting.adoc[Promote an Integration] +** xref:running/promoting.adoc[kamel promote CLI] ** xref:running/dry-build.adoc[Dry build] * xref:pipes/pipes.adoc[Run an Pipe] ** xref:pipes/bind-cli.adoc[kamel bind CLI] @@ -54,6 +55,7 @@ ** xref:traits:deployer.adoc[Deployer] ** xref:traits:deployment.adoc[Deployment] ** xref:traits:environment.adoc[Environment] +** xref:traits:gitops.adoc[Gitops] ** xref:traits:health.adoc[Health] ** xref:traits:ingress.adoc[Ingress] ** xref:traits:init-containers.adoc[Init Containers] diff --git a/docs/modules/ROOT/pages/running/build-from-git.adoc b/docs/modules/ROOT/pages/running/build-from-git.adoc index 25d043c411..902388eca2 100644 --- a/docs/modules/ROOT/pages/running/build-from-git.adoc +++ b/docs/modules/ROOT/pages/running/build-from-git.adoc @@ -60,18 +60,14 @@ metadata: spec: git: url: https://github.com/michalvavrik/sample.git - branch: feature/xyz # Use specific branch + # branch: feature/xyz # Use specific branch # tag: v1.2.3 # Or use specific tag # commit: f2b9bd064a62263ab53b3bfe6ac2b71e68dba45b # Or use specific commit ``` == Rebuild -In order to trigger a rebuild of an Integration you will need to `kamel reset` or to wipe off the Integration `status` as it normally happens for any other regular Integration. - -== GitOps - -The possibility to build directly from the Git repo will give you more flexibility to close the loop between your CICD which is taking care to build an application and to deploy it. +In order to trigger a rebuild of an Integration you will need to `kamel rebuild` or to wipe off the Integration `status` as it normally happens for any other regular Integration. == Future developments diff --git a/docs/modules/ROOT/pages/running/gitops.adoc b/docs/modules/ROOT/pages/running/gitops.adoc new file mode 100644 index 0000000000..1dd75b93f6 --- /dev/null +++ b/docs/modules/ROOT/pages/running/gitops.adoc @@ -0,0 +1,200 @@ +[[gitops]] += Camel GitOps + +Once your build is complete, you can configure the operator to run an opinionated GitOps strategy. Camel K has a built-in feature which allow the operator to push a branch on a given Git repository with the latest Integration candidate release built. In order to set the context, this would be the scenario: + +1. The dev operator builds the application from Git source +2. The dev operator push the container image +3. The operator creates a branch into a Git repo with the Integration custom resource pinned with the container image just built +4. (Optional) there could be a gateway such as a Pull Request to control the changes pushed are good to go +5. A CICD tool (eg, ArgoCD) will be watching the repo and be notified when a change Integration is ready for a given environment (eg, production) +6. The production operator will immediately start the Integration as it was a self managed Integration (it directly holds the container image) which does not require any build + +The feature is based on Kustomize and can be entirely configured via `gitops` trait. + +NOTE: the work described here is influenced by https://developers.redhat.com/e-books/path-gitops["The Path to GitOps"] book. + +== GitOps overlays + +The operator can create a Kustomize based overlay structure in order to simplify the creation of a **GitOps based deployment** process. Let's pretend we want to create a GitOps pipeline for two environments, as an example, *staging* and *production*. We need to configure the trait with the following configuration: + +```yaml +apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + name: sample +spec: +... + traits: + camel: + properties: + - my-env=dev + container: + requestMemory: 256Mi + gitops: + url: https://github.com/my-org/my-camel-apps.git + secret: my-gh-token + branchPush: cicd-listener + overlays: + - staging + - production +``` + +NOTE: There are more options to configure on the `gitops` trait. Feel free to have a look and learn on the trait documentation page directly. + +As soon as the build of the Integration is completed, the operator will prepare the commit with the overlays. The structure would be like the following directory tree: + +``` +/integrations/ +└── sample + ├── base + │   ├── integration.yaml + │   └── kustomization.yaml + ├── overlays + │   ├── production + │   │   ├── kustomization.yaml + │   │   └── patch-integration.yaml + │   └── staging + │   ├── kustomization.yaml + │   └── patch-integration.yaml + └── routes + └── XYZ.java + +``` + +The above structure could be used directly with `kubectl` (eg, `kubectl apply -k /tmp/integrations/sample/overlays/production`) or any CICD capable of running a similar deployment strategy. + +The important thing to notice is that the **base** Integration is adding the container image that we've just built and any other trait which is required for the application to run correctly (and without the need to be rebuilt) on another environment: + +```yaml +apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + name: test +spec: +... + traits: + camel: + runtimeVersion: 3.15.3 + properties: + - my-env=dev + container: + image: 10.110.254.179/camel-k/camel-k-kit-d4taqhk20aus73c0o74g@sha256:9d95d940291be22743c24fe5f2c973752e0e3953989d84936c57d81e6f179914 + requestMemory: 256Mi + jvm: + classpath: dependencies/*:dependencies/app/*:dependencies/lib/boot/*:dependencies/lib/main/*:dependencies/quarkus/* +``` + +What's cool is that each `patch-integration.yaml` (hence, each overlay) can be configured with different trait configuration (for example, resources configuration, replicas, ...). The tool will create a first empty configuration for those traits that are configuring deployment aspects, but won't override any existing overlay, so that you can change them directly in the git repository without the risk the operator to override them. Every new build, it will only change the `container.image` and any other configuration which is not explicitly defined in the overlay. + +For example, your "production" overlay patch may be in this case: + +```yaml +apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + name: test +spec: + traits: + camel: + properties: + - my-env=prod + container: + requestMemory: 2Gi +``` + +At next build, the patch won't change. So, you can safely trust your CICD that will release correctly, just refreshing the container image with the newest candidate release image. + +=== Manual gateway + +As you're pushing the changes on a branch, you may want to use the branch and create Pull Request, Merge Request or any other merging strategy used by the git implementation of your choice. This is a clever way to introduce a gateway and have some approval methodology that has to be reviewed by a human operator. As git does not mandate a standard approach for this feature, you will need to implement a strategy on your own. If you're using GitHub, you may, for example have GitHub Action to automate the creation of a PR each time a new commit happen on a given branch. + +=== Running Camel with ArgoCD + +argo-cd.readthedocs.io[ArgoCD] is one of the most popular CICD choices around. Once you have stored the project in a Git repository, if you're using a CICD technology like ArgoCD you can run your *production* pipeline as: + +``` +argocd app create my-ck-it-prod --repo https://git-server/repo/integrations/sample.git --path overlays/production --dest-server https://kubernetes.default.svc --dest-namespace prod +``` + +From this moment onward any change can be performed on the repository and it will be automatically refreshed by the CICD pipeline accordingly. + +NOTE: any other CICD technology can be adopted using the Git repository as source. + +=== Separated cluster + +The GitOps feature makes it possible to have a physical separated cluster for each environment. You may have a build only cluster only which is the one where the operator performs the build of the Camel applications. Then you have a testing environment where your QA team is validating the application and finally a separated cluster for running production workloads. All automated and in sync without the need to rebuild the application. They have to use the same container registry or you need to adopt some registry synchronization tooling to maintain in sync the repository. + +=== Single repository for multiple Camel applications + +You can have a single repository that will contain all your Integrations. If you have noticed, the Integrations will be added to an `integrations` root directory (you can configure it if you need). This is on purpose, as you may want a single repo with all your Kubernetes resources and some CICD just watching at them. So, the `integrations` will contain all your Integrations built and ready to run. + +NOTE: this is the approach suggested in https://developers.redhat.com/e-books/path-gitops["The Path to GitOps"] book. + +=== Push to the same repository you've used to build + +If you're building application from Git and you want to push the changes back to the same, then, you don't need to configure the Git repository for `gitops` trait. If nothing is specified, the trait will get the configuration from `.spec.git` Integration. This approach may be good when you want to have a single repository containing all aspects of an application. In this case we suggest to use a directory named `ci` or `cicd` as a convention to store your GitOps configuration. + +=== Chain of GitOps environments + +By default, the `gitops` trait will delete the trait configuration when creating the overlays. This is done in order to avoid a circular infinite push loop. In general, you don't need the trait in your overlays, unless you have some more complex GitOps chaining methodology. If you have a cascading GitOps mechanisms, you can include the configuration in the `patch-integration.yaml`. We can imagine a build which trigger a QA execution. And then, the QA execution may trigger a Production overlay execution. You can use the patch in each different step and configure the `gitops` trait accordingly. + +The above description would turn into something like: + +```yaml +apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + name: sample +spec: +... + traits: + camel: + properties: + - my-env=dev +... + gitops: + url: https://github.com/my-org/my-camel-apps.git + secret: my-gh-token + branchPush: cicd-listener-test + overlays: + - testing +``` + +Then, once the operator has built and pushed the change on the GitOps repo, you can configure your "testing" `patch-integration.yaml` overlay adding the `gitops` trait: + +```yaml +apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + name: test +spec: + traits: + camel: + properties: + - my-env=test + gitops: + url: https://github.com/my-org/my-camel-apps.git + secret: my-gh-token + branchPush: cicd-listener-prod + overlays: + - production +``` + +When the "testing" pipeline executes, it will push further a "production" overlay that can be the final stage of your process. With this strategy you can create a chain of pipelines each of them controlling the next step (ideally with some gateway to control the flow). + +=== Predetermined configuration + +The operator will add a patch configuration for any of the following trait configuration found in the source base Integration: + +* Affinity configuration +* Camel properties +* Container resources +* Environment variables +* JVM options +* Mount configuration +* Toleration configuration + +These are the traits that are executed at deployment time. However, you can add any trait you want. Only mind that the "build" traits or in general traits executed in any phase before "Deploy", won't take any effect. + +NOTE: feel free to ask to add any further configuration you require. diff --git a/docs/modules/ROOT/pages/running/promoting.adoc b/docs/modules/ROOT/pages/running/promoting.adoc index 8747d947c5..74c54a4710 100644 --- a/docs/modules/ROOT/pages/running/promoting.adoc +++ b/docs/modules/ROOT/pages/running/promoting.adoc @@ -57,8 +57,6 @@ Please notice that the Integration running in test is not altered in any way and [[traits]] == Moving traits -NOTE: this feature is available starting from version 2.5 - When you use the `promote` subcommand, you're also keeping the status of any configured trait along with the new promoted Integration. The tool is in fact in charge to recover the trait configuration of the source Integration and port it over to the new Integration promoted. This is particularly nice when you have certain traits which are requiring the scan the source code (for instance, Service trait). In this way, when you promote the new Integration, the traits will be automatically configured to copy any parameter, replicating the very exact behavior between the source and destination environment. @@ -66,9 +64,7 @@ This is particularly nice when you have certain traits which are requiring the s With this approach, you won't need to worry any longer about any trait which was requiring the source to be attached in order to automatically scan for features. [[gitops]] -== GitOps - -NOTE: this feature is available starting from version 2.6 +== Make your own GitOps The promote has also the possibility to create a Kustomize based overlay structure in order to simplify the creation of a **GitOps based deployment** process. Let's pretend we want to create a GitOps pipeline for two environments, as an example, *staging* and *production*. For each environment we can call the export command: @@ -120,18 +116,6 @@ The CLI has a predetermined set of configuration (traits) which are typically su The above structure could be used directly with `kubectl` (eg, `kubectl apply -k /tmp/integrations/promote-server/overlays/production`). For this reason it can be used *as is* to feed a Git repository and referenced in any CICD pipeline. -=== Running Camel with ArgoCD - -Once you have stored the project in a Git repository, if you're using a CICD technology like https://argo-cd.readthedocs.io[ArgoCD] you can run immediately your *production* pipeline as: - -``` -argocd app create my-ck-it-prod --repo https://git-server/repo/promote-server.git --path overlays/production --dest-server https://kubernetes.default.svc --dest-namespace prod -``` - -From this moment onward any change can be performed on the repository and it will be automatically refreshed by the CICD pipeline accordingly. - -NOTE: any other CICD technology can be adopted using the Git repository as source. - === Predetermined configuration The CLI will add a patch configuration for any of the following trait configuration found in the source base Integration: diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc index 3c1ed57e39..7bec909807 100644 --- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc +++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc @@ -6006,6 +6006,13 @@ Deprecated: no longer in use. The configuration of GC trait +|`gitops` + +*xref:#_camel_apache_org_v1_trait_GitOpsTrait[GitOpsTrait]* +| + + +The configuration of GitOps trait + |`health` + *xref:#_camel_apache_org_v1_trait_HealthTrait[HealthTrait]* | @@ -7204,6 +7211,110 @@ Discovery client cache to be used, either `disabled`, `disk` or `memory` (defaul Deprecated: no longer in use. +|=== + +[#_camel_apache_org_v1_trait_GitOpsTrait] +=== GitOpsTrait + +*Appears on:* + +* <<#_camel_apache_org_v1_Traits, Traits>> + +The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration built. +If the trait is enabled but no pull configuration is provided, then, the operator will use the values stored in Integration `.spec.git` field used +to pull the project. + + +[cols="2,2a",options="header"] +|=== +|Field +|Description + +|`Trait` + +*xref:#_camel_apache_org_v1_trait_Trait[Trait]* +|(Members of `Trait` are embedded into this type.) + + + + +|`url` + +string +| + + +the URL of the repository where the project is stored. + +|`secret` + +string +| + + +the Kubernetes secret where the Git token is stored. The operator will pick up the first secret key only, whichever the name it is. + +|`branch` + +string +| + + +the git branch to check out. + +|`tag` + +string +| + + +the git tag to check out. + +|`commit` + +string +| + + +the git commit (full SHA) to check out. + +|`branchPush` + +string +| + + +the git branch to push to. If omitted, the operator will push to a new branch named as `cicd/release-candidate-`. + +|`overlays` + +[]string +| + + +a list of overlays to provide (default \{"dev","stag","prod"}). + +|`overwriteOverlay` + +bool +| + + +a flag (default, false) to overwrite any existing overlay. + +|`integrationDirectory` + +string +| + + +The root path where to store Kustomize overlays (default `integrations`). + +|`committerName` + +string +| + + +The name used to commit the GitOps changes (default `Camel K Operator`). + +|`committerEmail` + +string +| + + +The email used to commit the GitOps changes (default `camel-k-operator@apache.org`). + + |=== [#_camel_apache_org_v1_trait_HealthTrait] @@ -9379,6 +9490,7 @@ The list of taints to tolerate, in the form `Key[=Value]:Effect[:Seconds]` * <<#_camel_apache_org_v1_trait_AffinityTrait, AffinityTrait>> * <<#_camel_apache_org_v1_trait_CronTrait, CronTrait>> * <<#_camel_apache_org_v1_trait_GCTrait, GCTrait>> +* <<#_camel_apache_org_v1_trait_GitOpsTrait, GitOpsTrait>> * <<#_camel_apache_org_v1_trait_HealthTrait, HealthTrait>> * <<#_camel_apache_org_v1_trait_IngressTrait, IngressTrait>> * <<#_camel_apache_org_v1_trait_InitContainersTrait, InitContainersTrait>> diff --git a/docs/modules/traits/pages/gitops.adoc b/docs/modules/traits/pages/gitops.adoc new file mode 100644 index 0000000000..5be372e39c --- /dev/null +++ b/docs/modules/traits/pages/gitops.adoc @@ -0,0 +1,78 @@ += Gitops Trait + +// Start of autogenerated code - DO NOT EDIT! (badges) +// End of autogenerated code - DO NOT EDIT! (badges) +// Start of autogenerated code - DO NOT EDIT! (description) +The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration built. +If the trait is enabled but no pull configuration is provided, then, the operator will use the values stored in Integration `.spec.git` field used +to pull the project. + + +This trait is available in the following profiles: **Kubernetes, Knative, OpenShift**. + +// End of autogenerated code - DO NOT EDIT! (description) +// Start of autogenerated code - DO NOT EDIT! (configuration) +== Configuration + +Trait properties can be specified when running any integration with the CLI: +[source,console] +---- +$ kamel run --trait gitops.[key]=[value] --trait gitops.[key2]=[value2] integration.yaml +---- +The following configuration options are available: + +[cols="2m,1m,5a"] +|=== +|Property | Type | Description + +| gitops.enabled +| bool +| Can be used to enable or disable a trait. All traits share this common property. + +| gitops.url +| string +| the URL of the repository where the project is stored. + +| gitops.secret +| string +| the Kubernetes secret where the Git token is stored. The operator will pick up the first secret key only, whichever the name it is. + +| gitops.branch +| string +| the git branch to check out. + +| gitops.tag +| string +| the git tag to check out. + +| gitops.commit +| string +| the git commit (full SHA) to check out. + +| gitops.branch-push +| string +| the git branch to push to. If omitted, the operator will push to a new branch named as `cicd/release-candidate-`. + +| gitops.overlays +| []string +| a list of overlays to provide (default {"dev","stag","prod"}). + +| gitops.overwrite-overlay +| bool +| a flag (default, false) to overwrite any existing overlay. + +| gitops.integration-directory +| string +| The root path where to store Kustomize overlays (default `integrations`). + +| gitops.committed-name +| string +| The name used to commit the GitOps changes (default `Camel K Operator`). + +| gitops.committed-email +| string +| The email used to commit the GitOps changes (default `camel-k-operator@apache.org`). + +|=== + +// End of autogenerated code - DO NOT EDIT! (configuration) diff --git a/e2e/common/git/git_test.go b/e2e/common/traits/git_test.go similarity index 97% rename from e2e/common/git/git_test.go rename to e2e/common/traits/git_test.go index 251e4bf513..fc53d70eff 100644 --- a/e2e/common/git/git_test.go +++ b/e2e/common/traits/git_test.go @@ -37,7 +37,7 @@ import ( func TestGitRepository(t *testing.T) { t.Parallel() WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { - t.Run("Camel Quarkus", func(t *testing.T) { + t.Run("Camel Main from Git", func(t *testing.T) { itName := "sample" g.Expect(KamelRun(t, ctx, ns, "--git", "https://github.com/squakez/sample.git", diff --git a/helm/camel-k/crds/camel-k-crds.yaml b/helm/camel-k/crds/camel-k-crds.yaml index fc406fa56a..ba73dbc5b1 100644 --- a/helm/camel-k/crds/camel-k-crds.yaml +++ b/helm/camel-k/crds/camel-k-crds.yaml @@ -4331,6 +4331,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -6659,6 +6724,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -8889,6 +9019,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -11096,6 +11291,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -20137,6 +20397,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -22293,6 +22618,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -32695,6 +33085,71 @@ spec: All traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the + operator will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes + (default `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes + (default `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. + All traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any + existing overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token + is stored. The operator will pick up the first secret + key only, whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project + is stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -34789,6 +35244,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: diff --git a/pkg/apis/camel/v1/common_types.go b/pkg/apis/camel/v1/common_types.go index f70e0f5b56..335435860c 100644 --- a/pkg/apis/camel/v1/common_types.go +++ b/pkg/apis/camel/v1/common_types.go @@ -208,6 +208,8 @@ type Traits struct { ErrorHandler *trait.ErrorHandlerTrait `json:"error-handler,omitempty" property:"error-handler"` // The configuration of GC trait GC *trait.GCTrait `json:"gc,omitempty" property:"gc"` + // The configuration of GitOps trait + GitOps *trait.GitOpsTrait `json:"gitops,omitempty" property:"gitops"` // The configuration of Health trait Health *trait.HealthTrait `json:"health,omitempty" property:"health"` // The configuration of Ingress trait diff --git a/pkg/apis/camel/v1/trait/gitops.go b/pkg/apis/camel/v1/trait/gitops.go new file mode 100644 index 0000000000..b6d406278d --- /dev/null +++ b/pkg/apis/camel/v1/trait/gitops.go @@ -0,0 +1,55 @@ +/* +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 trait + +// The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration built. +// If the trait is enabled but no pull configuration is provided, then, the operator will use the values stored in Integration `.spec.git` field used +// to pull the project. +// +// +camel-k:trait=gitops. +type GitOpsTrait struct { + Trait `json:",inline" property:",squash"` + + // the URL of the repository where the project is stored. + URL string `json:"url,omitempty" property:"url"` + // the Kubernetes secret where the Git token is stored. The operator will pick up the first secret key only, whichever the name it is. + Secret string `json:"secret,omitempty" property:"secret"` + // the git branch to check out. + Branch string `json:"branch,omitempty" property:"branch"` + // the git tag to check out. + Tag string `json:"tag,omitempty" property:"tag"` + // the git commit (full SHA) to check out. + Commit string `json:"commit,omitempty" property:"commit"` + // the git branch to push to. If omitted, the operator will push to a new branch named as `cicd/release-candidate-`. + BranchPush string `json:"branchPush,omitempty" property:"branch-push"` + // a list of overlays to provide (default {"dev","stag","prod"}). + // +kubebuilder:default={"dev","stag","prod"} + Overlays []string `json:"overlays,omitempty" property:"overlays"` + // a flag (default, false) to overwrite any existing overlay. + // +kubebuilder:default=false + OverwriteOverlay bool `json:"overwriteOverlay,omitempty" property:"overwrite-overlay"` + // The root path where to store Kustomize overlays (default `integrations`). + // +kubebuilder:default="integrations" + IntegrationDirectory string `json:"integrationDirectory,omitempty" property:"integration-directory"` + // The name used to commit the GitOps changes (default `Camel K Operator`). + // +kubebuilder:default="Camel K Operator" + CommiterName string `json:"committerName,omitempty" property:"committed-name"` + // The email used to commit the GitOps changes (default `camel-k-operator@apache.org`). + // +kubebuilder:default="camel-k-operator@apache.org" + CommiterEmail string `json:"committerEmail,omitempty" property:"committed-email"` +} diff --git a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go index 4cbe5128d7..9fe9ae2471 100644 --- a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go @@ -410,6 +410,27 @@ func (in *GCTrait) DeepCopy() *GCTrait { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitOpsTrait) DeepCopyInto(out *GitOpsTrait) { + *out = *in + in.Trait.DeepCopyInto(&out.Trait) + if in.Overlays != nil { + in, out := &in.Overlays, &out.Overlays + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitOpsTrait. +func (in *GitOpsTrait) DeepCopy() *GitOpsTrait { + if in == nil { + return nil + } + out := new(GitOpsTrait) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HealthTrait) DeepCopyInto(out *HealthTrait) { *out = *in diff --git a/pkg/apis/camel/v1/zz_generated.deepcopy.go b/pkg/apis/camel/v1/zz_generated.deepcopy.go index 78ffc53ea2..9c82cc79ca 100644 --- a/pkg/apis/camel/v1/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/zz_generated.deepcopy.go @@ -3228,6 +3228,11 @@ func (in *Traits) DeepCopyInto(out *Traits) { *out = new(trait.GCTrait) (*in).DeepCopyInto(*out) } + if in.GitOps != nil { + in, out := &in.GitOps, &out.GitOps + *out = new(trait.GitOpsTrait) + (*in).DeepCopyInto(*out) + } if in.Health != nil { in, out := &in.Health, &out.Health *out = new(trait.HealthTrait) diff --git a/pkg/builder/git.go b/pkg/builder/git.go index 0ee833501a..fd9fde41df 100644 --- a/pkg/builder/git.go +++ b/pkg/builder/git.go @@ -18,13 +18,9 @@ limitations under the License. package builder import ( - "errors" "path/filepath" - git "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/transport/http" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + util "github.com/apache/camel-k/v2/pkg/util/gitops" ) func init() { @@ -59,69 +55,15 @@ var Git = gitSteps{ } func cloneProject(ctx *builderContext) error { - depth := 1 - if ctx.Build.Git.Commit != "" { - // only the commit checkout requires full git project history - depth = 0 - } - gitCloneOptions := &git.CloneOptions{ - URL: ctx.Build.Git.URL, - Depth: depth, - } - - if ctx.Build.Git.Branch != "" { - if ctx.Build.Git.Tag != "" { - return errors.New("illegal arguments: cannot specify both git branch and tag") - } - if ctx.Build.Git.Commit != "" { - return errors.New("illegal arguments: cannot specify both git branch and commit") - } - gitCloneOptions.ReferenceName = plumbing.NewBranchReferenceName(ctx.Build.Git.Branch) - gitCloneOptions.SingleBranch = true - } else if ctx.Build.Git.Tag != "" { - if ctx.Build.Git.Commit != "" { - return errors.New("illegal arguments: cannot specify both git tag and commit") - } - gitCloneOptions.ReferenceName = plumbing.NewTagReferenceName(ctx.Build.Git.Tag) - gitCloneOptions.SingleBranch = true - } - + secretToken := "" if ctx.Build.Git.Secret != "" { - secret, err := ctx.Client.CoreV1().Secrets(ctx.Namespace).Get(ctx.C, ctx.Build.Git.Secret, metav1.GetOptions{}) - if err != nil { - return err - } - token := "" - for _, v := range secret.Data { - if v != nil { - token = string(v) - } - } - gitCloneOptions.Auth = &http.BasicAuth{ - Username: "camel-k", // yes, this can be anything except an empty string - Password: token, - } - } - - repo, err := git.PlainClone(filepath.Join(ctx.Path, "maven"), false, gitCloneOptions) - - if err != nil { - return err - } - - if ctx.Build.Git.Commit != "" { - worktree, err := repo.Worktree() - if err != nil { - return err - } - commitHash := plumbing.NewHash(ctx.Build.Git.Commit) - err = worktree.Checkout(&git.CheckoutOptions{ - Hash: commitHash, - }) + var err error + secretToken, err = util.GitToken(ctx.C, ctx.Client, ctx.Namespace, ctx.Build.Git.Secret) if err != nil { return err } } + _, err := util.CloneGitProject(*ctx.Build.Git, filepath.Join(ctx.Path, "maven"), secretToken) - return nil + return err } diff --git a/pkg/client/camel/applyconfiguration/camel/v1/traits.go b/pkg/client/camel/applyconfiguration/camel/v1/traits.go index 7bf13bcd95..f5a00307f4 100644 --- a/pkg/client/camel/applyconfiguration/camel/v1/traits.go +++ b/pkg/client/camel/applyconfiguration/camel/v1/traits.go @@ -37,6 +37,7 @@ type TraitsApplyConfiguration struct { Environment *trait.EnvironmentTrait `json:"environment,omitempty"` ErrorHandler *trait.ErrorHandlerTrait `json:"error-handler,omitempty"` GC *trait.GCTrait `json:"gc,omitempty"` + GitOps *trait.GitOpsTrait `json:"gitops,omitempty"` Health *trait.HealthTrait `json:"health,omitempty"` Ingress *trait.IngressTrait `json:"ingress,omitempty"` InitContainers *trait.InitContainersTrait `json:"init-containers,omitempty"` @@ -165,6 +166,14 @@ func (b *TraitsApplyConfiguration) WithGC(value trait.GCTrait) *TraitsApplyConfi return b } +// WithGitOps sets the GitOps field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GitOps field is set to the value of the last call. +func (b *TraitsApplyConfiguration) WithGitOps(value trait.GitOpsTrait) *TraitsApplyConfiguration { + b.GitOps = &value + return b +} + // WithHealth sets the Health field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Health field is set to the value of the last call. diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go index e7990a0812..3da80b8c96 100644 --- a/pkg/cmd/promote.go +++ b/pkg/cmd/promote.go @@ -20,17 +20,11 @@ package cmd import ( "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" + util "github.com/apache/camel-k/v2/pkg/util/gitops" "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" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -55,6 +49,7 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO 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") + cmd.Flags().Bool("overwrite", false, "Overwrite the overlay if it exists") return &cmd, &options } @@ -67,6 +62,7 @@ type promoteCmdOptions struct { OutputFormat string `mapstructure:"output" yaml:",omitempty"` Image bool `mapstructure:"image" yaml:",omitempty"` ToGitOpsDir string `mapstructure:"export-gitops-dir" yaml:",omitempty"` + Overwrite bool `mapstructure:"overwrite" yaml:",omitempty"` } func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error { @@ -142,12 +138,12 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { // Pipe promotion if promotePipe { - destPipe := o.editPipe(sourcePipe, sourceIntegration, sourceKit) + destPipe := util.EditPipe(sourcePipe, sourceIntegration, sourceKit, o.To, o.ToOperator) if o.OutputFormat != "" { return showPipeOutput(cmd, destPipe, o.OutputFormat, c.GetScheme()) } if o.ToGitOpsDir != "" { - err = appendKustomizePipe(destPipe, o.ToGitOpsDir) + err = util.AppendKustomizePipe(destPipe, o.ToGitOpsDir, o.Overwrite) if err != nil { return err } @@ -169,12 +165,12 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { } // Plain Integration promotion - destIntegration := o.editIntegration(sourceIntegration, sourceKit) + destIntegration := util.EditIntegration(sourceIntegration, sourceKit, o.To, o.ToOperator) if o.OutputFormat != "" { return showIntegrationOutput(cmd, destIntegration, o.OutputFormat) } if o.ToGitOpsDir != "" { - err = appendKustomizeIntegration(destIntegration, o.ToGitOpsDir) + err = util.AppendKustomizeIntegration(destIntegration, o.ToGitOpsDir, o.Overwrite) if err != nil { return err } @@ -239,154 +235,6 @@ 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 { - contImage := it.Status.Image - // 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 = nil - if it.Status.Traits != nil { - dstIt.Spec.Traits = *it.Status.Traits - } - 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 - mergedClasspath := getClasspath(kit, jvmTrait) - 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, jvmTraitSpec *traitv1.JVMTrait) string { - jvmTraitClasspath := "" - if jvmTraitSpec != nil { - jvmTraitClasspath = jvmTraitSpec.Classpath - } - kitClasspathSet := kit.Status.GetDependenciesPaths() - if !kitClasspathSet.IsEmpty() { - if jvmTraitClasspath != "" { - jvmTraitClasspathSet := getClasspathSet(jvmTraitClasspath) - kitClasspathSet = sets.Union(kitClasspathSet, jvmTraitClasspathSet) - } - classPaths := kitClasspathSet.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 -} - -// Return all annotations overriding the operator Id if provided. -func cloneAnnotations(ann map[string]string, operatorID string) map[string]string { - operatorIDAnnotationSet := false - newMap := make(map[string]string) - for k, v := range ann { - if k == v1.OperatorIDAnnotation { - if operatorID != "" { - newMap[v1.OperatorIDAnnotation] = operatorID - operatorIDAnnotationSet = true - } - } else { - newMap[k] = v - } - } - if !operatorIDAnnotationSet && operatorID != "" { - newMap[v1.OperatorIDAnnotation] = operatorID - } - - return newMap -} - -// Return all labels. The method is a reference if in the future we need to apply any filtering. -func cloneLabels(lbs map[string]string) map[string]string { - newMap := make(map[string]string) - for k, v := range lbs { - newMap[k] = v - } - - return newMap -} - -func (o *promoteCmdOptions) editPipe(kb *v1.Pipe, it *v1.Integration, kit *v1.IntegrationKit) *v1.Pipe { - contImage := it.Status.Image - // 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) - traits := it.Status.Traits - if traits == nil { - traits = &v1.Traits{} - } - if traits.Container == nil { - traits.Container = &traitv1.ContainerTrait{} - } - 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 traits.JVM == nil { - traits.JVM = &traitv1.JVMTrait{} - } - jvmTrait := traits.JVM - mergedClasspath := getClasspath(kit, jvmTrait) - 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 traits.Camel == nil { - traits.Camel = &traitv1.CamelTrait{} - } - traits.Camel.RuntimeVersion = kit.Status.RuntimeVersion - } - dst.SetTraits(traits) - - if dst.Spec.Source.Ref != nil { - dst.Spec.Source.Ref.Namespace = o.To - } - if dst.Spec.Sink.Ref != nil { - dst.Spec.Sink.Ref.Namespace = o.To - } - if dst.Spec.Steps != nil { - for _, step := range dst.Spec.Steps { - if step.Ref != nil { - step.Ref.Namespace = o.To - } - } - } - - return &dst -} - func (o *promoteCmdOptions) replaceResource(res k8sclient.Object) (bool, error) { return kubernetes.ReplaceResource(o.Context, o._client, res) } @@ -398,216 +246,3 @@ func (o *promoteCmdOptions) isDryRun() bool { 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 := patchTraits(baseIt.Spec.Traits) - - patchedIt := v1.NewIntegration("", baseIt.Name) - patchedIt.Spec = v1.IntegrationSpec{ - Traits: patchedTraits, - } - - return &patchedIt -} - -// getPipePatch will filter those traits/configuration we want to include in the Pipe patch. -func getPipePatch(basePipe *v1.Pipe) *v1.Pipe { - patchedTraits := patchTraits(*basePipe.Spec.Traits) - - patchedPipe := v1.NewPipe("", basePipe.Name) - patchedPipe.Spec = v1.PipeSpec{ - Traits: &patchedTraits, - } - - return &patchedPipe -} - -func patchTraits(baseTraits v1.Traits) v1.Traits { - patchedTraits := v1.Traits{} - if baseTraits.Affinity != nil { - patchedTraits.Affinity = baseTraits.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 = baseTraits.Toleration - } - - return patchedTraits -} - -// 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 -} diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml index 93c7226ce7..e32934e266 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml @@ -1082,6 +1082,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -3410,6 +3475,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml index 10b6040ab3..2ea56647c6 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml @@ -950,6 +950,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -3157,6 +3222,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml index 9492e84d4b..a6817e58da 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml @@ -7764,6 +7764,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -9920,6 +9985,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: diff --git a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml index 563a4a9997..a922f1582b 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml @@ -7819,6 +7819,71 @@ spec: All traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the + operator will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes + (default `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes + (default `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. + All traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any + existing overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token + is stored. The operator will pick up the first secret + key only, whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project + is stored. + type: string + type: object health: description: The configuration of Health trait properties: @@ -9913,6 +9978,71 @@ spec: traits share this common property. type: boolean type: object + gitops: + description: The configuration of GitOps trait + properties: + branch: + description: the git branch to check out. + type: string + branchPush: + description: the git branch to push to. If omitted, the operator + will push to a new branch named as `cicd/release-candidate-`. + type: string + commit: + description: the git commit (full SHA) to check out. + type: string + committerEmail: + default: camel-k-operator@apache.org + description: The email used to commit the GitOps changes (default + `camel-k-operator@apache.org`). + type: string + committerName: + default: Camel K Operator + description: The name used to commit the GitOps changes (default + `Camel K Operator`). + type: string + configuration: + description: |- + Legacy trait configuration parameters. + Deprecated: for backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Can be used to enable or disable a trait. All + traits share this common property. + type: boolean + integrationDirectory: + default: integrations + description: The root path where to store Kustomize overlays + (default `integrations`). + type: string + overlays: + default: + - dev + - stag + - prod + description: a list of overlays to provide (default {"dev","stag","prod"}). + items: + type: string + type: array + overwriteOverlay: + default: false + description: a flag (default, false) to overwrite any existing + overlay. + type: boolean + secret: + description: the Kubernetes secret where the Git token is + stored. The operator will pick up the first secret key only, + whichever the name it is. + type: string + tag: + description: the git tag to check out. + type: string + url: + description: the URL of the repository where the project is + stored. + type: string + type: object health: description: The configuration of Health trait properties: diff --git a/pkg/trait/git_test.go b/pkg/trait/git_test.go new file mode 100644 index 0000000000..ae2382a311 --- /dev/null +++ b/pkg/trait/git_test.go @@ -0,0 +1,95 @@ +/* +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 trait + +import ( + "testing" + + v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGitApplyMissingTasks(t *testing.T) { + it := v1.NewIntegration("default", "test") + it.Spec.Git = &v1.GitConfigSpec{} + it.Status = v1.IntegrationStatus{ + Phase: v1.IntegrationPhaseBuildSubmitted, + } + e := &Environment{ + Integration: &it, + } + g := gitTrait{} + + ok, _, err := g.Configure(e) + require.NoError(t, err) + assert.True(t, ok) + err = g.Apply(e) + require.Error(t, err) + assert.Equal(t, "unable to find builder task: test", err.Error()) + + e.Pipeline = []v1.Task{ + v1.Task{Builder: &v1.BuilderTask{}}, + } + + err = g.Apply(e) + require.Error(t, err) + assert.Equal(t, "unable to find package task: test", err.Error()) +} + +func TestGitApplyOk(t *testing.T) { + it := v1.NewIntegration("default", "test") + it.Spec.Git = &v1.GitConfigSpec{} + it.Status = v1.IntegrationStatus{ + Phase: v1.IntegrationPhaseBuildSubmitted, + } + e := &Environment{ + Integration: &it, + Pipeline: []v1.Task{ + v1.Task{Builder: &v1.BuilderTask{}}, + v1.Task{Package: &v1.BuilderTask{}}, + }, + } + g := gitTrait{} + + ok, _, err := g.Configure(e) + assert.True(t, ok) + require.NoError(t, err) + err = g.Apply(e) + require.NoError(t, err) + + buildTask := getBuilderTask(e.Pipeline) + require.NotNil(t, buildTask) + packageTask := getPackageTask(e.Pipeline) + require.NotNil(t, packageTask) + + assert.Contains(t, buildTask.Steps, + "github.com/apache/camel-k/v2/pkg/builder/CloneProject", + "github.com/apache/camel-k/v2/pkg/builder/InjectJibProfile", + "github.com/apache/camel-k/v2/pkg/builder/BuildMavenContext", + "github.com/apache/camel-k/v2/pkg/builder/ExecuteMavenContext", + "github.com/apache/camel-k/v2/pkg/builder/ComputeDependencies", + ) + + assert.Contains(t, packageTask.Steps, + "github.com/apache/camel-k/v2/pkg/builder/ComputeDependencies", + "github.com/apache/camel-k/v2/pkg/builder/StandardImageContext", + "github.com/apache/camel-k/v2/pkg/builder/JvmDockerfile", + ) +} diff --git a/pkg/trait/gitops.go b/pkg/trait/gitops.go new file mode 100644 index 0000000000..9f54b2f452 --- /dev/null +++ b/pkg/trait/gitops.go @@ -0,0 +1,208 @@ +/* +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 trait + +import ( + "context" + "errors" + "os" + "path/filepath" + "time" + + v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" + traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" + util "github.com/apache/camel-k/v2/pkg/util/gitops" + "github.com/apache/camel-k/v2/pkg/util/io" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "k8s.io/utils/ptr" + + git "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" +) + +const ( + gitOpsTraitID = "gitops" + gitOpsTraitOrder = 1700 +) + +type gitOpsTrait struct { + BaseTrait + traitv1.GitOpsTrait `property:",squash"` +} + +func newGitOpsTrait() Trait { + return &gitOpsTrait{ + BaseTrait: NewBaseTrait(gitOpsTraitID, gitOpsTraitOrder), + } +} + +func (t *gitOpsTrait) Configure(e *Environment) (bool, *TraitCondition, error) { + if e.Integration == nil || !ptr.Deref(t.Enabled, false) { + return false, nil, nil + } + + return e.IntegrationInPhase(v1.IntegrationPhaseDeploying), nil, nil +} + +func (t *gitOpsTrait) Apply(e *Environment) error { + // Register a post action that is in charge to create a PR on the Git project. + // It must be done on Deploying phase in order to catch the Integration status changed + // after all traits executed in that phase. + e.PostActions = append(e.PostActions, func(env *Environment) error { + gitToken, err := util.GitToken(env.Ctx, env.Client, env.Integration.Namespace, t.Secret) + if err != nil { + return err + } + if gitToken == "" { + return errors.New("no git token provided") + } + + return t.pushGitOpsRepo(env.Ctx, env.Integration, gitToken) + }) + + return nil +} + +// withTempDir wraps the execution of a function making sure to create a temporary directory and cleaning it when finishing +// the function. +func withTempDir(fn func(dir string) error) error { + dir, err := os.MkdirTemp("tmp", "integration-*") + if err != nil { + return err + } + defer os.RemoveAll(dir) + + return fn(dir) +} + +// pushGitOpsRepo makes sure to use a temporary directory. +func (t *gitOpsTrait) pushGitOpsRepo(ctx context.Context, it *v1.Integration, token string) error { + return withTempDir(func(dir string) error { + return t.pushGitOpsItInGitRepo(ctx, it, dir, token) + }) +} + +// pushGitOpsItInGitRepo is in charge to clone the repo, do the kustomize overlays and push the changes +// to a new branch. +func (t *gitOpsTrait) pushGitOpsItInGitRepo(ctx context.Context, it *v1.Integration, dir, token string) error { + gitConf := t.gitConf(it) + // Clone repo + repo, err := util.CloneGitProject(gitConf, dir, token) + if err != nil { + return err + } + w, err := repo.Worktree() + if err != nil { + return err + } + + // Create a new branch + nowDate := time.Now().Format("20060102-150405") + branchName := t.BranchPush + if branchName == "" { + branchName = "cicd/candidate-release-" + nowDate + } + commitMessage := "feat(ci): build completed on " + nowDate + branchRef := plumbing.NewBranchReferenceName(branchName) + + err = w.Checkout(&git.CheckoutOptions{ + Branch: branchRef, + Create: true, + }) + if err != nil { + return err + } + + // Generate Kustomize content (it may override upstream content) + ciCdDir := filepath.Join(dir, t.IntegrationDirectory) + err = os.MkdirAll(ciCdDir, io.FilePerm755) + if err != nil { + return err + } + + kit, err := getIntegrationKit(ctx, t.Client, it) + if err != nil { + return err + } + + for _, overlay := range t.Overlays { + destIntegration := util.EditIntegration(it, kit, overlay, "") + err = util.AppendKustomizeIntegration(destIntegration, ciCdDir, t.OverwriteOverlay) + if err != nil { + return err + } + } + + // Commit and push new content + _, err = w.Add(t.IntegrationDirectory) + if err != nil { + return err + } + _, err = w.Commit(commitMessage, &git.CommitOptions{ + Author: &object.Signature{ + Name: t.CommiterName, + Email: t.CommiterEmail, + When: time.Now(), + }, + }) + if err != nil { + return err + } + + gitPushOptions := &git.PushOptions{ + RemoteURL: gitConf.URL, + Auth: &http.BasicAuth{ + Username: "camel-k", + Password: token, + }, + RefSpecs: []config.RefSpec{ + config.RefSpec(branchRef + ":" + branchRef), + }, + } + + return repo.Push(gitPushOptions) +} + +// gitConf returns the git repo configuration where to pull the project from. If no value is provided, then, it takes +// the value coming from Integration git project (if specified). +func (t *gitOpsTrait) gitConf(it *v1.Integration) v1.GitConfigSpec { + gitConf := v1.GitConfigSpec{ + URL: t.URL, + Branch: t.Branch, + Tag: t.Tag, + Commit: t.Commit, + } + if it.Spec.Git != nil { + if gitConf.URL == "" { + gitConf.URL = it.Spec.Git.URL + } + if gitConf.Branch == "" { + gitConf.Branch = it.Spec.Git.Branch + } + if gitConf.Tag == "" { + gitConf.Tag = it.Spec.Git.Tag + } + if gitConf.Commit == "" { + gitConf.Commit = it.Spec.Git.Commit + } + } + + return gitConf +} diff --git a/pkg/trait/gitops_test.go b/pkg/trait/gitops_test.go new file mode 100644 index 0000000000..4acda7c8e6 --- /dev/null +++ b/pkg/trait/gitops_test.go @@ -0,0 +1,181 @@ +/* +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 trait + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + "time" + + v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" + "knative.dev/pkg/ptr" + + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + git "github.com/go-git/go-git/v5" +) + +func TestGitOpsAddAction(t *testing.T) { + trait, _ := newGitOpsTrait().(*gitOpsTrait) + trait.Enabled = ptr.Bool(true) + env := &Environment{ + Integration: &v1.Integration{ + Status: v1.IntegrationStatus{ + Phase: v1.IntegrationPhaseDeploying, + }, + }, + PostActions: []func(*Environment) error{}, + } + ok, _, err := trait.Configure(env) + require.NoError(t, err) + assert.True(t, ok) + err = trait.Apply(env) + require.NoError(t, err) + assert.Len(t, env.PostActions, 1) +} + +func TestGitOpsPushRepoDefault(t *testing.T) { + trait, _ := newGitOpsTrait().(*gitOpsTrait) + trait.Overlays = []string{"dev", "prod"} + trait.IntegrationDirectory = "integrations" + // As this test would require to access to a private repository, + // We are simulating a remote repository pointing to a local fake repository. + srcGitDir := t.TempDir() + tmpGitDir := t.TempDir() + err := initFakeGitRepo(srcGitDir) + require.NoError(t, err) + it := v1.NewIntegration("default", "test") + conf := &v1.GitConfigSpec{ + URL: srcGitDir, + } + it.Spec = v1.IntegrationSpec{ + Git: conf, + Sources: []v1.SourceSpec{v1.NewSourceSpec("Test.java", "bogus, irrelevant for test", v1.LanguageJavaSource)}, + } + it.Status = v1.IntegrationStatus{ + Image: "my-img-recently-baked", + } + + err = trait.pushGitOpsItInGitRepo(context.TODO(), &it, tmpGitDir, "fake") + require.NoError(t, err) + + lastCommitMessage, err := getLastCommitMessage(tmpGitDir) + require.NoError(t, err) + assert.Contains(t, lastCommitMessage, "feat(ci): build complete") + branchName, err := getBranchName(tmpGitDir) + require.NoError(t, err) + assert.Contains(t, branchName, "cicd/candidate-release") + remoteUrl, err := getRemoteURL(tmpGitDir) + require.NoError(t, err) + assert.Equal(t, srcGitDir, remoteUrl) + gitopsDir, err := os.Stat(filepath.Join(tmpGitDir, "integrations", it.Name)) + require.NoError(t, err) + assert.True(t, gitopsDir.IsDir()) + gitopsDir, err = os.Stat(filepath.Join(tmpGitDir, "integrations", it.Name, "overlays", "dev")) + require.NoError(t, err) + assert.True(t, gitopsDir.IsDir()) + gitopsDir, err = os.Stat(filepath.Join(tmpGitDir, "integrations", it.Name, "overlays", "prod")) + require.NoError(t, err) + assert.True(t, gitopsDir.IsDir()) +} + +// initFakeGitInmemoryRepo has the goal to create a fake a git repository into a given directory. +// We can use this to simulate pull and push activities. +func initFakeGitRepo(dirPath string) error { + repo, err := git.PlainInit(dirPath, false) + if err != nil { + return err + } + filePath := filepath.Join(dirPath, "README") + if err := os.WriteFile(filePath, []byte("Hello test!"), 0644); err != nil { + return err + } + wt, err := repo.Worktree() + if err != nil { + return err + } + _, err = wt.Add("README") + if err != nil { + return err + } + _, err = wt.Commit("init commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Tester", + Email: "tester@example.com", + When: time.Now(), + }, + }) + if err != nil { + return err + } + + return nil +} + +// getLastCommitMessage returns the latest commit message of the Git repository in dirPath. +func getLastCommitMessage(dirPath string) (string, error) { + repo, err := git.PlainOpen(dirPath) + if err != nil { + return "", err + } + headRef, err := repo.Head() + if err != nil { + return "", err + } + commit, err := repo.CommitObject(headRef.Hash()) + if err != nil { + return "", err + } + + return commit.Message, nil +} + +// getBranchName returns the branch name of a given directory. +func getBranchName(dirPath string) (string, error) { + repo, err := git.PlainOpen(dirPath) + if err != nil { + return "", err + } + headRef, err := repo.Head() + if err != nil { + return "", err + } + return headRef.Name().Short(), nil +} + +func getRemoteURL(dirPath string) (string, error) { + repo, err := git.PlainOpen(dirPath) + if err != nil { + return "", err + } + remote, err := repo.Remote("origin") + if err != nil { + return "", err + } + urls := remote.Config().URLs + if len(urls) == 0 { + return "", errors.New("no URLs found for remote 'origin'") + } + + return urls[0], nil +} diff --git a/pkg/trait/trait_register.go b/pkg/trait/trait_register.go index f083ce2b8d..c233ca7153 100644 --- a/pkg/trait/trait_register.go +++ b/pkg/trait/trait_register.go @@ -31,6 +31,7 @@ func init() { AddToTraits(newEnvironmentTrait) AddToTraits(newGCTrait) AddToTraits(newGitTrait) + AddToTraits(newGitOpsTrait) AddToTraits(newHealthTrait) AddToTraits(newInitContainersTrait) AddToTraits(NewInitTrait) diff --git a/pkg/trait/trait_types.go b/pkg/trait/trait_types.go index 27f04419e8..e56bb86a89 100644 --- a/pkg/trait/trait_types.go +++ b/pkg/trait/trait_types.go @@ -219,10 +219,14 @@ type Environment struct { // The IntegrationKits to be created for the Integration IntegrationKits []v1.IntegrationKit // The resources owned by the Integration that are applied to the API server - Resources *kubernetes.Collection - PostActions []func(*Environment) error - PostStepProcessors []func(*Environment) error - PostProcessors []func(*Environment) error + Resources *kubernetes.Collection + // Actions to be executed after each trait is completed for the given phase. + PostStepProcessors []func(*Environment) error + // Actions to be executed after all traits have completed for the given phase. + PostProcessors []func(*Environment) error + // Actions to be executed after all traits have completed for the given phase and Integration status set. + PostActions []func(*Environment) error + // Tasks pipeline to execute. Pipeline []v1.Task ConfiguredTraits []Trait ExecutedTraits []Trait diff --git a/pkg/util/gitops/gitops.go b/pkg/util/gitops/gitops.go new file mode 100644 index 0000000000..f4ab319a0b --- /dev/null +++ b/pkg/util/gitops/gitops.go @@ -0,0 +1,499 @@ +/* +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 util + +import ( + "context" + "errors" + "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" + git "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const kustomizationContent = `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +` + +// EditIntegration is in charge to create an Integration with the content required by the GitOps operations. +func EditIntegration(it *v1.Integration, kit *v1.IntegrationKit, toNamespace, toOperator string) *v1.Integration { + contImage := it.Status.Image + // Integration + dstIt := v1.NewIntegration(toNamespace, it.Name) + dstIt.Spec = *it.Spec.DeepCopy() + dstIt.Annotations = cloneAnnotations(it.Annotations, toOperator) + dstIt.Labels = cloneLabels(it.Labels) + dstIt.Spec.IntegrationKit = nil + if it.Status.Traits != nil { + dstIt.Spec.Traits = *it.Status.Traits + } + // We make sure not to propagate further the gitops trait + // to avoid infinite loops. If the user wants to do a chain based + // strategy, she can use the patch-integration and continue the chain on purpose + dstIt.Spec.Traits.GitOps = 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 + mergedClasspath := getClasspath(kit, jvmTrait) + 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, jvmTraitSpec *traitv1.JVMTrait) string { + jvmTraitClasspath := "" + if jvmTraitSpec != nil { + jvmTraitClasspath = jvmTraitSpec.Classpath + } + kitClasspathSet := kit.Status.GetDependenciesPaths() + if !kitClasspathSet.IsEmpty() { + if jvmTraitClasspath != "" { + jvmTraitClasspathSet := getClasspathSet(jvmTraitClasspath) + kitClasspathSet = sets.Union(kitClasspathSet, jvmTraitClasspathSet) + } + classPaths := kitClasspathSet.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 +} + +// Return all annotations overriding the operator Id if provided. +func cloneAnnotations(ann map[string]string, operatorID string) map[string]string { + operatorIDAnnotationSet := false + newMap := make(map[string]string) + for k, v := range ann { + if k == "kubectl.kubernetes.io/last-applied-configuration" { + continue + } + if k == v1.OperatorIDAnnotation { + if operatorID != "" { + newMap[v1.OperatorIDAnnotation] = operatorID + operatorIDAnnotationSet = true + } + } else { + newMap[k] = v + } + } + if !operatorIDAnnotationSet && operatorID != "" { + newMap[v1.OperatorIDAnnotation] = operatorID + } + + return newMap +} + +// Return all labels. The method is a reference if in the future we need to apply any filtering. +func cloneLabels(lbs map[string]string) map[string]string { + newMap := make(map[string]string) + for k, v := range lbs { + newMap[k] = v + } + + return newMap +} + +// EditPipe is in charge to create a Pipe with the content required by the GitOps operations. +func EditPipe(kb *v1.Pipe, it *v1.Integration, kit *v1.IntegrationKit, toNamespace, toOperator string) *v1.Pipe { + contImage := it.Status.Image + // Pipe + dst := v1.NewPipe(toNamespace, kb.Name) + dst.Spec = *kb.Spec.DeepCopy() + dst.Annotations = cloneAnnotations(kb.Annotations, toOperator) + dst.Labels = cloneLabels(kb.Labels) + traits := it.Status.Traits + if traits == nil { + traits = &v1.Traits{} + } + if traits.Container == nil { + traits.Container = &traitv1.ContainerTrait{} + } + 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 traits.JVM == nil { + traits.JVM = &traitv1.JVMTrait{} + } + jvmTrait := traits.JVM + mergedClasspath := getClasspath(kit, jvmTrait) + 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 traits.Camel == nil { + traits.Camel = &traitv1.CamelTrait{} + } + traits.Camel.RuntimeVersion = kit.Status.RuntimeVersion + } + dst.SetTraits(traits) + + if dst.Spec.Source.Ref != nil { + dst.Spec.Source.Ref.Namespace = toNamespace + } + if dst.Spec.Sink.Ref != nil { + dst.Spec.Sink.Ref.Namespace = toNamespace + } + if dst.Spec.Steps != nil { + for _, step := range dst.Spec.Steps { + if step.Ref != nil { + step.Ref.Namespace = toNamespace + } + } + } + + return &dst +} + +// AppendKustomizeIntegration creates a Kustomize GitOps based directory structure for the chosen Integration. +func AppendKustomizeIntegration(dstIt *v1.Integration, destinationDir string, overwrite bool) 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) + // We only create or override the overlay if it was explicitly requested or it does not exist yet. + if overwrite || !dirExists(newpath) { + 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 +} + +func dirExists(path string) bool { + info, err := os.Stat(path) + + return err == nil && info.IsDir() +} + +// getIntegrationPatch will filter those traits/configuration we want to include in the Integration patch. +func getIntegrationPatch(baseIt *v1.Integration) *v1.Integration { + patchedTraits := patchTraits(baseIt.Spec.Traits) + + patchedIt := v1.NewIntegration("", baseIt.Name) + patchedIt.Spec = v1.IntegrationSpec{ + Traits: patchedTraits, + } + + return &patchedIt +} + +// getPipePatch will filter those traits/configuration we want to include in the Pipe patch. +func getPipePatch(basePipe *v1.Pipe) *v1.Pipe { + patchedTraits := patchTraits(*basePipe.Spec.Traits) + + patchedPipe := v1.NewPipe("", basePipe.Name) + patchedPipe.Spec = v1.PipeSpec{ + Traits: &patchedTraits, + } + + return &patchedPipe +} + +func patchTraits(baseTraits v1.Traits) v1.Traits { + patchedTraits := v1.Traits{} + if baseTraits.Affinity != nil { + patchedTraits.Affinity = baseTraits.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 = baseTraits.Toleration + } + + return patchedTraits +} + +// AppendKustomizePipe creates a Kustomize GitOps based directory structure for the chosen Pipe. +func AppendKustomizePipe(dstPipe *v1.Pipe, destinationDir string, overwrite bool) 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) + // We only create or override the overlay if it was explicitly requested or it does not exist yet. + if overwrite || !dirExists(newpath) { + 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 +} + +// GitToken read the first secret data provided by the Integration Git Secret. +// Returns an empty string if the secret provided is empty. +func GitToken(ctx context.Context, c client.Client, namespace, secret string) (string, error) { + if secret == "" { + return "", nil + } + sec, err := c.CoreV1().Secrets(namespace).Get(ctx, secret, metav1.GetOptions{}) + if err != nil { + return "", err + } + for _, v := range sec.Data { + if v != nil { + return string(v), nil + } + } + + return "", nil +} + +// CloneGitProject is in charge to clone the project from a given Git repo configuration. +// If no secretToken is provided, then, it is assumed the project is public. +func CloneGitProject(gitConf v1.GitConfigSpec, dir, secretToken string) (*git.Repository, error) { + depth := 1 + if gitConf.Commit != "" { + // only the commit checkout requires full git project history + depth = 0 + } + gitCloneOptions := &git.CloneOptions{ + URL: gitConf.URL, + Depth: depth, + } + + if gitConf.Branch != "" { + if gitConf.Tag != "" { + return nil, errors.New("illegal arguments: cannot specify both git branch and tag") + } + if gitConf.Commit != "" { + return nil, errors.New("illegal arguments: cannot specify both git branch and commit") + } + gitCloneOptions.ReferenceName = plumbing.NewBranchReferenceName(gitConf.Branch) + gitCloneOptions.SingleBranch = true + } else if gitConf.Tag != "" { + if gitConf.Commit != "" { + return nil, errors.New("illegal arguments: cannot specify both git tag and commit") + } + gitCloneOptions.ReferenceName = plumbing.NewTagReferenceName(gitConf.Tag) + gitCloneOptions.SingleBranch = true + } + + if secretToken != "" { + gitCloneOptions.Auth = &http.BasicAuth{ + Username: "camel-k", // yes, this can be anything except an empty string + Password: secretToken, + } + } + + repo, err := git.PlainClone(dir, false, gitCloneOptions) + if err != nil { + return nil, err + } + + if gitConf.Commit != "" { + worktree, err := repo.Worktree() + if err != nil { + return nil, err + } + commitHash := plumbing.NewHash(gitConf.Commit) + err = worktree.Checkout(&git.CheckoutOptions{ + Hash: commitHash, + }) + if err != nil { + return nil, err + } + } + + return repo, nil +}