From ddcfdf84327196d46f4abd79242e6cb5a02f6e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20Hu=C3=9Fmann?= Date: Fri, 15 May 2026 13:01:09 +0200 Subject: [PATCH] code format --- .github/workflows/create-release.yml | 18 +- README.md | 94 +- .../cluster-resources/apps/argocd/README.md | 12 +- .../templates/namespaceJobTemplate.xml.ftl | 15 +- docs/Applications.md | 172 +- docs/Configuration.md | 348 +- docs/Deploy-Ingress-Controller.md | 62 +- docs/Developers.md | 166 +- docs/Installation.md | 33 +- docs/Running-on-Windows-Mac.md | 55 +- docs/configuration.schema.json | 2025 +++++++----- docs/content-loader/content-loader.md | 157 +- docs/images/app-repo-vs-gitops-repo.svg | 195 +- docs/images/argocd-repos.svg | 338 +- docs/images/autopilot-repo.svg | 318 +- .../gitops-playground-features.drawio.svg | 1521 ++++++++- .../images/gitops-playground-local.drawio.svg | 1397 +++++++- .../gitops-playground-production.drawio.svg | 1281 ++++++- docs/k3d.md | 43 +- renovate.json | 8 +- .../com/cloudogu/gitops/Application.groovy | 32 +- .../kubernetes/api/K8sJavaApiClient.groovy | 4 +- src/main/resources/proxy-config.json | 30 +- .../gitops/ApplicationConfiguratorTest.groovy | 1174 ++++--- .../cloudogu/gitops/ApplicationTest.groovy | 228 +- .../gitops/features/CertManagerTest.groovy | 8 +- .../gitops/features/ContentLoaderTest.groovy | 2195 ++++++------ .../gitops/features/JenkinsTest.groovy | 659 ++-- .../gitops/features/MonitoringTest.groovy | 1265 ++++--- .../argocd/ArgoCDRepoSetupTest.groovy | 354 +- .../gitops/features/argocd/ArgoCDTest.groovy | 2942 ++++++++--------- .../deployment/HelmStrategyTest.groovy | 69 +- .../scmmanager/ScmManagerSetupTest.groovy | 128 +- .../ScmManagerUrlResolverTest.groovy | 301 +- .../profiles/FullProfileTestIT.groovy | 304 +- .../profiles/MandantProfileTestIT.groovy | 202 +- .../profiles/PetclinicProfileTestIT.groovy | 216 +- .../rbac/ArgocdApplicationTest.groovy | 86 +- .../kubernetes/rbac/RbacDefinitionTest.groovy | 588 ++-- .../gitops/utils/AirGappedUtilsTest.groovy | 341 +- .../gitops/utils/FileSystemUtilsTest.groovy | 191 +- .../common/repo/some.yaml.ftl | 2 +- .../gitops/utils/git/ScmManagerMock.groovy | 226 +- 43 files changed, 12534 insertions(+), 7269 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 7ee3e5322..c22826781 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -12,12 +12,12 @@ jobs: name: Release pushed tag runs-on: ubuntu-latest steps: - - name: Create release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref_name }} - run: | - gh release create "$tag" \ - --repo="$GITHUB_REPOSITORY" \ - --title="${tag}" \ - --generate-notes + - name: Create release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref_name }} + run: | + gh release create "$tag" \ + --repo="$GITHUB_REPOSITORY" \ + --title="${tag}" \ + --generate-notes \ No newline at end of file diff --git a/README.md b/README.md index c0ce4015c..e6221c0c2 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,40 @@ [![Playground features](docs/images/gitops-playground-features.drawio.svg)](https://cdn.jsdelivr.net/gh/cloudogu/gitops-playground@main/docs/images/gitops-playground-features.drawio.svg "View full size") -Create a complete GitOps-based operational stack with all the tools you need for an internal developer platform, on your machine, in your datacenter or in the cloud! +Create a complete GitOps-based operational stack with all the tools you need for an internal developer platform, on your +machine, in your datacenter or in the cloud! * __Deployment__: GitOps via Argo CD with a ready-to-use [repo structure](docs/Applications.md#argo-cd) * __Monitoring__: [Prometheus and Grafana](docs/Applications.md#monitoring-tools) * __Secrets__ Management: [Vault and External Secrets Operator](docs/Applications.md#secrets-management-tools) * __Notifications__/Alerts: Grafana and ArgoCD can be predefined with either an external mailserver. -* __Pipelines__: Example applications using [Jenkins](docs/Applications.md#jenkins) with the [gitops-build-lib](https://github.com/cloudogu/gitops-build-lib) and [SCM-Manager](docs/Applications.md#scm-manager) +* __Pipelines__: Example applications using [Jenkins](docs/Applications.md#jenkins) with + the [gitops-build-lib](https://github.com/cloudogu/gitops-build-lib) + and [SCM-Manager](docs/Applications.md#scm-manager) * __Ingress__ Controller: [ingress](https://traefik.github.io/charts) * __Certificate__ Management: [cert-manager](https://cert-manager.io/) -* [Content Loader](docs/content-loader/content-loader.md): Completely customize what is pushed to Git during installation. +* [Content Loader](docs/content-loader/content-loader.md): Completely customize what is pushed to Git during + installation. This allows for adding your own end-user or IDP apps, creating repos, adding Argo CD tenants, etc. -* Runs on: - * local cluster (try it [with only one command](#tldr)), - * in the public cloud, +* Runs on: + * local cluster (try it [with only one command](#tldr)), + * in the public cloud, * and even air-gapped environments. -The gitops-playground is derived from our experiences in [consulting](https://platform.cloudogu.com/consulting/kubernetes-und-gitops/?mtm_campaign=gitops-playground&mtm_kwd=consulting&mtm_source=github&mtm_medium=link), -operating our internal developer platform (IDP) at [Cloudogu](https://cloudogu.com/?mtm_campaign=gitops-playground&mtm_kwd=cloudogu&mtm_source=github&mtm_medium=link) and is used in our [GitOps trainings](https://platform.cloudogu.com/en/trainings/gitops-continuous-operations/?mtm_campaign=gitops-playground&mtm_kwd=training&mtm_source=github&mtm_medium=link). +The gitops-playground is derived from our experiences +in [consulting](https://platform.cloudogu.com/consulting/kubernetes-und-gitops/?mtm_campaign=gitops-playground&mtm_kwd=consulting&mtm_source=github&mtm_medium=link), +operating our internal developer platform (IDP) +at [Cloudogu](https://cloudogu.com/?mtm_campaign=gitops-playground&mtm_kwd=cloudogu&mtm_source=github&mtm_medium=link) +and is used in +our [GitOps trainings](https://platform.cloudogu.com/en/trainings/gitops-continuous-operations/?mtm_campaign=gitops-playground&mtm_kwd=training&mtm_source=github&mtm_medium=link). -No need to read lots of books and operator docs, getting familiar with CLIs, +No need to read lots of books and operator docs, getting familiar with CLIs, ponder about GitOps Repository folder structures and promotion to different environments, etc. The GitOps Playground is a pre-configured environment to see GitOps in motion, including more advanced use cases like notifications, monitoring and secret management. We aim to be compatible with various environments, we even run in an air-gapped networks. - ## TL;DR You can try the GitOps Playground on a local Kubernetes cluster by running a single command: @@ -42,26 +49,30 @@ bash <(curl -s \ ghcr.io/cloudogu/gitops-playground --profile=full ``` -This will install the gop-platform with the profile full to showcase most of the features. To learn more about profiles, see [Profiles](#profiles) +This will install the gop-platform with the profile full to showcase most of the features. To learn more about profiles, +see [Profiles](#profiles) Note that on some linux distros like debian do not support subdomains of localhost. -There you might have to use `--base-url=http://local.gd` (see [local ingresses](docs/Deploy-Ingress-Controller.md#local-ingresses)). +There you might have to use `--base-url=http://local.gd` ( +see [local ingresses](docs/Deploy-Ingress-Controller.md#local-ingresses)). -We recommend running this command as an unprivileged user, that is inside the [docker group](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user). +We recommend running this command as an unprivileged user, that is inside +the [docker group](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user). ## Installation and Components A detailed document on how to install GOP in all possible environments can be found [here](docs/Installation.md). For a deep-dive into all components that GOP can install for you, see [Applications](docs/Applications.md) - ## Configuration You can configure GOP using CLI params, config file and/or config map. -Config file and map have the same format and offer a [schema file](https://raw.githubusercontent.com/cloudogu/gitops-playground/main/docs/configuration.schema.json). +Config file and map have the same format and offer +a [schema file](https://raw.githubusercontent.com/cloudogu/gitops-playground/main/docs/configuration.schema.json). Please find an overview of all CLI and config options [here](docs/Configuration.md) **Configuration precedence (highest to lowest):** + 1. Command-line parameters 2. Configuration files (`--config-file`) 3. Config maps (`--config-map`) @@ -71,65 +82,70 @@ That is, if you pass a param via CLI, for example, it will overwrite the corresp For a deep-dive into GOPs configuration, see [Configuration.md](docs/Configuration.md) ### Profiles -GOP includes some pre-defined profiles for easy usage, set `--profile=` to start GOP with your desired profile. + +GOP includes some pre-defined profiles for easy usage, set `--profile=` to start GOP with your desired +profile. Current existing profiles for argocd in non-operator mode: -| Profile | Features | Use-Case | +| Profile | Features | Use-Case | |------------------|------------------------------------------|--------------------------------------| -| minimal | Argo-cd, SCM-Manager | quick start to get going with gitops | -| content-examples | Argo-cd, SCM-Manager, Jenkins, Petclinic | demo a complete developer workflow | -| full | all available features | showcase a full-fledged IDP | - +| minimal | Argo-cd, SCM-Manager | quick start to get going with gitops | +| content-examples | Argo-cd, SCM-Manager, Jenkins, Petclinic | demo a complete developer workflow | +| full | all available features | showcase a full-fledged IDP | Follow profils for ArgoCD in Operator mode which has to be installed first: -| Profile | Features | Use-Case | +| Profile | Features | Use-Case | |---------------------------|------------------------------------------|----------------------------------------------------------------------| -| operator-minimal | Argo-cd, SCM-Manager | minimal example for an operator based gitops-stack | -| operator-content-examples | Argo-cd, Jenkins, SCM-Manager, Petclinic | demo a complete developer workflow in an operator based gitops-stack | -| operator-full | all available features | showcase a full-fledged cloud-native IDP with an operator | -| operator-mandant | special multi-tenant setup | see what a multi-tenant, operator based deployment could look like | - +| operator-minimal | Argo-cd, SCM-Manager | minimal example for an operator based gitops-stack | +| operator-content-examples | Argo-cd, Jenkins, SCM-Manager, Petclinic | demo a complete developer workflow in an +operator based gitops-stack | +| operator-full | all available features | showcase a full-fledged cloud-native IDP with an operator | +| operator-mandant | special multi-tenant setup | see what a multi-tenant, operator based deployment could look like | ## Remove playground For k3d, you can just `k3d cluster delete gitops-playground`. This will delete the whole cluster. If you want to delete k3d use `rm .local/bin/k3d`. - ## Additional Ressources We compiled a few helpful documents for the most common use-cases/scenarios: + - [Deploying an ingress controller](docs/Deploy-Ingress-Controller.md) - [Running GOP on Windows or Mac](docs/Running-on-Windows-Mac.md) - ## Development See [docs/Developers.md](docs/Developers.md) - ## License + Copyright © 2020 - present Cloudogu GmbH Licensed under AGPL-3, see [LICENSE](LICENSE) for details. -You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/. +You should have received a copy of the GNU Affero General Public License along with this program. If not, +see https://www.gnu.org/licenses/. -GitOps Playground© for use with Argo™, Git™, Jenkins®, Kubernetes®, Grafana®, Prometheus®, Vault® and SCM-Manager +GitOps Playground© for use with Argo™, Git™, Jenkins®, Kubernetes®, Grafana®, Prometheus®, Vault® and SCM-Manager Argo™ is an unregistered trademark of The Linux Foundation® Git™ is an unregistered trademark of Software Freedom Conservancy Inc. Jenkins® is a registered trademark of LF Charities Inc. Kubernetes® and the Kubernetes logo® are registered trademarks of The Linux Foundation® K8s® is a registered trademark of The Linux Foundation® -The Grafana Labs Marks are trademarks of Grafana Labs, and are used with Grafana Labs’ permission. We are not affiliated with, endorsed or sponsored by Grafana Labs or its affiliates. +The Grafana Labs Marks are trademarks of Grafana Labs, and are used with Grafana Labs’ permission. We are not affiliated +with, endorsed or sponsored by Grafana Labs or its affiliates. Prometheus® is a registered trademark of The Linux Foundation® -Vault® and the Vault logo® are registered trademarks of HashiCorp® (http://www.hashicorp.com/) +Vault® and the Vault logo® are registered trademarks of HashiCorp® (http://www.hashicorp.com/) ## Written Offer + Written Offer for Source Code: -Information on the license conditions and - if required by the license - on the source code is available free of charge on request. -However, some licenses require providing physical copies of the source or object code. If this is the case, you can request a copy of the source code. A small fee is charged for these services to cover the cost of physical distribution. +Information on the license conditions and - if required by the license - on the source code is available free of charge +on request. +However, some licenses require providing physical copies of the source or object code. If this is the case, you can +request a copy of the source code. A small fee is charged for these services to cover the cost of physical distribution. To receive a copy of the source code, you can either submit a written request to @@ -139,5 +155,7 @@ Garküche 1 or you may email hello@cloudogu.com. -Your request must be sent within three years from the date you received the software from Cloudogu that is the subject of your request or, in the case of source code licensed under the AGPL/GPL/LGPL v3, for as long as Cloudogu offers spare parts or customer support -for the product, including the components or binaries that are the subject of your request. +Your request must be sent within three years from the date you received the software from Cloudogu that is the subject +of your request or, in the case of source code licensed under the AGPL/GPL/LGPL v3, for as long as Cloudogu offers spare +parts or customer support +for the product, including the components or binaries that are the subject of your request. \ No newline at end of file diff --git a/argocd/cluster-resources/apps/argocd/README.md b/argocd/cluster-resources/apps/argocd/README.md index 0db628ff5..ef84f0672 100644 --- a/argocd/cluster-resources/apps/argocd/README.md +++ b/argocd/cluster-resources/apps/argocd/README.md @@ -1,20 +1,26 @@ # Argo CD Repo for managing Argo CD via GitOps. This repository contains the following folders: -* `applications`: Argo applications. One for each team pointing at their own repository and some general applications for managing the three folders of this repository + +* `applications`: Argo applications. One for each team pointing at their own repository and some general applications + for managing the three folders of this repository * `argocd`: Self managing Argo installation and configuration * `projects`: One Argo project for each team for clean organization and to distribute access rights ## Upgrade Argo CD to newer version -1. Look [here](https://artifacthub.io/packages/helm/argo/argocd#changelog) if there are necessary actions when upgrading to the new version + +1. Look [here](https://artifacthub.io/packages/helm/argo/argocd#changelog) if there are necessary actions when upgrading + to the new version 2. Change the version in `Chart.yaml` 3. run `helm dep update argocd` from the root of the repo 4. Push the modified `Chart.yaml`, `Chart.lock` and any changes from step 1, if there are any 5. Argo now upgrades itself ## What to do if argo breaks itself -If you make a commit, which breaks something from argo, and it fails to manage itself back to a healthy state with a + +If you make a commit, which breaks something from argo, and it fails to manage itself back to a healthy state with a new commit, than you have to fix argo with helm from your local computer. + ```bash # first fix the error diff --git a/argocd/cluster-resources/apps/jenkins/templates/namespaceJobTemplate.xml.ftl b/argocd/cluster-resources/apps/jenkins/templates/namespaceJobTemplate.xml.ftl index 2e348caec..536c30f73 100644 --- a/argocd/cluster-resources/apps/jenkins/templates/namespaceJobTemplate.xml.ftl +++ b/argocd/cluster-resources/apps/jenkins/templates/namespaceJobTemplate.xml.ftl @@ -5,7 +5,8 @@ - + false @@ -15,7 +16,8 @@ - + H H/4 * * * 86400000 @@ -37,14 +39,16 @@ - + true -1 -1 false - + H H/4 * * * 86400000 @@ -68,7 +72,8 @@ - + Jenkinsfile diff --git a/docs/Applications.md b/docs/Applications.md index 32ce60197..4c5016391 100644 --- a/docs/Applications.md +++ b/docs/Applications.md @@ -4,6 +4,7 @@ The GitOps playground creates a complete GitOps-based operational stack that can be used as an internal developer platform (IDP) on your Kubernetes cluster. The stack is composed of multiple applications, where some of them can be accessed via a web UI. + * Argo CD * Prometheus/Grafana * Jenkins @@ -20,18 +21,19 @@ We recommend using the `--ingress` and `--base-url` Parameters. With these, the applications are made available as subdomains of `base-url`. For example, `--base-url=http://localhost` leads to ` + * http://argocd.localhost * http://grafana.localhost * http://jenkins.localhost * http://scmm.localhost * http://vault.localhost -Of course, this would also work for production instances with proper domains, see [Deploy Ingresses](./Deploy-Ingress-Controller.md). +Of course, this would also work for production instances with proper domains, +see [Deploy Ingresses](./Deploy-Ingress-Controller.md). All applications are deployed via GitOps and can be found in the `cluster-resources` repository. See [Argo CD](#argo-cd) for more details on the repository structure. - ## Argo CD Argo CD is installed in a production-ready way that allows for operating Argo CD with Argo CD, using GitOps and @@ -43,103 +45,126 @@ When installing the GitOps playground, the following Git repository is created a * cluster-resources – example GitOps repository for a cluster admin or infra / platform team -Argo CD’s own management and configuration, which previously lived in a dedicated `argocd` repository, +Argo CD’s own management and configuration, which previously lived in a dedicated `argocd` repository, is now part of the `cluster-resources` repo under `apps/argocd`: ![example of argocd repo structure](images/argocd.png) ### Bootstrapping Argo CD + When the GitOps playground is installed, Argo CD is bootstrapped as follows: + 1. Argo CD is installed imperatively via a Helm chart. 2. Two resources are applied imperatively to the cluster: - * an `AppProject` called `argocd` - * an `Application` called `bootstrap` + * an `AppProject` called `argocd` + * an `Application` called `bootstrap` Both are stored in the `cluster-resources` repository under `apps/argocd/applications`. From there, everything is managed via GitOps. ### How Argo CD manages itself + The following Argo CD Applications live in `apps/argocd/applications`: * The `bootstrap` application manages the `apps/argocd/applications` folder (including itself). This allows changes to the bootstrap application and further application manifests to be managed via GitOps. -* The `argocd` application manages the folder `apps/argocd/argocd`, which contains Argo CD’s resources as an umbrella Helm chart. - * The umbrella chart pattern allows us to: - * describe configuration in `values.yaml` - * deploy additional resources (such as secrets and ingresses) via the `templates` folder - * `Chart.yaml` declares the Argo CD Helm chart as a dependency. - * `Chart.lock` pins the chart to a deterministic version from the upstream chart repository. - * This mechanism can be used to upgrade Argo CD via GitOps (by updating the chart version and syncing). -* The `projects` application manages the folder apps/argocd/projects, which contains the following `AppProject` resources: - * the `argocd` project (used for bootstrapping) - * the built-in default `project` (restricted for security) - * one project per team, for example: - * `cluster-resources` (platform admins, more cluster permissions) - * `example-apps` (application developers, fewer permissions) +* The `argocd` application manages the folder `apps/argocd/argocd`, which contains Argo CD’s resources as an umbrella + Helm chart. + * The umbrella chart pattern allows us to: + * describe configuration in `values.yaml` + * deploy additional resources (such as secrets and ingresses) via the `templates` folder + * `Chart.yaml` declares the Argo CD Helm chart as a dependency. + * `Chart.lock` pins the chart to a deterministic version from the upstream chart repository. + * This mechanism can be used to upgrade Argo CD via GitOps (by updating the chart version and syncing). +* The `projects` application manages the folder apps/argocd/projects, which contains the following `AppProject` + resources: + * the `argocd` project (used for bootstrapping) + * the built-in default `project` (restricted for security) + * one project per team, for example: + * `cluster-resources` (platform admins, more cluster permissions) + * `example-apps` (application developers, fewer permissions) ### Multi-source applications for features -Feature deployments (for example, monitoring, ingress, or other GOP features) are modeled as multi-source Argo CD Applications instead of using an App-of-Apps pattern. + +Feature deployments (for example, monitoring, ingress, or other GOP features) are modeled as multi-source Argo CD +Applications instead of using an App-of-Apps pattern. For some features, the GitOps Playground Operator (GOP): + 1. Writes values files into the `cluster-resources` repository under: ```powershell apps// -gop-helm.yaml -user-values.yaml ``` - The `*-gop-helm.yaml` file is managed by GOP, while `*-user-values.yaml` is intended for user overrides and is not overwritten. + The `*-gop-helm.yaml` file is managed by GOP, while `*-user-values.yaml` is intended for user overrides and is not + overwritten. 2. Generates an Argo CD `Application` in `apps/argocd/applications/.yaml` that uses two sources: - * Helm source (external chart) - * `repoURL`: the external Helm repository - * `chart` or `path`: the chart to deploy - * `targetRevision`: the chart version - * `helm.valueFiles`: includes the values from the `cluster-resources` repo via `$values/...` - (for example `$values/apps//-gop-helm.yaml` and - `$values/apps//-user-values.yaml`) - * Git source (values and additional manifests) - * `repoURL`: the `cluster-resources` repo - * `targetRevision`: typically `main` - * `ref`: set to `values` so the Helm source can reference `$values/...` - * `path`: `apps/` - * `directory.recurse: true` to pick up additional manifests in the feature folder - - If users create a `misc` subfolder under `apps/` (for example `apps//misc`) and add additional Kubernetes manifests there, these manifests are automatically included and deployed as part of the feature. + * Helm source (external chart) + * `repoURL`: the external Helm repository + * `chart` or `path`: the chart to deploy + * `targetRevision`: the chart version + * `helm.valueFiles`: includes the values from the `cluster-resources` repo via `$values/...` + (for example `$values/apps//-gop-helm.yaml` and + `$values/apps//-user-values.yaml`) + * Git source (values and additional manifests) + * `repoURL`: the `cluster-resources` repo + * `targetRevision`: typically `main` + * `ref`: set to `values` so the Helm source can reference `$values/...` + * `path`: `apps/` + * `directory.recurse: true` to pick up additional manifests in the feature folder + + If users create a `misc` subfolder under `apps/` (for example `apps//misc`) and add + additional Kubernetes manifests there, these manifests are automatically included and deployed as part of the + feature. Argo CD merges these sources, so each feature application is defined by: - * the external Helm chart (versioned and reproducible), and - * Git-managed configuration and manifests in the `cluster-resources` repo. -This multi-source pattern replaces the previous App-of-Apps based approach for feature deployments while still following the repo-per-team model. +* the external Helm chart (versioned and reproducible), and +* Git-managed configuration and manifests in the `cluster-resources` repo. + +This multi-source pattern replaces the previous App-of-Apps based approach for feature deployments while still following +the repo-per-team model. ### Application repo: example-apps -The `example-apps` repository demonstrates how application teams can structure their own GitOps repositories. Its layout looks like this: + +The `example-apps` repository demonstrates how application teams can structure their own GitOps repositories. Its layout +looks like this: ![example of example-apps repo structure](images/example.png) - * The folder `apps/argocd/applications` contains Argo CD `Application` manifests for the example workloads: +* The folder `apps/argocd/applications` contains Argo CD `Application` manifests for the example workloads: * `petclinic-plain.yaml` - * Each application implements the [Environment per App Pattern](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#global-vs-env-per-app):: +* Each application implements + the [Environment per App Pattern](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#global-vs-env-per-app):: * separate folders for `staging`and `production` - * optional `generatedResources/` subfolders where CI pipelines can write generated manifests (for example, templated messages or index files) + * optional `generatedResources/` subfolders where CI pipelines can write generated manifests (for example, templated + messages or index files) For example: -* `apps/spring-petclinic-plain/production` and `apps/spring-petclinic-plain/staging` contain + +* `apps/spring-petclinic-plain/production` and `apps/spring-petclinic-plain/staging` contain plain Kubernetes manifests (`deployment.yaml`, `service.yaml`, `ingress.yaml`) plus generated resources. The `example-apps` repo is thus a reference for how product teams can structure their GitOps repositories while still integrating cleanly with Argo CD and the multi-source pattern used by the platform. -To keep things simpler, the GitOps playground only uses one kubernetes cluster, effectively implementing the [Standalone](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#standalone) -pattern. However, the repo structure could also be used to serve multiple clusters, in a [Hub and Spoke](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#hub-and-spoke) pattern: +To keep things simpler, the GitOps playground only uses one kubernetes cluster, effectively implementing +the [Standalone](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#standalone) +pattern. However, the repo structure could also be used to serve multiple clusters, in +a [Hub and Spoke](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#hub-and-spoke) pattern: Additional clusters could either be defined in the `vaules.yaml` or as secrets via the `templates` folder. -We're also working on an optional implementation of the [namespaced](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#namespaced) pattern, using the [Argo CD operator](https://github.com/argoproj-labs/argocd-operator). +We're also working on an optional implementation of +the [namespaced](https://github.com/cloudogu/gitops-patterns/tree/8e1056f#namespaced) pattern, using +the [Argo CD operator](https://github.com/argoproj-labs/argocd-operator). ### cluster-resources -The playground installs cluster-resources (like prometheus, grafana, vault, external secrets operator, etc.) via the repo +The playground installs cluster-resources (like prometheus, grafana, vault, external secrets operator, etc.) via the +repo `argocd/cluster-resources`. When installing without Argo CD, the tools are installed using helm imperatively. @@ -155,14 +180,17 @@ See [parameters](./Configuration.md) for examples. * `--jenkins-password` The user has to have the following privileges: + * install plugins * set credentials * create jobs * restarting -To apply additional global environments for jenkins you can use `--jenkins-additional-envs "KEY1=value1,KEY2=value2"` parameter. +To apply additional global environments for jenkins you can use `--jenkins-additional-envs "KEY1=value1,KEY2=value2"` +parameter. -Note that the [example applications](#example-applications) pipelines will only run on a Jenkins that uses agents that provide +Note that the [example applications](#example-applications) pipelines will only run on a Jenkins that uses agents that +provide a docker host. That is, Jenkins must be able to run e.g. `docker ps` successfully on the agent. ## SCMs @@ -181,18 +209,22 @@ This group will serve as the main group for the GOP to create and manage all req [![gitlab ParentID](images/gitlab-parentid.png)](https://docs.gitlab.com/user/group/#find-the-group-id) -To authenticate with Gitlab provide a token token as password. More information can be found [here](https://docs.gitlab.com/api/rest/authentication/) or [here](https://docs.gitlab.com/user/profile/personal_access_tokens/) +To authenticate with Gitlab provide a token token as password. More information can be +found [here](https://docs.gitlab.com/api/rest/authentication/) +or [here](https://docs.gitlab.com/user/profile/personal_access_tokens/) The username should remain 'oauth2.0' to access the API, unless stated otherwise by GitLab documentation. + ### SCM-Manager You can set an external SCM-Manager via the following parameters when applying the playground. - See [parameters](./Configuration.md) for examples. +See [parameters](./Configuration.md) for examples. * `--scmm-url`, * `--scmm-username`, * `--scmm-password` The user on the scm has to have privileges to: + * add / edit users * add / edit permissions * add / edit repositories @@ -203,14 +235,14 @@ The user on the scm has to have privileges to: Set the parameter `--monitoring` so the [kube-prometheus-stack](https://github.com/prometheus-operator/kube-prometheus) via its [helm-chart](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack) -is being deployed including dashboards for +is being deployed including dashboards for + - ArgoCD - Traefik Ingress Controller - Prometheus - SCMManager - Jenkins. - Grafana can be used to query and visualize metrics via prometheus. It is exposed via ingress, e.g. http://grafana.localhost. Prometheus is not exposed by default. @@ -229,7 +261,8 @@ action: ![External Secret Operator <-> Vault - flow](https://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/cloudogu/gitops-playground/main/docs/plantuml-src/External-Secret-Operator-Flow.puml&fmt=svg) -For this to work, the GitOps playground configures the whole chain in Kubernetes and vault (when [dev mode](#dev-mode) is used): +For this to work, the GitOps playground configures the whole chain in Kubernetes and vault (when [dev mode](#dev-mode) +is used): ![External Secret Operator Custom Resources](https://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/cloudogu/gitops-playground/main/docs/plantuml-src/External-Secret-Operator-CRs.puml&fmt=svg) @@ -245,6 +278,7 @@ For this to work, the GitOps playground configures the whole chain in Kubernetes ### dev mode For testing you can set the parameter `--vault=dev` to deploy vault in development mode. This will lead to + * vault being transient, i.e. all changes during runtime are not persisted. Meaning a restart will reset to default. * Vault is initialized with some fixed secrets that are used in the example app, see below. * Vault authorization is initialized with service accounts used in example `SecretStore`s for external secrets operator @@ -258,6 +292,7 @@ the namespace `argocd-staging` and `argocd-production` namespaces When using `vault=prod` you'll have to initialize vault manually but on the other hand it will persist changes. If you want the example app to work, you'll have to manually + * set up vault, unseal it and * authorize the `vault` service accounts in `argocd-production` and `argocd-staging` namspaces. See `SecretStore`s and [dev-post-start.sh](../argocd/cluster-resources/apps/vault/templates/dev-post-start.ftl.sh) for an example. @@ -269,8 +304,8 @@ from a developer's point of view. These can be used with the profile "content-examples" They require a registry, so locally use `--registry` or pass in an existing instance using `registry-url`. -The examples very much rely on jenkins. So it is recommended to enable it using `--jenkins` or pass in an existing -instance using `--jenkins-url`. +The examples very much rely on jenkins. So it is recommended to enable it using `--jenkins` or pass in an existing +instance using `--jenkins-url`. The examples include staging and production environments, providing a ready-to-use solution for promotion. @@ -280,21 +315,23 @@ All applications are deployed via separated application and GitOps repos: * Separation of app repo (e.g. `petclinic-plain`) and GitOps repo (e.g. `argocd/example-app`) * Config is maintained in app repo, -* CI Server writes to GitOps repo and creates PullRequests, using the [gitops-build-lib](https://github.com/cloudogu/gitops-build-lib). +* CI Server writes to GitOps repo and creates PullRequests, using + the [gitops-build-lib](https://github.com/cloudogu/gitops-build-lib). The applications implement a simple staging mechanism: * After a successful Jenkins build, the staging application will be deployed into the cluster by ArogCD. * Deployment of production applications can be triggered by accepting pull requests. -* For some applications working without CI Server and committing directly to the GitOps repo is pragmatic +* For some applications working without CI Server and committing directly to the GitOps repo is pragmatic [![app-repo-vs-gitops-repo](images/app-repo-vs-gitops-repo.svg)](https://cdn.jsdelivr.net/gh/cloudogu/gitops-playground@main/docs/images/app-repo-vs-gitops-repo.svg "View full size") Note that the GitOps-related logic is implemented in the [gitops-build-lib](https://github.com/cloudogu/gitops-build-lib) for Jenkins. See the README there for more options like + * staging, * resource creation, -* validation (fail early / shift left). +* validation (fail early / shift left). For further understanding, also take a look at our GitOps pattern repository [cloudogu/gitops-patterns](https://github.com/cloudogu/gitops-patterns?tab=readme-ov-file#gitops-playground) @@ -303,28 +340,31 @@ Please note that it might take about a minute after the pull request has been ac deploying. Alternatively, you can trigger the deployment via ArgoCD's UI or CLI. - We recommend using the `--ingress` and `--base-url` Parameters. With these, the applications are made available as subdomains of `base-url`. -For example, `--base-url=http://localhost` leads to +For example, `--base-url=http://localhost` leads to http://staging.petclinic-plain.petclinic.localhost/. The `.petlinic.` part can be overridden using -`--petclinic-base-domain` (for the petlinic examples/exercises), or +`--petclinic-base-domain` (for the petlinic examples/exercises), or #### PetClinic with plain k8s resources -[Jenkinsfile](https://github.com/cloudogu/gitops-examples/example-apps-via-content-loader/argocd/petclinic-plain/Jenkinsfile.ftl) for `plain` deployment +[Jenkinsfile](https://github.com/cloudogu/gitops-examples/example-apps-via-content-loader/argocd/petclinic-plain/Jenkinsfile.ftl) +for `plain` deployment * Staging: http://staging.petclinic-plain.petclinic.localhost/ * Production: http://production.petclinic-plain.petclinic.localhost/ - Note that you have to accept a [pull request](http://scmm.localhost/scm/repo/argocd/example-apps/pull-requests/) for deployment + Note that you have to accept a [pull request](http://scmm.localhost/scm/repo/argocd/example-apps/pull-requests/) for + deployment #### PetClinic with helm -[Jenkinsfile](https://github.com/cloudogu/gitops-examples/example-apps-via-content-loader/argocd/petclinic-helm/Jenkinsfile.ftl) for `helm` deployment +[Jenkinsfile](https://github.com/cloudogu/gitops-examples/example-apps-via-content-loader/argocd/petclinic-helm/Jenkinsfile.ftl) +for `helm` deployment * Staging: http://staging.petclinic-helm.petclinic.localhost/ * Production: http://production.petclinic-helm.petclinic.localhost/ - Note that you have to accept a [pull request](http://scmm.localhost/scm/repo/argocd/example-apps/pull-requests/) for deployment + Note that you have to accept a [pull request](http://scmm.localhost/scm/repo/argocd/example-apps/pull-requests/) for + deployment \ No newline at end of file diff --git a/docs/Configuration.md b/docs/Configuration.md index 2063bb242..c21e494bf 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,6 +1,7 @@ # Overview of all CLI and config options -All options can be set via a [config file](./configuration.schema.json). Most options are also available as CLI parameters. +All options can be set via a [config file](./configuration.schema.json). Most options are also available as CLI +parameters. ## Table of Contents @@ -20,124 +21,124 @@ All options can be set via a [config file](./configuration.schema.json). Most op ## Registry -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--registry` | `registry.active` | Boolean | `false` | Installs a simple cluster-local registry for demonstration purposes. Warning: Registry does not provide authentication! | -| `--internal-registry-port` | `registry.internalPort` | Integer | `30000` | Port of registry registry. Ignored when a registry*url params are set | -| `--registry-url` | `registry.url` | String | `` | The url of your external registry, used for pushing images | -| `--registry-path` | `registry.path` | String | `` | Optional when registry-url is set | -| `--registry-username` | `registry.username` | String | `` | Optional when registry-url is set | -| `--registry-password` | `registry.password` | String | `` | Optional when registry-url is set | -| `--registry-proxy-url` | `registry.proxyUrl` | String | `` | The url of your proxy-registry. Used in pipelines to authorize pull base images. Use in conjunction with petclinic base image. Used in helm charts when create-image-pull-secrets is set. Use in conjunction with helm.*image fields. | -| `--registry-proxy-path` | `registry.proxyPath` | String | `` | Optional when registry-proxy-url is set and the registry is running on a non root web path. | -| `--registry-proxy-username` | `registry.proxyUsername` | String | `` | Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set. | -| `--registry-proxy-password` | `registry.proxyPassword` | String | `` | Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set. | -| `--registry-username-read-only` | `registry.readOnlyUsername` | String | `` | Optional alternative username for registry-url with read-only permissions that is used when create-image-pull-secrets is set. | -| `--registry-password-read-only` | `registry.readOnlyPassword` | String | `` | Optional alternative password for registry-url with read-only permissions that is used when create-image-pull-secrets is set. | -| `--create-image-pull-secrets` | `registry.createImagePullSecrets` | Boolean | `false` | Create image pull secrets for registry and proxy-registry for all GOP namespaces and helm charts. Uses proxy-username, read-only-username or registry-username (in this order). Use this if your cluster is not auto-provisioned with credentials for your private registries or if you configure individual helm images to be pulled from the proxy-registry that requires authentication. | -| - | `registry.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `registry.helm.chart` | String | `docker-registry` | Name of the Helm chart | -| - | `registry.helm.repoURL` | String | `https://twuni.github.io/docker-registry.helm` | Repository url from which the Helm chart should be obtained | -| - | `registry.helm.version` | String | `3.0.0` | The version of the Helm chart to be installed | +| CLI | Config key | Type | Default | Description | +|:--------------------------------|:----------------------------------|:--------|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--registry` | `registry.active` | Boolean | `false` | Installs a simple cluster-local registry for demonstration purposes. Warning: Registry does not provide authentication! | +| `--internal-registry-port` | `registry.internalPort` | Integer | `30000` | Port of registry registry. Ignored when a registry*url params are set | +| `--registry-url` | `registry.url` | String | `` | The url of your external registry, used for pushing images | +| `--registry-path` | `registry.path` | String | `` | Optional when registry-url is set | +| `--registry-username` | `registry.username` | String | `` | Optional when registry-url is set | +| `--registry-password` | `registry.password` | String | `` | Optional when registry-url is set | +| `--registry-proxy-url` | `registry.proxyUrl` | String | `` | The url of your proxy-registry. Used in pipelines to authorize pull base images. Use in conjunction with petclinic base image. Used in helm charts when create-image-pull-secrets is set. Use in conjunction with helm.*image fields. | +| `--registry-proxy-path` | `registry.proxyPath` | String | `` | Optional when registry-proxy-url is set and the registry is running on a non root web path. | +| `--registry-proxy-username` | `registry.proxyUsername` | String | `` | Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set. | +| `--registry-proxy-password` | `registry.proxyPassword` | String | `` | Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set. | +| `--registry-username-read-only` | `registry.readOnlyUsername` | String | `` | Optional alternative username for registry-url with read-only permissions that is used when create-image-pull-secrets is set. | +| `--registry-password-read-only` | `registry.readOnlyPassword` | String | `` | Optional alternative password for registry-url with read-only permissions that is used when create-image-pull-secrets is set. | +| `--create-image-pull-secrets` | `registry.createImagePullSecrets` | Boolean | `false` | Create image pull secrets for registry and proxy-registry for all GOP namespaces and helm charts. Uses proxy-username, read-only-username or registry-username (in this order). Use this if your cluster is not auto-provisioned with credentials for your private registries or if you configure individual helm images to be pulled from the proxy-registry that requires authentication. | +| - | `registry.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `registry.helm.chart` | String | `docker-registry` | Name of the Helm chart | +| - | `registry.helm.repoURL` | String | `https://twuni.github.io/docker-registry.helm` | Repository url from which the Helm chart should be obtained | +| - | `registry.helm.version` | String | `3.0.0` | The version of the Helm chart to be installed | ## Jenkins -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--jenkins` | `jenkins.active` | Boolean | `false` | Installs Jenkins as CI server | -| `--jenkins-skip-restart` | `jenkins.skipRestart` | Boolean | `false` | Skips restarting Jenkins after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades. | -| `--jenkins-skip-plugins` | `jenkins.skipPlugins` | Boolean | `false` | Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades. | -| `--jenkins-url` | `jenkins.url` | String | `` | The url of your external jenkins | -| `--jenkins-username` | `jenkins.username` | String | `admin` | Mandatory when jenkins-url is set | -| `--jenkins-password` | `jenkins.password` | String | `mK1KDmJOeg6Y` | Mandatory when jenkins-url is set | -| `--jenkins-metrics-username` | `jenkins.metricsUsername` | String | `metrics` | Mandatory when jenkins-url is set and monitoring enabled | -| `--jenkins-metrics-password` | `jenkins.metricsPassword` | String | `metrics` | Mandatory when jenkins-url is set and monitoring enabled | -| `--maven-central-mirror` | `jenkins.mavenCentralMirror` | String | `` | URL for maven mirror, used by applications built in Jenkins | -| `--jenkins-additional-envs` | `jenkins.additionalEnvs` | Map | `[:]` | Set additional environments to Jenkins | -| - | `jenkins.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `jenkins.helm.chart` | String | `jenkins` | Name of the Helm chart | -| - | `jenkins.helm.repoURL` | String | `https://charts.jenkins.io` | Repository url from which the Helm chart should be obtained | -| - | `jenkins.helm.version` | String | `5.9.18` | The version of the Helm chart to be installed | +| CLI | Config key | Type | Default | Description | +|:-----------------------------|:-----------------------------|:--------|:----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--jenkins` | `jenkins.active` | Boolean | `false` | Installs Jenkins as CI server | +| `--jenkins-skip-restart` | `jenkins.skipRestart` | Boolean | `false` | Skips restarting Jenkins after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades. | +| `--jenkins-skip-plugins` | `jenkins.skipPlugins` | Boolean | `false` | Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades. | +| `--jenkins-url` | `jenkins.url` | String | `` | The url of your external jenkins | +| `--jenkins-username` | `jenkins.username` | String | `admin` | Mandatory when jenkins-url is set | +| `--jenkins-password` | `jenkins.password` | String | `mK1KDmJOeg6Y` | Mandatory when jenkins-url is set | +| `--jenkins-metrics-username` | `jenkins.metricsUsername` | String | `metrics` | Mandatory when jenkins-url is set and monitoring enabled | +| `--jenkins-metrics-password` | `jenkins.metricsPassword` | String | `metrics` | Mandatory when jenkins-url is set and monitoring enabled | +| `--maven-central-mirror` | `jenkins.mavenCentralMirror` | String | `` | URL for maven mirror, used by applications built in Jenkins | +| `--jenkins-additional-envs` | `jenkins.additionalEnvs` | Map | `[:]` | Set additional environments to Jenkins | +| - | `jenkins.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `jenkins.helm.chart` | String | `jenkins` | Name of the Helm chart | +| - | `jenkins.helm.repoURL` | String | `https://charts.jenkins.io` | Repository url from which the Helm chart should be obtained | +| - | `jenkins.helm.version` | String | `5.9.18` | The version of the Helm chart to be installed | ## Multi Tenant -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--central-gitlab-url` | `multiTenant.gitlab.url` | String | `-` | URL for external Gitlab | -| `--central-gitlab-username` | `multiTenant.gitlab.username` | String | `-` | GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication | -| `--central-gitlab-token` | `multiTenant.gitlab.password` | String | `-` | Password for SCM Manager authentication | -| `--central-gitlab-group-id` | `multiTenant.gitlab.parentGroupId` | String | `-` | Main Group for Gitlab where the GOP creates it's groups/repos | -| `--central-scmm-internal` | `multiTenant.scmManager.internal` | Boolean | `-` | SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access | -| `--central-scmm-url` | `multiTenant.scmManager.url` | String | `-` | URL for the centralized Management Repo | -| `--central-scmm-username` | `multiTenant.scmManager.username` | String | `-` | CENTRAL SCMM username | -| `--central-scmm-password` | `multiTenant.scmManager.password` | String | `-` | CENTRAL SCMM password | -| `--central-scmm-namespace` | `multiTenant.scmManager.namespace` | String | `-` | Namespace where to find the Central SCMM | -| `--central-argocd-namespace` | `multiTenant.centralArgocdNamespace` | String | `argocd` | Namespace for the centralized Argocd | -| `--dedicated-instance` | `multiTenant.useDedicatedInstance` | Boolean | `false` | Toggles the Dedicated Instances Mode. See docs for more info | +| CLI | Config key | Type | Default | Description | +|:-----------------------------|:-------------------------------------|:--------|:---------|:-------------------------------------------------------------------------------------------------------| +| `--central-gitlab-url` | `multiTenant.gitlab.url` | String | `-` | URL for external Gitlab | +| `--central-gitlab-username` | `multiTenant.gitlab.username` | String | `-` | GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication | +| `--central-gitlab-token` | `multiTenant.gitlab.password` | String | `-` | Password for SCM Manager authentication | +| `--central-gitlab-group-id` | `multiTenant.gitlab.parentGroupId` | String | `-` | Main Group for Gitlab where the GOP creates it's groups/repos | +| `--central-scmm-internal` | `multiTenant.scmManager.internal` | Boolean | `-` | SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access | +| `--central-scmm-url` | `multiTenant.scmManager.url` | String | `-` | URL for the centralized Management Repo | +| `--central-scmm-username` | `multiTenant.scmManager.username` | String | `-` | CENTRAL SCMM username | +| `--central-scmm-password` | `multiTenant.scmManager.password` | String | `-` | CENTRAL SCMM password | +| `--central-scmm-namespace` | `multiTenant.scmManager.namespace` | String | `-` | Namespace where to find the Central SCMM | +| `--central-argocd-namespace` | `multiTenant.centralArgocdNamespace` | String | `argocd` | Namespace for the centralized Argocd | +| `--dedicated-instance` | `multiTenant.useDedicatedInstance` | Boolean | `false` | Toggles the Dedicated Instances Mode. See docs for more info | ## Scm -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| - | `scm.gitlab.internal` | Boolean | `-` | True if Gitlab is running in the same K8s cluster. For now we only support access by external URL | -| `--gitlab-url` | `scm.gitlab.url` | String | `-` | Base URL for the Gitlab instance | -| `--gitlab-username` | `scm.gitlab.username` | String | `-` | Defaults to: oauth2.0 when PAT token is given. | -| `--gitlab-token` | `scm.gitlab.password` | String | `-` | PAT Token for the account. Needs read/write repo permissions. See docs for mor information | -| `--gitlab-group-id` | `scm.gitlab.parentGroupId` | String | `-` | Number for the Gitlab Group where the repos and subgroups should be created | -| - | `scm.gitlab.gitOpsUsername` | String | `-` | Username for the Gitops User | -| `--scmm-url` | `scm.scmManager.url` | String | `-` | The host of your external scm-manager | -| `--scmm-namespace` | `scm.scmManager.namespace` | String | `-` | Namespace where SCM-Manager should run | -| `--scmm-username` | `scm.scmManager.username` | String | `-` | Mandatory when scmm-url is set | -| `--scmm-password` | `scm.scmManager.password` | String | `-` | Mandatory when scmm-url is set | -| - | `scm.scmManager.helm.values` | Map | `-` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `scm.scmManager.helm.chart` | String | `-` | Name of the Helm chart | -| - | `scm.scmManager.helm.repoURL` | String | `-` | Repository url from which the Helm chart should be obtained | -| - | `scm.scmManager.helm.version` | String | `-` | The version of the Helm chart to be installed | -| `--scmm-skip-restart` | `scm.scmManager.skipRestart` | Boolean | `-` | Skips restarting SCM-Manager after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.' | -| `--scmm-skip-plugins` | `scm.scmManager.skipPlugins` | Boolean | `-` | Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades. | -| - | `scm.scmManager.gitOpsUsername` | String | `-` | Username for the Gitops User | +| CLI | Config key | Type | Default | Description | +|:----------------------|:--------------------------------|:--------|:--------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| - | `scm.gitlab.internal` | Boolean | `-` | True if Gitlab is running in the same K8s cluster. For now we only support access by external URL | +| `--gitlab-url` | `scm.gitlab.url` | String | `-` | Base URL for the Gitlab instance | +| `--gitlab-username` | `scm.gitlab.username` | String | `-` | Defaults to: oauth2.0 when PAT token is given. | +| `--gitlab-token` | `scm.gitlab.password` | String | `-` | PAT Token for the account. Needs read/write repo permissions. See docs for mor information | +| `--gitlab-group-id` | `scm.gitlab.parentGroupId` | String | `-` | Number for the Gitlab Group where the repos and subgroups should be created | +| - | `scm.gitlab.gitOpsUsername` | String | `-` | Username for the Gitops User | +| `--scmm-url` | `scm.scmManager.url` | String | `-` | The host of your external scm-manager | +| `--scmm-namespace` | `scm.scmManager.namespace` | String | `-` | Namespace where SCM-Manager should run | +| `--scmm-username` | `scm.scmManager.username` | String | `-` | Mandatory when scmm-url is set | +| `--scmm-password` | `scm.scmManager.password` | String | `-` | Mandatory when scmm-url is set | +| - | `scm.scmManager.helm.values` | Map | `-` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `scm.scmManager.helm.chart` | String | `-` | Name of the Helm chart | +| - | `scm.scmManager.helm.repoURL` | String | `-` | Repository url from which the Helm chart should be obtained | +| - | `scm.scmManager.helm.version` | String | `-` | The version of the Helm chart to be installed | +| `--scmm-skip-restart` | `scm.scmManager.skipRestart` | Boolean | `-` | Skips restarting SCM-Manager after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.' | +| `--scmm-skip-plugins` | `scm.scmManager.skipPlugins` | Boolean | `-` | Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades. | +| - | `scm.scmManager.gitOpsUsername` | String | `-` | Username for the Gitops User | ## Application -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--config-file` | `application.configFiles` | List<String> | `[]` | - | -| `--config-map` | `application.configMaps` | List<String> | `[]` | - | -| `-d`, `--debug` | `application.debug` | Boolean | `-` | - | -| `-x`, `--trace` | `application.trace` | Boolean | `-` | - | -| `--output-config-file` | `application.outputConfigFile` | Boolean | `false` | - | -| `-v`, `--version` | `application.versionInfoRequested` | Boolean | `false` | - | -| `-h`, `--help` | `application.usageHelpRequested` | Boolean | `false` | - | -| `--insecure` | `application.insecure` | Boolean | `false` | Sets insecure-mode in cURL which skips cert validation | -| `--openshift` | `application.openshift` | Boolean | `false` | When set, openshift specific resources and configurations are applied | -| `--username` | `application.username` | String | `admin` | Set initial admin username | -| `--password` | `application.password` | String | `mK1KDmJOeg6Y` | Set initial admin passwords | -| `-y`, `--yes` | `application.yes` | Boolean | `false` | Skip confirmation | -| `--name-prefix` | `application.namePrefix` | String | `` | Set name-prefix for repos, jobs, namespaces | -| `--destroy` | `application.destroy` | Boolean | `false` | Unroll playground | -| `--pod-resources` | `application.podResources` | Boolean | `false` | Write kubernetes resource requests and limits on each pod | -| `--git-name` | `application.gitName` | String | `Cloudogu` | Sets git author and committer name used for initial commits | -| `--git-email` | `application.gitEmail` | String | `hello@cloudogu.com` | Sets git author and committer email used for initial commits | -| `--base-url` | `application.baseUrl` | String | `` | the external base url (TLD) for all tools, e.g. https://example.com or http://localhost:8080. The individual -url params for argocd, grafana and vault take precedence. | -| `--url-separator-hyphen` | `application.urlSeparatorHyphen` | Boolean | `false` | Use hyphens instead of dots to separate application name from base-url | -| `--mirror-repos` | `application.mirrorRepos` | Boolean | `false` | Changes the sources of deployed tools so they are not pulled from the internet, but are pulled from git and work in air-gapped environments. | -| `--skip-crds` | `application.skipCrds` | Boolean | `false` | Skip installation of CRDs. This requires prior installation of CRDs | -| `--namespace-isolation` | `application.namespaceIsolation` | Boolean | `false` | Configure tools to explicitly work with the given namespaces only, and not cluster-wide. This way GOP can be installed without having cluster-admin permissions. | -| `--netpols` | `application.netpols` | Boolean | `false` | Sets Network Policies | -| `--cluster-admin` | `application.clusterAdmin` | Boolean | `false` | Binds ArgoCD controllers to cluster-admin ClusterRole | -| `-p`, `--profile` | `application.profile` | String | `-` | Use predefined profile (full, only-argocd, operator-mandants aso.) | -| `--gop-namespace` | `application.gopNamespace` | String | `` | If set, GOP stores specific information in this namespace. | +| CLI | Config key | Type | Default | Description | +|:-------------------------|:-----------------------------------|:-------------------|:---------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--config-file` | `application.configFiles` | List<String> | `[]` | - | +| `--config-map` | `application.configMaps` | List<String> | `[]` | - | +| `-d`, `--debug` | `application.debug` | Boolean | `-` | - | +| `-x`, `--trace` | `application.trace` | Boolean | `-` | - | +| `--output-config-file` | `application.outputConfigFile` | Boolean | `false` | - | +| `-v`, `--version` | `application.versionInfoRequested` | Boolean | `false` | - | +| `-h`, `--help` | `application.usageHelpRequested` | Boolean | `false` | - | +| `--insecure` | `application.insecure` | Boolean | `false` | Sets insecure-mode in cURL which skips cert validation | +| `--openshift` | `application.openshift` | Boolean | `false` | When set, openshift specific resources and configurations are applied | +| `--username` | `application.username` | String | `admin` | Set initial admin username | +| `--password` | `application.password` | String | `mK1KDmJOeg6Y` | Set initial admin passwords | +| `-y`, `--yes` | `application.yes` | Boolean | `false` | Skip confirmation | +| `--name-prefix` | `application.namePrefix` | String | `` | Set name-prefix for repos, jobs, namespaces | +| `--destroy` | `application.destroy` | Boolean | `false` | Unroll playground | +| `--pod-resources` | `application.podResources` | Boolean | `false` | Write kubernetes resource requests and limits on each pod | +| `--git-name` | `application.gitName` | String | `Cloudogu` | Sets git author and committer name used for initial commits | +| `--git-email` | `application.gitEmail` | String | `hello@cloudogu.com` | Sets git author and committer email used for initial commits | +| `--base-url` | `application.baseUrl` | String | `` | the external base url (TLD) for all tools, e.g. https://example.com or http://localhost:8080. The individual -url params for argocd, grafana and vault take precedence. | +| `--url-separator-hyphen` | `application.urlSeparatorHyphen` | Boolean | `false` | Use hyphens instead of dots to separate application name from base-url | +| `--mirror-repos` | `application.mirrorRepos` | Boolean | `false` | Changes the sources of deployed tools so they are not pulled from the internet, but are pulled from git and work in air-gapped environments. | +| `--skip-crds` | `application.skipCrds` | Boolean | `false` | Skip installation of CRDs. This requires prior installation of CRDs | +| `--namespace-isolation` | `application.namespaceIsolation` | Boolean | `false` | Configure tools to explicitly work with the given namespaces only, and not cluster-wide. This way GOP can be installed without having cluster-admin permissions. | +| `--netpols` | `application.netpols` | Boolean | `false` | Sets Network Policies | +| `--cluster-admin` | `application.clusterAdmin` | Boolean | `false` | Binds ArgoCD controllers to cluster-admin ClusterRole | +| `-p`, `--profile` | `application.profile` | String | `-` | Use predefined profile (full, only-argocd, operator-mandants aso.) | +| `--gop-namespace` | `application.gopNamespace` | String | `` | If set, GOP stores specific information in this namespace. | ## Content -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| - | `content.namespaces` | List<String> | `[]` | Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging | -| - | `content.repos` | List<ContentRepositorySchema> | `[]` | ContentLoader repos to push into target environment | -| - | `content.variables` | Map | `[:]` | Additional variables to use in custom templates. | -| - | `content.helmReleases` | List<HelmReleaseSchema> | `[]` | - | -| `--content-whitelist` | `content.useWhitelist` | Boolean | `false` | Enables the whitelist for statics in content templating | -| - | `content.allowedStaticsWhitelist` | Set<String> | `[]` | Whitelist for Statics freemarker is allowing in user templates | +| CLI | Config key | Type | Default | Description | +|:----------------------|:----------------------------------|:------------------------------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| - | `content.namespaces` | List<String> | `[]` | Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging | +| - | `content.repos` | List<ContentRepositorySchema> | `[]` | ContentLoader repos to push into target environment | +| - | `content.variables` | Map | `[:]` | Additional variables to use in custom templates. | +| - | `content.helmReleases` | List<HelmReleaseSchema> | `[]` | - | +| `--content-whitelist` | `content.useWhitelist` | Boolean | `false` | Enables the whitelist for statics in content templating | +| - | `content.allowedStaticsWhitelist` | Set<String> | `[]` | Whitelist for Statics freemarker is allowing in user templates | ## Features @@ -145,88 +146,87 @@ Configuration of optional features supported by gitops-playground. ### Feature: Argocd -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--argocd` | `features.argocd.active` | Boolean | `false` | Install ArgoCD | -| `--argocd-operator` | `features.argocd.operator` | Boolean | `false` | Install ArgoCD via an already running ArgoCD Operator | -| `--argocd-url` | `features.argocd.url` | String | `` | The URL where argocd is accessible. It has to be the full URL with http:// or https:// | -| - | `features.argocd.env` | List<java.util.Map> | `-` | Pass a list of env vars to Argo CD components. Currently only works with operator | -| `--argocd-email-from` | `features.argocd.emailFrom` | String | `argocd@example.org` | Notifications, define Argo CD sender email address | -| `--argocd-email-to-user` | `features.argocd.emailToUser` | String | `app-team@example.org` | Notifications, define Argo CD user / app-team recipient email address | -| `--argocd-email-to-admin` | `features.argocd.emailToAdmin` | String | `infra@example.org` | Notifications, define Argo CD admin recipient email address | -| `--argocd-resource-inclusions-cluster` | `features.argocd.resourceInclusionsCluster` | String | `` | Internal Kubernetes API Server URL https://IP:PORT (kubernetes.default.svc). Needed in argocd-operator resourceInclusions. Use this parameter if argocd.operator=true and NOT running inside a Pod (remote mode). Full URL needed, for example: https://100.125.0.1:443 | -| `--argocd-namespace` | `features.argocd.namespace` | String | `argocd` | Defines the kubernetes namespace for ArgoCD | -| - | `features.argocd.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| CLI | Config key | Type | Default | Description | +|:---------------------------------------|:--------------------------------------------|:--------------------------------------------------------------|:-----------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--argocd` | `features.argocd.active` | Boolean | `false` | Install ArgoCD | +| `--argocd-operator` | `features.argocd.operator` | Boolean | `false` | Install ArgoCD via an already running ArgoCD Operator | +| `--argocd-url` | `features.argocd.url` | String | `` | The URL where argocd is accessible. It has to be the full URL with http:// or https:// | +| - | `features.argocd.env` | List<java.util.Map> | `-` | Pass a list of env vars to Argo CD components. Currently only works with operator | +| `--argocd-email-from` | `features.argocd.emailFrom` | String | `argocd@example.org` | Notifications, define Argo CD sender email address | +| `--argocd-email-to-user` | `features.argocd.emailToUser` | String | `app-team@example.org` | Notifications, define Argo CD user / app-team recipient email address | +| `--argocd-email-to-admin` | `features.argocd.emailToAdmin` | String | `infra@example.org` | Notifications, define Argo CD admin recipient email address | +| `--argocd-resource-inclusions-cluster` | `features.argocd.resourceInclusionsCluster` | String | `` | Internal Kubernetes API Server URL https://IP:PORT (kubernetes.default.svc). Needed in argocd-operator resourceInclusions. Use this parameter if argocd.operator=true and NOT running inside a Pod (remote mode). Full URL needed, for example: https://100.125.0.1:443 | +| `--argocd-namespace` | `features.argocd.namespace` | String | `argocd` | Defines the kubernetes namespace for ArgoCD | +| - | `features.argocd.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | ### Feature: Mail -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--smtp-address` | `features.mail.smtpAddress` | String | `` | Sets smtp port of external Mailserver | -| `--smtp-port` | `features.mail.smtpPort` | Integer | `-` | Sets smtp port of external Mailserver | -| `--smtp-user` | `features.mail.smtpUser` | String | `` | Sets smtp username for external Mailserver | -| `--smtp-password` | `features.mail.smtpPassword` | String | `` | Sets smtp password of external Mailserver | +| CLI | Config key | Type | Default | Description | +|:------------------|:-----------------------------|:--------|:--------|:-------------------------------------------| +| `--smtp-address` | `features.mail.smtpAddress` | String | `` | Sets smtp port of external Mailserver | +| `--smtp-port` | `features.mail.smtpPort` | Integer | `-` | Sets smtp port of external Mailserver | +| `--smtp-user` | `features.mail.smtpUser` | String | `` | Sets smtp username for external Mailserver | +| `--smtp-password` | `features.mail.smtpPassword` | String | `` | Sets smtp password of external Mailserver | ### Feature: Monitoring -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--metrics`, `--monitoring` | `features.monitoring.active` | Boolean | `false` | Installs the Kube-Prometheus-Stack. This includes Prometheus, the Prometheus operator, Grafana and some extra resources | -| `--grafana-url` | `features.monitoring.grafanaUrl` | String | `` | Sets url for grafana | -| `--grafana-email-from` | `features.monitoring.grafanaEmailFrom` | String | `grafana@example.org` | Notifications, define grafana alerts sender email address | -| `--grafana-email-to` | `features.monitoring.grafanaEmailTo` | String | `infra@example.org` | Notifications, define grafana alerts recipient email address | -| `--grafana-image` | `features.monitoring.helm.grafanaImage` | String | `` | Sets image for grafana | -| `--grafana-sidecar-image` | `features.monitoring.helm.grafanaSidecarImage` | String | `` | Sets image for grafana's sidecar | -| `--prometheus-image` | `features.monitoring.helm.prometheusImage` | String | `` | Sets image for prometheus | -| `--prometheus-operator-image` | `features.monitoring.helm.prometheusOperatorImage` | String | `` | Sets image for prometheus-operator | -| `--prometheus-config-reloader-image` | `features.monitoring.helm.prometheusConfigReloaderImage` | String | `` | Sets image for prometheus-operator's config-reloader | -| - | `features.monitoring.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `features.monitoring.helm.chart` | String | `kube-prometheus-stack` | Name of the Helm chart | -| - | `features.monitoring.helm.repoURL` | String | `https://prometheus-community.github.io/helm-charts` | Repository url from which the Helm chart should be obtained | -| - | `features.monitoring.helm.version` | String | `80.2.2` | The version of the Helm chart to be installed | +| CLI | Config key | Type | Default | Description | +|:-------------------------------------|:---------------------------------------------------------|:--------|:-----------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------| +| `--metrics`, `--monitoring` | `features.monitoring.active` | Boolean | `false` | Installs the Kube-Prometheus-Stack. This includes Prometheus, the Prometheus operator, Grafana and some extra resources | +| `--grafana-url` | `features.monitoring.grafanaUrl` | String | `` | Sets url for grafana | +| `--grafana-email-from` | `features.monitoring.grafanaEmailFrom` | String | `grafana@example.org` | Notifications, define grafana alerts sender email address | +| `--grafana-email-to` | `features.monitoring.grafanaEmailTo` | String | `infra@example.org` | Notifications, define grafana alerts recipient email address | +| `--grafana-image` | `features.monitoring.helm.grafanaImage` | String | `` | Sets image for grafana | +| `--grafana-sidecar-image` | `features.monitoring.helm.grafanaSidecarImage` | String | `` | Sets image for grafana's sidecar | +| `--prometheus-image` | `features.monitoring.helm.prometheusImage` | String | `` | Sets image for prometheus | +| `--prometheus-operator-image` | `features.monitoring.helm.prometheusOperatorImage` | String | `` | Sets image for prometheus-operator | +| `--prometheus-config-reloader-image` | `features.monitoring.helm.prometheusConfigReloaderImage` | String | `` | Sets image for prometheus-operator's config-reloader | +| - | `features.monitoring.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `features.monitoring.helm.chart` | String | `kube-prometheus-stack` | Name of the Helm chart | +| - | `features.monitoring.helm.repoURL` | String | `https://prometheus-community.github.io/helm-charts` | Repository url from which the Helm chart should be obtained | +| - | `features.monitoring.helm.version` | String | `80.2.2` | The version of the Helm chart to be installed | ### Feature: Secrets -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--external-secrets-image` | `features.secrets.externalSecrets.helm.image` | String | `` | Sets image for external secrets operator | -| `--external-secrets-certcontroller-image` | `features.secrets.externalSecrets.helm.certControllerImage` | String | `` | Sets image for external secrets operator's controller | -| `--external-secrets-webhook-image` | `features.secrets.externalSecrets.helm.webhookImage` | String | `` | Sets image for external secrets operator's webhook | -| - | `features.secrets.externalSecrets.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `features.secrets.externalSecrets.helm.chart` | String | `external-secrets` | Name of the Helm chart | -| - | `features.secrets.externalSecrets.helm.repoURL` | String | `https://charts.external-secrets.io` | Repository url from which the Helm chart should be obtained | -| - | `features.secrets.externalSecrets.helm.version` | String | `0.9.16` | The version of the Helm chart to be installed | -| `--vault-url` | `features.secrets.vault.url` | String | `` | Sets url for vault ui | -| `--vault-image` | `features.secrets.vault.helm.image` | String | `` | Sets image for vault | -| - | `features.secrets.vault.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `features.secrets.vault.helm.chart` | String | `vault` | Name of the Helm chart | -| - | `features.secrets.vault.helm.repoURL` | String | `https://helm.releases.hashicorp.com` | Repository url from which the Helm chart should be obtained | -| - | `features.secrets.vault.helm.version` | String | `0.25.0` | The version of the Helm chart to be installed | +| CLI | Config key | Type | Default | Description | +|:------------------------------------------|:------------------------------------------------------------|:-------|:--------------------------------------|:-----------------------------------------------------------------------------------------------------------------------| +| `--external-secrets-image` | `features.secrets.externalSecrets.helm.image` | String | `` | Sets image for external secrets operator | +| `--external-secrets-certcontroller-image` | `features.secrets.externalSecrets.helm.certControllerImage` | String | `` | Sets image for external secrets operator's controller | +| `--external-secrets-webhook-image` | `features.secrets.externalSecrets.helm.webhookImage` | String | `` | Sets image for external secrets operator's webhook | +| - | `features.secrets.externalSecrets.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `features.secrets.externalSecrets.helm.chart` | String | `external-secrets` | Name of the Helm chart | +| - | `features.secrets.externalSecrets.helm.repoURL` | String | `https://charts.external-secrets.io` | Repository url from which the Helm chart should be obtained | +| - | `features.secrets.externalSecrets.helm.version` | String | `0.9.16` | The version of the Helm chart to be installed | +| `--vault-url` | `features.secrets.vault.url` | String | `` | Sets url for vault ui | +| `--vault-image` | `features.secrets.vault.helm.image` | String | `` | Sets image for vault | +| - | `features.secrets.vault.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `features.secrets.vault.helm.chart` | String | `vault` | Name of the Helm chart | +| - | `features.secrets.vault.helm.repoURL` | String | `https://helm.releases.hashicorp.com` | Repository url from which the Helm chart should be obtained | +| - | `features.secrets.vault.helm.version` | String | `0.25.0` | The version of the Helm chart to be installed | ### Feature: Ingress -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--ingress` | `features.ingress.active` | Boolean | `false` | Sets and enables Ingress Controller | -| `--ingress-image` | `features.ingress.helm.image` | String | `` | The image of the Helm chart to be installed | -| - | `features.ingress.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `features.ingress.helm.chart` | String | `traefik` | Name of the Helm chart | -| - | `features.ingress.helm.repoURL` | String | `https://traefik.github.io/charts` | Repository url from which the Helm chart should be obtained | -| - | `features.ingress.helm.version` | String | `39.0.0` | The version of the Helm chart to be installed | +| CLI | Config key | Type | Default | Description | +|:------------------|:--------------------------------|:--------|:-----------------------------------|:-----------------------------------------------------------------------------------------------------------------------| +| `--ingress` | `features.ingress.active` | Boolean | `false` | Sets and enables Ingress Controller | +| `--ingress-image` | `features.ingress.helm.image` | String | `` | The image of the Helm chart to be installed | +| - | `features.ingress.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `features.ingress.helm.chart` | String | `traefik` | Name of the Helm chart | +| - | `features.ingress.helm.repoURL` | String | `https://traefik.github.io/charts` | Repository url from which the Helm chart should be obtained | +| - | `features.ingress.helm.version` | String | `39.0.0` | The version of the Helm chart to be installed | ### Feature: Cert Manager -| CLI | Config key | Type | Default | Description | -| :--- | :--- | :--- | :--- | :--- | -| `--cert-manager` | `features.certManager.active` | Boolean | `false` | Sets and enables Cert Manager | -| `--cert-manager-issuer` | `features.certManager.issuer` | String | `cluster-selfsigned` | Sets and enables Cert Manager | -| `--cert-manager-image` | `features.certManager.helm.image` | String | `` | Sets image for Cert Manager | -| `--cert-manager-webhook-image` | `features.certManager.helm.webhookImage` | String | `` | Sets webhook Image for Cert Manager | -| `--cert-manager-cainjector-image` | `features.certManager.helm.cainjectorImage` | String | `` | Sets cainjector Image for Cert Manager | -| `--cert-manager-acme-solver-image` | `features.certManager.helm.acmeSolverImage` | String | `` | Sets acmeSolver Image for Cert Manager | -| `--cert-manager-startup-api-check-image` | `features.certManager.helm.startupAPICheckImage` | String | `` | Sets startupAPICheck Image for Cert Manager | -| - | `features.certManager.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | -| - | `features.certManager.helm.chart` | String | `cert-manager` | Name of the Helm chart | -| - | `features.certManager.helm.repoURL` | String | `https://charts.jetstack.io` | Repository url from which the Helm chart should be obtained | -| - | `features.certManager.helm.version` | String | `1.19.4` | The version of the Helm chart to be installed | - +| CLI | Config key | Type | Default | Description | +|:-----------------------------------------|:-------------------------------------------------|:--------|:-----------------------------|:-----------------------------------------------------------------------------------------------------------------------| +| `--cert-manager` | `features.certManager.active` | Boolean | `false` | Sets and enables Cert Manager | +| `--cert-manager-issuer` | `features.certManager.issuer` | String | `cluster-selfsigned` | Sets and enables Cert Manager | +| `--cert-manager-image` | `features.certManager.helm.image` | String | `` | Sets image for Cert Manager | +| `--cert-manager-webhook-image` | `features.certManager.helm.webhookImage` | String | `` | Sets webhook Image for Cert Manager | +| `--cert-manager-cainjector-image` | `features.certManager.helm.cainjectorImage` | String | `` | Sets cainjector Image for Cert Manager | +| `--cert-manager-acme-solver-image` | `features.certManager.helm.acmeSolverImage` | String | `` | Sets acmeSolver Image for Cert Manager | +| `--cert-manager-startup-api-check-image` | `features.certManager.helm.startupAPICheckImage` | String | `` | Sets startupAPICheck Image for Cert Manager | +| - | `features.certManager.helm.values` | Map | `[:]` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | +| - | `features.certManager.helm.chart` | String | `cert-manager` | Name of the Helm chart | +| - | `features.certManager.helm.repoURL` | String | `https://charts.jetstack.io` | Repository url from which the Helm chart should be obtained | +| - | `features.certManager.helm.version` | String | `1.19.4` | The version of the Helm chart to be installed | \ No newline at end of file diff --git a/docs/Deploy-Ingress-Controller.md b/docs/Deploy-Ingress-Controller.md index 1cefe47f4..82ff608bb 100644 --- a/docs/Deploy-Ingress-Controller.md +++ b/docs/Deploy-Ingress-Controller.md @@ -1,17 +1,20 @@ # Ingress Controller -In the default installation the GitOps-Playground comes without an Ingress-Controller. +In the default installation the GitOps-Playground comes without an Ingress-Controller. We use Traefik as default Ingress-Controller. It can be enabled via the configfile or parameter `--ingress`. -In order to make use of the ingress controller, it is recommended to use it in conjunction with `--base-url`, which will create `Ingress` objects for all components of the GitOps playground. +In order to make use of the ingress controller, it is recommended to use it in conjunction with `--base-url`, which will +create `Ingress` objects for all components of the GitOps playground. The ingress controller is based on the helm chart [`ingress`](https://traefik.github.io/charts/). -Additional parameters from this chart's values.yaml file can be added to the installation through the gitops-playground [configuration file](./Configuration.md). +Additional parameters from this chart's values.yaml file can be added to the installation through the +gitops-playground [configuration file](./Configuration.md). Example: + ```yaml features: ingress: @@ -21,9 +24,11 @@ features: controller: replicaCount: 4 ``` + In this Example we override the default `controller.replicaCount` (GOP's default is 2). -This config file is merged with precedence over the defaults set by +This config file is merged with precedence over the defaults set by + * [the GOP](../argocd/cluster-resources/apps/ingress/templates/ingress-helm-values.ftl.yaml) and * [the charts itself](https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml). @@ -33,16 +38,20 @@ It is possible to deploy `Ingress` objects for all components. You can either * set a common base url (`--base-url=https://example.com`) or * individual URLS: + ``` --argocd-url https://argocd.example.com --grafana-url https://grafana.example.com --vault-url https://vault.example.com --petclinic-base-domain petclinic.example.com ``` + * or both, where the individual URLs take precedence. -Note: -* `jenkins-url` and `scmm-url` are for external services and do not lead to ingresses, but you can set them via `--base-url` for now. +Note: + +* `jenkins-url` and `scmm-url` are for external services and do not lead to ingresses, but you can set them via + `--base-url` for now. * In order to make use of the `Ingress` you need an ingress controller. If your cluster does not provide one, the Playground can deploy one for you, via the `--ingress` parameter. * For this to work, you need to set an `*.example.com` DNS record to the externalIP of the ingress controller. @@ -52,12 +61,12 @@ like http://argocd-example.com ## Subdomains vs hyphen-separated ingresses -* By default, the ingresses are built as subdomains of `--base-url`. -* You can change this behavior using the parameter `--url-separator-hyphen`. +* By default, the ingresses are built as subdomains of `--base-url`. +* You can change this behavior using the parameter `--url-separator-hyphen`. * With this, hyphens are used instead of dots to separate application name from base URL. -* Examples: - * `--base-url=https://xyz.example.org`: `argocd.xyz.example.org` (default) - * `--base-url=https://xyz.example.org`: `argocd-xyz.example.org` (`--url-separator-hyphen`) +* Examples: + * `--base-url=https://xyz.example.org`: `argocd.xyz.example.org` (default) + * `--base-url=https://xyz.example.org`: `argocd-xyz.example.org` (`--url-separator-hyphen`) * This is useful when you have a wildcard certificate for the TLD, but use a subdomain as base URL. Here, browsers accept the validity only for the first level of subdomains. @@ -65,23 +74,28 @@ like http://argocd-example.com The ingresses can also be used when running the playground on your local machine: -* Ingresses might be easier to remember than arbitrary port numbers and look better in demos +* Ingresses might be easier to remember than arbitrary port numbers and look better in demos * With ingresses, we can execute our [local clusters](./k3d.md) in higher isolation or multiple playgrounds concurrently * Ingresses are required [for running on Windows/Mac](./Running-on-Windows-Mac.md). -To use them locally, +To use them locally, + * init your cluster (`init-cluster.sh`). -* apply your playground with the following parameters - * `--base-url=http://localhost` - * this is possible on Windows (tested on 11), Mac (tested on Ventura) or when using Linux with [systemd-resolved](https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html) (default in Ubuntu, not Debian) - As an alternative, you could add all `*.localhost` entries to your `hosts` file. - Use `kubectl get ingress -A` to get a full list - * Then, you can reach argocd on `http://argocd.localhost`, for example - * `--base-url=http://local.gd` (or `127.0.0.1.nip.io`, `127.0.0.1.sslip.io`, or others) - * This should work for all other machines that have access to the internet without further config - * Then, you can reach argocd on `http://argocd.local.gd`, for example -* Note that when using port 80, the URLs are shorter, but you run into issues because port 80 is regarded as a privileged port. +* apply your playground with the following parameters + * `--base-url=http://localhost` + * this is possible on Windows (tested on 11), Mac (tested on Ventura) or when using Linux + with [systemd-resolved](https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html) ( + default in Ubuntu, not Debian) + As an alternative, you could add all `*.localhost` entries to your `hosts` file. + Use `kubectl get ingress -A` to get a full list + * Then, you can reach argocd on `http://argocd.localhost`, for example + * `--base-url=http://local.gd` (or `127.0.0.1.nip.io`, `127.0.0.1.sslip.io`, or others) + * This should work for all other machines that have access to the internet without further config + * Then, you can reach argocd on `http://argocd.local.gd`, for example +* Note that when using port 80, the URLs are shorter, but you run into issues because port 80 is regarded as a + privileged port. Java applications seem not to be able to reach `localhost:80` or even `127.0.0.1:80` (`NoRouteToHostException`) * You can change the port using `init-cluster.sh --bind-ingress-port=8080`. When you do, make sure to append the same port when applying the playground: `--base-url=http://localhost:8080` -* If your setup requires you to bind to a specific interface, you can just pass it with e.g. `--bind-ingress-port=127.0.0.1:80` +* If your setup requires you to bind to a specific interface, you can just pass it with e.g. + `--bind-ingress-port=127.0.0.1:80` \ No newline at end of file diff --git a/docs/Developers.md b/docs/Developers.md index fccc7d6b5..7290478d8 100644 --- a/docs/Developers.md +++ b/docs/Developers.md @@ -6,10 +6,10 @@ It provides workarounds or solutions for the given issues. ## Disclaimer -The versions listed in this README may not always reflect the most current release. -Please be aware that newer versions may exist. -The versions are also specified in the `Config.groovy` file, so it is recommended to consult that file for the latest version information. - +The versions listed in this README may not always reflect the most current release. +Please be aware that newer versions may exist. +The versions are also specified in the `Config.groovy` file, so it is recommended to consult that file for the latest +version information. ## Table of contents @@ -19,30 +19,30 @@ The versions are also specified in the `Config.groovy` file, so it is recommende - [Prerequisites](#prerequisites) - [Testing](#testing) - - [Unit-Tests](#unit-tests) - - [Integration-Tests](#integration-tests) + - [Unit-Tests](#unit-tests) + - [Integration-Tests](#integration-tests) - [Jenkins plugin installation issues](#jenkins-plugin-installation-issues) - - [Solution](#solution) - - [Updating all plugins](#updating-all-plugins) + - [Solution](#solution) + - [Updating all plugins](#updating-all-plugins) - [Local development](#local-development) - [Testing URL separator hyphens](#testing-url-separator-hyphens) - [External registry for development](#external-registry-for-development) - [Testing two registries](#testing-two-registries) - - [Basic test](#basic-test) - - [Proper test](#proper-test) + - [Basic test](#basic-test) + - [Proper test](#proper-test) - [Testing Network Policies locally](#testing-network-policies-locally) - [Emulate an airgapped environment](#emulate-an-airgapped-environment) - - [Setup cluster](#setup-cluster) - - [Install the playground](#install-the-playground) + - [Setup cluster](#setup-cluster) + - [Install the playground](#install-the-playground) - [Notifications / E-Mail](#notifications--e-mail) - [Troubleshooting](#troubleshooting) - - [Using ingresses locally](#using-ingresses-locally) + - [Using ingresses locally](#using-ingresses-locally) - [Generate schema.json](#generate-schemajson) - [Releasing](#releasing) - [Installing ArgoCD Operator](#installing-argocd-operator) - - [Prerequisites:](#prerequisites) - - [Installation Script](#installation-script) - - [Install ingress manually](#install-ingress-manually) + - [Prerequisites:](#prerequisites) + - [Installation Script](#installation-script) + - [Install ingress manually](#install-ingress-manually) @@ -59,12 +59,13 @@ The versions are also specified in the `Config.groovy` file, so it is recommende - [Golang](https://go.dev/doc/install) (only if you plan to use argo-cd operator) - [yq](https://mikefarah.gitbook.io/yq/) (useful for debugging purposes) -To check if you have all necessary tools installed, run the following command. If you don't see any error messages, you are good to go: +To check if you have all necessary tools installed, run the following command. If you don't see any error messages, you +are good to go: + ```bash java -version && mvn -version && docker version && k3d version && kubectl version && helm version ``` - ## Testing 1. There are integration tests implemented by Junit. Classes marked with 'IT' and the end. @@ -94,6 +95,7 @@ mvn clean test ``` where can be one of: + - full - full-prefix @@ -103,6 +105,7 @@ where can be one of: Note: 'operator-*' profiles requires you to install the argo-cd operator in a fresh cluster _before_ deploying the gop. This can be done by running: + ```bash ./scripts/local/install-argocd-operator.sh ``` @@ -114,27 +117,31 @@ Trying to overcome this issue we pinned all plugins within `scripts/jenkins/plug These pinned plugins get downloaded within the docker build and saved into a folder as `.hpi` files. Later on when configuring jenkins, we upload all the plugin files with the given version. -Turns out it does not completely circumvent this issue. In some cases jenkins updates these plugins automagically (as it seems) when installing the pinned version fails at first or being installed when resolving dependencies. -This again may lead to a broken jenkins, where some of the automatically updated plugins have changes within their dependencies. These dependencies than again are not updated but pinned and may cause issues. +Turns out it does not completely circumvent this issue. In some cases jenkins updates these plugins automagically (as it +seems) when installing the pinned version fails at first or being installed when resolving dependencies. +This again may lead to a broken jenkins, where some of the automatically updated plugins have changes within their +dependencies. These dependencies than again are not updated but pinned and may cause issues. -Since solving this issue may require some additional deep dive into bash scripts we like to get rid of in the future, we decided to give some hints how to easily solve the issue (and keep the plugins list up to date :]) instead of fixing it with tremendous effort. +Since solving this issue may require some additional deep dive into bash scripts we like to get rid of in the future, we +decided to give some hints how to easily solve the issue (and keep the plugins list up to date :]) instead of fixing it +with tremendous effort. ### Solution * Determine the plugins that cause the issue - * inspecting the logs of the jenkins-pod - * jenkins-ui (http://localhost:9090/manage) + * inspecting the logs of the jenkins-pod + * jenkins-ui (http://localhost:9090/manage) ![Jenkins-UI with broken plugins](images/example-plugin-install-fail.png) * Fix conflicts by updating the plugins with compatible versions - * Update all plugin versions via jenkins-ui (http://localhost:9090/pluginManager/) and restart + * Update all plugin versions via jenkins-ui (http://localhost:9090/pluginManager/) and restart ![Jenkins-UI update plugins](images/update-all-plugins.png) * Verify the plugin installation - * Check if jenkins starts up correctly and builds all example pipelines successfully - * verify installation of all plugins via jenkins-ui (http://localhost:9090/script) executing the following command + * Check if jenkins starts up correctly and builds all example pipelines successfully + * verify installation of all plugins via jenkins-ui (http://localhost:9090/script) executing the following command ![Jenkins-UI plugin list](images/get-plugin-list.png) @@ -145,8 +152,8 @@ Jenkins.instance.pluginManager.activePlugins.sort().each { ``` * Share and publish your plugin updates - * Make sure you have updated `plugins.txt` with working versions of the plugins - * commit and push changes to your feature-branch and submit a pr + * Make sure you have updated `plugins.txt` with working versions of the plugins + * commit and push changes to your feature-branch and submit a pr Note that `plugins.txt` contains the whole dependency tree, including transitive plugin dependencies. The bare minimum of plugins that are needed is this: @@ -162,21 +169,24 @@ scm-manager # Used in example builds workflow-aggregator # Pipelines plugin, used in example builds ``` -Note that, when running locally we also need `kubernetes` and `configuration-as-code` but these are contained in [our -jenkins helm image](https://github.com/cloudogu/jenkins-helm-image/blob/5.8.1-1/Dockerfile) (extracted from the +Note that, when running locally we also need `kubernetes` and `configuration-as-code` but these are contained in [our +jenkins helm image](https://github.com/cloudogu/jenkins-helm-image/blob/5.8.1-1/Dockerfile) (extracted from the [corresponding helm chart version](https://github.com/jenkinsci/helm-charts/blob/jenkins-5.8.1/charts/jenkins/values.yaml)). +### Updating all plugins -### Updating all plugins -To get a minimal list of plugins, start an empty jenkins that uses [the base image of our image](https://github.com/cloudogu/jenkins-helm-image/blob/main/Dockerfile): +To get a minimal list of plugins, start an empty jenkins that +uses [the base image of our image](https://github.com/cloudogu/jenkins-helm-image/blob/main/Dockerfile): ```shell docker run --rm -v $RANDOM-tmp-jenkins:/var/jenkins_home jenkins/jenkins:2.479.2-jdk17 ``` + We need a volume to persist the plugins when jenkins restarts. (These can be cleaned up afterwards like so: `docker volume ls -q | grep jenkins | xargs -I {} docker volume rm {}`). Then + * manually install the bare minimum of plugins mentioned above * extract the plugins using the groovy console as mentioned above * Write the output into `plugins.txt` @@ -186,33 +196,35 @@ We should automate this! ## Local development * Run locally - * Run from IDE (allows for easy debugging), works e.g. with IntelliJ IDEA - Note: If you encounter `error=2, No such file or directory`, - it might be necessary to explicitly set your `PATH` in Run Configuration's Environment Section. - * From shell: - Run + * Run from IDE (allows for easy debugging), works e.g. with IntelliJ IDEA + Note: If you encounter `error=2, No such file or directory`, + it might be necessary to explicitly set your `PATH` in Run Configuration's Environment Section. + * From shell: + Run + ```shell + ./mvnw package -DskipTests + ./mvnw exec:java -Dexec.arguments="" + ``` +* Running inside the container: + * Build and run dev Container: ```shell - ./mvnw package -DskipTests - ./mvnw exec:java -Dexec.arguments="" + docker build -t gitops-playground:dev --build-arg ENV=dev --progress=plain --pull . + docker run --rm -it -u $(id -u) -v ~/.config/k3d/kubeconfig-gitops-playground.yaml:/home/.kube/config \ + --net=host gitops-playground:dev #params ``` -* Running inside the container: - * Build and run dev Container: - ```shell - docker build -t gitops-playground:dev --build-arg ENV=dev --progress=plain --pull . - docker run --rm -it -u $(id -u) -v ~/.config/k3d/kubeconfig-gitops-playground.yaml:/home/.kube/config \ - --net=host gitops-playground:dev #params - ``` - * Hint: You can speed up the process by installing the Jenkins plugins from your filesystem, instead of from the internet. - To do so, download the plugins into a folder, then set this folder vie env var: - `JENKINS_PLUGIN_FOLDER=$(pwd) java -classpath .. # See above`. - A working combination of plugins be extracted from the image: - ```bash - id=$(docker create --pull=always ghcr.io/cloudogu/gitops-playground:main) - docker cp $id:/gitops/jenkins-plugins . - docker rm -v $id - ``` + * Hint: You can speed up the process by installing the Jenkins plugins from your filesystem, instead of from the + internet. + To do so, download the plugins into a folder, then set this folder vie env var: + `JENKINS_PLUGIN_FOLDER=$(pwd) java -classpath .. # See above`. + A working combination of plugins be extracted from the image: + ```bash + id=$(docker create --pull=always ghcr.io/cloudogu/gitops-playground:main) + docker cp $id:/gitops/jenkins-plugins . + docker rm -v $id + ``` ## Testing URL separator hyphens + ```bash docker run --rm -t -u $(id -u) \ -v ~/.config/k3d/kubeconfig-gitops-playground.yaml:/home/.kube/config \ @@ -230,24 +242,28 @@ kubectl get --all-namespaces ingress -o json 2> /dev/null | jq -r '.items[] | .s ## External registry for development If you need to emulate an "external", private registry with credentials, then install it like so: + ```bash helm repo add harbor https://helm.goharbor.io helm upgrade -i my-harbor harbor/harbor -f ./scripts/dev/external-registry-values.yaml --version 1.14.2 --namespace harbor --create-namespace ``` Once it's up and running either create your own private project or just set the existing `library` to private: + ```bash curl -X PUT -u admin:Harbor12345 'http://localhost:30002/api/v2.0/projects/1' -H 'Content-Type: application/json' \ --data-raw '{"metadata":{"public":"false", "id":1,"project_id":1}}' ``` Then either import external images like so (requires `skopeo` but no prior pulling or insecure config necessary): + ```bash skopeo copy docker://alpine/kubectl:1.35.4 --dest-creds admin:Harbor12345 --dest-tls-verify=false docker://localhost:30002/library/kubectl:1.35.4 ``` Alternatively, you could push existing images from your docker daemon. -However, this takes longer (pull first) and you'll have to make sure to add `localhost:30002` to `insecure-registries` in `/etc/docker/daemon.json` and restart your docker daemon first. +However, this takes longer (pull first) and you'll have to make sure to add `localhost:30002` to `insecure-registries` +in `/etc/docker/daemon.json` and restart your docker daemon first. ```bash docker login localhost:30002 -u admin -p Harbor12345 @@ -271,6 +287,7 @@ That is, for most helm charts, you'll need to set an individual value. ## Testing two registries ### Basic test + * Start playground once, * then again with these parameters: `--registry-url=localhost:30000 --registry-proxy-url=localhost:30000 --registry-proxy-username=Proxy --registry-proxy-password=Proxy12345` @@ -284,16 +301,18 @@ That is, for most helm charts, you'll need to set an individual value. * Important: Harbor has to be set up after initializing the cluster, but before installing GOP. Otherwise GOP deploys its own registry, leading to port conflicts: `Service "harbor" is invalid: spec.ports[0].nodePort: Invalid value: 30000: provided port is already allocated` -* By default, `docker run` relies on the `gitops-playground:dev` image. +* By default, `docker run` relies on the `gitops-playground:dev` image. **Setup** To set-up harbor with two projects, you can use the target "prepare-two-registries". + ```shell make prepare-two-registries ``` Afer that, deploy GOP with the generated config file: + ```bash # Create a docker container or use an available image from a registry # docker build -t gop:dev . @@ -325,11 +344,14 @@ docker run --rm -t -u $(id -u) \ ## Testing Network Policies locally -The first increment of our `--netpols` feature is intended to be used on openshift and with an external Cloudogu Ecosystem. +The first increment of our `--netpols` feature is intended to be used on openshift and with an external Cloudogu +Ecosystem. That's why we need to initialize our local cluster with some netpols for everything to work. -* The `-jenkins` , `-scm-manager` and `-registry` namespace needs to be accesible from outside the cluster (so GOP apply via `docker run` has access) -* Emulate OpenShift default netPols: allow network communication inside namespaces and access by ingress controller + +* The `-jenkins` , `-scm-manager` and `-registry` namespace needs to be accesible from outside + the cluster (so GOP apply via `docker run` has access) +* Emulate OpenShift default netPols: allow network communication inside namespaces and access by ingress controller After the cluster is initialized and before GOP is applied, do the following: @@ -394,7 +416,8 @@ done Let's set up our local playground to emulate an airgapped env, as some of our customers have. -Note that with approach bellow, the whole k3d cluster is airgapped with one exception: the Jenkins agents can work around this. +Note that with approach bellow, the whole k3d cluster is airgapped with one exception: the Jenkins agents can work +around this. To be able to run the `docker` plugin in Jenkins (in a k3d cluster that only provides containerd) we mount the host's docker socket into the agents. From there it can start containers which are not airgapped. @@ -407,6 +430,7 @@ like images or helm charts. ### Setup cluster You can prepare the airgapped cluster, by calling make with the "prepare-airgappe-cluster" target: + ```bash make prepare-airgapped-cluster ``` @@ -426,6 +450,7 @@ Don't disconnect from the internet yet, because source code, as there are no parameters to do so. So, start the installation and once Argo CD is running, go offline. + ```bash docker run -it -u $(id -u) \ -v ~/.config/k3d/kubeconfig-airgapped-playground.yaml:/home/.kube/config \ @@ -433,7 +458,6 @@ docker run -it -u $(id -u) \ --net=host gitops-playground:latest --config-file=/gop.yaml -x ``` - ## Notifications / E-Mail Notifications are implemented via Mail. @@ -446,7 +470,8 @@ To test with an external mail server, set up the configuration as follows: ``` For testing, an email can be sent via the Grafana UI. -Go to Alerting > Notifications, here at contact Points click on the right side at provisioned email contact on "View contact point" +Go to Alerting > Notifications, here at contact Points click on the right side at provisioned email contact on "View +contact point" Here you can check if the configuration is implemented correctly and fire up a Testmail. For testing Argo CD, just uncomment some of the defaultTriggers in it's values.yaml and it will send a lot of emails. @@ -454,6 +479,7 @@ For testing Argo CD, just uncomment some of the defaultTriggers in it's values.y ## Troubleshooting When stuck in `Pending` this might be due to volumes not being provisioned + ```bash k get pod -n kube-system NAME READY STATUS RESTARTS AGE @@ -486,8 +512,7 @@ argocd argocd-server traefik argocd.local Where opening for example http://argocd.localhost in your browser should work. The `base-domain` parameters lead to URLs in the following schema: -`..`, e.g. - +`..`, e.g. ## Generate schema.json @@ -528,7 +553,8 @@ git checkout main \ For now, please start a Jenkins Build of `main` manually. We might introduce tag builds in our Jenkins organization at a later stage. -A GitHub release containing all merged PRs since the last release is create automatically via a [GitHub action](../.github/workflows/create-release.yml) +A GitHub release containing all merged PRs since the last release is create automatically via +a [GitHub action](../.github/workflows/create-release.yml) ## Installing ArgoCD Operator @@ -538,7 +564,7 @@ This guide provides instructions for developers to install the ArgoCD Operator l Ensure you have the following installed on your system: -- Git: For cloning the repository. +- Git: For cloning the repository. - golang: Version >= 1.24 ### Installation Script @@ -554,11 +580,11 @@ make deploy IMG=quay.io/argoprojlabs/argocd-operator:v0.15.0 ### Install ingress manually -The ArgoCD installed via Operator is namespace isolated and therefor can not deploy an ingress-controller, because of global scoped configurations. +The ArgoCD installed via Operator is namespace isolated and therefor can not deploy an ingress-controller, because of +global scoped configurations. GOP has to be startet with ``` --insecure ``` because of we do not use https locally. We have to install the ingress-controller manually: - ```shell helm upgrade --install traefik traefik/traefik --version 4.12.1 --namespace traefik --create-namespace ``` @@ -569,4 +595,4 @@ If the helm repos are not present or up-to-date: helm repo add traefik https://traefik.github.io/charts helm repo update helm install traefik traefik/traefik --version 39.0.0 -``` +``` \ No newline at end of file diff --git a/docs/Installation.md b/docs/Installation.md index 90df94cc1..c970f349e 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -3,28 +3,31 @@ There a several options for running the GitOps playground * on a local k3d cluster - Works best on Linux, but is possible on [Windows and Mac](./Running-on-Windows-Mac.md). + Works best on Linux, but is possible on [Windows and Mac](./Running-on-Windows-Mac.md). * on a remote k8s cluster * each with the option * to use an external Jenkins, SCM-Manager and registry - (this can be run in production, e.g. with a [Cloudogu Ecosystem](https://cloudogu.com/ecosystem/funktionsweise)) or + (this can be run in production, e.g. with a [Cloudogu Ecosystem](https://cloudogu.com/ecosystem/funktionsweise)) + or * to run everything inside the cluster (for demo only) The diagrams below show an overview of the playground's architecture and three scenarios for running the playground. -For a simpler overview including all optional features such as monitoring and secrets management see intro at the very top. +For a simpler overview including all optional features such as monitoring and secrets management see intro at the very +top. Note that running Jenkins inside the cluster is meant for demo purposes only. The third graphic shows our production scenario with the Cloudogu EcoSystem (CES). Here better security and build performance is achieved using ephemeral Jenkins build agents spawned in the cloud. ### Overview -| Playground on local machine | Production environment with Cloudogu EcoSystem | -|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------| + +| Playground on local machine | Production environment with Cloudogu EcoSystem | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [![Playground on local machine](./images/gitops-playground-local.drawio.svg)](https://cdn.jsdelivr.net/gh/cloudogu/gitops-playground@main/docs/images/gitops-playground-local.drawio.svg "View full size") | [![A possible production environment](./images/gitops-playground-production.drawio.svg)](https://cdn.jsdelivr.net/gh/cloudogu/gitops-playground@main/docs/images/gitops-playground-production.drawio.svg "View full size") | ### Create Cluster -You can apply the GitOps playground to +You can apply the GitOps playground to * a local k3d cluster (see [docs](./k3d.md) or [script](../scripts/init-cluster.sh) for more details): ```shell @@ -32,9 +35,11 @@ You can apply the GitOps playground to https://raw.githubusercontent.com/cloudogu/gitops-playground/main/scripts/init-cluster.sh) ``` * or almost any k8s cluster. - Note that if you want to deploy Jenkins inside the cluster, you either need Docker as container runtime or set Jenkins up to run its build on an agent that provides Docker. + Note that if you want to deploy Jenkins inside the cluster, you either need Docker as container runtime or set Jenkins + up to run its build on an agent that provides Docker. -For the local cluster, you can avoid hitting DockerHub's rate limiting by using a mirror via the `--docker-io-registry-mirror` parameter. +For the local cluster, you can avoid hitting DockerHub's rate limiting by using a mirror via the +`--docker-io-registry-mirror` parameter. For example: @@ -43,13 +48,14 @@ bash <(curl -s \ https://raw.githubusercontent.com/cloudogu/gitops-playground/main/scripts/init-cluster.sh) --docker-io-registry-mirror https://mirror.gcr.io ``` -This parameter is passed on the containerd used by k3d. +This parameter is passed on the containerd used by k3d. In addition, the Jobs run by Jenkins are using the host's Docker daemon. To avoid rate limits there, you might have to configure a mirror there as well. This can be done in the `/etc/docker/daemon.json` or in the config of Docker Desktop. For example: + ```json { "registry-mirrors": ["https://mirror.gcr.io"] @@ -83,12 +89,15 @@ docker run --rm -t -u $(id -u) \ ``` Note: + * `docker pull` in advance makes sure you have the newest image, even if you ran this command before. - Of course, you could also specify a specific [version of the image](https://github.com/cloudogu/gitops-playground/pkgs/container/gitops-playground/versions). + Of course, you could also specify a + specific [version of the image](https://github.com/cloudogu/gitops-playground/pkgs/container/gitops-playground/versions). * Using the host network makes it possible to determine `localhost` and to use k3d's kubeconfig without altering, as it access the API server via a port bound to localhost. * We run as the local user in order to avoid file permission issues with the `kubeconfig-${CLUSTER_NAME}.yaml.` -* If you experience issues and want to access the full log files, use the following command while the container is running: +* If you experience issues and want to access the full log files, use the following command while the container is + running: ```bash docker exec -it \ @@ -122,4 +131,4 @@ kubectl delete clusterrolebinding/gitops-playground-job-executer \ ``` In general `docker run` should work here as well. But GKE, for example, uses gcloud and python in their kubeconfig. -Running inside the cluster avoids these kinds of issues. +Running inside the cluster avoids these kinds of issues. \ No newline at end of file diff --git a/docs/Running-on-Windows-Mac.md b/docs/Running-on-Windows-Mac.md index 7cfb382e4..34338ba9d 100644 --- a/docs/Running-on-Windows-Mac.md +++ b/docs/Running-on-Windows-Mac.md @@ -1,14 +1,20 @@ # Running on Windows or Mac -* In general: We cannot use the `host` network, so it's easiest to access via [ingress controller](./Deploy-Ingress-Controller.md). +* In general: We cannot use the `host` network, so it's easiest to access + via [ingress controller](./Deploy-Ingress-Controller.md). * `--base-url=http://localhost --ingress` should work on both Windows and Mac. -* In case of problems resolving e.g. `jenkins.localhost`, you could try using `--base-url=http://local.gd` or similar, as described in [ingress controller](./Deploy-Ingress-Controller.md#local-ingresses). +* In case of problems resolving e.g. `jenkins.localhost`, you could try using `--base-url=http://local.gd` or similar, + as described in [ingress controller](./Deploy-Ingress-Controller.md#local-ingresses). ## Mac and Windows WSL -On macOS and when using the Windows Subsystem Linux on Windows (WSL), you can just run our [TL;DR command](../README.md#tldr) after installing Docker. +On macOS and when using the Windows Subsystem Linux on Windows (WSL), you can just run +our [TL;DR command](../README.md#tldr) after installing Docker. -For Windows, we recommend using [Windows Subsystem for Linux version 2](https://learn.microsoft.com/en-us/windows/wsl/install#install-wsl-command) (WSL2) with a [native installation of Docker Engine](https://docs.docker.com/engine/install/), because it's easier to set up and less prone to errors. +For Windows, we recommend +using [Windows Subsystem for Linux version 2](https://learn.microsoft.com/en-us/windows/wsl/install#install-wsl-command) ( +WSL2) with a [native installation of Docker Engine](https://docs.docker.com/engine/install/), because it's easier to set +up and less prone to errors. For macOS, please increase the Memory limit in Docker Desktop (for your DockerVM) to be > 10 GB. Recommendation: 16GB. @@ -23,15 +29,17 @@ bash <(curl -s \ # If you want to try all features, you might want to add these params: --mail --monitoring --vault=dev ``` -When you encounter errors with port 80 you might want to use e.g. -* `init-cluster.sh) --bind-ingress-port=8080` and +When you encounter errors with port 80 you might want to use e.g. + +* `init-cluster.sh) --bind-ingress-port=8080` and * `--base-url=http://localhost:8080` instead. ## Windows Docker Desktop * As mentioned in the previous section, we recommend using WSL2 with a native Docker Engine. * If you must, you can also run using Docker Desktop from native Windows console (see bellow) -* However, there seems to be a problem when the Jenkins Jobs running the playground access docker, e.g. +* However, there seems to be a problem when the Jenkins Jobs running the playground access docker, e.g. + ``` $ docker run -t -d -u 0:133 -v ... -e ******** alpine/kubectl:1.35.4 cat docker top e69b92070acf3c1d242f4341eb1fa225cc40b98733b0335f7237a01b4425aff3 -eo pid,comm @@ -39,19 +47,25 @@ process apparently never started in /tmp/gitops-playground-jenkins-agent/workspa (running Jenkins temporarily with -Dorg.jenkinsci.plugins.durabletask.BourneShellScript.LAUNCH_DIAGNOSTICS=true might make the problem clearer) Cannot contact default-1bg7f: java.nio.file.NoSuchFileException: /tmp/gitops-playground-jenkins-agent/workspace/xample-apps_petclinic-plain_main/.configRepoTempDir@tmp/durable-7f109066/output.txt ``` -* In Docker Desktop, it's recommended to use WSL2 as backend. -* Using the Hyper-V backend should also work, but we experienced random `CrashLoopBackoff`s of running pods due to liveness probe timeouts. + +* In Docker Desktop, it's recommended to use WSL2 as backend. +* Using the Hyper-V backend should also work, but we experienced random `CrashLoopBackoff`s of running pods due to + liveness probe timeouts. Same as for macOS, increasing the Memory limit in Docker Desktop (for your DockerVM) to be > 10 GB might help. Recommendation: 16GB. Here is how you can start the playground from a Windows-native PowerShell console: -* [Install k3d](https://k3d.io/stable/#releases), see [init-cluster.sh](../scripts/init-cluster.sh) for `K3D_VERSION`, e.g. using `winget` +* [Install k3d](https://k3d.io/stable/#releases), see [init-cluster.sh](../scripts/init-cluster.sh) for `K3D_VERSION`, + e.g. using `winget` + ```powershell winget install k3d --version x.y.z ``` + * Create k3d cluster. - See `K3S_VERSION` in [init-cluster.sh](../scripts/init-cluster.sh) for `$image`, then execute + See `K3S_VERSION` in [init-cluster.sh](../scripts/init-cluster.sh) for `$image`, then execute + ```powershell $ingress_port = "80" $registry_port = "30000" @@ -69,18 +83,21 @@ k3d cluster create gitops-playground ` # Write $HOME/.config/k3d/kubeconfig-gitops-playground.yaml k3d kubeconfig write gitops-playground ``` + * Note that - * You can ignore the warning about docker.sock - * We're mounting the docker socket, so it can be used by the Jenkins Agents for the docker-plugin. - * Windows seems not to provide a group id for the docker socket. So the Jenkins Agents run as root user. - * If you prefer running with an unprivileged user, consider running on WSL2, Mac or Linux - * You could also add `-v gitops-playground-build-cache:/tmp@server:0 ` to persist the Cache of the Jenkins agent between restarts of k3d containers. + * You can ignore the warning about docker.sock + * We're mounting the docker socket, so it can be used by the Jenkins Agents for the docker-plugin. + * Windows seems not to provide a group id for the docker socket. So the Jenkins Agents run as root user. + * If you prefer running with an unprivileged user, consider running on WSL2, Mac or Linux + * You could also add `-v gitops-playground-build-cache:/tmp@server:0 ` to persist the Cache of the Jenkins agent + between restarts of k3d containers. * Apply playground: - Note that when using a `$registry_port` other than `30000` append the command `--internal-registry-port=$registry_port` bellow - + Note that when using a `$registry_port` other than `30000` append the command + `--internal-registry-port=$registry_port` bellow + ```powershell docker run --rm -t --pull=always ` -v $HOME/.config/k3d/kubeconfig-gitops-playground.yaml:/home/.kube/config ` --net=host ` ghcr.io/cloudogu/gitops-playground --yes --argocd --ingress --base-url=http://localhost:$ingress_port # more params go here -``` +``` \ No newline at end of file diff --git a/docs/configuration.schema.json b/docs/configuration.schema.json index cb53e6bca..70422642d 100644 --- a/docs/configuration.schema.json +++ b/docs/configuration.schema.json @@ -1,897 +1,1436 @@ { - "$schema" : "https://json-schema.org/draft/2020-12/schema", - "$defs" : { - "HelmConfigWithValues-nullable" : { - "type" : [ "object", "null" ], - "properties" : { - "chart" : { - "type" : [ "string", "null" ], - "description" : "Name of the Helm chart" - }, - "repoURL" : { - "type" : [ "string", "null" ], - "description" : "Repository url from which the Helm chart should be obtained" - }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" - }, - "version" : { - "type" : [ "string", "null" ], - "description" : "The version of the Helm chart to be installed" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "HelmConfigWithValues-nullable": { + "type": [ + "object", + "null" + ], + "properties": { + "chart": { + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" + }, + "repoURL": { + "type": [ + "string", + "null" + ], + "description": "Repository url from which the Helm chart should be obtained" + }, + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + }, + "version": { + "type": [ + "string", + "null" + ], + "description": "The version of the Helm chart to be installed" } }, - "additionalProperties" : false + "additionalProperties": false }, - "Map(String,Object)-nullable" : { - "type" : [ "object", "null" ] + "Map(String,Object)-nullable": { + "type": [ + "object", + "null" + ] }, - "Map(String,String)" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" + "Map(String,String)": { + "type": "object", + "additionalProperties": { + "type": "string" } }, - "ScmProviderType-nullable" : { - "anyOf" : [ { - "type" : "null" - }, { - "type" : "string", - "enum" : [ "GITLAB", "SCM_MANAGER" ] - } ] + "ScmProviderType-nullable": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "GITLAB", + "SCM_MANAGER" + ] + } + ] } }, - "type" : "object", - "properties" : { - "application" : { - "type" : [ "object", "null" ], - "properties" : { - "baseUrl" : { - "type" : [ "string", "null" ], - "description" : "the external base url (TLD) for all tools, e.g. https://example.com or http://localhost:8080. The individual -url params for argocd, grafana and vault take precedence." - }, - "clusterAdmin" : { - "type" : [ "boolean", "null" ], - "description" : "Binds ArgoCD controllers to cluster-admin ClusterRole" - }, - "destroy" : { - "type" : [ "boolean", "null" ], - "description" : "Unroll playground" - }, - "gitEmail" : { - "type" : [ "string", "null" ], - "description" : "Sets git author and committer email used for initial commits" - }, - "gitName" : { - "type" : [ "string", "null" ], - "description" : "Sets git author and committer name used for initial commits" - }, - "gopNamespace" : { - "type" : [ "string", "null" ], - "description" : "If set, GOP stores specific information in this namespace." - }, - "insecure" : { - "type" : [ "boolean", "null" ], - "description" : "Sets insecure-mode in cURL which skips cert validation" - }, - "mirrorRepos" : { - "type" : [ "boolean", "null" ], - "description" : "Changes the sources of deployed tools so they are not pulled from the internet, but are pulled from git and work in air-gapped environments." - }, - "namePrefix" : { - "type" : [ "string", "null" ], - "description" : "Set name-prefix for repos, jobs, namespaces" - }, - "namespaceIsolation" : { - "type" : [ "boolean", "null" ], - "description" : "Configure tools to explicitly work with the given namespaces only, and not cluster-wide. This way GOP can be installed without having cluster-admin permissions." - }, - "netpols" : { - "type" : [ "boolean", "null" ], - "description" : "Sets Network Policies" - }, - "openshift" : { - "type" : [ "boolean", "null" ], - "description" : "When set, openshift specific resources and configurations are applied" - }, - "password" : { - "type" : [ "string", "null" ], - "description" : "Set initial admin passwords" - }, - "podResources" : { - "type" : [ "boolean", "null" ], - "description" : "Write kubernetes resource requests and limits on each pod" - }, - "profile" : { - "type" : [ "string", "null" ], - "description" : "Use predefined profile (full, only-argocd, operator-mandants aso.)" - }, - "skipCrds" : { - "type" : [ "boolean", "null" ], - "description" : "Skip installation of CRDs. This requires prior installation of CRDs" - }, - "urlSeparatorHyphen" : { - "type" : [ "boolean", "null" ], - "description" : "Use hyphens instead of dots to separate application name from base-url" - }, - "username" : { - "type" : [ "string", "null" ], - "description" : "Set initial admin username" - }, - "yes" : { - "type" : [ "boolean", "null" ], - "description" : "Skip confirmation" + "type": "object", + "properties": { + "application": { + "type": [ + "object", + "null" + ], + "properties": { + "baseUrl": { + "type": [ + "string", + "null" + ], + "description": "the external base url (TLD) for all tools, e.g. https://example.com or http://localhost:8080. The individual -url params for argocd, grafana and vault take precedence." + }, + "clusterAdmin": { + "type": [ + "boolean", + "null" + ], + "description": "Binds ArgoCD controllers to cluster-admin ClusterRole" + }, + "destroy": { + "type": [ + "boolean", + "null" + ], + "description": "Unroll playground" + }, + "gitEmail": { + "type": [ + "string", + "null" + ], + "description": "Sets git author and committer email used for initial commits" + }, + "gitName": { + "type": [ + "string", + "null" + ], + "description": "Sets git author and committer name used for initial commits" + }, + "gopNamespace": { + "type": [ + "string", + "null" + ], + "description": "If set, GOP stores specific information in this namespace." + }, + "insecure": { + "type": [ + "boolean", + "null" + ], + "description": "Sets insecure-mode in cURL which skips cert validation" + }, + "mirrorRepos": { + "type": [ + "boolean", + "null" + ], + "description": "Changes the sources of deployed tools so they are not pulled from the internet, but are pulled from git and work in air-gapped environments." + }, + "namePrefix": { + "type": [ + "string", + "null" + ], + "description": "Set name-prefix for repos, jobs, namespaces" + }, + "namespaceIsolation": { + "type": [ + "boolean", + "null" + ], + "description": "Configure tools to explicitly work with the given namespaces only, and not cluster-wide. This way GOP can be installed without having cluster-admin permissions." + }, + "netpols": { + "type": [ + "boolean", + "null" + ], + "description": "Sets Network Policies" + }, + "openshift": { + "type": [ + "boolean", + "null" + ], + "description": "When set, openshift specific resources and configurations are applied" + }, + "password": { + "type": [ + "string", + "null" + ], + "description": "Set initial admin passwords" + }, + "podResources": { + "type": [ + "boolean", + "null" + ], + "description": "Write kubernetes resource requests and limits on each pod" + }, + "profile": { + "type": [ + "string", + "null" + ], + "description": "Use predefined profile (full, only-argocd, operator-mandants aso.)" + }, + "skipCrds": { + "type": [ + "boolean", + "null" + ], + "description": "Skip installation of CRDs. This requires prior installation of CRDs" + }, + "urlSeparatorHyphen": { + "type": [ + "boolean", + "null" + ], + "description": "Use hyphens instead of dots to separate application name from base-url" + }, + "username": { + "type": [ + "string", + "null" + ], + "description": "Set initial admin username" + }, + "yes": { + "type": [ + "boolean", + "null" + ], + "description": "Skip confirmation" } }, - "additionalProperties" : false, - "description" : "Application configuration parameter for GOP" + "additionalProperties": false, + "description": "Application configuration parameter for GOP" }, - "content" : { - "type" : [ "object", "null" ], - "properties" : { - "allowedStaticsWhitelist" : { - "description" : "Whitelist for Statics freemarker is allowing in user templates", - "type" : [ "array", "null" ], - "items" : { - "type" : "string" + "content": { + "type": [ + "object", + "null" + ], + "properties": { + "allowedStaticsWhitelist": { + "description": "Whitelist for Statics freemarker is allowing in user templates", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" } }, - "helmReleases" : { - "description" : "", - "type" : [ "array", "null" ], - "items" : { - "type" : "object", - "properties" : { - "chart" : { - "type" : [ "string", "null" ], - "description" : "Helm chart name to install. For HTTP(S) repos this is the chart name from the repo index; for OCI this is the chart artifact name." + "helmReleases": { + "description": "", + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "chart": { + "type": [ + "string", + "null" + ], + "description": "Helm chart name to install. For HTTP(S) repos this is the chart name from the repo index; for OCI this is the chart artifact name." }, - "name" : { - "type" : [ "string", "null" ], - "description" : "Logical name of the Helm release. Used as the feature folder name under 'apps/' and as default for 'releaseName' if not set." + "name": { + "type": [ + "string", + "null" + ], + "description": "Logical name of the Helm release. Used as the feature folder name under 'apps/' and as default for 'releaseName' if not set." }, - "namespace" : { - "type" : [ "string", "null" ], - "description" : "Kubernetes namespace to deploy the release into." + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Kubernetes namespace to deploy the release into." }, - "releaseName" : { - "type" : [ "string", "null" ], - "description" : "Helm release name. If empty, the value of 'name' is used." + "releaseName": { + "type": [ + "string", + "null" + ], + "description": "Helm release name. If empty, the value of 'name' is used." }, - "repoURL" : { - "type" : [ "string", "null" ], - "description" : "Helm repository URL to fetch the chart from. Use an HTTP(S) Helm repo (must provide an index.yaml) or an OCI registry URL (oci://...)." + "repoURL": { + "type": [ + "string", + "null" + ], + "description": "Helm repository URL to fetch the chart from. Use an HTTP(S) Helm repo (must provide an index.yaml) or an OCI registry URL (oci://...)." }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Optional inline Helm values. These values are merged on top of 'valuesFile' (if set) and override keys from the file. Use this for small overrides without maintaining a separate file." + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Optional inline Helm values. These values are merged on top of 'valuesFile' (if set) and override keys from the file. Use this for small overrides without maintaining a separate file." }, - "valuesPath" : { - "type" : [ "string", "null" ], - "description" : "Optional path to a YAML values file to load Helm values from.The file must be accessible locally on the machine running GOP. Inline 'values' will be merged on top (inline overrides file)." + "valuesPath": { + "type": [ + "string", + "null" + ], + "description": "Optional path to a YAML values file to load Helm values from.The file must be accessible locally on the machine running GOP. Inline 'values' will be merged on top (inline overrides file)." }, - "version" : { - "type" : [ "string", "null" ], - "description" : "Chart version to deploy. Required for Helm charts in Argo CD. For HTTP(S) Helm repos you may use a SemVer range like '*' to always pick the newest version. For OCI registries, specify an explicit version/tag." + "version": { + "type": [ + "string", + "null" + ], + "description": "Chart version to deploy. Required for Helm charts in Argo CD. For HTTP(S) Helm repos you may use a SemVer range like '*' to always pick the newest version. For OCI registries, specify an explicit version/tag." } }, - "additionalProperties" : false + "additionalProperties": false } }, - "namespaces" : { - "description" : "Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging", - "type" : [ "array", "null" ], - "items" : { - "type" : "string" + "namespaces": { + "description": "Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" } }, - "repos" : { - "description" : "ContentLoader repos to push into target environment", - "type" : [ "array", "null" ], - "items" : { - "type" : "object", - "properties" : { - "createJenkinsJob" : { - "type" : [ "boolean", "null" ], - "description" : "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches." + "repos": { + "description": "ContentLoader repos to push into target environment", + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "createJenkinsJob": { + "type": [ + "boolean", + "null" + ], + "description": "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches." }, - "credentials" : { - "type" : [ "object", "null" ], - "properties" : { - "passwordKey" : { - "type" : [ "string", "null" ], - "description" : "Credentials Object to authenticate against content repo. Allows using a K8s Secret" + "credentials": { + "type": [ + "object", + "null" + ], + "properties": { + "passwordKey": { + "type": [ + "string", + "null" + ], + "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" }, - "secretName" : { - "type" : [ "string", "null" ], - "description" : "Credentials Object to authenticate against content repo. Allows using a K8s Secret" + "secretName": { + "type": [ + "string", + "null" + ], + "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" }, - "secretNamespace" : { - "type" : [ "string", "null" ], - "description" : "Credentials Object to authenticate against content repo. Allows using a K8s Secret" + "secretNamespace": { + "type": [ + "string", + "null" + ], + "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" }, - "username" : { - "type" : [ "string", "null" ], - "description" : "Credentials Object to authenticate against content repo. Allows using a K8s Secret" + "username": { + "type": [ + "string", + "null" + ], + "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" }, - "usernameKey" : { - "type" : [ "string", "null" ], - "description" : "Credentials Object to authenticate against content repo. Allows using a K8s Secret" + "usernameKey": { + "type": [ + "string", + "null" + ], + "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" } }, - "additionalProperties" : false, - "description" : "Credentials Object to authenticate against content repo. Allows using a K8s Secret" + "additionalProperties": false, + "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" }, - "overwriteMode" : { - "anyOf" : [ { - "type" : "null" - }, { - "type" : "string", - "enum" : [ "INIT", "RESET", "UPGRADE" ] - } ], - "description" : "This defines, how customer repos will be updated.\nINIT - push only if repo does not exist.\nRESET - delete all files after cloning source - files not in content are deleted\nUPGRADE - clone and copy - existing files will be overwritten, files not in content are kept. For type: MIRROR reset and upgrade have same result: in both cases source repo will be force pushed to target repo." + "overwriteMode": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "INIT", + "RESET", + "UPGRADE" + ] + } + ], + "description": "This defines, how customer repos will be updated.\nINIT - push only if repo does not exist.\nRESET - delete all files after cloning source - files not in content are deleted\nUPGRADE - clone and copy - existing files will be overwritten, files not in content are kept. For type: MIRROR reset and upgrade have same result: in both cases source repo will be force pushed to target repo." }, - "path" : { - "type" : [ "string", "null" ], - "description" : "Path within the content repo to process" + "path": { + "type": [ + "string", + "null" + ], + "description": "Path within the content repo to process" }, - "ref" : { - "type" : [ "string", "null" ], - "description" : "Reference for a specific branch, tag, or commit. Emtpy defaults to default branch of the repo. With type MIRROR: ref must not be a commit hash; Choosing a ref only mirrors the ref but does not delete other branches/tags!" + "ref": { + "type": [ + "string", + "null" + ], + "description": "Reference for a specific branch, tag, or commit. Emtpy defaults to default branch of the repo. With type MIRROR: ref must not be a commit hash; Choosing a ref only mirrors the ref but does not delete other branches/tags!" }, - "target" : { - "type" : [ "string", "null" ], - "description" : "Target repo for the repository in the for of namespace/name. Must contain one slash to separate namespace from name." + "target": { + "type": [ + "string", + "null" + ], + "description": "Target repo for the repository in the for of namespace/name. Must contain one slash to separate namespace from name." }, - "targetRef" : { - "type" : [ "string", "null" ], - "description" : "Reference for a specific branch or tag in the target repo of a MIRROR or COPY repo. If ref is a tag, targetRef is treated as tag as well. Except: targetRef is full ref like refs/heads/my-branch or refs/tags/my-tag. Empty defaults to the source ref." + "targetRef": { + "type": [ + "string", + "null" + ], + "description": "Reference for a specific branch or tag in the target repo of a MIRROR or COPY repo. If ref is a tag, targetRef is treated as tag as well. Except: targetRef is full ref like refs/heads/my-branch or refs/tags/my-tag. Empty defaults to the source ref." }, - "templating" : { - "type" : [ "boolean", "null" ], - "description" : "When true, template all files ending in .ftl within the repo" + "templating": { + "type": [ + "boolean", + "null" + ], + "description": "When true, template all files ending in .ftl within the repo" }, - "type" : { - "anyOf" : [ { - "type" : "null" - }, { - "type" : "string", - "enum" : [ "FOLDER_BASED", "COPY", "MIRROR" ] - } ], - "description" : "ContentLoader Repos can either be:\ncopied (only the files, starting on ref, starting at path within the repo. Requires target)\n, mirrored (FORCE pushes ref or the whole git repo if no ref set). Requires target, does not allow path and template.)\nfolderBased (folder structure is interpreted as repos. That is, root folder becomes namespace in SCM, sub folders become repository names in SCM, files are copied. Requires target.)" + "type": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "FOLDER_BASED", + "COPY", + "MIRROR" + ] + } + ], + "description": "ContentLoader Repos can either be:\ncopied (only the files, starting on ref, starting at path within the repo. Requires target)\n, mirrored (FORCE pushes ref or the whole git repo if no ref set). Requires target, does not allow path and template.)\nfolderBased (folder structure is interpreted as repos. That is, root folder becomes namespace in SCM, sub folders become repository names in SCM, files are copied. Requires target.)" }, - "url" : { - "type" : [ "string", "null" ], - "description" : "URL of the content repo. Mandatory for each type." + "url": { + "type": [ + "string", + "null" + ], + "description": "URL of the content repo. Mandatory for each type." } }, - "additionalProperties" : false + "additionalProperties": false } }, - "useWhitelist" : { - "type" : [ "boolean", "null" ], - "description" : "Enables the whitelist for statics in content templating" + "useWhitelist": { + "type": [ + "boolean", + "null" + ], + "description": "Enables the whitelist for statics in content templating" }, - "variables" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Additional variables to use in custom templates." + "variables": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Additional variables to use in custom templates." } }, - "additionalProperties" : false, - "description" : "Config parameters for content, i.e. end-user or tenant applications as opposed to cluster-resources" + "additionalProperties": false, + "description": "Config parameters for content, i.e. end-user or tenant applications as opposed to cluster-resources" }, - "features" : { - "type" : [ "object", "null" ], - "properties" : { - "argocd" : { - "type" : [ "object", "null" ], - "properties" : { - "active" : { - "type" : [ "boolean", "null" ], - "description" : "Install ArgoCD" - }, - "emailFrom" : { - "type" : [ "string", "null" ], - "description" : "Notifications, define Argo CD sender email address" - }, - "emailToAdmin" : { - "type" : [ "string", "null" ], - "description" : "Notifications, define Argo CD admin recipient email address" - }, - "emailToUser" : { - "type" : [ "string", "null" ], - "description" : "Notifications, define Argo CD user / app-team recipient email address" - }, - "env" : { - "description" : "Pass a list of env vars to Argo CD components. Currently only works with operator", - "type" : [ "array", "null" ], - "items" : { - "$ref" : "#/$defs/Map(String,String)", - "additionalProperties" : { - "type" : "string" + "features": { + "type": [ + "object", + "null" + ], + "properties": { + "argocd": { + "type": [ + "object", + "null" + ], + "properties": { + "active": { + "type": [ + "boolean", + "null" + ], + "description": "Install ArgoCD" + }, + "emailFrom": { + "type": [ + "string", + "null" + ], + "description": "Notifications, define Argo CD sender email address" + }, + "emailToAdmin": { + "type": [ + "string", + "null" + ], + "description": "Notifications, define Argo CD admin recipient email address" + }, + "emailToUser": { + "type": [ + "string", + "null" + ], + "description": "Notifications, define Argo CD user / app-team recipient email address" + }, + "env": { + "description": "Pass a list of env vars to Argo CD components. Currently only works with operator", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Map(String,String)", + "additionalProperties": { + "type": "string" } } }, - "namespace" : { - "type" : [ "string", "null" ], - "description" : "Defines the kubernetes namespace for ArgoCD" + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Defines the kubernetes namespace for ArgoCD" }, - "operator" : { - "type" : [ "boolean", "null" ], - "description" : "Install ArgoCD via an already running ArgoCD Operator" + "operator": { + "type": [ + "boolean", + "null" + ], + "description": "Install ArgoCD via an already running ArgoCD Operator" }, - "resourceInclusionsCluster" : { - "type" : [ "string", "null" ], - "description" : "Internal Kubernetes API Server URL https://IP:PORT (kubernetes.default.svc). Needed in argocd-operator resourceInclusions. Use this parameter if argocd.operator=true and NOT running inside a Pod (remote mode). Full URL needed, for example: https://100.125.0.1:443" + "resourceInclusionsCluster": { + "type": [ + "string", + "null" + ], + "description": "Internal Kubernetes API Server URL https://IP:PORT (kubernetes.default.svc). Needed in argocd-operator resourceInclusions. Use this parameter if argocd.operator=true and NOT running inside a Pod (remote mode). Full URL needed, for example: https://100.125.0.1:443" }, - "url" : { - "type" : [ "string", "null" ], - "description" : "The URL where argocd is accessible. It has to be the full URL with http:// or https://" + "url": { + "type": [ + "string", + "null" + ], + "description": "The URL where argocd is accessible. It has to be the full URL with http:// or https://" }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" } }, - "additionalProperties" : false, - "description" : "Config Parameter for the ArgoCD Operator" - }, - "certManager" : { - "type" : [ "object", "null" ], - "properties" : { - "active" : { - "type" : [ "boolean", "null" ], - "description" : "Sets and enables Cert Manager" - }, - "helm" : { - "type" : [ "object", "null" ], - "properties" : { - "acmeSolverImage" : { - "type" : [ "string", "null" ], - "description" : "Sets acmeSolver Image for Cert Manager" + "additionalProperties": false, + "description": "Config Parameter for the ArgoCD Operator" + }, + "certManager": { + "type": [ + "object", + "null" + ], + "properties": { + "active": { + "type": [ + "boolean", + "null" + ], + "description": "Sets and enables Cert Manager" + }, + "helm": { + "type": [ + "object", + "null" + ], + "properties": { + "acmeSolverImage": { + "type": [ + "string", + "null" + ], + "description": "Sets acmeSolver Image for Cert Manager" }, - "cainjectorImage" : { - "type" : [ "string", "null" ], - "description" : "Sets cainjector Image for Cert Manager" + "cainjectorImage": { + "type": [ + "string", + "null" + ], + "description": "Sets cainjector Image for Cert Manager" }, - "chart" : { - "type" : [ "string", "null" ], - "description" : "Name of the Helm chart" + "chart": { + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" }, - "image" : { - "type" : [ "string", "null" ], - "description" : "Sets image for Cert Manager" + "image": { + "type": [ + "string", + "null" + ], + "description": "Sets image for Cert Manager" }, - "repoURL" : { - "type" : [ "string", "null" ], - "description" : "Repository url from which the Helm chart should be obtained" + "repoURL": { + "type": [ + "string", + "null" + ], + "description": "Repository url from which the Helm chart should be obtained" }, - "startupAPICheckImage" : { - "type" : [ "string", "null" ], - "description" : "Sets startupAPICheck Image for Cert Manager" + "startupAPICheckImage": { + "type": [ + "string", + "null" + ], + "description": "Sets startupAPICheck Image for Cert Manager" }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" }, - "version" : { - "type" : [ "string", "null" ], - "description" : "The version of the Helm chart to be installed" + "version": { + "type": [ + "string", + "null" + ], + "description": "The version of the Helm chart to be installed" }, - "webhookImage" : { - "type" : [ "string", "null" ], - "description" : "Sets webhook Image for Cert Manager" + "webhookImage": { + "type": [ + "string", + "null" + ], + "description": "Sets webhook Image for Cert Manager" } }, - "additionalProperties" : false, - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + "additionalProperties": false, + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." }, - "issuer" : { - "type" : [ "string", "null" ], - "description" : "Sets and enables Cert Manager" + "issuer": { + "type": [ + "string", + "null" + ], + "description": "Sets and enables Cert Manager" } }, - "additionalProperties" : false, - "description" : "Config parameters for the Cert Manager" - }, - "ingress" : { - "type" : [ "object", "null" ], - "properties" : { - "active" : { - "type" : [ "boolean", "null" ], - "description" : "Sets and enables Ingress Controller" - }, - "helm" : { - "type" : [ "object", "null" ], - "properties" : { - "chart" : { - "type" : [ "string", "null" ], - "description" : "Name of the Helm chart" + "additionalProperties": false, + "description": "Config parameters for the Cert Manager" + }, + "ingress": { + "type": [ + "object", + "null" + ], + "properties": { + "active": { + "type": [ + "boolean", + "null" + ], + "description": "Sets and enables Ingress Controller" + }, + "helm": { + "type": [ + "object", + "null" + ], + "properties": { + "chart": { + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" }, - "image" : { - "type" : [ "string", "null" ], - "description" : "The image of the Helm chart to be installed" + "image": { + "type": [ + "string", + "null" + ], + "description": "The image of the Helm chart to be installed" }, - "repoURL" : { - "type" : [ "string", "null" ], - "description" : "Repository url from which the Helm chart should be obtained" + "repoURL": { + "type": [ + "string", + "null" + ], + "description": "Repository url from which the Helm chart should be obtained" }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" }, - "version" : { - "type" : [ "string", "null" ], - "description" : "The version of the Helm chart to be installed" + "version": { + "type": [ + "string", + "null" + ], + "description": "The version of the Helm chart to be installed" } }, - "additionalProperties" : false, - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + "additionalProperties": false, + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." } }, - "additionalProperties" : false, - "description" : "Config parameters for the Ingress Controller" - }, - "mail" : { - "type" : [ "object", "null" ], - "properties" : { - "smtpAddress" : { - "type" : [ "string", "null" ], - "description" : "Sets smtp port of external Mailserver" - }, - "smtpPassword" : { - "type" : [ "string", "null" ], - "description" : "Sets smtp password of external Mailserver" - }, - "smtpPort" : { - "type" : [ "integer", "null" ], - "description" : "Sets smtp port of external Mailserver" - }, - "smtpUser" : { - "type" : [ "string", "null" ], - "description" : "Sets smtp username for external Mailserver" + "additionalProperties": false, + "description": "Config parameters for the Ingress Controller" + }, + "mail": { + "type": [ + "object", + "null" + ], + "properties": { + "smtpAddress": { + "type": [ + "string", + "null" + ], + "description": "Sets smtp port of external Mailserver" + }, + "smtpPassword": { + "type": [ + "string", + "null" + ], + "description": "Sets smtp password of external Mailserver" + }, + "smtpPort": { + "type": [ + "integer", + "null" + ], + "description": "Sets smtp port of external Mailserver" + }, + "smtpUser": { + "type": [ + "string", + "null" + ], + "description": "Sets smtp username for external Mailserver" } }, - "additionalProperties" : false, - "description" : "Config parameters for mail servers" - }, - "monitoring" : { - "type" : [ "object", "null" ], - "properties" : { - "active" : { - "type" : [ "boolean", "null" ], - "description" : "Installs the Kube-Prometheus-Stack. This includes Prometheus, the Prometheus operator, Grafana and some extra resources" - }, - "grafanaEmailFrom" : { - "type" : [ "string", "null" ], - "description" : "Notifications, define grafana alerts sender email address" - }, - "grafanaEmailTo" : { - "type" : [ "string", "null" ], - "description" : "Notifications, define grafana alerts recipient email address" - }, - "grafanaUrl" : { - "type" : [ "string", "null" ], - "description" : "Sets url for grafana" - }, - "helm" : { - "type" : [ "object", "null" ], - "properties" : { - "chart" : { - "type" : [ "string", "null" ], - "description" : "Name of the Helm chart" + "additionalProperties": false, + "description": "Config parameters for mail servers" + }, + "monitoring": { + "type": [ + "object", + "null" + ], + "properties": { + "active": { + "type": [ + "boolean", + "null" + ], + "description": "Installs the Kube-Prometheus-Stack. This includes Prometheus, the Prometheus operator, Grafana and some extra resources" + }, + "grafanaEmailFrom": { + "type": [ + "string", + "null" + ], + "description": "Notifications, define grafana alerts sender email address" + }, + "grafanaEmailTo": { + "type": [ + "string", + "null" + ], + "description": "Notifications, define grafana alerts recipient email address" + }, + "grafanaUrl": { + "type": [ + "string", + "null" + ], + "description": "Sets url for grafana" + }, + "helm": { + "type": [ + "object", + "null" + ], + "properties": { + "chart": { + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" }, - "grafanaImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for grafana" + "grafanaImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for grafana" }, - "grafanaSidecarImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for grafana's sidecar" + "grafanaSidecarImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for grafana's sidecar" }, - "prometheusConfigReloaderImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for prometheus-operator's config-reloader" + "prometheusConfigReloaderImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for prometheus-operator's config-reloader" }, - "prometheusImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for prometheus" + "prometheusImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for prometheus" }, - "prometheusOperatorImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for prometheus-operator" + "prometheusOperatorImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for prometheus-operator" }, - "repoURL" : { - "type" : [ "string", "null" ], - "description" : "Repository url from which the Helm chart should be obtained" + "repoURL": { + "type": [ + "string", + "null" + ], + "description": "Repository url from which the Helm chart should be obtained" }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" }, - "version" : { - "type" : [ "string", "null" ], - "description" : "The version of the Helm chart to be installed" + "version": { + "type": [ + "string", + "null" + ], + "description": "The version of the Helm chart to be installed" } }, - "additionalProperties" : false, - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + "additionalProperties": false, + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." } }, - "additionalProperties" : false, - "description" : "Config parameters for the Monitoring system (prometheus)" - }, - "secrets" : { - "type" : [ "object", "null" ], - "properties" : { - "externalSecrets" : { - "type" : [ "object", "null" ], - "properties" : { - "helm" : { - "type" : [ "object", "null" ], - "properties" : { - "certControllerImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for external secrets operator's controller" + "additionalProperties": false, + "description": "Config parameters for the Monitoring system (prometheus)" + }, + "secrets": { + "type": [ + "object", + "null" + ], + "properties": { + "externalSecrets": { + "type": [ + "object", + "null" + ], + "properties": { + "helm": { + "type": [ + "object", + "null" + ], + "properties": { + "certControllerImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for external secrets operator's controller" }, - "chart" : { - "type" : [ "string", "null" ], - "description" : "Name of the Helm chart" + "chart": { + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" }, - "image" : { - "type" : [ "string", "null" ], - "description" : "Sets image for external secrets operator" + "image": { + "type": [ + "string", + "null" + ], + "description": "Sets image for external secrets operator" }, - "repoURL" : { - "type" : [ "string", "null" ], - "description" : "Repository url from which the Helm chart should be obtained" + "repoURL": { + "type": [ + "string", + "null" + ], + "description": "Repository url from which the Helm chart should be obtained" }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" }, - "version" : { - "type" : [ "string", "null" ], - "description" : "The version of the Helm chart to be installed" + "version": { + "type": [ + "string", + "null" + ], + "description": "The version of the Helm chart to be installed" }, - "webhookImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for external secrets operator's webhook" + "webhookImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for external secrets operator's webhook" } }, - "additionalProperties" : false, - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + "additionalProperties": false, + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." } }, - "additionalProperties" : false, - "description" : "Config parameters for the external secrets operator" - }, - "vault" : { - "type" : [ "object", "null" ], - "properties" : { - "helm" : { - "type" : [ "object", "null" ], - "properties" : { - "chart" : { - "type" : [ "string", "null" ], - "description" : "Name of the Helm chart" + "additionalProperties": false, + "description": "Config parameters for the external secrets operator" + }, + "vault": { + "type": [ + "object", + "null" + ], + "properties": { + "helm": { + "type": [ + "object", + "null" + ], + "properties": { + "chart": { + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" }, - "image" : { - "type" : [ "string", "null" ], - "description" : "Sets image for vault" + "image": { + "type": [ + "string", + "null" + ], + "description": "Sets image for vault" }, - "repoURL" : { - "type" : [ "string", "null" ], - "description" : "Repository url from which the Helm chart should be obtained" + "repoURL": { + "type": [ + "string", + "null" + ], + "description": "Repository url from which the Helm chart should be obtained" }, - "values" : { - "$ref" : "#/$defs/Map(String,Object)-nullable", - "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + "values": { + "$ref": "#/$defs/Map(String,Object)-nullable", + "description": "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" }, - "version" : { - "type" : [ "string", "null" ], - "description" : "The version of the Helm chart to be installed" + "version": { + "type": [ + "string", + "null" + ], + "description": "The version of the Helm chart to be installed" } }, - "additionalProperties" : false, - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + "additionalProperties": false, + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." }, - "mode" : { - "anyOf" : [ { - "type" : "null" - }, { - "type" : "string", - "enum" : [ "dev", "prod" ] - } ], - "description" : "Installs Hashicorp vault and the external secrets operator. Possible values: dev, prod." + "mode": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "dev", + "prod" + ] + } + ], + "description": "Installs Hashicorp vault and the external secrets operator. Possible values: dev, prod." }, - "url" : { - "type" : [ "string", "null" ], - "description" : "Sets url for vault ui" + "url": { + "type": [ + "string", + "null" + ], + "description": "Sets url for vault ui" } }, - "additionalProperties" : false, - "description" : "Config parameters for the secrets-vault" + "additionalProperties": false, + "description": "Config parameters for the secrets-vault" } }, - "additionalProperties" : false, - "description" : "Config parameters for the secrets management" + "additionalProperties": false, + "description": "Config parameters for the secrets management" } }, - "additionalProperties" : false, - "description" : "Config parameters for features or tools" + "additionalProperties": false, + "description": "Config parameters for features or tools" }, - "jenkins" : { - "type" : [ "object", "null" ], - "properties" : { - "active" : { - "type" : [ "boolean", "null" ], - "description" : "Installs Jenkins as CI server" - }, - "additionalEnvs" : { - "anyOf" : [ { - "type" : "null" - }, { - "$ref" : "#/$defs/Map(String,String)" - } ], - "description" : "Set additional environments to Jenkins", - "additionalProperties" : { - "type" : "string" + "jenkins": { + "type": [ + "object", + "null" + ], + "properties": { + "active": { + "type": [ + "boolean", + "null" + ], + "description": "Installs Jenkins as CI server" + }, + "additionalEnvs": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/Map(String,String)" + } + ], + "description": "Set additional environments to Jenkins", + "additionalProperties": { + "type": "string" } }, - "helm" : { - "$ref" : "#/$defs/HelmConfigWithValues-nullable", - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + "helm": { + "$ref": "#/$defs/HelmConfigWithValues-nullable", + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." }, - "mavenCentralMirror" : { - "type" : [ "string", "null" ], - "description" : "URL for maven mirror, used by applications built in Jenkins" + "mavenCentralMirror": { + "type": [ + "string", + "null" + ], + "description": "URL for maven mirror, used by applications built in Jenkins" }, - "metricsPassword" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when jenkins-url is set and monitoring enabled" + "metricsPassword": { + "type": [ + "string", + "null" + ], + "description": "Mandatory when jenkins-url is set and monitoring enabled" }, - "metricsUsername" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when jenkins-url is set and monitoring enabled" + "metricsUsername": { + "type": [ + "string", + "null" + ], + "description": "Mandatory when jenkins-url is set and monitoring enabled" }, - "password" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when jenkins-url is set" + "password": { + "type": [ + "string", + "null" + ], + "description": "Mandatory when jenkins-url is set" }, - "skipPlugins" : { - "type" : [ "boolean", "null" ], - "description" : "Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades." + "skipPlugins": { + "type": [ + "boolean", + "null" + ], + "description": "Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades." }, - "skipRestart" : { - "type" : [ "boolean", "null" ], - "description" : "Skips restarting Jenkins after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades." + "skipRestart": { + "type": [ + "boolean", + "null" + ], + "description": "Skips restarting Jenkins after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades." }, - "url" : { - "type" : [ "string", "null" ], - "description" : "The url of your external jenkins" + "url": { + "type": [ + "string", + "null" + ], + "description": "The url of your external jenkins" }, - "username" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when jenkins-url is set" + "username": { + "type": [ + "string", + "null" + ], + "description": "Mandatory when jenkins-url is set" } }, - "additionalProperties" : false, - "description" : "Config parameters for Jenkins CI/CD Pipeline Server" + "additionalProperties": false, + "description": "Config parameters for Jenkins CI/CD Pipeline Server" }, - "multiTenant" : { - "type" : [ "object", "null" ], - "properties" : { - "centralArgocdNamespace" : { - "type" : [ "string", "null" ], - "description" : "Namespace for the centralized Argocd" - }, - "gitlab" : { - "type" : [ "object", "null" ], - "properties" : { - "parentGroupId" : { - "type" : [ "string", "null" ], - "description" : "Main Group for Gitlab where the GOP creates it's groups/repos" - }, - "password" : { - "type" : [ "string", "null" ], - "description" : "Password for SCM Manager authentication" - }, - "url" : { - "type" : [ "string", "null" ], - "description" : "URL for external Gitlab" - }, - "username" : { - "type" : [ "string", "null" ], - "description" : "GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication" + "multiTenant": { + "type": [ + "object", + "null" + ], + "properties": { + "centralArgocdNamespace": { + "type": [ + "string", + "null" + ], + "description": "Namespace for the centralized Argocd" + }, + "gitlab": { + "type": [ + "object", + "null" + ], + "properties": { + "parentGroupId": { + "type": [ + "string", + "null" + ], + "description": "Main Group for Gitlab where the GOP creates it's groups/repos" + }, + "password": { + "type": [ + "string", + "null" + ], + "description": "Password for SCM Manager authentication" + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "URL for external Gitlab" + }, + "username": { + "type": [ + "string", + "null" + ], + "description": "GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication" } }, - "additionalProperties" : false, - "description" : "Config for GITLAB" - }, - "scmManager" : { - "type" : [ "object", "null" ], - "properties" : { - "internal" : { - "type" : [ "boolean", "null" ], - "description" : "SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access" - }, - "namespace" : { - "type" : [ "string", "null" ], - "description" : "Namespace where to find the Central SCMM" - }, - "password" : { - "type" : [ "string", "null" ], - "description" : "CENTRAL SCMM password" - }, - "url" : { - "type" : [ "string", "null" ], - "description" : "URL for the centralized Management Repo" - }, - "username" : { - "type" : [ "string", "null" ], - "description" : "CENTRAL SCMM username" + "additionalProperties": false, + "description": "Config for GITLAB" + }, + "scmManager": { + "type": [ + "object", + "null" + ], + "properties": { + "internal": { + "type": [ + "boolean", + "null" + ], + "description": "SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access" + }, + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Namespace where to find the Central SCMM" + }, + "password": { + "type": [ + "string", + "null" + ], + "description": "CENTRAL SCMM password" + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "URL for the centralized Management Repo" + }, + "username": { + "type": [ + "string", + "null" + ], + "description": "CENTRAL SCMM username" } }, - "additionalProperties" : false, - "description" : "Config for GITLAB" + "additionalProperties": false, + "description": "Config for GITLAB" }, - "scmProviderType" : { - "$ref" : "#/$defs/ScmProviderType-nullable", - "description" : "The SCM provider type. Possible values: SCM_MANAGER, GITLAB" + "scmProviderType": { + "$ref": "#/$defs/ScmProviderType-nullable", + "description": "The SCM provider type. Possible values: SCM_MANAGER, GITLAB" }, - "useDedicatedInstance" : { - "type" : [ "boolean", "null" ], - "description" : "Toggles the Dedicated Instances Mode. See docs for more info" + "useDedicatedInstance": { + "type": [ + "boolean", + "null" + ], + "description": "Toggles the Dedicated Instances Mode. See docs for more info" } }, - "additionalProperties" : false, - "description" : "Multi Tenant Configs" + "additionalProperties": false, + "description": "Multi Tenant Configs" }, - "registry" : { - "type" : [ "object", "null" ], - "properties" : { - "active" : { - "type" : [ "boolean", "null" ], - "description" : "Installs a simple cluster-local registry for demonstration purposes. Warning: Registry does not provide authentication!" - }, - "createImagePullSecrets" : { - "type" : [ "boolean", "null" ], - "description" : "Create image pull secrets for registry and proxy-registry for all GOP namespaces and helm charts. Uses proxy-username, read-only-username or registry-username (in this order). Use this if your cluster is not auto-provisioned with credentials for your private registries or if you configure individual helm images to be pulled from the proxy-registry that requires authentication." - }, - "helm" : { - "$ref" : "#/$defs/HelmConfigWithValues-nullable", - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." - }, - "internalPort" : { - "type" : [ "integer", "null" ], - "description" : "Port of registry registry. Ignored when a registry*url params are set" - }, - "password" : { - "type" : [ "string", "null" ], - "description" : "Optional when registry-url is set" - }, - "path" : { - "type" : [ "string", "null" ], - "description" : "Optional when registry-url is set" - }, - "proxyPassword" : { - "type" : [ "string", "null" ], - "description" : "Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set." - }, - "proxyPath" : { - "type" : [ "string", "null" ], - "description" : "Optional when registry-proxy-url is set and the registry is running on a non root web path." - }, - "proxyUrl" : { - "type" : [ "string", "null" ], - "description" : "The url of your proxy-registry. Used in pipelines to authorize pull base images. Use in conjunction with petclinic base image. Used in helm charts when create-image-pull-secrets is set. Use in conjunction with helm.*image fields." - }, - "proxyUsername" : { - "type" : [ "string", "null" ], - "description" : "Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set." - }, - "readOnlyPassword" : { - "type" : [ "string", "null" ], - "description" : "Optional alternative password for registry-url with read-only permissions that is used when create-image-pull-secrets is set." - }, - "readOnlyUsername" : { - "type" : [ "string", "null" ], - "description" : "Optional alternative username for registry-url with read-only permissions that is used when create-image-pull-secrets is set." - }, - "url" : { - "type" : [ "string", "null" ], - "description" : "The url of your external registry, used for pushing images" - }, - "username" : { - "type" : [ "string", "null" ], - "description" : "Optional when registry-url is set" + "registry": { + "type": [ + "object", + "null" + ], + "properties": { + "active": { + "type": [ + "boolean", + "null" + ], + "description": "Installs a simple cluster-local registry for demonstration purposes. Warning: Registry does not provide authentication!" + }, + "createImagePullSecrets": { + "type": [ + "boolean", + "null" + ], + "description": "Create image pull secrets for registry and proxy-registry for all GOP namespaces and helm charts. Uses proxy-username, read-only-username or registry-username (in this order). Use this if your cluster is not auto-provisioned with credentials for your private registries or if you configure individual helm images to be pulled from the proxy-registry that requires authentication." + }, + "helm": { + "$ref": "#/$defs/HelmConfigWithValues-nullable", + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + }, + "internalPort": { + "type": [ + "integer", + "null" + ], + "description": "Port of registry registry. Ignored when a registry*url params are set" + }, + "password": { + "type": [ + "string", + "null" + ], + "description": "Optional when registry-url is set" + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "Optional when registry-url is set" + }, + "proxyPassword": { + "type": [ + "string", + "null" + ], + "description": "Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set." + }, + "proxyPath": { + "type": [ + "string", + "null" + ], + "description": "Optional when registry-proxy-url is set and the registry is running on a non root web path." + }, + "proxyUrl": { + "type": [ + "string", + "null" + ], + "description": "The url of your proxy-registry. Used in pipelines to authorize pull base images. Use in conjunction with petclinic base image. Used in helm charts when create-image-pull-secrets is set. Use in conjunction with helm.*image fields." + }, + "proxyUsername": { + "type": [ + "string", + "null" + ], + "description": "Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set." + }, + "readOnlyPassword": { + "type": [ + "string", + "null" + ], + "description": "Optional alternative password for registry-url with read-only permissions that is used when create-image-pull-secrets is set." + }, + "readOnlyUsername": { + "type": [ + "string", + "null" + ], + "description": "Optional alternative username for registry-url with read-only permissions that is used when create-image-pull-secrets is set." + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "The url of your external registry, used for pushing images" + }, + "username": { + "type": [ + "string", + "null" + ], + "description": "Optional when registry-url is set" } }, - "additionalProperties" : false, - "description" : "Config parameters for Registry" + "additionalProperties": false, + "description": "Config parameters for Registry" }, - "scm" : { - "type" : [ "object", "null" ], - "properties" : { - "gitlab" : { - "type" : [ "object", "null" ], - "properties" : { - "gitOpsUsername" : { - "type" : [ "string", "null" ], - "description" : "Username for the Gitops User" - }, - "internal" : { - "type" : [ "boolean", "null" ], - "description" : "True if Gitlab is running in the same K8s cluster. For now we only support access by external URL" - }, - "parentGroupId" : { - "type" : [ "string", "null" ], - "description" : "Number for the Gitlab Group where the repos and subgroups should be created" - }, - "password" : { - "type" : [ "string", "null" ], - "description" : "PAT Token for the account. Needs read/write repo permissions. See docs for mor information" - }, - "url" : { - "type" : [ "string", "null" ], - "description" : "Base URL for the Gitlab instance" - }, - "username" : { - "type" : [ "string", "null" ], - "description" : "Defaults to: oauth2.0 when PAT token is given." + "scm": { + "type": [ + "object", + "null" + ], + "properties": { + "gitlab": { + "type": [ + "object", + "null" + ], + "properties": { + "gitOpsUsername": { + "type": [ + "string", + "null" + ], + "description": "Username for the Gitops User" + }, + "internal": { + "type": [ + "boolean", + "null" + ], + "description": "True if Gitlab is running in the same K8s cluster. For now we only support access by external URL" + }, + "parentGroupId": { + "type": [ + "string", + "null" + ], + "description": "Number for the Gitlab Group where the repos and subgroups should be created" + }, + "password": { + "type": [ + "string", + "null" + ], + "description": "PAT Token for the account. Needs read/write repo permissions. See docs for mor information" + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "Base URL for the Gitlab instance" + }, + "username": { + "type": [ + "string", + "null" + ], + "description": "Defaults to: oauth2.0 when PAT token is given." } }, - "additionalProperties" : false, - "description" : "Config for GITLAB" - }, - "scmManager" : { - "type" : [ "object", "null" ], - "properties" : { - "gitOpsUsername" : { - "type" : [ "string", "null" ], - "description" : "Username for the Gitops User" - }, - "helm" : { - "$ref" : "#/$defs/HelmConfigWithValues-nullable", - "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." - }, - "namespace" : { - "type" : [ "string", "null" ], - "description" : "Namespace where SCM-Manager should run" - }, - "password" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when scmm-url is set" - }, - "skipPlugins" : { - "type" : [ "boolean", "null" ], - "description" : "Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades." - }, - "skipRestart" : { - "type" : [ "boolean", "null" ], - "description" : "Skips restarting SCM-Manager after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'" - }, - "url" : { - "type" : [ "string", "null" ], - "description" : "The host of your external scm-manager" - }, - "username" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when scmm-url is set" + "additionalProperties": false, + "description": "Config for GITLAB" + }, + "scmManager": { + "type": [ + "object", + "null" + ], + "properties": { + "gitOpsUsername": { + "type": [ + "string", + "null" + ], + "description": "Username for the Gitops User" + }, + "helm": { + "$ref": "#/$defs/HelmConfigWithValues-nullable", + "description": "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + }, + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Namespace where SCM-Manager should run" + }, + "password": { + "type": [ + "string", + "null" + ], + "description": "Mandatory when scmm-url is set" + }, + "skipPlugins": { + "type": [ + "boolean", + "null" + ], + "description": "Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades." + }, + "skipRestart": { + "type": [ + "boolean", + "null" + ], + "description": "Skips restarting SCM-Manager after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'" + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "The host of your external scm-manager" + }, + "username": { + "type": [ + "string", + "null" + ], + "description": "Mandatory when scmm-url is set" } }, - "additionalProperties" : false, - "description" : "Config for GITLAB" + "additionalProperties": false, + "description": "Config for GITLAB" }, - "scmProviderType" : { - "$ref" : "#/$defs/ScmProviderType-nullable", - "description" : "The SCM provider type. Possible values: SCM_MANAGER, GITLAB" + "scmProviderType": { + "$ref": "#/$defs/ScmProviderType-nullable", + "description": "The SCM provider type. Possible values: SCM_MANAGER, GITLAB" } }, - "additionalProperties" : false, - "description" : "Config parameters for Scm" + "additionalProperties": false, + "description": "Config parameters for Scm" } }, - "additionalProperties" : false + "additionalProperties": false } \ No newline at end of file diff --git a/docs/content-loader/content-loader.md b/docs/content-loader/content-loader.md index f23db1610..25f4c3aa1 100644 --- a/docs/content-loader/content-loader.md +++ b/docs/content-loader/content-loader.md @@ -1,10 +1,10 @@ # Content loader Documentation -This documentation shows the content loader feature and its usage. +This documentation shows the content loader feature and its usage. -Content loader offers the ability of hooking into the GitOps Playground (GOP) installation process, allowing for +Content loader offers the ability of hooking into the GitOps Playground (GOP) installation process, allowing for customization of what is pushed to Git. -This can be used to deploy your own content, e.g. your own applications, or adding tenants to Argo CD, etc. +This can be used to deploy your own content, e.g. your own applications, or adding tenants to Argo CD, etc. Example for a GOP content repository: @@ -14,9 +14,10 @@ Example for a GOP content repository: This document will use the short for "repo" from here on, for brevity. In case for two registries or airgapped mode, you have to specify the registry sources via own config.yaml settings -unter the content.variables.images section. +unter the content.variables.images section. # Table of contents + @@ -37,14 +38,14 @@ unter the content.variables.images section. - # Purpose of content loader You like the idea of automatically rolling out IDPs using GOP but -* want to initialize it with your own real-world end-user application that is automatically deployed as a turnkey solution, +* want to initialize it with your own real-world end-user application that is automatically deployed as a turnkey + solution, * want to add multiple tenants with Argo CD, -* want to have different or additional IDP tools, e.g. logging/backup/tracing, etc. +* want to have different or additional IDP tools, e.g. logging/backup/tracing, etc. Then content loader is for you! It allows you to define content using a folder structure inside a Git repo (so-called content repo) that is picked up @@ -52,7 +53,8 @@ during GOP apply optionally run through a templating engine and then pushed to t # Meaning of the term "content" -- Currently, GOP (version > 0.11.0) consists of example applications, which are maintained [here](https://github.com/cloudogu/gitops-examples), +- Currently, GOP (version > 0.11.0) consists of example applications, which are + maintained [here](https://github.com/cloudogu/gitops-examples), in addition to the actual IDP (ArgoCD, Prometheus, etc.). ➡️ Turnkey-solution deployed via GitOps pipelines - We refer to these applications as "content". @@ -60,24 +62,30 @@ during GOP apply optionally run through a templating engine and then pushed to t - These applications include: - Code (e.g., repo `argocd/petclinic-helm`) - Configuration (Argo CD `Application` and YAML resources in the GitOps repo `example-tenant/gitops`) - - Basic configuration of the tenant (Argo CD `AppProject` and `Application` of Applications in the repo `argocd/argocd`) - - Potentially dependencies, e.g., `3rd-party-dependencies/gitops-build-lib` and `3rd-party-dependencies/spring-boot-helm-chart` - - Potentially `Jenkinsfile`s that describe how to build and push images and start the GitOps process by writing resources to Git that are then picked up by ArgoCD. + - Basic configuration of the tenant (Argo CD `AppProject` and `Application` of Applications in the repo + `argocd/argocd`) + - Potentially dependencies, e.g., `3rd-party-dependencies/gitops-build-lib` and + `3rd-party-dependencies/spring-boot-helm-chart` + - Potentially `Jenkinsfile`s that describe how to build and push images and start the GitOps process by writing + resources to Git that are then picked up by ArgoCD. In addition, a Jenkins job that pulls and executes the `Jenkinsfile`. - After installing GOP, the example applications are built by Jenkins and deployed by ArgoCD via GitOps. Finally, these end-user applications can be reached via HTTP(S) via their ingress (turnkey-solution). -- The content loader feature provides the possibility to deliver your own custom content, i.e. real-world applications instead of examples. +- The content loader feature provides the possibility to deliver your own custom content, i.e. real-world applications + instead of examples. # Content loader concepts The content deployed by GOP can be completely defined via configuration. -This allows for -* changing all Git repos created by GOP. -* adding new Git repos, e.g. for end-user applications (including their dependencies such as Helm charts or build libraries) as well as IDP applications, such as monitoring tools. +This allows for + +* changing all Git repos created by GOP. +* adding new Git repos, e.g. for end-user applications (including their dependencies such as Helm charts or build + libraries) as well as IDP applications, such as monitoring tools. This is done by means of a `content` section within GOP's config file (the one being specified by `--config-file`). -This config holds references to Git repos that contain the actual content to be pushed to Git. +This config holds references to Git repos that contain the actual content to be pushed to Git. Here is a schematic example of the `content` section that will be described in the following: @@ -104,51 +112,64 @@ content: examples: true # deploy example content described in https://github.com/cloudogu/gitops-playground ``` -See [here for a full example](../../src/main/resources/application-content-examples.yaml). +See [here for a full example](../../src/main/resources/application-content-examples.yaml). The [TL;DR](#tldr) sections shows how to see this example in action. ## Content repos + - The content is defined in Git repos, known as content repos (`content.repos`) -- There are different `type`s of content repos: `MIRROR`, `COPY`, and `FOLDER_BASED` (see [here](#different-types-of-content-repos) for details). -- For these types of content repos, the `overrideMode` determines how to handle previously existing files in the repo: `INIT`, `UPGRADE`, `RESET` ([see overrideMode](#the-overridemode)). -- Templating with [Freemarker](https://freemarker.apache.org/) can be enabled via `templating: true` for each content repo. - - The templates can access the config and custom variables defined in `content.variables`. +- There are different `type`s of content repos: `MIRROR`, `COPY`, and `FOLDER_BASED` ( + see [here](#different-types-of-content-repos) for details). +- For these types of content repos, the `overrideMode` determines how to handle previously existing files in the repo: + `INIT`, `UPGRADE`, `RESET` ([see overrideMode](#the-overridemode)). +- Templating with [Freemarker](https://freemarker.apache.org/) can be enabled via `templating: true` for each content + repo. + - The templates can access the config and custom variables defined in `content.variables`. - See [templating](#templating) for more details. - Multiple content repos can be specified in the `content.repos` field - GOP merges these repos in the defined order into a directory structure. - This allows you to overwrite files from all repos created by GOP. - - One use case for this is, for example, a base repo that specifies the basic structure of all GOP instances (e.g. repo structure for tenants) in a cloud environment and more specialized repos that contain specific applications (e.g. per tenant). - - Another use case is to keep the configuration (YAML) in one repo and the code in another to deploy different config with the same code. - Current examples is `petclinic-plain` (see [Mirror/copy example](#mirrorcopy-repo-and-add-specific-files)). + - One use case for this is, for example, a base repo that specifies the basic structure of all GOP instances (e.g. + repo structure for tenants) in a cloud environment and more specialized repos that contain specific applications ( + e.g. per tenant). + - Another use case is to keep the configuration (YAML) in one repo and the code in another to deploy different + config with the same code. + Current examples is `petclinic-plain` (see [Mirror/copy example](#mirrorcopy-repo-and-add-specific-files)). - Existing repos, e.g., `argocd/argocd`, can be extended by `COPY`, and `FOLDER_BASED` content repos. - - ArgoCD `AppProjects` and `Applications` can be defined in the content. + - ArgoCD `AppProjects` and `Applications` can be defined in the content. - This also allows you to hook into the configuration of Argo CD and, for example, define tenants. ## Additional options -- **Kubernetes namespaces** needed for the content (e.g. `example-tenant-staging`) can be specified via the `content.namespaces` field. +- **Kubernetes namespaces** needed for the content (e.g. `example-tenant-staging`) can be specified via the + `content.namespaces` field. - GOP deploys the namespaces listed therein via GitOps (repo `argocd/cluster-resources`, application `misc`) - In each namespace, the following is set up: - - the configured ImagePullSecrets - - RBAC resources + - the configured ImagePullSecrets + - RBAC resources - `NetworkPolicies` (e.g. to enable Prometheus to access the metrics). - - The namespaces allow templating, e.g., `‘${config.application.namePrefix}example-tenant-staging’, ‘${config.application.namePrefix}example-tenant-production’` + - The namespaces allow templating, e.g., + `‘${config.application.namePrefix}example-tenant-staging’, ‘${config.application.namePrefix}example-tenant-production’` - Note that you can add arbitrary namespaces. They don't necessarily need to be related to your content. - With `--openshift` GOP creates `ProjectRequests` instead of `Namespaces`. - **Jenkins**: Automatic generation of Jenkins jobs based on the content is possible. When enable for a content repo (via `content.repos.createJenkinsJob` set to `true`) a job is created - for each SCM Manager namespace found in the content - that contains a `Jenkinsfile`. -- The **example content** (see [README/Example Applications](../../README.md#example-applications)) can be activated via the `content.examples` field. - This content is defined in the [repo](https://github.com/cloudogu/gitops-playground/) and shipped with the image (does not require internet access at runtime) - +- The **example content** (see [README/Example Applications](../../README.md#example-applications)) can be activated via + the `content.examples` field. + This content is defined in the [repo](https://github.com/cloudogu/gitops-playground/) and shipped with the image (does + not require internet access at runtime) ## Different types of content repos There are different types of content repos: `MIRROR`, `COPY`, and `FOLDER_BASED`. -- `MIRROR` (default): The entire content repo is mirrored to the target repo if it does not yet exist ([see overrideMode](#the-overridemode)). + +- `MIRROR` (default): The entire content repo is mirrored to the target repo if it does not yet + exist ([see overrideMode](#the-overridemode)). - `COPY`: Only the files (no Git history) are copied to the target repo, then committed and pushed. -- `FOLDER_BASED`: Using the folder structure in the content repo, multiple repos can be created and initialized or changed in the target. +- `FOLDER_BASED`: Using the folder structure in the content repo, multiple repos can be created and initialized or + changed in the target. **Global Properties** @@ -157,19 +178,20 @@ There are different types of content repos: `MIRROR`, `COPY`, and `FOLDER_BASED` Defaults: - `COPY` / `FOLDER_BASED`: Default branch of repo. - `MIRROR`: All branches and tags of repo -- `overrideMode` (`INIT`, `UPGRADE`, `RESET`) defines how to handle existing files in the target repo ([see overrideMode](#the-overridemode)). +- `overrideMode` (`INIT`, `UPGRADE`, `RESET`) defines how to handle existing files in the target + repo ([see overrideMode](#the-overridemode)). - `username`, `password` — credentials -- `createJenkinsJob` — If `true` a Jenkins job is created for the associated SCM Manager namespace, when the following is also true: +- `createJenkinsJob` — If `true` a Jenkins job is created for the associated SCM Manager namespace, when the following + is also true: - Jenkins is active in GOP (`--jenkins`), and - there is a `Jenkinsfile` in one of the content repos or the specified `refs`. - ### `MIRROR` -A content repo is mirrored completely (or only a `ref`) to the target repo (including Git history). +A content repo is mirrored completely (or only a `ref`) to the target repo (including Git history). **Caution** -Force push is used here! +Force push is used here! Note that by default only new repos are mirrored. To overwrite `overrideMode: RESET` must be set ([see overrideMode](#the-overridemode)). @@ -186,7 +208,6 @@ This might be implemented at a later point. - Exception:` targetRef` is a full ref such as` refs/heads/my-branch` or `refs/tags/my-tag`. - If `targetRef` is empty, the source `ref` is used by default. - ### `COPY` Only the files (no Git history) are copied and committed to the target repo. @@ -194,18 +215,21 @@ Only the files (no Git history) are copied and committed to the target repo. **Properties** - `target` (required) — target repo, e.g. `namespace/name` -- `targetRef` — Git reference in `target` to which is pushed (branch or tag). +- `targetRef` — Git reference in `target` to which is pushed (branch or tag). - If `ref `is a tag, `targetRef` is also treated as a tag. - - Exception:` targetRef` is a complete `ref `such as `refs/heads/my-branch` or `refs/tags/my-tag`. + - Exception:` targetRef` is a complete `ref `such as `refs/heads/my-branch` or `refs/tags/my-tag`. - If` targetRef` is empty, the source `ref `is used by default. - `path `- Folder within the content repo from which to copy -- `templating`- If `true`, all `.ftl` files are rendered using [Freemarker](https://freemarker.apache.org/) before being pushed to `target` ([see templating](#templating)). - +- `templating`- If `true`, all `.ftl` files are rendered using [Freemarker](https://freemarker.apache.org/) before being + pushed to `target` ([see templating](#templating)). ### `FOLDER_BASED` -- Using the folder structure in the content repo, multiple repos can be created in the target and initialized or extended using `COPY`. + +- Using the folder structure in the content repo, multiple repos can be created in the target and initialized or + extended using `COPY`. - That is, The top two directory levels of the repo determine the target repos in GOP. -- Example: The contents of the `example-tenant/gitops` folder are pushed to the `gitops` repo in the `example-tenant` namespace. +- Example: The contents of the `example-tenant/gitops` folder are pushed to the `gitops` repo in the `example-tenant` + namespace. ![content-hooks-folder-based.png](content-hooks-folder-based.png) @@ -215,24 +239,30 @@ This allows, for example, adding additional Argo CD applications and even your o - `target` (required) — target repo, e.g. `namespace/name` - `path` — source folder in the content repo used for copying -- `templating` — If `true`, all `.ftl` files are rendered using [Freemarker](https://freemarker.apache.org/) before being pushed to `target` ([see templating](#templating)). +- `templating` — If `true`, all `.ftl` files are rendered using [Freemarker](https://freemarker.apache.org/) before + being pushed to `target` ([see templating](#templating)). ## The overrideMode -For all types of content repos, the `overrideMode` determines how to handle previously existing files in the repo: `INIT`, `UPGRADE`, `RESET`. +For all types of content repos, the `overrideMode` determines how to handle previously existing files in the repo: +`INIT`, `UPGRADE`, `RESET`. + - `INIT` (default): Only push if the repo does not exist - `UPGRADE`: Clone and copy – existing files are overwritten, files that are not in the content are retained. - `RESET`: Delete all files after cloning the source, then copy new files. This results in files that are not in the content being deleted. **Note** -With `MIRROR`, `RESET` does not reset the entire repo. Specific effect: Branches that exist in the target but not in the source are retained. +With `MIRROR`, `RESET` does not reset the entire repo. Specific effect: Branches that exist in the target but not in the +source are retained. **Important** If existing repos of GOP are to be extended, e.g., `cluster-resources`, the `overrideMode` must be set to `UPGRADE`. ## Repo Credentials -For private Repositories you can specify credentials via the `username` and `password` fields under credendials. See CLI Params for more details. + +For private Repositories you can specify credentials via the `username` and `password` fields under credendials. See CLI +Params for more details. You can also use a Kubernetes secret for the credentials. ```yaml @@ -249,6 +279,7 @@ content: username: 'abc' password: 'ey...' ``` + or with kubernetes secret ```yaml @@ -269,8 +300,10 @@ content: ``` ## Templating -When `templating` is enabled, all files ending in `.ftl` are rendered using [Freemarker](https://freemarker.apache.org/) during GOP installation and the result is created under the same name without the `.ftl` extension. -Example`values.yam;.ftl` is written to `values.yaml`. + +When `templating` is enabled, all files ending in `.ftl` are rendered using [Freemarker](https://freemarker.apache.org/) +during GOP installation and the result is created under the same name without the `.ftl` extension. +Example`values.yam;.ftl` is written to `values.yaml`. The entire configuration of GOP is available as `config` variable in the templates. @@ -278,7 +311,7 @@ In addition, you can define your own variables (`content.variables`). This makes it possible to write parameterizable content that can be used for different instances. Example: `config.content.variables.my` from the schematic example in [concepts](#content-loader-concepts). -In Freemarker you can use static methods from GOP and JDK. +In Freemarker you can use static methods from GOP and JDK. An [example from GOP code](https://github.com/cloudogu/gitops-playground/blob/0.11.0/applications/cluster-resources/monitoring/prometheus-stack-helm-values.ftl.yaml#L111): ```yaml @@ -292,8 +325,13 @@ image: tag : ${operatorImageObject.tag} ``` -By default, Freemarker grants access to all static resources within the project. To add an extra layer of security, set the content.useWhitelist property to true in the GOP configuration, or use the --content-whitelist CLI flag to enable the static resources whitelist. -To specify which static resources should be accessible, add them to the allowedStaticsWhitelist in the configuration. A default set of static resources is already provided as an example. + +By default, Freemarker grants access to all static resources within the project. To add an extra layer of security, set +the content.useWhitelist property to true in the GOP configuration, or use the --content-whitelist CLI flag to enable +the static resources whitelist. +To specify which static resources should be accessible, add them to the allowedStaticsWhitelist in the configuration. A +default set of static resources is already provided as an example. + # TL;DR How to get started with content loader? @@ -318,16 +356,16 @@ Most applications mentioned in [README/Example Applications](../../README.md#exa This chapter describes real-world use cases that can be done via content loader. - ## Mirror the entire repo on every call + ```yaml - url: 'https://github.com/cloudogu/spring-boot-helm-chart' target: '3rd-party/spring-boot-helm' overrideMode: RESET ``` - ## Create additional tenant in Argo CD + ```yaml - url: 'https://github.com/cloudogu/gitops-playground.git' path: 'docs/content-loader' @@ -343,7 +381,8 @@ This chapter describes real-world use cases that can be done via content loader. This example first `MIRROR`s a repo, then adds a `Dockerfile` and `Jenkinsfile` and creates a Jenkins job. -As an alternative, you can add the type `COPY` in the first repo (petclinic), resulting in only the files being pushed to `target` (without the git history). +As an alternative, you can add the type `COPY` in the first repo (petclinic), resulting in only the files being pushed +to `target` (without the git history). Reminder: no type means `MIRROR` (default). ```yaml @@ -361,4 +400,4 @@ Reminder: no type means `MIRROR` (default). templating: true type: FOLDER_BASED overrideMode: UPGRADE -``` +``` \ No newline at end of file diff --git a/docs/images/app-repo-vs-gitops-repo.svg b/docs/images/app-repo-vs-gitops-repo.svg index 56d3c81d0..e2b911fa9 100644 --- a/docs/images/app-repo-vs-gitops-repo.svg +++ b/docs/images/app-repo-vs-gitops-repo.svg @@ -1,4 +1,197 @@ -
push via PR
push via PR
push
push
push
push
push via PR
push via PR
pull
pull
CI server
CI server
Developer
Devel...
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + + + + + + + + +
+
+
+ push via PR +
+
+
+
+ push via PR + +
+
+ + + + + +
+
+
+ push +
+
+
+
+ push + +
+
+ + + + + +
+
+
+ push +
+
+
+
+ push + +
+
+ + + + + +
+
+
+ push via PR +
+
+
+
+ push via PR + +
+
+ + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+ + + + +
+
+
+ CI server +
+
+
+
+ CI server + +
+
+ + + + + +
+
+
+ Developer +
+
+
+
+ Devel... + +
+
+
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/images/argocd-repos.svg b/docs/images/argocd-repos.svg index 46f22f237..e57848e8f 100644 --- a/docs/images/argocd-repos.svg +++ b/docs/images/argocd-repos.svg @@ -1,4 +1,340 @@ -
1
1
6
6
4
4
2
2
8
8
9
9
7
7
Developer
Devel...
https://github.com/argoproj/argo-helm/releases/download/argo-cd-5.23.5...
https://github.com/argoproj/argo-helm/releases/download/argo-cd-5.23.5...
3
3
5
5
Platform admin
Platform admin
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + + + + + + + + + +
+
+
+ 1 +
+
+
+
+ 1 + +
+
+ + + + + + +
+
+
+ 6 +
+
+
+
+ 6 + +
+
+ + + + + + +
+
+
+ 4 +
+
+
+
+ 4 + +
+
+ + + + + + +
+
+
+ 2 +
+
+
+
+ 2 + +
+
+ + + + + + +
+
+
+ 8 +
+
+
+
+ 8 + +
+
+ + + + + + +
+
+
+ 9 +
+
+
+
+ 9 + +
+
+ + + + + + +
+
+
+ 7 +
+
+
+
+ 7 + +
+
+ + + + + + + +
+
+
+ Developer +
+
+
+
+ Devel... + +
+
+ + + + + +
+
+
+ https://github.com/argoproj/argo-helm/releases/download/argo-cd-5.23.5... +
+
+
+
+ + https://github.com/argoproj/argo-helm/releases/download/argo-cd-5.23.5... + +
+
+ + + + + + +
+
+
+ 3 +
+
+
+
+ 3 + +
+
+ + + + + + + +
+
+
+ 5 +
+
+
+
+ 5 + +
+
+ + + + + +
+
+
+ Platform admin +
+
+
+
+ Platform admin + +
+
+
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/images/autopilot-repo.svg b/docs/images/autopilot-repo.svg index f233a5417..48940d99d 100644 --- a/docs/images/autopilot-repo.svg +++ b/docs/images/autopilot-repo.svg @@ -1,4 +1,320 @@ -5

path:
**/staging/config.json

path:...
6
6
7
7
8
8
2
2
path: 
*.json
path:...
4
4
github.com/argoproj-labs/argocd-autopilot/blob/main/manifests/base/
github.com/argoproj-labs/argocd-autopilot/blob/main/manifests/base/
github.com/argoproj/argo-cd/blob/stable/manifests/install.yaml
github.com/argoproj/argo-cd/blob/stable/manifests/install.yaml
3
3
1
1
autopilot-bootstrap
autopilot-bootstrap
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + + 5 + + + + + + + +
+
+
+

path:
**/staging/config.json +

+
+
+
+
+ + path:... + +
+
+ + + +
+
+
+ 6 +
+
+
+
+ 6 + +
+
+ + + + + + +
+
+
+ 7 +
+
+
+
+ 7 + +
+
+ + + + + + +
+
+
+ 8 +
+
+
+
+ 8 + +
+
+ + + + + + +
+
+
+ 2 +
+
+
+
+ 2 + +
+
+ + + + + + +
+
+
+ path: 
*.json +
+
+
+
+
+ + path:... + +
+
+ + + +
+
+
+ 4 +
+
+
+
+ 4 + +
+
+ + + + + + +
+
+
+ github.com/argoproj-labs/argocd-autopilot/blob/main/manifests/base/ +
+
+
+
+ + github.com/argoproj-labs/argocd-autopilot/blob/main/manifests/base/ + +
+
+ + + + +
+
+
+ github.com/argoproj/argo-cd/blob/stable/manifests/install.yaml +
+
+
+
+ + github.com/argoproj/argo-cd/blob/stable/manifests/install.yaml + +
+
+ + + + + + + + + +
+
+
+ 3 +
+
+
+
+ 3 + +
+
+ + + + + +
+
+
+ 1 +
+
+
+
+ 1 + +
+
+ + + + +
+
+
+ autopilot-bootstrap +
+
+
+
+ autopilot-bootstrap + +
+
+ +
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/images/gitops-playground-features.drawio.svg b/docs/images/gitops-playground-features.drawio.svg index 54208fa66..24de7c8e4 100644 --- a/docs/images/gitops-playground-features.drawio.svg +++ b/docs/images/gitops-playground-features.drawio.svg @@ -1,4 +1,1523 @@ -
   bootstrap 
tools 
+ examples
bootstrap...

GOP
GOP
Grafana
Grafana
©Cloudogu GmbH 2026: GitOps Playground© for use with  Argo™, Git™, Jenkins®, Kubernetes®, Prometheus®, Grafana®, Vault®, traefik proxy® and SCM-Manager
©Cloudogu GmbH 2026: GitOps Playground© for use with  Argo™, Git™, Jenkins®, Kubernetes®, Prometheus®, Grafana®, Vault®, traefik proxy® and SCM-Manager
®
®
send alert
send alert
apply resources
apply resources
ArgoCD
ArgoCD
Jenkins
Jenkins
®
®
SCM-Manager
SCM-Manager
Config Repository
Config Rep...
App
Repository
App...
pull
pull
OCI
Registry
OCI...
push config
and create pull request
push config...
pull
pull
Text
Text
push image
push image
Your Cloud provider
Your Cloud provider
API-Server
API-Server
®
®
send query
send query
scrape 1…n metrics
scrape 1…n met...
create secret
create secret
read secret
read secret
External
Secret
Operator
Externa...
®
®
Prometheus 
Prometh...
®
®
create secret
create secret
view metrics
view metrics
view application and state
view application and state
User
User
push
push
accept pull request
accept pull request
K8s Cluster
K8s Cluster
®
®
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+    bootstrap  +
tools 
+
+ examples
+
+
+
+
+ bootstrap... + +
+
+
+
+
+ + + + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + +
+
+
+ + + GOP + + +
+
+
+
+ GOP + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ Grafana +
+
+
+
+ Grafana + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ + ©Cloudogu GmbH 2026: GitOps Playground© for use with  Argo™, Git™, + Jenkins®, Kubernetes®, Prometheus®, Grafana®, Vault®, traefik + proxy® and SCM-Manager + +
+
+
+
+ ©Cloudogu GmbH 2026: GitOps Playground© for use with  Argo™, + Git™, Jenkins®, Kubernetes®, Prometheus®, Grafana®, Vault®, traefik proxy® and + SCM-Manager + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + + + +
+
+
+ send alert +
+
+
+
+ send alert + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ apply resources +
+
+
+
+ apply resources + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ArgoCD +
+
+
+
+ ArgoCD + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ Jenkins +
+
+
+
+
+ Jenkins + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ SCM-Manager +
+
+
+
+ SCM-Manager + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ Config Repository +
+
+
+
+ Config Rep... + +
+
+
+
+ + + + + + + + +
+
+
+ ™ +
+
+
+
+ ™ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ App +
+
+ Repository +
+
+
+
+
+
+
+ App... + +
+
+
+
+ + + + + + + + +
+
+
+ ™ +
+
+
+
+ ™ + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+
OCI
+
Registry +
+
+
+
+
+
+ OCI... + +
+
+
+
+ + + + + +
+ + + + + + + + + + +
+
+
+ push config
and create pull request +
+
+
+
+ push config... + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ Text +
+
+
+
+ Text + +
+
+
+
+ + + + + +
+
+
+ push image +
+
+
+
+ push image + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ Your Cloud provider +
+
+
+
+ Your Cloud provider + +
+
+
+
+ + + + + + + + +
+
+
+ ™ +
+
+
+
+ ™ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ API-Server +
+
+
+
+ API-Server + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+ + + + + +
+ + + + + + + + + + +
+
+
+ send query +
+
+
+
+ send query + +
+
+
+
+
+ + + + + + + + +
+
+
+ scrape 1…n metrics +
+
+
+
+ scrape 1…n met... + +
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ create secret +
+
+
+
+ create secret + +
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ read secret +
+
+
+
+ read secret + +
+
+
+
+
+ + + + + + + + +
+
+
+
External
+
Secret
+
Operator +
+
+
+
+
+
+ Externa... + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ Prometheus  + + +
+
+
+
+ Prometh... + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + + +
+
+
+ create secret +
+
+
+
+ create secret + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ view metrics +
+
+
+
+ view metrics + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ view application and state +
+
+
+
+ view application and state + +
+
+
+
+
+ + + + + + + + +
+
+
+ User +
+
+
+
+ User + +
+
+
+
+ + + + + + + + + + +
+
+
+ push +
+
+
+
+ push + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ accept pull request +
+
+
+
+ accept pull request + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ K8s Cluster +
+
+
+
+
+
+ K8s Cluster + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+
+
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/images/gitops-playground-local.drawio.svg b/docs/images/gitops-playground-local.drawio.svg index b6c58244c..58f119320 100644 --- a/docs/images/gitops-playground-local.drawio.svg +++ b/docs/images/gitops-playground-local.drawio.svg @@ -1,4 +1,1399 @@ -
k3d Container
k3d Container
®
®
Your host
Your host
Jenkins
Jenkins
®
®
SCM-Manager
SCM-Manager
Config Repository
Config Rep...
App
Repository
App...
pull
pull
OCI
Registry
OCI...
run
run
push config
and create pull request
push config...
pull
pull
Text
Text
push image
push image
©Cloudogu GmbH 2024: GitOps Playground© for use with  Argo™, Git™, Jenkins®, Kubernetes®, Prometheus®, Grafana®, Vault®, traefik proxy® and SCM-Manager
©Cloudogu GmbH 2024: GitOps Playground© for use with  Argo™, Git™, Jenkins®, Kubernetes®, Prometheus®, Grafana®, Vault®, traefik proxy...
send alert
send alert
apply resources
apply resources
ArgoCD
ArgoCD
API-Server
API-Server
®
®
Prometheus 
Prometh...
®
®
send query
send query
Grafana
Grafana
scrape 1…n metrics
scrape 1…n met...
create secret
create secret
read secret
read secret
External
Secret
Operator
Externa...
®
®
Containerization Tool
Containe...
   bootstrap 
tools 
+ examples
bootstrap...

GOP
GOP
run
run
Text is not SVG - cannot display
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ k3d +  Container +
+
+
+
+ k3d Container + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ Your host +
+
+
+
+ Your host + +
+
+
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ Jenkins +
+
+
+
+
+ Jenkins + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ SCM-Manager +
+
+
+
+ SCM-Manager + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ Config Repository +
+
+
+
+ Config Rep... + +
+
+
+
+ + + + + + + + +
+
+
+ +
+
+
+
+ ™ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ App +
+
+ Repository +
+
+
+
+
+
+
+ App... + +
+
+
+
+ + + + + + + + +
+
+
+ +
+
+
+
+ ™ + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+
+
+
+ + + + + + + + + + + + + + + + + +
+
+
+
OCI
+
Registry +
+
+
+
+
+
+ OCI... + +
+
+
+
+ + + + + +
+
+ + + + + + + + + + +
+
+
+ run +
+
+
+
+ run + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ push config
and create pull request +
+
+
+
+ push config... + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ Text +
+
+
+
+ Text + +
+
+
+
+ + + + + +
+
+
+ push image +
+
+
+
+ push image + +
+
+
+
+
+ + + + + + + + +
+
+
+ + + ©Cloudogu GmbH 2024: GitOps + Playground© for use with  Argo™, Git™, Jenkins®, + Kubernetes®, Prometheus®, Grafana®, Vault®, traefik + proxy® and SCM-Manager + + + +
+
+
+
+ ©Cloudogu GmbH 2024: GitOps Playground© for + use with  Argo™, Git™, Jenkins®, Kubernetes®, Prometheus®, Grafana®, Vault®, traefik + proxy... + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ send alert +
+
+
+
+ send alert + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ apply resources +
+
+
+
+ apply resources + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ArgoCD +
+
+
+
+ ArgoCD + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ™ +
+
+
+
+ ™ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ API-Server +
+
+
+
+ API-Server + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Prometheus  + + +
+
+
+
+ Prometh... + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ send query +
+
+
+
+ send query + +
+
+
+
+
+ + + + + + + + +
+
+
+ Grafana +
+
+
+
+ Grafana + +
+
+
+
+ + + + + + + + +
+
+
+ scrape 1…n metrics +
+
+
+
+ scrape 1…n met... + +
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ create secret +
+
+
+
+ create secret + +
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ read secret +
+
+
+
+ read secret + +
+
+
+
+
+ + + + + + + + +
+
+
+
External
+
Secret
+
Operator +
+
+
+
+
+
+ Externa... + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ Containerization Tool +
+
+
+
+ Containe... + +
+
+
+
+
+ + + + + + + + + + +
+
+
+    bootstrap  +
tools 
+
+ examples
+
+
+
+
+ bootstrap... + +
+
+
+
+
+ + + + + + + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + +
+
+
+ + + GOP + + +
+
+
+
+ GOP + +
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ run +
+
+
+
+ run + +
+
+
+
+
+ + + + + +
+
+
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/images/gitops-playground-production.drawio.svg b/docs/images/gitops-playground-production.drawio.svg index cdf9eedef..5e0adba71 100644 --- a/docs/images/gitops-playground-production.drawio.svg +++ b/docs/images/gitops-playground-production.drawio.svg @@ -1,4 +1,1283 @@ -
pull
pull
OCI
Registry
OCI...
apply resources
apply resources
ArgoCD
ArgoCD
Prometheus
Prometh...
®
®
send query
send query
read secret
read secret
API-Server
API-Ser...
®
®
create secret
create secret
Grafana
Grafana
scrape 1…n metrics
scrape 1…n met...
External
Secret
Operator
Externa...
K8s Cluster
K8s Cluster
®
®
Jenkins
Jenkins
®
®
SCM-Manager
SCM-Manager
GitOps Repository
GitOps Rep...
App
Repository
App...
pull
pull
push
push
push
push
pull
pull
Your Cloud provider
Your Cloud provider
®
®
©Cloudogu GmbH 2026: GitOps Playground© for use with  Argo™, Git™, Jenkins®, Kubernetes®, Grafana®, Prometheus®, Vault®, traefik proxy® and SCM-Manager
©Cloudogu GmbH 2026: GitOps Playground© for use with  Argo™, Git™, Jenkins®, Kubernetes®, Grafana®, Prometheus®, Vault®, traefik proxy® and SCM-Manager
Cloudogu LOP
Cloudogu LOP
Cloudogu EcoSystem
Cloudogu EcoSystem
Cloudogu GOP
Cloudogu GOP
Text is not SVG - cannot display
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
OCI
+
Registry +
+
+
+
+
+
+ OCI... + +
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ apply resources +
+
+
+
+ apply resources + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ArgoCD +
+
+
+
+ ArgoCD + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Prometheus +
+
+
+
+ Prometh... + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ send query +
+
+
+
+ send query + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ read secret +
+
+
+
+ read secret + +
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ API-Server +
+
+
+
+ API-Ser... + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ create secret +
+
+
+
+ create secret + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ Grafana +
+
+
+
+ Grafana + +
+
+
+
+ + + + + + + + +
+
+
+ scrape 1…n metrics +
+
+
+
+ scrape 1…n met... + +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+
External
+
Secret
+
Operator +
+
+
+
+
+
+ Externa... + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ K8s Cluster +
+
+
+
+
+
+ K8s Cluster + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ Jenkins +
+
+
+
+
+ Jenkins + +
+
+
+
+ + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ SCM-Manager +
+
+
+
+ SCM-Manager + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ GitOps Repository +
+
+
+
+ GitOps Rep... + +
+
+
+
+ + + + + + + + +
+
+
+ ™ +
+
+
+
+ ™ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
App
+
Repository +
+
+
+
+
+
+ App... + +
+
+
+
+ + + + + + + + +
+
+
+ ™ +
+
+
+
+ ™ + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ push +
+
+
+
+ push + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ push +
+
+
+
+ push + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ pull +
+
+
+
+ pull + +
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ Your Cloud provider +
+
+
+
+ Your Cloud provider + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ ® +
+
+
+
+ ® + +
+
+
+
+ + + + + + + + +
+
+
+ ™ +
+
+
+
+ ™ + +
+
+
+
+ + + + + + + + +
+
+
+ + + ©Cloudogu GmbH 2026: GitOps Playground© for use with  Argo™, + Git™, Jenkins®, Kubernetes®, Grafana®, Prometheus®, + Vault®, traefik proxy® and + SCM-Manager + + +
+
+
+
+ ©Cloudogu GmbH 2026: GitOps Playground© for + use with  Argo™, Git™, Jenkins®, Kubernetes®, Grafana®, Prometheus®, Vault®, traefik + proxy® and SCM-Manager + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Cloudogu LOP +
+
+
+
+ Cloudogu LOP + +
+
+
+
+
+ + + + + +
+ + + + + + + + + + + + + +
+
+
+ Cloudogu EcoSystem +
+
+
+
+ Cloudogu EcoSystem + +
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ Cloudogu GOP +
+
+
+
+ Cloudogu GOP + +
+
+
+
+
+ + + + + + + + + + +
+
+
+ + + + Text is not SVG - cannot display + + +
\ No newline at end of file diff --git a/docs/k3d.md b/docs/k3d.md index a6e204c70..55a3f82e1 100644 --- a/docs/k3d.md +++ b/docs/k3d.md @@ -11,7 +11,7 @@ bash <(curl -s https://raw.githubusercontent.com/cloudogu/gitops-playground/main ``` If you plan to interact with your cluster directly (not only via GitOps), we recommend -installing `kubectl` (see [here](https://kubernetes.io/docs/tasks/tools/#kubectl)). +installing `kubectl` (see [here](https://kubernetes.io/docs/tasks/tools/#kubectl)). ### Create Cluster without installing k3d @@ -33,7 +33,8 @@ docker run --rm -it -u $(id -u ):$(getent group docker | cut -d: -f3) \ For deletion use `-c "k3d cluster rm gitops-playground"` in the last line. For this to work on MacOS and Windows with WSL2 we would have to overcome the missing docker group there. -One option would be to run the container as root to access the docker socket and then `chown` the files back to the original user, e.g. like so: +One option would be to run the container as root to access the docker socket and then `chown` the files back to the +original user, e.g. like so: ```shell K3D_VERSION=$(curl -s https://raw.githubusercontent.com/cloudogu/gitops-playground/main/scripts/init-cluster.sh | grep '^K3D_VERSION=' | cut -d '=' -f 2) @@ -44,45 +45,53 @@ docker run --rm -it -u $(id -u ):$(getent group docker | cut -d: -f3) \ ghcr.io/k3d-io/k3d:${K3D_VERSION}-dind \ -c "bash <(curl -s https://raw.githubusercontent.com/cloudogu/gitops-playground/main/scripts/init-cluster.sh) && chown -R \$(ls -ld /root/.config/k3d/ | awk '{print \$3 \":\" \$4}') /root/.config/k3d/" ``` + This example only writes cluster's kubeconfig to `~/.config/k3d`, but not to your default `~/.kube/config`. -So to access the cluster, your would have to use `export KUBECONFIG=$HOME/.config/k3d/kubeconfig-gitops-playground.yaml` +So to access the cluster, your would have to use `export KUBECONFIG=$HOME/.config/k3d/kubeconfig-gitops-playground.yaml` or also add a `-v` and `chown` for `.kube/config`. ## Parameters ### --cluster-name + `--cluster-name` - default: `gitops-playground` ### --bind-localhost -`--bind-localhost` - binds the cluster to the host network (`localhost`). This only makes sense on Linux, as on Windows and Mac the `host` network from Docker's perspective is not the localhost you can access from your browser. + +`--bind-localhost` - binds the cluster to the host network (`localhost`). This only makes sense on Linux, as on Windows +and Mac the `host` network from Docker's perspective is not the localhost you can access from your browser. When using this argument, the URLs of the application will be reachable via localhost. e.g. `localhost:9092` for Argo CD. -We used this for more convenience during development, before we introduced `--bind-ingress-port`. +We used this for more convenience during development, before we introduced `--bind-ingress-port`. -By now, using no arguments (which sets [`--bind-ingress-port=80`](#--bind-ingress-port)) makes more sense for most use cases. +By now, using no arguments (which sets [`--bind-ingress-port=80`](#--bind-ingress-port)) makes more sense for most use +cases. -Even with `--bind-localhost`, there still is one port that has to be bound to localhost: the registry port. -For registries other than localhost or local ip addresses, docker will use HTTPS, leading to errors on `docker push` in the example application's Jenkins Jobs. -Note that if you use this option and the registry's default port 30000 is already bound on localhost +Even with `--bind-localhost`, there still is one port that has to be bound to localhost: the registry port. +For registries other than localhost or local ip addresses, docker will use HTTPS, leading to errors on `docker push` in +the example application's Jenkins Jobs. +Note that if you use this option and the registry's default port 30000 is already bound on localhost (e.g. when starting more than one instance of the playground) you can overwrite it with `--bind-registry-port`. -You can also bin the registry port will be bound to an arbitrary free port on localhost using `--bind-registry-port=0`, **which we don't recommend**. -Note that this port changes on every restart of the k3d container, rendering the registry inside the playground's jenkins inaccessible. +You can also bin the registry port will be bound to an arbitrary free port on localhost using `--bind-registry-port=0`, +**which we don't recommend**. +Note that this port changes on every restart of the k3d container, rendering the registry inside the playground's +jenkins inaccessible. -This port has to be passed on when creating the playground via the `--internal-registry-port` parameter. For example: +This port has to be passed on when creating the playground via the `--internal-registry-port` parameter. For example: ### --bind-ingress-port By default, ingress controller is bound to `localhost:80`, i.e. `localhost`. -Can be disabled by setting `--bind-ingress-port=-` +Can be disabled by setting `--bind-ingress-port=-` -This feature can be used for local ingresses which are the only way to run the playground on Windows and Mac, +This feature can be used for local ingresses which are the only way to run the playground on Windows and Mac, reduce the risk of port conflicts and might be more convenient than using port numbers. -See [README](../README.md) "Running on Windows or Mac" and "Local ingresses". +See [README](../README.md) "Running on Windows or Mac" and "Local ingresses". When there is a problem with the ingress controller, there are two options of reaching Argo CD, etc. -#### Find the IP address of the k3d docker container +#### Find the IP address of the k3d docker container Find out the IP address to access the services in the playground, the following docker command will do @@ -103,8 +112,6 @@ scripts/init-cluster.sh --bind-ports=9090:9090,9091:9091,9092:9092 After applying GOP, you can reach Argo CD on `localhost:9092`. See [README](../README.md), `Example Applications` for the port numbers. - - ## Implementation details The script basically starts a k3d cluster with a command such as this: diff --git a/renovate.json b/renovate.json index faf26f995..cb618efda 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,6 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "baseBranchPatterns": ["develop"] -} + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "baseBranchPatterns": [ + "develop" + ] +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/Application.groovy b/src/main/groovy/com/cloudogu/gitops/Application.groovy index f6c28a6de..f06c864e4 100644 --- a/src/main/groovy/com/cloudogu/gitops/Application.groovy +++ b/src/main/groovy/com/cloudogu/gitops/Application.groovy @@ -42,22 +42,22 @@ class Application { log.debug("Application finished") } - private void storeGopInformationInSecret(Config config) { - String namespace = "gop-job" // Fallback, if run from IDE - if (!config.application.gopNamespace.isEmpty()) { - // if set, take namespace from configuration - namespace = "${config.application.gopNamespace}" - } else if (this.k8sClient.k8sJavaApiClient.getCurrentNamespace() != null) { - // if gop-namespace not set, take namespace from running GOP - namespace = this.k8sClient.k8sJavaApiClient.getCurrentNamespace() - } - log.debug("Storing GOP configuration in secret 'gop-configuration' in namespace '${namespace}'") - k8sClient.createNamespace(namespace) - k8sClient.createSecret('generic', 'gop-configuration', namespace, - new Tuple2('gop-initial-password', config.DEFAULT_ADMIN_PW), - new Tuple2('gop-config', config.toYaml(true))) - } - + private void storeGopInformationInSecret(Config config) { + String namespace = "gop-job" + // Fallback, if run from IDE + if (!config.application.gopNamespace.isEmpty()) { + // if set, take namespace from configuration + namespace = "${config.application.gopNamespace}" + } else if (this.k8sClient.k8sJavaApiClient.getCurrentNamespace() != null) { + // if gop-namespace not set, take namespace from running GOP + namespace = this.k8sClient.k8sJavaApiClient.getCurrentNamespace() + } + log.debug("Storing GOP configuration in secret 'gop-configuration' in namespace '${namespace}'") + k8sClient.createNamespace(namespace) + k8sClient.createSecret('generic', 'gop-configuration', namespace, + new Tuple2('gop-initial-password', config.DEFAULT_ADMIN_PW), + new Tuple2('gop-config', config.toYaml(true))) + } List getFeatures() { return features diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy index 5456d6041..967ebc5d2 100644 --- a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy +++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy @@ -93,6 +93,6 @@ class K8sJavaApiClient { * @return */ String getCurrentNamespace() { - return this.client.getNamespace() - } + return this.client.getNamespace() + } } \ No newline at end of file diff --git a/src/main/resources/proxy-config.json b/src/main/resources/proxy-config.json index b87283c63..141ca89fa 100644 --- a/src/main/resources/proxy-config.json +++ b/src/main/resources/proxy-config.json @@ -1,9 +1,23 @@ [ - ["java.util.function.Consumer"], - ["java.util.function.Function"], - ["java.util.function.Predicate"], - ["java.util.function.Supplier"], - ["com.cloudogu.gitops.git.providers.scmmanager.apiUsersApi"], - ["com.cloudogu.gitops.git.providers.scmmanager.api.RepositoryApi"], - ["java.io.FileFilter"] -] + [ + "java.util.function.Consumer" + ], + [ + "java.util.function.Function" + ], + [ + "java.util.function.Predicate" + ], + [ + "java.util.function.Supplier" + ], + [ + "com.cloudogu.gitops.git.providers.scmmanager.apiUsersApi" + ], + [ + "com.cloudogu.gitops.git.providers.scmmanager.api.RepositoryApi" + ], + [ + "java.io.FileFilter" + ] +] \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy b/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy index 6430559e6..1e03b0077 100644 --- a/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy @@ -1,5 +1,9 @@ package com.cloudogu.gitops +import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable +import static groovy.test.GroovyAssert.shouldFail +import static org.assertj.core.api.Assertions.assertThat + import com.cloudogu.gitops.config.ApplicationConfigurator import com.cloudogu.gitops.config.CommonFeatureConfig import com.cloudogu.gitops.config.Config @@ -10,630 +14,570 @@ import com.cloudogu.gitops.features.deployment.DeploymentStrategy import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.features.git.config.ScmTenantSchema import com.cloudogu.gitops.git.GitRepoFactory -import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.kubernetes.api.HelmClient import com.cloudogu.gitops.kubernetes.api.K8sClient +import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.TestLogger import com.cloudogu.gitops.utils.git.GitHandlerForTests import com.cloudogu.gitops.utils.git.ScmManagerMock + import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mock import org.mockito.Mockito -import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable -import static groovy.test.GroovyAssert.shouldFail -import static org.assertj.core.api.Assertions.assertThat - class ApplicationConfiguratorTest { - static final String EXPECTED_REGISTRY_URL = 'http://my-reg' - static final int EXPECTED_REGISTRY_INTERNAL_PORT = 33333 - static final Config.VaultMode EXPECTED_VAULT_MODE = Config.VaultMode.dev - public static final String EXPECTED_JENKINS_URL = 'http://my-jenkins' - public static final String EXPECTED_SCMM_URL = 'http://my-scmm' - - private ApplicationConfigurator applicationConfigurator - private FileSystemUtils fileSystemUtils - private TestLogger testLogger - private CommonFeatureConfig commonFeatureConfig - private ContentLoader featureContent - private ArgoCD featureArgoCd - - @Mock - ScmManagerMock scmManagerMock = new ScmManagerMock() - - Config testConfig = Config.fromMap([ - application: [ - localHelmChartFolder: 'someValue', - namePrefix : '' - ], - registry : [ - url : EXPECTED_REGISTRY_URL, - proxyUrl : "proxy-$EXPECTED_REGISTRY_URL", - proxyUsername: "proxy-user", - proxyPassword: "proxy-pw", - internalPort : EXPECTED_REGISTRY_INTERNAL_PORT, - ], - jenkins : [ - url: EXPECTED_JENKINS_URL - ], - scm : [ - scmManager: [ - url: EXPECTED_SCMM_URL - ], - ], - multiTenant: [ - scmManager: [ - url: '' - ] - ], - features : [ - secrets: [ - vault: [ - mode: EXPECTED_VAULT_MODE - ] - ], - ] - ]) - -// // We have to set this value using env vars, which makes tests complicated, so ignore it -// Config almostEmptyConfig = Config.fromMap([ -// application: [ -// localHelmChartFolder: 'someValue', -// ], -// ]) - - @BeforeEach - void setup() { - fileSystemUtils = new FileSystemUtils() - applicationConfigurator = new ApplicationConfigurator(fileSystemUtils) - testLogger = new TestLogger(applicationConfigurator.getClass()) - commonFeatureConfig = new CommonFeatureConfig() - - K8sClient k8sClient = Mockito.mock(K8sClient) - HelmClient helmClient = Mockito.mock(HelmClient) - GitRepoFactory gitRepoFactory = Mockito.mock(GitRepoFactory) - - DeploymentStrategy deploymentStrategy = Mockito.mock(DeploymentStrategy) - - - GitHandler gitHandler = new GitHandlerForTests(testConfig, scmManagerMock) - featureContent = Mockito.spy(new ContentLoader(testConfig, k8sClient, gitRepoFactory, Mockito.mock(Jenkins), gitHandler, fileSystemUtils, deploymentStrategy)) - featureArgoCd = Mockito.spy(new ArgoCD(testConfig, k8sClient, helmClient, fileSystemUtils, gitRepoFactory, gitHandler)) - } - - @Test - void "correct config with no programm arguments"() { - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.jenkins.url).isEqualTo(EXPECTED_JENKINS_URL) - assertThat(actualConfig.jenkins.internal).isEqualTo(false) - assertThat(actualConfig.features.secrets.vault.mode).isEqualTo(EXPECTED_VAULT_MODE) - - // Dynamic value (depends on vault mode) - assertThat(actualConfig.features.secrets.active).isEqualTo(true) - } - - @Test - void "sets config application runningInsideK8s"() { - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "127.0.0.1").execute { - Config actualConfig = applicationConfigurator.initConfig(testConfig) - assertThat(actualConfig.application.runningInsideK8s).isEqualTo(true) - } - } - - @Test - void 'Sets jenkins active if external url is set'() { - testConfig.jenkins.url = 'external' - def actualConfig = applicationConfigurator.initConfig(testConfig) - assertThat(actualConfig.jenkins.active).isEqualTo(true) - } - - @Test - void 'Leaves Jenkins urlForScmm empty, if not active'() { - testConfig.jenkins.url = '' - testConfig.jenkins.active = false - - def actualConfig = applicationConfigurator.initConfig(testConfig) - assertThat(actualConfig.jenkins.urlForScm).isEmpty() - } - - @Test - void 'Fails if monitoring local is not set'() { - testConfig.application.mirrorRepos = true - testConfig.application.localHelmChartFolder = '' - - def exception = shouldFail(RuntimeException) { - commonFeatureConfig.validateConfig(testConfig) - } - assertThat(exception.message).isEqualTo('Missing config for localHelmChartFolder.\n' + - 'Either run inside the official container image or setting env var LOCAL_HELM_CHART_FOLDER=\'charts\' ' + - 'after running \'scripts/downloadHelmCharts.sh\' from the repo') - } - - @Test - void 'Fails if createImagePullSecrets is used without secrets'() { - testConfig.registry.createImagePullSecrets = true - - def exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - assertThat(exception.message).isEqualTo('createImagePullSecrets needs to be used with either registry username and password or the readOnly variants') - } - - @Test - void 'Fails if content repo is set without mandatory params'() { - - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: ''), - ] - def exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo('content.repos requires a url parameter.') - - - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.COPY, target: "missing_slash"), - ] - exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo('content.target needs / to separate namespace/group from repo name. Repo: abc') - } - - @Test - void 'Fails if COPY repo misses target parameter'() { - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.COPY), - ] - def exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo('content.repos.type COPY requires content.repos.target to be set. Repo: abc') - } - - @Test - void 'Fails if FOLDER_BASED repo has target parameter'() { - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.FOLDER_BASED, target: 'namespace/repo'), - ] - def exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo('content.repos.type FOLDER_BASED does not support target parameter. Repo: abc') - - - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.FOLDER_BASED, targetRef: 'someRef'), - ] - exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo('content.repos.type FOLDER_BASED does not support targetRef parameter. Repo: abc') - } - - @Test - void 'Fails if MIRROR repo has invalid configuration'() { - // Test missing target parameter - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.MIRROR), - ] - def exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo('content.repos.type MIRROR requires content.repos.target to be set. Repo: abc') - - // Test setting path - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.MIRROR, - target: 'namespace/repo', path: 'non-default-path'), - ] - exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo("content.repos.type MIRROR does not support path. Current path: non-default-path. Repo: abc") - - // Test templating enabled - testConfig.content.repos = [ - new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.MIRROR, - target: 'namespace/repo', templating: true), - ] - exception = shouldFail(RuntimeException) { - featureContent.preConfigInit(testConfig) - } - assertThat(exception.message).isEqualTo('content.repos.type MIRROR does not support templating. Repo: abc') - } - - @Test - void 'Ignores empty localHemlChartFolder, if mirrorRepos is not set'() { - testConfig.application.mirrorRepos = false - testConfig.application.localHelmChartFolder = '' - - applicationConfigurator.initConfig(testConfig) - // no exceptions means success - } - - @Test - void "base url: evaluates for all tools"() { - testConfig.application.baseUrl = 'http://localhost' - - testConfig.features.argocd.active = true - testConfig.features.monitoring.active = true - testConfig.features.secrets.active = true - - Config actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd.localhost") - assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("http://grafana.localhost") - assertThat(actualConfig.features.secrets.vault.url).isEqualTo("http://vault.localhost") - assertThat(actualConfig.scm.scmManager.ingress).isEqualTo("scmm.localhost") - assertThat(actualConfig.jenkins.ingress).isEqualTo("jenkins.localhost") - } - - @Test - void "base url with url-hyphens: evaluates for all tools"() { - testConfig.application.baseUrl = 'http://localhost' - testConfig.application.urlSeparatorHyphen = true - - testConfig.features.argocd.active = true - testConfig.features.monitoring.active = true - testConfig.features.secrets.active = true - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd-localhost") - assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("http://grafana-localhost") - assertThat(actualConfig.features.secrets.vault.url).isEqualTo("http://vault-localhost") - assertThat(actualConfig.scm.scmManager.ingress).isEqualTo("scmm-localhost") - assertThat(actualConfig.jenkins.ingress).isEqualTo("jenkins-localhost") - } - - @Test - void "base url: also works when port is included "() { - testConfig.application.baseUrl = 'http://localhost:8080' - testConfig.features.argocd.active = true - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd.localhost:8080") - } - - @Test - void "base url: also works when port is included and use url-hyphens is set"() { - testConfig.application.baseUrl = 'http://localhost:6502' - testConfig.features.argocd.active = true - testConfig.application.urlSeparatorHyphen = true - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd-localhost:6502") - } - - - @Test - void "base url: does not evaluate for inactive tools"() { - testConfig.features.argocd.active = false - testConfig.features.mail.active = false - testConfig.features.monitoring.active = false - testConfig.features.secrets.active = false - - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.features.argocd.url).isEqualTo('') - assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo('') - assertThat(actualConfig.features.secrets.vault.url).isEqualTo('') - } - - @Test - void "base url: individual url params take precedence"() { - testConfig.application.baseUrl = 'http://localhost' - - testConfig.features.argocd.active = true - testConfig.features.mail.active = true - testConfig.features.monitoring.active = true - testConfig.features.secrets.active = true - - testConfig.features.argocd.url = 'argocd' - testConfig.features.monitoring.grafanaUrl = 'grafana' - testConfig.features.secrets.vault.url = 'vault' - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.features.argocd.url).isEqualTo("argocd") - assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("grafana") - assertThat(actualConfig.features.secrets.vault.url).isEqualTo("vault") - } - - @Test - void "Sets namePrefix"() { - testConfig.application.namePrefix = 'my-prefix' - - def actualConfig = applicationConfigurator.initConfig(testConfig) - assertThat(actualConfig.application.namePrefix.toString()).isEqualTo('my-prefix-') - assertThat(actualConfig.application.namePrefixForEnvVars.toString()).isEqualTo('MY_PREFIX_') - } - - @Test - void "Sets namePrefix when ending in hyphen"() { - testConfig.application.namePrefix = 'my-prefix-' - - def actualConfig = applicationConfigurator.initConfig(testConfig) - assertThat(actualConfig.application.namePrefix.toString()).isEqualTo('my-prefix-') - assertThat(actualConfig.application.namePrefixForEnvVars.toString()).isEqualTo('MY_PREFIX_') - } - - @Test - void "Registry: Sets to external when only registry URL set"() { - testConfig.registry.proxyUrl = null - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.registry.internal).isEqualTo(false) - assertThat(actualConfig.registry.active).isEqualTo(true) - } - - @Test - void "Registry: Fails when proxy but no username and password set"() { - def expectedException = 'Proxy URL needs to be used with proxy-username and proxy-password' - - testConfig.registry.proxyUsername = null - def exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - assertThat(exception.message).isEqualTo(expectedException) - - testConfig.registry.proxyUsername = 'something' - testConfig.registry.proxyPassword = null - exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - assertThat(exception.message).isEqualTo(expectedException) - - testConfig.registry.proxyUsername = null - exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - assertThat(exception.message).isEqualTo(expectedException) - } - - @Test - void "validateEnvConfig allows valid env entries"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' - testConfig.features.argocd.env = [ - [name: "ENV_VAR_1", value: "value1"], - [name: "ENV_VAR_2", value: "value2"] - ] as List> - - // No exception should be thrown - applicationConfigurator.initConfig(testConfig) - } - - @Test - void "validateEnvConfig throws exception for missing 'name' in env entry"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' - testConfig.features.argocd.env = [ - [name: "ENV_VAR_1", value: "value1"], - [value: "value2"] // Missing 'name' - ] as List> - - def exception = shouldFail(IllegalArgumentException) { - applicationConfigurator.initConfig(testConfig) - featureArgoCd.postConfigInit(testConfig) - } - - assertThat(exception.message).contains("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: [value:value2]") - } - - @Test - void "validateEnvConfig throws exception for missing 'value' in env entry"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' - testConfig.features.argocd.env = [ - [name: "ENV_VAR_1", value: "value1"], - [name: "ENV_VAR_2"] // Missing 'value' - ] as List> - - def exception = shouldFail(IllegalArgumentException) { - applicationConfigurator.initConfig(testConfig) - featureArgoCd.postConfigInit(testConfig) - } - - assertThat(exception.message).contains("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: [name:ENV_VAR_2]") - } - - @Test - void "validateEnvConfig throws exception for non-map env entry"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' - testConfig.features.argocd.env = [ - [name: "ENV_VAR_1", value: "value1"], - "invalid_entry" // Invalid entry - ] as List> - - def exception = shouldFail(IllegalArgumentException) { - applicationConfigurator.initConfig(testConfig) - featureArgoCd.postConfigInit(testConfig) - } - - assertThat(exception.message).contains("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: invalid_entry") - } - - @Test - void "validateEnvConfig allows empty env list"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' - testConfig.features.argocd.env - - // No exception should be thrown - applicationConfigurator.initConfig(testConfig) - } - - @Test - void "validateEnvConfig skips validation when operator is false"() { - testConfig.features.argocd.operator = false - testConfig.features.argocd.env = [ - [name: "ENV_VAR_1", value: "value1"], - [value: "value2"] // Invalid entry, but should be ignored - ] as List> - - // No exception should be thrown - applicationConfigurator.initConfig(testConfig) - } - - @Test - void "should skip resourceInclusionsCluster setup when ArgoCD operator is not enabled"() { - testConfig.features.argocd.operator = false - - // Calling the method should not make any changes to the config - applicationConfigurator.initConfig(testConfig) - - assertThat(testLogger.getLogs().search("ArgoCD operator is not enabled. Skipping features.argocd.resourceInclusionsCluster setup.")) - .isNotEmpty() - } - - @Test - void "should validate and accept user-provided valid resourceInclusionsCluster URL"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = "https://valid-url.com" - - // Calling the method should accept the valid URL and not throw any exception - applicationConfigurator.initConfig(testConfig) - - assertThat(testConfig.features.argocd.resourceInclusionsCluster).isEqualTo("https://valid-url.com") - assertThat(testLogger.getLogs().search("Validating user-provided features.argocd.resourceInclusionsCluster URL: https://valid-url.com")) - .isNotEmpty() - assertThat(testLogger.getLogs().search("Found valid URL in features.argocd.resourceInclusionsCluster: https://valid-url.com")) - .isNotEmpty() - } - - @Test - void "should throw exception for user-provided invalid resourceInclusionsCluster URL"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = "invalid-url" - - def exception = shouldFail(IllegalArgumentException) { - applicationConfigurator.initConfig(testConfig) - } - - assertThat(exception.message).contains("Invalid URL for 'features.argocd.resourceInclusionsCluster': invalid-url.") - } - - @Test - void "should set resourceInclusionsCluster using Kubernetes ENV variables when not provided by user"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = null - - // Set Kubernetes ENV variables - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "127.0.0.1") - .and("KUBERNETES_SERVICE_PORT", "6443") - .execute { - Config actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.features.argocd.resourceInclusionsCluster).isEqualTo("https://127.0.0.1:6443") - - assertThat(testLogger.getLogs().search("Successfully set features.argocd.resourceInclusionsCluster via Kubernetes ENV to: https://127.0.0.1:6443")) - .isNotEmpty() - } - } - - @Test - void "MultiTenant Mode Central SCM Url"() { - testConfig.multiTenant.scmManager.url = "scmm.localhost/scm" - testConfig.application.namePrefix = "foo" - applicationConfigurator.initConfig(testConfig) - assertThat(testConfig.multiTenant.scmManager.url).toString() == "scmm.localhost/scm/" - } - - @Test - void "should throw exception when Kubernetes ENV variables are not set and resourceInclusionsCluster is null"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = null - - def exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - - assertThat(exception.message).contains("Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true. Ensure Kubernetes environment variables 'KUBERNETES_SERVICE_HOST' and 'KUBERNETES_SERVICE_PORT' are set properly.") - } - - @Test - void "should throw exception when Kubernetes ENV variables are not set and resourceInclusionsCluster is empty"() { - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = '' - - def exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - - assertThat(exception.message).contains("Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true. Ensure Kubernetes environment variables 'KUBERNETES_SERVICE_HOST' and 'KUBERNETES_SERVICE_PORT' are set properly.") - } - - @Test - void "should throw exception for invalid Kubernetes constructed URL"() { - // Set ArgoCD operator to true - testConfig.features.argocd.operator = true - testConfig.features.argocd.resourceInclusionsCluster = null - - // Set invalid Kubernetes ENV variables - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "invalid_host") - .and("KUBERNETES_SERVICE_PORT", "not_a_port") - .execute { - def exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - - assertThat(exception.message).contains("Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true.") - } - - assertThat(testLogger.getLogs().search("Constructed internal Kubernetes API Server URL: https://invalid_host:not_a_port")).isNotEmpty() - } - - List getAllFieldNames(Class clazz, String parentField = '', List fieldNames = []) { - clazz.declaredFields.each { field -> - def currentField = parentField + field.name - if (field.type instanceof Class - && !field.type.isArray() - && field.type.name.startsWith(Config.class.getPackageName())) { - println "nested class $field.type, $currentField + '.', $fieldNames" - getAllFieldNames(field.type, currentField + '.', fieldNames) - } else { - if (!field.name.startsWith('_') && !field.name.startsWith('$') && field.name != 'metaClass') { - fieldNames.add(currentField) - } - } - } - return fieldNames - } - - List getAllKeys(Map map, String parentKey = '', List keysList = []) { - map.each { key, value -> - def currentKey = parentKey + key - if (value instanceof Map && !value.isEmpty()) { - getAllKeys(value, currentKey + '.', keysList) - } else { - keysList.add(currentKey) - } - } - return keysList - } - - private static Config minimalConfig() { - def config = new Config() - config.application = new Config.ApplicationSchema( - localHelmChartFolder: 'someValue', - namePrefix: '' - ) - config.scm = new ScmTenantSchema( - scmManager: new ScmTenantSchema.ScmManagerTenantConfig( - url: '' - ) - ) - return config - } -} + static final String EXPECTED_REGISTRY_URL = 'http://my-reg' + static final int EXPECTED_REGISTRY_INTERNAL_PORT = 33333 + static final Config.VaultMode EXPECTED_VAULT_MODE = Config.VaultMode.dev + public static final String EXPECTED_JENKINS_URL = 'http://my-jenkins' + public static final String EXPECTED_SCMM_URL = 'http://my-scmm' + + private ApplicationConfigurator applicationConfigurator + private FileSystemUtils fileSystemUtils + private TestLogger testLogger + private CommonFeatureConfig commonFeatureConfig + private ContentLoader featureContent + private ArgoCD featureArgoCd + + @Mock + ScmManagerMock scmManagerMock = new ScmManagerMock() + + Config testConfig = Config.fromMap([application: [localHelmChartFolder: 'someValue', + namePrefix : ''], + registry : [url : EXPECTED_REGISTRY_URL, + proxyUrl : "proxy-$EXPECTED_REGISTRY_URL", + proxyUsername: "proxy-user", + proxyPassword: "proxy-pw", + internalPort : EXPECTED_REGISTRY_INTERNAL_PORT,], + jenkins : [url: EXPECTED_JENKINS_URL], + scm : [scmManager: [url: EXPECTED_SCMM_URL],], + multiTenant: [scmManager: [url: '']], + features : [secrets: [vault: [mode: EXPECTED_VAULT_MODE]],]]) + + // // We have to set this value using env vars, which makes tests complicated, so ignore it + // Config almostEmptyConfig = Config.fromMap([ + // application: [ + // localHelmChartFolder: 'someValue', + // ], + // ]) + + @BeforeEach + void setup() { + fileSystemUtils = new FileSystemUtils() + applicationConfigurator = new ApplicationConfigurator(fileSystemUtils) + testLogger = new TestLogger(applicationConfigurator.getClass()) + commonFeatureConfig = new CommonFeatureConfig() + + K8sClient k8sClient = Mockito.mock(K8sClient) + HelmClient helmClient = Mockito.mock(HelmClient) + GitRepoFactory gitRepoFactory = Mockito.mock(GitRepoFactory) + + DeploymentStrategy deploymentStrategy = Mockito.mock(DeploymentStrategy) + + GitHandler gitHandler = new GitHandlerForTests(testConfig, scmManagerMock) + featureContent = Mockito.spy(new ContentLoader(testConfig, k8sClient, gitRepoFactory, Mockito.mock(Jenkins), gitHandler, fileSystemUtils, deploymentStrategy)) + featureArgoCd = Mockito.spy(new ArgoCD(testConfig, k8sClient, helmClient, fileSystemUtils, gitRepoFactory, gitHandler)) + } + + @Test + void "correct config with no programm arguments"() { + + def actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.jenkins.url).isEqualTo(EXPECTED_JENKINS_URL) + assertThat(actualConfig.jenkins.internal).isEqualTo(false) + assertThat(actualConfig.features.secrets.vault.mode).isEqualTo(EXPECTED_VAULT_MODE) + + // Dynamic value (depends on vault mode) + assertThat(actualConfig.features.secrets.active).isEqualTo(true) + } + + @Test + void "sets config application runningInsideK8s"() { + withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "127.0.0.1").execute { + Config actualConfig = applicationConfigurator.initConfig(testConfig) + assertThat(actualConfig.application.runningInsideK8s).isEqualTo(true) + } + } + + @Test + void 'Sets jenkins active if external url is set'() { + testConfig.jenkins.url = 'external' + def actualConfig = applicationConfigurator.initConfig(testConfig) + assertThat(actualConfig.jenkins.active).isEqualTo(true) + } + + @Test + void 'Leaves Jenkins urlForScmm empty, if not active'() { + testConfig.jenkins.url = '' + testConfig.jenkins.active = false + + def actualConfig = applicationConfigurator.initConfig(testConfig) + assertThat(actualConfig.jenkins.urlForScm).isEmpty() + } + + @Test + void 'Fails if monitoring local is not set'() { + testConfig.application.mirrorRepos = true + testConfig.application.localHelmChartFolder = '' + + def exception = shouldFail(RuntimeException) { + commonFeatureConfig.validateConfig(testConfig) + } + assertThat(exception.message).isEqualTo('Missing config for localHelmChartFolder.\n' + + 'Either run inside the official container image or setting env var LOCAL_HELM_CHART_FOLDER=\'charts\' ' + + 'after running \'scripts/downloadHelmCharts.sh\' from the repo') + } + + @Test + void 'Fails if createImagePullSecrets is used without secrets'() { + testConfig.registry.createImagePullSecrets = true + + def exception = shouldFail(RuntimeException) { + applicationConfigurator.initConfig(testConfig) + } + assertThat(exception.message).isEqualTo('createImagePullSecrets needs to be used with either registry username and password or the readOnly variants') + } + + @Test + void 'Fails if content repo is set without mandatory params'() { + + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: ''),] + def exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo('content.repos requires a url parameter.') + + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.COPY, target: "missing_slash"),] + exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo('content.target needs / to separate namespace/group from repo name. Repo: abc') + } + + @Test + void 'Fails if COPY repo misses target parameter'() { + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.COPY),] + def exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo('content.repos.type COPY requires content.repos.target to be set. Repo: abc') + } + + @Test + void 'Fails if FOLDER_BASED repo has target parameter'() { + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.FOLDER_BASED, target: 'namespace/repo'),] + def exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo('content.repos.type FOLDER_BASED does not support target parameter. Repo: abc') + + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.FOLDER_BASED, targetRef: 'someRef'),] + exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo('content.repos.type FOLDER_BASED does not support targetRef parameter. Repo: abc') + } + + @Test + void 'Fails if MIRROR repo has invalid configuration'() { + // Test missing target parameter + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.MIRROR),] + def exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo('content.repos.type MIRROR requires content.repos.target to be set. Repo: abc') + + // Test setting path + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.MIRROR, + target: 'namespace/repo', path: 'non-default-path'),] + exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo("content.repos.type MIRROR does not support path. Current path: non-default-path. Repo: abc") + + // Test templating enabled + testConfig.content.repos = [new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.MIRROR, + target: 'namespace/repo', templating: true),] + exception = shouldFail(RuntimeException) { + featureContent.preConfigInit(testConfig) + } + assertThat(exception.message).isEqualTo('content.repos.type MIRROR does not support templating. Repo: abc') + } + + @Test + void 'Ignores empty localHemlChartFolder, if mirrorRepos is not set'() { + testConfig.application.mirrorRepos = false + testConfig.application.localHelmChartFolder = '' + + applicationConfigurator.initConfig(testConfig) + // no exceptions means success + } + + @Test + void "base url: evaluates for all tools"() { + testConfig.application.baseUrl = 'http://localhost' + + testConfig.features.argocd.active = true + testConfig.features.monitoring.active = true + testConfig.features.secrets.active = true + + Config actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd.localhost") + assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("http://grafana.localhost") + assertThat(actualConfig.features.secrets.vault.url).isEqualTo("http://vault.localhost") + assertThat(actualConfig.scm.scmManager.ingress).isEqualTo("scmm.localhost") + assertThat(actualConfig.jenkins.ingress).isEqualTo("jenkins.localhost") + } + + @Test + void "base url with url-hyphens: evaluates for all tools"() { + testConfig.application.baseUrl = 'http://localhost' + testConfig.application.urlSeparatorHyphen = true + + testConfig.features.argocd.active = true + testConfig.features.monitoring.active = true + testConfig.features.secrets.active = true + + def actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd-localhost") + assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("http://grafana-localhost") + assertThat(actualConfig.features.secrets.vault.url).isEqualTo("http://vault-localhost") + assertThat(actualConfig.scm.scmManager.ingress).isEqualTo("scmm-localhost") + assertThat(actualConfig.jenkins.ingress).isEqualTo("jenkins-localhost") + } + + @Test + void "base url: also works when port is included "() { + testConfig.application.baseUrl = 'http://localhost:8080' + testConfig.features.argocd.active = true + + def actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd.localhost:8080") + } + + @Test + void "base url: also works when port is included and use url-hyphens is set"() { + testConfig.application.baseUrl = 'http://localhost:6502' + testConfig.features.argocd.active = true + testConfig.application.urlSeparatorHyphen = true + + def actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.features.argocd.url).isEqualTo("http://argocd-localhost:6502") + } + + @Test + void "base url: does not evaluate for inactive tools"() { + testConfig.features.argocd.active = false + testConfig.features.mail.active = false + testConfig.features.monitoring.active = false + testConfig.features.secrets.active = false + + def actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.features.argocd.url).isEqualTo('') + assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo('') + assertThat(actualConfig.features.secrets.vault.url).isEqualTo('') + } + + @Test + void "base url: individual url params take precedence"() { + testConfig.application.baseUrl = 'http://localhost' + + testConfig.features.argocd.active = true + testConfig.features.mail.active = true + testConfig.features.monitoring.active = true + testConfig.features.secrets.active = true + + testConfig.features.argocd.url = 'argocd' + testConfig.features.monitoring.grafanaUrl = 'grafana' + testConfig.features.secrets.vault.url = 'vault' + + def actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.features.argocd.url).isEqualTo("argocd") + assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("grafana") + assertThat(actualConfig.features.secrets.vault.url).isEqualTo("vault") + } + + @Test + void "Sets namePrefix"() { + testConfig.application.namePrefix = 'my-prefix' + + def actualConfig = applicationConfigurator.initConfig(testConfig) + assertThat(actualConfig.application.namePrefix.toString()).isEqualTo('my-prefix-') + assertThat(actualConfig.application.namePrefixForEnvVars.toString()).isEqualTo('MY_PREFIX_') + } + + @Test + void "Sets namePrefix when ending in hyphen"() { + testConfig.application.namePrefix = 'my-prefix-' + + def actualConfig = applicationConfigurator.initConfig(testConfig) + assertThat(actualConfig.application.namePrefix.toString()).isEqualTo('my-prefix-') + assertThat(actualConfig.application.namePrefixForEnvVars.toString()).isEqualTo('MY_PREFIX_') + } + + @Test + void "Registry: Sets to external when only registry URL set"() { + testConfig.registry.proxyUrl = null + + def actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.registry.internal).isEqualTo(false) + assertThat(actualConfig.registry.active).isEqualTo(true) + } + + @Test + void "Registry: Fails when proxy but no username and password set"() { + def expectedException = 'Proxy URL needs to be used with proxy-username and proxy-password' + + testConfig.registry.proxyUsername = null + def exception = shouldFail(RuntimeException) { + applicationConfigurator.initConfig(testConfig) + } + assertThat(exception.message).isEqualTo(expectedException) + + testConfig.registry.proxyUsername = 'something' + testConfig.registry.proxyPassword = null + exception = shouldFail(RuntimeException) { + applicationConfigurator.initConfig(testConfig) + } + assertThat(exception.message).isEqualTo(expectedException) + + testConfig.registry.proxyUsername = null + exception = shouldFail(RuntimeException) { + applicationConfigurator.initConfig(testConfig) + } + assertThat(exception.message).isEqualTo(expectedException) + } + + @Test + void "validateEnvConfig allows valid env entries"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' + testConfig.features.argocd.env = [[name: "ENV_VAR_1", value: "value1"], + [name: "ENV_VAR_2", value: "value2"]] as List> + + // No exception should be thrown + applicationConfigurator.initConfig(testConfig) + } + + @Test + void "validateEnvConfig throws exception for missing 'name' in env entry"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' + testConfig.features.argocd.env = [[name: "ENV_VAR_1", value: "value1"], + [value: "value2"] // Missing 'name' + ] as List> + + def exception = shouldFail(IllegalArgumentException) { + applicationConfigurator.initConfig(testConfig) + featureArgoCd.postConfigInit(testConfig) + } + + assertThat(exception.message).contains("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: [value:value2]") + } + + @Test + void "validateEnvConfig throws exception for missing 'value' in env entry"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' + testConfig.features.argocd.env = [[name: "ENV_VAR_1", value: "value1"], + [name: "ENV_VAR_2"] // Missing 'value' + ] as List> + + def exception = shouldFail(IllegalArgumentException) { + applicationConfigurator.initConfig(testConfig) + featureArgoCd.postConfigInit(testConfig) + } + + assertThat(exception.message).contains("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: [name:ENV_VAR_2]") + } + + @Test + void "validateEnvConfig throws exception for non-map env entry"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' + testConfig.features.argocd.env = [[name: "ENV_VAR_1", value: "value1"], + "invalid_entry" // Invalid entry + ] as List> + + def exception = shouldFail(IllegalArgumentException) { + applicationConfigurator.initConfig(testConfig) + featureArgoCd.postConfigInit(testConfig) + } + + assertThat(exception.message).contains("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: invalid_entry") + } + + @Test + void "validateEnvConfig allows empty env list"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' + testConfig.features.argocd.env + + // No exception should be thrown + applicationConfigurator.initConfig(testConfig) + } + + @Test + void "validateEnvConfig skips validation when operator is false"() { + testConfig.features.argocd.operator = false + testConfig.features.argocd.env = [[name: "ENV_VAR_1", value: "value1"], + [value: "value2"] // Invalid entry, but should be ignored + ] as List> + + // No exception should be thrown + applicationConfigurator.initConfig(testConfig) + } + + @Test + void "should skip resourceInclusionsCluster setup when ArgoCD operator is not enabled"() { + testConfig.features.argocd.operator = false + + // Calling the method should not make any changes to the config + applicationConfigurator.initConfig(testConfig) + + assertThat(testLogger.getLogs().search("ArgoCD operator is not enabled. Skipping features.argocd.resourceInclusionsCluster setup.")) + .isNotEmpty() + } + + @Test + void "should validate and accept user-provided valid resourceInclusionsCluster URL"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = "https://valid-url.com" + + // Calling the method should accept the valid URL and not throw any exception + applicationConfigurator.initConfig(testConfig) + + assertThat(testConfig.features.argocd.resourceInclusionsCluster).isEqualTo("https://valid-url.com") + assertThat(testLogger.getLogs().search("Validating user-provided features.argocd.resourceInclusionsCluster URL: https://valid-url.com")) + .isNotEmpty() + assertThat(testLogger.getLogs().search("Found valid URL in features.argocd.resourceInclusionsCluster: https://valid-url.com")) + .isNotEmpty() + } + + @Test + void "should throw exception for user-provided invalid resourceInclusionsCluster URL"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = "invalid-url" + + def exception = shouldFail(IllegalArgumentException) { + applicationConfigurator.initConfig(testConfig) + } + + assertThat(exception.message).contains("Invalid URL for 'features.argocd.resourceInclusionsCluster': invalid-url.") + } + + @Test + void "should set resourceInclusionsCluster using Kubernetes ENV variables when not provided by user"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = null + + // Set Kubernetes ENV variables + withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "127.0.0.1") + .and("KUBERNETES_SERVICE_PORT", "6443") + .execute { + Config actualConfig = applicationConfigurator.initConfig(testConfig) + + assertThat(actualConfig.features.argocd.resourceInclusionsCluster).isEqualTo("https://127.0.0.1:6443") + + assertThat(testLogger.getLogs().search("Successfully set features.argocd.resourceInclusionsCluster via Kubernetes ENV to: https://127.0.0.1:6443")) + .isNotEmpty() + } + } + + @Test + void "MultiTenant Mode Central SCM Url"() { + testConfig.multiTenant.scmManager.url = "scmm.localhost/scm" + testConfig.application.namePrefix = "foo" + applicationConfigurator.initConfig(testConfig) + assertThat(testConfig.multiTenant.scmManager.url).toString() == "scmm.localhost/scm/" + } + + @Test + void "should throw exception when Kubernetes ENV variables are not set and resourceInclusionsCluster is null"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = null + + def exception = shouldFail(RuntimeException) { + applicationConfigurator.initConfig(testConfig) + } + + assertThat(exception.message).contains("Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true. Ensure Kubernetes environment variables 'KUBERNETES_SERVICE_HOST' and 'KUBERNETES_SERVICE_PORT' are set properly.") + } + + @Test + void "should throw exception when Kubernetes ENV variables are not set and resourceInclusionsCluster is empty"() { + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = '' + + def exception = shouldFail(RuntimeException) { + applicationConfigurator.initConfig(testConfig) + } + + assertThat(exception.message).contains("Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true. Ensure Kubernetes environment variables 'KUBERNETES_SERVICE_HOST' and 'KUBERNETES_SERVICE_PORT' are set properly.") + } + + @Test + void "should throw exception for invalid Kubernetes constructed URL"() { + // Set ArgoCD operator to true + testConfig.features.argocd.operator = true + testConfig.features.argocd.resourceInclusionsCluster = null + + // Set invalid Kubernetes ENV variables + withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "invalid_host") + .and("KUBERNETES_SERVICE_PORT", "not_a_port") + .execute { + def exception = shouldFail(RuntimeException) { + applicationConfigurator.initConfig(testConfig) + } + + assertThat(exception.message).contains("Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true.") + } + + assertThat(testLogger.getLogs().search("Constructed internal Kubernetes API Server URL: https://invalid_host:not_a_port")).isNotEmpty() + } + + List getAllFieldNames(Class clazz, String parentField = '', List fieldNames = []) { + clazz.declaredFields.each { field -> + def currentField = parentField + field.name + if (field.type instanceof Class && !field.type.isArray() && field.type.name.startsWith(Config.class.getPackageName())) { + println "nested class $field.type, $currentField + '.', $fieldNames" + getAllFieldNames(field.type, currentField + '.', fieldNames) + } else { + if (!field.name.startsWith('_') && !field.name.startsWith('$') && field.name != 'metaClass') { + fieldNames.add(currentField) + } + } + } + return fieldNames + } + + List getAllKeys(Map map, String parentKey = '', List keysList = []) { + map.each { key, value -> + def currentKey = parentKey + key + if (value instanceof Map && !value.isEmpty()) { + getAllKeys(value, currentKey + '.', keysList) + } else { + keysList.add(currentKey) + } + } + return keysList + } + + private static Config minimalConfig() { + def config = new Config() + config.application = new Config.ApplicationSchema(localHelmChartFolder: 'someValue', + namePrefix: '') + config.scm = new ScmTenantSchema(scmManager: new ScmTenantSchema.ScmManagerTenantConfig(url: '')) + return config + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy b/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy index 6f85b4546..fe357ce2d 100644 --- a/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy @@ -1,136 +1,122 @@ package com.cloudogu.gitops +import static org.assertj.core.api.Assertions.assertThat + import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.git.config.ScmTenantSchema -import groovy.transform.CompileStatic + import io.micronaut.context.ApplicationContext -import org.junit.jupiter.api.Test -import static org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test class ApplicationTest { - private Config config = new Config() + private Config config = new Config() + + @Test + void 'feature\'s ordering is correct'() { + def application = ApplicationContext.run() + .registerSingleton(config) + .getBean(Application) + def features = application.features.collect { it.class.simpleName } - @Test - void 'feature\'s ordering is correct'() { - def application = ApplicationContext.run() - .registerSingleton(config) - .getBean(Application) - def features = application.features.collect { it.class.simpleName } + assertThat(features).isEqualTo(['Registry', 'GitHandler', 'Jenkins', 'ArgoCD', 'Ingress', 'CertManager', 'Monitoring', 'ExternalSecretsOperator', 'Vault', 'ContentLoader']) + } - assertThat(features).isEqualTo(['Registry', 'GitHandler' ,'Jenkins', 'ArgoCD', 'Ingress', 'CertManager', 'Monitoring', 'ExternalSecretsOperator', 'Vault', 'ContentLoader']) - } + @Test + void 'get active namespaces correctly'() { + config.registry.active = true + config.jenkins.active = true + config.features.monitoring.active = true + config.features.argocd.active = true + config.features.ingress.active = true + config.application.namePrefix = 'test1-' + config.content.namespaces = ['${config.application.namePrefix}example-apps-staging', + '${config.application.namePrefix}example-apps-production'] + List namespaceList = new ArrayList<>(Arrays.asList("test1-argocd", + "test1-example-apps-staging", + "test1-example-apps-production", + "test1-" + config.features.ingress.ingressNamespace, + "test1-monitoring", + "test1-registry", + "test1-jenkins")) + def application = ApplicationContext.run() + .registerSingleton(config) + .getBean(Application) + application.setNamespaceListToConfig(config) + assertThat(config.application.namespaces.getActiveNamespaces()).containsExactlyInAnyOrderElementsOf(namespaceList) + } - @Test - void 'get active namespaces correctly'() { - config.registry.active = true - config.jenkins.active = true - config.features.monitoring.active = true - config.features.argocd.active = true - config.features.ingress.active = true - config.application.namePrefix = 'test1-' - config.content.namespaces = [ - '${config.application.namePrefix}example-apps-staging', - '${config.application.namePrefix}example-apps-production' - ] - List namespaceList = new ArrayList<>(Arrays.asList( - "test1-argocd", - "test1-example-apps-staging", - "test1-example-apps-production", - "test1-" + config.features.ingress.ingressNamespace, - "test1-monitoring", - "test1-registry", - "test1-jenkins" - )) - def application = ApplicationContext.run() - .registerSingleton(config) - .getBean(Application) - application.setNamespaceListToConfig(config) - assertThat(config.application.namespaces.getActiveNamespaces()).containsExactlyInAnyOrderElementsOf(namespaceList) - } + @Test + void 'get active namespaces correctly in Openshift'() { + config.registry.active = true + config.jenkins.active = true + config.features.monitoring.active = true + config.features.argocd.active = true + config.features.ingress.active = true + config.application.namePrefix = 'test1-' + config.application.openshift = true + config.content.namespaces = ['${config.application.namePrefix}example-apps-staging', + '${config.application.namePrefix}example-apps-production'] + List namespaceList = new ArrayList<>(Arrays.asList("test1-argocd", + "test1-example-apps-staging", + "test1-example-apps-production", + "test1-" + config.features.ingress.ingressNamespace, + "test1-monitoring", + "test1-registry", + "test1-jenkins")) + def application = ApplicationContext.run() + .registerSingleton(config) + .getBean(Application) + application.setNamespaceListToConfig(config) + assertThat(config.application.namespaces.getActiveNamespaces()).containsExactlyInAnyOrderElementsOf(namespaceList) + } - @Test - void 'get active namespaces correctly in Openshift'() { - config.registry.active = true - config.jenkins.active = true - config.features.monitoring.active = true - config.features.argocd.active = true - config.features.ingress.active = true - config.application.namePrefix = 'test1-' - config.application.openshift = true - config.content.namespaces = [ - '${config.application.namePrefix}example-apps-staging', - '${config.application.namePrefix}example-apps-production' - ] - List namespaceList = new ArrayList<>(Arrays.asList( - "test1-argocd", - "test1-example-apps-staging", - "test1-example-apps-production", - "test1-" + config.features.ingress.ingressNamespace, - "test1-monitoring", - "test1-registry", - "test1-jenkins" - )) - def application = ApplicationContext.run() - .registerSingleton(config) - .getBean(Application) - application.setNamespaceListToConfig(config) - assertThat(config.application.namespaces.getActiveNamespaces()).containsExactlyInAnyOrderElementsOf(namespaceList) - } + @Test + void 'handles content namespaces without template'() { + config.content.namespaces = ['example-apps-staging', + 'example-apps-production'] + def application = ApplicationContext.run() + .registerSingleton(config) + .getBean(Application) + application.setNamespaceListToConfig(config) + assertThat(config.application.namespaces.getActiveNamespaces()).containsAll(["example-apps-staging", + "example-apps-production",]) + } - @Test - void 'handles content namespaces without template'() { - config.content.namespaces = [ - 'example-apps-staging', - 'example-apps-production' - ] - def application = ApplicationContext.run() - .registerSingleton(config) - .getBean(Application) - application.setNamespaceListToConfig(config) - assertThat(config.application.namespaces.getActiveNamespaces()).containsAll([ - "example-apps-staging", - "example-apps-production", - ]) - } + @Test + void 'handles empty content namespaces'() { + def application = ApplicationContext.run() + .registerSingleton(config) + .getBean(Application) + application.setNamespaceListToConfig(config) + // No exception == happy + } - @Test - void 'handles empty content namespaces'() { - def application = ApplicationContext.run() - .registerSingleton(config) - .getBean(Application) - application.setNamespaceListToConfig(config) - // No exception == happy - } - @Test - void 'get active namespaces correctly in Openshift if jenkins and scm are external'() { - config.registry.active = true - config.jenkins.active = true - config.jenkins.internal = false - config.scm.scmManager = new ScmTenantSchema.ScmManagerTenantConfig() - config.scm.scmManager.internal = false - config.features.monitoring.active = true - config.features.argocd.active = true - config.features.ingress.active = true - config.application.namePrefix = 'test1-' - config.application.openshift = true - config.content.namespaces = [ - '${config.application.namePrefix}example-apps-staging', - '${config.application.namePrefix}example-apps-production' - ] - List namespaceList = new ArrayList<>(Arrays.asList( - "test1-argocd", - "test1-example-apps-staging", - "test1-example-apps-production", - "test1-" + config.features.ingress.ingressNamespace, - "test1-monitoring", - "test1-registry", - )) - def application = ApplicationContext.run() - .registerSingleton(config) - .getBean(Application) - application.setNamespaceListToConfig(config) - assertThat(config.application.namespaces.getActiveNamespaces()).containsExactlyInAnyOrderElementsOf(namespaceList) - } -} + @Test + void 'get active namespaces correctly in Openshift if jenkins and scm are external'() { + config.registry.active = true + config.jenkins.active = true + config.jenkins.internal = false + config.scm.scmManager = new ScmTenantSchema.ScmManagerTenantConfig() + config.scm.scmManager.internal = false + config.features.monitoring.active = true + config.features.argocd.active = true + config.features.ingress.active = true + config.application.namePrefix = 'test1-' + config.application.openshift = true + config.content.namespaces = ['${config.application.namePrefix}example-apps-staging', + '${config.application.namePrefix}example-apps-production'] + List namespaceList = new ArrayList<>(Arrays.asList("test1-argocd", + "test1-example-apps-staging", + "test1-example-apps-production", + "test1-" + config.features.ingress.ingressNamespace, + "test1-monitoring", + "test1-registry",)) + def application = ApplicationContext.run() + .registerSingleton(config) + .getBean(Application) + application.setNamespaceListToConfig(config) + assertThat(config.application.namespaces.getActiveNamespaces()).containsExactlyInAnyOrderElementsOf(namespaceList) + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy index 6e0d25fd2..88fce3884 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy @@ -49,8 +49,8 @@ class CertManagerTest { createCertManager().install() verify(deploymentStrategy).deployFeature('https://charts.jetstack.io', 'cert-manager', - 'cert-manager', chartVersion, 'cert-manager', - 'cert-manager', temporaryYamlFile, RepoType.HELM) + 'cert-manager', chartVersion, 'cert-manager', + 'cert-manager', temporaryYamlFile, RepoType.HELM) } @Test @@ -98,8 +98,8 @@ class CertManagerTest { assertThat(helmConfig.value.version).isEqualTo(chartVersion) // important check: scmmRepoUrl is overridden with our values. verify(deploymentStrategy).deployFeature('http://scmm.scm-manager.svc.cluster.local/scm/repo/a/b', - 'cert-manager', '.', chartVersion, 'cert-manager', - 'cert-manager', temporaryYamlFile, RepoType.GIT) + 'cert-manager', '.', chartVersion, 'cert-manager', + 'cert-manager', temporaryYamlFile, RepoType.GIT) } @Test diff --git a/src/test/groovy/com/cloudogu/gitops/features/ContentLoaderTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/ContentLoaderTest.groovy index 0cce931e7..d9a94a3e7 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/ContentLoaderTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/ContentLoaderTest.groovy @@ -1,9 +1,20 @@ package com.cloudogu.gitops.features +import static ContentLoader.RepoCoordinate +import static com.cloudogu.gitops.config.Config.ContentRepoType +import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema +import static com.cloudogu.gitops.config.Config.OverwriteMode +import static groovy.test.GroovyAssert.shouldFail +import static org.assertj.core.api.Assertions.assertThat +import static org.mockito.ArgumentMatchers.any +import static org.mockito.ArgumentMatchers.eq +import static org.mockito.Mockito.* + import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.config.Credentials import com.cloudogu.gitops.features.deployment.DeploymentStrategy import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.features.git.config.ScmTenantSchema import com.cloudogu.gitops.git.GitRepoFactory import com.cloudogu.gitops.kubernetes.api.K8sClient import com.cloudogu.gitops.utils.CommandExecutor @@ -14,9 +25,12 @@ import com.cloudogu.gitops.utils.git.GitHandlerForTests import com.cloudogu.gitops.utils.git.ScmManagerMock import com.cloudogu.gitops.utils.git.TestGitRepoFactory import com.cloudogu.gitops.utils.git.TestScmManagerApiClient -import com.cloudogu.gitops.features.git.config.ScmTenantSchema + +import java.nio.file.Files +import java.nio.file.Path import groovy.util.logging.Slf4j import groovy.yaml.YamlSlurper + import io.fabric8.kubernetes.api.model.Secret import io.fabric8.kubernetes.api.model.SecretBuilder import io.fabric8.kubernetes.client.KubernetesClient @@ -33,1186 +47,1041 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import org.mockito.ArgumentCaptor -import java.nio.file.Files -import java.nio.file.Path - -import static ContentLoader.RepoCoordinate -import static com.cloudogu.gitops.config.Config.ContentRepoType -import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema -import static com.cloudogu.gitops.config.Config.OverwriteMode -import static groovy.test.GroovyAssert.shouldFail -import static org.assertj.core.api.Assertions.assertThat -import static org.mockito.ArgumentMatchers.any -import static org.mockito.ArgumentMatchers.eq -import static org.mockito.Mockito.* - @Slf4j -@EnableKubernetesMockClient(crud=true) +@EnableKubernetesMockClient(crud = true) class ContentLoaderTest { - static List foldersToDelete = new ArrayList() - - Config config = new Config( - application: new Config.ApplicationSchema( - namePrefix: 'foo-' - ), - scm : new ScmTenantSchema( - scmManager: new ScmTenantSchema.ScmManagerTenantConfig( - url: '' - ) - ), - registry : new Config.RegistrySchema( - url : 'reg-url', - path : 'reg-path', - username : 'reg-user', - password : 'reg-pw', - createImagePullSecrets: false - ) - ) - - KubernetesClient client - CommandExecutorForTest k8sCommands = new CommandExecutorForTest() - K8sClientForTest k8sClient = new K8sClientForTest(config, k8sCommands) - TestGitRepoFactory scmmRepoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) - TestScmManagerApiClient scmmApiClient = new TestScmManagerApiClient(config) - Jenkins jenkins = mock(Jenkins.class) - ScmManagerMock scmManagerMock = new ScmManagerMock() - GitHandler gitHandler = new GitHandlerForTests(config, scmManagerMock) - DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) - FileSystemUtils fileSystemUtils = new FileSystemUtils() - - @TempDir - File tmpDir - - - List expectedTargetRepos = [ - new RepoCoordinate(namespace: "common", repoName: "repo"), - new RepoCoordinate(namespace: "ns1a", repoName: "repo1a1"), - new RepoCoordinate(namespace: "ns1a", repoName: "repo1a2"), - new RepoCoordinate(namespace: "ns1b", repoName: "repo1b1"), - new RepoCoordinate(namespace: "ns1b", repoName: "repo1b2"), - new RepoCoordinate(namespace: "ns2a", repoName: "repo2a1"), - new RepoCoordinate(namespace: "ns2a", repoName: "repo2a2"), - new RepoCoordinate(namespace: "ns2b", repoName: "repo2b1"), - new RepoCoordinate(namespace: "ns2b", repoName: "repo2b2"), - new RepoCoordinate(namespace: "copy", repoName: "repo1"), - new RepoCoordinate(namespace: "copy", repoName: "repo2"), - ] - - List contentRepos = [ - // copy-typed repo writing to their own target - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), type: ContentRepoType.COPY, target: 'copy/repo1'), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'copy/repo2', path: 'subPath'), - - // Same folder as in copyRepos -> Should be combined - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath'), - - // Contains ftl - new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED, templating: true), - // Contains a templated file that should be ignored - new ContentRepositorySchema(url: createContentRepo('folderBasedRepo2'), type: ContentRepoType.FOLDER_BASED, path: 'subPath'), - - ] - - @AfterAll - static void cleanFolders() { - foldersToDelete.each { it.deleteDir() } - - } - - - @Test - void 'deploys image pull secrets'() { - config.registry.createImagePullSecrets = true - config.content.namespaces = ['example-apps-staging', 'example-apps-production'] - - createContent(config).install() - - assertRegistrySecrets('reg-user', 'reg-pw') - } - - @Test - void 'deploys image pull secrets from read-only vars'() { - config.registry.createImagePullSecrets = true - config.content.namespaces = ['example-apps-staging', 'example-apps-production'] - config.registry.readOnlyUsername = 'other-user' - config.registry.readOnlyPassword = 'other-pw' - - createContent(config).install() - - assertRegistrySecrets('other-user', 'other-pw') - } - - @Test - void 'deploys additional image pull secrets for proxy registry'() { - config.registry.createImagePullSecrets = true - config.content.namespaces = ['example-apps-staging', 'example-apps-production'] - config.registry.twoRegistries = true - config.registry.proxyUrl = 'proxy-url' - config.registry.proxyUsername = 'proxy-user' - config.registry.proxyPassword = 'proxy-pw' - - // Simulate argocd Namespace does not exist - k8sCommands.enqueueOutput(new CommandExecutor.Output('namespace not found', '', 1)) // Namespace not exit - k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl - k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl - k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl - k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl - k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 1)) // Namespace not exit - - createContent(config).install() - - assertRegistrySecrets('reg-user', 'reg-pw') - - k8sClient.commandExecutorForTest.assertExecuted('kubectl create namespace example-apps-staging') - k8sClient.commandExecutorForTest.assertExecuted('kubectl create namespace example-apps-production') - k8sClient.commandExecutorForTest.assertExecuted( - 'kubectl create secret docker-registry proxy-registry -n example-apps-staging' + - ' --docker-server proxy-url --docker-username proxy-user --docker-password proxy-pw') - k8sClient.commandExecutorForTest.assertExecuted( - 'kubectl create secret docker-registry proxy-registry -n example-apps-production' + - ' --docker-server proxy-url --docker-username proxy-user --docker-password proxy-pw') - } - - @Test - void 'Combines content repos successfully'() { - - config.content.repos = contentRepos - - def repos = createContent(config).cloneContentRepos() - - expectedTargetRepos.each { expected -> - assertThat(new File(findRoot(repos), "${expected.namespace}/${expected.repoName}/file")).exists().isFile() - } - - assertThat(new File(findRoot(repos), "common/repo/file").text).contains("folderBasedRepo2") // Last repo "wins" - - assertThat(new File(findRoot(repos), "common/repo/folderBasedRepo1")).exists().isFile() - assertThat(new File(findRoot(repos), "common/repo/folderBasedRepo2")).exists().isFile() - assertThat(new File(findRoot(repos), "common/repo/copyRepo1")).exists().isFile() - assertThat(new File(findRoot(repos), "common/repo/copyRepo2")).exists().isFile() - - // Assert Templating - assertThat(new File(findRoot(repos), "common/repo/some.yaml")).exists() - assertThat(new File(findRoot(repos), "common/repo/some.yaml").text).contains("namePrefix: foo-") - // Assert not templating for this folder-based repo - assertThat(new File(findRoot(repos), "common/repo/someOther.yaml.ftl")).exists() - assertThat(new File(findRoot(repos), "common/repo/someOther.yaml.ftl").text).contains('namePrefix: ${config.application.namePrefix}') - } - - @Test - void 'supports content variables'() { - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED, templating: true) - ] - config.content.variables.someapp = [somevalue: 'this is a custom variable'] - - def repos = createContent(config).cloneContentRepos() - - // Assert Templating - assertThat(new File(findRoot(repos), "common/repo/some.yaml")).exists() - assertThat(new File(findRoot(repos), "common/repo/some.yaml").text).contains("namePrefix: foo-") - assertThat(new File(findRoot(repos), "common/repo/some.yaml").text).contains("myvar: this is a custom variable") - } - - @Test - void 'Authenticates content Repos'() { - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', credentials: new Credentials('user', 'pw')) - ] - - def content = createContent(config) - content.cloneContentRepos() - - ArgumentCaptor captor = ArgumentCaptor.forClass(UsernamePasswordCredentialsProvider) - verify(content.cloneSpy).setCredentialsProvider(captor.capture()) - - - def value = captor.value - assertThat(value.properties.username).isEqualTo('user') - assertThat(value.properties.password).isEqualTo('pw'.toCharArray()) - } - - @Test - @DisplayName("Authenticates content Repos with secret") - void authenticatesContentReposWithSecret() { - this.k8sClient.k8sJavaApiClient.client=client - Secret secret = new SecretBuilder() - .withNewMetadata() - .withName("secret-test-name") - .withNamespace("default") - .endMetadata() - .withType("Opaque") - .withData(Map.of( - "username", "YWRtaW4=", - "password", "czNjcjN0" - )) - .build() - - - this.k8sClient.k8sJavaApiClient.client.secrets() - .inNamespace("default") - .resource(secret) - .create() - - config.content.repos = [ - new ContentRepositorySchema( - url: createContentRepo('copyRepo1'), - ref: 'main', type: ContentRepoType.COPY, - target: 'common/repo', - credentials: new Credentials(null,null,'secret-test-name','default')) - ] - - def content = createContent(config) - content.cloneContentRepos() - - ArgumentCaptor captor = ArgumentCaptor.forClass(UsernamePasswordCredentialsProvider) - verify(content.cloneSpy).setCredentialsProvider(captor.capture()) - def value = captor.value - assertThat(value.properties.username).isEqualTo('admin') - assertThat(value.properties.password).isEqualTo('s3cr3t'.toCharArray()) - } - - @Test - void 'Checks out commit refs, tags and non-default branches for content repos'() { - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'someTag', type: ContentRepoType.COPY, target: 'common/tag'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: '8bc1d1165468359b16d9771d4a9a3df26afc03e8', type: ContentRepoType.COPY, target: 'common/ref'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'someBranch', type: ContentRepoType.COPY, target: 'common/branch') - ] - - def repos = createContent(config).cloneContentRepos() - - assertThat(new File(findRoot(repos), "common/tag/README.md")).exists().isFile() - assertThat(new File(findRoot(repos), "common/tag/README.md").text).contains("someTag") - - assertThat(new File(findRoot(repos), "common/ref/README.md")).exists().isFile() - assertThat(new File(findRoot(repos), "common/ref/README.md").text).contains("main") - - assertThat(new File(findRoot(repos), "common/branch/README.md")).exists().isFile() - assertThat(new File(findRoot(repos), "common/branch/README.md").text).contains("someBranch") - } - - @Test - void 'Checks out default branch when no ref set'() { - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('', 'git-repo-different-default-branch'), target: 'common/default', type: ContentRepoType.COPY), - ] - - def repos = createContent(config).cloneContentRepos() - - assertThat(new File(findRoot(repos), "common/default/README.md")).exists().isFile() - assertThat(new File(findRoot(repos), "common/default/README.md").text).contains("different") - } - - @Test - void 'Fails if commit ref does not exist'() { - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'someTag', type: ContentRepoType.COPY, target: 'common/tag'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'does/not/exist', type: ContentRepoType.FOLDER_BASED, target: 'does not matter'), - ] - - def exception = shouldFail(RuntimeException) { - createContent(config).cloneContentRepos() - } - - assertThat(exception.message).startsWith("Reference 'does/not/exist' not found in content repository") - } - - @Test - void 'Respects order of folder-based repositories'() { - config.content.repos = [ - // Note the different order! - new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), ref: 'main', type: ContentRepoType.FOLDER_BASED), - new ContentRepositorySchema(url: createContentRepo('folderBasedRepo2'), ref: 'main', type: ContentRepoType.FOLDER_BASED, path: 'subPath'), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath'), - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), - ] - - def repos = createContent(config).cloneContentRepos() - - assertThat(new File(findRoot(repos), "common/repo/file").text).contains("copyRepo1") - // Last repo "wins" - } - - @Test - void 'Is able to COPY into MIRRORED repo'() { - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/repo'), - new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED, overwriteMode: OverwriteMode.UPGRADE), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath') - ] - - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - - def expectedRepo = 'common/repo' - // clone target repo, to ensure, changes in remote repo. - try (def git = cloneRepo(expectedRepo, tmpDir)) { - assertThat(new File(tmpDir, "file").text).contains("copyRepo2") // Last repo "wins" - assertThat(new File(tmpDir, "mirrorRepo1")).exists().isFile() - assertThat(new File(tmpDir, "copyRepo2")).exists().isFile() - assertThat(new File(tmpDir, "folderBasedRepo1")).exists().isFile() - - // Assert mirrors branches and tags of non-folderBased repos - // Verify tag exists and points to correct content - git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches - - assertTag(git, 'someTag') - assertBranch(git, 'someBranch') - } - } - - @Test - void 'Handles mirror and copy together'() { - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath'), - new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, overwriteMode: OverwriteMode.RESET, target: 'common/repo'), - ] - - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - - def expectedRepo = 'common/repo' - // clone target repo, to ensure, changes in remote repo. - try (def git = cloneRepo(expectedRepo, tmpDir)) { - assertThat(new File(tmpDir, "file").text).contains("mirrorRepo1") // Last repo "wins" - assertThat(new File(tmpDir, "folderBasedRepo1")).doesNotExist() - assertThat(new File(tmpDir, "copyRepo2")).doesNotExist() - - // Assert mirrors branches and tags of non-folderBased repos - // Verify tag exists and points to correct content - git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches - - assertTag(git, 'someTag') - assertBranch(git, 'someBranch') - } - } - - @Test - void 'Handles multiple mirrors of the same repo with different refs'() { - def repoToMirror = createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags') - config.content.repos = [ - new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'main', target: 'common/repo'), - new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'someBranch', target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE), - new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'someTag', target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath') - ] - - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - - def expectedRepo = 'common/repo' - // clone target repo, to ensure, changes in remote repo. - try (def git = cloneRepo(expectedRepo, tmpDir)) { - assertThat(new File(tmpDir, "file").text).contains("copyRepo2") // Last repo "wins" - assertThat(new File(tmpDir, "mirrorRepo1")).exists().isFile() - - git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches - - assertTag(git, 'someTag') - assertBranch(git, 'someBranch') - } - } - - @Test - void 'Handles targetRefs'() { - config.content.repos = [ - // From branch to branch or tag to tag - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/tag', ref: 'someTag', targetRef: 'my-tag'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/branch', ref: 'someBranch', targetRef: 'my-branch'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/tag', ref: 'someTag', targetRef: 'my-tag'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/branch', ref: 'someBranch', targetRef: 'my-branch'), - - // From tag to branch or the other way round - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/tag2branch', ref: 'someTag', targetRef: 'refs/heads/my-branch'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/branch2tag', ref: 'someBranch', targetRef: 'refs/tags/my-tag'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/tag2branch', ref: 'someTag', targetRef: 'refs/heads/my-branch'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/branch2tag', ref: 'someBranch', targetRef: 'refs/tags/my-tag'), - ] - - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - - // From branch to branch or tag to tag - assertTagAndReadme('mirror/tag', 'my-tag', "someTag") - assertBranchAndReadme('mirror/branch', 'my-branch', "someBranch") - - assertTagAndReadme('copy/tag', 'my-tag', "someTag") - assertBranchAndReadme('copy/branch', 'my-branch', "someBranch") - - // From tag to branch or the other way round - assertTagAndReadme('mirror/branch2tag', 'my-tag', "someBranch") - assertBranchAndReadme('mirror/tag2branch', 'my-branch', "someTag") - - assertTagAndReadme('copy/branch2tag', 'my-tag', "someBranch") - assertBranchAndReadme('copy/tag2branch', 'my-branch', "someTag") - } - - @Test - void 'Handles multiple mirrors of the same repo with different refs, where one is not pushed'() { - // This test case does not make too much sense but used to cause git problems when we merged all content repos into a single folder, like - // TransportException: Missing unknown 5bcf50f0537bf4d2719a82e9b0950fbac92b3ecc - def repoToMirror = createContentRepo('copyRepo1', 'git-repository-with-branches-tags') - config.content.repos = [ - new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'main', target: 'common/repo'), - new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'someBranch', target: 'common/repo') /* Deliberately not use overwriteMode here !*/, - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath') - ] - - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - // No exception means success - } + static List foldersToDelete = new ArrayList() + + Config config = new Config(application: new Config.ApplicationSchema(namePrefix: 'foo-'), + scm: new ScmTenantSchema(scmManager: new ScmTenantSchema.ScmManagerTenantConfig(url: '')), + registry: new Config.RegistrySchema(url: 'reg-url', + path: 'reg-path', + username: 'reg-user', + password: 'reg-pw', + createImagePullSecrets: false)) + + KubernetesClient client + CommandExecutorForTest k8sCommands = new CommandExecutorForTest() + K8sClientForTest k8sClient = new K8sClientForTest(config, k8sCommands) + TestGitRepoFactory scmmRepoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) + TestScmManagerApiClient scmmApiClient = new TestScmManagerApiClient(config) + Jenkins jenkins = mock(Jenkins.class) + ScmManagerMock scmManagerMock = new ScmManagerMock() + GitHandler gitHandler = new GitHandlerForTests(config, scmManagerMock) + DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) + FileSystemUtils fileSystemUtils = new FileSystemUtils() + + @TempDir + File tmpDir + + List expectedTargetRepos = [new RepoCoordinate(namespace: "common", repoName: "repo"), + new RepoCoordinate(namespace: "ns1a", repoName: "repo1a1"), + new RepoCoordinate(namespace: "ns1a", repoName: "repo1a2"), + new RepoCoordinate(namespace: "ns1b", repoName: "repo1b1"), + new RepoCoordinate(namespace: "ns1b", repoName: "repo1b2"), + new RepoCoordinate(namespace: "ns2a", repoName: "repo2a1"), + new RepoCoordinate(namespace: "ns2a", repoName: "repo2a2"), + new RepoCoordinate(namespace: "ns2b", repoName: "repo2b1"), + new RepoCoordinate(namespace: "ns2b", repoName: "repo2b2"), + new RepoCoordinate(namespace: "copy", repoName: "repo1"), + new RepoCoordinate(namespace: "copy", repoName: "repo2"),] + + List contentRepos = [// copy-typed repo writing to their own target + new ContentRepositorySchema(url: createContentRepo('copyRepo1'), type: ContentRepoType.COPY, target: 'copy/repo1'), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'copy/repo2', path: 'subPath'), + + // Same folder as in copyRepos -> Should be combined + new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath'), + + // Contains ftl + new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED, templating: true), + // Contains a templated file that should be ignored + new ContentRepositorySchema(url: createContentRepo('folderBasedRepo2'), type: ContentRepoType.FOLDER_BASED, path: 'subPath'), + + ] + + @AfterAll + static void cleanFolders() { + foldersToDelete.each { it.deleteDir() } + + } + + @Test + void 'deploys image pull secrets'() { + config.registry.createImagePullSecrets = true + config.content.namespaces = ['example-apps-staging', 'example-apps-production'] + + createContent(config).install() + + assertRegistrySecrets('reg-user', 'reg-pw') + } + + @Test + void 'deploys image pull secrets from read-only vars'() { + config.registry.createImagePullSecrets = true + config.content.namespaces = ['example-apps-staging', 'example-apps-production'] + config.registry.readOnlyUsername = 'other-user' + config.registry.readOnlyPassword = 'other-pw' + + createContent(config).install() + + assertRegistrySecrets('other-user', 'other-pw') + } + + @Test + void 'deploys additional image pull secrets for proxy registry'() { + config.registry.createImagePullSecrets = true + config.content.namespaces = ['example-apps-staging', 'example-apps-production'] + config.registry.twoRegistries = true + config.registry.proxyUrl = 'proxy-url' + config.registry.proxyUsername = 'proxy-user' + config.registry.proxyPassword = 'proxy-pw' + + // Simulate argocd Namespace does not exist + k8sCommands.enqueueOutput(new CommandExecutor.Output('namespace not found', '', 1)) // Namespace not exit + k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl + k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl + k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl + k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 0)) // other kubectl + k8sCommands.enqueueOutput(new CommandExecutor.Output('', '', 1)) // Namespace not exit + + createContent(config).install() + + assertRegistrySecrets('reg-user', 'reg-pw') + + k8sClient.commandExecutorForTest.assertExecuted('kubectl create namespace example-apps-staging') + k8sClient.commandExecutorForTest.assertExecuted('kubectl create namespace example-apps-production') + k8sClient.commandExecutorForTest.assertExecuted('kubectl create secret docker-registry proxy-registry -n example-apps-staging' + + ' --docker-server proxy-url --docker-username proxy-user --docker-password proxy-pw') + k8sClient.commandExecutorForTest.assertExecuted('kubectl create secret docker-registry proxy-registry -n example-apps-production' + + ' --docker-server proxy-url --docker-username proxy-user --docker-password proxy-pw') + } + + @Test + void 'Combines content repos successfully'() { + + config.content.repos = contentRepos + + def repos = createContent(config).cloneContentRepos() + + expectedTargetRepos.each { expected -> assertThat(new File(findRoot(repos), "${expected.namespace}/${expected.repoName}/file")).exists().isFile() + } + + assertThat(new File(findRoot(repos), "common/repo/file").text).contains("folderBasedRepo2") // Last repo "wins" + + assertThat(new File(findRoot(repos), "common/repo/folderBasedRepo1")).exists().isFile() + assertThat(new File(findRoot(repos), "common/repo/folderBasedRepo2")).exists().isFile() + assertThat(new File(findRoot(repos), "common/repo/copyRepo1")).exists().isFile() + assertThat(new File(findRoot(repos), "common/repo/copyRepo2")).exists().isFile() + + // Assert Templating + assertThat(new File(findRoot(repos), "common/repo/some.yaml")).exists() + assertThat(new File(findRoot(repos), "common/repo/some.yaml").text).contains("namePrefix: foo-") + // Assert not templating for this folder-based repo + assertThat(new File(findRoot(repos), "common/repo/someOther.yaml.ftl")).exists() + assertThat(new File(findRoot(repos), "common/repo/someOther.yaml.ftl").text).contains('namePrefix: ${config.application.namePrefix}') + } + + @Test + void 'supports content variables'() { + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED, templating: true)] + config.content.variables.someapp = [somevalue: 'this is a custom variable'] + + def repos = createContent(config).cloneContentRepos() + + // Assert Templating + assertThat(new File(findRoot(repos), "common/repo/some.yaml")).exists() + assertThat(new File(findRoot(repos), "common/repo/some.yaml").text).contains("namePrefix: foo-") + assertThat(new File(findRoot(repos), "common/repo/some.yaml").text).contains("myvar: this is a custom variable") + } + + @Test + void 'Authenticates content Repos'() { + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', credentials: new Credentials('user', 'pw'))] + + def content = createContent(config) + content.cloneContentRepos() + + ArgumentCaptor captor = ArgumentCaptor.forClass(UsernamePasswordCredentialsProvider) + verify(content.cloneSpy).setCredentialsProvider(captor.capture()) + + def value = captor.value + assertThat(value.properties.username).isEqualTo('user') + assertThat(value.properties.password).isEqualTo('pw'.toCharArray()) + } + + @Test + @DisplayName("Authenticates content Repos with secret") + void authenticatesContentReposWithSecret() { + this.k8sClient.k8sJavaApiClient.client = client + Secret secret = new SecretBuilder() + .withNewMetadata() + .withName("secret-test-name") + .withNamespace("default") + .endMetadata() + .withType("Opaque") + .withData(Map.of("username", "YWRtaW4=", + "password", "czNjcjN0")) + .build() + + this.k8sClient.k8sJavaApiClient.client.secrets() + .inNamespace("default") + .resource(secret) + .create() + + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), + ref: 'main', type: ContentRepoType.COPY, + target: 'common/repo', + credentials: new Credentials(null, null, 'secret-test-name', 'default'))] + + def content = createContent(config) + content.cloneContentRepos() + + ArgumentCaptor captor = ArgumentCaptor.forClass(UsernamePasswordCredentialsProvider) + verify(content.cloneSpy).setCredentialsProvider(captor.capture()) + def value = captor.value + assertThat(value.properties.username).isEqualTo('admin') + assertThat(value.properties.password).isEqualTo('s3cr3t'.toCharArray()) + } + + @Test + void 'Checks out commit refs, tags and non-default branches for content repos'() { + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'someTag', type: ContentRepoType.COPY, target: 'common/tag'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: '8bc1d1165468359b16d9771d4a9a3df26afc03e8', type: ContentRepoType.COPY, target: 'common/ref'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'someBranch', type: ContentRepoType.COPY, target: 'common/branch')] + + def repos = createContent(config).cloneContentRepos() + + assertThat(new File(findRoot(repos), "common/tag/README.md")).exists().isFile() + assertThat(new File(findRoot(repos), "common/tag/README.md").text).contains("someTag") + + assertThat(new File(findRoot(repos), "common/ref/README.md")).exists().isFile() + assertThat(new File(findRoot(repos), "common/ref/README.md").text).contains("main") + + assertThat(new File(findRoot(repos), "common/branch/README.md")).exists().isFile() + assertThat(new File(findRoot(repos), "common/branch/README.md").text).contains("someBranch") + } + + @Test + void 'Checks out default branch when no ref set'() { + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('', 'git-repo-different-default-branch'), target: 'common/default', type: ContentRepoType.COPY),] + + def repos = createContent(config).cloneContentRepos() + + assertThat(new File(findRoot(repos), "common/default/README.md")).exists().isFile() + assertThat(new File(findRoot(repos), "common/default/README.md").text).contains("different") + } + + @Test + void 'Fails if commit ref does not exist'() { + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'someTag', type: ContentRepoType.COPY, target: 'common/tag'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'does/not/exist', type: ContentRepoType.FOLDER_BASED, target: 'does not matter'),] + + def exception = shouldFail(RuntimeException) { + createContent(config).cloneContentRepos() + } + + assertThat(exception.message).startsWith("Reference 'does/not/exist' not found in content repository") + } + + @Test + void 'Respects order of folder-based repositories'() { + config.content.repos = [// Note the different order! + new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), ref: 'main', type: ContentRepoType.FOLDER_BASED), + new ContentRepositorySchema(url: createContentRepo('folderBasedRepo2'), ref: 'main', type: ContentRepoType.FOLDER_BASED, path: 'subPath'), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath'), + new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'),] + + def repos = createContent(config).cloneContentRepos() + + assertThat(new File(findRoot(repos), "common/repo/file").text).contains("copyRepo1") + // Last repo "wins" + } + + @Test + void 'Is able to COPY into MIRRORED repo'() { + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/repo'), + new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED, overwriteMode: OverwriteMode.UPGRADE), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath')] + + scmmApiClient.mockRepoApiBehaviour() + + createContent(config).install() + + def expectedRepo = 'common/repo' + // clone target repo, to ensure, changes in remote repo. + try (def git = cloneRepo(expectedRepo, tmpDir)) { + assertThat(new File(tmpDir, "file").text).contains("copyRepo2") // Last repo "wins" + assertThat(new File(tmpDir, "mirrorRepo1")).exists().isFile() + assertThat(new File(tmpDir, "copyRepo2")).exists().isFile() + assertThat(new File(tmpDir, "folderBasedRepo1")).exists().isFile() + + // Assert mirrors branches and tags of non-folderBased repos + // Verify tag exists and points to correct content + git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches + + assertTag(git, 'someTag') + assertBranch(git, 'someBranch') + } + } + + @Test + void 'Handles mirror and copy together'() { + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath'), + new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, overwriteMode: OverwriteMode.RESET, target: 'common/repo'),] + + scmmApiClient.mockRepoApiBehaviour() + + createContent(config).install() + + def expectedRepo = 'common/repo' + // clone target repo, to ensure, changes in remote repo. + try (def git = cloneRepo(expectedRepo, tmpDir)) { + assertThat(new File(tmpDir, "file").text).contains("mirrorRepo1") // Last repo "wins" + assertThat(new File(tmpDir, "folderBasedRepo1")).doesNotExist() + assertThat(new File(tmpDir, "copyRepo2")).doesNotExist() + + // Assert mirrors branches and tags of non-folderBased repos + // Verify tag exists and points to correct content + git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches + + assertTag(git, 'someTag') + assertBranch(git, 'someBranch') + } + } + + @Test + void 'Handles multiple mirrors of the same repo with different refs'() { + def repoToMirror = createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags') + config.content.repos = [new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'main', target: 'common/repo'), + new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'someBranch', target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE), + new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'someTag', target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath')] + + scmmApiClient.mockRepoApiBehaviour() + + createContent(config).install() + + def expectedRepo = 'common/repo' + // clone target repo, to ensure, changes in remote repo. + try (def git = cloneRepo(expectedRepo, tmpDir)) { + assertThat(new File(tmpDir, "file").text).contains("copyRepo2") // Last repo "wins" + assertThat(new File(tmpDir, "mirrorRepo1")).exists().isFile() + + git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches + + assertTag(git, 'someTag') + assertBranch(git, 'someBranch') + } + } + + @Test + void 'Handles targetRefs'() { + config.content.repos = [// From branch to branch or tag to tag + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/tag', ref: 'someTag', targetRef: 'my-tag'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/branch', ref: 'someBranch', targetRef: 'my-branch'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/tag', ref: 'someTag', targetRef: 'my-tag'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/branch', ref: 'someBranch', targetRef: 'my-branch'), + + // From tag to branch or the other way round + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/tag2branch', ref: 'someTag', targetRef: 'refs/heads/my-branch'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'mirror/branch2tag', ref: 'someBranch', targetRef: 'refs/tags/my-tag'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/tag2branch', ref: 'someTag', targetRef: 'refs/heads/my-branch'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.COPY, target: 'copy/branch2tag', ref: 'someBranch', targetRef: 'refs/tags/my-tag'),] + + scmmApiClient.mockRepoApiBehaviour() + + createContent(config).install() + + // From branch to branch or tag to tag + assertTagAndReadme('mirror/tag', 'my-tag', "someTag") + assertBranchAndReadme('mirror/branch', 'my-branch', "someBranch") + + assertTagAndReadme('copy/tag', 'my-tag', "someTag") + assertBranchAndReadme('copy/branch', 'my-branch', "someBranch") + + // From tag to branch or the other way round + assertTagAndReadme('mirror/branch2tag', 'my-tag', "someBranch") + assertBranchAndReadme('mirror/tag2branch', 'my-branch', "someTag") + + assertTagAndReadme('copy/branch2tag', 'my-tag', "someBranch") + assertBranchAndReadme('copy/tag2branch', 'my-branch', "someTag") + } + + @Test + void 'Handles multiple mirrors of the same repo with different refs, where one is not pushed'() { + // This test case does not make too much sense but used to cause git problems when we merged all content repos into a single folder, like + // TransportException: Missing unknown 5bcf50f0537bf4d2719a82e9b0950fbac92b3ecc + def repoToMirror = createContentRepo('copyRepo1', 'git-repository-with-branches-tags') + config.content.repos = [new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'main', target: 'common/repo'), + new ContentRepositorySchema(url: repoToMirror, type: ContentRepoType.MIRROR, ref: 'someBranch', target: 'common/repo') /* Deliberately not use overwriteMode here !*/, + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.UPGRADE, path: 'subPath')] + + scmmApiClient.mockRepoApiBehaviour() + createContent(config).install() + // No exception means success + } - @Test - void 'Is able to MIRROR into repo that has same commits'() { - // This test case does not make too much sense but used to cause git problems when copying .git from source to target - // java.lang.IllegalArgumentException: File parameter 'destFile is not writable: '/tmp/../.git/objects/pack/pack-524e3f54c7b28a98a4995948dfc8e75f1642840f.pack' - // This only occurs when the same .pack files exists in .git because they are read-only - // So for our testcase we just mirror the same repo twice - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/repo'), - new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/repo', overwriteMode: OverwriteMode.RESET), - ] + @Test + void 'Is able to MIRROR into repo that has same commits'() { + // This test case does not make too much sense but used to cause git problems when copying .git from source to target + // java.lang.IllegalArgumentException: File parameter 'destFile is not writable: '/tmp/../.git/objects/pack/pack-524e3f54c7b28a98a4995948dfc8e75f1642840f.pack' + // This only occurs when the same .pack files exists in .git because they are read-only + // So for our testcase we just mirror the same repo twice + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/repo'), + new ContentRepositorySchema(url: createContentRepo('mirrorRepo1', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/repo', overwriteMode: OverwriteMode.RESET),] - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - // No exception means success - } - - @Test - void 'Parses Repo coordinates'() { + scmmApiClient.mockRepoApiBehaviour() - config.content.repos = contentRepos - - def content = createContent(config) - - def actualTargetRepos = content.cloneContentRepos() - def repos = actualTargetRepos + createContent(config).install() + // No exception means success + } - assertThat(actualTargetRepos).hasSameSizeAs(expectedTargetRepos) + @Test + void 'Parses Repo coordinates'() { - expectedTargetRepos.each { expected -> - - def actual = actualTargetRepos.findAll { actual -> - actual.namespace == expected.namespace && actual.repoName == expected.repoName - } - assertThat(actual).withFailMessage( - "Could not find repo with namespace=${expected.namespace} and repo=${expected.repoName} in ${actualTargetRepos}" - ).hasSize(1) - - assertThat(actual[0].clonedContentRepo.absolutePath).isEqualTo( - new File(findRoot(repos), "${expected.namespace}/${expected.repoName}").absolutePath) - } - } - - @Test - void 'Creates and pushes content repos, whole flow '() { - config.content.repos = contentRepos + - [ - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/mirror'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: 'main', target: 'common/mirrorWithBranchRef'), - new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: 'someTag', target: 'common/mirrorWithTagRef'), - ] - - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - - def expectedRepo = 'copy/repo1' - // clone target repo, to ensure, changes in remote repo. - try (def git = cloneRepo(expectedRepo, tmpDir)) { - - def commitMsg = git.log().call().iterator().next().getFullMessage() - assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) - - assertThat(new File(tmpDir, "file").text).contains("copyRepo1") - assertThat(new File(tmpDir, "copyRepo1")).exists().isFile() - } - - expectedRepo = 'common/mirror' - try (def git = cloneRepo(expectedRepo, createRandomSubDir())) { - // Assert mirrors branches and tags of non-folderBased repos - // Verify tag exists and points to correct content - git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches - - assertTag(git, 'someTag') - assertBranch(git, 'someBranch') - } - - expectedRepo = 'common/mirrorWithBranchRef' - try (def git = cloneRepo(expectedRepo, createRandomSubDir())) { - - git.fetch().setRefSpecs("refs/*:refs/*").call() - - assertNoTags(git) - assertOnlyBranch(git, 'main') - } - - expectedRepo = 'common/mirrorWithTagRef' - try (def git = cloneRepo(expectedRepo, createRandomSubDir())) { - - git.fetch().setRefSpecs("refs/*:refs/*").call() - - assertTag(git, 'someTag') - assertOnlyBranch(git, 'main') - } - - // Mirroring commit references is not supported - config.content.repos = [new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: '8bc1d1165468359b16d9771d4a9a3df26afc03e8', target: 'common/mirrorWithCommitRef')] - - def exception = shouldFail(RuntimeException) { - createContent(config).install() - } - assertThat(exception.message).startsWith('Mirroring commit references is not supported for content repos at the moment. content repository') - assertThat(exception.message).endsWith('ref: 8bc1d1165468359b16d9771d4a9a3df26afc03e8') - - - // Mirroring short commit references is not supported as well - config.content.repos = [new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: '8bc1d11', target: 'common/mirrorWithShortCommitRef')] - - exception = shouldFail(RuntimeException) { - createContent(config).install() - } - assertThat(exception.message).startsWith('Mirroring commit references is not supported for content repos at the moment. content repository') - assertThat(exception.message).endsWith('ref: 8bc1d11') - - // Don't bother validating all other repos here. - // If it works for the most complex one, the other ones will work as well. - // The other tests are already asserting correct combining (including order) and parsing of the repos. - } - - static void assertOnlyBranch(Git git, String branch) { - def branches = assertBranch(git, branch) - def otherBranches = branches.findAll { !it.name.contains(branch) } - assertThat(otherBranches) - .withFailMessage("More than the expected branch main found. Available branches: ${otherBranches.collect { it.name }}") - .hasSize(0) - } - - static void assertNoTags(Git git) { - def tags = git.tagList().call() - assertThat(tags) - .withFailMessage("No tags in mirrored repo with ref expected. Available tags: ${tags.collect { it.name }}") - .hasSize(0) - } - - static List assertBranch(Git git, String someBranch) { - def branches = git.branchList().call() - assertThat(branches.findAll { it.name == "refs/heads/${someBranch}" }) - .withFailMessage("Branch '${someBranch}' not found in git repository. Available branches: ${branches.collect { it.name }}") - .hasSize(1) - return branches - } - - static void assertTag(Git git, String expectedTag) { - def tags = git.tagList().call() - assertThat(tags.findAll { it.name == "refs/tags/$expectedTag" }) - .withFailMessage("Tag '$expectedTag' not found in git repository. Available tags: ${tags.collect { it.name }}") - .hasSize(1) - } - - @Test - void 'Reset common repo to repo '() { - /** - * Prepare Testcase - * using all defined repos -> common/repo is used by copyRepo1 + 2 - * file content after that: copyRepo2 - * - * Then again "RESET" to copyRepo1. - * file content after that should be: copyRepo1 - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath') - - ] - def expectedRepo = 'common/repo' - def repo = scmmRepoProvider.getRepo(expectedRepo, scmManagerMock) - scmManagerMock.initOnceRepo(repo.repoTarget) - createContent(config).install() - - String url = repo.getGitRepositoryUrl() - // clone repo, to ensure, changes in remote repo. - try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { - - - verify(repo).createRepositoryAndSetPermission(any(String.class), eq(false)) - - def commitMsg = git.log().call().iterator().next().getFullMessage() - assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) - - assertThat(new File(tmpDir, "file").text).contains("copyRepo2") - assertThat(new File(tmpDir, "copyRepo2")).exists().isFile() - } - - /** - * End of preparation - * - * Now Reset to an copied repo - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.RESET), - ] - - createContent(config).install() - scmManagerMock.clearInitOnce() - - def folderAfterReset = File.createTempDir('second-cloned-repo') - folderAfterReset.deleteOnExit() - // clone repo, to ensure, changes in remote repo. - try (def git2 = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(folderAfterReset).call()) { - - assertThat(git2).isNotNull() - // because copyRepo1 is only part of repo1 - assertThat(new File(folderAfterReset, "file").text).contains("copyRepo1") - // should not exists, if RESET to first repo - assertThat(new File(folderAfterReset, "copyRepo2").exists()).isFalse() - - } - - - } - - @Test - void 'Update common repo test '() { - /** - * Prepare Testcase - * using all defined repos -> common/repo is used by copyRepo1 + 2 - * file content after that: copyRepo2 - * - * Then again "RESET" to copyRepo1. - * file content after that should be: copyRepo1 - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), - ] - - scmmApiClient.mockRepoApiBehaviour() - - createContent(config).install() - - def expectedRepo = 'common/repo' - def repo = scmmRepoProvider.getRepo(expectedRepo, new ScmManagerMock()) - - def url = repo.getGitRepositoryUrl() - // clone repo, to ensure, changes in remote repo. - try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { - - - verify(repo).createRepositoryAndSetPermission(any(String.class), eq(false)) - - def commitMsg = git.log().call().iterator().next().getFullMessage() - assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) - - assertThat(new File(tmpDir, "file").text).contains("copyRepo1") - assertThat(new File(tmpDir, "copyRepo1")).exists().isFile() - - } - /** - * End of preparation - * - * Now Upgrade to type copy - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath', overwriteMode: OverwriteMode.UPGRADE) - ] - - - createContent(config).install() - - def folderAfterReset = File.createTempDir('second-cloned-repo') - folderAfterReset.deleteOnExit() - // clone repo, to ensure, changes in remote repo. - try (def git2 = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(folderAfterReset).call()) { - - assertThat(git2).isNotNull() - // because copyRepo1 is only part of repo1 - assertThat(new File(folderAfterReset, "file").text).contains("copyRepo2") - // should not exists, if RESET to first repo - assertThat(new File(folderAfterReset, "copyRepo2").exists()).isTrue() - - } - } - - @Test - void 'init common repo, expect unchanged repo'() { - /** - * Prepare Testcase - * using all defined repos -> common/repo is used by copyRepo1 + 2 - * file content after that: copyRepo2 - * - * Then again "RESET" to copyRepo1. - * file content after that should be: copyRepo1 - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), - new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath') - - ] - def expectedRepo = 'common/repo' - def repo = scmmRepoProvider.getRepo(expectedRepo, scmManagerMock) - scmManagerMock.initOnceRepo(repo.repoTarget) - createContent(config).install() - - def url = repo.getGitRepositoryUrl() - // clone repo, to ensure, changes in remote repo. - try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { - - verify(repo).createRepositoryAndSetPermission(any(String.class), eq(false)) - - def commitMsg = git.log().call().iterator().next().getFullMessage() - assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) - - assertThat(new File(tmpDir, "file").text).contains("copyRepo2") - assertThat(new File(tmpDir, "copyRepo2")).exists().isFile() - } - - /** - * End of preparation - * - * Now INit to a copied repo - * no changes expected, file still has copyRepo2 and so on - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.INIT), - ] - - createContent(config).install() - scmManagerMock.clearInitOnce() - - def folderAfterReset = File.createTempDir('second-cloned-repo') - folderAfterReset.deleteOnExit() - // clone repo, to ensure, changes in remote repo. - try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(folderAfterReset).call()) { - - assertThat(git).isNotNull() - // because copyRepo1 is only part of repo1 - assertThat(new File(folderAfterReset, "file").text).contains("copyRepo2") - // should not exists, if RESET to first repo - assertThat(new File(folderAfterReset, "copyRepo2").exists()).isTrue() - - } - - } - - @Test - void 'ensure Jenkinsjob will be created'() { - /** - * Prepare Testcase - * using all defined repos -> common/repo is used by copyRepo1 + 2 - * file content after that: copyRepo2 - * - * Then again "RESET" to copyRepo1. - * file content after that should be: copyRepo1 - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, createJenkinsJob: true, target: 'common/repo'), - ] - scmmApiClient.mockRepoApiBehaviour() - when(jenkins.isEnabled()).thenReturn(true) - - createContent(config).install() - verify(jenkins).createJenkinsjob(any(), any()) - } - - @Test - void 'ensure Jenkinsjob creation will be ignored'() { - /** - * Prepare Testcase - * using all defined repos -> common/repo is used by copyRepo1 + 2 - * file content after that: copyRepo2 - * - * Then again "RESET" to copyRepo1. - * file content after that should be: copyRepo1 - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, createJenkinsJob: false, target: 'common/repo'), - ] - scmmApiClient.mockRepoApiBehaviour() - when(jenkins.isEnabled()).thenReturn(false) - createContent(config).install() - verify(jenkins, never()).createJenkinsjob(any(), any()) - } - - @Test - void 'ensure Jenkinsjob will not be created, if jenkins is not enables'() { - /** - * Prepare Testcase - * using all defined repos -> common/repo is used by copyRepo1 + 2 - * file content after that: copyRepo2 - * - * Then again "RESET" to copyRepo1. - * file content after that should be: copyRepo1 - */ - config.content.repos = [ - new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, createJenkinsJob: false, target: 'common/repo'), - ] - scmmApiClient.mockRepoApiBehaviour() - when(jenkins.isEnabled()).thenReturn(false) - - createContent(config).install() - verify(jenkins, never()).createJenkinsjob(any(), any()) - } - - @Test - void 'deployHelmReleasesFromContent skips when helmReleases missing or empty'() { - def contentLoader = createContent(config) - contentLoader.install() - - assertThat(contentLoader.deployCalls).isEmpty() - } - - @Test - void 'deployHelmReleasesFromContent calls deployHelmChart with valuesPath and helm config'() { - // Arrange: create a real values file on disk - Path valuesFile = Files.createTempFile("harbor-values-", ".yaml") - Files.writeString(valuesFile, """ + config.content.repos = contentRepos + + def content = createContent(config) + + def actualTargetRepos = content.cloneContentRepos() + def repos = actualTargetRepos + + assertThat(actualTargetRepos).hasSameSizeAs(expectedTargetRepos) + + expectedTargetRepos.each { expected -> + + def actual = actualTargetRepos.findAll { actual -> actual.namespace == expected.namespace && actual.repoName == expected.repoName + } + assertThat(actual).withFailMessage("Could not find repo with namespace=${expected.namespace} and repo=${expected.repoName} in ${actualTargetRepos}").hasSize(1) + + assertThat(actual[0].clonedContentRepo.absolutePath).isEqualTo(new File(findRoot(repos), "${expected.namespace}/${expected.repoName}").absolutePath) + } + } + + @Test + void 'Creates and pushes content repos, whole flow '() { + config.content.repos = contentRepos + [new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, target: 'common/mirror'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: 'main', target: 'common/mirrorWithBranchRef'), + new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: 'someTag', target: 'common/mirrorWithTagRef'),] + + scmmApiClient.mockRepoApiBehaviour() + + createContent(config).install() + + def expectedRepo = 'copy/repo1' + // clone target repo, to ensure, changes in remote repo. + try (def git = cloneRepo(expectedRepo, tmpDir)) { + + def commitMsg = git.log().call().iterator().next().getFullMessage() + assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) + + assertThat(new File(tmpDir, "file").text).contains("copyRepo1") + assertThat(new File(tmpDir, "copyRepo1")).exists().isFile() + } + + expectedRepo = 'common/mirror' + try (def git = cloneRepo(expectedRepo, createRandomSubDir())) { + // Assert mirrors branches and tags of non-folderBased repos + // Verify tag exists and points to correct content + git.fetch().setRefSpecs("refs/*:refs/*").call() // Fetch all tags and branches + + assertTag(git, 'someTag') + assertBranch(git, 'someBranch') + } + + expectedRepo = 'common/mirrorWithBranchRef' + try (def git = cloneRepo(expectedRepo, createRandomSubDir())) { + + git.fetch().setRefSpecs("refs/*:refs/*").call() + + assertNoTags(git) + assertOnlyBranch(git, 'main') + } + + expectedRepo = 'common/mirrorWithTagRef' + try (def git = cloneRepo(expectedRepo, createRandomSubDir())) { + + git.fetch().setRefSpecs("refs/*:refs/*").call() + + assertTag(git, 'someTag') + assertOnlyBranch(git, 'main') + } + + // Mirroring commit references is not supported + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: '8bc1d1165468359b16d9771d4a9a3df26afc03e8', target: 'common/mirrorWithCommitRef')] + + def exception = shouldFail(RuntimeException) { + createContent(config).install() + } + assertThat(exception.message).startsWith('Mirroring commit references is not supported for content repos at the moment. content repository') + assertThat(exception.message).endsWith('ref: 8bc1d1165468359b16d9771d4a9a3df26afc03e8') + + + // Mirroring short commit references is not supported as well + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), type: ContentRepoType.MIRROR, ref: '8bc1d11', target: 'common/mirrorWithShortCommitRef')] + + exception = shouldFail(RuntimeException) { + createContent(config).install() + } + assertThat(exception.message).startsWith('Mirroring commit references is not supported for content repos at the moment. content repository') + assertThat(exception.message).endsWith('ref: 8bc1d11') + + // Don't bother validating all other repos here. + // If it works for the most complex one, the other ones will work as well. + // The other tests are already asserting correct combining (including order) and parsing of the repos. + } + + static void assertOnlyBranch(Git git, String branch) { + def branches = assertBranch(git, branch) + def otherBranches = branches.findAll { !it.name.contains(branch) } + assertThat(otherBranches) + .withFailMessage("More than the expected branch main found. Available branches: ${otherBranches.collect { it.name }}") + .hasSize(0) + } + + static void assertNoTags(Git git) { + def tags = git.tagList().call() + assertThat(tags) + .withFailMessage("No tags in mirrored repo with ref expected. Available tags: ${tags.collect { it.name }}") + .hasSize(0) + } + + static List assertBranch(Git git, String someBranch) { + def branches = git.branchList().call() + assertThat(branches.findAll { it.name == "refs/heads/${someBranch}" }) + .withFailMessage("Branch '${someBranch}' not found in git repository. Available branches: ${branches.collect { it.name }}") + .hasSize(1) + return branches + } + + static void assertTag(Git git, String expectedTag) { + def tags = git.tagList().call() + assertThat(tags.findAll { it.name == "refs/tags/$expectedTag" }) + .withFailMessage("Tag '$expectedTag' not found in git repository. Available tags: ${tags.collect { it.name }}") + .hasSize(1) + } + + @Test + void 'Reset common repo to repo '() { + /** + * Prepare Testcase + * using all defined repos -> common/repo is used by copyRepo1 + 2 + * file content after that: copyRepo2 + * + * Then again "RESET" to copyRepo1. + * file content after that should be: copyRepo1*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath') + + ] + def expectedRepo = 'common/repo' + def repo = scmmRepoProvider.getRepo(expectedRepo, scmManagerMock) + scmManagerMock.initOnceRepo(repo.repoTarget) + createContent(config).install() + + String url = repo.getGitRepositoryUrl() + // clone repo, to ensure, changes in remote repo. + try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { + + verify(repo).createRepositoryAndSetPermission(any(String.class), eq(false)) + + def commitMsg = git.log().call().iterator().next().getFullMessage() + assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) + + assertThat(new File(tmpDir, "file").text).contains("copyRepo2") + assertThat(new File(tmpDir, "copyRepo2")).exists().isFile() + } + + /** + * End of preparation + * + * Now Reset to an copied repo*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.RESET),] + + createContent(config).install() + scmManagerMock.clearInitOnce() + + def folderAfterReset = File.createTempDir('second-cloned-repo') + folderAfterReset.deleteOnExit() + // clone repo, to ensure, changes in remote repo. + try (def git2 = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(folderAfterReset).call()) { + + assertThat(git2).isNotNull() + // because copyRepo1 is only part of repo1 + assertThat(new File(folderAfterReset, "file").text).contains("copyRepo1") + // should not exists, if RESET to first repo + assertThat(new File(folderAfterReset, "copyRepo2").exists()).isFalse() + + } + + } + + @Test + void 'Update common repo test '() { + /** + * Prepare Testcase + * using all defined repos -> common/repo is used by copyRepo1 + 2 + * file content after that: copyRepo2 + * + * Then again "RESET" to copyRepo1. + * file content after that should be: copyRepo1*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'),] + + scmmApiClient.mockRepoApiBehaviour() + + createContent(config).install() + + def expectedRepo = 'common/repo' + def repo = scmmRepoProvider.getRepo(expectedRepo, new ScmManagerMock()) + + def url = repo.getGitRepositoryUrl() + // clone repo, to ensure, changes in remote repo. + try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { + + verify(repo).createRepositoryAndSetPermission(any(String.class), eq(false)) + + def commitMsg = git.log().call().iterator().next().getFullMessage() + assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) + + assertThat(new File(tmpDir, "file").text).contains("copyRepo1") + assertThat(new File(tmpDir, "copyRepo1")).exists().isFile() + + } + /** + * End of preparation + * + * Now Upgrade to type copy*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath', overwriteMode: OverwriteMode.UPGRADE)] + + createContent(config).install() + + def folderAfterReset = File.createTempDir('second-cloned-repo') + folderAfterReset.deleteOnExit() + // clone repo, to ensure, changes in remote repo. + try (def git2 = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(folderAfterReset).call()) { + + assertThat(git2).isNotNull() + // because copyRepo1 is only part of repo1 + assertThat(new File(folderAfterReset, "file").text).contains("copyRepo2") + // should not exists, if RESET to first repo + assertThat(new File(folderAfterReset, "copyRepo2").exists()).isTrue() + + } + } + + @Test + void 'init common repo, expect unchanged repo'() { + /** + * Prepare Testcase + * using all defined repos -> common/repo is used by copyRepo1 + 2 + * file content after that: copyRepo2 + * + * Then again "RESET" to copyRepo1. + * file content after that should be: copyRepo1*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), + new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath') + + ] + def expectedRepo = 'common/repo' + def repo = scmmRepoProvider.getRepo(expectedRepo, scmManagerMock) + scmManagerMock.initOnceRepo(repo.repoTarget) + createContent(config).install() + + def url = repo.getGitRepositoryUrl() + // clone repo, to ensure, changes in remote repo. + try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { + + verify(repo).createRepositoryAndSetPermission(any(String.class), eq(false)) + + def commitMsg = git.log().call().iterator().next().getFullMessage() + assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) + + assertThat(new File(tmpDir, "file").text).contains("copyRepo2") + assertThat(new File(tmpDir, "copyRepo2")).exists().isFile() + } + + /** + * End of preparation + * + * Now INit to a copied repo + * no changes expected, file still has copyRepo2 and so on*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo', overwriteMode: OverwriteMode.INIT),] + + createContent(config).install() + scmManagerMock.clearInitOnce() + + def folderAfterReset = File.createTempDir('second-cloned-repo') + folderAfterReset.deleteOnExit() + // clone repo, to ensure, changes in remote repo. + try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(folderAfterReset).call()) { + + assertThat(git).isNotNull() + // because copyRepo1 is only part of repo1 + assertThat(new File(folderAfterReset, "file").text).contains("copyRepo2") + // should not exists, if RESET to first repo + assertThat(new File(folderAfterReset, "copyRepo2").exists()).isTrue() + + } + + } + + @Test + void 'ensure Jenkinsjob will be created'() { + /** + * Prepare Testcase + * using all defined repos -> common/repo is used by copyRepo1 + 2 + * file content after that: copyRepo2 + * + * Then again "RESET" to copyRepo1. + * file content after that should be: copyRepo1*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, createJenkinsJob: true, target: 'common/repo'),] + scmmApiClient.mockRepoApiBehaviour() + when(jenkins.isEnabled()).thenReturn(true) + + createContent(config).install() + verify(jenkins).createJenkinsjob(any(), any()) + } + + @Test + void 'ensure Jenkinsjob creation will be ignored'() { + /** + * Prepare Testcase + * using all defined repos -> common/repo is used by copyRepo1 + 2 + * file content after that: copyRepo2 + * + * Then again "RESET" to copyRepo1. + * file content after that should be: copyRepo1*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, createJenkinsJob: false, target: 'common/repo'),] + scmmApiClient.mockRepoApiBehaviour() + when(jenkins.isEnabled()).thenReturn(false) + createContent(config).install() + verify(jenkins, never()).createJenkinsjob(any(), any()) + } + + @Test + void 'ensure Jenkinsjob will not be created, if jenkins is not enables'() { + /** + * Prepare Testcase + * using all defined repos -> common/repo is used by copyRepo1 + 2 + * file content after that: copyRepo2 + * + * Then again "RESET" to copyRepo1. + * file content after that should be: copyRepo1*/ + config.content.repos = [new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, createJenkinsJob: false, target: 'common/repo'),] + scmmApiClient.mockRepoApiBehaviour() + when(jenkins.isEnabled()).thenReturn(false) + + createContent(config).install() + verify(jenkins, never()).createJenkinsjob(any(), any()) + } + + @Test + void 'deployHelmReleasesFromContent skips when helmReleases missing or empty'() { + def contentLoader = createContent(config) + contentLoader.install() + + assertThat(contentLoader.deployCalls).isEmpty() + } + + @Test + void 'deployHelmReleasesFromContent calls deployHelmChart with valuesPath and helm config'() { + // Arrange: create a real values file on disk + Path valuesFile = Files.createTempFile("harbor-values-", ".yaml") + Files.writeString(valuesFile, """ expose: type: ingress """.stripIndent()) - def cfg = Config.fromMap( - content: [ - helmReleases: [ - [ - name : 'harbor', - repoURL : 'https://helm.goharbor.io', - chart : 'harbor', - version : '1.18.2', - namespace : 'my-prefix-harbor', - releaseName: 'harbor', - valuesPath : valuesFile.toString() - ] - ] - ] - ) - - def contentLoader = createContent(cfg) - contentLoader.install() - - assertThat(contentLoader.deployCalls).hasSize(1) - def call = contentLoader.deployCalls[0] - - assertThat(call.featureName).isEqualTo('harbor') - assertThat(call.releaseName).isEqualTo('harbor') - assertThat(call.namespace).isEqualTo('my-prefix-harbor') - - // IMPORTANT: With the new implementation you likely pass a merged temp file, - // not the original valuesPath. So assert it's a file that exists. - assertThat(call.valuesPath).isNotBlank() - assertThat(Path.of(call.valuesPath).toFile()).exists() - - assertThat(call.helmConfig.repoURL).isEqualTo('https://helm.goharbor.io') - assertThat(call.helmConfig.chart).isEqualTo('harbor') - assertThat(call.helmConfig.version).isEqualTo('1.18.2') - assertThat(call.config).isSameAs(cfg) - } - - @Test - void 'deployHelmReleasesFromContent reads values file and inline values override file values'(@TempDir Path tempDir) { - // values file: replicas=1 - Path valuesFile = tempDir.resolve("harbor-values.yaml") - Files.writeString(valuesFile, """ + def cfg = Config.fromMap(content: [helmReleases: [[name : 'harbor', + repoURL : 'https://helm.goharbor.io', + chart : 'harbor', + version : '1.18.2', + namespace : 'my-prefix-harbor', + releaseName: 'harbor', + valuesPath : valuesFile.toString()]]]) + + def contentLoader = createContent(cfg) + contentLoader.install() + + assertThat(contentLoader.deployCalls).hasSize(1) + def call = contentLoader.deployCalls[0] + + assertThat(call.featureName).isEqualTo('harbor') + assertThat(call.releaseName).isEqualTo('harbor') + assertThat(call.namespace).isEqualTo('my-prefix-harbor') + + // IMPORTANT: With the new implementation you likely pass a merged temp file, + // not the original valuesPath. So assert it's a file that exists. + assertThat(call.valuesPath).isNotBlank() + assertThat(Path.of(call.valuesPath).toFile()).exists() + + assertThat(call.helmConfig.repoURL).isEqualTo('https://helm.goharbor.io') + assertThat(call.helmConfig.chart).isEqualTo('harbor') + assertThat(call.helmConfig.version).isEqualTo('1.18.2') + assertThat(call.config).isSameAs(cfg) + } + + @Test + void 'deployHelmReleasesFromContent reads values file and inline values override file values'(@TempDir Path tempDir) { + // values file: replicas=1 + Path valuesFile = tempDir.resolve("harbor-values.yaml") + Files.writeString(valuesFile, """ replicas: 1 service: type: ClusterIP """.stripIndent()) - def cfg = Config.fromMap( - content: [ - helmReleases: [ - [ - name : 'harbor', - repoURL : 'https://helm.goharbor.io', - chart : 'harbor', - version : '1.18.2', - namespace : 'my-prefix-harbor', - releaseName: 'harbor', - valuesPath : valuesFile.toString(), - values : [ - replicas: 2, // override file - service : [type: 'NodePort'] // override nested - ] - ] - ] - ] - ) - - def contentLoader = createContent(cfg) - contentLoader.install() - - assertThat(contentLoader.deployCalls).hasSize(1) - - def call = contentLoader.deployCalls[0] - - // IMPORTANT: valuesPath is a temp file created by writeTempFile(...) - Path mergedTemp = Path.of(call.valuesPath) - assertThat(mergedTemp).exists() - - def mergedYaml = new YamlSlurper().parse(mergedTemp.toFile()) as Map - - // inline overrides file - assertThat(mergedYaml['replicas']).isEqualTo(2) - assertThat(((Map) mergedYaml['service'])['type']).isEqualTo('NodePort') - } - - @Test - void 'deployHelmReleasesFromContent uses values file when inline values are empty'(@TempDir Path tempDir) { - Path valuesFile = tempDir.resolve("values.yaml") - Files.writeString(valuesFile, """ + def cfg = Config.fromMap(content: [helmReleases: [[name : 'harbor', + repoURL : 'https://helm.goharbor.io', + chart : 'harbor', + version : '1.18.2', + namespace : 'my-prefix-harbor', + releaseName: 'harbor', + valuesPath : valuesFile.toString(), + values : [replicas: 2, // override file + service : [type: 'NodePort'] // override nested + ]]]]) + + def contentLoader = createContent(cfg) + contentLoader.install() + + assertThat(contentLoader.deployCalls).hasSize(1) + + def call = contentLoader.deployCalls[0] + + // IMPORTANT: valuesPath is a temp file created by writeTempFile(...) + Path mergedTemp = Path.of(call.valuesPath) + assertThat(mergedTemp).exists() + + def mergedYaml = new YamlSlurper().parse(mergedTemp.toFile()) as Map + + // inline overrides file + assertThat(mergedYaml['replicas']).isEqualTo(2) + assertThat(((Map) mergedYaml['service'])['type']).isEqualTo('NodePort') + } + + @Test + void 'deployHelmReleasesFromContent uses values file when inline values are empty'(@TempDir Path tempDir) { + Path valuesFile = tempDir.resolve("values.yaml") + Files.writeString(valuesFile, """ replicas: 1 """.stripIndent()) - def cfg = Config.fromMap( - content: [ - helmReleases: [ - [ - name : 'elasticsearch', - repoURL : 'https://helm.elastic.co', - chart : 'elasticsearch', - version : '8.5.1', - namespace : 'my-prefix-elasticsearch', - valuesPath: valuesFile.toString() - // no values - ] - ] - ] - ) - - def contentLoader = createContent(cfg) - contentLoader.install() - - assertThat(contentLoader.deployCalls).hasSize(1) - - def call = contentLoader.deployCalls[0] - Path mergedTemp = Path.of(call.valuesPath) - assertThat(mergedTemp).exists() - - def mergedYaml = new YamlSlurper().parse(mergedTemp.toFile()) as Map - assertThat(mergedYaml['replicas']).isEqualTo(1) - } - - @Test - void 'deployHelmReleasesFromContent uses inline values when no helmValuesPath is set'() { - def cfg = Config.fromMap( - content: [ - helmReleases: [ - [ - name : 'elasticsearch', - repoURL : 'https://helm.elastic.co', - chart : 'elasticsearch', - version : '8.5.1', - namespace: 'my-prefix-elasticsearch', - values : [ - replicas: 2 - ] - // helmValuesPath empty / missing - ] - ] - ] - ) - - def contentLoader = createContent(cfg) - contentLoader.install() - - assertThat(contentLoader.deployCalls).hasSize(1) - - def call = contentLoader.deployCalls[0] - Path mergedTemp = Path.of(call.valuesPath) - assertThat(mergedTemp).exists() - - def mergedYaml = new YamlSlurper().parse(mergedTemp.toFile()) as Map - assertThat(mergedYaml['replicas']).isEqualTo(2) - } - - @Test - void 'deployHelmReleasesFromContent defaults chart version to wildcard when missing'() { - def cfg = Config.fromMap( - content: [ - helmReleases: [ - [ - name : 'harbor', - repoURL : 'https://helm.goharbor.io', - chart : 'harbor', - version : ' ', // blank - namespace : 'my-prefix-harbor', - releaseName: 'harbor', - values : [foo: 'bar'] - ] - ] - ] - ) - - def contentLoader = createContent(cfg) - contentLoader.install() - - assertThat(contentLoader.deployCalls).hasSize(1) - def call = contentLoader.deployCalls[0] - - assertThat(call.helmConfig.version).isEqualTo('*') - } - - static String createContentRepo(String initPath = '', String baseBareRepo = 'git-repository') { - // The bare repo works as the "remote" - def bareRepoDir = File.createTempDir('gitops-playground-test-content-repo') - bareRepoDir.deleteOnExit() - foldersToDelete << bareRepoDir - // init with bare repo - FileUtils.copyDirectory(new File(System.getProperty("user.dir") + "/src/test/groovy/com/cloudogu/gitops/utils/data/${baseBareRepo}/"), bareRepoDir) - def bareRepoUri = 'file://' + bareRepoDir.absolutePath - log.debug("Repo $initPath: bare repo $bareRepoUri") - - if (initPath) { - // Add initPath to bare repo - def tempRepo = File.createTempDir('gitops-playground-temp-repo') - tempRepo.deleteOnExit() - foldersToDelete << tempRepo - log.debug("Repo $initPath: cloned bare repo to $tempRepo") - try (def git = Git.cloneRepository() - .setURI(bareRepoUri) - .setBranch('main') - .setDirectory(tempRepo) - .call()) { - - - FileUtils.copyDirectory(new File(System.getProperty("user.dir") + '/src/test/groovy/com/cloudogu/gitops/utils/data/contentRepos/' + initPath), tempRepo) - - git.add().addFilepattern(".").call() - - // Avoid complications with local developer's git config, e.g. when git config --global commit.gpgSign true - SystemReader.getInstance().userConfig.clear() - git.commit().setMessage("Initialize with $initPath").call() - git.push().call() - tempRepo.delete() - } - } - - return bareRepoUri - } - - private Map parseYaml(String path) { - return new YamlSlurper().parse(new File(path)) as Map - } - - private void assertRegistrySecrets(String regUser, String regPw) { - List expectedNamespaces = ["example-apps-staging", "example-apps-production"] - expectedNamespaces.each { - - k8sClient.commandExecutorForTest.assertExecuted( - "kubectl create secret docker-registry registry -n ${it}" + - " --docker-server reg-url --docker-username $regUser --docker-password ${regPw}" + - ' --dry-run=client -oyaml | kubectl apply -f-') - - def patchCommand = k8sClient.commandExecutorForTest.assertExecuted( - "kubectl patch serviceaccount default -n ${it}") - String patchFile = (patchCommand =~ /--patch-file=([\S]+)/)?.findResult { (it as List)[1] } - assertThat(parseActualYaml(new File(patchFile))['imagePullSecrets'] as List).hasSize(1) - assertThat((parseActualYaml(new File(patchFile))['imagePullSecrets'] as List)[0] as Map).containsEntry('name', 'registry') - } - } - - private ContentLoaderForTest createContent(Config config) { - new ContentLoaderForTest(config, k8sClient, scmmRepoProvider, jenkins, gitHandler, fileSystemUtils, deploymentStrategy) - } - - private static parseActualYaml(File pathToYamlFile) { - def ys = new YamlSlurper() - return ys.parse(pathToYamlFile) - } - - private static String findRoot(List repos) { - def result = new File(repos.get(0).getClonedContentRepo().getParent()).getParent() - return result; - - } - - Git cloneRepo(String expectedRepo, File repoFolder) { - def repo = scmmRepoProvider.getRepo(expectedRepo, new ScmManagerMock()) - def url = repo.getGitRepositoryUrl() - - def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(repoFolder).call() - git.getRepository().getConfig().setBoolean("gc", null, "autoDetach", false) - return git - } - - - private File createRandomSubDir(String prefix = '') { - def randomDir = tmpDir.toPath().resolve("${prefix ? "${prefix}-" : ''}${System.currentTimeMillis()}").toFile() - randomDir.mkdirs() - return randomDir - } - - void assertTagAndReadme(String repo, String expectedTag, String expectedReadmeContent) { - def repoFolder = createRandomSubDir() - try (def git = cloneRepo(repo, repoFolder)) { - git.fetch().setRefSpecs("refs/*:refs/*").call() - assertTag(git, expectedTag) - - git.checkout().setName(expectedTag).call() - assertThat(new File(repoFolder, "README.md")).exists().isFile() - assertThat(new File(repoFolder, "README.md").text).contains(expectedReadmeContent) - } - } - - void assertBranchAndReadme(String repo, String expectedBranch, String expectedReadmeContent) { - def repoFolder = createRandomSubDir() - try (def git = cloneRepo(repo, repoFolder)) { - git.fetch().setRefSpecs("refs/*:refs/*").call() - assertBranch(git, expectedBranch) - - git.checkout().setName(expectedBranch).call() - assertThat(new File(repoFolder, "README.md")).exists().isFile() - assertThat(new File(repoFolder, "README.md").text).contains(expectedReadmeContent) - } - } - - class ContentLoaderForTest extends ContentLoader { - List deployCalls = [] - CloneCommand cloneSpy - - ContentLoaderForTest(Config config, K8sClient k8sClient, GitRepoFactory repoProvider, Jenkins jenkins, GitHandler gitHandler, FileSystemUtils fileSystemUtils, DeploymentStrategy deploymentStrategy) { - super(config, k8sClient, repoProvider, jenkins, gitHandler, fileSystemUtils, deploymentStrategy) - } - - @Override - protected void deployHelmChart(String featureName, - String releaseName, - String namespace, - Config.HelmConfigWithValues helmConfig, - String helmValuesTemplatePath, - Config config) { - deployCalls << new DeployCall( - featureName: featureName, - releaseName: releaseName, - namespace: namespace, - helmConfig: helmConfig, - valuesPath: helmValuesTemplatePath, - config: config - ) - } - - @Override - protected CloneCommand gitClone() { - cloneSpy = spy(super.gitClone().setNoCheckout(true)) - } - } - static class DeployCall { - String featureName - String releaseName - String namespace - Config.HelmConfigWithValues helmConfig - String valuesPath - Config config - } -} + def cfg = Config.fromMap(content: [helmReleases: [[name : 'elasticsearch', + repoURL : 'https://helm.elastic.co', + chart : 'elasticsearch', + version : '8.5.1', + namespace : 'my-prefix-elasticsearch', + valuesPath: valuesFile.toString() + // no values + ]]]) + + def contentLoader = createContent(cfg) + contentLoader.install() + + assertThat(contentLoader.deployCalls).hasSize(1) + + def call = contentLoader.deployCalls[0] + Path mergedTemp = Path.of(call.valuesPath) + assertThat(mergedTemp).exists() + + def mergedYaml = new YamlSlurper().parse(mergedTemp.toFile()) as Map + assertThat(mergedYaml['replicas']).isEqualTo(1) + } + + @Test + void 'deployHelmReleasesFromContent uses inline values when no helmValuesPath is set'() { + def cfg = Config.fromMap(content: [helmReleases: [[name : 'elasticsearch', + repoURL : 'https://helm.elastic.co', + chart : 'elasticsearch', + version : '8.5.1', + namespace: 'my-prefix-elasticsearch', + values : [replicas: 2] + // helmValuesPath empty / missing + ]]]) + + def contentLoader = createContent(cfg) + contentLoader.install() + + assertThat(contentLoader.deployCalls).hasSize(1) + + def call = contentLoader.deployCalls[0] + Path mergedTemp = Path.of(call.valuesPath) + assertThat(mergedTemp).exists() + + def mergedYaml = new YamlSlurper().parse(mergedTemp.toFile()) as Map + assertThat(mergedYaml['replicas']).isEqualTo(2) + } + + @Test + void 'deployHelmReleasesFromContent defaults chart version to wildcard when missing'() { + def cfg = Config.fromMap(content: [helmReleases: [[name : 'harbor', + repoURL : 'https://helm.goharbor.io', + chart : 'harbor', + version : ' ', // blank + namespace : 'my-prefix-harbor', + releaseName: 'harbor', + values : [foo: 'bar']]]]) + + def contentLoader = createContent(cfg) + contentLoader.install() + + assertThat(contentLoader.deployCalls).hasSize(1) + def call = contentLoader.deployCalls[0] + + assertThat(call.helmConfig.version).isEqualTo('*') + } + + static String createContentRepo(String initPath = '', String baseBareRepo = 'git-repository') { + // The bare repo works as the "remote" + def bareRepoDir = File.createTempDir('gitops-playground-test-content-repo') + bareRepoDir.deleteOnExit() + foldersToDelete << bareRepoDir + // init with bare repo + FileUtils.copyDirectory(new File(System.getProperty("user.dir") + "/src/test/groovy/com/cloudogu/gitops/utils/data/${baseBareRepo}/"), bareRepoDir) + def bareRepoUri = 'file://' + bareRepoDir.absolutePath + log.debug("Repo $initPath: bare repo $bareRepoUri") + + if (initPath) { + // Add initPath to bare repo + def tempRepo = File.createTempDir('gitops-playground-temp-repo') + tempRepo.deleteOnExit() + foldersToDelete << tempRepo + log.debug("Repo $initPath: cloned bare repo to $tempRepo") + try (def git = Git.cloneRepository() + .setURI(bareRepoUri) + .setBranch('main') + .setDirectory(tempRepo) + .call()) { + + FileUtils.copyDirectory(new File(System.getProperty("user.dir") + '/src/test/groovy/com/cloudogu/gitops/utils/data/contentRepos/' + initPath), tempRepo) + + git.add().addFilepattern(".").call() + + // Avoid complications with local developer's git config, e.g. when git config --global commit.gpgSign true + SystemReader.getInstance().userConfig.clear() + git.commit().setMessage("Initialize with $initPath").call() + git.push().call() + tempRepo.delete() + } + } + + return bareRepoUri + } + + private Map parseYaml(String path) { + return new YamlSlurper().parse(new File(path)) as Map + } + + private void assertRegistrySecrets(String regUser, String regPw) { + List expectedNamespaces = ["example-apps-staging", "example-apps-production"] + expectedNamespaces.each { + + k8sClient.commandExecutorForTest.assertExecuted("kubectl create secret docker-registry registry -n ${it}" + " --docker-server reg-url --docker-username $regUser --docker-password ${regPw}" + + ' --dry-run=client -oyaml | kubectl apply -f-') + + def patchCommand = k8sClient.commandExecutorForTest.assertExecuted("kubectl patch serviceaccount default -n ${it}") + String patchFile = (patchCommand =~ /--patch-file=([\S]+)/)?.findResult { (it as List)[1] } + assertThat(parseActualYaml(new File(patchFile))['imagePullSecrets'] as List).hasSize(1) + assertThat((parseActualYaml(new File(patchFile))['imagePullSecrets'] as List)[0] as Map).containsEntry('name', 'registry') + } + } + + private ContentLoaderForTest createContent(Config config) { + new ContentLoaderForTest(config, k8sClient, scmmRepoProvider, jenkins, gitHandler, fileSystemUtils, deploymentStrategy) + } + + private static parseActualYaml(File pathToYamlFile) { + def ys = new YamlSlurper() + return ys.parse(pathToYamlFile) + } + + private static String findRoot(List repos) { + def result = new File(repos.get(0).getClonedContentRepo().getParent()).getParent() + return result; + + } + + Git cloneRepo(String expectedRepo, File repoFolder) { + def repo = scmmRepoProvider.getRepo(expectedRepo, new ScmManagerMock()) + def url = repo.getGitRepositoryUrl() + + def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(repoFolder).call() + git.getRepository().getConfig().setBoolean("gc", null, "autoDetach", false) + return git + } + + private File createRandomSubDir(String prefix = '') { + def randomDir = tmpDir.toPath().resolve("${prefix ? "${prefix}-" : ''}${System.currentTimeMillis()}").toFile() + randomDir.mkdirs() + return randomDir + } + + void assertTagAndReadme(String repo, String expectedTag, String expectedReadmeContent) { + def repoFolder = createRandomSubDir() + try (def git = cloneRepo(repo, repoFolder)) { + git.fetch().setRefSpecs("refs/*:refs/*").call() + assertTag(git, expectedTag) + + git.checkout().setName(expectedTag).call() + assertThat(new File(repoFolder, "README.md")).exists().isFile() + assertThat(new File(repoFolder, "README.md").text).contains(expectedReadmeContent) + } + } + + void assertBranchAndReadme(String repo, String expectedBranch, String expectedReadmeContent) { + def repoFolder = createRandomSubDir() + try (def git = cloneRepo(repo, repoFolder)) { + git.fetch().setRefSpecs("refs/*:refs/*").call() + assertBranch(git, expectedBranch) + + git.checkout().setName(expectedBranch).call() + assertThat(new File(repoFolder, "README.md")).exists().isFile() + assertThat(new File(repoFolder, "README.md").text).contains(expectedReadmeContent) + } + } + + class ContentLoaderForTest extends ContentLoader { + List deployCalls = [] + CloneCommand cloneSpy + + ContentLoaderForTest(Config config, K8sClient k8sClient, GitRepoFactory repoProvider, Jenkins jenkins, GitHandler gitHandler, FileSystemUtils fileSystemUtils, + DeploymentStrategy deploymentStrategy) { + super(config, k8sClient, repoProvider, jenkins, gitHandler, fileSystemUtils, deploymentStrategy) + } + + @Override + protected void deployHelmChart(String featureName, + String releaseName, + String namespace, + Config.HelmConfigWithValues helmConfig, + String helmValuesTemplatePath, + Config config) { + deployCalls << new DeployCall(featureName: featureName, + releaseName: releaseName, + namespace: namespace, + helmConfig: helmConfig, + valuesPath: helmValuesTemplatePath, + config: config) + } + + @Override + protected CloneCommand gitClone() { + cloneSpy = spy(super.gitClone().setNoCheckout(true)) + } + } + + static class DeployCall { + String featureName + String releaseName + String namespace + Config.HelmConfigWithValues helmConfig + String valuesPath + Config config + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy index a30489e94..3b3fd36df 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy @@ -1,373 +1,362 @@ package com.cloudogu.gitops.features +import static com.cloudogu.gitops.features.deployment.DeploymentStrategy.RepoType +import static org.assertj.core.api.Assertions.assertThat +import static org.mockito.ArgumentMatchers.* +import static org.mockito.Mockito.* + import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.features.deployment.DeploymentStrategy import com.cloudogu.gitops.features.deployment.HelmStrategy import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.features.git.config.ScmTenantSchema import com.cloudogu.gitops.jenkins.GlobalPropertyManager import com.cloudogu.gitops.jenkins.JobManager import com.cloudogu.gitops.jenkins.PrometheusConfigurator import com.cloudogu.gitops.jenkins.UserManager +import com.cloudogu.gitops.kubernetes.api.K8sClient import com.cloudogu.gitops.utils.CommandExecutorForTest import com.cloudogu.gitops.utils.FileSystemUtils -import com.cloudogu.gitops.kubernetes.api.K8sClient import com.cloudogu.gitops.utils.NetworkingUtils import com.cloudogu.gitops.utils.git.GitHandlerForTests import com.cloudogu.gitops.utils.git.ScmManagerMock -import com.cloudogu.gitops.features.git.config.ScmTenantSchema + +import java.nio.file.Path import groovy.yaml.YamlSlurper + import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.ArgumentCaptor import org.mockito.Mock -import java.nio.file.Path - -import static com.cloudogu.gitops.features.deployment.DeploymentStrategy.* -import static org.assertj.core.api.Assertions.assertThat -import static org.mockito.ArgumentMatchers.* -import static org.mockito.Mockito.* - class JenkinsTest { - Config config = new Config( - scm: new ScmTenantSchema( - scmManager: new ScmTenantSchema.ScmManagerTenantConfig( - urlForJenkins: "testUrlJenkins" - )), - jenkins: new Config.JenkinsSchema(active: true)) - - String expectedNodeName = 'something' - - CommandExecutorForTest commandExecutor = new CommandExecutorForTest() - GlobalPropertyManager globalPropertyManager = mock(GlobalPropertyManager) - JobManager jobManger = mock(JobManager) - UserManager userManager = mock(UserManager) - PrometheusConfigurator prometheusConfigurator = mock(PrometheusConfigurator) - HelmStrategy deploymentStrategy = mock(HelmStrategy) - Path temporaryYamlFile - NetworkingUtils networkingUtils = mock(NetworkingUtils.class) - K8sClient k8sClient = mock(K8sClient) - - @Mock - ScmManagerMock scmManagerMock = new ScmManagerMock() - GitHandler gitHandler = new GitHandlerForTests(config, scmManagerMock) - - @BeforeEach - void setup() { - // waitForInternalNodeIp -> waitForNode() - when(k8sClient.waitForNode()).thenReturn("node/${expectedNodeName}".toString()) - when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any())).thenReturn('') - } - - @Test - void 'Installs Jenkins'() { - def jenkins = createJenkins() - - config.jenkins.url = 'http://jenkins' - config.jenkins.helm.chart = 'jen-chart' - config.jenkins.helm.repoURL = 'https://jen-repo' - config.jenkins.helm.version = '4.8.1' - config.jenkins.username = 'jenusr' - config.jenkins.password = 'jenpw' - config.jenkins.internalBashImage = 'bash:42' - config.jenkins.internalDockerClientVersion = '23' - - when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any(String[].class))).thenReturn(''' + Config config = new Config(scm: new ScmTenantSchema(scmManager: new ScmTenantSchema.ScmManagerTenantConfig(urlForJenkins: "testUrlJenkins")), + jenkins: new Config.JenkinsSchema(active: true)) + + String expectedNodeName = 'something' + + CommandExecutorForTest commandExecutor = new CommandExecutorForTest() + GlobalPropertyManager globalPropertyManager = mock(GlobalPropertyManager) + JobManager jobManger = mock(JobManager) + UserManager userManager = mock(UserManager) + PrometheusConfigurator prometheusConfigurator = mock(PrometheusConfigurator) + HelmStrategy deploymentStrategy = mock(HelmStrategy) + Path temporaryYamlFile + NetworkingUtils networkingUtils = mock(NetworkingUtils.class) + K8sClient k8sClient = mock(K8sClient) + + @Mock + ScmManagerMock scmManagerMock = new ScmManagerMock() + GitHandler gitHandler = new GitHandlerForTests(config, scmManagerMock) + + @BeforeEach + void setup() { + // waitForInternalNodeIp -> waitForNode() + when(k8sClient.waitForNode()).thenReturn("node/${expectedNodeName}".toString()) + when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any())).thenReturn('') + } + + @Test + void 'Installs Jenkins'() { + def jenkins = createJenkins() + + config.jenkins.url = 'http://jenkins' + config.jenkins.helm.chart = 'jen-chart' + config.jenkins.helm.repoURL = 'https://jen-repo' + config.jenkins.helm.version = '4.8.1' + config.jenkins.username = 'jenusr' + config.jenkins.password = 'jenpw' + config.jenkins.internalBashImage = 'bash:42' + config.jenkins.internalDockerClientVersion = '23' + + when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any(String[].class))).thenReturn(''' root:x:0: daemon:x:1: docker:x:42:me me:x:1000:''') - jenkins.install() + jenkins.install() - verify(deploymentStrategy).deployFeature('https://jen-repo', 'jenkins', - 'jen-chart', '4.8.1', 'jenkins', - 'jenkins', temporaryYamlFile, RepoType.HELM) - verify(k8sClient).label('node', expectedNodeName, new Tuple2('node', 'jenkins')) - verify(k8sClient).labelRemove('node', '--all', '', 'node') - verify(k8sClient).createSecret('generic', 'jenkins-credentials', 'jenkins', - new Tuple2('jenkins-admin-user', 'jenusr'), - new Tuple2('jenkins-admin-password', 'jenpw')) + verify(deploymentStrategy).deployFeature('https://jen-repo', 'jenkins', + 'jen-chart', '4.8.1', 'jenkins', + 'jenkins', temporaryYamlFile, RepoType.HELM) + verify(k8sClient).label('node', expectedNodeName, new Tuple2('node', 'jenkins')) + verify(k8sClient).labelRemove('node', '--all', '', 'node') + verify(k8sClient).createSecret('generic', 'jenkins-credentials', 'jenkins', + new Tuple2('jenkins-admin-user', 'jenusr'), + new Tuple2('jenkins-admin-password', 'jenpw')) - assertThat(parseActualYaml()['dockerClientVersion'].toString()).isEqualTo('23') + assertThat(parseActualYaml()['dockerClientVersion'].toString()).isEqualTo('23') - assertThat(parseActualYaml()['controller']['image']['tag']).isEqualTo('4.8.1') + assertThat(parseActualYaml()['controller']['image']['tag']).isEqualTo('4.8.1') - assertThat(parseActualYaml()['controller']['jenkinsUrl']).isEqualTo('http://jenkins') - assertThat(parseActualYaml()['controller']['serviceType']).isEqualTo('NodePort') + assertThat(parseActualYaml()['controller']['jenkinsUrl']).isEqualTo('http://jenkins') + assertThat(parseActualYaml()['controller']['serviceType']).isEqualTo('NodePort') - assertThat(parseActualYaml()['controller']['ingress']).isNull() + assertThat(parseActualYaml()['controller']['ingress']).isNull() - List customInitContainers = parseActualYaml()['controller']['customInitContainers'] as List - assertThat(customInitContainers[0]['image']).isEqualTo('bash:42') + List customInitContainers = parseActualYaml()['controller']['customInitContainers'] as List + assertThat(customInitContainers[0]['image']).isEqualTo('bash:42') - assertThat(parseActualYaml()['agent']['runAsUser']).isEqualTo(1000) - assertThat(parseActualYaml()['agent']['runAsGroup']).isEqualTo(42) + assertThat(parseActualYaml()['agent']['runAsUser']).isEqualTo(1000) + assertThat(parseActualYaml()['agent']['runAsGroup']).isEqualTo(42) - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor overridesCaptor = ArgumentCaptor.forClass(Map.class); - verify(k8sClient).run(nameCaptor.capture(), anyString(), eq(jenkins.namespace), overridesCaptor.capture(), any(String[].class)) - assertThat(nameCaptor.value).startsWith('tmp-docker-gid-grepper-') - List containers = overridesCaptor.value['spec']['containers'] as List - assertThat(containers[0]['image'].toString()).isEqualTo('bash:42') - } + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor overridesCaptor = ArgumentCaptor.forClass(Map.class); + verify(k8sClient).run(nameCaptor.capture(), anyString(), eq(jenkins.namespace), overridesCaptor.capture(), any(String[].class)) + assertThat(nameCaptor.value).startsWith('tmp-docker-gid-grepper-') + List containers = overridesCaptor.value['spec']['containers'] as List + assertThat(containers[0]['image'].toString()).isEqualTo('bash:42') + } - @Test - void 'Installs Jenkins without dockerGid'() { - when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any())).thenReturn(''' + @Test + void 'Installs Jenkins without dockerGid'() { + when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any())).thenReturn(''' root:x:0: daemon:x:1: me:x:1000:''') - createJenkins().install() - - assertThat(parseActualYaml()['agent']['runAsUser']).isEqualTo('0') - assertThat(parseActualYaml()['agent']['runAsGroup']).isEqualTo('133') - } - - @Test - void 'Installs only if internal'() { - config.jenkins.internal = false - - createJenkins().install() - verify(deploymentStrategy, never()).deployFeature(anyString(), anyString(), anyString(), anyString(), - anyString(), anyString(), any(Path)) - - assertThat(temporaryYamlFile).isNull() - } - - @Test - void 'Additional helm values are merged with default values'() { - config.jenkins.helm.values = [ - controller: [ - nodePort: 42 - ] - ] - - createJenkins().install() - - assertThat(parseActualYaml()['controller']['nodePort']).isEqualTo(42) - } - - @Test - void 'Enables ingress when baseUrl is set'() { - config.jenkins.ingress = 'jenkins.localhost' - config.application.baseUrl = 'someBaseUrl' - - createJenkins().install() - - assertThat(parseActualYaml()['controller']['ingress']['enabled']).isEqualTo(true) - assertThat(parseActualYaml()['controller']['ingress']['hostName']).isEqualTo('jenkins.localhost') - } - - @Test - void 'Maps config properly'() { - config.application.trace = true - config.features.argocd.active = true - config.scm.scmManager.url = 'http://scmm.scm-manager.svc.cluster.local/scm' - config.scm.scmManager.username = 'scmm-usr' - config.scm.scmManager.password = 'scmm-pw' - config.application.namePrefix = 'my-prefix-' - config.application.namePrefixForEnvVars = 'MY_PREFIX_' - config.registry.url = 'reg-url' - config.registry.path = 'reg-path' - config.registry.username = 'reg-usr' - config.registry.password = 'reg-pw' - config.registry.proxyUrl = 'reg-proxy-url' - config.registry.proxyPath = 'reg-proxy-path' - config.registry.proxyUsername = 'reg-proxy-usr' - config.registry.proxyPassword = 'reg-proxy-pw' - config.jenkins.internal = false - config.jenkins.helm.version = '4.8.1' - config.jenkins.username = 'jenusr' - config.jenkins.password = 'jenpw' - config.jenkins.url = 'http://jenkins' - config.jenkins.metricsUsername = 'metrics-usr' - config.jenkins.metricsPassword = 'metrics-pw' - config.jenkins.skipPlugins = true - config.jenkins.skipRestart = true - - createJenkins().install() - - def env = getEnvAsMap() - assertThat(commandExecutor.actualCommands[0]).isEqualTo( - "${System.getProperty('user.dir')}/scripts/jenkins/init-jenkins.sh" as String) - - assertThat(env['TRACE']).isEqualTo('true') - assertThat(env['INTERNAL_JENKINS']).isEqualTo('false') - assertThat(env['JENKINS_HELM_CHART_VERSION']).isEqualTo('4.8.1') - assertThat(env['JENKINS_URL']).isEqualTo('http://jenkins') - assertThat(env['JENKINS_USERNAME']).isEqualTo('jenusr') - assertThat(env['JENKINS_PASSWORD']).isEqualTo('jenpw') - assertThat(env['JENKINS_USERNAME']).isEqualTo('jenusr') - assertThat(env['NAME_PREFIX']).isEqualTo('my-prefix-') - assertThat(env['INSECURE']).isEqualTo('false') - - assertThat(env['SCM_URL']).isEqualTo('http://scmm.scm-manager.svc.cluster.local/scm') - assertThat(env['SCM_PASSWORD']).isEqualTo(scmManagerMock.credentials.password) - assertThat(env['INSTALL_ARGOCD']).isEqualTo('true') - - assertThat(env['SKIP_PLUGINS']).isEqualTo('true') - assertThat(env['SKIP_RESTART']).isEqualTo('true') - - verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_SCM_URL', 'http://scmm.scm-manager.svc.cluster.local/scm') - verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_K8S_VERSION', Config.K8S_VERSION) - - verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_URL', 'reg-url') - verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_PATH', 'reg-path') - verify(globalPropertyManager, never()).setGlobalProperty(eq('MY_PREFIX_REGISTRY_PROXY_URL'), anyString()) - verify(globalPropertyManager, never()).setGlobalProperty(eq('MY_PREFIX_REGISTRY_PROXY_PATH'), anyString()) - verify(globalPropertyManager, never()).setGlobalProperty(eq('MAVEN_CENTRAL_MIRROR'), anyString()) - - verify(userManager).createUser('metrics-usr', 'metrics-pw') - verify(userManager).grantPermission('metrics-usr', UserManager.Permissions.METRICS_VIEW) - } - - @Test - void 'Does not configure prometheus when external Jenkins'() { - config.features.monitoring.active = true - config.jenkins.internal = false - - createJenkins().install() - - verify(prometheusConfigurator, never()).enableAuthentication() - } - - @Test - void 'Does not configure prometheus when monitoring off'() { - config.features.monitoring.active = false - config.jenkins.internal = true - - createJenkins().install() - - verify(prometheusConfigurator, never()).enableAuthentication() - } - - @Test - void 'Configures prometheus'() { - config.features.monitoring.active = true - config.jenkins.internal = true - - createJenkins().install() - - verify(prometheusConfigurator).enableAuthentication() - } - - @Test - void "URL: Use k8s service name if running as k8s pod"() { - config.jenkins.internal = true - config.application.runningInsideK8s = true - - createJenkins().install() - assertThat(config.jenkins.url).isEqualTo("http://jenkins.jenkins.svc.cluster.local:80") - } - - @Test - void "URL: Use local ip and nodePort when outside of k8s"() { - config.jenkins.internal = true - config.application.runningInsideK8s = false - - when(networkingUtils.findClusterBindAddress()).thenReturn('192.168.16.2') - when(k8sClient.waitForNodePort(anyString(), anyString())).thenReturn('42') - - createJenkins().install() - assertThat(config.jenkins.url).endsWith('192.168.16.2:42') - } - - @Test - void 'Handles two registries'() { - config.registry.twoRegistries = true - config.application.namePrefix = 'my-prefix-' - config.application.namePrefixForEnvVars = 'MY_PREFIX_' - - config.registry.url = 'reg-url' - config.registry.path = 'reg-path' - config.registry.username = 'reg-usr' - config.registry.password = 'reg-pw' - config.registry.proxyUrl = 'reg-proxy-url' - config.registry.proxyPath = 'reg-proxy-path' - config.registry.proxyUsername = 'reg-proxy-usr' - config.registry.proxyPassword = 'reg-proxy-pw' - - createJenkins().install() - - verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_PROXY_URL', 'reg-proxy-url') - verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_PROXY_PATH', 'reg-proxy-path') - - verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_REGISTRY_URL'), anyString()) - verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_REGISTRY_PATH'), anyString()) - - } - - @Test - void 'Does not create create job credentials when argo cd is deactivated'() { - config.application.namePrefixForEnvVars = 'MY_PREFIX_' - when(userManager.isUsingCasSecurityRealm()).thenReturn(true) - - createJenkins().install() - - verify(userManager, never()).createUser(anyString(), anyString()) - } - - @Test - void 'Global property is set for additional envs'() { - - config.jenkins.additionalEnvs = [ - ADDITIONAL_DOCKER_RUN_ARGS: '-u0:0' - ] - - createJenkins().install() - verify(globalPropertyManager).setGlobalProperty(eq('ADDITIONAL_DOCKER_RUN_ARGS'), eq('-u0:0')) - } - - @Test - void 'Does not create create user if CAS security realm is used'() { - config.features.argocd.active = false - - createJenkins().install() - verify(jobManger, never()).createCredential(anyString(), anyString(), anyString(), anyString(), anyString()) - verify(jobManger, never()).startJob(anyString()) - } - - @Test - void 'Properly handles null values'() { - config.application.baseUrl = null - createJenkins().install() - - def env = getEnvAsMap() - assertThat(env['BASE_URL']).isNotEqualTo('null') - } - - @Test - void 'Sets maven mirror '() { - config.registry.url = 'some value' - config.jenkins.mavenCentralMirror = 'http://test' - config.application.namePrefixForEnvVars = 'MY_PREFIX_' - - createJenkins().install() - - verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_MAVEN_CENTRAL_MIRROR'), eq("http://test")) - } - - protected Map getEnvAsMap() { - commandExecutor.environment.collectEntries { it.split('=') } - } - - private Jenkins createJenkins() { - when(networkingUtils.createUrl(anyString(), anyString(), anyString())).thenCallRealMethod() - when(networkingUtils.createUrl(anyString(), anyString())).thenCallRealMethod() - new Jenkins(config, commandExecutor, new FileSystemUtils() { - @Override - Path writeTempFile(Map mergeMap) { - def ret = super.writeTempFile(mergeMap) - temporaryYamlFile = Path.of(ret.toString().replace(".ftl", "")) - // Path after template invocation - return ret - } - }, globalPropertyManager, jobManger, userManager, prometheusConfigurator, deploymentStrategy, k8sClient, networkingUtils, gitHandler) - } - - private Map parseActualYaml() { - def ys = new YamlSlurper() - return ys.parse(temporaryYamlFile) as Map - } -} + createJenkins().install() + + assertThat(parseActualYaml()['agent']['runAsUser']).isEqualTo('0') + assertThat(parseActualYaml()['agent']['runAsGroup']).isEqualTo('133') + } + + @Test + void 'Installs only if internal'() { + config.jenkins.internal = false + + createJenkins().install() + verify(deploymentStrategy, never()).deployFeature(anyString(), anyString(), anyString(), anyString(), + anyString(), anyString(), any(Path)) + + assertThat(temporaryYamlFile).isNull() + } + + @Test + void 'Additional helm values are merged with default values'() { + config.jenkins.helm.values = [controller: [nodePort: 42]] + + createJenkins().install() + + assertThat(parseActualYaml()['controller']['nodePort']).isEqualTo(42) + } + + @Test + void 'Enables ingress when baseUrl is set'() { + config.jenkins.ingress = 'jenkins.localhost' + config.application.baseUrl = 'someBaseUrl' + + createJenkins().install() + + assertThat(parseActualYaml()['controller']['ingress']['enabled']).isEqualTo(true) + assertThat(parseActualYaml()['controller']['ingress']['hostName']).isEqualTo('jenkins.localhost') + } + + @Test + void 'Maps config properly'() { + config.application.trace = true + config.features.argocd.active = true + config.scm.scmManager.url = 'http://scmm.scm-manager.svc.cluster.local/scm' + config.scm.scmManager.username = 'scmm-usr' + config.scm.scmManager.password = 'scmm-pw' + config.application.namePrefix = 'my-prefix-' + config.application.namePrefixForEnvVars = 'MY_PREFIX_' + config.registry.url = 'reg-url' + config.registry.path = 'reg-path' + config.registry.username = 'reg-usr' + config.registry.password = 'reg-pw' + config.registry.proxyUrl = 'reg-proxy-url' + config.registry.proxyPath = 'reg-proxy-path' + config.registry.proxyUsername = 'reg-proxy-usr' + config.registry.proxyPassword = 'reg-proxy-pw' + config.jenkins.internal = false + config.jenkins.helm.version = '4.8.1' + config.jenkins.username = 'jenusr' + config.jenkins.password = 'jenpw' + config.jenkins.url = 'http://jenkins' + config.jenkins.metricsUsername = 'metrics-usr' + config.jenkins.metricsPassword = 'metrics-pw' + config.jenkins.skipPlugins = true + config.jenkins.skipRestart = true + + createJenkins().install() + + def env = getEnvAsMap() + assertThat(commandExecutor.actualCommands[0]).isEqualTo("${System.getProperty('user.dir')}/scripts/jenkins/init-jenkins.sh" as String) + + assertThat(env['TRACE']).isEqualTo('true') + assertThat(env['INTERNAL_JENKINS']).isEqualTo('false') + assertThat(env['JENKINS_HELM_CHART_VERSION']).isEqualTo('4.8.1') + assertThat(env['JENKINS_URL']).isEqualTo('http://jenkins') + assertThat(env['JENKINS_USERNAME']).isEqualTo('jenusr') + assertThat(env['JENKINS_PASSWORD']).isEqualTo('jenpw') + assertThat(env['JENKINS_USERNAME']).isEqualTo('jenusr') + assertThat(env['NAME_PREFIX']).isEqualTo('my-prefix-') + assertThat(env['INSECURE']).isEqualTo('false') + + assertThat(env['SCM_URL']).isEqualTo('http://scmm.scm-manager.svc.cluster.local/scm') + assertThat(env['SCM_PASSWORD']).isEqualTo(scmManagerMock.credentials.password) + assertThat(env['INSTALL_ARGOCD']).isEqualTo('true') + + assertThat(env['SKIP_PLUGINS']).isEqualTo('true') + assertThat(env['SKIP_RESTART']).isEqualTo('true') + + verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_SCM_URL', 'http://scmm.scm-manager.svc.cluster.local/scm') + verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_K8S_VERSION', Config.K8S_VERSION) + + verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_URL', 'reg-url') + verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_PATH', 'reg-path') + verify(globalPropertyManager, never()).setGlobalProperty(eq('MY_PREFIX_REGISTRY_PROXY_URL'), anyString()) + verify(globalPropertyManager, never()).setGlobalProperty(eq('MY_PREFIX_REGISTRY_PROXY_PATH'), anyString()) + verify(globalPropertyManager, never()).setGlobalProperty(eq('MAVEN_CENTRAL_MIRROR'), anyString()) + + verify(userManager).createUser('metrics-usr', 'metrics-pw') + verify(userManager).grantPermission('metrics-usr', UserManager.Permissions.METRICS_VIEW) + } + + @Test + void 'Does not configure prometheus when external Jenkins'() { + config.features.monitoring.active = true + config.jenkins.internal = false + + createJenkins().install() + + verify(prometheusConfigurator, never()).enableAuthentication() + } + + @Test + void 'Does not configure prometheus when monitoring off'() { + config.features.monitoring.active = false + config.jenkins.internal = true + + createJenkins().install() + + verify(prometheusConfigurator, never()).enableAuthentication() + } + + @Test + void 'Configures prometheus'() { + config.features.monitoring.active = true + config.jenkins.internal = true + + createJenkins().install() + + verify(prometheusConfigurator).enableAuthentication() + } + + @Test + void "URL: Use k8s service name if running as k8s pod"() { + config.jenkins.internal = true + config.application.runningInsideK8s = true + + createJenkins().install() + assertThat(config.jenkins.url).isEqualTo("http://jenkins.jenkins.svc.cluster.local:80") + } + + @Test + void "URL: Use local ip and nodePort when outside of k8s"() { + config.jenkins.internal = true + config.application.runningInsideK8s = false + + when(networkingUtils.findClusterBindAddress()).thenReturn('192.168.16.2') + when(k8sClient.waitForNodePort(anyString(), anyString())).thenReturn('42') + + createJenkins().install() + assertThat(config.jenkins.url).endsWith('192.168.16.2:42') + } + + @Test + void 'Handles two registries'() { + config.registry.twoRegistries = true + config.application.namePrefix = 'my-prefix-' + config.application.namePrefixForEnvVars = 'MY_PREFIX_' + + config.registry.url = 'reg-url' + config.registry.path = 'reg-path' + config.registry.username = 'reg-usr' + config.registry.password = 'reg-pw' + config.registry.proxyUrl = 'reg-proxy-url' + config.registry.proxyPath = 'reg-proxy-path' + config.registry.proxyUsername = 'reg-proxy-usr' + config.registry.proxyPassword = 'reg-proxy-pw' + + createJenkins().install() + + verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_PROXY_URL', 'reg-proxy-url') + verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_PROXY_PATH', 'reg-proxy-path') + + verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_REGISTRY_URL'), anyString()) + verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_REGISTRY_PATH'), anyString()) + + } + + @Test + void 'Does not create create job credentials when argo cd is deactivated'() { + config.application.namePrefixForEnvVars = 'MY_PREFIX_' + when(userManager.isUsingCasSecurityRealm()).thenReturn(true) + + createJenkins().install() + + verify(userManager, never()).createUser(anyString(), anyString()) + } + + @Test + void 'Global property is set for additional envs'() { + + config.jenkins.additionalEnvs = [ADDITIONAL_DOCKER_RUN_ARGS: '-u0:0'] + + createJenkins().install() + verify(globalPropertyManager).setGlobalProperty(eq('ADDITIONAL_DOCKER_RUN_ARGS'), eq('-u0:0')) + } + + @Test + void 'Does not create create user if CAS security realm is used'() { + config.features.argocd.active = false + + createJenkins().install() + verify(jobManger, never()).createCredential(anyString(), anyString(), anyString(), anyString(), anyString()) + verify(jobManger, never()).startJob(anyString()) + } + + @Test + void 'Properly handles null values'() { + config.application.baseUrl = null + createJenkins().install() + + def env = getEnvAsMap() + assertThat(env['BASE_URL']).isNotEqualTo('null') + } + + @Test + void 'Sets maven mirror '() { + config.registry.url = 'some value' + config.jenkins.mavenCentralMirror = 'http://test' + config.application.namePrefixForEnvVars = 'MY_PREFIX_' + + createJenkins().install() + + verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_MAVEN_CENTRAL_MIRROR'), eq("http://test")) + } + + protected Map getEnvAsMap() { + commandExecutor.environment.collectEntries { it.split('=') } + } + + private Jenkins createJenkins() { + when(networkingUtils.createUrl(anyString(), anyString(), anyString())).thenCallRealMethod() + when(networkingUtils.createUrl(anyString(), anyString())).thenCallRealMethod() + new Jenkins(config, commandExecutor, new FileSystemUtils() { + @Override + Path writeTempFile(Map mergeMap) { + def ret = super.writeTempFile(mergeMap) + temporaryYamlFile = Path.of(ret.toString().replace(".ftl", "")) + // Path after template invocation + return ret + } + }, globalPropertyManager, jobManger, userManager, prometheusConfigurator, deploymentStrategy, k8sClient, networkingUtils, gitHandler) + } + + private Map parseActualYaml() { + def ys = new YamlSlurper() + return ys.parse(temporaryYamlFile) as Map + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/features/MonitoringTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/MonitoringTest.groovy index 838c6c4cb..bc1974da7 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/MonitoringTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/MonitoringTest.groovy @@ -1,8 +1,9 @@ package com.cloudogu.gitops.features -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.mockito.ArgumentCaptor +import static com.cloudogu.gitops.features.deployment.DeploymentStrategy.RepoType +import static org.assertj.core.api.Assertions.assertThat +import static org.mockito.ArgumentMatchers.any +import static org.mockito.Mockito.* import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.DeploymentStrategy @@ -17,153 +18,121 @@ import java.nio.file.Files import java.nio.file.Path import groovy.yaml.YamlSlurper -import static com.cloudogu.gitops.features.deployment.DeploymentStrategy.RepoType -import static org.assertj.core.api.Assertions.assertThat -import static org.mockito.ArgumentMatchers.any -import static org.mockito.Mockito.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor class MonitoringTest { - Config config = Config.fromMap( - registry: [ - internal : true, - createImagePullSecrets: false - ], - scm: [ - scmManager: [ - internal: true - ] - ], - jenkins: [ - internal : true, - active: true, - metricsUsername: 'metrics', - metricsPassword: 'metrics', - ], - application: [ - username : 'abc', - password : '123', - openshift : false, - namePrefix : 'foo-', - mirrorRepos : false, - podResources : false, - skipCrds : false, - namespaceIsolation: false, - gitName : 'Cloudogu', - gitEmail : 'hello@cloudogu.com', - netpols : false, - namespaces : [ - dedicatedNamespaces: [ - "test1-default", - "test1-argocd", - "test1-monitoring", - "test1-secrets" - ] as LinkedHashSet, - tenantNamespaces : [ - "test1-example-apps-staging", - "test1-example-apps-production" - ] as LinkedHashSet - ] - ], - features: [ - argocd : [ - active: true - ], - monitoring : [ - active : true, - grafanaUrl : '', - grafanaEmailFrom: 'grafana@example.org', - grafanaEmailTo : 'infra@example.org', - helm : [ - chart : 'kube-prometheus-stack', - repoURL: 'https://prom', - version: '19.2.2' - ] - ], - secrets : [ - active: true - ], - ingress: [ - active: true - ] - ]) - - K8sClientForTest k8sClient = new K8sClientForTest(config) - CommandExecutorForTest k8sCommandExecutor = k8sClient.commandExecutorForTest - DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) - AirGappedUtils airGappedUtils = mock(AirGappedUtils) - Path temporaryYamlFilePrometheus = null - FileSystemUtils fileSystemUtils = new FileSystemUtils() - File clusterResourcesRepoDir - - GitHandler gitHandler = mock(GitHandler.class) - ScmManagerMock scmManagerMock - - @BeforeEach - void setup() { - scmManagerMock = new ScmManagerMock() - } - - - @Test - void "is disabled via active flag"() { - config.features.monitoring.active = false - createStack(scmManagerMock).install() - assertThat(temporaryYamlFilePrometheus).isNull() - assertThat(k8sCommandExecutor.actualCommands).isEmpty() - verifyNoMoreInteractions(deploymentStrategy) - } - - @Test - void 'When mailServer disabled: Does not include mail configurations into cluster resources'() { - config.features.mail.active = null // user should not do this in real. - createStack(scmManagerMock).install() - - def yaml = parseActualYaml() - assertThat(yaml['grafana']['notifiers']).isNull() - } - - @Test - void 'When mailServer enabled: Includes mail configurations into cluster resources'() { - config.features.mail.active = true - createStack(scmManagerMock).install() - assertThat(parseActualYaml()['grafana']['notifiers']).isNotNull() - } - - @Test - void "When Email Addresses is set"() { - config.features.mail.active = true - config.features.monitoring.grafanaEmailFrom = 'grafana@example.com' - config.features.monitoring.grafanaEmailTo = 'infra@example.com' - createStack(scmManagerMock).install() - - def notifiersYaml = parseActualYaml()['grafana']['notifiers']['notifiers.yaml']['notifiers']['settings'] as List - assertThat(notifiersYaml[0]['addresses']).isEqualTo('infra@example.com') - assertThat(parseActualYaml()['grafana']['env']['GF_SMTP_FROM_ADDRESS']).isEqualTo('grafana@example.com') - } - - @Test - void "When Email Addresses is NOT set"() { - config.features.mail.active = true - createStack(scmManagerMock).install() - - def notifiersYaml = parseActualYaml()['grafana']['notifiers']['notifiers.yaml']['notifiers']['settings'] as List - assertThat(notifiersYaml[0]['addresses']).isEqualTo('infra@example.org') - assertThat(parseActualYaml()['grafana']['env']['GF_SMTP_FROM_ADDRESS']).isEqualTo('grafana@example.org') - } - - @Test - void 'When external Mailserver is set'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - config.features.mail.smtpPort = 1010110 - config.features.monitoring.grafanaEmailTo = 'grafana@example.com' - // needed to check that yaml is inserted correctly - - createStack(scmManagerMock).install() - def contactPointsYaml = parseActualYaml() - - assertThat(contactPointsYaml['grafana']['alerting']['contactpoints.yaml']).isEqualTo(new YamlSlurper().parseText( - """ + Config config = Config.fromMap(registry: [internal : true, + createImagePullSecrets: false], + scm: [scmManager: [internal: true]], + jenkins: [internal : true, + active : true, + metricsUsername: 'metrics', + metricsPassword: 'metrics',], + application: [username : 'abc', + password : '123', + openshift : false, + namePrefix : 'foo-', + mirrorRepos : false, + podResources : false, + skipCrds : false, + namespaceIsolation: false, + gitName : 'Cloudogu', + gitEmail : 'hello@cloudogu.com', + netpols : false, + namespaces : [dedicatedNamespaces: ["test1-default", + "test1-argocd", + "test1-monitoring", + "test1-secrets"] as LinkedHashSet, + tenantNamespaces : ["test1-example-apps-staging", + "test1-example-apps-production"] as LinkedHashSet]], + features: [argocd : [active: true], + monitoring: [active : true, + grafanaUrl : '', + grafanaEmailFrom: 'grafana@example.org', + grafanaEmailTo : 'infra@example.org', + helm : [chart : 'kube-prometheus-stack', + repoURL: 'https://prom', + version: '19.2.2']], + secrets : [active: true], + ingress : [active: true]]) + + K8sClientForTest k8sClient = new K8sClientForTest(config) + CommandExecutorForTest k8sCommandExecutor = k8sClient.commandExecutorForTest + DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) + AirGappedUtils airGappedUtils = mock(AirGappedUtils) + Path temporaryYamlFilePrometheus = null + FileSystemUtils fileSystemUtils = new FileSystemUtils() + File clusterResourcesRepoDir + + GitHandler gitHandler = mock(GitHandler.class) + ScmManagerMock scmManagerMock + + @BeforeEach + void setup() { + scmManagerMock = new ScmManagerMock() + } + + @Test + void "is disabled via active flag"() { + config.features.monitoring.active = false + createStack(scmManagerMock).install() + assertThat(temporaryYamlFilePrometheus).isNull() + assertThat(k8sCommandExecutor.actualCommands).isEmpty() + verifyNoMoreInteractions(deploymentStrategy) + } + + @Test + void 'When mailServer disabled: Does not include mail configurations into cluster resources'() { + config.features.mail.active = null // user should not do this in real. + createStack(scmManagerMock).install() + + def yaml = parseActualYaml() + assertThat(yaml['grafana']['notifiers']).isNull() + } + + @Test + void 'When mailServer enabled: Includes mail configurations into cluster resources'() { + config.features.mail.active = true + createStack(scmManagerMock).install() + assertThat(parseActualYaml()['grafana']['notifiers']).isNotNull() + } + + @Test + void "When Email Addresses is set"() { + config.features.mail.active = true + config.features.monitoring.grafanaEmailFrom = 'grafana@example.com' + config.features.monitoring.grafanaEmailTo = 'infra@example.com' + createStack(scmManagerMock).install() + + def notifiersYaml = parseActualYaml()['grafana']['notifiers']['notifiers.yaml']['notifiers']['settings'] as List + assertThat(notifiersYaml[0]['addresses']).isEqualTo('infra@example.com') + assertThat(parseActualYaml()['grafana']['env']['GF_SMTP_FROM_ADDRESS']).isEqualTo('grafana@example.com') + } + + @Test + void "When Email Addresses is NOT set"() { + config.features.mail.active = true + createStack(scmManagerMock).install() + + def notifiersYaml = parseActualYaml()['grafana']['notifiers']['notifiers.yaml']['notifiers']['settings'] as List + assertThat(notifiersYaml[0]['addresses']).isEqualTo('infra@example.org') + assertThat(parseActualYaml()['grafana']['env']['GF_SMTP_FROM_ADDRESS']).isEqualTo('grafana@example.org') + } + + @Test + void 'When external Mailserver is set'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + config.features.mail.smtpPort = 1010110 + config.features.monitoring.grafanaEmailTo = 'grafana@example.com' + // needed to check that yaml is inserted correctly + + createStack(scmManagerMock).install() + def contactPointsYaml = parseActualYaml() + + assertThat(contactPointsYaml['grafana']['alerting']['contactpoints.yaml']).isEqualTo(new YamlSlurper().parseText(""" apiVersion: 1 contactPoints: - orgId: 1 @@ -174,11 +143,8 @@ contactPoints: type: email settings: addresses: ${config.features.monitoring.grafanaEmailTo} -""" - ) - ) - assertThat(contactPointsYaml['grafana']['alerting']['notification-policies.yaml']).isEqualTo(new YamlSlurper().parseText( - ''' +""")) + assertThat(contactPointsYaml['grafana']['alerting']['notification-policies.yaml']).isEqualTo(new YamlSlurper().parseText(''' apiVersion: 1 policies: - orgId: 1 @@ -187,474 +153,454 @@ policies: routes: - receiver: email group_by: ["grafana_folder", "alertname"] -''' - )) +''')) + + assertThat(contactPointsYaml['grafana']['env']['GF_SMTP_HOST']).isEqualTo('smtp.example.com:1010110') + } + + @Test + void 'When external Mailserver is set with user'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + config.features.mail.smtpUser = 'mailserver@example.com' + + createStack(scmManagerMock).install() + + assertThat(parseActualYaml()['grafana']['smtp']['existingSecret']).isEqualTo('grafana-email-secret') + k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user=mailserver@example.com --from-literal password=') + } + + @Test + void 'When external Mailserver is set with password'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + config.features.mail.smtpPassword = '1101ABCabc&/+*~' + + createStack(scmManagerMock).install() + assertThat(parseActualYaml()['grafana']['smtp']['existingSecret']).isEqualTo('grafana-email-secret') + k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user= --from-literal password=1101ABCabc&/+*~') + } + + @Test + void 'When external Mailserver is set without user and password'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + + createStack(scmManagerMock).install() + + assertThat(parseActualYaml()['grafana']['valuesFrom']).isNull() + assertThat(parseActualYaml()['grafana']['smtp']).isNull() + k8sCommandExecutor.assertNotExecuted('kubectl create secret generic grafana-email-secret') + } + + @Test + void 'Check if kubernetes secret will be created when external emailservers credential is set'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + config.features.mail.smtpUser = 'grafana@example.com' + config.features.mail.smtpPassword = '1101ABCabc&/+*~' + + createStack(scmManagerMock).install() + + k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user=grafana@example.com --from-literal password=1101ABCabc&/+*~') + } - assertThat(contactPointsYaml['grafana']['env']['GF_SMTP_HOST']).isEqualTo('smtp.example.com:1010110') - } - - @Test - void 'When external Mailserver is set with user'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - config.features.mail.smtpUser = 'mailserver@example.com' + @Test + void 'When external Mailserver is set without port'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + + createStack(scmManagerMock).install() + def contactPointsYaml = parseActualYaml() - createStack(scmManagerMock).install() + assertThat(contactPointsYaml['grafana']['env']['GF_SMTP_HOST']).isEqualTo('smtp.example.com') + } - assertThat(parseActualYaml()['grafana']['smtp']['existingSecret']).isEqualTo('grafana-email-secret') - k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user=mailserver@example.com --from-literal password=') - } + @Test + void 'When external Mailserver is NOT set'() { + config.features.mail.active = null // user should not do this in real. + createStack(scmManagerMock).install() + def contactPointsYaml = parseActualYaml() - @Test - void 'When external Mailserver is set with password'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - config.features.mail.smtpPassword = '1101ABCabc&/+*~' - - createStack(scmManagerMock).install() - assertThat(parseActualYaml()['grafana']['smtp']['existingSecret']).isEqualTo('grafana-email-secret') - k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user= --from-literal password=1101ABCabc&/+*~') - } - - @Test - void 'When external Mailserver is set without user and password'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - - createStack(scmManagerMock).install() - - assertThat(parseActualYaml()['grafana']['valuesFrom']).isNull() - assertThat(parseActualYaml()['grafana']['smtp']).isNull() - k8sCommandExecutor.assertNotExecuted('kubectl create secret generic grafana-email-secret') - } - - @Test - void 'Check if kubernetes secret will be created when external emailservers credential is set'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - config.features.mail.smtpUser = 'grafana@example.com' - config.features.mail.smtpPassword = '1101ABCabc&/+*~' - - createStack(scmManagerMock).install() - - k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user=grafana@example.com --from-literal password=1101ABCabc&/+*~') - } - - @Test - void 'When external Mailserver is set without port'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - - createStack(scmManagerMock).install() - def contactPointsYaml = parseActualYaml() - - assertThat(contactPointsYaml['grafana']['env']['GF_SMTP_HOST']).isEqualTo('smtp.example.com') - } - - @Test - void 'When external Mailserver is NOT set'() { - config.features.mail.active = null // user should not do this in real. - createStack(scmManagerMock).install() - def contactPointsYaml = parseActualYaml() - - assertThat(contactPointsYaml['grafana']['alerting']).isNull() - } - - @Test - void "configures admin user if requested"() { - config.application.username = "my-user" - config.application.password = "hunter2" - createStack(scmManagerMock).install() - - assertThat(parseActualYaml()['grafana']['adminUser']).isEqualTo('my-user') - assertThat(parseActualYaml()['grafana']['adminPassword']).isEqualTo('hunter2') - } - - @Test - void 'uses ingress if enabled'() { - config.features.monitoring.grafanaUrl = 'http://grafana.local' - - createStack(scmManagerMock).install() - - def serviceYaml = parseActualYaml()['grafana']['ingress'] - assertThat(serviceYaml['enabled']).isEqualTo(true) - assertThat((serviceYaml['hosts'] as List)[0]).isEqualTo('grafana.local') - } - - @Test - void 'does not use ingress by default'() { - createStack(scmManagerMock).install() - - assertThat(parseActualYaml()['grafana'] as Map).doesNotContainKey('ingress') - } - - @Test - void 'cleanupUnusedDashboards removes all dashboards for disabled features'() { - config.features.monitoring.active = true - config.features.ingress.active = false - config.jenkins.active = false - config.scm.scmManager.url = null // triggers scmm dashboard cleanup - - createStack(scmManagerMock).install() - - File dashboardDir = new File(clusterResourcesRepoDir, "apps/prometheusstack/misc/dashboard") - - assertThat(new File(dashboardDir, "traefik-dashboard.yaml")).doesNotExist() - assertThat(new File(dashboardDir, "traefik-dashboard-requests-handling.yaml")).doesNotExist() - assertThat(new File(dashboardDir, "jenkins-dashboard.yaml")).doesNotExist() - assertThat(new File(dashboardDir, "scmm-dashboard.yaml")).doesNotExist() - } - - @Test - void 'Applies Prometheus ServiceMonitor CRD from file before installing (air-gapped mode)'() { - // Arrange - config.features.monitoring.active = true - config.application.mirrorRepos = true - config.application.skipCrds = false - - Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) - config.application.localHelmChartFolder = rootChartsFolder.toString() - - Path crdFile = rootChartsFolder.resolve( - "${config.features.monitoring.helm.chart}/charts/crds/crds/crd-servicemonitors.yaml" - ) - Files.createDirectories(crdFile.parent) - Files.writeString(crdFile, "dummy") // content can be anything for this test - - Path chartYaml = rootChartsFolder.resolve("${config.features.monitoring.helm.chart}/Chart.yaml") - Files.createDirectories(chartYaml.parent) - Files.writeString(chartYaml, "apiVersion: v2\nname: kube-prometheus-stack\nversion: 42.0.3\n") - - createStack(scmManagerMock).install() - k8sCommandExecutor.assertExecuted("kubectl apply -f ${crdFile}") - - } - - @Test - void 'Applies Prometheus ServiceMonitor CRD from GitHub before installing'() { - config.features.monitoring.active = true - config.application.mirrorRepos = false // optional, but makes intent explicit - config.application.skipCrds = false // optional, but makes intent explicit - - createStack(scmManagerMock).install() - - k8sCommandExecutor.assertExecuted( - "kubectl apply -f https://raw.githubusercontent.com/prometheus-community/helm-charts/" + - "kube-prometheus-stack-${config.features.monitoring.helm.version}/" + - "charts/kube-prometheus-stack/charts/crds/crds/crd-servicemonitors.yaml" - ) - } - - @Test - void 'does not apply ServiceMonitor CRD when monitoring is disabled'() { - config.features.monitoring.active = false // important - config.application.skipCrds = false // so it would apply if enabled - config.application.mirrorRepos = false // avoid local chart access - - createStack(scmManagerMock).install() - - // no CRD apply should happen at all - k8sCommandExecutor.assertNotExecuted('kubectl apply -f https://raw.githubusercontent.com/prometheus-community/helm-charts/') - } - - @Test - void 'uses remote scmm url if requested'() { - createStack(scmManagerMock).install() - - def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List - assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:8080') - assertThat(additionalScrapeConfigs[0]['metrics_path']).isEqualTo('/scm/api/v2/metrics/prometheus') - assertThat(additionalScrapeConfigs[0]['scheme']).isEqualTo('http') - - // scrape config for jenkins is unchanged - assertThat(((additionalScrapeConfigs[1]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('jenkins.foo-jenkins.svc.cluster.local') - assertThat(additionalScrapeConfigs[1]['scheme']).isEqualTo('http') - assertThat(additionalScrapeConfigs[1]['metrics_path']).isEqualTo('/prometheus') - } - - @Test - void 'uses remote jenkins url if requested'() { - config.jenkins["internal"] = false - config.jenkins["url"] = 'https://localhost:9090/jenkins' - createStack(scmManagerMock).install() - def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List - - // scrape config for scmm is unchanged - assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:8080') - assertThat(additionalScrapeConfigs[0]['scheme']).isEqualTo('http') - assertThat(additionalScrapeConfigs[0]['metrics_path']).isEqualTo('/scm/api/v2/metrics/prometheus') - - - assertThat(((additionalScrapeConfigs[1]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:9090') - assertThat(additionalScrapeConfigs[1]['metrics_path']).isEqualTo('/jenkins/prometheus') - assertThat(additionalScrapeConfigs[1]['scheme']).isEqualTo('https') - } - - @Test - void 'configures custom metrics user for jenkins'() { - config.jenkins["metricsUsername"] = 'external-metrics-username' - config.jenkins["metricsPassword"] = 'hunter2' - createStack(scmManagerMock).install() - - assertThat(k8sCommandExecutor.actualCommands[1]).isEqualTo("kubectl create secret generic prometheus-metrics-creds-jenkins -n foo-monitoring --from-literal password=hunter2 --dry-run=client -oyaml | kubectl apply -f-") - def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List - assertThat(additionalScrapeConfigs[1]['basic_auth']['username']).isEqualTo('external-metrics-username') - } - - @Test - void "configures custom image for grafana"() { - config.features.monitoring.helm.grafanaImage = "localhost:5000/grafana/grafana:the-tag" - createStack(scmManagerMock).install() - - assertThat(parseActualYaml()['grafana']['image']['registry']).isEqualTo('localhost:5000') - assertThat(parseActualYaml()['grafana']['image']['repository']).isEqualTo('grafana/grafana') - assertThat(parseActualYaml()['grafana']['image']['tag']).isEqualTo('the-tag') - } - - @Test - void "configures custom image for grafana-sidecar"() { - config.features.monitoring.helm.grafanaSidecarImage = "localhost:5000/grafana/sidecar:the-tag" - createStack(scmManagerMock).install() - - assertThat(parseActualYaml()['grafana']['sidecar']['image']['registry']).isEqualTo('localhost:5000') - assertThat(parseActualYaml()['grafana']['sidecar']['image']['repository']).isEqualTo('grafana/sidecar') - assertThat(parseActualYaml()['grafana']['sidecar']['image']['tag']).isEqualTo('the-tag') - } - - @Test - void "configures custom image for prometheus and operator"() { - config.features.monitoring.helm.prometheusImage = "localhost:5000/prometheus/prometheus:v1" - config.features.monitoring.helm.prometheusOperatorImage = "localhost:5000/prometheus-operator/prometheus-operator:v2" - config.features.monitoring.helm.prometheusConfigReloaderImage = "localhost:5000/prometheus-operator/prometheus-config-reloader:v3" - - createStack(scmManagerMock).install() - - def actualYaml = parseActualYaml() - assertThat(actualYaml['prometheus']['prometheusSpec']['image']['registry']).isEqualTo('localhost:5000') - assertThat(actualYaml['prometheus']['prometheusSpec']['image']['repository']).isEqualTo('prometheus/prometheus') - assertThat(actualYaml['prometheus']['prometheusSpec']['image']['tag']).isEqualTo('v1') - assertThat(actualYaml['prometheusOperator']['image']['registry']).isEqualTo('localhost:5000') - assertThat(actualYaml['prometheusOperator']['image']['repository']).isEqualTo('prometheus-operator/prometheus-operator') - assertThat(actualYaml['prometheusOperator']['image']['tag']).isEqualTo('v2') - assertThat(actualYaml['prometheusOperator']['prometheusConfigReloader']['image']['registry']).isEqualTo('localhost:5000') - assertThat(actualYaml['prometheusOperator']['prometheusConfigReloader']['image']['repository']).isEqualTo('prometheus-operator/prometheus-config-reloader') - assertThat(actualYaml['prometheusOperator']['prometheusConfigReloader']['image']['tag']).isEqualTo('v3') - } - - @Test - void 'deploys image pull secrets for proxy registry'() { - config.registry.createImagePullSecrets = true - config.registry.proxyUrl = 'proxy-url' - config.registry.proxyUsername = 'proxy-user' - config.registry.proxyPassword = 'proxy-pw' - - createStack(scmManagerMock).install() - - k8sClient.commandExecutorForTest.assertExecuted( - 'kubectl create secret docker-registry proxy-registry -n foo-monitoring' + - ' --docker-server proxy-url --docker-username proxy-user --docker-password proxy-pw') - assertThat(parseActualYaml()['global']['imagePullSecrets']).isEqualTo([[name: 'proxy-registry']]) - } - - @Test - void 'helm release is installed'() { - createStack(scmManagerMock).install() - - assertThat(k8sCommandExecutor.actualCommands[0].trim()).isEqualTo( - 'kubectl create secret generic prometheus-metrics-creds-scmm -n foo-monitoring --from-literal password=123 --dry-run=client -oyaml | kubectl apply -f-') - - verify(deploymentStrategy).deployFeature('https://prom', 'monitoring', - 'kube-prometheus-stack', '19.2.2', 'foo-monitoring', - 'kube-prometheus-stack', temporaryYamlFilePrometheus, RepoType.HELM) - /* This corresponds to - 'helm repo add prometheusstack https://prom' - 'helm upgrade -i kube-prometheus-stack prometheusstack/kube-prometheus-stack --version 19.2.2' + - " --values ${temporaryYamlFile} --namespace foo-monitoring --create-namespace") */ - - def yaml = parseActualYaml() - assertThat(yaml['grafana']['adminUser']).isEqualTo('abc') - assertThat(yaml['grafana']['adminPassword']).isEqualTo(123) - - assertThat(yaml['prometheusOperator'] as Map).doesNotContainKey('resources') - assertThat(yaml['grafana'] as Map).doesNotContainKey('resources') - assertThat(yaml['grafana']['sidecar'] as Map).doesNotContainKey('resources') - assertThat(yaml['prometheus']['prometheusSpec'] as Map).doesNotContainKey('resources') - - assertThat(yaml['prometheusOperator']['securityContext']).isNull() - assertThat(yaml['grafana']['securityContext']).isNull() - assertThat(yaml['prometheus']['prometheusSpec']['securityContext']).isNull() - - assertThat(yaml['kubeApiServer']).isNull() - - assertThat(yaml['prometheusOperator']['admissionWebhooks']['enabled']).isEqualTo(false) - assertThat(yaml['prometheusOperator']['tls']['enabled']).isEqualTo(false) - assertThat(yaml['prometheusOperator']['kubeletService']).isNull() - assertThat(yaml['prometheusOperator']['namespaces']).isNull() - assertThat(yaml).doesNotContainKey('global') - - assertThat(yaml['grafana']['rbac']).isNull() - assertThat(yaml['grafana']['sidecar']['dashboards']['searchNamespace']).isEqualTo('ALL') - - assertThat(yaml['crds']).isNull() - assertThat(new File("$clusterResourcesRepoDir/misc/monitoring/rbac")).doesNotExist() - } - - @Test - void 'Skips CRDs'() { - config.application.skipCrds = true - - createStack(scmManagerMock).install() - - assertThat(parseActualYaml()['crds']['enabled']).isEqualTo(false) - } - - @Test - void 'Sets pod resource limits and requests'() { - config.application.podResources = true - - createStack(scmManagerMock).install() - - def yaml = parseActualYaml() - assertThat(yaml['prometheusOperator']['resources'] as Map).containsKeys('limits', 'requests') - assertThat(yaml['prometheusOperator']['prometheusConfigReloader']['resources'] as Map).containsKeys('limits', 'requests') - assertThat(yaml['grafana']['resources'] as Map) containsKeys('limits', 'requests') - assertThat(yaml['grafana']['sidecar']['resources'] as Map) containsKeys('limits', 'requests') - assertThat(yaml['prometheus']['prometheusSpec']['resources'] as Map) containsKeys('limits', 'requests') - } - - @Test - void 'works with openshift'() { - config.application.openshift = true - // Prepare UID - String realoutput = '{"app.kubernetes.io/created-by":"Internal OpenShift","openshift.io/description":"","openshift.io/display-name":"","openshift.io/requester":"myUser@mydomain.de","openshift.io/sa.scc.mcs":"s0:c30,c25","openshift.io/sa.scc.supplemental-groups":"1000920000/10000","openshift.io/sa.scc.uid-range":"1000920000/10000","project-type":"customer"}' - k8sCommandExecutor.enqueueOutput(new CommandExecutor.Output('', realoutput, 0)) - - createStack(scmManagerMock).install() - - def yaml = parseActualYaml() - assertThat(yaml['prometheusOperator']['securityContext']).isNotNull() - assertThat(yaml['prometheusOperator']['securityContext']['fsGroup']).isNull() - assertThat(yaml['prometheusOperator']['securityContext']['runAsGroup']).isNull() - assertThat(yaml['prometheusOperator']['securityContext']['runAsUser']).isNull() - - assertThat(yaml['grafana']['securityContext']).isNotNull() - assertThat(yaml['grafana']['securityContext']['fsGroup']).isEqualTo(1000920000) - assertThat(yaml['grafana']['securityContext']['runAsGroup']).isEqualTo(1000920000) - assertThat(yaml['grafana']['securityContext']['runAsUser']).isEqualTo(1000920000) - - assertThat(yaml['prometheus']['prometheusSpec']['securityContext']).isNotNull() - assertThat(yaml['prometheus']['prometheusSpec']['securityContext']['fsGroup']).isNull() - assertThat(yaml['prometheus']['prometheusSpec']['securityContext']['runAsGroup']).isNull() - assertThat(yaml['prometheus']['prometheusSpec']['securityContext']['runAsUser']).isNull() - } - - @Test - void 'works with namespaceIsolation'() { - config.application.namespaceIsolation = true - - def prometheusStack = createStack(scmManagerMock) - prometheusStack.install() - - def yaml = parseActualYaml() - assertThat(yaml['global']['rbac']['create']).isEqualTo(false) - - for (String namespace : config.application.namespaces.getActiveNamespaces()) { - def rbacYaml = new File("$clusterResourcesRepoDir/apps/monitoring/misc/rbac/${namespace}.yaml") - assertThat(rbacYaml.text).contains("namespace: ${namespace}") - assertThat(rbacYaml.text).contains(" namespace: foo-monitoring") - } - - assertThat(yaml['kubeApiServer']['enabled']).isEqualTo(false) - - assertThat(yaml['prometheusOperator']['kubeletService']['enabled']).isEqualTo(false) - assertThat(yaml['prometheusOperator']['namespaces']['releaseNamespace']).isEqualTo(false) - assertThat(yaml['prometheusOperator']['namespaces']['additional'] as List).hasSameElementsAs(config.application.namespaces.getActiveNamespaces()) - - assertThat(yaml['grafana']['rbac']['create']).isEqualTo(false) - assertThat(yaml['grafana']['sidecar']['dashboards']['searchNamespace']).isEqualTo(config.application.namespaces.getActiveNamespaces().join(',')) - } - - @Test - void 'network policies are created for prometheus'() { - config.application.netpols = true - //config.application.namespaces.dedicatedNamespaces = ["testnamespace1", "testnamespace2"] - def prometheusStack = createStack(scmManagerMock) - prometheusStack.install() - - for (String namespace : config.application.namespaces.getActiveNamespaces()) { - def netPolsYaml = new File("$clusterResourcesRepoDir/apps/monitoring/misc/netpols/${namespace}.yaml") - assertThat(netPolsYaml.text).contains("namespace: ${namespace}") - } - } - - @Test - void 'helm releases are installed in air-gapped mode'() { - config.application.mirrorRepos = true - when(airGappedUtils.mirrorHelmRepoToGit(any(Config.HelmConfig))).thenReturn('a/b') - - Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) - config.application.localHelmChartFolder = rootChartsFolder.toString() - - Path prometheusSourceChart = rootChartsFolder.resolve('kube-prometheus-stack') - Files.createDirectories(prometheusSourceChart) - - Map prometheusChartYaml = [version: '1.2.3'] - fileSystemUtils.writeYaml(prometheusChartYaml, prometheusSourceChart.resolve('Chart.yaml').toFile()) - - scmManagerMock.inClusterBase = new URI("http://scmm.foo-scm-manager.svc.cluster.local/scm") - createStack(scmManagerMock).install() - - def helmConfig = ArgumentCaptor.forClass(Config.HelmConfig) - verify(airGappedUtils).mirrorHelmRepoToGit(helmConfig.capture()) - assertThat(helmConfig.value.chart).isEqualTo('kube-prometheus-stack') - assertThat(helmConfig.value.repoURL).isEqualTo('https://prom') - assertThat(helmConfig.value.version).isEqualTo('19.2.2') - verify(deploymentStrategy).deployFeature( - 'http://scmm.foo-scm-manager.svc.cluster.local/scm/repo/a/b', - 'monitoring', '.', '1.2.3', 'foo-monitoring', - 'kube-prometheus-stack', temporaryYamlFilePrometheus, RepoType.GIT) - } - - @Test - void 'Merges additional helm values merged with default values'() { - config.features.monitoring.helm.values = [ - key : [ - some: 'thing', - one : 1 - ], - prometheus: [ - prometheusSpec: [ - scrapeConfigSelectorNilUsesHelmValues: null - ] - ] - ] - - createStack(scmManagerMock).install() - def actual = parseActualYaml() - - assertThat(actual['key']['some']).isEqualTo('thing') - assertThat(actual['key']['one']).isEqualTo(1) - assertThat(actual['prometheus']['prometheusSpec']['scrapeConfigSelectorNilUsesHelmValues']).isEqualTo(null) - } - - @Test - void 'ServiceMonitor selectors'() { - config.application.namePrefix = "test1-" - config.features.argocd.active = true - config.features.secrets.active = true - config.features.ingress.active = false - LinkedHashSet namespaceList = [ - "test1-argocd", - "test1-monitoring", - "test1-example-apps-staging", - "test1-example-apps-production", - "test1-secrets" - ] - config.application.namespaces.dedicatedNamespaces = namespaceList - createStack(scmManagerMock).install() - def actual = parseActualYaml() - - assertThat(actual['prometheus']['prometheusSpec']['serviceMonitorNamespaceSelector']).isEqualTo(new YamlSlurper().parseText(''' + assertThat(contactPointsYaml['grafana']['alerting']).isNull() + } + + @Test + void "configures admin user if requested"() { + config.application.username = "my-user" + config.application.password = "hunter2" + createStack(scmManagerMock).install() + + assertThat(parseActualYaml()['grafana']['adminUser']).isEqualTo('my-user') + assertThat(parseActualYaml()['grafana']['adminPassword']).isEqualTo('hunter2') + } + + @Test + void 'uses ingress if enabled'() { + config.features.monitoring.grafanaUrl = 'http://grafana.local' + + createStack(scmManagerMock).install() + + def serviceYaml = parseActualYaml()['grafana']['ingress'] + assertThat(serviceYaml['enabled']).isEqualTo(true) + assertThat((serviceYaml['hosts'] as List)[0]).isEqualTo('grafana.local') + } + + @Test + void 'does not use ingress by default'() { + createStack(scmManagerMock).install() + + assertThat(parseActualYaml()['grafana'] as Map).doesNotContainKey('ingress') + } + + @Test + void 'cleanupUnusedDashboards removes all dashboards for disabled features'() { + config.features.monitoring.active = true + config.features.ingress.active = false + config.jenkins.active = false + config.scm.scmManager.url = null // triggers scmm dashboard cleanup + + createStack(scmManagerMock).install() + + File dashboardDir = new File(clusterResourcesRepoDir, "apps/prometheusstack/misc/dashboard") + + assertThat(new File(dashboardDir, "traefik-dashboard.yaml")).doesNotExist() + assertThat(new File(dashboardDir, "traefik-dashboard-requests-handling.yaml")).doesNotExist() + assertThat(new File(dashboardDir, "jenkins-dashboard.yaml")).doesNotExist() + assertThat(new File(dashboardDir, "scmm-dashboard.yaml")).doesNotExist() + } + + @Test + void 'Applies Prometheus ServiceMonitor CRD from file before installing (air-gapped mode)'() { + // Arrange + config.features.monitoring.active = true + config.application.mirrorRepos = true + config.application.skipCrds = false + + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) + config.application.localHelmChartFolder = rootChartsFolder.toString() + + Path crdFile = rootChartsFolder.resolve("${config.features.monitoring.helm.chart}/charts/crds/crds/crd-servicemonitors.yaml") + Files.createDirectories(crdFile.parent) + Files.writeString(crdFile, "dummy") // content can be anything for this test + + Path chartYaml = rootChartsFolder.resolve("${config.features.monitoring.helm.chart}/Chart.yaml") + Files.createDirectories(chartYaml.parent) + Files.writeString(chartYaml, "apiVersion: v2\nname: kube-prometheus-stack\nversion: 42.0.3\n") + + createStack(scmManagerMock).install() + k8sCommandExecutor.assertExecuted("kubectl apply -f ${crdFile}") + + } + + @Test + void 'Applies Prometheus ServiceMonitor CRD from GitHub before installing'() { + config.features.monitoring.active = true + config.application.mirrorRepos = false // optional, but makes intent explicit + config.application.skipCrds = false // optional, but makes intent explicit + + createStack(scmManagerMock).install() + + k8sCommandExecutor.assertExecuted("kubectl apply -f https://raw.githubusercontent.com/prometheus-community/helm-charts/" + "kube-prometheus-stack-${config.features.monitoring.helm.version}/" + + "charts/kube-prometheus-stack/charts/crds/crds/crd-servicemonitors.yaml") + } + + @Test + void 'does not apply ServiceMonitor CRD when monitoring is disabled'() { + config.features.monitoring.active = false // important + config.application.skipCrds = false // so it would apply if enabled + config.application.mirrorRepos = false // avoid local chart access + + createStack(scmManagerMock).install() + + // no CRD apply should happen at all + k8sCommandExecutor.assertNotExecuted('kubectl apply -f https://raw.githubusercontent.com/prometheus-community/helm-charts/') + } + + @Test + void 'uses remote scmm url if requested'() { + createStack(scmManagerMock).install() + + def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List + assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:8080') + assertThat(additionalScrapeConfigs[0]['metrics_path']).isEqualTo('/scm/api/v2/metrics/prometheus') + assertThat(additionalScrapeConfigs[0]['scheme']).isEqualTo('http') + + // scrape config for jenkins is unchanged + assertThat(((additionalScrapeConfigs[1]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('jenkins.foo-jenkins.svc.cluster.local') + assertThat(additionalScrapeConfigs[1]['scheme']).isEqualTo('http') + assertThat(additionalScrapeConfigs[1]['metrics_path']).isEqualTo('/prometheus') + } + + @Test + void 'uses remote jenkins url if requested'() { + config.jenkins["internal"] = false + config.jenkins["url"] = 'https://localhost:9090/jenkins' + createStack(scmManagerMock).install() + def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List + + // scrape config for scmm is unchanged + assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:8080') + assertThat(additionalScrapeConfigs[0]['scheme']).isEqualTo('http') + assertThat(additionalScrapeConfigs[0]['metrics_path']).isEqualTo('/scm/api/v2/metrics/prometheus') + + assertThat(((additionalScrapeConfigs[1]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:9090') + assertThat(additionalScrapeConfigs[1]['metrics_path']).isEqualTo('/jenkins/prometheus') + assertThat(additionalScrapeConfigs[1]['scheme']).isEqualTo('https') + } + + @Test + void 'configures custom metrics user for jenkins'() { + config.jenkins["metricsUsername"] = 'external-metrics-username' + config.jenkins["metricsPassword"] = 'hunter2' + createStack(scmManagerMock).install() + + assertThat(k8sCommandExecutor.actualCommands[1]).isEqualTo("kubectl create secret generic prometheus-metrics-creds-jenkins -n foo-monitoring --from-literal password=hunter2 --dry-run=client -oyaml | kubectl apply -f-") + def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List + assertThat(additionalScrapeConfigs[1]['basic_auth']['username']).isEqualTo('external-metrics-username') + } + + @Test + void "configures custom image for grafana"() { + config.features.monitoring.helm.grafanaImage = "localhost:5000/grafana/grafana:the-tag" + createStack(scmManagerMock).install() + + assertThat(parseActualYaml()['grafana']['image']['registry']).isEqualTo('localhost:5000') + assertThat(parseActualYaml()['grafana']['image']['repository']).isEqualTo('grafana/grafana') + assertThat(parseActualYaml()['grafana']['image']['tag']).isEqualTo('the-tag') + } + + @Test + void "configures custom image for grafana-sidecar"() { + config.features.monitoring.helm.grafanaSidecarImage = "localhost:5000/grafana/sidecar:the-tag" + createStack(scmManagerMock).install() + + assertThat(parseActualYaml()['grafana']['sidecar']['image']['registry']).isEqualTo('localhost:5000') + assertThat(parseActualYaml()['grafana']['sidecar']['image']['repository']).isEqualTo('grafana/sidecar') + assertThat(parseActualYaml()['grafana']['sidecar']['image']['tag']).isEqualTo('the-tag') + } + + @Test + void "configures custom image for prometheus and operator"() { + config.features.monitoring.helm.prometheusImage = "localhost:5000/prometheus/prometheus:v1" + config.features.monitoring.helm.prometheusOperatorImage = "localhost:5000/prometheus-operator/prometheus-operator:v2" + config.features.monitoring.helm.prometheusConfigReloaderImage = "localhost:5000/prometheus-operator/prometheus-config-reloader:v3" + + createStack(scmManagerMock).install() + + def actualYaml = parseActualYaml() + assertThat(actualYaml['prometheus']['prometheusSpec']['image']['registry']).isEqualTo('localhost:5000') + assertThat(actualYaml['prometheus']['prometheusSpec']['image']['repository']).isEqualTo('prometheus/prometheus') + assertThat(actualYaml['prometheus']['prometheusSpec']['image']['tag']).isEqualTo('v1') + assertThat(actualYaml['prometheusOperator']['image']['registry']).isEqualTo('localhost:5000') + assertThat(actualYaml['prometheusOperator']['image']['repository']).isEqualTo('prometheus-operator/prometheus-operator') + assertThat(actualYaml['prometheusOperator']['image']['tag']).isEqualTo('v2') + assertThat(actualYaml['prometheusOperator']['prometheusConfigReloader']['image']['registry']).isEqualTo('localhost:5000') + assertThat(actualYaml['prometheusOperator']['prometheusConfigReloader']['image']['repository']).isEqualTo('prometheus-operator/prometheus-config-reloader') + assertThat(actualYaml['prometheusOperator']['prometheusConfigReloader']['image']['tag']).isEqualTo('v3') + } + + @Test + void 'deploys image pull secrets for proxy registry'() { + config.registry.createImagePullSecrets = true + config.registry.proxyUrl = 'proxy-url' + config.registry.proxyUsername = 'proxy-user' + config.registry.proxyPassword = 'proxy-pw' + + createStack(scmManagerMock).install() + + k8sClient.commandExecutorForTest.assertExecuted('kubectl create secret docker-registry proxy-registry -n foo-monitoring' + + ' --docker-server proxy-url --docker-username proxy-user --docker-password proxy-pw') + assertThat(parseActualYaml()['global']['imagePullSecrets']).isEqualTo([[name: 'proxy-registry']]) + } + + @Test + void 'helm release is installed'() { + createStack(scmManagerMock).install() + + assertThat(k8sCommandExecutor.actualCommands[0].trim()).isEqualTo('kubectl create secret generic prometheus-metrics-creds-scmm -n foo-monitoring --from-literal password=123 --dry-run=client -oyaml | kubectl apply -f-') + + verify(deploymentStrategy).deployFeature('https://prom', 'monitoring', + 'kube-prometheus-stack', '19.2.2', 'foo-monitoring', + 'kube-prometheus-stack', temporaryYamlFilePrometheus, RepoType.HELM) + /* This corresponds to + 'helm repo add prometheusstack https://prom' + 'helm upgrade -i kube-prometheus-stack prometheusstack/kube-prometheus-stack --version 19.2.2' + + " --values ${temporaryYamlFile} --namespace foo-monitoring --create-namespace") */ + + def yaml = parseActualYaml() + assertThat(yaml['grafana']['adminUser']).isEqualTo('abc') + assertThat(yaml['grafana']['adminPassword']).isEqualTo(123) + + assertThat(yaml['prometheusOperator'] as Map).doesNotContainKey('resources') + assertThat(yaml['grafana'] as Map).doesNotContainKey('resources') + assertThat(yaml['grafana']['sidecar'] as Map).doesNotContainKey('resources') + assertThat(yaml['prometheus']['prometheusSpec'] as Map).doesNotContainKey('resources') + + assertThat(yaml['prometheusOperator']['securityContext']).isNull() + assertThat(yaml['grafana']['securityContext']).isNull() + assertThat(yaml['prometheus']['prometheusSpec']['securityContext']).isNull() + + assertThat(yaml['kubeApiServer']).isNull() + + assertThat(yaml['prometheusOperator']['admissionWebhooks']['enabled']).isEqualTo(false) + assertThat(yaml['prometheusOperator']['tls']['enabled']).isEqualTo(false) + assertThat(yaml['prometheusOperator']['kubeletService']).isNull() + assertThat(yaml['prometheusOperator']['namespaces']).isNull() + assertThat(yaml).doesNotContainKey('global') + + assertThat(yaml['grafana']['rbac']).isNull() + assertThat(yaml['grafana']['sidecar']['dashboards']['searchNamespace']).isEqualTo('ALL') + + assertThat(yaml['crds']).isNull() + assertThat(new File("$clusterResourcesRepoDir/misc/monitoring/rbac")).doesNotExist() + } + + @Test + void 'Skips CRDs'() { + config.application.skipCrds = true + + createStack(scmManagerMock).install() + + assertThat(parseActualYaml()['crds']['enabled']).isEqualTo(false) + } + + @Test + void 'Sets pod resource limits and requests'() { + config.application.podResources = true + + createStack(scmManagerMock).install() + + def yaml = parseActualYaml() + assertThat(yaml['prometheusOperator']['resources'] as Map).containsKeys('limits', 'requests') + assertThat(yaml['prometheusOperator']['prometheusConfigReloader']['resources'] as Map).containsKeys('limits', 'requests') + assertThat(yaml['grafana']['resources'] as Map) containsKeys('limits', 'requests') + assertThat(yaml['grafana']['sidecar']['resources'] as Map) containsKeys('limits', 'requests') + assertThat(yaml['prometheus']['prometheusSpec']['resources'] as Map) containsKeys('limits', 'requests') + } + + @Test + void 'works with openshift'() { + config.application.openshift = true + // Prepare UID + String realoutput = '{"app.kubernetes.io/created-by":"Internal OpenShift","openshift.io/description":"","openshift.io/display-name":"","openshift.io/requester":"myUser@mydomain.de","openshift.io/sa.scc.mcs":"s0:c30,c25","openshift.io/sa.scc.supplemental-groups":"1000920000/10000","openshift.io/sa.scc.uid-range":"1000920000/10000","project-type":"customer"}' + k8sCommandExecutor.enqueueOutput(new CommandExecutor.Output('', realoutput, 0)) + + createStack(scmManagerMock).install() + + def yaml = parseActualYaml() + assertThat(yaml['prometheusOperator']['securityContext']).isNotNull() + assertThat(yaml['prometheusOperator']['securityContext']['fsGroup']).isNull() + assertThat(yaml['prometheusOperator']['securityContext']['runAsGroup']).isNull() + assertThat(yaml['prometheusOperator']['securityContext']['runAsUser']).isNull() + + assertThat(yaml['grafana']['securityContext']).isNotNull() + assertThat(yaml['grafana']['securityContext']['fsGroup']).isEqualTo(1000920000) + assertThat(yaml['grafana']['securityContext']['runAsGroup']).isEqualTo(1000920000) + assertThat(yaml['grafana']['securityContext']['runAsUser']).isEqualTo(1000920000) + + assertThat(yaml['prometheus']['prometheusSpec']['securityContext']).isNotNull() + assertThat(yaml['prometheus']['prometheusSpec']['securityContext']['fsGroup']).isNull() + assertThat(yaml['prometheus']['prometheusSpec']['securityContext']['runAsGroup']).isNull() + assertThat(yaml['prometheus']['prometheusSpec']['securityContext']['runAsUser']).isNull() + } + + @Test + void 'works with namespaceIsolation'() { + config.application.namespaceIsolation = true + + def prometheusStack = createStack(scmManagerMock) + prometheusStack.install() + + def yaml = parseActualYaml() + assertThat(yaml['global']['rbac']['create']).isEqualTo(false) + + for (String namespace : config.application.namespaces.getActiveNamespaces()) { + def rbacYaml = new File("$clusterResourcesRepoDir/apps/monitoring/misc/rbac/${namespace}.yaml") + assertThat(rbacYaml.text).contains("namespace: ${namespace}") + assertThat(rbacYaml.text).contains(" namespace: foo-monitoring") + } + + assertThat(yaml['kubeApiServer']['enabled']).isEqualTo(false) + + assertThat(yaml['prometheusOperator']['kubeletService']['enabled']).isEqualTo(false) + assertThat(yaml['prometheusOperator']['namespaces']['releaseNamespace']).isEqualTo(false) + assertThat(yaml['prometheusOperator']['namespaces']['additional'] as List).hasSameElementsAs(config.application.namespaces.getActiveNamespaces()) + + assertThat(yaml['grafana']['rbac']['create']).isEqualTo(false) + assertThat(yaml['grafana']['sidecar']['dashboards']['searchNamespace']).isEqualTo(config.application.namespaces.getActiveNamespaces().join(',')) + } + + @Test + void 'network policies are created for prometheus'() { + config.application.netpols = true + //config.application.namespaces.dedicatedNamespaces = ["testnamespace1", "testnamespace2"] + def prometheusStack = createStack(scmManagerMock) + prometheusStack.install() + + for (String namespace : config.application.namespaces.getActiveNamespaces()) { + def netPolsYaml = new File("$clusterResourcesRepoDir/apps/monitoring/misc/netpols/${namespace}.yaml") + assertThat(netPolsYaml.text).contains("namespace: ${namespace}") + } + } + + @Test + void 'helm releases are installed in air-gapped mode'() { + config.application.mirrorRepos = true + when(airGappedUtils.mirrorHelmRepoToGit(any(Config.HelmConfig))).thenReturn('a/b') + + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) + config.application.localHelmChartFolder = rootChartsFolder.toString() + + Path prometheusSourceChart = rootChartsFolder.resolve('kube-prometheus-stack') + Files.createDirectories(prometheusSourceChart) + + Map prometheusChartYaml = [version: '1.2.3'] + fileSystemUtils.writeYaml(prometheusChartYaml, prometheusSourceChart.resolve('Chart.yaml').toFile()) + + scmManagerMock.inClusterBase = new URI("http://scmm.foo-scm-manager.svc.cluster.local/scm") + createStack(scmManagerMock).install() + + def helmConfig = ArgumentCaptor.forClass(Config.HelmConfig) + verify(airGappedUtils).mirrorHelmRepoToGit(helmConfig.capture()) + assertThat(helmConfig.value.chart).isEqualTo('kube-prometheus-stack') + assertThat(helmConfig.value.repoURL).isEqualTo('https://prom') + assertThat(helmConfig.value.version).isEqualTo('19.2.2') + verify(deploymentStrategy).deployFeature('http://scmm.foo-scm-manager.svc.cluster.local/scm/repo/a/b', + 'monitoring', '.', '1.2.3', 'foo-monitoring', + 'kube-prometheus-stack', temporaryYamlFilePrometheus, RepoType.GIT) + } + + @Test + void 'Merges additional helm values merged with default values'() { + config.features.monitoring.helm.values = [key : [some: 'thing', + one : 1], + prometheus: [prometheusSpec: [scrapeConfigSelectorNilUsesHelmValues: null]]] + + createStack(scmManagerMock).install() + def actual = parseActualYaml() + + assertThat(actual['key']['some']).isEqualTo('thing') + assertThat(actual['key']['one']).isEqualTo(1) + assertThat(actual['prometheus']['prometheusSpec']['scrapeConfigSelectorNilUsesHelmValues']).isEqualTo(null) + } + + @Test + void 'ServiceMonitor selectors'() { + config.application.namePrefix = "test1-" + config.features.argocd.active = true + config.features.secrets.active = true + config.features.ingress.active = false + LinkedHashSet namespaceList = ["test1-argocd", + "test1-monitoring", + "test1-example-apps-staging", + "test1-example-apps-production", + "test1-secrets"] + config.application.namespaces.dedicatedNamespaces = namespaceList + createStack(scmManagerMock).install() + def actual = parseActualYaml() + + assertThat(actual['prometheus']['prometheusSpec']['serviceMonitorNamespaceSelector']).isEqualTo(new YamlSlurper().parseText(''' matchExpressions: - key: kubernetes.io/metadata.name operator: In @@ -664,46 +610,45 @@ matchExpressions: - test1-example-apps-staging - test1-example-apps-production - test1-secrets -''' - )) - } - - private Monitoring createStack(ScmManagerMock scmManagerMock) { - // We use the real FileSystemUtils and not a mock to make sure file editing works as expected - when(gitHandler.getResourcesScm()).thenReturn(scmManagerMock) - def configuration = config - TestGitRepoFactory repoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) { - @Override - GitRepo getRepo(String repoTarget,GitProvider scm) { - def repo = super.getRepo(repoTarget, scmManagerMock) - clusterResourcesRepoDir = new File(repo.getAbsoluteLocalRepoTmpDir()) - - // Create dummy dashboards so cleanupUnusedDashboards can delete them - def dashboardDir = new File(clusterResourcesRepoDir, "apps/monitoring/misc/dashboard") - dashboardDir.mkdirs() - - new File(dashboardDir, "traefik-dashboard.yaml").text = "dummy" - new File(dashboardDir, "traefik-dashboard-requests-handling.yaml").text = "dummy" - new File(dashboardDir, "jenkins-dashboard.yaml").text = "dummy" - new File(dashboardDir, "scmm-dashboard.yaml").text = "dummy" - - return repo - } - - } - - new Monitoring(configuration, new FileSystemUtils() { - @Override - Path writeTempFile(Map mapValues) { - def ret = super.writeTempFile(mapValues) - temporaryYamlFilePrometheus = Path.of(ret.toString().replace(".ftl", "")) - return ret - } - }, deploymentStrategy, k8sClient, airGappedUtils, repoProvider, gitHandler) - } - - private Map parseActualYaml() { - def ys = new YamlSlurper() - return ys.parse(temporaryYamlFilePrometheus) as Map - } -} +''')) + } + + private Monitoring createStack(ScmManagerMock scmManagerMock) { + // We use the real FileSystemUtils and not a mock to make sure file editing works as expected + when(gitHandler.getResourcesScm()).thenReturn(scmManagerMock) + def configuration = config + TestGitRepoFactory repoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) { + @Override + GitRepo getRepo(String repoTarget, GitProvider scm) { + def repo = super.getRepo(repoTarget, scmManagerMock) + clusterResourcesRepoDir = new File(repo.getAbsoluteLocalRepoTmpDir()) + + // Create dummy dashboards so cleanupUnusedDashboards can delete them + def dashboardDir = new File(clusterResourcesRepoDir, "apps/monitoring/misc/dashboard") + dashboardDir.mkdirs() + + new File(dashboardDir, "traefik-dashboard.yaml").text = "dummy" + new File(dashboardDir, "traefik-dashboard-requests-handling.yaml").text = "dummy" + new File(dashboardDir, "jenkins-dashboard.yaml").text = "dummy" + new File(dashboardDir, "scmm-dashboard.yaml").text = "dummy" + + return repo + } + + } + + new Monitoring(configuration, new FileSystemUtils() { + @Override + Path writeTempFile(Map mapValues) { + def ret = super.writeTempFile(mapValues) + temporaryYamlFilePrometheus = Path.of(ret.toString().replace(".ftl", "")) + return ret + } + }, deploymentStrategy, k8sClient, airGappedUtils, repoProvider, gitHandler) + } + + private Map parseActualYaml() { + def ys = new YamlSlurper() + return ys.parse(temporaryYamlFilePrometheus) as Map + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetupTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetupTest.groovy index b9fe4d31e..b358b1e35 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetupTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetupTest.groovy @@ -1,7 +1,7 @@ package com.cloudogu.gitops.features.argocd -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test +import static org.assertj.core.api.Assertions.assertThat +import static org.junit.jupiter.api.Assertions.assertThrows import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.git.providers.GitProvider @@ -12,204 +12,190 @@ import com.cloudogu.gitops.utils.git.TestGitRepoFactory import java.nio.file.Path -import static org.assertj.core.api.Assertions.assertThat -import static org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test class ArgoCDRepoSetupTest { - Config config - GitProvider tenantProvider - GitProvider centralProvider - - @BeforeEach - void setUp() { - config = Config.fromMap( - application: [ - namePrefix: '', - netpols : true, - namespaces: [ - dedicatedNamespaces: ["argocd", "monitoring", "secrets"], - tenantNamespaces : ["example-apps-staging", "example-apps-production"] - ] - ], - scm: [ - scmManager: [internal: true], - gitlab : [url: ''] - ], - multiTenant: [ - scmManager : [url: ''], - gitlab : [url: ''], - useDedicatedInstance : false, - centralArgocdNamespace: 'argocd' - ], - features: [ - argocd : [ - operator : false, - active : true, - namespace: 'argocd' - ], - certManager : [active: false], - ingress: [active: true], - monitoring : [active: true, helm: [chart: 'kube-prometheus-stack', version: '42.0.3']], - mail : [active: false], - secrets : [active: true], - ] - ) - - def providers = TestGitProvider.buildProviders(config) - tenantProvider = providers.tenant as GitProvider - centralProvider = providers.central as GitProvider - } - - private ArgoCDRepoSetup createSetup(FileSystemUtils fs) { - def repoFactory = new TestGitRepoFactory(config, new FileSystemUtils()) - repoFactory.defaultProvider = tenantProvider - - def gitHandler = new GitHandlerForTests(config, tenantProvider, centralProvider) - return ArgoCDRepoSetup.create(config, fs, repoFactory, gitHandler) - } - - @Test - void 'create() single instance creates only cluster-resources and no tenantBootstrap'() { - config.multiTenant.useDedicatedInstance = false - - def setup = createSetup(new FileSystemUtils()) - - assertThat(setup.tenantBootstrap).isNull() - assertThat(setup.clusterResources).isNotNull() - assertThat(setup.allRepos).hasSize(1) - assertThat(setup.clusterResources.repo.repoTarget).isEqualTo('argocd/cluster-resources') - } - - @Test - void 'create() dedicated instance creates tenantBootstrap and clusterResources'() { - config.multiTenant.useDedicatedInstance = true - - def setup = createSetup(new FileSystemUtils()) - - assertThat(setup.tenantBootstrap).isNotNull() - assertThat(setup.clusterResources).isNotNull() - assertThat(setup.allRepos).hasSize(2) - } - - @Test - void 'tenantRepoLayout throws in single instance mode'() { - config.multiTenant.useDedicatedInstance = false - - def setup = createSetup(new FileSystemUtils()) - - assertThrows(IllegalStateException) { - setup.tenantRepoLayout() - } - } - - @Test - void 'prepareClusterResourcesRepo deletes helmDir when operator is enabled'() { - config.features.argocd.operator = true - config.multiTenant.useDedicatedInstance = false - config.application.netpols = true - def setup = createSetup(new FileSystemUtils()) - - setup.initLocalRepos() - setup.prepareClusterResourcesRepo() - - def clusterRepoLayout = setup.clusterRepoLayout() - assertThat(Path.of(clusterRepoLayout.helmDir())).doesNotExist() - } - - @Test - void 'prepareClusterResourcesRepo deletes operatorDir when operator is disabled'() { - config.features.argocd.operator = false - config.multiTenant.useDedicatedInstance = false - config.application.netpols = true - - def setup = createSetup(new FileSystemUtils()) - - setup.initLocalRepos() - setup.prepareClusterResourcesRepo() - - def clusterRepoLayout = setup.clusterRepoLayout() - assertThat(Path.of(clusterRepoLayout.operatorDir())).doesNotExist() - assertThat(Path.of(clusterRepoLayout.helmDir())).exists() - - } - - @Test - void 'prepareClusterResourcesRepo in dedicated mode deletes multiTenant folder'() { - config.features.argocd.operator = false - config.multiTenant.useDedicatedInstance = true - config.application.netpols = true - - def setup = createSetup(new FileSystemUtils()) - - setup.initLocalRepos() - setup.prepareClusterResourcesRepo() - - def clusterRepoLayout = setup.clusterRepoLayout() - - assertThat(Path.of(clusterRepoLayout.applicationsDir())).exists() - assertThat(Path.of(clusterRepoLayout.projectsDir())).exists() - assertThat(Path.of(clusterRepoLayout.multiTenantDir())).doesNotExist() - } + Config config + GitProvider tenantProvider + GitProvider centralProvider + + @BeforeEach + void setUp() { + config = Config.fromMap(application: [namePrefix: '', + netpols : true, + namespaces: [dedicatedNamespaces: ["argocd", "monitoring", "secrets"], + tenantNamespaces : ["example-apps-staging", "example-apps-production"]]], + scm: [scmManager: [internal: true], + gitlab : [url: '']], + multiTenant: [scmManager : [url: ''], + gitlab : [url: ''], + useDedicatedInstance : false, + centralArgocdNamespace: 'argocd'], + features: [argocd : [operator : false, + active : true, + namespace: 'argocd'], + certManager: [active: false], + ingress : [active: true], + monitoring : [active: true, helm: [chart: 'kube-prometheus-stack', version: '42.0.3']], + mail : [active: false], + secrets : [active: true],]) + + def providers = TestGitProvider.buildProviders(config) + tenantProvider = providers.tenant as GitProvider + centralProvider = providers.central as GitProvider + } + + private ArgoCDRepoSetup createSetup(FileSystemUtils fs) { + def repoFactory = new TestGitRepoFactory(config, new FileSystemUtils()) + repoFactory.defaultProvider = tenantProvider + + def gitHandler = new GitHandlerForTests(config, tenantProvider, centralProvider) + return ArgoCDRepoSetup.create(config, fs, repoFactory, gitHandler) + } + + @Test + void 'create() single instance creates only cluster-resources and no tenantBootstrap'() { + config.multiTenant.useDedicatedInstance = false + + def setup = createSetup(new FileSystemUtils()) + + assertThat(setup.tenantBootstrap).isNull() + assertThat(setup.clusterResources).isNotNull() + assertThat(setup.allRepos).hasSize(1) + assertThat(setup.clusterResources.repo.repoTarget).isEqualTo('argocd/cluster-resources') + } + + @Test + void 'create() dedicated instance creates tenantBootstrap and clusterResources'() { + config.multiTenant.useDedicatedInstance = true + + def setup = createSetup(new FileSystemUtils()) + + assertThat(setup.tenantBootstrap).isNotNull() + assertThat(setup.clusterResources).isNotNull() + assertThat(setup.allRepos).hasSize(2) + } + + @Test + void 'tenantRepoLayout throws in single instance mode'() { + config.multiTenant.useDedicatedInstance = false + + def setup = createSetup(new FileSystemUtils()) + + assertThrows(IllegalStateException) { + setup.tenantRepoLayout() + } + } + + @Test + void 'prepareClusterResourcesRepo deletes helmDir when operator is enabled'() { + config.features.argocd.operator = true + config.multiTenant.useDedicatedInstance = false + config.application.netpols = true + def setup = createSetup(new FileSystemUtils()) + + setup.initLocalRepos() + setup.prepareClusterResourcesRepo() + + def clusterRepoLayout = setup.clusterRepoLayout() + assertThat(Path.of(clusterRepoLayout.helmDir())).doesNotExist() + } + + @Test + void 'prepareClusterResourcesRepo deletes operatorDir when operator is disabled'() { + config.features.argocd.operator = false + config.multiTenant.useDedicatedInstance = false + config.application.netpols = true + + def setup = createSetup(new FileSystemUtils()) + + setup.initLocalRepos() + setup.prepareClusterResourcesRepo() + + def clusterRepoLayout = setup.clusterRepoLayout() + assertThat(Path.of(clusterRepoLayout.operatorDir())).doesNotExist() + assertThat(Path.of(clusterRepoLayout.helmDir())).exists() + + } + + @Test + void 'prepareClusterResourcesRepo in dedicated mode deletes multiTenant folder'() { + config.features.argocd.operator = false + config.multiTenant.useDedicatedInstance = true + config.application.netpols = true + + def setup = createSetup(new FileSystemUtils()) + + setup.initLocalRepos() + setup.prepareClusterResourcesRepo() + + def clusterRepoLayout = setup.clusterRepoLayout() + + assertThat(Path.of(clusterRepoLayout.applicationsDir())).exists() + assertThat(Path.of(clusterRepoLayout.projectsDir())).exists() + assertThat(Path.of(clusterRepoLayout.multiTenantDir())).doesNotExist() + } - @Test - void 'prepareClusterResourcesRepo in single instance deletes multiTenant folder'() { - config.features.argocd.operator = false - config.multiTenant.useDedicatedInstance = false - config.application.netpols = true + @Test + void 'prepareClusterResourcesRepo in single instance deletes multiTenant folder'() { + config.features.argocd.operator = false + config.multiTenant.useDedicatedInstance = false + config.application.netpols = true - def setup = createSetup(new FileSystemUtils()) + def setup = createSetup(new FileSystemUtils()) - setup.initLocalRepos() - setup.prepareClusterResourcesRepo() + setup.initLocalRepos() + setup.prepareClusterResourcesRepo() - def clusterRepoLayout = setup.clusterRepoLayout() - assertThat(Path.of(clusterRepoLayout.multiTenantDir())).doesNotExist() - } + def clusterRepoLayout = setup.clusterRepoLayout() + assertThat(Path.of(clusterRepoLayout.multiTenantDir())).doesNotExist() + } - @Test - void 'prepareClusterResourcesRepo deletes netpol file when netpols disabled'() { - config.application.netpols = false + @Test + void 'prepareClusterResourcesRepo deletes netpol file when netpols disabled'() { + config.application.netpols = false - def setup = createSetup(new FileSystemUtils()) + def setup = createSetup(new FileSystemUtils()) - setup.initLocalRepos() - setup.prepareClusterResourcesRepo() + setup.initLocalRepos() + setup.prepareClusterResourcesRepo() - def clusterRepoLayout = setup.clusterRepoLayout() - assertThat(Path.of(clusterRepoLayout.netpolFile())).doesNotExist() - } + def clusterRepoLayout = setup.clusterRepoLayout() + assertThat(Path.of(clusterRepoLayout.netpolFile())).doesNotExist() + } - @Test - void 'create() sets subDirsToCopy based on enabled features'() { - config.features.ingress.active = true - config.features.monitoring.active = false - config.features.secrets.active = false - config.jenkins.active = false - config.features.mail.active = false - config.features.certManager.active = false + @Test + void 'create() sets subDirsToCopy based on enabled features'() { + config.features.ingress.active = true + config.features.monitoring.active = false + config.features.secrets.active = false + config.jenkins.active = false + config.features.mail.active = false + config.features.certManager.active = false - def setup = createSetup(new FileSystemUtils()) - def dirs = setup.clusterResources.subDirsToCopy as Set + def setup = createSetup(new FileSystemUtils()) + def dirs = setup.clusterResources.subDirsToCopy as Set - assertThat(dirs).contains(RepoLayout.argocdSubdirRel()) - assertThat(dirs).contains(RepoLayout.ingressSubdirRel()) + assertThat(dirs).contains(RepoLayout.argocdSubdirRel()) + assertThat(dirs).contains(RepoLayout.ingressSubdirRel()) - assertThat(dirs).doesNotContain(RepoLayout.monitoringSubdirRel()) - assertThat(dirs).doesNotContain(RepoLayout.secretsSubdirRel()) - assertThat(dirs).doesNotContain(RepoLayout.vaultSubdirRel()) - assertThat(dirs).doesNotContain(RepoLayout.jenkinsSubdirRel()) - assertThat(dirs).doesNotContain(RepoLayout.certManagerSubdirRel()) - } + assertThat(dirs).doesNotContain(RepoLayout.monitoringSubdirRel()) + assertThat(dirs).doesNotContain(RepoLayout.secretsSubdirRel()) + assertThat(dirs).doesNotContain(RepoLayout.vaultSubdirRel()) + assertThat(dirs).doesNotContain(RepoLayout.jenkinsSubdirRel()) + assertThat(dirs).doesNotContain(RepoLayout.certManagerSubdirRel()) + } - @Test - void 'create() includes secrets + vault subdirs when secrets feature active'() { - config.features.secrets.active = true + @Test + void 'create() includes secrets + vault subdirs when secrets feature active'() { + config.features.secrets.active = true - def setup = createSetup(new FileSystemUtils()) - def dirs = setup.clusterResources.subDirsToCopy as Set + def setup = createSetup(new FileSystemUtils()) + def dirs = setup.clusterResources.subDirsToCopy as Set - assertThat(dirs).contains(RepoLayout.secretsSubdirRel()) - assertThat(dirs).contains(RepoLayout.vaultSubdirRel()) - } + assertThat(dirs).contains(RepoLayout.secretsSubdirRel()) + assertThat(dirs).contains(RepoLayout.vaultSubdirRel()) + } } \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy index 1b14a5e52..94d313d76 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy @@ -1,8 +1,8 @@ package com.cloudogu.gitops.features.argocd -import org.junit.jupiter.api.Test -import org.mockito.Spy -import org.springframework.security.crypto.bcrypt.BCrypt +import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable +import static org.assertj.core.api.Assertions.assertThat +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.git.GitRepo @@ -23,1582 +23,1446 @@ import groovy.io.FileType import groovy.json.JsonSlurper import groovy.yaml.YamlSlurper -import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable -import static org.assertj.core.api.Assertions.assertThat -import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode +import org.junit.jupiter.api.Test +import org.mockito.Spy +import org.springframework.security.crypto.bcrypt.BCrypt class ArgoCDTest { - Map buildImages = [ - kubectl : 'kubectl-value', - helm : 'helm-value', - kubeval : 'kubeval-value', - helmKubeval: 'helmKubeval-value', - yamllint : 'yamllint-value' - ] - - Config config = Config.fromMap( - application: [ - openshift : false, - insecure : false, - password : '123', - username : 'something', - namePrefix : '', - namePrefixForEnvVars: '', - gitName : 'Cloudogu', - gitEmail : 'hello@cloudogu.com', - namespaces : [ - dedicatedNamespaces: ["argocd", "monitoring", "traefik", "secrets"], - tenantNamespaces : ["example-apps-staging", "example-apps-production"] - ] - ], - scm: [ - scmManager: [ - internal: true], - gitlab : [ - url: '' - ] - ], - multiTenant: [ - scmManager : [ - url: '' - ], - gitlab : [ - url: '' - ], - useDedicatedInstance: false - ], - content: [ - repos: [ - [ - url: 'https://github.com/cloudogu/gitops-build-lib', - target: '3rd-party-dependencies/gitops-build-lib', - overwriteMode: 'RESET' - ], - [ - url: 'https://github.com/cloudogu/ces-build-lib', - target: '3rd-party-dependencies/ces-build-lib', - overwriteMode: 'RESET' - ], - [ - url: 'https://github.com/cloudogu/spring-boot-helm-chart', - target: '3rd-party-dependencies/spring-boot-helm-chart', - overwriteMode: 'RESET' - ], - [ - url: 'https://github.com/cloudogu/spring-petclinic', - target: 'argocd/petclinic-plain', - ref: 'feature/gitops_ready', - targetRef: 'main', - overwriteMode: 'UPGRADE', - createJenkinsJob: true - ], - [ - url: 'https://github.com/cloudogu/spring-petclinic', - target: 'argocd/petclinic-helm', - ref: 'feature/gitops_ready', - targetRef: 'main', - overwriteMode: 'UPGRADE', - createJenkinsJob: true - ], - [ - url: 'https://github.com/cloudogu/gitops-playground', - path: 'example-apps-via-content-loader/', - ref: 'main', - templating: true, - type: 'FOLDER_BASED', - overwriteMode: 'UPGRADE' - ] - ], - namespaces: [ - "example-apps-production", - "example-apps-staging" - ], - variables: [ - petclinic: [ - baseDomain: 'petclinic.localhost' - ], - images: [ - kubectl: 'alpine/kubectl:1.35.0', - helm: 'ghcr.io/cloudogu/helm:3.16.4-1', - kubeval: 'ghcr.io/cloudogu/helm:3.16.4-1', - helmKubeval: 'ghcr.io/cloudogu/helm:3.16.4-1', - yamllint: 'cytopia/yamllint:1.25-0.7', - petclinic: 'eclipse-temurin:17-jre-alpine', - maven: '' - ] - ] - ], - features: [ - argocd : [ - operator : false, - active : true, - configOnly : true, - emailFrom : 'argocd@example.org', - emailToUser : 'app-team@example.org', - emailToAdmin : 'infra@example.org', - resourceInclusionsCluster: '' - ], - monitoring : [ - active: true, - helm : [ - chart : 'kube-prometheus-stack', - version: '42.0.3' - ] - ], - ingress: [ - active: true - ], - secrets : [ - active: true, - - ] - ] - ) - - @Spy - CommandExecutor test = new CommandExecutor() - - CommandExecutorForTest k8sCommands = new CommandExecutorForTest() - CommandExecutorForTest helmCommands = new CommandExecutorForTest() -// GitRepo argocdRepo - String actualHelmValuesFile - GitRepo clusterResourcesRepo - List petClinicRepos = [] - ArgoCD argocd - RepoLayout clusterResourcesRepoLayout - - @Test - void 'Installs argoCD'() { - // Simulate argocd Namespace does not exist - k8sCommands.enqueueOutput(new CommandExecutor.Output('namespace not found', '', 1)) - - def argocd = createArgoCD() - argocd.install() - this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo - - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - - - k8sCommands.assertExecuted('kubectl create namespace argocd') - - // check values.yaml - List filesWithInternalSCMM = findFilesContaining( - new File(clusterResourcesRepoLayout.rootDir()), - clusterResourcesRepo.gitProvider.url - ) - assertThat(filesWithInternalSCMM).isNotEmpty() - assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['server']['service']['type']) - .isEqualTo('ClusterIP') - assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['argocdUrl']).isNull() - - assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['crds']).isNull() - assertThat(parseActualYaml(actualHelmValuesFile)['global']).isNull() - - // check repoTemplateSecretName - k8sCommands.assertExecuted('kubectl create secret generic argocd-repo-creds-scm -n argocd') - k8sCommands.assertExecuted('kubectl label secret argocd-repo-creds-scm -n argocd') - - // Check dependency build and helm install (Chart liegt jetzt unter apps/argocd/argocd) - assertThat(helmCommands.actualCommands[0].trim()) - .isEqualTo('helm repo add argo https://argoproj.github.io/argo-helm') - assertThat(helmCommands.actualCommands[1].trim()) - .isEqualTo("helm dependency build ${clusterResourcesRepoLayout.helmDir()}".toString()) - assertThat(helmCommands.actualCommands[2].trim()) - .isEqualTo("helm upgrade -i argocd ${clusterResourcesRepoLayout.helmDir()} --create-namespace --namespace argocd".toString()) - - // Check patched PW - def patchCommand = k8sCommands.assertExecuted('kubectl patch secret argocd-secret -n argocd') - String patchFile = (patchCommand =~ /--patch-file=([\S]+)/)?.findResult { (it as List)[1] } - assertThat(BCrypt.checkpw(config.application.password as String, - parseActualYaml(patchFile)['stringData']['admin.password'] as String)) - .as("Password hash missmatch").isTrue() - - // Check bootstrapping (liegt jetzt unter argocd/projects und argocd/applications) - k8sCommands.assertExecuted("kubectl apply -f ${Path.of(clusterResourcesRepoLayout.projectsDir(), 'argocd.yaml')}") - k8sCommands.assertExecuted("kubectl apply -f ${Path.of(clusterResourcesRepoLayout.applicationsDir(), 'bootstrap.yaml')}") - - def deleteCommand = k8sCommands.assertExecuted('kubectl delete secret -n argocd') - assertThat(deleteCommand).contains('owner=helm', 'name=argocd') - - // Operator disabled -> operator Ordner sollte fehlen - assertThat(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toFile()).doesNotExist() - assertThat(Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile()).doesNotExist() - - // Projects (jetzt unter argocd/projects) - def clusterRessourcesYaml = new YamlSlurper().parse( - Path.of(clusterResourcesRepoLayout.projectsDir(), 'cluster-resources.yaml') - ) - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'https://prometheus-community.github.io/helm-charts' - ) - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'http://scmm-scm-manager.default.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack' - ) - - // Applications (jetzt unter argocd/applications) - def argocdYaml = new YamlSlurper().parse( - Path.of(clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml') - ) - assertThat(argocdYaml['spec']['source']['directory']).isNull() - - // Neuer Pfad: Chart liegt unter argocd/argocd (nicht mehr nur argocd/) - assertThat(argocdYaml['spec']['source']['path'] as String) - .isIn('apps/argocd/argocd', 'apps/argocd/argocd/') - } - - @Test - void 'Installs Argo CD with custom values'() { - config.features.argocd.values = ['argo-cd': [key: 'value']] - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - def valuesYaml = parseActualYaml(actualHelmValuesFile) - assertThat(valuesYaml['argo-cd']['key']).isEqualTo('value') - } - - @Test - void 'When monitoring disabled: Does not push path monitoring to cluster resources'() { - config.features.monitoring.active = false - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - assertThat(new File(clusterResourcesRepoLayout.monitoringDir())).doesNotExist() - } - - @Test - void 'When monitoring enabled: Does push path monitoring to cluster resources'() { - config.features.monitoring.active = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - assertThat(new File(clusterResourcesRepoLayout.monitoringDir())).exists() - assertValidDashboards(clusterResourcesRepoLayout.monitoringDir()) - } - - void assertValidDashboards(String monitoringPath) { - Files.walk(Path.of(monitoringPath)) - .filter { it.toString() ==~ /.*-dashboard\.yaml/ }.each { Path path -> - def dashboardConfigMap = null - - assertThatCode { - dashboardConfigMap = parseActualYaml(path.toString()) - }.as("Invalid YAML in ${path.fileName}").doesNotThrowAnyException() - - assertThat(dashboardConfigMap.data as Map).hasSize(1) - .as('Expected only on dashboard json within map') - assertThatCode { - def dashboardJsonString = (dashboardConfigMap.data as Map).entrySet().first().value as String - new JsonSlurper().parseText(dashboardJsonString) - }.as("Invalid JSON in ${path.fileName}").doesNotThrowAnyException() - } - } - - - - @Test - void 'When mailServer disabled: Does not include mail configurations into cluster resources'() { - config.features.mail.active = false - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - - def valuesYaml = parseActualYaml(actualHelmValuesFile) - assertThat(valuesYaml['argo-cd']['notifications']['enabled']).isEqualTo(false) - assertThat(valuesYaml['argo-cd']['notifications']['notifiers']).isNull() - } - - @Test - void 'When mailServer enabled: Includes mail configurations into cluster resources'() { - config.features.mail.active = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - def valuesYaml = parseActualYaml(actualHelmValuesFile) - - assertThat(valuesYaml['argo-cd']['notifications']['enabled']).isEqualTo(true) - assertThat(valuesYaml['argo-cd']['notifications']['notifiers']).isNotNull() - } - - @Test - void 'When emailaddress is set: Include given email addresses into configurations'() { - config.features.mail.active = true - config.features.argocd.emailFrom = 'argocd@example.com' - config.features.argocd.emailToUser = 'app-team@example.com' - config.features.argocd.emailToAdmin = 'argocd@example.com' - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - def valuesYaml = parseActualYaml(actualHelmValuesFile) - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), "cluster-resources.yaml") - def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml') - def defaultYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), 'default.yaml') - - assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)['from']).isEqualTo("argocd@example.com") - assertThat(clusterRessourcesYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('argocd@example.com') - assertThat(argocdYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.on-sync-status-unknown.email']).isEqualTo('argocd@example.com') - assertThat(defaultYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('argocd@example.com') - } - - @Test - void 'When emailaddress is NOT set: Use default email addresses in configurations'() { - config.features.mail.active = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - def valuesYaml = parseActualYaml(actualHelmValuesFile) - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), "cluster-resources.yaml") - def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml') - def defaultYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), 'default.yaml') - - assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)['from']).isEqualTo("argocd@example.org") - assertThat(clusterRessourcesYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('infra@example.org') - assertThat(argocdYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.on-sync-status-unknown.email']).isEqualTo('infra@example.org') - assertThat(defaultYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('infra@example.org') - } - - @Test - void 'When external Mailserver is set'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - config.features.mail.smtpPort = 1010110 - config.features.mail.smtpUser = 'argo@example.com' - config.features.mail.smtpPassword = '1101:ABCabc&/+*~' - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - - def serviceEmail = new YamlSlurper().parseText( - parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['notifiers']['service.email'] as String) - - assertThat(serviceEmail['host']).isEqualTo(config.features.mail.smtpAddress) - assertThat(serviceEmail['port']).isEqualTo(config.features.mail.smtpPort) - // username and password are both linked to the k8s secret. Secrets will be created at runtime, in this test - assertThat(serviceEmail['username']).isEqualTo('$email-username') - assertThat(serviceEmail['password']).isEqualTo('$email-password') - - def createMailSecretCommand = k8sCommands.assertExecuted('kubectl create secret generic argocd-notifications-secret -n argocd') - assertThat(createMailSecretCommand).contains('email-username', config.features.mail.smtpUser as CharSequence) - assertThat(createMailSecretCommand).contains('email-password', config.features.mail.smtpPassword as CharSequence) - } - - @Test - void 'When external emailservers username is set, check if kubernetes secret will be created'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - config.features.mail.smtpUser = 'argo@example.com' - - createArgoCD().install() - - def createMailSecretCommand = k8sCommands.assertExecuted('kubectl create secret generic argocd-notifications-secret -n argocd') - assertThat(createMailSecretCommand).contains('email-username', config.features.mail.smtpUser as CharSequence) - } - - @Test - void 'When external emailservers password is set, check if kubernetes secret will be created'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - config.features.mail.smtpPassword = '1101:ABCabc&/+*~' - - createArgoCD().install() - - def createMailSecretCommand = k8sCommands.assertExecuted('kubectl create secret generic argocd-notifications-secret -n argocd') - assertThat(createMailSecretCommand).contains('email-password', config.features.mail.smtpPassword as CharSequence) - } - - - @Test - void 'When external Mailserver is set without port, user, password'() { - config.features.mail.active = true - config.features.mail.smtpAddress = 'smtp.example.com' - - def argocd = createArgoCD() - argocd.install() - - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - - def serviceEmail = new YamlSlurper().parseText( - parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['notifiers']['service.email'] as String) - - k8sCommands.assertNotExecuted('kubectl create secret generic argocd-notifications-secret') - - assertThat(serviceEmail['host']).isEqualTo("smtp.example.com") - assertThat(serviceEmail as Map).doesNotContainKey('port') - assertThat(serviceEmail as Map).doesNotContainKey('username') - assertThat(serviceEmail as Map).doesNotContainKey('password') - } - - @Test - void 'When external Mailserver is NOT set'() { - config.features.mail.active = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - def valuesYaml = parseActualYaml(actualHelmValuesFile) - - assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)['port']).isEqualTo(1025) - assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)) doesNotHaveToString('username') - assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)) doesNotHaveToString('password') - } - - @Test - void 'When vault disabled: Does not push path "secrets" to cluster resources'() { - config.features.secrets.active = false - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - assertThat(new File(clusterResourcesRepoLayout.vaultDir())).doesNotExist() - } - - @Test - void 'Prepares repos for air-gapped mode'() { - config.features.monitoring.active = false - config.application.mirrorRepos = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), "cluster-resources.yaml") - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack') - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'https://prometheus-community.github.io/helm-charts') - } - - - @Test - void 'Pushes repos with empty name-prefix'() { - def argocd = createArgoCD() - argocd.install() - this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - - assertArgoCdYamlPrefixes(clusterResourcesRepo.gitProvider.url, '', clusterResourcesRepoLayout) - } - - @Test - void 'Creates Jenkinsfiles for two registries'() { - config.registry.twoRegistries = true - createArgoCD().install() - - assertJenkinsfileRegistryCredentials() - } - - @Test - void 'Pushes repos with name-prefix'() { - config.application.namePrefix = 'abc-' - - def argocd = createArgoCD() - argocd.install() - this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - assertArgoCdYamlPrefixes(clusterResourcesRepo.gitProvider.url, config.application.namePrefix, clusterResourcesRepoLayout) - } - - @Test - void 'SecurityContext null in Openshift'() { - config.application.openshift = true - createArgoCD().install() - - for (def petclinicRepo : petClinicRepos) { - if (petclinicRepo.repoTarget.contains('argocd/petclinic-plain')) { - assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/staging/deployment.yaml').text).contains('runAsUser: null') - assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/staging/deployment.yaml').text).contains('runAsGroup: null') - } - if (petclinicRepo.repoTarget.contains('argocd/petclinic-helm')) { - assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).contains('runAsUser: null') - assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).contains('runAsGroup: null') - } - } - } - - @Test - void 'Skips CRDs for argo cd'() { - config.application.skipCrds = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - - assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['crds']['install']).isEqualTo(false) - } - - @Test - void 'Write maven mirror into jenkinsfiles'() { - config.jenkins.mavenCentralMirror = 'http://test' - createArgoCD().install() - - for (def petclinicRepo : petClinicRepos) { - assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, 'Jenkinsfile').text).contains( - 'mvn.useMirrors([name: \'maven-central-mirror\', mirrorOf: \'central\', url: env.MAVEN_CENTRAL_MIRROR])' - ) - } - } - - @Test - void 'ArgoCD with active network policies'() { - config.application.netpols = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" - - assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['global']['networkPolicy']['create']).isEqualTo(true) - assertThat(new File(clusterResourcesRepoLayout.argocdRoot(), '/argocd/values.yaml').text.contains("namespace: monitoring")) - assertThat(new File(clusterResourcesRepoLayout.argocdRoot(), '/argocd/templates/allow-namespaces.yaml').text.contains("namespace: monitoring")) - assertThat(new File(clusterResourcesRepoLayout.argocdRoot(), '/argocd/templates/allow-namespaces.yaml').text.contains("namespace: default")) - } - - private void assertArgoCdYamlPrefixes(String scmmUrl, String expectedPrefix, RepoLayout repoLayout) { - - assertAllYamlFiles(new File(repoLayout.argocdRoot()), 'projects', 3) { Path file -> - def yaml = parseActualYaml(file.toString()) - List sourceRepos = yaml['spec']['sourceRepos'] as List - // Some projects might not have sourceRepos - if (sourceRepos) { - sourceRepos.each { - if (it.startsWith(scmmUrl)) { - assertThat(it) - .as("$file sourceRepos have name prefix") - .startsWith("${scmmUrl}/repo/${expectedPrefix}argocd") - } - } - } - - String metadataNamespace = yaml['metadata']['namespace'] as String - if (metadataNamespace) { - assertThat(metadataNamespace) - .as("$file metadata.namespace has name prefix") - .isEqualTo("${expectedPrefix}argocd".toString()) - } - - List sourceNamespaces = yaml['spec']['sourceNamespaces'] as List - if (sourceNamespaces) { - sourceNamespaces.each { - if (it != '*') { - assertThat(it) - .as("$file spec.sourceNamespace has name prefix") - .startsWith("${expectedPrefix}") - } - } - } - } - - assertAllYamlFiles(new File(repoLayout.argocdRoot()), 'applications', 3) { Path file -> - def yaml = parseActualYaml(file.toString()) - assertThat(yaml['spec']['source']['repoURL'] as String) - .as("$file repoURL have name prefix") - .startsWith("${scmmUrl}/repo/${expectedPrefix}argocd") - - assertThat(yaml['metadata']['namespace']) - .as("$file metadata.namspace has name prefix") - .isEqualTo("${expectedPrefix}argocd".toString()) - - assertThat(yaml['spec']['destination']['namespace']) - .as("$file spec.destination.namspace has name prefix") - .isEqualTo("${expectedPrefix}argocd".toString()) - } - - //checks all other folder for prefixed yaml files except "apps/argocd" - assertAllYamlFiles(new File(repoLayout.rootDir()), 'apps', 9, - [ '/apps/argocd/' ]) { Path it -> - - def yaml = parseActualYaml(it.toString()) - List yamlDocuments = yaml instanceof List ? yaml : [yaml] - for (def document in yamlDocuments) { - if (document && document['kind'] != 'Namespace') { - def metadataNamespace = document['metadata']['namespace'] as String - assertThat(metadataNamespace) - .as("$it metadata.namespace has name prefix") - .startsWith("${expectedPrefix}") - } - } - } - } - - private static void assertAllYamlFiles( - File rootDir, - String childDir, - Integer numberOfFiles, - List excludeContains = [], - Closure cl - ) { - def rootPath = Path.of(rootDir.absolutePath, childDir) - - def yamlFiles = Files.walk(rootPath) - .filter { Files.isRegularFile(it) } - .filter { Path p -> - def s = p.toString().replace('\\', '/') - (s.endsWith('.yaml') || s.endsWith('.yml')) && - !excludeContains.any { ex -> s.contains(ex) } - } - .collect(Collectors.toList()) - - yamlFiles.each(cl) - - assertThat(yamlFiles.size()).isEqualTo(numberOfFiles) - } - - - private static List findFilesContaining(File folder, String stringToSearch) { - List result = [] - folder.eachFileRecurse(FileType.FILES) { - if (it.text.contains(stringToSearch)) { - result += it - } - } - return result - } - - ArgoCD createArgoCD() { - def argoCD = ArgoCDForTest.newWithAutoProviders(config, k8sCommands, helmCommands) - return argoCD - } - - void assertJenkinsfileRegistryCredentials() { - List defaultRegistryExpectedLines = [ - 'String pathPrefix = !dockerRegistryPath?.trim() ? "" : "${dockerRegistryPath}/"', - 'imageName = "${dockerRegistryBaseUrl}/${pathPrefix}${application}:${imageTag}"' - ] - List twoRegistriesExpectedLines = [ - 'String proxyPathPrefix = !dockerRegistryProxyPath?.trim() ? "" : "${dockerRegistryProxyPath}/"', - 'docker.withRegistry("https://${dockerRegistryProxyBaseUrl}/${proxyPathPrefix}", dockerRegistryProxyCredentials) {', - ] - - for (def petclinicRepo : petClinicRepos) { - String jenkinsfile = new File(petclinicRepo.absoluteLocalRepoTmpDir, 'Jenkinsfile').text - - defaultRegistryExpectedLines.each { expectedEnvVar -> - assertThat(jenkinsfile).contains(expectedEnvVar) - } - - if (config.registry['twoRegistries']) { - twoRegistriesExpectedLines.each { expectedEnvVar -> - assertThat(jenkinsfile).contains(expectedEnvVar) - } - } else { - twoRegistriesExpectedLines.each { expectedEnvVar -> - assertThat(jenkinsfile).doesNotContain(expectedEnvVar) - } - } - } - } - - @Test - void 'Prepares ArgoCD repo with Operator configuration file'() { - def argocd = setupOperatorTest() - - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - def rbacConfigPath = Path.of(clusterResourcesRepoLayout.operatorRbacDir()) - - assertThat(argocdConfigPath.toFile()).exists() - assertThat(rbacConfigPath.toFile()).exists() - - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - assertThat(yaml['apiVersion']).isEqualTo('argoproj.io/v1beta1') - assertThat(yaml['kind']).isEqualTo('ArgoCD') - } - - @Test - void 'No files for operator when operator is false'() { - def argocd = createArgoCD() - - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - def rbacConfigPath = Path.of(clusterResourcesRepoLayout.operatorRbacDir()) - - assertThat(argocdConfigPath.toFile()).doesNotExist() - assertThat(rbacConfigPath.toFile()).doesNotExist() - } - - @Test - void 'Deploys with operator without OpenShift configuration'() { - def argocd = setupOperatorTest(openshift: false) - - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - - k8sCommands.assertExecuted("kubectl apply -f ${argocdConfigPath}") - - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - assertThat(yaml['spec']['rbac']).isNull() - assertThat(yaml['spec']['sso']).isNull() - - def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml') - assertThat(argocdYaml['spec']['source']['directory']['recurse'] as Boolean).isTrue() - assertThat(argocdYaml['spec']['source']['path']).isEqualTo('apps/argocd/operator/') - // Here we should assert all <#if argocd.isOperator> in YAML 😐️ - } - - @Test - void 'RBACs with operator using RbacDefinition outputs'() { - config.application.namePrefix = "testPrefix-" - - LinkedHashSet expectedNamespaces = [ - "testPrefix-monitoring", - "testPrefix-secrets", - "testPrefix-traefik", - "testPrefix-example-apps-staging", - "testPrefix-example-apps-production" - ] - // have to prepare activeNamespaces for unit-test, Application.groovy is setting this in integration way - config.application.namespaces.dedicatedNamespaces = new LinkedHashSet([ - "monitoring", - "secrets", - "traefik", - "example-apps-staging", - "example-apps-production" - ]) - - def argocd = setupOperatorTest(openshift: false) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - File rbacPath = Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile() + Map buildImages = [kubectl : 'kubectl-value', + helm : 'helm-value', + kubeval : 'kubeval-value', + helmKubeval: 'helmKubeval-value', + yamllint : 'yamllint-value'] + + Config config = Config.fromMap(application: [openshift : false, + insecure : false, + password : '123', + username : 'something', + namePrefix : '', + namePrefixForEnvVars: '', + gitName : 'Cloudogu', + gitEmail : 'hello@cloudogu.com', + namespaces : [dedicatedNamespaces: ["argocd", "monitoring", "traefik", "secrets"], + tenantNamespaces : ["example-apps-staging", "example-apps-production"]]], + scm: [scmManager: [internal: true], + gitlab : [url: '']], + multiTenant: [scmManager : [url: ''], + gitlab : [url: ''], + useDedicatedInstance: false], + content: [repos : [[url : 'https://github.com/cloudogu/gitops-build-lib', + target : '3rd-party-dependencies/gitops-build-lib', + overwriteMode: 'RESET'], + [url : 'https://github.com/cloudogu/ces-build-lib', + target : '3rd-party-dependencies/ces-build-lib', + overwriteMode: 'RESET'], + [url : 'https://github.com/cloudogu/spring-boot-helm-chart', + target : '3rd-party-dependencies/spring-boot-helm-chart', + overwriteMode: 'RESET'], + [url : 'https://github.com/cloudogu/spring-petclinic', + target : 'argocd/petclinic-plain', + ref : 'feature/gitops_ready', + targetRef : 'main', + overwriteMode : 'UPGRADE', + createJenkinsJob: true], + [url : 'https://github.com/cloudogu/spring-petclinic', + target : 'argocd/petclinic-helm', + ref : 'feature/gitops_ready', + targetRef : 'main', + overwriteMode : 'UPGRADE', + createJenkinsJob: true], + [url : 'https://github.com/cloudogu/gitops-playground', + path : 'example-apps-via-content-loader/', + ref : 'main', + templating : true, + type : 'FOLDER_BASED', + overwriteMode: 'UPGRADE']], + namespaces: ["example-apps-production", + "example-apps-staging"], + variables : [petclinic: [baseDomain: 'petclinic.localhost'], + images : [kubectl : 'alpine/kubectl:1.35.0', + helm : 'ghcr.io/cloudogu/helm:3.16.4-1', + kubeval : 'ghcr.io/cloudogu/helm:3.16.4-1', + helmKubeval: 'ghcr.io/cloudogu/helm:3.16.4-1', + yamllint : 'cytopia/yamllint:1.25-0.7', + petclinic : 'eclipse-temurin:17-jre-alpine', + maven : '']]], + features: [argocd : [operator : false, + active : true, + configOnly : true, + emailFrom : 'argocd@example.org', + emailToUser : 'app-team@example.org', + emailToAdmin : 'infra@example.org', + resourceInclusionsCluster: ''], + monitoring: [active: true, + helm : [chart : 'kube-prometheus-stack', + version: '42.0.3']], + ingress : [active: true], + secrets : [active: true, + + ]]) + + @Spy + CommandExecutor test = new CommandExecutor() + + CommandExecutorForTest k8sCommands = new CommandExecutorForTest() + CommandExecutorForTest helmCommands = new CommandExecutorForTest() + // GitRepo argocdRepo + String actualHelmValuesFile + GitRepo clusterResourcesRepo + List petClinicRepos = [] + ArgoCD argocd + RepoLayout clusterResourcesRepoLayout + + @Test + void 'Installs argoCD'() { + // Simulate argocd Namespace does not exist + k8sCommands.enqueueOutput(new CommandExecutor.Output('namespace not found', '', 1)) + + def argocd = createArgoCD() + argocd.install() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + + k8sCommands.assertExecuted('kubectl create namespace argocd') + + // check values.yaml + List filesWithInternalSCMM = findFilesContaining(new File(clusterResourcesRepoLayout.rootDir()), + clusterResourcesRepo.gitProvider.url) + assertThat(filesWithInternalSCMM).isNotEmpty() + assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['server']['service']['type']) + .isEqualTo('ClusterIP') + assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['argocdUrl']).isNull() + + assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['crds']).isNull() + assertThat(parseActualYaml(actualHelmValuesFile)['global']).isNull() + + // check repoTemplateSecretName + k8sCommands.assertExecuted('kubectl create secret generic argocd-repo-creds-scm -n argocd') + k8sCommands.assertExecuted('kubectl label secret argocd-repo-creds-scm -n argocd') + + // Check dependency build and helm install (Chart liegt jetzt unter apps/argocd/argocd) + assertThat(helmCommands.actualCommands[0].trim()) + .isEqualTo('helm repo add argo https://argoproj.github.io/argo-helm') + assertThat(helmCommands.actualCommands[1].trim()) + .isEqualTo("helm dependency build ${clusterResourcesRepoLayout.helmDir()}".toString()) + assertThat(helmCommands.actualCommands[2].trim()) + .isEqualTo("helm upgrade -i argocd ${clusterResourcesRepoLayout.helmDir()} --create-namespace --namespace argocd".toString()) + + // Check patched PW + def patchCommand = k8sCommands.assertExecuted('kubectl patch secret argocd-secret -n argocd') + String patchFile = (patchCommand =~ /--patch-file=([\S]+)/)?.findResult { (it as List)[1] } + assertThat(BCrypt.checkpw(config.application.password as String, + parseActualYaml(patchFile)['stringData']['admin.password'] as String)) + .as("Password hash missmatch").isTrue() + + // Check bootstrapping (liegt jetzt unter argocd/projects und argocd/applications) + k8sCommands.assertExecuted("kubectl apply -f ${Path.of(clusterResourcesRepoLayout.projectsDir(), 'argocd.yaml')}") + k8sCommands.assertExecuted("kubectl apply -f ${Path.of(clusterResourcesRepoLayout.applicationsDir(), 'bootstrap.yaml')}") + + def deleteCommand = k8sCommands.assertExecuted('kubectl delete secret -n argocd') + assertThat(deleteCommand).contains('owner=helm', 'name=argocd') + + // Operator disabled -> operator Ordner sollte fehlen + assertThat(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toFile()).doesNotExist() + assertThat(Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile()).doesNotExist() + + // Projects (jetzt unter argocd/projects) + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of(clusterResourcesRepoLayout.projectsDir(), 'cluster-resources.yaml')) + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains('https://prometheus-community.github.io/helm-charts') + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain('http://scmm-scm-manager.default.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack') + + // Applications (jetzt unter argocd/applications) + def argocdYaml = new YamlSlurper().parse(Path.of(clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml')) + assertThat(argocdYaml['spec']['source']['directory']).isNull() + + // Neuer Pfad: Chart liegt unter argocd/argocd (nicht mehr nur argocd/) + assertThat(argocdYaml['spec']['source']['path'] as String) + .isIn('apps/argocd/argocd', 'apps/argocd/argocd/') + } + + @Test + void 'Installs Argo CD with custom values'() { + config.features.argocd.values = ['argo-cd': [key: 'value']] + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + def valuesYaml = parseActualYaml(actualHelmValuesFile) + assertThat(valuesYaml['argo-cd']['key']).isEqualTo('value') + } + + @Test + void 'When monitoring disabled: Does not push path monitoring to cluster resources'() { + config.features.monitoring.active = false + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + assertThat(new File(clusterResourcesRepoLayout.monitoringDir())).doesNotExist() + } + + @Test + void 'When monitoring enabled: Does push path monitoring to cluster resources'() { + config.features.monitoring.active = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + assertThat(new File(clusterResourcesRepoLayout.monitoringDir())).exists() + assertValidDashboards(clusterResourcesRepoLayout.monitoringDir()) + } + + void assertValidDashboards(String monitoringPath) { + Files.walk(Path.of(monitoringPath)) + .filter { it.toString() ==~ /.*-dashboard\.yaml/ }.each { Path path -> + def dashboardConfigMap = null + + assertThatCode { + dashboardConfigMap = parseActualYaml(path.toString()) + }.as("Invalid YAML in ${path.fileName}").doesNotThrowAnyException() + + assertThat(dashboardConfigMap.data as Map).hasSize(1) + .as('Expected only on dashboard json within map') + assertThatCode { + def dashboardJsonString = (dashboardConfigMap.data as Map).entrySet().first().value as String + new JsonSlurper().parseText(dashboardJsonString) + }.as("Invalid JSON in ${path.fileName}").doesNotThrowAnyException() + } + } + + @Test + void 'When mailServer disabled: Does not include mail configurations into cluster resources'() { + config.features.mail.active = false + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + + def valuesYaml = parseActualYaml(actualHelmValuesFile) + assertThat(valuesYaml['argo-cd']['notifications']['enabled']).isEqualTo(false) + assertThat(valuesYaml['argo-cd']['notifications']['notifiers']).isNull() + } + + @Test + void 'When mailServer enabled: Includes mail configurations into cluster resources'() { + config.features.mail.active = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + def valuesYaml = parseActualYaml(actualHelmValuesFile) + + assertThat(valuesYaml['argo-cd']['notifications']['enabled']).isEqualTo(true) + assertThat(valuesYaml['argo-cd']['notifications']['notifiers']).isNotNull() + } + + @Test + void 'When emailaddress is set: Include given email addresses into configurations'() { + config.features.mail.active = true + config.features.argocd.emailFrom = 'argocd@example.com' + config.features.argocd.emailToUser = 'app-team@example.com' + config.features.argocd.emailToAdmin = 'argocd@example.com' + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + def valuesYaml = parseActualYaml(actualHelmValuesFile) + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), "cluster-resources.yaml") + def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml') + def defaultYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), 'default.yaml') + + assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)['from']).isEqualTo("argocd@example.com") + assertThat(clusterRessourcesYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('argocd@example.com') + assertThat(argocdYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.on-sync-status-unknown.email']).isEqualTo('argocd@example.com') + assertThat(defaultYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('argocd@example.com') + } + + @Test + void 'When emailaddress is NOT set: Use default email addresses in configurations'() { + config.features.mail.active = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + def valuesYaml = parseActualYaml(actualHelmValuesFile) + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), "cluster-resources.yaml") + def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml') + def defaultYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), 'default.yaml') + + assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)['from']).isEqualTo("argocd@example.org") + assertThat(clusterRessourcesYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('infra@example.org') + assertThat(argocdYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.on-sync-status-unknown.email']).isEqualTo('infra@example.org') + assertThat(defaultYaml['metadata']['annotations']['notifications.argoproj.io/subscribe.email']).isEqualTo('infra@example.org') + } + + @Test + void 'When external Mailserver is set'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + config.features.mail.smtpPort = 1010110 + config.features.mail.smtpUser = 'argo@example.com' + config.features.mail.smtpPassword = '1101:ABCabc&/+*~' + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + + def serviceEmail = new YamlSlurper().parseText(parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['notifiers']['service.email'] as String) + + assertThat(serviceEmail['host']).isEqualTo(config.features.mail.smtpAddress) + assertThat(serviceEmail['port']).isEqualTo(config.features.mail.smtpPort) + // username and password are both linked to the k8s secret. Secrets will be created at runtime, in this test + assertThat(serviceEmail['username']).isEqualTo('$email-username') + assertThat(serviceEmail['password']).isEqualTo('$email-password') + + def createMailSecretCommand = k8sCommands.assertExecuted('kubectl create secret generic argocd-notifications-secret -n argocd') + assertThat(createMailSecretCommand).contains('email-username', config.features.mail.smtpUser as CharSequence) + assertThat(createMailSecretCommand).contains('email-password', config.features.mail.smtpPassword as CharSequence) + } + + @Test + void 'When external emailservers username is set, check if kubernetes secret will be created'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + config.features.mail.smtpUser = 'argo@example.com' + + createArgoCD().install() + + def createMailSecretCommand = k8sCommands.assertExecuted('kubectl create secret generic argocd-notifications-secret -n argocd') + assertThat(createMailSecretCommand).contains('email-username', config.features.mail.smtpUser as CharSequence) + } + + @Test + void 'When external emailservers password is set, check if kubernetes secret will be created'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + config.features.mail.smtpPassword = '1101:ABCabc&/+*~' + + createArgoCD().install() + + def createMailSecretCommand = k8sCommands.assertExecuted('kubectl create secret generic argocd-notifications-secret -n argocd') + assertThat(createMailSecretCommand).contains('email-password', config.features.mail.smtpPassword as CharSequence) + } + + @Test + void 'When external Mailserver is set without port, user, password'() { + config.features.mail.active = true + config.features.mail.smtpAddress = 'smtp.example.com' + + def argocd = createArgoCD() + argocd.install() + + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + + def serviceEmail = new YamlSlurper().parseText(parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['notifiers']['service.email'] as String) + + k8sCommands.assertNotExecuted('kubectl create secret generic argocd-notifications-secret') + + assertThat(serviceEmail['host']).isEqualTo("smtp.example.com") + assertThat(serviceEmail as Map).doesNotContainKey('port') + assertThat(serviceEmail as Map).doesNotContainKey('username') + assertThat(serviceEmail as Map).doesNotContainKey('password') + } + + @Test + void 'When external Mailserver is NOT set'() { + config.features.mail.active = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + def valuesYaml = parseActualYaml(actualHelmValuesFile) + + assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)['port']).isEqualTo(1025) + assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)) doesNotHaveToString('username') + assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)) doesNotHaveToString('password') + } + + @Test + void 'When vault disabled: Does not push path "secrets" to cluster resources'() { + config.features.secrets.active = false + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + assertThat(new File(clusterResourcesRepoLayout.vaultDir())).doesNotExist() + } + + @Test + void 'Prepares repos for air-gapped mode'() { + config.features.monitoring.active = false + config.application.mirrorRepos = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), "cluster-resources.yaml") + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains('http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack') + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain('https://prometheus-community.github.io/helm-charts') + } + + @Test + void 'Pushes repos with empty name-prefix'() { + def argocd = createArgoCD() + argocd.install() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + assertArgoCdYamlPrefixes(clusterResourcesRepo.gitProvider.url, '', clusterResourcesRepoLayout) + } + + @Test + void 'Creates Jenkinsfiles for two registries'() { + config.registry.twoRegistries = true + createArgoCD().install() + + assertJenkinsfileRegistryCredentials() + } + + @Test + void 'Pushes repos with name-prefix'() { + config.application.namePrefix = 'abc-' + + def argocd = createArgoCD() + argocd.install() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + assertArgoCdYamlPrefixes(clusterResourcesRepo.gitProvider.url, config.application.namePrefix, clusterResourcesRepoLayout) + } + + @Test + void 'SecurityContext null in Openshift'() { + config.application.openshift = true + createArgoCD().install() + + for (def petclinicRepo : petClinicRepos) { + if (petclinicRepo.repoTarget.contains('argocd/petclinic-plain')) { + assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/staging/deployment.yaml').text).contains('runAsUser: null') + assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/staging/deployment.yaml').text).contains('runAsGroup: null') + } + if (petclinicRepo.repoTarget.contains('argocd/petclinic-helm')) { + assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).contains('runAsUser: null') + assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).contains('runAsGroup: null') + } + } + } + + @Test + void 'Skips CRDs for argo cd'() { + config.application.skipCrds = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + + assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['crds']['install']).isEqualTo(false) + } + + @Test + void 'Write maven mirror into jenkinsfiles'() { + config.jenkins.mavenCentralMirror = 'http://test' + createArgoCD().install() + + for (def petclinicRepo : petClinicRepos) { + assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, 'Jenkinsfile').text).contains('mvn.useMirrors([name: \'maven-central-mirror\', mirrorOf: \'central\', url: env.MAVEN_CENTRAL_MIRROR])') + } + } + + @Test + void 'ArgoCD with active network policies'() { + config.application.netpols = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + this.actualHelmValuesFile = "${clusterResourcesRepoLayout.helmDir()}/values.yaml" + + assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['global']['networkPolicy']['create']).isEqualTo(true) + assertThat(new File(clusterResourcesRepoLayout.argocdRoot(), '/argocd/values.yaml').text.contains("namespace: monitoring")) + assertThat(new File(clusterResourcesRepoLayout.argocdRoot(), '/argocd/templates/allow-namespaces.yaml').text.contains("namespace: monitoring")) + assertThat(new File(clusterResourcesRepoLayout.argocdRoot(), '/argocd/templates/allow-namespaces.yaml').text.contains("namespace: default")) + } + + private void assertArgoCdYamlPrefixes(String scmmUrl, String expectedPrefix, RepoLayout repoLayout) { + + assertAllYamlFiles(new File(repoLayout.argocdRoot()), 'projects', 3) { Path file -> + def yaml = parseActualYaml(file.toString()) + List sourceRepos = yaml['spec']['sourceRepos'] as List + // Some projects might not have sourceRepos + if (sourceRepos) { + sourceRepos.each { + if (it.startsWith(scmmUrl)) { + assertThat(it) + .as("$file sourceRepos have name prefix") + .startsWith("${scmmUrl}/repo/${expectedPrefix}argocd") + } + } + } + + String metadataNamespace = yaml['metadata']['namespace'] as String + if (metadataNamespace) { + assertThat(metadataNamespace) + .as("$file metadata.namespace has name prefix") + .isEqualTo("${expectedPrefix}argocd".toString()) + } + + List sourceNamespaces = yaml['spec']['sourceNamespaces'] as List + if (sourceNamespaces) { + sourceNamespaces.each { + if (it != '*') { + assertThat(it) + .as("$file spec.sourceNamespace has name prefix") + .startsWith("${expectedPrefix}") + } + } + } + } + + assertAllYamlFiles(new File(repoLayout.argocdRoot()), 'applications', 3) { Path file -> + def yaml = parseActualYaml(file.toString()) + assertThat(yaml['spec']['source']['repoURL'] as String) + .as("$file repoURL have name prefix") + .startsWith("${scmmUrl}/repo/${expectedPrefix}argocd") + + assertThat(yaml['metadata']['namespace']) + .as("$file metadata.namspace has name prefix") + .isEqualTo("${expectedPrefix}argocd".toString()) + + assertThat(yaml['spec']['destination']['namespace']) + .as("$file spec.destination.namspace has name prefix") + .isEqualTo("${expectedPrefix}argocd".toString()) + } + + //checks all other folder for prefixed yaml files except "apps/argocd" + assertAllYamlFiles(new File(repoLayout.rootDir()), 'apps', 9, + ['/apps/argocd/']) { Path it -> + + def yaml = parseActualYaml(it.toString()) + List yamlDocuments = yaml instanceof List ? yaml : [yaml] + for (def document in yamlDocuments) { + if (document && document['kind'] != 'Namespace') { + def metadataNamespace = document['metadata']['namespace'] as String + assertThat(metadataNamespace) + .as("$it metadata.namespace has name prefix") + .startsWith("${expectedPrefix}") + } + } + } + } + + private static void assertAllYamlFiles(File rootDir, + String childDir, + Integer numberOfFiles, + List excludeContains = [], + Closure cl) { + def rootPath = Path.of(rootDir.absolutePath, childDir) + + def yamlFiles = Files.walk(rootPath) + .filter { Files.isRegularFile(it) } + .filter { Path p -> + def s = p.toString().replace('\\', '/') + (s.endsWith('.yaml') || s.endsWith('.yml')) && !excludeContains.any { ex -> s.contains(ex) } + } + .collect(Collectors.toList()) + + yamlFiles.each(cl) + + assertThat(yamlFiles.size()).isEqualTo(numberOfFiles) + } + + private static List findFilesContaining(File folder, String stringToSearch) { + List result = [] + folder.eachFileRecurse(FileType.FILES) { + if (it.text.contains(stringToSearch)) { + result += it + } + } + return result + } + + ArgoCD createArgoCD() { + def argoCD = ArgoCDForTest.newWithAutoProviders(config, k8sCommands, helmCommands) + return argoCD + } + + void assertJenkinsfileRegistryCredentials() { + List defaultRegistryExpectedLines = ['String pathPrefix = !dockerRegistryPath?.trim() ? "" : "${dockerRegistryPath}/"', + 'imageName = "${dockerRegistryBaseUrl}/${pathPrefix}${application}:${imageTag}"'] + List twoRegistriesExpectedLines = ['String proxyPathPrefix = !dockerRegistryProxyPath?.trim() ? "" : "${dockerRegistryProxyPath}/"', + 'docker.withRegistry("https://${dockerRegistryProxyBaseUrl}/${proxyPathPrefix}", dockerRegistryProxyCredentials) {',] + + for (def petclinicRepo : petClinicRepos) { + String jenkinsfile = new File(petclinicRepo.absoluteLocalRepoTmpDir, 'Jenkinsfile').text + + defaultRegistryExpectedLines.each { expectedEnvVar -> assertThat(jenkinsfile).contains(expectedEnvVar) + } + + if (config.registry['twoRegistries']) { + twoRegistriesExpectedLines.each { expectedEnvVar -> assertThat(jenkinsfile).contains(expectedEnvVar) + } + } else { + twoRegistriesExpectedLines.each { expectedEnvVar -> assertThat(jenkinsfile).doesNotContain(expectedEnvVar) + } + } + } + } + + @Test + void 'Prepares ArgoCD repo with Operator configuration file'() { + def argocd = setupOperatorTest() + + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + def rbacConfigPath = Path.of(clusterResourcesRepoLayout.operatorRbacDir()) + + assertThat(argocdConfigPath.toFile()).exists() + assertThat(rbacConfigPath.toFile()).exists() + + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + assertThat(yaml['apiVersion']).isEqualTo('argoproj.io/v1beta1') + assertThat(yaml['kind']).isEqualTo('ArgoCD') + } + + @Test + void 'No files for operator when operator is false'() { + def argocd = createArgoCD() + + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + def rbacConfigPath = Path.of(clusterResourcesRepoLayout.operatorRbacDir()) + + assertThat(argocdConfigPath.toFile()).doesNotExist() + assertThat(rbacConfigPath.toFile()).doesNotExist() + } + + @Test + void 'Deploys with operator without OpenShift configuration'() { + def argocd = setupOperatorTest(openshift: false) + + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + + k8sCommands.assertExecuted("kubectl apply -f ${argocdConfigPath}") + + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + assertThat(yaml['spec']['rbac']).isNull() + assertThat(yaml['spec']['sso']).isNull() + + def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.applicationsDir(), 'argocd.yaml') + assertThat(argocdYaml['spec']['source']['directory']['recurse'] as Boolean).isTrue() + assertThat(argocdYaml['spec']['source']['path']).isEqualTo('apps/argocd/operator/') + // Here we should assert all <#if argocd.isOperator> in YAML 😐️ + } + + @Test + void 'RBACs with operator using RbacDefinition outputs'() { + config.application.namePrefix = "testPrefix-" + + LinkedHashSet expectedNamespaces = ["testPrefix-monitoring", + "testPrefix-secrets", + "testPrefix-traefik", + "testPrefix-example-apps-staging", + "testPrefix-example-apps-production"] + // have to prepare activeNamespaces for unit-test, Application.groovy is setting this in integration way + config.application.namespaces.dedicatedNamespaces = new LinkedHashSet(["monitoring", + "secrets", + "traefik", + "example-apps-staging", + "example-apps-production"]) + + def argocd = setupOperatorTest(openshift: false) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + File rbacPath = Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile() + + expectedNamespaces.each { String ns -> + File roleFile = new File(rbacPath, "role-argocd-${ns}.yaml") + File bindingFile = new File(rbacPath, "rolebinding-argocd-${ns}.yaml") + + assertThat(roleFile).exists() + assertThat(bindingFile).exists() - expectedNamespaces.each { String ns -> - File roleFile = new File(rbacPath, "role-argocd-${ns}.yaml") - File bindingFile = new File(rbacPath, "rolebinding-argocd-${ns}.yaml") + Map roleYaml = new YamlSlurper().parse(roleFile) as Map + Map bindingYaml = new YamlSlurper().parse(bindingFile) as Map - assertThat(roleFile).exists() - assertThat(bindingFile).exists() + assertThat(roleYaml["kind"]).isEqualTo("Role") + assertThat(roleYaml["metadata"]["name"]).isEqualTo("argocd") + assertThat(roleYaml["metadata"]["namespace"]).isEqualTo(ns) - Map roleYaml = new YamlSlurper().parse(roleFile) as Map - Map bindingYaml = new YamlSlurper().parse(bindingFile) as Map + assertThat(bindingYaml["kind"]).isEqualTo("RoleBinding") + assertThat(bindingYaml["metadata"]["name"]).isEqualTo("argocd") + assertThat(bindingYaml["metadata"]["namespace"]).isEqualTo(ns) - assertThat(roleYaml["kind"]).isEqualTo("Role") - assertThat(roleYaml["metadata"]["name"]).isEqualTo("argocd") - assertThat(roleYaml["metadata"]["namespace"]).isEqualTo(ns) + List> subjects = bindingYaml["subjects"] as List> + assertThat(subjects).isNotEmpty() + assertThat(subjects*.kind).containsOnly("ServiceAccount") + assertThat(subjects*.namespace).containsOnly("testPrefix-argocd") + assertThat(subjects*.name).containsExactlyInAnyOrder("argocd-argocd-server", + "argocd-argocd-application-controller", + "argocd-applicationset-controller") - assertThat(bindingYaml["kind"]).isEqualTo("RoleBinding") - assertThat(bindingYaml["metadata"]["name"]).isEqualTo("argocd") - assertThat(bindingYaml["metadata"]["namespace"]).isEqualTo(ns) + Map roleRef = bindingYaml["roleRef"] as Map + assertThat(roleRef).isNotNull() + assertThat(roleRef["name"]).isEqualTo("argocd") + assertThat(roleRef["kind"]).isEqualTo("Role") + } + } - List> subjects = bindingYaml["subjects"] as List> - assertThat(subjects).isNotEmpty() - assertThat(subjects*.kind).containsOnly("ServiceAccount") - assertThat(subjects*.namespace).containsOnly("testPrefix-argocd") - assertThat(subjects*.name).containsExactlyInAnyOrder( - "argocd-argocd-server", - "argocd-argocd-application-controller", - "argocd-applicationset-controller" - ) + @Test + void 'Deploys with operator with OpenShift configuration'() { + def argocd = setupOperatorTest(openshift: true) - Map roleRef = bindingYaml["roleRef"] as Map - assertThat(roleRef).isNotNull() - assertThat(roleRef["name"]).isEqualTo("argocd") - assertThat(roleRef["kind"]).isEqualTo("Role") - } - } + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + k8sCommands.assertExecuted("kubectl apply -f ${argocdConfigPath}") - @Test - void 'Deploys with operator with OpenShift configuration'() { - def argocd = setupOperatorTest(openshift: true) + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + assertThat(yaml['spec']['sso']).isNotNull() + assertThat(yaml['spec']['sso']['dex']['openShiftOAuth']).isEqualTo(true) + assertThat(yaml['spec']['sso']['provider']).isEqualTo('dex') + assertThat(yaml['spec']['rbac']).isNotNull() + assertThat(yaml['spec']['server']['route']['enabled']).isEqualTo(true) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + k8sCommands.assertNotExecuted("kubectl patch service argocd-server -n argocd") + } - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - k8sCommands.assertExecuted("kubectl apply -f ${argocdConfigPath}") + @Test + void 'check if external_secrets_io and monitoring_coreos_com is set'() { - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - assertThat(yaml['spec']['sso']).isNotNull() - assertThat(yaml['spec']['sso']['dex']['openShiftOAuth']).isEqualTo(true) - assertThat(yaml['spec']['sso']['provider']).isEqualTo('dex') - assertThat(yaml['spec']['rbac']).isNotNull() - assertThat(yaml['spec']['server']['route']['enabled']).isEqualTo(true) + config.features.monitoring.active = true + config.features.secrets.active = true - k8sCommands.assertNotExecuted("kubectl patch service argocd-server -n argocd") - } + String expectedMonitoring = 'monitoring.coreos.com' + String expectedExternalSecret = 'external-secrets.io' - @Test - void 'check if external_secrets_io and monitoring_coreos_com is set'() { + def argocd = setupOperatorTest(openshift: true) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - config.features.monitoring.active = true - config.features.secrets.active = true + def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) - String expectedMonitoring = 'monitoring.coreos.com' - String expectedExternalSecret = 'external-secrets.io' - - def argocd = setupOperatorTest(openshift: true) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) + def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String - def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String + assertThat(resourceInclusionsString.contains(expectedMonitoring)).isTrue() + assertThat(resourceInclusionsString.contains(expectedExternalSecret)).isTrue() + } - assertThat(resourceInclusionsString.contains(expectedMonitoring)).isTrue() - assertThat(resourceInclusionsString.contains(expectedExternalSecret)).isTrue() - } + @Test + void 'check if external_secrets_io and monitoring_coreos_com is not set'() { - @Test - void 'check if external_secrets_io and monitoring_coreos_com is not set'() { + config.features.monitoring.active = false + config.features.secrets.active = false - config.features.monitoring.active = false - config.features.secrets.active = false + String expectedMonitoring = 'monitoring.coreos.com' + String expectedExternalSecret = 'external-secrets.io' - String expectedMonitoring = 'monitoring.coreos.com' - String expectedExternalSecret = 'external-secrets.io' + def argocd = setupOperatorTest(openshift: true) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - def argocd = setupOperatorTest(openshift: true) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) - def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) + def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String - def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String + assertThat(resourceInclusionsString.contains(expectedMonitoring)).isFalse() + assertThat(resourceInclusionsString.contains(expectedExternalSecret)).isFalse() + } - assertThat(resourceInclusionsString.contains(expectedMonitoring)).isFalse() - assertThat(resourceInclusionsString.contains(expectedExternalSecret)).isFalse() - } + @Test + void 'Correctly sets resourceInclusions from config'() { + def argocd = setupOperatorTest() + // Set the config to a custom resourceInclusionsCluster value + config.features.argocd.resourceInclusionsCluster = 'https://192.168.0.1:6443' - @Test - void 'Correctly sets resourceInclusions from config'() { - def argocd = setupOperatorTest() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - // Set the config to a custom resourceInclusionsCluster value - config.features.argocd.resourceInclusionsCluster = 'https://192.168.0.1:6443' + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + def expectedClusterUrl = 'https://192.168.0.1:6443' - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + // Retrieve and parse the resourceInclusions string into structured YAML + def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String + def parsedResourceInclusions = new YamlSlurper().parseText(resourceInclusionsString) - def expectedClusterUrl = 'https://192.168.0.1:6443' + // Iterate over the parsed resource inclusions and check the 'clusters' field + parsedResourceInclusions.each { resource -> + assertThat(resource as Map).containsKey('clusters') + assertThat(resource['clusters'] as List).contains(expectedClusterUrl) + } + } - // Retrieve and parse the resourceInclusions string into structured YAML - def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String - def parsedResourceInclusions = new YamlSlurper().parseText(resourceInclusionsString) + @Test + void 'resourceInclusionsCluster from config file trumps ENVs'() { + def argocd = setupOperatorTest() - // Iterate over the parsed resource inclusions and check the 'clusters' field - parsedResourceInclusions.each { resource -> - assertThat(resource as Map).containsKey('clusters') - assertThat(resource['clusters'] as List).contains(expectedClusterUrl) - } - } + // Set the config to a custom internalKubernetesApiUrl value + config.application.internalKubernetesApiUrl = 'https://192.168.0.1:6443' - @Test - void 'resourceInclusionsCluster from config file trumps ENVs'() { - def argocd = setupOperatorTest() + // Set environment variables for Kubernetes API server + withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "100.125.0.1") + .and("KUBERNETES_SERVICE_PORT", "443") + .execute { + argocd.install() + } - // Set the config to a custom internalKubernetesApiUrl value - config.application.internalKubernetesApiUrl = 'https://192.168.0.1:6443' + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - // Set environment variables for Kubernetes API server - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "100.125.0.1") - .and("KUBERNETES_SERVICE_PORT", "443") - .execute { - argocd.install() - } + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + def expectedClusterUrlFromConfig = "https://192.168.0.1:6443" - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + // Retrieve and parse the resourceInclusions string into structured YAML + def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String + def parsedResourceInclusions = new YamlSlurper().parseText(resourceInclusionsString) - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - def expectedClusterUrlFromConfig = "https://192.168.0.1:6443" + // Ensure that the clusters field uses the config value, not the env variables + parsedResourceInclusions.each { resource -> + assertThat(resource as Map).containsKey('clusters') + assertThat(resource['clusters'] as List).contains(expectedClusterUrlFromConfig) + // Make sure the environment variable value does not appear + assertThat(resource['clusters'] as List).doesNotContain("https://100.125.0.1:443") + } + } - // Retrieve and parse the resourceInclusions string into structured YAML - def resourceInclusionsString = yaml['spec']['resourceInclusions'] as String - def parsedResourceInclusions = new YamlSlurper().parseText(resourceInclusionsString) + @Test + void 'Sets env variables in ArgoCD components when provided'() { + def argocd = setupOperatorTest() - // Ensure that the clusters field uses the config value, not the env variables - parsedResourceInclusions.each { resource -> - assertThat(resource as Map).containsKey('clusters') - assertThat(resource['clusters'] as List).contains(expectedClusterUrlFromConfig) - // Make sure the environment variable value does not appear - assertThat(resource['clusters'] as List).doesNotContain("https://100.125.0.1:443") - } - } + // Set environment variables for ArgoCD + config.features.argocd.env = [[name: "ENV_VAR_1", value: "value1"], + [name: "ENV_VAR_2", value: "value2"]] as List - @Test - void 'Sets env variables in ArgoCD components when provided'() { - def argocd = setupOperatorTest() - - // Set environment variables for ArgoCD - config.features.argocd.env = [ - [name: "ENV_VAR_1", value: "value1"], - [name: "ENV_VAR_2", value: "value2"] - ] as List - - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - - def expectedEnv = [ - [name: "ENV_VAR_1", value: "value1"], - [name: "ENV_VAR_2", value: "value2"] - ] - - // Check that the env variables are added to the relevant components - assertThat(yaml['spec']['applicationSet']['env']).isEqualTo(expectedEnv) - assertThat(yaml['spec']['notifications']['env']).isEqualTo(expectedEnv) - assertThat(yaml['spec']['controller']['env']).isEqualTo(expectedEnv) - assertThat(yaml['spec']['repo']['env']).isEqualTo(expectedEnv) - assertThat(yaml['spec']['server']['env']).isEqualTo(expectedEnv) - } - - @Test - void 'Does not set env variables when none are provided'() { - def argocd = setupOperatorTest() - - // Ensure env is an empty list (default) - config.features.argocd.env = [] - - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - - // Check that the env variables are not present - assertThat(yaml['spec']['applicationSet'] as Map).doesNotContainKey('env') - assertThat(yaml['spec']['notifications'] as Map).doesNotContainKey('env') - assertThat(yaml['spec']['controller'] as Map).doesNotContainKey('env') - assertThat(yaml['spec']['redis'] as Map).doesNotContainKey('env') - assertThat(yaml['spec']['repo'] as Map).doesNotContainKey('env') - assertThat(yaml['spec']['server'] as Map).doesNotContainKey('env') - } - - @Test - void 'Sets single env variable in ArgoCD components when provided'() { - def argocd = setupOperatorTest() - - // Set a single environment variable for ArgoCD - config.features.argocd.env = [ - [name: "ENV_VAR_SINGLE", value: "singleValue"] - ] as List - - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) - def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) - - def expectedEnv = [ - [name: "ENV_VAR_SINGLE", value: "singleValue"] - ] - - // Check that the single env variable is added to the relevant components - assertThat(yaml['spec']['applicationSet']['env']).isEqualTo(expectedEnv) - assertThat(yaml['spec']['notifications']['env']).isEqualTo(expectedEnv) - assertThat(yaml['spec']['controller']['env']).isEqualTo(expectedEnv) - assertThat(yaml['spec']['server']['env']).isEqualTo(expectedEnv) - } - - @Test - void 'Creates all necessary namespaces'() { - def argoCD = createArgoCD() - simulateNamespaceCreation() - argoCD.install() - - config.application.namespaces.getActiveNamespaces().each { namespace -> - k8sCommands.assertExecuted("kubectl create namespace ${namespace}") - } - } - - @Test - void 'Operator config sets server insecure to true when insecure is set'() { - config.application.insecure = true - def argocd = setupOperatorTest() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) - assertThat(yaml['spec']['server']['insecure']).isEqualTo(true) - } - - @Test - void 'Operator config sets custom values'() { - config.features.argocd.values = [key: 'value'] - config.features.argocd.values = [spec: [key: 'value']] - def argocd = setupOperatorTest() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) - assertThat(yaml['spec']['key']).isEqualTo('value') - } - - @Test - void 'Operator config sets server_insecure to false when insecure is not set'() { - def argocd = setupOperatorTest() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) - assertThat(yaml['spec']['server']['insecure']).isEqualTo(false) - } - - @Test - void 'Generates correct ingress yaml with expected host when insecure is true and not on OpenShift'() { - config.application.insecure = true - config.features.argocd.url = "http://argocd.localhost" - def argocd = setupOperatorTest(openshift: false) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") - assertThat(ingressFile) - .as("Ingress file should be generated for insecure mode on non-OpenShift") - .exists() - - def ingressYaml = parseActualYaml(ingressFile.toString()) - - def rules = ingressYaml['spec']['rules'] as List - def host = rules[0]['host'] - assertThat(host) - .as("Ingress host should match configured ArgoCD hostname") - .isEqualTo(new URL(config.features.argocd.url).host) - } - - @Test - void 'Does not generate ingress yaml when insecure is false'() { - config.application.insecure = false - def argocd = setupOperatorTest(openshift: false) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") - assertThat(ingressFile) - .as("Ingress file should not be generated when insecure is false") - .doesNotExist() - } - - @Test - void 'Does not generate ingress yaml when running on OpenShift'() { - config.application.insecure = true - def argocd = setupOperatorTest(openshift: true) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") - assertThat(ingressFile) - .as("Ingress file should not be generated on OpenShift") - .doesNotExist() - } - - @Test - void 'Does not generate ingress yaml when insecure is false and OpenShift is true'() { - config.application.insecure = false - def argocd = setupOperatorTest(openshift: true) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") - assertThat(ingressFile) - .as("Ingress file should not be generated when both flags are false") - .doesNotExist() - } - - @Test - void 'Central Bootstrapping for Tenant Applications'() { - setupDedicatedInstanceMode() - - def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") - assertThat(ingressFile) - .as("Ingress file should not be generated when both flags are false") - .doesNotExist() - } - - @Test - void 'GOP DedicatedInstances Central templating works correctly'() { - setupDedicatedInstanceMode() - //Central Applications - assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/argocd.yaml")).exists() - assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/bootstrap.yaml")).exists() - assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/projects.yaml")).exists() - assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/example-apps.yaml")).doesNotExist() - - def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/applications/argocd.yaml") - def bootstrapYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/applications/bootstrap.yaml") - def projectsYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/applications/projects.yaml") - - assertThat(argocdYaml['metadata']['name']).isEqualTo('testPrefix-argocd') - assertThat(argocdYaml['metadata']['namespace']).isEqualTo('argocd') - assertThat(argocdYaml['spec']['project']).isEqualTo('testPrefix') - assertThat(argocdYaml['spec']['source']['path']).isEqualTo('apps/argocd/operator/') - - assertThat(bootstrapYaml['metadata']['name']).isEqualTo('testPrefix-bootstrap') - assertThat(bootstrapYaml['metadata']['namespace']).isEqualTo('argocd') - assertThat(bootstrapYaml['spec']['project']).isEqualTo('testPrefix') - assertThat(bootstrapYaml['spec']['source']['repoURL']).isEqualTo("scmm.testhost/scm/repo/testPrefix-argocd/cluster-resources.git") - - assertThat(projectsYaml['metadata']['name']).isEqualTo('testPrefix-projects') - assertThat(projectsYaml['metadata']['namespace']).isEqualTo('argocd') - assertThat(projectsYaml['spec']['project']).isEqualTo('testPrefix') - - //Central Project - assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/projects/tenant.yaml")).exists() - - def tenantProject = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/projects/tenant.yaml") - - assertThat(tenantProject['metadata']['name']).isEqualTo('testPrefix') - assertThat(tenantProject['metadata']['namespace']).isEqualTo('argocd') - def sourceRepos = (List) tenantProject['spec']['sourceRepos'] - assertThat(sourceRepos[0]).isEqualTo('scmm.testhost/scm/repo/testPrefix-argocd/cluster-resources.git') - } - - @Test - void 'Append namespaces to Argocd argocd-default-cluster-config secrets'() { - config.application.namespaces.dedicatedNamespaces = new LinkedHashSet(['dedi-test1', 'dedi-test2', 'dedi-test3']) - config.application.namespaces.tenantNamespaces = new LinkedHashSet(['tenant-test1', 'tenant-test2', 'tenant-test3']) - setupDedicatedInstanceMode() - k8sCommands.assertExecuted('kubectl get secret argocd-default-cluster-config -n argocd -ojsonpath={.data.namespaces}') - k8sCommands.assertExecuted('kubectl patch secret argocd-default-cluster-config -n argocd --patch-file=/tmp/gitops-playground-patch-') - } - - @Test - void 'multiTenant folder gets deleted correctly if not in dedicated mode'() { - config.multiTenant.useDedicatedInstance = false - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'multiTenant/')).doesNotExist() - assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'applications/')).exists() - assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'projects/')).exists() - } - - @Test - void 'deleting unused folder in dedicated mode'() { - config.multiTenant.useDedicatedInstance = true - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'multiTenant/')).doesNotExist() - assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'applications/')).exists() - assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'projects/')).exists() - } - - @Test - void 'RBACs generated correctly'() { - config.application.namespaces.tenantNamespaces = new LinkedHashSet(['testprefix-tenant-test1', 'testprefix-tenant-test2', 'testprefix-tenant-test3']) - setupDedicatedInstanceMode() - - File rbacFolder = new File(clusterResourcesRepoLayout.operatorRbacDir()) - File rbacTenantFolder = new File(clusterResourcesRepoLayout.operatorRbacDir() + "/tenant") - assertThat(rbacFolder).exists() - assertThat(rbacTenantFolder).exists() - - assertThat(rbacFolder.listFiles().count { it.isFile() }).isEqualTo(14) - assertThat(rbacTenantFolder.listFiles().count { it.isFile() }).isEqualTo(6) - - rbacFolder.eachFile { file -> - if (file.name.startsWith("role-") && file.name.contains('dedi')) { - def rbacFile = new YamlSlurper().parse(Path.of file.path) - assertThat(rbacFile['metadata']['namespace']).isIn(config.application.namespaces.getActiveNamespaces()) - } - if (file.name.startsWith("rolebinding-") && file.name.contains('dedi')) { - def rbacFile = new YamlSlurper().parse(Path.of file.path) - assertThat(rbacFile['subjects']['namespace']).isEqualTo(["argocd", "argocd", "argocd"]) - } - } - - rbacTenantFolder.eachFile { file -> - if (file.name.startsWith("role-")) { - def rbacFile = new YamlSlurper().parse(Path.of file.path) - assertThat(rbacFile['metadata']['namespace']).isIn(config.application.namespaces.tenantNamespaces) - } - - if (file.name.startsWith("rolebinding-")) { - def rbacFile = new YamlSlurper().parse(Path.of file.path) - assertThat(rbacFile['subjects']['namespace']).isEqualTo(["testPrefix-argocd", "testPrefix-argocd", "testPrefix-argocd"]) - } - } - - } - - @Test - void 'Operator RBAC includes node access rules when not on OpenShift'() { - config.application.namePrefix = "testprefix-" - - def argocd = setupOperatorTest(openshift: false) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - print config.toMap() - - File rbacDir = Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile() - File roleFile = new File(rbacDir, "role-argocd-testprefix-monitoring.yaml") - - Map yaml = new YamlSlurper().parse(roleFile) as Map - List> rules = yaml["rules"] as List> - - assertThat(rules).anyMatch { rule -> - List resources = rule["resources"] as List - resources.contains("nodes") && resources.contains("nodes/metrics") - } - } - - @Test - void 'Operator RBAC does not include node access rules when on OpenShift'() { - config.application.namePrefix = "testprefix-" - - def argocd = setupOperatorTest(openshift: true) - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - File rbacDir = Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile() - File roleFile = new File(rbacDir, "role-argocd-testprefix-monitoring.yaml") - println roleFile - - Map yaml = new YamlSlurper().parse(roleFile) as Map - List> rules = yaml["rules"] as List> - - assertThat(rules).noneMatch { rule -> - List resources = rule["resources"] as List - resources.contains("nodes") && resources.contains("nodes/metrics") - } - } - - @Test - void 'If not using mirror, ensure source repos in cluster-resources got right URL'() { - config.application.mirrorRepos = false - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') - clusterRessourcesYaml['spec']['sourceRepos'] - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'https://charts.external-secrets.io', - 'https://codecentric.github.io/helm-charts', - 'https://prometheus-community.github.io/helm-charts', - 'https://traefik.github.io/charts', - 'https://helm.releases.hashicorp.com', - 'https://charts.jetstack.io' - ) - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager' - ) - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/traefik.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git' - ) - } - - @Test - void 'If using mirror, ensure source repos in cluster-resources got right URL'() { - config.application.mirrorRepos = true - - def argocd = createArgoCD() - argocd.install() - - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') - clusterRessourcesYaml['spec']['sourceRepos'] - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', - 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', - 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', - 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', - 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager' - - ) - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/traefik.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git' - ) - } - - @Test - void 'If using mirror with GitLab, ensure source repos in cluster-resources got right URL'() { - config.application.mirrorRepos = true - config.scm.scmProviderType = 'GITLAB' - config.scm.gitlab.url = 'https://testGitLab.com/testgroup' - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') - clusterRessourcesYaml['spec']['sourceRepos'] - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'https://testGitLab.com/testgroup/3rd-party-dependencies/kube-prometheus-stack.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/traefik.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/external-secrets.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/vault.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/cert-manager.git' - ) - } - - @Test - void 'If using mirror with GitLab with prefix, ensure source repos in cluster-resources got right URL'() { - config.application.mirrorRepos = true - config.scm.scmProviderType = 'GITLAB' - config.scm.gitlab.url = "https://testGitLab.com/testgroup" - config.application.namePrefix = 'test1-' - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') - clusterRessourcesYaml['spec']['sourceRepos'] - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'https://testGitLab.com/testgroup/3rd-party-dependencies/kube-prometheus-stack.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/traefik.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/external-secrets.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/vault.git', - 'https://testGitLab.com/testgroup/3rd-party-dependencies/cert-manager.git' - ) - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager' - ) - } - - @Test - void 'If using mirror with name-prefix, ensure source repos in cluster-resources got right URL'() { - config.application.mirrorRepos = true - config.application.namePrefix = 'test1-' - - def argocd = createArgoCD() - argocd.install() - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') - clusterRessourcesYaml['spec']['sourceRepos'] - - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager' - ) - - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/traefik.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git' - ) - } - - void setupDedicatedInstanceMode() { - config.application.namePrefix = 'testPrefix-' - config.multiTenant.scmManager.url = 'scmm.testhost/scm' - config.multiTenant.scmManager.username = 'testUserName' - config.multiTenant.scmManager.password = 'testPassword' - config.multiTenant.useDedicatedInstance = true - this.argocd = setupOperatorTest() - argocd.install() - this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo - clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() - - } - - protected ArgoCD setupOperatorTest(Map options = [:]) { - config.features.argocd.operator = true - config.features.argocd.resourceInclusionsCluster = 'https://192.168.0.1:6443' - config.application.openshift = options.openshift ?: false - - def argoCD = createArgoCD() - - if (config.multiTenant.useDedicatedInstance) { - config.content.repos ? setupMockResponsesFor(MockReponses.MULTI_TENANT_WITH_EXAMPLES) : setupMockResponsesFor(MockReponses.MULTI_TENANT) - } else { - setupMockResponsesFor(MockReponses.SINGLE_TENANT) - } - - return argoCD - } - - enum MockReponses { - SINGLE_TENANT, - MULTI_TENANT, - MULTI_TENANT_WITH_EXAMPLES - } - - //Mock Responses for Testing - void setupMockResponsesFor(MockReponses mockReponses) { - switch (mockReponses) { - case MockReponses.SINGLE_TENANT -> { - k8sCommands.enqueueOutputs([ - queueUpAllNamespacesExist(), - new CommandExecutor.Output('', '', 0), // Monitoring CRDs applied - new CommandExecutor.Output('', '', 0), // ArgoCD Secret applied - new CommandExecutor.Output('', '', 0), // Labeling ArgoCD Secret - new CommandExecutor.Output('', '', 0), // ArgoCD operator YAML applied - new CommandExecutor.Output('', 'Available', 0), // ArgoCD resource reached desired phase - ].flatten() as Queue) - } - case MockReponses.MULTI_TENANT_WITH_EXAMPLES -> mockReponseMultiTenant() - case MockReponses.MULTI_TENANT -> mockReponseMultiTenant() - } - } - - void mockReponseMultiTenant() { - k8sCommands.enqueueOutputs([ - queueUpAllNamespacesExist(), - new CommandExecutor.Output('', '', 0), // Monitoring CRDs applied - - new CommandExecutor.Output('', '', 0), // ArgoCD SCM Secret applied - new CommandExecutor.Output('', '', 0), // Labeling ArgoCD SCM Secret - new CommandExecutor.Output('', '', 0), // ArgoCD SCM central Secret applied - new CommandExecutor.Output('', '', 0), // Labeling ArgoCD central SCM Secret - - new CommandExecutor.Output('', '', 0), // ArgoCD operator YAML applied - new CommandExecutor.Output('', 'Available', 0), // ArgoCD resource reached desired phase - - new CommandExecutor.Output('', '', 0), // ArgoCD argocd-cluster password secret - new CommandExecutor.Output('', '', 0), // ArgoCD argocd-secret - - new CommandExecutor.Output('', '', 0), // argocd-default-cluster-config patched - new CommandExecutor.Output('', '', 0), // ArgoCD argocd-secret - new CommandExecutor.Output('', 'dGVzdG5hbWVzcGFjZTEsdGVzdG5hbWVzcGFjZTI=', 0), // getting argocd-default-cluster-config from central Argocd - new CommandExecutor.Output('', '', 0), // setting argocd-default-cluster-config from central Argocd - ].flatten() as Queue) - } - - private void simulateNamespaceCreation() { - Queue outputs = new LinkedList() - config.application.namespaces.getActiveNamespaces().each { namespace -> - outputs.add(new CommandExecutor.Output("${namespace} not found", "", 1)) - outputs.add(new CommandExecutor.Output("${namespace} created", "", 0)) - } - k8sCommands.enqueueOutputs(outputs) - } - - private Queue queueUpAllNamespacesExist() { - return new LinkedList( - config.application.namespaces.getActiveNamespaces().collect { namespace -> new CommandExecutor.Output(namespace, "", 0) } - ) - } - - private static void mockPrefixActiveNamespaces(Config config) { - def prefix = config.application.namePrefix ?: "" - - config.application.namespaces.with { - dedicatedNamespaces = new LinkedHashSet<>( - dedicatedNamespaces.collect { (prefix + it).toString() } - ) - tenantNamespaces = new LinkedHashSet<>( - tenantNamespaces.collect { (prefix + it).toString() } - ) - } - } - - static class ArgoCDForTest extends ArgoCD { - final Config cfg - final GitProvider tenantProvider - final GitProvider centralProvider - - static ArgoCDForTest newWithAutoProviders(Config cfg, - CommandExecutorForTest k8sCommands, - CommandExecutorForTest helmCommands) { - def provider = TestGitProvider.buildProviders(cfg) - return new ArgoCDForTest( - cfg, - k8sCommands, - helmCommands, - provider.tenant as GitProvider, - provider.central as GitProvider - ) - } - - ArgoCDForTest(Config cfg, - CommandExecutorForTest k8sCommands, - CommandExecutorForTest helmCommands, - GitProvider tenantProvider, - GitProvider centralProvider) { - super( - cfg, - new K8sClientForTest(cfg, k8sCommands), - new HelmClient(helmCommands), - new FileSystemUtils(), - new TestGitRepoFactory(cfg, new FileSystemUtils()), - new GitHandlerForTests(cfg, tenantProvider, centralProvider) - ) - this.cfg = cfg - this.tenantProvider = tenantProvider - this.centralProvider = centralProvider - mockPrefixActiveNamespaces(cfg) - } - - GitRepo getClusterResourcesRepo() { - return getRepoSetup().clusterResources?.repo - } - - RepoLayout getClusterRepoLayout() { - return getRepoSetup().clusterRepoLayout() - } - - } - - private Map parseActualYaml(String pathToYamlFile) { - File yamlFile = new File(pathToYamlFile) - def ys = new YamlSlurper() - return ys.parse(yamlFile) as Map - } - -} + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + + def expectedEnv = [[name: "ENV_VAR_1", value: "value1"], + [name: "ENV_VAR_2", value: "value2"]] + + // Check that the env variables are added to the relevant components + assertThat(yaml['spec']['applicationSet']['env']).isEqualTo(expectedEnv) + assertThat(yaml['spec']['notifications']['env']).isEqualTo(expectedEnv) + assertThat(yaml['spec']['controller']['env']).isEqualTo(expectedEnv) + assertThat(yaml['spec']['repo']['env']).isEqualTo(expectedEnv) + assertThat(yaml['spec']['server']['env']).isEqualTo(expectedEnv) + } + + @Test + void 'Does not set env variables when none are provided'() { + def argocd = setupOperatorTest() + + // Ensure env is an empty list (default) + config.features.argocd.env = [] + + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + + // Check that the env variables are not present + assertThat(yaml['spec']['applicationSet'] as Map).doesNotContainKey('env') + assertThat(yaml['spec']['notifications'] as Map).doesNotContainKey('env') + assertThat(yaml['spec']['controller'] as Map).doesNotContainKey('env') + assertThat(yaml['spec']['redis'] as Map).doesNotContainKey('env') + assertThat(yaml['spec']['repo'] as Map).doesNotContainKey('env') + assertThat(yaml['spec']['server'] as Map).doesNotContainKey('env') + } + + @Test + void 'Sets single env variable in ArgoCD components when provided'() { + def argocd = setupOperatorTest() + + // Set a single environment variable for ArgoCD + config.features.argocd.env = [[name: "ENV_VAR_SINGLE", value: "singleValue"]] as List + + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def argocdConfigPath = Path.of(clusterResourcesRepoLayout.operatorConfigFile()) + def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) + + def expectedEnv = [[name: "ENV_VAR_SINGLE", value: "singleValue"]] + + // Check that the single env variable is added to the relevant components + assertThat(yaml['spec']['applicationSet']['env']).isEqualTo(expectedEnv) + assertThat(yaml['spec']['notifications']['env']).isEqualTo(expectedEnv) + assertThat(yaml['spec']['controller']['env']).isEqualTo(expectedEnv) + assertThat(yaml['spec']['server']['env']).isEqualTo(expectedEnv) + } + + @Test + void 'Creates all necessary namespaces'() { + def argoCD = createArgoCD() + simulateNamespaceCreation() + argoCD.install() + + config.application.namespaces.getActiveNamespaces().each { namespace -> k8sCommands.assertExecuted("kubectl create namespace ${namespace}") + } + } + + @Test + void 'Operator config sets server insecure to true when insecure is set'() { + config.application.insecure = true + def argocd = setupOperatorTest() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) + assertThat(yaml['spec']['server']['insecure']).isEqualTo(true) + } + + @Test + void 'Operator config sets custom values'() { + config.features.argocd.values = [key: 'value'] + config.features.argocd.values = [spec: [key: 'value']] + def argocd = setupOperatorTest() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) + assertThat(yaml['spec']['key']).isEqualTo('value') + } + + @Test + void 'Operator config sets server_insecure to false when insecure is not set'() { + def argocd = setupOperatorTest() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def yaml = parseActualYaml(Path.of(clusterResourcesRepoLayout.operatorConfigFile()).toString()) + assertThat(yaml['spec']['server']['insecure']).isEqualTo(false) + } + + @Test + void 'Generates correct ingress yaml with expected host when insecure is true and not on OpenShift'() { + config.application.insecure = true + config.features.argocd.url = "http://argocd.localhost" + def argocd = setupOperatorTest(openshift: false) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should be generated for insecure mode on non-OpenShift") + .exists() + + def ingressYaml = parseActualYaml(ingressFile.toString()) + + def rules = ingressYaml['spec']['rules'] as List + def host = rules[0]['host'] + assertThat(host) + .as("Ingress host should match configured ArgoCD hostname") + .isEqualTo(new URL(config.features.argocd.url).host) + } + + @Test + void 'Does not generate ingress yaml when insecure is false'() { + config.application.insecure = false + def argocd = setupOperatorTest(openshift: false) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should not be generated when insecure is false") + .doesNotExist() + } + + @Test + void 'Does not generate ingress yaml when running on OpenShift'() { + config.application.insecure = true + def argocd = setupOperatorTest(openshift: true) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should not be generated on OpenShift") + .doesNotExist() + } + + @Test + void 'Does not generate ingress yaml when insecure is false and OpenShift is true'() { + config.application.insecure = false + def argocd = setupOperatorTest(openshift: true) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should not be generated when both flags are false") + .doesNotExist() + } + + @Test + void 'Central Bootstrapping for Tenant Applications'() { + setupDedicatedInstanceMode() + + def ingressFile = new File(clusterResourcesRepoLayout.operatorDir(), "ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should not be generated when both flags are false") + .doesNotExist() + } + + @Test + void 'GOP DedicatedInstances Central templating works correctly'() { + setupDedicatedInstanceMode() + //Central Applications + assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/argocd.yaml")).exists() + assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/bootstrap.yaml")).exists() + assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/projects.yaml")).exists() + assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/applications/example-apps.yaml")).doesNotExist() + + def argocdYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/applications/argocd.yaml") + def bootstrapYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/applications/bootstrap.yaml") + def projectsYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/applications/projects.yaml") + + assertThat(argocdYaml['metadata']['name']).isEqualTo('testPrefix-argocd') + assertThat(argocdYaml['metadata']['namespace']).isEqualTo('argocd') + assertThat(argocdYaml['spec']['project']).isEqualTo('testPrefix') + assertThat(argocdYaml['spec']['source']['path']).isEqualTo('apps/argocd/operator/') + + assertThat(bootstrapYaml['metadata']['name']).isEqualTo('testPrefix-bootstrap') + assertThat(bootstrapYaml['metadata']['namespace']).isEqualTo('argocd') + assertThat(bootstrapYaml['spec']['project']).isEqualTo('testPrefix') + assertThat(bootstrapYaml['spec']['source']['repoURL']).isEqualTo("scmm.testhost/scm/repo/testPrefix-argocd/cluster-resources.git") + + assertThat(projectsYaml['metadata']['name']).isEqualTo('testPrefix-projects') + assertThat(projectsYaml['metadata']['namespace']).isEqualTo('argocd') + assertThat(projectsYaml['spec']['project']).isEqualTo('testPrefix') + + //Central Project + assertThat(new File(clusterResourcesRepoLayout.argocdRoot() + "/projects/tenant.yaml")).exists() + + def tenantProject = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.argocdRoot(), "/projects/tenant.yaml") + + assertThat(tenantProject['metadata']['name']).isEqualTo('testPrefix') + assertThat(tenantProject['metadata']['namespace']).isEqualTo('argocd') + def sourceRepos = (List) tenantProject['spec']['sourceRepos'] + assertThat(sourceRepos[0]).isEqualTo('scmm.testhost/scm/repo/testPrefix-argocd/cluster-resources.git') + } + + @Test + void 'Append namespaces to Argocd argocd-default-cluster-config secrets'() { + config.application.namespaces.dedicatedNamespaces = new LinkedHashSet(['dedi-test1', 'dedi-test2', 'dedi-test3']) + config.application.namespaces.tenantNamespaces = new LinkedHashSet(['tenant-test1', 'tenant-test2', 'tenant-test3']) + setupDedicatedInstanceMode() + k8sCommands.assertExecuted('kubectl get secret argocd-default-cluster-config -n argocd -ojsonpath={.data.namespaces}') + k8sCommands.assertExecuted('kubectl patch secret argocd-default-cluster-config -n argocd --patch-file=/tmp/gitops-playground-patch-') + } + + @Test + void 'multiTenant folder gets deleted correctly if not in dedicated mode'() { + config.multiTenant.useDedicatedInstance = false + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'multiTenant/')).doesNotExist() + assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'applications/')).exists() + assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'projects/')).exists() + } + + @Test + void 'deleting unused folder in dedicated mode'() { + config.multiTenant.useDedicatedInstance = true + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'multiTenant/')).doesNotExist() + assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'applications/')).exists() + assertThat(Path.of(clusterResourcesRepoLayout.argocdRoot(), 'projects/')).exists() + } + + @Test + void 'RBACs generated correctly'() { + config.application.namespaces.tenantNamespaces = new LinkedHashSet(['testprefix-tenant-test1', 'testprefix-tenant-test2', 'testprefix-tenant-test3']) + setupDedicatedInstanceMode() + + File rbacFolder = new File(clusterResourcesRepoLayout.operatorRbacDir()) + File rbacTenantFolder = new File(clusterResourcesRepoLayout.operatorRbacDir() + "/tenant") + assertThat(rbacFolder).exists() + assertThat(rbacTenantFolder).exists() + + assertThat(rbacFolder.listFiles().count { it.isFile() }).isEqualTo(14) + assertThat(rbacTenantFolder.listFiles().count { it.isFile() }).isEqualTo(6) + + rbacFolder.eachFile { file -> + if (file.name.startsWith("role-") && file.name.contains('dedi')) { + def rbacFile = new YamlSlurper().parse(Path.of file.path) + assertThat(rbacFile['metadata']['namespace']).isIn(config.application.namespaces.getActiveNamespaces()) + } + if (file.name.startsWith("rolebinding-") && file.name.contains('dedi')) { + def rbacFile = new YamlSlurper().parse(Path.of file.path) + assertThat(rbacFile['subjects']['namespace']).isEqualTo(["argocd", "argocd", "argocd"]) + } + } + + rbacTenantFolder.eachFile { file -> + if (file.name.startsWith("role-")) { + def rbacFile = new YamlSlurper().parse(Path.of file.path) + assertThat(rbacFile['metadata']['namespace']).isIn(config.application.namespaces.tenantNamespaces) + } + + if (file.name.startsWith("rolebinding-")) { + def rbacFile = new YamlSlurper().parse(Path.of file.path) + assertThat(rbacFile['subjects']['namespace']).isEqualTo(["testPrefix-argocd", "testPrefix-argocd", "testPrefix-argocd"]) + } + } + + } + + @Test + void 'Operator RBAC includes node access rules when not on OpenShift'() { + config.application.namePrefix = "testprefix-" + + def argocd = setupOperatorTest(openshift: false) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + print config.toMap() + + File rbacDir = Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile() + File roleFile = new File(rbacDir, "role-argocd-testprefix-monitoring.yaml") + + Map yaml = new YamlSlurper().parse(roleFile) as Map + List> rules = yaml["rules"] as List> + + assertThat(rules).anyMatch { rule -> + List resources = rule["resources"] as List + resources.contains("nodes") && resources.contains("nodes/metrics") + } + } + + @Test + void 'Operator RBAC does not include node access rules when on OpenShift'() { + config.application.namePrefix = "testprefix-" + + def argocd = setupOperatorTest(openshift: true) + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + File rbacDir = Path.of(clusterResourcesRepoLayout.operatorRbacDir()).toFile() + File roleFile = new File(rbacDir, "role-argocd-testprefix-monitoring.yaml") + println roleFile + + Map yaml = new YamlSlurper().parse(roleFile) as Map + List> rules = yaml["rules"] as List> + + assertThat(rules).noneMatch { rule -> + List resources = rule["resources"] as List + resources.contains("nodes") && resources.contains("nodes/metrics") + } + } + + @Test + void 'If not using mirror, ensure source repos in cluster-resources got right URL'() { + config.application.mirrorRepos = false + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') + clusterRessourcesYaml['spec']['sourceRepos'] + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains('https://charts.external-secrets.io', + 'https://codecentric.github.io/helm-charts', + 'https://prometheus-community.github.io/helm-charts', + 'https://traefik.github.io/charts', + 'https://helm.releases.hashicorp.com', + 'https://charts.jetstack.io') + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain('http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager') + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain('http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/traefik.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git') + } + + @Test + void 'If using mirror, ensure source repos in cluster-resources got right URL'() { + config.application.mirrorRepos = true + + def argocd = createArgoCD() + argocd.install() + + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') + clusterRessourcesYaml['spec']['sourceRepos'] + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains('http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', + 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', + 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', + 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', + 'http://scmm.scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager' + + ) + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain('http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/traefik.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', + 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git') + } + + @Test + void 'If using mirror with GitLab, ensure source repos in cluster-resources got right URL'() { + config.application.mirrorRepos = true + config.scm.scmProviderType = 'GITLAB' + config.scm.gitlab.url = 'https://testGitLab.com/testgroup' + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') + clusterRessourcesYaml['spec']['sourceRepos'] + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains('https://testGitLab.com/testgroup/3rd-party-dependencies/kube-prometheus-stack.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/traefik.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/external-secrets.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/vault.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/cert-manager.git') + } + + @Test + void 'If using mirror with GitLab with prefix, ensure source repos in cluster-resources got right URL'() { + config.application.mirrorRepos = true + config.scm.scmProviderType = 'GITLAB' + config.scm.gitlab.url = "https://testGitLab.com/testgroup" + config.application.namePrefix = 'test1-' + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') + clusterRessourcesYaml['spec']['sourceRepos'] + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains('https://testGitLab.com/testgroup/3rd-party-dependencies/kube-prometheus-stack.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/traefik.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/external-secrets.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/vault.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/cert-manager.git') + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain('http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager') + } + + @Test + void 'If using mirror with name-prefix, ensure source repos in cluster-resources got right URL'() { + config.application.mirrorRepos = true + config.application.namePrefix = 'test1-' + + def argocd = createArgoCD() + argocd.install() + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of clusterResourcesRepoLayout.projectsDir(), '/cluster-resources.yaml') + clusterRessourcesYaml['spec']['sourceRepos'] + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains('http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/traefik', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager') + + assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain('http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/traefik.git', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', + 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git') + } + + void setupDedicatedInstanceMode() { + config.application.namePrefix = 'testPrefix-' + config.multiTenant.scmManager.url = 'scmm.testhost/scm' + config.multiTenant.scmManager.username = 'testUserName' + config.multiTenant.scmManager.password = 'testPassword' + config.multiTenant.useDedicatedInstance = true + this.argocd = setupOperatorTest() + argocd.install() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + clusterResourcesRepoLayout = (argocd as ArgoCDForTest).getClusterRepoLayout() + + } + + protected ArgoCD setupOperatorTest(Map options = [:]) { + config.features.argocd.operator = true + config.features.argocd.resourceInclusionsCluster = 'https://192.168.0.1:6443' + config.application.openshift = options.openshift ?: false + + def argoCD = createArgoCD() + + if (config.multiTenant.useDedicatedInstance) { + config.content.repos ? setupMockResponsesFor(MockReponses.MULTI_TENANT_WITH_EXAMPLES) : setupMockResponsesFor(MockReponses.MULTI_TENANT) + } else { + setupMockResponsesFor(MockReponses.SINGLE_TENANT) + } + + return argoCD + } + + enum MockReponses { + SINGLE_TENANT, + MULTI_TENANT, + MULTI_TENANT_WITH_EXAMPLES + } + + //Mock Responses for Testing + void setupMockResponsesFor(MockReponses mockReponses) { + switch (mockReponses) { + case MockReponses.SINGLE_TENANT -> { + k8sCommands.enqueueOutputs([queueUpAllNamespacesExist(), + new CommandExecutor.Output('', '', 0), // Monitoring CRDs applied + new CommandExecutor.Output('', '', 0), // ArgoCD Secret applied + new CommandExecutor.Output('', '', 0), // Labeling ArgoCD Secret + new CommandExecutor.Output('', '', 0), // ArgoCD operator YAML applied + new CommandExecutor.Output('', 'Available', 0), // ArgoCD resource reached desired phase + ].flatten() as Queue) + } + case MockReponses.MULTI_TENANT_WITH_EXAMPLES -> mockReponseMultiTenant() + case MockReponses.MULTI_TENANT -> mockReponseMultiTenant() + } + } + + void mockReponseMultiTenant() { + k8sCommands.enqueueOutputs([queueUpAllNamespacesExist(), + new CommandExecutor.Output('', '', 0), // Monitoring CRDs applied + + new CommandExecutor.Output('', '', 0), // ArgoCD SCM Secret applied + new CommandExecutor.Output('', '', 0), // Labeling ArgoCD SCM Secret + new CommandExecutor.Output('', '', 0), // ArgoCD SCM central Secret applied + new CommandExecutor.Output('', '', 0), // Labeling ArgoCD central SCM Secret + + new CommandExecutor.Output('', '', 0), // ArgoCD operator YAML applied + new CommandExecutor.Output('', 'Available', 0), // ArgoCD resource reached desired phase + + new CommandExecutor.Output('', '', 0), // ArgoCD argocd-cluster password secret + new CommandExecutor.Output('', '', 0), // ArgoCD argocd-secret + + new CommandExecutor.Output('', '', 0), // argocd-default-cluster-config patched + new CommandExecutor.Output('', '', 0), // ArgoCD argocd-secret + new CommandExecutor.Output('', 'dGVzdG5hbWVzcGFjZTEsdGVzdG5hbWVzcGFjZTI=', 0), // getting argocd-default-cluster-config from central Argocd + new CommandExecutor.Output('', '', 0), // setting argocd-default-cluster-config from central Argocd + ].flatten() as Queue) + } + + private void simulateNamespaceCreation() { + Queue outputs = new LinkedList() + config.application.namespaces.getActiveNamespaces().each { namespace -> + outputs.add(new CommandExecutor.Output("${namespace} not found", "", 1)) + outputs.add(new CommandExecutor.Output("${namespace} created", "", 0)) + } + k8sCommands.enqueueOutputs(outputs) + } + + private Queue queueUpAllNamespacesExist() { + return new LinkedList(config.application.namespaces.getActiveNamespaces().collect { namespace -> new CommandExecutor.Output(namespace, "", 0) }) + } + + private static void mockPrefixActiveNamespaces(Config config) { + def prefix = config.application.namePrefix ?: "" + + config.application.namespaces.with { + dedicatedNamespaces = new LinkedHashSet<>(dedicatedNamespaces.collect { (prefix + it).toString() }) + tenantNamespaces = new LinkedHashSet<>(tenantNamespaces.collect { (prefix + it).toString() }) + } + } + + static class ArgoCDForTest extends ArgoCD { + final Config cfg + final GitProvider tenantProvider + final GitProvider centralProvider + + static ArgoCDForTest newWithAutoProviders(Config cfg, + CommandExecutorForTest k8sCommands, + CommandExecutorForTest helmCommands) { + def provider = TestGitProvider.buildProviders(cfg) + return new ArgoCDForTest(cfg, + k8sCommands, + helmCommands, + provider.tenant as GitProvider, + provider.central as GitProvider) + } + + ArgoCDForTest(Config cfg, + CommandExecutorForTest k8sCommands, + CommandExecutorForTest helmCommands, + GitProvider tenantProvider, + GitProvider centralProvider) { + super(cfg, + new K8sClientForTest(cfg, k8sCommands), + new HelmClient(helmCommands), + new FileSystemUtils(), + new TestGitRepoFactory(cfg, new FileSystemUtils()), + new GitHandlerForTests(cfg, tenantProvider, centralProvider)) + this.cfg = cfg + this.tenantProvider = tenantProvider + this.centralProvider = centralProvider + mockPrefixActiveNamespaces(cfg) + } + + GitRepo getClusterResourcesRepo() { + return getRepoSetup().clusterResources?.repo + } + + RepoLayout getClusterRepoLayout() { + return getRepoSetup().clusterRepoLayout() + } + + } + + private Map parseActualYaml(String pathToYamlFile) { + File yamlFile = new File(pathToYamlFile) + def ys = new YamlSlurper() + return ys.parse(yamlFile) as Map + } + +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/features/deployment/HelmStrategyTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/deployment/HelmStrategyTest.groovy index 7ad8f898b..a6616c155 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/deployment/HelmStrategyTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/deployment/HelmStrategyTest.groovy @@ -1,6 +1,9 @@ package com.cloudogu.gitops.features.deployment -import org.junit.jupiter.api.Test +import static groovy.test.GroovyAssert.shouldFail +import static org.assertj.core.api.Assertions.assertThat +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.verify import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.kubernetes.api.HelmClient @@ -8,42 +11,36 @@ import com.cloudogu.gitops.kubernetes.api.HelmClient import java.nio.file.Files import java.nio.file.Path -import static groovy.test.GroovyAssert.shouldFail -import static org.assertj.core.api.Assertions.assertThat -import static org.mockito.Mockito.mock -import static org.mockito.Mockito.verify +import org.junit.jupiter.api.Test class HelmStrategyTest { - HelmClient helmClient = mock(HelmClient) - - @Test - void 'deploys feature using helm client'() { - Path valuesYaml = Files.createTempFile('', '') - - createStrategy().deployFeature("repoURL", "repoName", "chart", "version", "foo-namespace", "releaseName", valuesYaml) - - verify(helmClient).addRepo("repoName", "repoURL") - verify(helmClient).upgrade("releaseName", "repoName/chart", [ - namespace: "foo-namespace", - version: "version", - values: valuesYaml.toString() - ]) - } - - @Test - void 'Fails to deploy from git'() { - def exception = shouldFail(RuntimeException) { - createStrategy().deployFeature("http://repoURL", "repoName", "chart", "version", "namespace", - "releaseName", Path.of("values.yaml"), DeploymentStrategy.RepoType.GIT) - } - assertThat(exception.message).isEqualTo( - "Unable to deploy helm chart via Helm CLI from Git URL, because helm does not support this out of the box.\n" + - "Repo URL: http://repoURL") - - } - - protected HelmStrategy createStrategy() { - new HelmStrategy(new Config(application: new Config.ApplicationSchema(namePrefix: "foo-")), helmClient) - } + HelmClient helmClient = mock(HelmClient) + + @Test + void 'deploys feature using helm client'() { + Path valuesYaml = Files.createTempFile('', '') + + createStrategy().deployFeature("repoURL", "repoName", "chart", "version", "foo-namespace", "releaseName", valuesYaml) + + verify(helmClient).addRepo("repoName", "repoURL") + verify(helmClient).upgrade("releaseName", "repoName/chart", [namespace: "foo-namespace", + version : "version", + values : valuesYaml.toString()]) + } + + @Test + void 'Fails to deploy from git'() { + def exception = shouldFail(RuntimeException) { + createStrategy().deployFeature("http://repoURL", "repoName", "chart", "version", "namespace", + "releaseName", Path.of("values.yaml"), DeploymentStrategy.RepoType.GIT) + } + assertThat(exception.message).isEqualTo("Unable to deploy helm chart via Helm CLI from Git URL, because helm does not support this out of the box.\n" + + "Repo URL: http://repoURL") + + } + + protected HelmStrategy createStrategy() { + new HelmStrategy(new Config(application: new Config.ApplicationSchema(namePrefix: "foo-")), helmClient) + } } \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetupTest.groovy b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetupTest.groovy index d3cb069f6..732862704 100644 --- a/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetupTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetupTest.groovy @@ -1,8 +1,8 @@ package com.cloudogu.gitops.git.providers.scmmanager -import org.junit.jupiter.api.Test -import retrofit2.Call -import retrofit2.Response +import static org.mockito.ArgumentMatchers.any +import static org.mockito.ArgumentMatchers.eq +import static org.mockito.Mockito.* import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.HelmStrategy @@ -10,81 +10,69 @@ import com.cloudogu.gitops.git.providers.scmmanager.api.PluginApi import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApi import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApiClient -import static org.mockito.ArgumentMatchers.any -import static org.mockito.ArgumentMatchers.eq -import static org.mockito.Mockito.* +import org.junit.jupiter.api.Test +import retrofit2.Call +import retrofit2.Response class ScmManagerSetupTest { - ScmManager scmManager = mock(ScmManager.class) + ScmManager scmManager = mock(ScmManager.class) - HelmStrategy helmStrategy = mock(HelmStrategy.class) - ScmManagerApiClient apiClient = mock(ScmManagerApiClient.class) + HelmStrategy helmStrategy = mock(HelmStrategy.class) + ScmManagerApiClient apiClient = mock(ScmManagerApiClient.class) - PluginApi pluginApi = mock(PluginApi.class) - ScmManagerApi generalApi = mock(ScmManagerApi.class) + PluginApi pluginApi = mock(PluginApi.class) + ScmManagerApi generalApi = mock(ScmManagerApi.class) - Config config = Config.fromMap([ - application: [ - namePrefix: 'test', - ], - scm : [ - scmManager: [ - internal : true, - url : "", - namespace : "scm-manager", - username : "admin", - password : "admin", - helm : [ - chart : "scm-manager", - repoURL: "https://packages.scm-manager.org/repository/helm-v2-releases/", - version: "3.11.2", - values : [:] - ], - urlForJenkins : "http://scmm.scm-manager.svc.cluster.local/scm", - ingress : "scmm.master.localhost", - skipRestart : false, - skipPlugins : false, - gitOpsUsername: "" - ] - ] - ]) + Config config = Config.fromMap([application: [namePrefix: 'test',], + scm : [scmManager: [internal : true, + url : "", + namespace : "scm-manager", + username : "admin", + password : "admin", + helm : [chart : "scm-manager", + repoURL: "https://packages.scm-manager.org/repository/helm-v2-releases/", + version: "3.11.2", + values : [:]], + urlForJenkins : "http://scmm.scm-manager.svc.cluster.local/scm", + ingress : "scmm.master.localhost", + skipRestart : false, + skipPlugins : false, + gitOpsUsername: ""]]]) - @Test - void 'Helm chart is installed correctly'() { - when(scmManager.getConfig()).thenReturn(config) - when(scmManager.getHelmStrategy()).thenReturn(helmStrategy) - when(scmManager.getScmmConfig()).thenReturn(config.scm.scmManager) - ScmManagerSetup scmManagerSetup = new ScmManagerSetup(scmManager) - scmManagerSetup.setupHelm() - verify(helmStrategy).deployFeature( - eq( "https://packages.scm-manager.org/repository/helm-v2-releases/"), - eq("scm-manager"), - any(), - eq("3.11.2"), - eq("scm-manager"), - eq("scmm"), - any() - ) - } + @Test + void 'Helm chart is installed correctly'() { + when(scmManager.getConfig()).thenReturn(config) + when(scmManager.getHelmStrategy()).thenReturn(helmStrategy) + when(scmManager.getScmmConfig()).thenReturn(config.scm.scmManager) + ScmManagerSetup scmManagerSetup = new ScmManagerSetup(scmManager) + scmManagerSetup.setupHelm() + verify(helmStrategy).deployFeature(eq("https://packages.scm-manager.org/repository/helm-v2-releases/"), + eq("scm-manager"), + any(), + eq("3.11.2"), + eq("scm-manager"), + eq("scmm"), + any()) + } - @Test - void 'ScmManager Plugins are installed correctly'() { - when(scmManager.getConfig()).thenReturn(config) - when(scmManager.getHelmStrategy()).thenReturn(helmStrategy) - when(scmManager.getScmmConfig()).thenReturn(config.scm.scmManager) - when(scmManager.getApiClient()).thenReturn(apiClient) + @Test + void 'ScmManager Plugins are installed correctly'() { + when(scmManager.getConfig()).thenReturn(config) + when(scmManager.getHelmStrategy()).thenReturn(helmStrategy) + when(scmManager.getScmmConfig()).thenReturn(config.scm.scmManager) + when(scmManager.getApiClient()).thenReturn(apiClient) - Call apiCall = mock(Call.class) + Call apiCall = mock(Call.class) - when(pluginApi.install(any(),any())).thenReturn(apiCall) - when(generalApi.checkScmmAvailable()).thenReturn(apiCall) - when(apiClient.pluginApi()).thenReturn(pluginApi) - when(apiClient.generalApi()).thenReturn(generalApi) - when(apiCall.execute()).thenReturn(Response.success(null)) - ScmManagerSetup scmManagerSetup = new ScmManagerSetup(scmManager) - scmManagerSetup.installScmmPlugins() - verify(pluginApi,atLeast(10)).install(any(),any()) - } + when(pluginApi.install(any(), any())).thenReturn(apiCall) + when(generalApi.checkScmmAvailable()).thenReturn(apiCall) + when(apiClient.pluginApi()).thenReturn(pluginApi) + when(apiClient.generalApi()).thenReturn(generalApi) + when(apiCall.execute()).thenReturn(Response.success(null)) + ScmManagerSetup scmManagerSetup = new ScmManagerSetup(scmManager) + scmManagerSetup.installScmmPlugins() + verify(pluginApi, atLeast(10)).install(any(), any()) + } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolverTest.groovy b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolverTest.groovy index d128eb3f2..305bc9055 100644 --- a/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolverTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolverTest.groovy @@ -1,166 +1,157 @@ package com.cloudogu.gitops.git.providers.scmmanager -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension +import static org.junit.jupiter.api.Assertions.* +import static org.mockito.ArgumentMatchers.any +import static org.mockito.ArgumentMatchers.eq +import static org.mockito.Mockito.* import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.git.config.ScmTenantSchema import com.cloudogu.gitops.kubernetes.api.K8sClient import com.cloudogu.gitops.utils.NetworkingUtils -import static org.junit.jupiter.api.Assertions.* -import static org.mockito.ArgumentMatchers.any -import static org.mockito.ArgumentMatchers.eq -import static org.mockito.Mockito.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension @ExtendWith(MockitoExtension.class) class ScmManagerUrlResolverTest { - private Config config - - @Mock - private K8sClient k8s - @Mock - private NetworkingUtils net - - - @BeforeEach - void setUp() { - config = new Config( - application: new Config.ApplicationSchema( - namePrefix: 'fv40-', - runningInsideK8s: false - ) - ) - } - - private ScmManagerUrlResolver resolverWith(Map args = [:]) { - def scmmCofig = new ScmTenantSchema.ScmManagerTenantConfig() - scmmCofig.internal = (args.containsKey('internal') ? args.internal : true) - scmmCofig.namespace = (args.containsKey('namespace') ? args.namespace : "scm-manager") - scmmCofig.url = (args.containsKey('url') ? args.url : "") - scmmCofig.ingress = (args.containsKey('ingress') ? args.ingress : "") - - return new ScmManagerUrlResolver(config, scmmCofig, k8s, net) - } - - // ---------- Client base & API ---------- - @Test - void "clientBase(): internal + outside K8s uses NodePort and appends 'scm' (no trailing slash) and only resolves NodePort once"() { - when(k8s.waitForNodePort(eq('scmm'), any())).thenReturn("30080") - when(net.findClusterBindAddress()).thenReturn("10.0.0.1") - - def r = resolverWith() - URI base1 = r.clientBase() - URI base2 = r.clientBase() - - assertEquals("http://10.0.0.1:30080/scm", base1.toString()) - assertEquals(base1, base2) - - verify(k8s, times(1)).waitForNodePort("scmm", "scm-manager") - verify(net, times(1)).findClusterBindAddress() - verifyNoMoreInteractions(k8s, net) - } - - @Test - void "clientApiBase(): appends 'api' to the client base"() { - when(k8s.waitForNodePort("scmm", "scm-manager")).thenReturn("30080") - when(net.findClusterBindAddress()).thenReturn("10.0.0.1") - - var urlResolver = resolverWith() - assertEquals("http://10.0.0.1:30080/scm/api/", urlResolver.clientApiBase().toString()) - } - - // ---------- Repo base & URLs ---------- - @Test - void "clientRepoUrl(): trims repoTarget and removes trailing slash"() { - when(k8s.waitForNodePort("scmm", "scm-manager")).thenReturn("30080") - when(net.findClusterBindAddress()).thenReturn("10.0.0.1") - - var urlResolver = resolverWith() - assertEquals("http://10.0.0.1:30080/scm/repo/ns/project", - urlResolver.clientRepoUrl(" ns/project ")) - } - - // ---------- In-cluster base & URLs ---------- - @Test - void "inClusterBase(): internal uses service DNS "() { - def r = resolverWith(namespace: "custom-ns", internal: true) - assertEquals("http://scmm.custom-ns.svc.cluster.local/scm", r.inClusterBase().toString()) - } - - - @Test - void "inClusterBase(): external uses external base + 'scm'"() { - var r = resolverWith(internal: false, url: "https://scmm.external") - assertEquals("https://scmm.external/scm", r.inClusterBase().toString()) - } - - - @Test - void "inClusterRepoUrl(): builds full in-cluster repo URL without trailing slash"() { - var urlResolver = resolverWith() - assertEquals("http://scmm.scm-manager.svc.cluster.local/scm/repo/admin/admin", - urlResolver.inClusterRepoUrl("admin/admin")) - } - - @Test - void "inClusterRepoPrefix(): includes configured namePrefix (empty prefix yields base path)"() { - // with non-empty namePrefix - config.application.namePrefix = 'fv40-' - def r1 = resolverWith() - assertEquals('http://scmm.scm-manager.svc.cluster.local/scm/repo/fv40-', r1.inClusterRepoPrefix()) - - // with empty/blank namePrefix - config.application.namePrefix = ' ' - def r2 = resolverWith() - assertEquals('http://scmm.scm-manager.svc.cluster.local/scm/repo/', r2.inClusterRepoPrefix()) - } - - // ---------- externalBase selection & error ---------- - @Test - void "externalBase(): prefers 'url' over 'ingress'"() { - def r = resolverWith(internal: false, url: 'https://scmm.external', ingress: 'ingress.example.org') - assertEquals('https://scmm.external/scm', r.inClusterBase().toString()) - } - - @Test - void "externalBase(): uses 'ingress' when 'url' is missing"() { - def r = resolverWith(internal: false, url: null, ingress: 'ingress.example.org') - assertEquals('http://ingress.example.org/scm', r.inClusterBase().toString()) - } - - @Test - void "externalBase(): throws when neither 'url' nor 'ingress' is set"() { - def r = resolverWith(internal: false, url: null, ingress: null) - def ex = assertThrows(IllegalArgumentException) { r.inClusterBase() } - assertTrue(ex.message.contains('Either scmm.url or scmm.ingress must be set when internal=false')) - } - - - @Test - void "nodePortBase(): falls back to default namespace 'scm-manager' when none provided"() { - when(k8s.waitForNodePort(eq('scmm'), eq('scm-manager'))).thenReturn("30080") - when(net.findClusterBindAddress()).thenReturn('10.0.0.1') - - def r = resolverWith(namespace: null) - assertEquals('http://10.0.0.1:30080/scm', r.clientBase().toString()) - } - - // ---------- helpers behavior ---------- - @Test - void "ensureScm(): adds 'scm' if missing and keeps it if present"() { - def r1 = resolverWith(internal: false, url: 'https://scmm.localhost') - assertEquals('https://scmm.localhost/scm', r1.clientBase().toString()) - } - - - // ---------- prometheus endpoint ---------- - @Test - void "prometheusEndpoint(): resolves "() { - def r = resolverWith(internal: false, url: 'https://scmm.localhost') - assertEquals('https://scmm.localhost/scm/api/v2/metrics/prometheus', r.prometheusEndpoint().toString()) - } -} + private Config config + + @Mock + private K8sClient k8s + @Mock + private NetworkingUtils net + + @BeforeEach + void setUp() { + config = new Config(application: new Config.ApplicationSchema(namePrefix: 'fv40-', + runningInsideK8s: false)) + } + + private ScmManagerUrlResolver resolverWith(Map args = [:]) { + def scmmCofig = new ScmTenantSchema.ScmManagerTenantConfig() + scmmCofig.internal = (args.containsKey('internal') ? args.internal : true) + scmmCofig.namespace = (args.containsKey('namespace') ? args.namespace : "scm-manager") + scmmCofig.url = (args.containsKey('url') ? args.url : "") + scmmCofig.ingress = (args.containsKey('ingress') ? args.ingress : "") + + return new ScmManagerUrlResolver(config, scmmCofig, k8s, net) + } + + // ---------- Client base & API ---------- + @Test + void "clientBase(): internal + outside K8s uses NodePort and appends 'scm' (no trailing slash) and only resolves NodePort once"() { + when(k8s.waitForNodePort(eq('scmm'), any())).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn("10.0.0.1") + + def r = resolverWith() + URI base1 = r.clientBase() + URI base2 = r.clientBase() + + assertEquals("http://10.0.0.1:30080/scm", base1.toString()) + assertEquals(base1, base2) + + verify(k8s, times(1)).waitForNodePort("scmm", "scm-manager") + verify(net, times(1)).findClusterBindAddress() + verifyNoMoreInteractions(k8s, net) + } + + @Test + void "clientApiBase(): appends 'api' to the client base"() { + when(k8s.waitForNodePort("scmm", "scm-manager")).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn("10.0.0.1") + + var urlResolver = resolverWith() + assertEquals("http://10.0.0.1:30080/scm/api/", urlResolver.clientApiBase().toString()) + } + + // ---------- Repo base & URLs ---------- + @Test + void "clientRepoUrl(): trims repoTarget and removes trailing slash"() { + when(k8s.waitForNodePort("scmm", "scm-manager")).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn("10.0.0.1") + + var urlResolver = resolverWith() + assertEquals("http://10.0.0.1:30080/scm/repo/ns/project", + urlResolver.clientRepoUrl(" ns/project ")) + } + + // ---------- In-cluster base & URLs ---------- + @Test + void "inClusterBase(): internal uses service DNS "() { + def r = resolverWith(namespace: "custom-ns", internal: true) + assertEquals("http://scmm.custom-ns.svc.cluster.local/scm", r.inClusterBase().toString()) + } + + @Test + void "inClusterBase(): external uses external base + 'scm'"() { + var r = resolverWith(internal: false, url: "https://scmm.external") + assertEquals("https://scmm.external/scm", r.inClusterBase().toString()) + } + + @Test + void "inClusterRepoUrl(): builds full in-cluster repo URL without trailing slash"() { + var urlResolver = resolverWith() + assertEquals("http://scmm.scm-manager.svc.cluster.local/scm/repo/admin/admin", + urlResolver.inClusterRepoUrl("admin/admin")) + } + + @Test + void "inClusterRepoPrefix(): includes configured namePrefix (empty prefix yields base path)"() { + // with non-empty namePrefix + config.application.namePrefix = 'fv40-' + def r1 = resolverWith() + assertEquals('http://scmm.scm-manager.svc.cluster.local/scm/repo/fv40-', r1.inClusterRepoPrefix()) + + // with empty/blank namePrefix + config.application.namePrefix = ' ' + def r2 = resolverWith() + assertEquals('http://scmm.scm-manager.svc.cluster.local/scm/repo/', r2.inClusterRepoPrefix()) + } + + // ---------- externalBase selection & error ---------- + @Test + void "externalBase(): prefers 'url' over 'ingress'"() { + def r = resolverWith(internal: false, url: 'https://scmm.external', ingress: 'ingress.example.org') + assertEquals('https://scmm.external/scm', r.inClusterBase().toString()) + } + + @Test + void "externalBase(): uses 'ingress' when 'url' is missing"() { + def r = resolverWith(internal: false, url: null, ingress: 'ingress.example.org') + assertEquals('http://ingress.example.org/scm', r.inClusterBase().toString()) + } + + @Test + void "externalBase(): throws when neither 'url' nor 'ingress' is set"() { + def r = resolverWith(internal: false, url: null, ingress: null) + def ex = assertThrows(IllegalArgumentException) { r.inClusterBase() } + assertTrue(ex.message.contains('Either scmm.url or scmm.ingress must be set when internal=false')) + } + + @Test + void "nodePortBase(): falls back to default namespace 'scm-manager' when none provided"() { + when(k8s.waitForNodePort(eq('scmm'), eq('scm-manager'))).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn('10.0.0.1') + + def r = resolverWith(namespace: null) + assertEquals('http://10.0.0.1:30080/scm', r.clientBase().toString()) + } + + // ---------- helpers behavior ---------- + @Test + void "ensureScm(): adds 'scm' if missing and keeps it if present"() { + def r1 = resolverWith(internal: false, url: 'https://scmm.localhost') + assertEquals('https://scmm.localhost/scm', r1.clientBase().toString()) + } + + // ---------- prometheus endpoint ---------- + @Test + void "prometheusEndpoint(): resolves "() { + def r = resolverWith(internal: false, url: 'https://scmm.localhost') + assertEquals('https://scmm.localhost/scm/api/v2/metrics/prometheus', r.prometheusEndpoint().toString()) + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/integration/profiles/FullProfileTestIT.groovy b/src/test/groovy/com/cloudogu/gitops/integration/profiles/FullProfileTestIT.groovy index ee2f79691..bc6532431 100644 --- a/src/test/groovy/com/cloudogu/gitops/integration/profiles/FullProfileTestIT.groovy +++ b/src/test/groovy/com/cloudogu/gitops/integration/profiles/FullProfileTestIT.groovy @@ -1,5 +1,12 @@ package com.cloudogu.gitops.integration.profiles +import static org.assertj.core.api.Assertions.fail + +import com.cloudogu.gitops.integration.TestK8sHelper + +import java.util.concurrent.TimeUnit +import groovy.util.logging.Slf4j + import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.KubernetesClientBuilder import io.fabric8.kubernetes.client.KubernetesClientException @@ -8,13 +15,6 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledIfSystemProperty -import com.cloudogu.gitops.integration.TestK8sHelper - -import java.util.concurrent.TimeUnit -import groovy.util.logging.Slf4j - -import static org.assertj.core.api.Assertions.fail - /** * This test ensures all Pods and Namespaces are available, runnning at a startet GOP with - more or less - defaulöt values. * @@ -24,157 +24,151 @@ import static org.assertj.core.api.Assertions.fail @EnabledIfSystemProperty(named = "micronaut.environments", matches = "full") class FullProfileTestIT extends ProfileTestSetup { - /** - * Gets path to kubeconfig */ - static final String RUNNING = "Running" - static final String EXAMPLE_APPS_NAMESPACE = 'example-apps-staging' - - @BeforeAll - static void labelMyTest() { - log.info '########### K8S SMOKE TESTS PROFILE full ###########' - waitUntilAllPodsRunning() - } - - - private static void waitUntilAllPodsRunning() { - // if cert-manager is online, argocd is online, too! - Awaitility.await().atMost(40, TimeUnit.MINUTES).untilAsserted { - TestK8sHelper.checkAllPodsRunningInNamespace(EXAMPLE_APPS_NAMESPACE) - } - } - - @Test - void ensureJenkinsPodIsStarted() { - TestK8sHelper.checkAllPodsRunningInNamespace('jenkins', 'jenkins') - } - - @Test - void ensureArgoCDIsOnlineAndPodsAreRunning() { - String expectedPod1 = "argocd-application-controller" - String expectedPod2 = "argocd-applicationset-controller" -// String expectedPod3 = "argocd-notifications-controller" // not stable - String expectedPod4 = "argocd-redis" - String expectedPod5 = "argocd-repo-server" - String expectedPod6 = "argocd-server" - - List expectedPods = [expectedPod1, expectedPod2, /* expectedPod3,*/ expectedPod4, expectedPod5, expectedPod6,] - - - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - - def actualPods = client.pods().inNamespace('argocd').list().getItems() - - // 1. Verify all expected pods are present - def missingPods = expectedPods.findAll { prefix -> !actualPods.any { it.getMetadata().getName().startsWith(prefix) } - } - assert missingPods.isEmpty(): "Missing these pods in argocd: ${missingPods}" - - // 2. Verify all relevant pods are in 'Running' phase - def notRunningPods = actualPods.findAll { pod -> expectedPods.any { prefix -> pod.getMetadata().getName().startsWith(prefix) } - }.findAll { pod -> pod.getStatus().getPhase() != RUNNING - } - - assert notRunningPods.isEmpty(): "These pods are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" - - } catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - } - - @Test - void ensureScmmPodIsStarted() { - - TestK8sHelper.checkAllPodsRunningInNamespace('scm-manager') - } - - @Test - void ensureNamespacesExists() { - List expectedNamespaces = ["argocd", - "cert-manager", - "jenkins", - "registry", - "scm-manager", - "default", - "example-apps-production", - "example-apps-staging", - "ingress", - "kube-node-lease", - "kube-public", - "kube-system", - "monitoring", - "secrets"] as List - - - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - - def currentNames = client.namespaces().list().getItems() - - // 1. Verify all expected pods are present - def missingNamespace = expectedNamespaces.findAll { prefix -> !currentNames.any { it.getMetadata().getName().startsWith(prefix) } - } - assert missingNamespace.isEmpty(): "Missing these Namespace: ${missingNamespace}" - - - } catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - - - } - -/** - * tests searches for ingress services and ensure ingress is used as loadbalancer*/ - @Test - void ensureIngressIsOnline() { - TestK8sHelper.checkAllPodsRunningInNamespace('ingress', 'traefik') - } - - @Test - void ensureCertManagerIsOnline() { - TestK8sHelper.checkAllPodsRunningInNamespace('cert-manager') - } - - @Test - void ensureVaultIsOnline() { - TestK8sHelper.checkAllPodsRunningInNamespace('secrets', 'vault-0') - } - - @Test - void ensureRegistryIsOnline() { - TestK8sHelper.checkAllPodsRunningInNamespace('registry', 'docker-registry') - } - - @Test - void ensureExternalSecretsPodsRunning() { - - String expectedPod1 = "external-secrets-webhook" - String expectedPod2 = "external-secrets-cert-controller" - - List expectedPods = [expectedPod1, expectedPod2] - - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - - def actualPods = client.pods().inNamespace('secrets').list().getItems() - - // 1. Verify all expected pods are present - def missingPods = expectedPods.findAll { prefix -> !actualPods.any { it.getMetadata().getName().startsWith(prefix) } - } - assert missingPods.isEmpty(): "Missing these pods in secrets: ${missingPods}" + /** + * Gets path to kubeconfig */ + static final String RUNNING = "Running" + static final String EXAMPLE_APPS_NAMESPACE = 'example-apps-staging' + + @BeforeAll + static void labelMyTest() { + log.info '########### K8S SMOKE TESTS PROFILE full ###########' + waitUntilAllPodsRunning() + } + + private static void waitUntilAllPodsRunning() { + // if cert-manager is online, argocd is online, too! + Awaitility.await().atMost(40, TimeUnit.MINUTES).untilAsserted { + TestK8sHelper.checkAllPodsRunningInNamespace(EXAMPLE_APPS_NAMESPACE) + } + } + + @Test + void ensureJenkinsPodIsStarted() { + TestK8sHelper.checkAllPodsRunningInNamespace('jenkins', 'jenkins') + } + + @Test + void ensureArgoCDIsOnlineAndPodsAreRunning() { + String expectedPod1 = "argocd-application-controller" + String expectedPod2 = "argocd-applicationset-controller" + // String expectedPod3 = "argocd-notifications-controller" // not stable + String expectedPod4 = "argocd-redis" + String expectedPod5 = "argocd-repo-server" + String expectedPod6 = "argocd-server" + + List expectedPods = [expectedPod1, expectedPod2, /* expectedPod3,*/ expectedPod4, expectedPod5, expectedPod6,] + + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + + def actualPods = client.pods().inNamespace('argocd').list().getItems() + + // 1. Verify all expected pods are present + def missingPods = expectedPods.findAll { prefix -> !actualPods.any { it.getMetadata().getName().startsWith(prefix) } + } + assert missingPods.isEmpty(): "Missing these pods in argocd: ${missingPods}" + + // 2. Verify all relevant pods are in 'Running' phase + def notRunningPods = actualPods.findAll { pod -> expectedPods.any { prefix -> pod.getMetadata().getName().startsWith(prefix) } + }.findAll { pod -> pod.getStatus().getPhase() != RUNNING + } + + assert notRunningPods.isEmpty(): "These pods are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" + + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + } + + @Test + void ensureScmmPodIsStarted() { + + TestK8sHelper.checkAllPodsRunningInNamespace('scm-manager') + } + + @Test + void ensureNamespacesExists() { + List expectedNamespaces = ["argocd", + "cert-manager", + "jenkins", + "registry", + "scm-manager", + "default", + "example-apps-production", + "example-apps-staging", + "ingress", + "kube-node-lease", + "kube-public", + "kube-system", + "monitoring", + "secrets"] as List + + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + + def currentNames = client.namespaces().list().getItems() + + // 1. Verify all expected pods are present + def missingNamespace = expectedNamespaces.findAll { prefix -> !currentNames.any { it.getMetadata().getName().startsWith(prefix) } + } + assert missingNamespace.isEmpty(): "Missing these Namespace: ${missingNamespace}" + + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + + } + + /** + * tests searches for ingress services and ensure ingress is used as loadbalancer*/ + @Test + void ensureIngressIsOnline() { + TestK8sHelper.checkAllPodsRunningInNamespace('ingress', 'traefik') + } + + @Test + void ensureCertManagerIsOnline() { + TestK8sHelper.checkAllPodsRunningInNamespace('cert-manager') + } + + @Test + void ensureVaultIsOnline() { + TestK8sHelper.checkAllPodsRunningInNamespace('secrets', 'vault-0') + } + + @Test + void ensureRegistryIsOnline() { + TestK8sHelper.checkAllPodsRunningInNamespace('registry', 'docker-registry') + } + + @Test + void ensureExternalSecretsPodsRunning() { - // 2. Verify all relevant pods are in 'Running' phase - def notRunningPods = actualPods.findAll { pod -> expectedPods.any { prefix -> pod.getMetadata().getName().startsWith(prefix) } - }.findAll { pod -> pod.getStatus().getPhase() != RUNNING - } + String expectedPod1 = "external-secrets-webhook" + String expectedPod2 = "external-secrets-cert-controller" + + List expectedPods = [expectedPod1, expectedPod2] - assert notRunningPods.isEmpty(): "These pods are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + + def actualPods = client.pods().inNamespace('secrets').list().getItems() - // vault-0, external-secrets-webhook, external-secrets-, external-secrets-cert-controller - assert actualPods.size() == 4 + // 1. Verify all expected pods are present + def missingPods = expectedPods.findAll { prefix -> !actualPods.any { it.getMetadata().getName().startsWith(prefix) } + } + assert missingPods.isEmpty(): "Missing these pods in secrets: ${missingPods}" - } catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - } + // 2. Verify all relevant pods are in 'Running' phase + def notRunningPods = actualPods.findAll { pod -> expectedPods.any { prefix -> pod.getMetadata().getName().startsWith(prefix) } + }.findAll { pod -> pod.getStatus().getPhase() != RUNNING + } + + assert notRunningPods.isEmpty(): "These pods are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" + + // vault-0, external-secrets-webhook, external-secrets-, external-secrets-cert-controller + assert actualPods.size() == 4 + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.groovy b/src/test/groovy/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.groovy index fc59cc3bd..e8aff5b78 100644 --- a/src/test/groovy/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.groovy +++ b/src/test/groovy/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.groovy @@ -1,7 +1,12 @@ package com.cloudogu.gitops.integration.profiles +import static org.assertj.core.api.Assertions.fail + import com.cloudogu.gitops.integration.TestK8sHelper + +import java.util.concurrent.TimeUnit import groovy.util.logging.Slf4j + import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.KubernetesClientBuilder import io.fabric8.kubernetes.client.KubernetesClientException @@ -11,10 +16,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.DisabledIfSystemProperty import org.junit.jupiter.api.condition.EnabledIfSystemProperty -import java.util.concurrent.TimeUnit - -import static org.assertj.core.api.Assertions.fail - /** * This test ensures all Pods and Namespaces are available, runnning at a startet GOP with - more or less - defaulöt values. * @@ -24,101 +25,98 @@ import static org.assertj.core.api.Assertions.fail @EnabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") class MandantProfileTestIT extends ProfileTestSetup { - /** - * Gets path to kubeconfig */ - static final String RUNNING = "Running" - static final String TENANT_POD_FOR_CONDITION = 'argocd-application-controller' - static final String TENANT_NAMESPACE_ARGOCD = 'tenant1-argocd' - static final String TENANT_NAMESPACE_REGISTRY = 'tenant1-registry' - static final String TENANT_NAMESPACE_SCM = 'tenant1-scm-manager' - - @BeforeAll - static void labelMyTest() { - log.info '########### PROFILE Operator-Mandants ###########' - waitUntilTenantIsReady() - } - - private static void waitUntilTenantIsReady() { - // tenant is created very late after running GOP twice! - Awaitility.await().atMost(40, TimeUnit.MINUTES).untilAsserted { - assert TestK8sHelper.checkAllPodsRunningInNamespace(TENANT_NAMESPACE_REGISTRY, "docker-registry") && - TestK8sHelper.checkAllPodsRunningInNamespace(TENANT_NAMESPACE_SCM, 'scmm-') - } - } - - @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") - // just local - @Test - void ensureJenkinsPodIsStartedOnTenant() { - TestK8sHelper.checkAllPodsRunningInNamespace('tenant1-jenkins', 'jenkins') - } - - @Test - void ensureRegistryPodIsStartedOnTenant() { - TestK8sHelper.checkAllPodsRunningInNamespace('tenant1-registry', 'docker-registry') - } - - @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") - // just local - @Test - void ensureArgocdPodsAreStartedOnTenant() { - def argocdNamespace = TENANT_NAMESPACE_ARGOCD - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-application-controller') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-applicationset-controller') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-redis') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-repo-server') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-server') - } - - @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") - // just local - @Test - void ensureArgocdPodsAreStartedOnCentral() { - def argocdNamespace = 'argocd' - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-application-controller') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-applicationset-controller') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-redis') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-repo-server') - TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-server') - } - - @Test - void ensureScmmPodIsStarted() { - - TestK8sHelper.checkAllPodsRunningInNamespace('scm-manager') - } - - @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") - // just local - @Test - void ensureNamespacesExists() { - List expectedNamespaces = ["argocd", - "argocd-operator-system", - "scm-manager", - "default", - "tenant1-argocd", - "tenant1-jenkins", - "tenant1-registry", - "tenant1-example-apps-staging", - "tenant1-example-apps-staging", - "tenant1-scm-manager", - "kube-node-lease", - "kube-public", - "kube-system"] as List - - - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - - def currentNames = client.namespaces().list().getItems() - - // 1. Verify all expected pods are present - def missingNamespace = expectedNamespaces.findAll { prefix -> !currentNames.any { it.getMetadata().getName().startsWith(prefix) } - } - assert missingNamespace.isEmpty(): "Missing these Namespace: ${missingNamespace}" - - - } catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - } -} + /** + * Gets path to kubeconfig */ + static final String RUNNING = "Running" + static final String TENANT_POD_FOR_CONDITION = 'argocd-application-controller' + static final String TENANT_NAMESPACE_ARGOCD = 'tenant1-argocd' + static final String TENANT_NAMESPACE_REGISTRY = 'tenant1-registry' + static final String TENANT_NAMESPACE_SCM = 'tenant1-scm-manager' + + @BeforeAll + static void labelMyTest() { + log.info '########### PROFILE Operator-Mandants ###########' + waitUntilTenantIsReady() + } + + private static void waitUntilTenantIsReady() { + // tenant is created very late after running GOP twice! + Awaitility.await().atMost(40, TimeUnit.MINUTES).untilAsserted { + assert TestK8sHelper.checkAllPodsRunningInNamespace(TENANT_NAMESPACE_REGISTRY, "docker-registry") && TestK8sHelper.checkAllPodsRunningInNamespace(TENANT_NAMESPACE_SCM, 'scmm-') + } + } + + @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") + // just local + @Test + void ensureJenkinsPodIsStartedOnTenant() { + TestK8sHelper.checkAllPodsRunningInNamespace('tenant1-jenkins', 'jenkins') + } + + @Test + void ensureRegistryPodIsStartedOnTenant() { + TestK8sHelper.checkAllPodsRunningInNamespace('tenant1-registry', 'docker-registry') + } + + @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") + // just local + @Test + void ensureArgocdPodsAreStartedOnTenant() { + def argocdNamespace = TENANT_NAMESPACE_ARGOCD + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-application-controller') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-applicationset-controller') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-redis') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-repo-server') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-server') + } + + @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") + // just local + @Test + void ensureArgocdPodsAreStartedOnCentral() { + def argocdNamespace = 'argocd' + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-application-controller') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-applicationset-controller') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-redis') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-repo-server') + TestK8sHelper.checkAllPodsRunningInNamespace(argocdNamespace, 'argocd-server') + } + + @Test + void ensureScmmPodIsStarted() { + + TestK8sHelper.checkAllPodsRunningInNamespace('scm-manager') + } + + @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") + // just local + @Test + void ensureNamespacesExists() { + List expectedNamespaces = ["argocd", + "argocd-operator-system", + "scm-manager", + "default", + "tenant1-argocd", + "tenant1-jenkins", + "tenant1-registry", + "tenant1-example-apps-staging", + "tenant1-example-apps-staging", + "tenant1-scm-manager", + "kube-node-lease", + "kube-public", + "kube-system"] as List + + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + + def currentNames = client.namespaces().list().getItems() + + // 1. Verify all expected pods are present + def missingNamespace = expectedNamespaces.findAll { prefix -> !currentNames.any { it.getMetadata().getName().startsWith(prefix) } + } + assert missingNamespace.isEmpty(): "Missing these Namespace: ${missingNamespace}" + + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.groovy b/src/test/groovy/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.groovy index 7e1a03c53..1c31d989b 100644 --- a/src/test/groovy/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.groovy +++ b/src/test/groovy/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.groovy @@ -1,133 +1,125 @@ package com.cloudogu.gitops.integration.profiles +import static org.assertj.core.api.Assertions.assertThat +import static org.assertj.core.api.Assertions.fail + import com.cloudogu.gitops.integration.TestK8sHelper + +import java.util.concurrent.TimeUnit import groovy.util.logging.Slf4j + import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.KubernetesClientBuilder import io.fabric8.kubernetes.client.KubernetesClientException import org.awaitility.Awaitility import org.awaitility.core.ConditionTimeoutException -import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.DisabledIfSystemProperty import org.junit.jupiter.api.condition.EnabledIfSystemProperty -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.extension.RegisterExtension -import org.junit.jupiter.api.extension.TestWatcher - -import java.util.concurrent.TimeUnit - -import static org.assertj.core.api.Assertions.assertThat -import static org.assertj.core.api.Assertions.fail /** * This tests can only be successfull, if one of theses profiles used. * - * * To run locally: add -Dmicronaut.environments=content-examples to your execute configuration - */ + * * To run locally: add -Dmicronaut.environments=content-examples to your execute configuration*/ @Slf4j @EnabledIfSystemProperty(named = "micronaut.environments", matches = "full|operator-full|content-examples") class PetclinicProfileTestIT extends ProfileTestSetup { - static String exampleStagingNs = 'example-apps-staging' - - @BeforeAll - static void labelTest() { - println "###### Testing Petclinic ######" - // petclinic need most of time to run. If online, we can start all tests. - try { - Awaitility.await() - .atMost(40, TimeUnit.MINUTES) - .pollInterval(5, TimeUnit.SECONDS) - .untilAsserted { - waitUntilPetclinicIsRunning() - } - } catch (ConditionTimeoutException timeoutEx) { - TestK8sHelper.dumpNamespacesAndPods() - fail('Cluster not ready, sth false.', timeoutEx) - } - } - // Start condition - private static void waitUntilPetclinicIsRunning() { - // Check Pod - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - def actualPods = client.pods().inNamespace(exampleStagingNs).list().getItems() - assert !actualPods.isEmpty(): "No pods found in petclinc - namespace: ${exampleStagingNs}" - def notRunningPods = actualPods.findAll { pod -> - pod.getStatus().getPhase() != "Running" - } - assert !actualPods.isEmpty() && notRunningPods.isEmpty(): "These pods in ${exampleStagingNs} are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" - } - catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - } - - - @Test - void ensurePetclinicIsRunningOnStages() { - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - - // Check Pod - def actualPods = client.pods().inNamespace(exampleStagingNs).list().getItems() - - assert !actualPods.isEmpty(): "No pods found in petclinc - namespace: ${exampleStagingNs}" - - def notRunningPods = actualPods.findAll { pod -> - pod.getStatus().getPhase() != "Running" - } - - assert notRunningPods.isEmpty(): "These pods in ${exampleStagingNs} are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" - - } catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - } - - @DisabledIfSystemProperty(named = "micronaut.environments", matches = "full|operator-full|content-examples") - @Test - void ensurePetclinicIngressIsOnline() { - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - def nameOfServiceAndIngress = "spring-petclinic-plain" - // check Ingress - def ingress = client.network() - .v1() - .ingresses() - .inNamespace(exampleStagingNs) - .withName(nameOfServiceAndIngress) - .get() - - assert ingress != null: "Ingress '${nameOfServiceAndIngress}' not found in '${exampleStagingNs}'" - - def hosts = (ingress.spec?.rules ?: []) - .collect { it?.host } - .findAll { it } - - assert hosts.get(0).contains("petclinic") // in this case, petclinic do not care about prefix - } catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - } - - @DisabledIfSystemProperty(named = "micronaut.environments", matches = "full|operator-full|content-examples") - @Test - void ensurePetclinicServidsdsdceIsOnline() { - try (KubernetesClient client = new KubernetesClientBuilder().build()) { - - // Check Service - def nameOfServiceAndIngress = "spring-petclinic-plain" - def service = client.services() - .inNamespace(exampleStagingNs) - .withName(nameOfServiceAndIngress) - .get() - - assertThat(service).isNotNull() - - - } catch (KubernetesClientException ex) { - fail("Unexpected Kubernetes exception", ex) - } - } - -} + static String exampleStagingNs = 'example-apps-staging' + + @BeforeAll + static void labelTest() { + println "###### Testing Petclinic ######" + // petclinic need most of time to run. If online, we can start all tests. + try { + Awaitility.await() + .atMost(40, TimeUnit.MINUTES) + .pollInterval(5, TimeUnit.SECONDS) + .untilAsserted { + waitUntilPetclinicIsRunning() + } + } catch (ConditionTimeoutException timeoutEx) { + TestK8sHelper.dumpNamespacesAndPods() + fail('Cluster not ready, sth false.', timeoutEx) + } + } + + // Start condition + private static void waitUntilPetclinicIsRunning() { + // Check Pod + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + def actualPods = client.pods().inNamespace(exampleStagingNs).list().getItems() + assert !actualPods.isEmpty(): "No pods found in petclinc - namespace: ${exampleStagingNs}" + def notRunningPods = actualPods.findAll { pod -> pod.getStatus().getPhase() != "Running" + } + assert !actualPods.isEmpty() && notRunningPods.isEmpty(): "These pods in ${exampleStagingNs} are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + } + + @Test + void ensurePetclinicIsRunningOnStages() { + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + + // Check Pod + def actualPods = client.pods().inNamespace(exampleStagingNs).list().getItems() + + assert !actualPods.isEmpty(): "No pods found in petclinc - namespace: ${exampleStagingNs}" + + def notRunningPods = actualPods.findAll { pod -> pod.getStatus().getPhase() != "Running" + } + + assert notRunningPods.isEmpty(): "These pods in ${exampleStagingNs} are not yet running: ${notRunningPods.collect { it.getMetadata().getName() + ':' + it.getStatus().getPhase() }}" + + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + } + + @DisabledIfSystemProperty(named = "micronaut.environments", matches = "full|operator-full|content-examples") + @Test + void ensurePetclinicIngressIsOnline() { + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + def nameOfServiceAndIngress = "spring-petclinic-plain" + // check Ingress + def ingress = client.network() + .v1() + .ingresses() + .inNamespace(exampleStagingNs) + .withName(nameOfServiceAndIngress) + .get() + + assert ingress != null: "Ingress '${nameOfServiceAndIngress}' not found in '${exampleStagingNs}'" + + def hosts = (ingress.spec?.rules ?: []) + .collect { it?.host } + .findAll { it } + + assert hosts.get(0).contains("petclinic") // in this case, petclinic do not care about prefix + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + } + + @DisabledIfSystemProperty(named = "micronaut.environments", matches = "full|operator-full|content-examples") + @Test + void ensurePetclinicServidsdsdceIsOnline() { + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + + // Check Service + def nameOfServiceAndIngress = "spring-petclinic-plain" + def service = client.services() + .inNamespace(exampleStagingNs) + .withName(nameOfServiceAndIngress) + .get() + + assertThat(service).isNotNull() + + } catch (KubernetesClientException ex) { + fail("Unexpected Kubernetes exception", ex) + } + } + +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy index 69d48307b..a27ded8fc 100644 --- a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy @@ -1,6 +1,6 @@ package com.cloudogu.gitops.kubernetes.rbac -import org.junit.jupiter.api.Test +import static org.assertj.core.api.Assertions.assertThat import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.git.GitRepo @@ -9,53 +9,43 @@ import com.cloudogu.gitops.utils.FileSystemUtils import groovy.yaml.YamlSlurper -import static org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test class ArgocdApplicationTest { - - Config config = Config.fromMap([ - scm : [ - scmManager: [username: 'user', - password: 'pass', - host : 'localhost', - ], - gitlab : [username: 'user', - password: 'pass', - - ] - ], - application: [ - namePrefix: '', - insecure : false, - gitName : 'Test User', - gitEmail : 'test@example.com' - ] - ]) - - @Test - void 'simple ArgoCD Application with common values'() { - - GitRepo repo = new GitRepo(config, null, "my-repo", new FileSystemUtils()) - - new ArgoApplication( - 'example-apps', - 'testurl.com/argocd/example-apps', - 'testprefix-argocd', - 'testnamespace', - 'argocd/') - .generate(repo, 'testsubfolder/test') - - - File file = new File(repo.getAbsoluteLocalRepoTmpDir(), "testsubfolder/test/argocd-application-example-apps-testprefix-argocd.yaml") - assertThat(file).exists() - Map yaml = new YamlSlurper().parse(file) as Map - - assertThat(yaml["metadata"]["name"]).isEqualTo('example-apps') - assertThat(yaml["metadata"]["namespace"]).isEqualTo('testprefix-argocd') - assertThat(yaml["spec"]["destination"]["namespace"]).isEqualTo('testnamespace') - - assertThat(yaml["spec"]["source"]["path"]).isEqualTo('argocd/') - assertThat(yaml["spec"]["source"]["repoURL"]).isEqualTo('testurl.com/argocd/example-apps') - } -} + Config config = Config.fromMap([scm : [scmManager: [username: 'user', + password: 'pass', + host : 'localhost',], + gitlab : [username: 'user', + password: 'pass', + + ]], + application: [namePrefix: '', + insecure : false, + gitName : 'Test User', + gitEmail : 'test@example.com']]) + + @Test + void 'simple ArgoCD Application with common values'() { + + GitRepo repo = new GitRepo(config, null, "my-repo", new FileSystemUtils()) + + new ArgoApplication('example-apps', + 'testurl.com/argocd/example-apps', + 'testprefix-argocd', + 'testnamespace', + 'argocd/') + .generate(repo, 'testsubfolder/test') + + File file = new File(repo.getAbsoluteLocalRepoTmpDir(), "testsubfolder/test/argocd-application-example-apps-testprefix-argocd.yaml") + assertThat(file).exists() + Map yaml = new YamlSlurper().parse(file) as Map + + assertThat(yaml["metadata"]["name"]).isEqualTo('example-apps') + assertThat(yaml["metadata"]["namespace"]).isEqualTo('testprefix-argocd') + assertThat(yaml["spec"]["destination"]["namespace"]).isEqualTo('testnamespace') + + assertThat(yaml["spec"]["source"]["path"]).isEqualTo('argocd/') + assertThat(yaml["spec"]["source"]["repoURL"]).isEqualTo('testurl.com/argocd/example-apps') + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy index 5cce795b5..2c1a48dd6 100644 --- a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy @@ -1,6 +1,7 @@ package com.cloudogu.gitops.kubernetes.rbac -import org.junit.jupiter.api.Test +import static org.assertj.core.api.Assertions.assertThat +import static org.junit.jupiter.api.Assertions.assertThrows import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.git.GitRepo @@ -8,304 +9,293 @@ import com.cloudogu.gitops.utils.FileSystemUtils import groovy.yaml.YamlSlurper -import static org.assertj.core.api.Assertions.assertThat -import static org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test class RbacDefinitionTest { - private final Config config = Config.fromMap([ - scm : [ - scmManager: [ - username: 'user', - password: 'pass', - protocol: 'http', - host : 'localhost', - ], - ], - application: [ - namePrefix: '', - insecure : false, - gitName : 'Test User', - gitEmail : 'test@example.com' - ] - ]) - - private final GitRepo repo = new GitRepo(config, null, "my-repo", new FileSystemUtils()) - - @Test - void 'generates at least one RBAC YAML file'() { - new RbacDefinition(Role.Variant.ARGOCD) - .withName("access") - .withNamespace("testing") - .withServiceAccountsFrom("testing", ["reader"]) - .withRepo(repo) - .withConfig(config) - .generate() - - File outputDir = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac") - File[] yamlFiles = outputDir.listFiles({ file -> file.name.endsWith(".yaml") } as FileFilter) - List fileNames = yamlFiles.collect { it.name } - - assertThat(yamlFiles).isNotEmpty() - assertThat(fileNames).anyMatch { it.contains("role") || it.contains("rolebinding") } - } - - @Test - void 'fails if name is missing'() { - def ex = assertThrows(IllegalArgumentException) { - new RbacDefinition(Role.Variant.ARGOCD) - .withNamespace("testing") - .withServiceAccountsFrom("testing", ["reader"]) - .withRepo(repo) - .withConfig(config) - .generate() - } - - assertThat(ex.message).contains("name must not be blank") - } - - - @Test - void 'fails if namespace is missing'() { - def ex = assertThrows(IllegalArgumentException) { - new RbacDefinition(Role.Variant.ARGOCD) - .withName("access") - .withServiceAccountsFrom("testing", ["reader"]) - .withRepo(repo) - .withConfig(config) - .generate() - } - - assertThat(ex.message).contains("namespace must not be blank") - } - - @Test - void 'fails if service accounts are empty'() { - def ex = assertThrows(IllegalArgumentException) { - new RbacDefinition(Role.Variant.ARGOCD) - .withName("access") - .withNamespace("testing") - .withRepo(repo) - .withConfig(config) - .withServiceAccounts([]) // leer übergeben - .generate() - } - assertThat(ex.message).contains("At least one service account") - } - - @Test - void 'accepts service accounts via withServiceAccounts directly'() { - def sa = new ServiceAccountRef("myns", "mysa") - - new RbacDefinition(Role.Variant.ARGOCD) - .withName("direct") - .withNamespace("myns") - .withServiceAccounts([sa]) - .withRepo(repo) - .withConfig(config) - .generate() - - File f = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac/rolebinding-direct-myns.yaml") - assertThat(f).exists() - } - - @Test - void 'custom subfolder is respected'() { - String custom = "custom-dir" - new RbacDefinition(Role.Variant.ARGOCD) - .withName("custom") - .withNamespace("testing") - .withSubfolder(custom) - .withServiceAccountsFrom("testing", ["reader"]) - .withRepo(repo) - .withConfig(config) - .generate() - - File out = new File(repo.getAbsoluteLocalRepoTmpDir(), custom) - File[] yamlFiles = out.listFiles({ file -> file.name.endsWith(".yaml") } as FileFilter) - List fileNames = yamlFiles.collect { it.name } - - assertThat(yamlFiles).isNotEmpty() - assertThat(fileNames).anyMatch { it.contains("role") || it.contains("rolebinding") } - } - - @Test - void 'multiple service accounts are rendered correctly'() { - new RbacDefinition(Role.Variant.ARGOCD) - .withName("multi") - .withNamespace("testing") - .withServiceAccountsFrom("testing", ["reader", "writer", "admin"]) - .withRepo(repo) - .withConfig(config) - .generate() - - File[] files = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac").listFiles() - List fileNames = files.collect { it.name } - assertThat(fileNames).anyMatch { it.contains("role") } - } - - @Test - void 'custom role and binding file names are rendered'() { - new RbacDefinition(Role.Variant.ARGOCD) - .withName("myrole") - .withNamespace("custom-ns") - .withServiceAccountsFrom("custom-ns", ["sa1"]) - .withRepo(repo) - .withConfig(config) - .generate() - - File outputDir = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac") - List fileNames = outputDir.listFiles().collect { it.name } - - assertThat(fileNames).contains("role-myrole-custom-ns.yaml", "rolebinding-myrole-custom-ns.yaml") - } - - @Test - void 'subfolder can be nested'() { - String nested = "some/nested/path" - new RbacDefinition(Role.Variant.ARGOCD) - .withName("nestedtest") - .withNamespace("ns") - .withServiceAccountsFrom("ns", ["sa1"]) - .withSubfolder(nested) - .withRepo(repo) - .withConfig(config) - .generate() - - File outputDir = new File(repo.getAbsoluteLocalRepoTmpDir(), nested) - List fileNames = outputDir.listFiles().collect { it.name } - - assertThat(fileNames).contains("role-nestedtest-ns.yaml", "rolebinding-nestedtest-ns.yaml") - } - - @Test - void 'fails if repo is not set'() { - IllegalStateException ex = assertThrows(IllegalStateException) { - new RbacDefinition(Role.Variant.ARGOCD) - .withName("failtest") - .withNamespace("ns") - .withServiceAccountsFrom("ns", ["sa1"]) - .withConfig(config) - .generate() - } - - assertThat(ex.message).contains("SCMM repo must be set using withRepo() before calling generate()") - } - - @Test - void 'rendered rolebinding yaml contains correct service accounts'() { - List saList = ["reader", "writer"] - String ns = "rbac-test" - - new RbacDefinition(Role.Variant.ARGOCD) - .withName("test") - .withNamespace(ns) - .withServiceAccountsFrom(ns, saList) - .withRepo(repo) - .withConfig(config) - .generate() - - String path = "rbac/rolebinding-test-${ns}.yaml".toString() - File file = new File(repo.getAbsoluteLocalRepoTmpDir(), path) - Map yaml = new YamlSlurper().parse(file) as Map - - assertThat(yaml["metadata"]["name"]).isEqualTo("test") - assertThat(yaml["metadata"]["namespace"]).isEqualTo(ns) - - List names = yaml["subjects"].collect { it['name'] as String } - assertThat(names).containsExactlyInAnyOrderElementsOf(saList) - - List namespaces = yaml["subjects"].collect { it['namespace'] as String } - assertThat(namespaces).containsOnly(ns) - - assertThat(yaml["roleRef"]["name"]).isEqualTo("test") - assertThat(yaml["roleRef"]["kind"]).isEqualTo("Role") - } - - @Test - void 'rendered role yaml contains correct metadata'() { - String name = "myrole" - String ns = "custom-ns" - - new RbacDefinition(Role.Variant.ARGOCD) - .withName(name) - .withNamespace(ns) - .withServiceAccountsFrom(ns, ["sa1"]) - .withRepo(repo) - .withConfig(config) - .generate() - - String path = "rbac/role-${name}-${ns}.yaml".toString() - File file = new File(repo.getAbsoluteLocalRepoTmpDir(), path) - Map yaml = new YamlSlurper().parse(file) as Map - - assertThat(yaml["metadata"]["name"]).isEqualTo(name) - assertThat(yaml["metadata"]["namespace"]).isEqualTo(ns) - } - - @Test - void 'renders node access rules in argocd-role only when not on OpenShift'() { - config.application.openshift = false - - GitRepo tempRepo = new GitRepo(config, null, "rbac-test", new FileSystemUtils()) - - new RbacDefinition(Role.Variant.ARGOCD) - .withName("nodecheck") - .withNamespace("monitoring") - .withServiceAccountsFrom("monitoring", ["sa1"]) - .withRepo(tempRepo) - .withConfig(config) - .generate() - - File roleFile = new File(tempRepo.getAbsoluteLocalRepoTmpDir(), "rbac/role-nodecheck-monitoring.yaml") - Map yaml = new YamlSlurper().parse(roleFile) as Map - List rules = yaml["rules"] as List - - assertThat(rules).anyMatch { rule -> - List resources = rule["resources"] as List - List verbs = rule["verbs"] as List - resources.containsAll(["nodes", "nodes/metrics"]) && - verbs.containsAll(["get", "list", "watch"]) - } - } - - @Test - void 'does not render node access rules in argocd-role when on OpenShift'() { - config.application.openshift = true - - GitRepo tempRepo = new GitRepo(config, null, "rbac-test", new FileSystemUtils()) - - new RbacDefinition(Role.Variant.ARGOCD) - .withName("nodecheck") - .withNamespace("monitoring") - .withServiceAccountsFrom("monitoring", ["sa1"]) - .withRepo(tempRepo) - .withConfig(config) - .generate() - - File roleFile = new File(tempRepo.getAbsoluteLocalRepoTmpDir(), "rbac/role-nodecheck-monitoring.yaml") - Map yaml = new YamlSlurper().parse(roleFile) as Map - List rules = yaml["rules"] as List - - assertThat(rules).noneMatch { rule -> - List resources = rule["resources"] as List - resources.contains("nodes") && resources.contains("nodes/metrics") - } - } - - @Test - void 'fails if config is not set'() { - def ex = assertThrows(IllegalArgumentException) { - new RbacDefinition(Role.Variant.ARGOCD) - .withName("failtest") - .withNamespace("ns") - .withServiceAccountsFrom("ns", ["sa"]) - .withRepo(repo) - .generate() - } - - assertThat(ex.message).contains("Config must not be null") - // oder je nach deiner tatsächlichen Exception-Message - } - -} + private final Config config = Config.fromMap([scm : [scmManager: [username: 'user', + password: 'pass', + protocol: 'http', + host : 'localhost',],], + application: [namePrefix: '', + insecure : false, + gitName : 'Test User', + gitEmail : 'test@example.com']]) + + private final GitRepo repo = new GitRepo(config, null, "my-repo", new FileSystemUtils()) + + @Test + void 'generates at least one RBAC YAML file'() { + new RbacDefinition(Role.Variant.ARGOCD) + .withName("access") + .withNamespace("testing") + .withServiceAccountsFrom("testing", ["reader"]) + .withRepo(repo) + .withConfig(config) + .generate() + + File outputDir = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac") + File[] yamlFiles = outputDir.listFiles({ file -> file.name.endsWith(".yaml") } as FileFilter) + List fileNames = yamlFiles.collect { it.name } + + assertThat(yamlFiles).isNotEmpty() + assertThat(fileNames).anyMatch { it.contains("role") || it.contains("rolebinding") } + } + + @Test + void 'fails if name is missing'() { + def ex = assertThrows(IllegalArgumentException) { + new RbacDefinition(Role.Variant.ARGOCD) + .withNamespace("testing") + .withServiceAccountsFrom("testing", ["reader"]) + .withRepo(repo) + .withConfig(config) + .generate() + } + + assertThat(ex.message).contains("name must not be blank") + } + + @Test + void 'fails if namespace is missing'() { + def ex = assertThrows(IllegalArgumentException) { + new RbacDefinition(Role.Variant.ARGOCD) + .withName("access") + .withServiceAccountsFrom("testing", ["reader"]) + .withRepo(repo) + .withConfig(config) + .generate() + } + + assertThat(ex.message).contains("namespace must not be blank") + } + + @Test + void 'fails if service accounts are empty'() { + def ex = assertThrows(IllegalArgumentException) { + new RbacDefinition(Role.Variant.ARGOCD) + .withName("access") + .withNamespace("testing") + .withRepo(repo) + .withConfig(config) + .withServiceAccounts([]) // leer übergeben + .generate() + } + assertThat(ex.message).contains("At least one service account") + } + + @Test + void 'accepts service accounts via withServiceAccounts directly'() { + def sa = new ServiceAccountRef("myns", "mysa") + + new RbacDefinition(Role.Variant.ARGOCD) + .withName("direct") + .withNamespace("myns") + .withServiceAccounts([sa]) + .withRepo(repo) + .withConfig(config) + .generate() + + File f = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac/rolebinding-direct-myns.yaml") + assertThat(f).exists() + } + + @Test + void 'custom subfolder is respected'() { + String custom = "custom-dir" + new RbacDefinition(Role.Variant.ARGOCD) + .withName("custom") + .withNamespace("testing") + .withSubfolder(custom) + .withServiceAccountsFrom("testing", ["reader"]) + .withRepo(repo) + .withConfig(config) + .generate() + + File out = new File(repo.getAbsoluteLocalRepoTmpDir(), custom) + File[] yamlFiles = out.listFiles({ file -> file.name.endsWith(".yaml") } as FileFilter) + List fileNames = yamlFiles.collect { it.name } + + assertThat(yamlFiles).isNotEmpty() + assertThat(fileNames).anyMatch { it.contains("role") || it.contains("rolebinding") } + } + + @Test + void 'multiple service accounts are rendered correctly'() { + new RbacDefinition(Role.Variant.ARGOCD) + .withName("multi") + .withNamespace("testing") + .withServiceAccountsFrom("testing", ["reader", "writer", "admin"]) + .withRepo(repo) + .withConfig(config) + .generate() + + File[] files = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac").listFiles() + List fileNames = files.collect { it.name } + assertThat(fileNames).anyMatch { it.contains("role") } + } + + @Test + void 'custom role and binding file names are rendered'() { + new RbacDefinition(Role.Variant.ARGOCD) + .withName("myrole") + .withNamespace("custom-ns") + .withServiceAccountsFrom("custom-ns", ["sa1"]) + .withRepo(repo) + .withConfig(config) + .generate() + + File outputDir = new File(repo.getAbsoluteLocalRepoTmpDir(), "rbac") + List fileNames = outputDir.listFiles().collect { it.name } + + assertThat(fileNames).contains("role-myrole-custom-ns.yaml", "rolebinding-myrole-custom-ns.yaml") + } + + @Test + void 'subfolder can be nested'() { + String nested = "some/nested/path" + new RbacDefinition(Role.Variant.ARGOCD) + .withName("nestedtest") + .withNamespace("ns") + .withServiceAccountsFrom("ns", ["sa1"]) + .withSubfolder(nested) + .withRepo(repo) + .withConfig(config) + .generate() + + File outputDir = new File(repo.getAbsoluteLocalRepoTmpDir(), nested) + List fileNames = outputDir.listFiles().collect { it.name } + + assertThat(fileNames).contains("role-nestedtest-ns.yaml", "rolebinding-nestedtest-ns.yaml") + } + + @Test + void 'fails if repo is not set'() { + IllegalStateException ex = assertThrows(IllegalStateException) { + new RbacDefinition(Role.Variant.ARGOCD) + .withName("failtest") + .withNamespace("ns") + .withServiceAccountsFrom("ns", ["sa1"]) + .withConfig(config) + .generate() + } + + assertThat(ex.message).contains("SCMM repo must be set using withRepo() before calling generate()") + } + + @Test + void 'rendered rolebinding yaml contains correct service accounts'() { + List saList = ["reader", "writer"] + String ns = "rbac-test" + + new RbacDefinition(Role.Variant.ARGOCD) + .withName("test") + .withNamespace(ns) + .withServiceAccountsFrom(ns, saList) + .withRepo(repo) + .withConfig(config) + .generate() + + String path = "rbac/rolebinding-test-${ns}.yaml".toString() + File file = new File(repo.getAbsoluteLocalRepoTmpDir(), path) + Map yaml = new YamlSlurper().parse(file) as Map + + assertThat(yaml["metadata"]["name"]).isEqualTo("test") + assertThat(yaml["metadata"]["namespace"]).isEqualTo(ns) + + List names = yaml["subjects"].collect { it['name'] as String } + assertThat(names).containsExactlyInAnyOrderElementsOf(saList) + + List namespaces = yaml["subjects"].collect { it['namespace'] as String } + assertThat(namespaces).containsOnly(ns) + + assertThat(yaml["roleRef"]["name"]).isEqualTo("test") + assertThat(yaml["roleRef"]["kind"]).isEqualTo("Role") + } + + @Test + void 'rendered role yaml contains correct metadata'() { + String name = "myrole" + String ns = "custom-ns" + + new RbacDefinition(Role.Variant.ARGOCD) + .withName(name) + .withNamespace(ns) + .withServiceAccountsFrom(ns, ["sa1"]) + .withRepo(repo) + .withConfig(config) + .generate() + + String path = "rbac/role-${name}-${ns}.yaml".toString() + File file = new File(repo.getAbsoluteLocalRepoTmpDir(), path) + Map yaml = new YamlSlurper().parse(file) as Map + + assertThat(yaml["metadata"]["name"]).isEqualTo(name) + assertThat(yaml["metadata"]["namespace"]).isEqualTo(ns) + } + + @Test + void 'renders node access rules in argocd-role only when not on OpenShift'() { + config.application.openshift = false + + GitRepo tempRepo = new GitRepo(config, null, "rbac-test", new FileSystemUtils()) + + new RbacDefinition(Role.Variant.ARGOCD) + .withName("nodecheck") + .withNamespace("monitoring") + .withServiceAccountsFrom("monitoring", ["sa1"]) + .withRepo(tempRepo) + .withConfig(config) + .generate() + + File roleFile = new File(tempRepo.getAbsoluteLocalRepoTmpDir(), "rbac/role-nodecheck-monitoring.yaml") + Map yaml = new YamlSlurper().parse(roleFile) as Map + List rules = yaml["rules"] as List + + assertThat(rules).anyMatch { rule -> + List resources = rule["resources"] as List + List verbs = rule["verbs"] as List + resources.containsAll(["nodes", "nodes/metrics"]) && verbs.containsAll(["get", "list", "watch"]) + } + } + + @Test + void 'does not render node access rules in argocd-role when on OpenShift'() { + config.application.openshift = true + + GitRepo tempRepo = new GitRepo(config, null, "rbac-test", new FileSystemUtils()) + + new RbacDefinition(Role.Variant.ARGOCD) + .withName("nodecheck") + .withNamespace("monitoring") + .withServiceAccountsFrom("monitoring", ["sa1"]) + .withRepo(tempRepo) + .withConfig(config) + .generate() + + File roleFile = new File(tempRepo.getAbsoluteLocalRepoTmpDir(), "rbac/role-nodecheck-monitoring.yaml") + Map yaml = new YamlSlurper().parse(roleFile) as Map + List rules = yaml["rules"] as List + + assertThat(rules).noneMatch { rule -> + List resources = rule["resources"] as List + resources.contains("nodes") && resources.contains("nodes/metrics") + } + } + + @Test + void 'fails if config is not set'() { + def ex = assertThrows(IllegalArgumentException) { + new RbacDefinition(Role.Variant.ARGOCD) + .withName("failtest") + .withNamespace("ns") + .withServiceAccountsFrom("ns", ["sa"]) + .withRepo(repo) + .generate() + } + + assertThat(ex.message).contains("Config must not be null") + // oder je nach deiner tatsächlichen Exception-Message + } + +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy b/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy index 98d0c9958..e32c918b2 100644 --- a/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy @@ -1,9 +1,9 @@ package com.cloudogu.gitops.utils -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.lib.Ref -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test +import static groovy.test.GroovyAssert.shouldFail +import static org.assertj.core.api.Assertions.assertThat +import static org.mockito.ArgumentMatchers.* +import static org.mockito.Mockito.* import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.git.GitHandler @@ -20,189 +20,158 @@ import java.nio.file.Files import java.nio.file.Path import groovy.yaml.YamlSlurper -import static groovy.test.GroovyAssert.shouldFail -import static org.assertj.core.api.Assertions.assertThat -import static org.mockito.ArgumentMatchers.* -import static org.mockito.Mockito.* +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test class AirGappedUtilsTest { - Config config = Config.fromMap([ - application: [ - localHelmChartFolder: '', - gitName : 'Cloudogu', - gitEmail : 'hello@cloudogu.com'], - scm : [ - scmManager: [ - url: ''] - ] - ]) - - Config.HelmConfig helmConfig = new Config.HelmConfig([ - chart : 'kube-prometheus-stack', - repoURL: 'https://kube-prometheus-stack-repo-url', - version: '58.2.1' - ]) - - Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) - TestGitRepoFactory gitRepoFactory = new TestGitRepoFactory(config, new FileSystemUtils()) - FileSystemUtils fileSystemUtils = new FileSystemUtils() - TestScmManagerApiClient scmmApiClient = new TestScmManagerApiClient(config) - HelmClient helmClient = mock(HelmClient) - GitHandler gitHandler = new GitHandlerForTests(config, new ScmManagerMock()) - - @BeforeEach - void setUp() { - def response = scmmApiClient.mockSuccessfulResponse(201) - when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response) - when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response) - - } - - @Test - void 'Prepares repos for air-gapped use'() { - setupForAirgappedUse() - - def actualRepoNamespaceAndName = createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) - - assertThat(actualRepoNamespaceAndName).isEqualTo( - "${GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/kube-prometheus-stack".toString()) - assertAirGapped() - } - - @Test - void 'Fails when unable to resolve version of dependencies'() { - setupForAirgappedUse([:]) - def exception = shouldFail(RuntimeException) { - createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) - } - - assertThat(exception.message).isEqualTo( - 'Unable to determine proper version for dependency grafana (version: 7.3.*) ' + - 'from repo 3rd-party-dependencies/kube-prometheus-stack' - ) - } - - @Test - void 'Also works for charts without dependencies'() { - setupForAirgappedUse(null, []) - createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) - - GitRepo prometheusRepo = gitRepoFactory.repos['3rd-party-dependencies/kube-prometheus-stack'] - def actualPrometheusChartYaml = new YamlSlurper().parse(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.yaml')) - - def dependencies = actualPrometheusChartYaml['dependencies'] - assertThat(dependencies).isNull() - } - - @Test - void 'Fails for invalid helm charts'() { - setupForAirgappedUse() - - def expectedException = new RuntimeException() - doThrow(expectedException).when(helmClient).template(anyString(), anyString()) - - def exception = shouldFail(RuntimeException) { - createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) - } - - assertThat(exception.getMessage()).isEqualTo( - "Helm chart in folder ${rootChartsFolder}/kube-prometheus-stack seems invalid.".toString()) - assertThat(exception.getCause()).isSameAs(expectedException) - } - - protected void setupForAirgappedUse(Map chartLock = null, List dependencies = null) { - Path sourceChart = rootChartsFolder.resolve('kube-prometheus-stack') - Files.createDirectories(sourceChart) - Map prometheusChartYaml = [ - version : '1.2.3', - name : 'kube-prometheus-stack-chart', - dependencies: [ - [ - condition : 'crds.enabled', - name : 'crds', - repository: '', - version : '0.0.0' - ], - [ - condition : 'grafana.enabled', - name : 'grafana', - repository: 'https://grafana-repo-url', - version : '7.3.*', - ] - ] - ] - - if (dependencies != null) { - if (dependencies.isEmpty()) { - prometheusChartYaml.remove('dependencies') - } else { - prometheusChartYaml['dependencies'] = dependencies - } - } - - fileSystemUtils.writeYaml(prometheusChartYaml, sourceChart.resolve('Chart.yaml').toFile()) - - if (chartLock == null) { - chartLock = [ - dependencies: [ - [ - name : 'crds', - repository: "", - version : '0.0.0' - ], - [ - name : 'grafana', - repository: 'https://grafana.github.io/helm-charts', - version : '7.3.9' - ] - ] - ] - } - fileSystemUtils.writeYaml(chartLock, sourceChart.resolve('Chart.lock').toFile()) - - config.application.localHelmChartFolder = rootChartsFolder.toString() - } - - protected void assertAirGapped() { - GitRepo prometheusRepo = gitRepoFactory.repos['3rd-party-dependencies/kube-prometheus-stack'] - assertThat(prometheusRepo).isNotNull() - assertThat(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.lock')).doesNotExist() - - def ys = new YamlSlurper() - def actualPrometheusChartYaml = ys.parse(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.yaml')) - assertThat(actualPrometheusChartYaml['name']).isEqualTo('kube-prometheus-stack-chart') - - def dependencies = actualPrometheusChartYaml['dependencies'] as List - assertThat(dependencies).hasSize(2) - assertThat(dependencies[0]['name']).isEqualTo('crds') - assertThat(dependencies[0]['version']).isEqualTo('0.0.0') - assertThat(dependencies[0]['repository']).isEqualTo('') - assertThat(dependencies[1]['name']).isEqualTo('grafana') - assertThat(dependencies[1]['version']).isEqualTo('7.3.9') - assertThat(dependencies[1]['repository']).isEqualTo('') - - assertHelmRepoCommits(prometheusRepo, '1.2.3', 'Chart kube-prometheus-stack-chart, version: 1.2.3\n\n' + - 'Source: https://kube-prometheus-stack-repo-url\nDependencies localized to run in air-gapped environments') - - verify(prometheusRepo).createRepositoryAndSetPermission( - eq("Mirror of Helm chart kube-prometheus-stack from https://kube-prometheus-stack-repo-url"), - eq(false) - ) - } - - - void assertHelmRepoCommits(GitRepo repo, String expectedTag, String expectedCommitMessage) { - def commits = Git.open(new File(repo.absoluteLocalRepoTmpDir)).log().setMaxCount(1).all().call().collect() - assertThat(commits.size()).isEqualTo(1) - assertThat(commits[0].fullMessage).isEqualTo(expectedCommitMessage) - - List tags = Git.open(new File(repo.absoluteLocalRepoTmpDir)).tagList().call() - assertThat(tags.size()).isEqualTo(1) - assertThat(tags[0].name).isEqualTo("refs/tags/${expectedTag}".toString()) - } - - AirGappedUtils createAirGappedUtils() { - new AirGappedUtils(config, gitRepoFactory, fileSystemUtils, helmClient, gitHandler) - } + Config config = Config.fromMap([application: [localHelmChartFolder: '', + gitName : 'Cloudogu', + gitEmail : 'hello@cloudogu.com'], + scm : [scmManager: [url: '']]]) + + Config.HelmConfig helmConfig = new Config.HelmConfig([chart : 'kube-prometheus-stack', + repoURL: 'https://kube-prometheus-stack-repo-url', + version: '58.2.1']) + + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) + TestGitRepoFactory gitRepoFactory = new TestGitRepoFactory(config, new FileSystemUtils()) + FileSystemUtils fileSystemUtils = new FileSystemUtils() + TestScmManagerApiClient scmmApiClient = new TestScmManagerApiClient(config) + HelmClient helmClient = mock(HelmClient) + GitHandler gitHandler = new GitHandlerForTests(config, new ScmManagerMock()) + + @BeforeEach + void setUp() { + def response = scmmApiClient.mockSuccessfulResponse(201) + when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response) + when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response) + + } + + @Test + void 'Prepares repos for air-gapped use'() { + setupForAirgappedUse() + + def actualRepoNamespaceAndName = createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) + + assertThat(actualRepoNamespaceAndName).isEqualTo("${GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/kube-prometheus-stack".toString()) + assertAirGapped() + } + + @Test + void 'Fails when unable to resolve version of dependencies'() { + setupForAirgappedUse([:]) + def exception = shouldFail(RuntimeException) { + createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) + } + + assertThat(exception.message).isEqualTo('Unable to determine proper version for dependency grafana (version: 7.3.*) ' + + 'from repo 3rd-party-dependencies/kube-prometheus-stack') + } + + @Test + void 'Also works for charts without dependencies'() { + setupForAirgappedUse(null, []) + createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) + + GitRepo prometheusRepo = gitRepoFactory.repos['3rd-party-dependencies/kube-prometheus-stack'] + def actualPrometheusChartYaml = new YamlSlurper().parse(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.yaml')) + + def dependencies = actualPrometheusChartYaml['dependencies'] + assertThat(dependencies).isNull() + } + + @Test + void 'Fails for invalid helm charts'() { + setupForAirgappedUse() + + def expectedException = new RuntimeException() + doThrow(expectedException).when(helmClient).template(anyString(), anyString()) + + def exception = shouldFail(RuntimeException) { + createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) + } + + assertThat(exception.getMessage()).isEqualTo("Helm chart in folder ${rootChartsFolder}/kube-prometheus-stack seems invalid.".toString()) + assertThat(exception.getCause()).isSameAs(expectedException) + } + + protected void setupForAirgappedUse(Map chartLock = null, List dependencies = null) { + Path sourceChart = rootChartsFolder.resolve('kube-prometheus-stack') + Files.createDirectories(sourceChart) + Map prometheusChartYaml = [version : '1.2.3', + name : 'kube-prometheus-stack-chart', + dependencies: [[condition : 'crds.enabled', + name : 'crds', + repository: '', + version : '0.0.0'], + [condition : 'grafana.enabled', + name : 'grafana', + repository: 'https://grafana-repo-url', + version : '7.3.*',]]] + + if (dependencies != null) { + if (dependencies.isEmpty()) { + prometheusChartYaml.remove('dependencies') + } else { + prometheusChartYaml['dependencies'] = dependencies + } + } + + fileSystemUtils.writeYaml(prometheusChartYaml, sourceChart.resolve('Chart.yaml').toFile()) + + if (chartLock == null) { + chartLock = [dependencies: [[name : 'crds', + repository: "", + version : '0.0.0'], + [name : 'grafana', + repository: 'https://grafana.github.io/helm-charts', + version : '7.3.9']]] + } + fileSystemUtils.writeYaml(chartLock, sourceChart.resolve('Chart.lock').toFile()) + + config.application.localHelmChartFolder = rootChartsFolder.toString() + } + + protected void assertAirGapped() { + GitRepo prometheusRepo = gitRepoFactory.repos['3rd-party-dependencies/kube-prometheus-stack'] + assertThat(prometheusRepo).isNotNull() + assertThat(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.lock')).doesNotExist() + + def ys = new YamlSlurper() + def actualPrometheusChartYaml = ys.parse(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.yaml')) + assertThat(actualPrometheusChartYaml['name']).isEqualTo('kube-prometheus-stack-chart') + + def dependencies = actualPrometheusChartYaml['dependencies'] as List + assertThat(dependencies).hasSize(2) + assertThat(dependencies[0]['name']).isEqualTo('crds') + assertThat(dependencies[0]['version']).isEqualTo('0.0.0') + assertThat(dependencies[0]['repository']).isEqualTo('') + assertThat(dependencies[1]['name']).isEqualTo('grafana') + assertThat(dependencies[1]['version']).isEqualTo('7.3.9') + assertThat(dependencies[1]['repository']).isEqualTo('') + + assertHelmRepoCommits(prometheusRepo, '1.2.3', 'Chart kube-prometheus-stack-chart, version: 1.2.3\n\n' + + 'Source: https://kube-prometheus-stack-repo-url\nDependencies localized to run in air-gapped environments') + + verify(prometheusRepo).createRepositoryAndSetPermission(eq("Mirror of Helm chart kube-prometheus-stack from https://kube-prometheus-stack-repo-url"), + eq(false)) + } + + void assertHelmRepoCommits(GitRepo repo, String expectedTag, String expectedCommitMessage) { + def commits = Git.open(new File(repo.absoluteLocalRepoTmpDir)).log().setMaxCount(1).all().call().collect() + assertThat(commits.size()).isEqualTo(1) + assertThat(commits[0].fullMessage).isEqualTo(expectedCommitMessage) + + List tags = Git.open(new File(repo.absoluteLocalRepoTmpDir)).tagList().call() + assertThat(tags.size()).isEqualTo(1) + assertThat(tags[0].name).isEqualTo("refs/tags/${expectedTag}".toString()) + } + + AirGappedUtils createAirGappedUtils() { + new AirGappedUtils(config, gitRepoFactory, fileSystemUtils, helmClient, gitHandler) + } } \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/FileSystemUtilsTest.groovy b/src/test/groovy/com/cloudogu/gitops/utils/FileSystemUtilsTest.groovy index 75c036e64..32b1a7e65 100644 --- a/src/test/groovy/com/cloudogu/gitops/utils/FileSystemUtilsTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/utils/FileSystemUtilsTest.groovy @@ -1,104 +1,105 @@ package com.cloudogu.gitops.utils -import org.junit.jupiter.api.Test +import static org.assertj.core.api.Assertions.assertThat import java.nio.file.Files import java.nio.file.Path -import java.util.stream.Collectors -import static org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test class FileSystemUtilsTest { - FileSystemUtils fileSystemUtils = new FileSystemUtils() - - @Test - void copiesToTempDir() { - def expectedText = 'someText' - - File someFile = File.createTempFile(getClass().getSimpleName(), '') - someFile.withWriter { { - it.println expectedText - }} - Path tmpFile = fileSystemUtils.copyToTempDir(someFile.absolutePath) - - assertThat(tmpFile.toAbsolutePath().toString()).isNotEqualTo(someFile.getAbsoluteFile()) - assertThat(tmpFile.toFile().getText().trim()).isEqualTo(expectedText) - } - - @Test - void 'makes read-only folders writable recursively'() { - // Create temporary directory with nested structure - Path parentDir = Files.createTempDirectory(this.class.getSimpleName()) - - // Create some regular files - File regularFile = new File(parentDir.toFile(), "regularFile.txt") - regularFile.createNewFile() - - // Create nested directory - File nestedDir = new File(parentDir.toFile(), "nestedDir") - nestedDir.mkdir() - - // Create read-only file in nested directory - File readOnlyFile = new File(nestedDir, "readOnlyFile.txt") - readOnlyFile.createNewFile() - readOnlyFile.setWritable(false) - - // Create another read-only file in parent directory - File anotherReadOnlyFile = new File(parentDir.toFile(), "anotherReadOnlyFile.txt") - anotherReadOnlyFile.createNewFile() - anotherReadOnlyFile.setWritable(false) - - // Verify files are indeed read-only - assertThat(readOnlyFile.canWrite()).isFalse() - assertThat(anotherReadOnlyFile.canWrite()).isFalse() - - FileSystemUtils.makeWritable(parentDir.toFile()) - - // Verify all files are now writable - assertThat(regularFile.canWrite()).isTrue() - assertThat(readOnlyFile.canWrite()).isTrue() - assertThat(anotherReadOnlyFile.canWrite()).isTrue() - - // Clean up - parentDir.toFile().deleteDir() - } - - @Test - void 'reads and writes yaml'() { - Path tmpFile = fileSystemUtils.createTempFile() - Map yaml = [foo: 'bar', nested: [a: 1, b: 2]] - - fileSystemUtils.writeYaml(yaml, tmpFile.toFile()) - Map result = fileSystemUtils.readYaml(tmpFile) - - assertThat(result).isEqualTo(yaml) - } - - @Test - void 'readYaml falls back to classpath'() { - // testMainConfig.yaml exists in src/test/resources, so it is on the classpath - Map result = fileSystemUtils.readYaml(Path.of('testMainConfig.yaml')) - - assertThat(result) - .extracting('registry.internalPort') - .isEqualTo(30000) - } - - @Test - void 'readYaml falls back to classpath and removes src main resources'() { - // application-minimal.yaml exists in src/main/resources - // We simulate a path that might be in a config file pointing to the source tree - Map result = fileSystemUtils.readYaml(Path.of('src/main/resources/application-minimal.yaml')) - - assertThat(result) - .extracting('application.yes') - .isEqualTo(true) - } - - @Test - void 'readYaml returns empty map if not found'() { - Map result = fileSystemUtils.readYaml(Path.of('non-existent.yaml')) - assertThat(result).isEmpty() - } -} + FileSystemUtils fileSystemUtils = new FileSystemUtils() + + @Test + void copiesToTempDir() { + def expectedText = 'someText' + + File someFile = File.createTempFile(getClass().getSimpleName(), '') + someFile.withWriter { + { + it.println expectedText + } + } + Path tmpFile = fileSystemUtils.copyToTempDir(someFile.absolutePath) + + assertThat(tmpFile.toAbsolutePath().toString()).isNotEqualTo(someFile.getAbsoluteFile()) + assertThat(tmpFile.toFile().getText().trim()).isEqualTo(expectedText) + } + + @Test + void 'makes read-only folders writable recursively'() { + // Create temporary directory with nested structure + Path parentDir = Files.createTempDirectory(this.class.getSimpleName()) + + // Create some regular files + File regularFile = new File(parentDir.toFile(), "regularFile.txt") + regularFile.createNewFile() + + // Create nested directory + File nestedDir = new File(parentDir.toFile(), "nestedDir") + nestedDir.mkdir() + + // Create read-only file in nested directory + File readOnlyFile = new File(nestedDir, "readOnlyFile.txt") + readOnlyFile.createNewFile() + readOnlyFile.setWritable(false) + + // Create another read-only file in parent directory + File anotherReadOnlyFile = new File(parentDir.toFile(), "anotherReadOnlyFile.txt") + anotherReadOnlyFile.createNewFile() + anotherReadOnlyFile.setWritable(false) + + // Verify files are indeed read-only + assertThat(readOnlyFile.canWrite()).isFalse() + assertThat(anotherReadOnlyFile.canWrite()).isFalse() + + FileSystemUtils.makeWritable(parentDir.toFile()) + + // Verify all files are now writable + assertThat(regularFile.canWrite()).isTrue() + assertThat(readOnlyFile.canWrite()).isTrue() + assertThat(anotherReadOnlyFile.canWrite()).isTrue() + + // Clean up + parentDir.toFile().deleteDir() + } + + @Test + void 'reads and writes yaml'() { + Path tmpFile = fileSystemUtils.createTempFile() + Map yaml = [foo: 'bar', nested: [a: 1, b: 2]] + + fileSystemUtils.writeYaml(yaml, tmpFile.toFile()) + Map result = fileSystemUtils.readYaml(tmpFile) + + assertThat(result).isEqualTo(yaml) + } + + @Test + void 'readYaml falls back to classpath'() { + // testMainConfig.yaml exists in src/test/resources, so it is on the classpath + Map result = fileSystemUtils.readYaml(Path.of('testMainConfig.yaml')) + + assertThat(result) + .extracting('registry.internalPort') + .isEqualTo(30000) + } + + @Test + void 'readYaml falls back to classpath and removes src main resources'() { + // application-minimal.yaml exists in src/main/resources + // We simulate a path that might be in a config file pointing to the source tree + Map result = fileSystemUtils.readYaml(Path.of('src/main/resources/application-minimal.yaml')) + + assertThat(result) + .extracting('application.yes') + .isEqualTo(true) + } + + @Test + void 'readYaml returns empty map if not found'() { + Map result = fileSystemUtils.readYaml(Path.of('non-existent.yaml')) + assertThat(result).isEmpty() + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/data/contentRepos/folderBasedRepo1/common/repo/some.yaml.ftl b/src/test/groovy/com/cloudogu/gitops/utils/data/contentRepos/folderBasedRepo1/common/repo/some.yaml.ftl index 839931839..c6be2d1b2 100644 --- a/src/test/groovy/com/cloudogu/gitops/utils/data/contentRepos/folderBasedRepo1/common/repo/some.yaml.ftl +++ b/src/test/groovy/com/cloudogu/gitops/utils/data/contentRepos/folderBasedRepo1/common/repo/some.yaml.ftl @@ -1,5 +1,5 @@ # Just write some variable to smoke test templating namePrefix: ${config.application.namePrefix} <#if config.content.variables.someapp??> -myvar: ${config.content.variables.someapp.somevalue} + myvar: ${config.content.variables.someapp.somevalue} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/git/ScmManagerMock.groovy b/src/test/groovy/com/cloudogu/gitops/utils/git/ScmManagerMock.groovy index 8a54a5201..6cf95928d 100644 --- a/src/test/groovy/com/cloudogu/gitops/utils/git/ScmManagerMock.groovy +++ b/src/test/groovy/com/cloudogu/gitops/utils/git/ScmManagerMock.groovy @@ -6,123 +6,121 @@ import com.cloudogu.gitops.git.providers.GitProvider import com.cloudogu.gitops.git.providers.RepoUrlScope import com.cloudogu.gitops.git.providers.Scope - /** * Lightweight test double for ScmManager/GitProvider. * - Configurable in-cluster and client bases * - Optional namePrefix to model “tenant” behavior - * - Records createRepository / setRepositoryPermission calls for assertions - */ + * - Records createRepository / setRepositoryPermission calls for assertions*/ class ScmManagerMock implements GitProvider { - private final Set initOnceRepos = [] as Set - private final Map createCalls = [:].withDefault{0} - - void initOnceRepo(String fullName) { initOnceRepos << fullName } - void clearInitOnce() { initOnceRepos.clear(); createCalls.clear() } - - - // --- configurable --- - URI inClusterBase = new URI("http://scmm.scm-manager.svc.cluster.local/scm") - URI clientBase = new URI("http://localhost:8080/scm") - String namePrefix = "" // e.g., "fv40-" for tenant mode - Credentials credentials = new Credentials("gitops", "gitops") - String gitOpsUsername = "gitops" - URI prometheus = new URI("http://localhost:8080/scm/api/v2/metrics/prometheus") - - // --- call recordings for assertions --- - final List createdRepos = [] - final List permissionCalls = [] - /** Optional sequence to control createRepository() return values per call */ - List nextCreateResults = [] // empty -> default true - - @Override - boolean createRepository(String repoTarget, String description, boolean initialize) { - if (initOnceRepos.contains(repoTarget)) { - return ++createCalls[repoTarget] == 1 // 1. call true, then false - } - createdRepos << repoTarget - // Pretend repository was created successfully. - // If you need idempotency checks, examine createdRepos.count(repoTarget) in your tests. - return nextCreateResults ? nextCreateResults.remove(0) : true - } - - @Override - void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) { - permissionCalls << [ - repoTarget: repoTarget, - principal : principal, - role : role, - scope : scope - ] - } - - /** …/scm/repo// */ - @Override - String repoUrl(String repoTarget, RepoUrlScope scope) { - URI base = (scope == RepoUrlScope.CLIENT) ? clientBase : inClusterBase - def cleanedBase = withoutTrailingSlash(base).toString() - return "${cleanedBase}/repo/${repoTarget}" - } - - /** In-cluster repo prefix: …/scm/repo/[] */ - @Override - String repoPrefix() { - def base = withoutTrailingSlash(inClusterBase).toString() - def prefix = (namePrefix ?: "").strip() - return "${base}/repo/${prefix}" - } - - @Override - Credentials getCredentials() { - return credentials - } - - - /** …/scm/api/v2/metrics/prometheus */ - @Override - URI prometheusMetricsEndpoint() { - return prometheus - } - - @Override - void deleteRepository(String namespace, String repository, boolean prefixNamespace) { - - } - - @Override - void deleteUser(String name) { - - } - - @Override - void setDefaultBranch(String repoTarget, String branch) { - - } - - /** In-cluster base …/scm (without trailing slash) */ - @Override - String getUrl() { - return inClusterBase.toString() - } - - @Override - String getProtocol() { - return inClusterBase.scheme // e.g., "http" - } - - @Override - String getHost() { - return inClusterBase.host // e.g., "scmm.ns.svc.cluster.local" - } - - @Override - String getGitOpsUsername() { - return gitOpsUsername - } - // --- helpers --- - private static URI withoutTrailingSlash(URI uri) { - def s = uri.toString() - return new URI(s.endsWith("/") ? s.substring(0, s.length() - 1) : s) - } -} + private final Set initOnceRepos = [] as Set + private final Map createCalls = [:].withDefault { 0 } + + void initOnceRepo(String fullName) { initOnceRepos << fullName } + + void clearInitOnce() { initOnceRepos.clear(); createCalls.clear() } + + // --- configurable --- + URI inClusterBase = new URI("http://scmm.scm-manager.svc.cluster.local/scm") + URI clientBase = new URI("http://localhost:8080/scm") + String namePrefix = "" + // e.g., "fv40-" for tenant mode + Credentials credentials = new Credentials("gitops", "gitops") + String gitOpsUsername = "gitops" + URI prometheus = new URI("http://localhost:8080/scm/api/v2/metrics/prometheus") + + // --- call recordings for assertions --- + final List createdRepos = [] + final List permissionCalls = [] + /** Optional sequence to control createRepository() return values per call */ + List nextCreateResults = [] + // empty -> default true + + @Override + boolean createRepository(String repoTarget, String description, boolean initialize) { + if (initOnceRepos.contains(repoTarget)) { + return ++createCalls[repoTarget] == 1 // 1. call true, then false + } + createdRepos << repoTarget + // Pretend repository was created successfully. + // If you need idempotency checks, examine createdRepos.count(repoTarget) in your tests. + return nextCreateResults ? nextCreateResults.remove(0) : true + } + + @Override + void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) { + permissionCalls << [repoTarget: repoTarget, + principal : principal, + role : role, + scope : scope] + } + + /** …/scm/repo// */ + @Override + String repoUrl(String repoTarget, RepoUrlScope scope) { + URI base = (scope == RepoUrlScope.CLIENT) ? clientBase : inClusterBase + def cleanedBase = withoutTrailingSlash(base).toString() + return "${cleanedBase}/repo/${repoTarget}" + } + + /** In-cluster repo prefix: …/scm/repo/[] */ + @Override + String repoPrefix() { + def base = withoutTrailingSlash(inClusterBase).toString() + def prefix = (namePrefix ?: "").strip() + return "${base}/repo/${prefix}" + } + + @Override + Credentials getCredentials() { + return credentials + } + + /** …/scm/api/v2/metrics/prometheus */ + @Override + URI prometheusMetricsEndpoint() { + return prometheus + } + + @Override + void deleteRepository(String namespace, String repository, boolean prefixNamespace) { + + } + + @Override + void deleteUser(String name) { + + } + + @Override + void setDefaultBranch(String repoTarget, String branch) { + + } + + /** In-cluster base …/scm (without trailing slash) */ + @Override + String getUrl() { + return inClusterBase.toString() + } + + @Override + String getProtocol() { + return inClusterBase.scheme // e.g., "http" + } + + @Override + String getHost() { + return inClusterBase.host // e.g., "scmm.ns.svc.cluster.local" + } + + @Override + String getGitOpsUsername() { + return gitOpsUsername + } + + // --- helpers --- + private static URI withoutTrailingSlash(URI uri) { + def s = uri.toString() + return new URI(s.endsWith("/") ? s.substring(0, s.length() - 1) : s) + } +} \ No newline at end of file