From 0c2c1a8427bb59ded2f8e5ec8d77f00bf97ea53c Mon Sep 17 00:00:00 2001 From: Thomas Michael Date: Thu, 3 Sep 2026 14:31:45 +0200 Subject: [PATCH] using credentials for all gitops config options --- Makefile | 8 + docs/Configuration.md | 291 +- docs/Developers.md | 2 +- docs/configuration.schema.json | 3009 +++++++++-------- scripts/dev/gop-secrets-values.yaml | 79 + scripts/dev/gop-secrets.yaml | 39 + .../gitops/application/Application.java | 3 +- .../gitops/cli/GitopsPlaygroundCli.java | 21 +- .../com/cloudogu/gitops/config/Config.java | 25 +- .../cloudogu/gitops/config/Credentials.java | 12 + .../gitops/config/scm/ScmCentralSchema.java | 11 +- .../gitops/config/scm/ScmTenantSchema.java | 13 +- .../kubernetes/api/K8sClient.java | 2684 ++++++++------- .../gitops/tools/common/CommonToolConfig.java | 116 +- .../cli/ApplicationConfiguratorTest.groovy | 11 +- .../gitops/config/schema/ConfigTest.groovy | 128 +- .../schema/CredentialsDelegationTest.groovy | 100 + .../kubernetes/api/K8sClientTest.groovy | 2 +- .../scmmanager/ScmManagerSetupTest.groovy | 4 +- 19 files changed, 3493 insertions(+), 3065 deletions(-) create mode 100644 scripts/dev/gop-secrets-values.yaml create mode 100644 scripts/dev/gop-secrets.yaml create mode 100644 src/test/groovy/com/cloudogu/gitops/config/schema/CredentialsDelegationTest.groovy diff --git a/Makefile b/Makefile index 3f03e38b0..7d545a17c 100644 --- a/Makefile +++ b/Makefile @@ -34,5 +34,13 @@ image: ## builds the docker image for local testing docker buildx prune -f && docker build . -t local/gop echo "created docker image local/gop" +.PHONY: gop-config-in-secrets +gop-config-in-secrets: ## installs cluster and add secrets for gop-tools for easy testing + ./scripts/init-cluster.sh + kubectl create namespace gop-job + kubectl apply -f ./scripts/dev/gop-secrets.yaml + echo "cluster with credentials in secrets" + + %: @: diff --git a/docs/Configuration.md b/docs/Configuration.md index 74d06ef63..06328be06 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -21,135 +21,182 @@ parameters. ## 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-namespace` | `registry.namespace` | String | `registry` | Optional defines the kubernetes namespace for registry. | -| - | `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.credentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.credentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.credentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.credentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.credentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `--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.proxyCredentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.proxyCredentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.proxyCredentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.proxyCredentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `registry.proxyCredentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `--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-namespace` | `registry.namespace` | String | `registry` | Optional defines the kubernetes namespace for registry. | +| - | `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 | `xHX6SPqtRtpo` | 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 | -| `--jenkins-image` | `jenkins.jenkinsImage` | String | `` | Sets image for Jenkins | -| `--maven-central-mirror` | `jenkins.mavenCentralMirror` | String | `` | URL for maven mirror, used by applications built in Jenkins | -| - | `jenkins.oidc.providerName` | String | `Keycloak` | Name of the OIDC provider displayed in tool login screens | -| - | `jenkins.oidc.issuerUrl` | String | `` | OIDC issuer URL, for example http://keycloak.local.gd/realms/gop | -| - | `jenkins.oidc.clientId` | String | `jenkins` | OIDC client ID | -| - | `jenkins.oidc.clientSecret` | String | `` | OIDC client secret | -| - | `jenkins.oidc.scopes` | List<String> | `[openid, profile, email]` | OIDC scopes requested by the tool | -| - | `jenkins.oidc.adminGroupName` | String | `` | OIDC group that receives full admin permissions in all OIDC-enabled tools | -| `--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 | -| `--jenkins-namespace` | `jenkins.namespace` | String | `jenkins` | Optional defines the kubernetes namespace for 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 | `EYQkU&j1A4FJ` | Mandatory when jenkins-url is set | +| - | `jenkins.credentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.credentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.credentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.credentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.credentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `--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 | +| - | `jenkins.metricsCredentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.metricsCredentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.metricsCredentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.metricsCredentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `jenkins.metricsCredentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `--jenkins-image` | `jenkins.jenkinsImage` | String | `` | Sets image for Jenkins | +| `--maven-central-mirror` | `jenkins.mavenCentralMirror` | String | `` | URL for maven mirror, used by applications built in Jenkins | +| - | `jenkins.oidc.providerName` | String | `Keycloak` | Name of the OIDC provider displayed in tool login screens | +| - | `jenkins.oidc.issuerUrl` | String | `` | OIDC issuer URL, for example http://keycloak.local.gd/realms/gop | +| - | `jenkins.oidc.clientId` | String | `jenkins` | OIDC client ID | +| - | `jenkins.oidc.clientSecret` | String | `` | OIDC client secret | +| - | `jenkins.oidc.scopes` | List<String> | `[openid, profile, email]` | OIDC scopes requested by the tool | +| - | `jenkins.oidc.adminGroupName` | String | `` | OIDC group that receives full admin permissions in all OIDC-enabled tools | +| `--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 | +| `--jenkins-namespace` | `jenkins.namespace` | String | `jenkins` | Optional defines the kubernetes namespace for Jenkins. | ## 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-scm-provider` | `multiTenant.scmProviderType` | ScmProviderType | `SCM_MANAGER` | The SCM provider type. Possible values: SCM_MANAGER, GITLAB | +| `--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 | +| - | `multiTenant.gitlab.credentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.gitlab.credentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.gitlab.credentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.gitlab.credentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.gitlab.credentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `--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 | +| - | `multiTenant.scmManager.credentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.scmManager.credentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.scmManager.credentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.scmManager.credentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `multiTenant.scmManager.credentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `--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-image` | `scm.scmManager.scmmImage` | String | `-` | Sets image for SCM-Manager | -| `--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-provider` | `scm.scmProviderType` | ScmProviderType | `SCM_MANAGER` | The SCM provider type. Possible values: SCM_MANAGER, GITLAB | +| - | `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 | +| - | `scm.gitlab.credentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.gitlab.credentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.gitlab.credentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.gitlab.credentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.gitlab.credentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `--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.credentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.scmManager.credentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.scmManager.credentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.scmManager.credentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `scm.scmManager.credentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `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-image` | `scm.scmManager.scmmImage` | String | `-` | Sets image for SCM-Manager | +| `--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 | `xHX6SPqtRtpo` | 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. | -| `-n`, `--namespace` | `application.namespace` | String | `` | If set, GOP uses the same Kubernetes namespace for all tools and examples. Attention! Only use for test purposes. | +| CLI | Config key | Type | Default | Description | +|:-------------------------|:------------------------------------------|:-------------------|:---------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--config-file` | `application.configFiles` | List<String> | `[]` | - | +| `--config-map` | `application.configMaps` | List<String> | `[]` | - | +| `-d`, `--debug` | `application.debug` | Boolean | `false` | - | +| `-x`, `--trace` | `application.trace` | Boolean | `false` | - | +| `--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 | `EYQkU&j1A4FJ` | Set initial admin passwords | +| - | `application.credentials.username` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `application.credentials.secretNamespace` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `application.credentials.secretName` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `application.credentials.usernameKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| - | `application.credentials.passwordKey` | String | `-` | Credentials Object to authenticate against content repo. Allows using a K8s Secret | +| `-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. | +| `-n`, `--namespace` | `application.namespace` | String | `` | If set, GOP uses the same Kubernetes namespace for all tools and examples. Attention! Only use for test purposes. | ## 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> | `[]` | Additional Helm releases to deploy through Argo CD without requiring a content Git repository. | -| `--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> | `[]` | Additional Helm releases to deploy through Argo CD without requiring a content Git repository. | +| `--content-whitelist` | `content.useWhitelist` | Boolean | `false` | Enables the whitelist for statics in content templating | +| - | `content.allowedStaticsWhitelist` | Set<String> | `[com.cloudogu.gitops.utils.DockerImageParser, java.lang.Float, java.lang.Long, java.lang.Double, java.lang.Boolean, java.lang.Math, java.lang.String, java.lang.Integer]` | Whitelist for Statics freemarker is allowing in user templates | ## Tools @@ -168,7 +215,7 @@ Configuration of optional tools supported by gitops-playground. | `--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 | +| - | `features.argocd.values` | Map | `{}` | Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration | | - | `features.argocd.oidc.providerName` | String | `Keycloak` | Name of the OIDC provider displayed in tool login screens | | - | `features.argocd.oidc.issuerUrl` | String | `` | OIDC issuer URL, for example http://keycloak.local.gd/realms/gop | | - | `features.argocd.oidc.clientId` | String | `argocd` | OIDC client ID | @@ -204,7 +251,7 @@ Configuration of optional tools supported by gitops-playground. | `--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.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 | @@ -217,10 +264,11 @@ Configuration of optional tools supported by gitops-playground. | `--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.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` | `features.secrets.vault.mode` | VaultMode | `-` | Installs Hashicorp vault and the external secrets operator. Possible values: dev, prod. | | `--vault-url` | `features.secrets.vault.url` | String | `` | Sets url for vault ui | | - | `features.secrets.vault.oidc.providerName` | String | `Keycloak` | Name of the OIDC provider displayed in tool login screens | | - | `features.secrets.vault.oidc.issuerUrl` | String | `` | OIDC issuer URL, for example http://keycloak.local.gd/realms/gop | @@ -229,7 +277,7 @@ Configuration of optional tools supported by gitops-playground. | - | `features.secrets.vault.oidc.scopes` | List<String> | `[openid, profile, email]` | OIDC scopes requested by the tool | | - | `features.secrets.vault.oidc.adminGroupName` | String | `` | OIDC group that receives full admin permissions in all OIDC-enabled tools | | `--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.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 | @@ -241,7 +289,7 @@ Configuration of optional tools supported by gitops-playground. |:----------------------|:------------------------------------|:--------|:-----------------------------------|:-----------------------------------------------------------------------------------------------------------------------| | `--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.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 | @@ -259,7 +307,8 @@ Configuration of optional tools supported by gitops-playground. | `--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.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 +| - | `features.certManager.helm.version` | String | `1.19.4` | The version of the Helm chart to be installed | + diff --git a/docs/Developers.md b/docs/Developers.md index 2c835c31d..46382634b 100644 --- a/docs/Developers.md +++ b/docs/Developers.md @@ -355,7 +355,7 @@ 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 . -GOP_IMAGE=ghcr.io/cloudogu/gitops-playground +GOP_IMAGE=local/gop PATH_TWO_REGISTRIES=./scripts/local/two-registries.yaml #Adjust to path above docker run --rm -t -u $(id -u) \ diff --git a/docs/configuration.schema.json b/docs/configuration.schema.json index c05e36dab..ffd598f5c 100644 --- a/docs/configuration.schema.json +++ b/docs/configuration.schema.json @@ -1,1563 +1,1608 @@ { - "$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 - }, - "Map(String,Object)-nullable": { - "type": [ - "object", - "null" - ] - }, - "Map(String,String)": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "OidcSchema-nullable": { - "type": [ - "object", - "null" - ], - "properties": { - "adminGroupName": { - "type": [ - "string", - "null" - ], - "description": "OIDC group that receives full admin permissions in all OIDC-enabled tools" - }, - "clientId": { - "type": [ - "string", - "null" - ], - "description": "OIDC client ID" - }, - "clientSecret": { - "type": [ - "string", - "null" - ], - "description": "OIDC client secret" - }, - "issuerUrl": { - "type": [ - "string", - "null" - ], - "description": "OIDC issuer URL, for example http://keycloak.local.gd/realms/gop" - }, - "providerName": { - "type": [ - "string", - "null" - ], - "description": "Name of the OIDC provider displayed in tool login screens" - }, - "scopes": { - "description": "OIDC scopes requested by the tool", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "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" - }, - "namespace": { - "type": [ - "string", - "null" - ], - "description": "If set, GOP uses the same Kubernetes namespace for all tools and examples. Attention! Only use for test purposes." - }, - "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" - }, - "content": { - "type": [ - "object", - "null" - ], - "properties": { - "allowedStaticsWhitelist": { - "description": "Whitelist for Statics freemarker is allowing in user templates", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "helmReleases": { - "description": "Additional Helm releases to deploy through Argo CD without requiring a content Git repository.", - "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." - }, - "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." - }, - "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." - }, - "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." - } - }, - "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" - } - }, - "repos": { - "description": "ContentLoader repos to push into target environment", - "type": [ - "array", - "null" - ], - "items": { - "type": "object", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Credentials-nullable": { + "type": [ + "object", + "null" + ], "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" + "passwordKey": { + "type": [ + "string", + "null" ], "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" - }, - "secretName": { + }, + "secretName": { "type": [ - "string", - "null" + "string", + "null" ], "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" - }, - "secretNamespace": { + }, + "secretNamespace": { "type": [ - "string", - "null" + "string", + "null" ], "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" - }, - "username": { + }, + "username": { "type": [ - "string", - "null" + "string", + "null" ], "description": "Credentials Object to authenticate against content repo. Allows using a K8s Secret" - }, - "usernameKey": { + }, + "usernameKey": { "type": [ - "string", - "null" + "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" - }, - "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" - }, - "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." - }, - "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" - }, - "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." - } - }, - "additionalProperties": false - } - }, - "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." - } - }, - "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" } - } - }, - "namespace": { - "type": [ - "string", - "null" - ], - "description": "Defines the kubernetes namespace for ArgoCD" - }, - "oidc": { - "$ref": "#/$defs/OidcSchema-nullable", - "description": "OIDC Config for this tool. See docs for more infos" }, - "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" - }, - "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" - } - }, - "additionalProperties": false, - "description": "Config Parameter for the ArgoCD Operator" + "additionalProperties": false }, - "certManager": { - "type": [ - "object", - "null" - ], - "properties": { - "active": { - "type": [ - "boolean", - "null" - ], - "description": "Sets and enables Cert Manager" - }, - "helm": { - "type": [ + "HelmConfigWithValues-nullable": { + "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" - }, + ], + "properties": { "chart": { - "type": [ - "string", - "null" - ], - "description": "Name of the Helm chart" - }, - "image": { - "type": [ - "string", - "null" - ], - "description": "Sets image for Cert Manager" + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" }, "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" + "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" + "$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" - }, - "webhookImage": { - "type": [ - "string", - "null" - ], - "description": "Sets webhook Image for Cert Manager" + "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." }, - "issuer": { - "type": [ - "string", - "null" - ], - "description": "Sets and enables Cert Manager" - }, - "namespace": { - "type": [ - "string", - "null" - ], - "description": "Optional defines the kubernetes namespace for Cert Manager" - } - }, - "additionalProperties": false, - "description": "Config parameters for the Cert Manager" + "additionalProperties": false }, - "ingress": { - "type": [ - "object", - "null" - ], - "properties": { - "active": { - "type": [ - "boolean", - "null" - ], - "description": "Sets and enables Ingress Controller" - }, - "helm": { - "type": [ + "Map(String,Object)-nullable": { + "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" - }, - "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, - "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." - }, - "ingressNamespace": { - "type": [ - "string", - "null" - ], - "description": "Optional defines the kubernetes namespace for Ingress Controller" - } - }, - "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" - }, - "grafanaImage": { - "type": [ - "string", - "null" - ], - "description": "Sets image for grafana" - }, - "grafanaSidecarImage": { - "type": [ - "string", - "null" - ], - "description": "Sets image for grafana's sidecar" - }, - "prometheusConfigReloaderImage": { - "type": [ - "string", - "null" - ], - "description": "Sets image for prometheus-operator's config-reloader" - }, - "prometheusImage": { - "type": [ - "string", - "null" - ], - "description": "Sets image for prometheus" - }, - "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" - }, - "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, - "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": "Optional defines the kubernetes namespace for monitoring." - }, - "oidc": { - "$ref": "#/$defs/OidcSchema-nullable", - "description": "OIDC Config for this tool. See docs for more infos" + "Map(String,String)": { + "type": "object", + "additionalProperties": { + "type": "string" } - }, - "additionalProperties": false, - "description": "Config parameters for the Monitoring system (prometheus)" }, - "secrets": { - "type": [ - "object", - "null" - ], - "properties": { - "externalSecrets": { - "type": [ + "OidcSchema-nullable": { + "type": [ "object", "null" - ], - "properties": { - "helm": { - "type": [ - "object", - "null" - ], - "properties": { - "certControllerImage": { - "type": [ + ], + "properties": { + "adminGroupName": { + "type": [ "string", "null" - ], - "description": "Sets image for external secrets operator's controller" - }, - "chart": { - "type": [ + ], + "description": "OIDC group that receives full admin permissions in all OIDC-enabled tools" + }, + "clientId": { + "type": [ "string", "null" - ], - "description": "Name of the Helm chart" - }, - "image": { - "type": [ + ], + "description": "OIDC client ID" + }, + "clientSecret": { + "type": [ "string", "null" - ], - "description": "Sets image for external secrets operator" - }, - "repoURL": { - "type": [ + ], + "description": "OIDC client secret" + }, + "issuerUrl": { + "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": [ + ], + "description": "OIDC issuer URL, for example http://keycloak.local.gd/realms/gop" + }, + "providerName": { + "type": [ "string", "null" - ], - "description": "The version of the Helm chart to be installed" - }, - "webhookImage": { - "type": [ - "string", + ], + "description": "Name of the OIDC provider displayed in tool login screens" + }, + "scopes": { + "description": "OIDC scopes requested by the tool", + "type": [ + "array", "null" - ], - "description": "Sets image for external secrets operator's webhook" + ], + "items": { + "type": "string" } - }, - "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" - }, - "namespace": { - "type": [ - "string", - "null" - ], - "description": "Optional defines the kubernetes namespace for secrets." }, - "vault": { - "type": [ + "additionalProperties": false + }, + "ScmProviderType-nullable": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "GITLAB", + "SCM_MANAGER" + ] + } + ] + } + }, + "type": "object", + "properties": { + "application": { + "type": [ "object", "null" - ], - "properties": { - "helm": { - "type": [ - "object", - "null" - ], - "properties": { - "chart": { - "type": [ + ], + "properties": { + "baseUrl": { + "type": [ "string", "null" - ], - "description": "Name of the Helm chart" - }, - "image": { - "type": [ + ], + "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" + }, + "credentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "Application configuration parameter for GOP" + }, + "destroy": { + "type": [ + "boolean", + "null" + ], + "description": "Unroll playground" + }, + "gitEmail": { + "type": [ "string", "null" - ], - "description": "Sets image for vault" - }, - "repoURL": { - "type": [ + ], + "description": "Sets git author and committer email used for initial commits" + }, + "gitName": { + "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": [ + ], + "description": "Sets git author and committer name used for initial commits" + }, + "gopNamespace": { + "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." - }, - "mode": { - "anyOf": [ - { - "type": "null" - }, - { - "type": "string", - "enum": [ - "dev", - "prod" - ] - } - ], - "description": "Installs Hashicorp vault and the external secrets operator. Possible values: dev, prod." + ], + "description": "If set, GOP stores specific information in this namespace." }, - "oidc": { - "$ref": "#/$defs/OidcSchema-nullable", - "description": "OIDC Config for this tool. See docs for more infos" + "insecure": { + "type": [ + "boolean", + "null" + ], + "description": "Sets insecure-mode in cURL which skips cert validation" }, - "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 management" - } - }, - "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" - } - }, - "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." - }, - "jenkinsImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for 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" - }, - "metricsUsername": { - "type": [ - "string", - "null" - ], - "description": "Mandatory when jenkins-url is set and monitoring enabled" - }, - "namespace": { - "type": [ - "string", - "null" - ], - "description": "Optional defines the kubernetes namespace for Jenkins." - }, - "oidc": { - "$ref": "#/$defs/OidcSchema-nullable", - "description": "OIDC Config for this tool. See docs for more infos" - }, - "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." - }, - "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" - }, - "username": { - "type": [ - "string", - "null" - ], - "description": "Mandatory when jenkins-url is set" - } - }, - "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" + "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" + }, + "namespace": { + "type": [ + "string", + "null" + ], + "description": "If set, GOP uses the same Kubernetes namespace for all tools and examples. Attention! Only use for test purposes." + }, + "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" + } }, - "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" + "additionalProperties": false, + "description": "Application configuration parameter for GOP" }, - "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", + "content": { + "type": [ + "object", "null" - ], - "description": "URL for the centralized Management Repo" + ], + "properties": { + "allowedStaticsWhitelist": { + "description": "Whitelist for Statics freemarker is allowing in user templates", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "helmReleases": { + "description": "Additional Helm releases to deploy through Argo CD without requiring a content Git repository.", + "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." + }, + "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." + }, + "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." + }, + "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." + } + }, + "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" + } + }, + "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": { + "$ref": "#/$defs/Credentials-nullable", + "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." + }, + "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!" + }, + "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." + }, + "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.)" + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "URL of the content repo. Mandatory for each type." + } + }, + "additionalProperties": false + } + }, + "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." + } }, - "username": { - "type": [ - "string", - "null" - ], - "description": "CENTRAL SCMM username" - } - }, - "additionalProperties": false, - "description": "Config for SCM-Manager" - }, - "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" - } - }, - "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" - }, - "namespace": { - "type": [ - "string", - "null" - ], - "description": "Optional defines the kubernetes namespace for registry." - }, - "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." + "additionalProperties": false, + "description": "Config parameters for content, i.e. end-user or tenant applications as opposed to cluster-resources" }, - "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" - }, - "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", + "features": { + "type": [ + "object", "null" - ], - "description": "Base URL for the Gitlab instance" + ], + "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" + }, + "oidc": { + "$ref": "#/$defs/OidcSchema-nullable", + "description": "OIDC Config for this tool. See docs for more infos" + }, + "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" + }, + "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" + } + }, + "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" + }, + "chart": { + "type": [ + "string", + "null" + ], + "description": "Name of the Helm chart" + }, + "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" + }, + "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" + }, + "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" + } + }, + "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" + }, + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Optional defines the kubernetes namespace for 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" + }, + "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" + }, + "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, + "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." + }, + "ingressNamespace": { + "type": [ + "string", + "null" + ], + "description": "Optional defines the kubernetes namespace for Ingress Controller" + } + }, + "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" + }, + "grafanaImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for grafana" + }, + "grafanaSidecarImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for grafana's sidecar" + }, + "prometheusConfigReloaderImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for prometheus-operator's config-reloader" + }, + "prometheusImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for prometheus" + }, + "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" + }, + "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, + "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": "Optional defines the kubernetes namespace for monitoring." + }, + "oidc": { + "$ref": "#/$defs/OidcSchema-nullable", + "description": "OIDC Config for this tool. See docs for more infos" + } + }, + "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" + }, + "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" + }, + "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" + }, + "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": "Config parameters for the external secrets operator" + }, + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Optional defines the kubernetes namespace for secrets." + }, + "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" + }, + "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, + "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." + }, + "oidc": { + "$ref": "#/$defs/OidcSchema-nullable", + "description": "OIDC Config for this tool. See docs for more infos" + }, + "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 management" + } }, - "username": { - "type": [ - "string", - "null" - ], - "description": "Defaults to: oauth2.0 when PAT token is given." - } - }, - "additionalProperties": false, - "description": "Config for GITLAB" + "additionalProperties": false, + "description": "Config parameters for features or tools" }, - "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", + "jenkins": { + "type": [ + "object", "null" - ], - "description": "Mandatory when scmm-url is set" - }, - "scmmImage" : { - "type" : [ "string", "null" ], - "description" : "Sets image for SCM-Manager" + ], + "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" + } + }, + "credentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "Config parameters for Jenkins CI/CD Pipeline Server" + }, + "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." + }, + "jenkinsImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for Jenkins" + }, + "mavenCentralMirror": { + "type": [ + "string", + "null" + ], + "description": "URL for maven mirror, used by applications built in Jenkins" + }, + "metricsCredentials": { + "$ref": "#/$defs/Credentials-nullable", + "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" + }, + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Optional defines the kubernetes namespace for Jenkins." + }, + "oidc": { + "$ref": "#/$defs/OidcSchema-nullable", + "description": "OIDC Config for this tool. See docs for more infos" + }, + "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." + }, + "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" + }, + "username": { + "type": [ + "string", + "null" + ], + "description": "Mandatory when jenkins-url is set" + } }, - "skipPlugins": { - "type": [ - "boolean", + "additionalProperties": false, + "description": "Config parameters for Jenkins CI/CD Pipeline Server" + }, + "multiTenant": { + "type": [ + "object", "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." + ], + "properties": { + "centralArgocdNamespace": { + "type": [ + "string", + "null" + ], + "description": "Namespace for the centralized Argocd" + }, + "gitlab": { + "type": [ + "object", + "null" + ], + "properties": { + "credentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "URL for external Gitlab" + }, + "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": { + "credentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "CENTRAL SCMM username" + }, + "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 SCM-Manager" + }, + "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" + } }, - "skipRestart": { - "type": [ - "boolean", + "additionalProperties": false, + "description": "Multi Tenant Configs" + }, + "registry": { + "type": [ + "object", "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.'" + ], + "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." + }, + "credentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "Config parameters for Registry" + }, + "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" + }, + "namespace": { + "type": [ + "string", + "null" + ], + "description": "Optional defines the kubernetes namespace for registry." + }, + "password": { + "type": [ + "string", + "null" + ], + "description": "Optional when registry-url is set" + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "Optional when registry-url is set" + }, + "proxyCredentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "Config parameters for Registry" + }, + "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" + } }, - "url": { - "type": [ - "string", + "additionalProperties": false, + "description": "Config parameters for Registry" + }, + "scm": { + "type": [ + "object", "null" - ], - "description": "The host of your external scm-manager" + ], + "properties": { + "gitlab": { + "type": [ + "object", + "null" + ], + "properties": { + "credentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "Base URL for the Gitlab instance" + }, + "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": { + "credentials": { + "$ref": "#/$defs/Credentials-nullable", + "description": "Mandatory when scmm-url is set" + }, + "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" + }, + "scmmImage": { + "type": [ + "string", + "null" + ], + "description": "Sets image for SCM-Manager" + }, + "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 SCM-Manager" + }, + "scmProviderType": { + "$ref": "#/$defs/ScmProviderType-nullable", + "description": "The SCM provider type. Possible values: SCM_MANAGER, GITLAB" + } }, - "username": { - "type": [ - "string", - "null" - ], - "description": "Mandatory when scmm-url is set" - } - }, - "additionalProperties": false, - "description": "Config for SCM-Manager" - }, - "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/scripts/dev/gop-secrets-values.yaml b/scripts/dev/gop-secrets-values.yaml new file mode 100644 index 000000000..2dae5adea --- /dev/null +++ b/scripts/dev/gop-secrets-values.yaml @@ -0,0 +1,79 @@ +# $schema: https://raw.githubusercontent.com/cloudogu/gitops-playground/main/docs/configuration.schema.json +application: + "yes": true + baseUrl: http://localhost + credentials: + secretName: argocd-credentials + secretNamespace: gop-job +scm: + scmManager: + credentials: + secretName: scm-tenant-credentials + secretNamespace: gop-job +features: + certManager: + active: true + argocd: + active: true + operator: false + ingress: + active: true + monitoring: + active: true + secrets: + vault: + mode: "dev" +jenkins: + active: true + credentials: + secretName: jenkins-credentials + secretNamespace: gop-job +registry: + active: true + credentials: + secretName: registry-credentials + secretNamespace: gop-job +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-examples + path: example-apps-via-content-loader/ + ref: main + templating: true + type: FOLDER_BASED + overwriteMode: UPGRADE + + namespaces: + - ${config.application.namePrefix}example-apps-production + - ${config.application.namePrefix}example-apps-staging + variables: + petclinic: + baseDomain: "petclinic" + images: + kubectl: "alpine/kubectl:latest" + helm: "ghcr.io/cloudogu/helm:latest" + kubeval: "ghcr.io/cloudogu/helm:latest" + helmKubeval: "ghcr.io/cloudogu/helm:latest" + yamllint: "cytopia/yamllint:1.25-0.7" + petclinic: "eclipse-temurin:17-jre" + maven: "" diff --git a/scripts/dev/gop-secrets.yaml b/scripts/dev/gop-secrets.yaml new file mode 100644 index 000000000..384b2664c --- /dev/null +++ b/scripts/dev/gop-secrets.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: Secret +metadata: + name: jenkins-credentials + namespace: gop-job +type: Opaque +stringData: + username: admin + password: this_is_for_your_ads +--- +apiVersion: v1 +kind: Secret +metadata: + name: argocd-credentials + namespace: gop-job +type: Opaque +stringData: + username: admin + password: who_can_read_this +--- +apiVersion: v1 +kind: Secret +metadata: + name: registry-credentials + namespace: gop-job +type: Opaque +stringData: + username: myregistry + password: mypassword +--- +apiVersion: v1 +kind: Secret +metadata: + name: scm-tenant-credentials + namespace: gop-job +type: Opaque +stringData: + username: miniadmin + password: this_is_my_password \ No newline at end of file diff --git a/src/main/java/com/cloudogu/gitops/application/Application.java b/src/main/java/com/cloudogu/gitops/application/Application.java index 7aa14621b..251539be5 100644 --- a/src/main/java/com/cloudogu/gitops/application/Application.java +++ b/src/main/java/com/cloudogu/gitops/application/Application.java @@ -59,7 +59,6 @@ public void start() { gitHandler.validate(); DeploymentContext context = contextBuilder.build(); - setNamespaceListToConfig(context); storeGopInformationInSecret(); gitHandler.prepareProviders(context); @@ -104,7 +103,7 @@ public void setNamespaceListToConfig(DeploymentContext context) { config, "statics", new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build() - .getStaticModels() + .getStaticModels() ) )); } catch (Exception e) { diff --git a/src/main/java/com/cloudogu/gitops/cli/GitopsPlaygroundCli.java b/src/main/java/com/cloudogu/gitops/cli/GitopsPlaygroundCli.java index 640d6570f..2910174cf 100644 --- a/src/main/java/com/cloudogu/gitops/cli/GitopsPlaygroundCli.java +++ b/src/main/java/com/cloudogu/gitops/cli/GitopsPlaygroundCli.java @@ -88,7 +88,7 @@ public ReturnCode run(String[] args) { Application app = context.getBean(Application.class); Config config = readConfigs(args); - runHook(app, "preConfigInit", ConfigLifecycleHook::preConfigInit, config); + runHook(app, "preConfigInit", ConfigLifecycleHook::preConfigInit, config, k8sClient); if (config.getApplication().getOutputConfigFile()) { log.info(config.toYaml(false)); @@ -97,7 +97,7 @@ public ReturnCode run(String[] args) { config = applicationConfigurator.initConfig(config); log.debug("Actual config: {}", config.toYaml(true)); - runHook(app, "postConfigInit", ConfigLifecycleHook::postConfigInit, config); + runHook(app, "postConfigInit", ConfigLifecycleHook::postConfigInit, config, k8sClient); context.close(); context = createApplicationContext(); @@ -149,9 +149,9 @@ private static boolean confirm(String message, Config config) { "Calling confirm for message: {} | yes = {} | System.in class: {}", message, config.getApplication() - .getYes(), + .getYes(), System.in.getClass() - .getName() + .getName() ); if (config.getApplication().getYes()) { return true; @@ -207,7 +207,7 @@ public void setSimpleLogPattern() { rootLogger(loggerContext).detachAppender(STDOUT_APPENDER_NAME); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); encoder.setPattern(LOGGER_PATTERN_TOKEN.matcher(THREAD_PATTERN_TOKEN.matcher(defaultPattern).replaceAll(" ")) - .replaceAll(" ")); + .replaceAll(" ")); encoder.setContext(loggerContext); encoder.start(); ConsoleAppender appender = new ConsoleAppender<>(); @@ -270,13 +270,13 @@ private Config readConfigs(String[] args) { log.debug( "mergedConfig yes before parseArgs: {}", mergedConfig.getApplication() != null ? mergedConfig.getApplication() - .getYes() : "null" + .getYes() : "null" ); new CommandLine(mergedConfig).parseArgs(args); log.debug( "mergedConfig yes after parseArgs: {}", mergedConfig.getApplication() != null ? mergedConfig.getApplication() - .getYes() : "null" + .getYes() : "null" ); return mergedConfig; @@ -315,9 +315,10 @@ public static void runHook( Application app, String hookName, BiConsumer hook, - Config config) { + Config config, + K8sClient k8sClient) { List configLifecycleHooks = new ArrayList<>(); - configLifecycleHooks.add(new CommonToolConfig()); + configLifecycleHooks.add(new CommonToolConfig(k8sClient)); for (AbstractTool tool : app.getTools()) { if (tool instanceof ConfigLifecycleHook configLifecycleHook) { configLifecycleHooks.add(configLifecycleHook); @@ -331,7 +332,7 @@ public static void runHook( } catch (Exception e) { throw new RuntimeException( "Failed to execute hook " + hookName + " on " + configLifecycleHook.getClass() - .getName(), e + .getName(), e ); } } diff --git a/src/main/java/com/cloudogu/gitops/config/Config.java b/src/main/java/com/cloudogu/gitops/config/Config.java index 21a64aa5b..7afd31c97 100644 --- a/src/main/java/com/cloudogu/gitops/config/Config.java +++ b/src/main/java/com/cloudogu/gitops/config/Config.java @@ -382,6 +382,9 @@ public static class RegistrySchema { @JsonPropertyDescription(REGISTRY_PASSWORD_DESCRIPTION) private String password = ""; + @JsonPropertyDescription(REGISTRY_DESCRIPTION) + private Credentials credentials; + @Option(names = {"--registry-proxy-url"}, description = REGISTRY_PROXY_URL_DESCRIPTION) @JsonPropertyDescription(REGISTRY_PROXY_URL_DESCRIPTION) private String proxyUrl = ""; @@ -398,6 +401,9 @@ public static class RegistrySchema { @JsonPropertyDescription(REGISTRY_PROXY_PASSWORD_DESCRIPTION) private String proxyPassword = ""; + @JsonPropertyDescription(REGISTRY_DESCRIPTION) + private Credentials proxyCredentials; + @Option(names = {"--registry-username-read-only"}, description = REGISTRY_USERNAME_RO_DESCRIPTION) @JsonPropertyDescription(REGISTRY_USERNAME_RO_DESCRIPTION) private String readOnlyUsername = ""; @@ -459,6 +465,9 @@ public static class JenkinsSchema { @JsonPropertyDescription(JENKINS_PASSWORD_DESCRIPTION) private String password = DEFAULT_ADMIN_PW; + @JsonPropertyDescription(JENKINS_DESCRIPTION) + private Credentials credentials; + @Option(names = {"--jenkins-metrics-username"}, description = JENKINS_METRICS_USERNAME_DESCRIPTION) @JsonPropertyDescription(JENKINS_METRICS_USERNAME_DESCRIPTION) private String metricsUsername = "metrics"; @@ -467,6 +476,9 @@ public static class JenkinsSchema { @JsonPropertyDescription(JENKINS_METRICS_PASSWORD_DESCRIPTION) private String metricsPassword = "metrics"; + @JsonPropertyDescription(JENKINS_METRICS_PASSWORD_DESCRIPTION) + private Credentials metricsCredentials; + @Option(names = {"--jenkins-image"}, description = JENKINS_IMAGE_DESCRIPTION) @JsonPropertyDescription(JENKINS_IMAGE_DESCRIPTION) private String jenkinsImage = ""; @@ -548,6 +560,9 @@ public static class ApplicationSchema { @JsonPropertyDescription(PASSWORD_DESCRIPTION) private String password = DEFAULT_ADMIN_PW; + @JsonPropertyDescription(APPLICATION_DESCRIPTION) + private Credentials credentials; + @Option(names = {"-y", "--yes"}, description = PIPE_YES_DESCRIPTION) @JsonPropertyDescription(PIPE_YES_DESCRIPTION) private Boolean yes = false; @@ -1021,9 +1036,9 @@ public enum VaultMode { @JsonCreator public static VaultMode fromExternalValue(String value) { return Arrays.stream(values()) - .filter(mode -> mode.externalValue.equalsIgnoreCase(value)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Unknown Vault mode: " + value)); + .filter(mode -> mode.externalValue.equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown Vault mode: " + value)); } @JsonValue @@ -1074,8 +1089,8 @@ public List changeProperties( BeanDescription beanDesc, List beanProperties) { return beanProperties.stream() - .filter(writer -> writer.getAnnotation(JsonPropertyDescription.class) != null) - .toList(); + .filter(writer -> writer.getAnnotation(JsonPropertyDescription.class) != null) + .toList(); } })); return mapper; diff --git a/src/main/java/com/cloudogu/gitops/config/Credentials.java b/src/main/java/com/cloudogu/gitops/config/Credentials.java index b58ca60c7..ba5a7d057 100644 --- a/src/main/java/com/cloudogu/gitops/config/Credentials.java +++ b/src/main/java/com/cloudogu/gitops/config/Credentials.java @@ -75,10 +75,22 @@ public Credentials( public Credentials(Credentials unsafeCredentials) { if (unsafeCredentials != null) { + this.username = unsafeCredentials.username; + this.password = unsafeCredentials.password; this.secretNamespace = unsafeCredentials.secretNamespace; this.secretName = unsafeCredentials.secretName; this.usernameKey = unsafeCredentials.usernameKey; this.passwordKey = unsafeCredentials.passwordKey; } } + + /** + * ensures that secretName and secretNamespace are not null + * + * @return true, if User sets credentials with secretName and secretNamespace otherwise false. + */ + @JsonIgnore + public boolean isUsed() { + return secretName != null && secretNamespace != null; + } } diff --git a/src/main/java/com/cloudogu/gitops/config/scm/ScmCentralSchema.java b/src/main/java/com/cloudogu/gitops/config/scm/ScmCentralSchema.java index a2ea99af7..d158c55ee 100644 --- a/src/main/java/com/cloudogu/gitops/config/scm/ScmCentralSchema.java +++ b/src/main/java/com/cloudogu/gitops/config/scm/ScmCentralSchema.java @@ -35,6 +35,10 @@ public static class GitlabCentralConfig implements GitlabConfig { @JsonPropertyDescription(CENTRAL_GITLAB_PASSWORD_DESCRIPTION) private String password = ""; + @JsonPropertyDescription(CENTRAL_GITLAB_URL_DESCRIPTION) + private Credentials credentials; + + @Option(names = {"--central-gitlab-group-id"}, description = CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION) @JsonPropertyDescription(CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION) private String parentGroupId = ""; @@ -44,7 +48,7 @@ public static class GitlabCentralConfig implements GitlabConfig { @Override public Credentials getCredentials() { - return new Credentials(username, password); + return credentials != null ? credentials : new Credentials(username, password); } } @@ -74,6 +78,9 @@ public static class ScmManagerCentralConfig implements ScmManagerConfig { @JsonPropertyDescription(CENTRAL_SCMM_PASSWORD_DESCRIPTION) private String password = ""; + @JsonPropertyDescription(CENTRAL_SCMM_USERNAME_DESCRIPTION) + private Credentials credentials; + @Option(names = {"--central-scmm-namespace"}, description = CENTRAL_SCMM_NAMESPACE_DESCRIPTION) @JsonPropertyDescription(CENTRAL_SCMM_NAMESPACE_DESCRIPTION) private String namespace = "scm-manager"; @@ -92,7 +99,7 @@ public Config.HelmConfigWithValues getHelm() { @Override public Credentials getCredentials() { - return new Credentials(username, password); + return credentials != null ? credentials : new Credentials(username, password); } } } diff --git a/src/main/java/com/cloudogu/gitops/config/scm/ScmTenantSchema.java b/src/main/java/com/cloudogu/gitops/config/scm/ScmTenantSchema.java index 23545d387..5f80bf4df 100644 --- a/src/main/java/com/cloudogu/gitops/config/scm/ScmTenantSchema.java +++ b/src/main/java/com/cloudogu/gitops/config/scm/ScmTenantSchema.java @@ -71,6 +71,9 @@ public static class GitlabTenantConfig implements GitlabConfig { @JsonPropertyDescription(GITLAB_TOKEN_DESCRIPTION) private String password; + @JsonPropertyDescription(GITLAB_URL_DESCRIPTION) + private Credentials credentials; + @Option(names = {"--gitlab-group-id"}, description = GITLAB_PARENT_GROUP_ID) @JsonPropertyDescription(GITLAB_PARENT_GROUP_ID) private String parentGroupId = ""; @@ -81,10 +84,10 @@ public static class GitlabTenantConfig implements GitlabConfig { private String defaultVisibility = ""; @Override - @JsonIgnore public Credentials getCredentials() { - return new Credentials(username, password); + return credentials != null ? credentials : new Credentials(username, password); } + } @Getter @@ -117,6 +120,9 @@ public static class ScmManagerTenantConfig implements ScmManagerConfig { @JsonPropertyDescription(SCMM_PASSWORD_DESCRIPTION) private String password = Config.DEFAULT_ADMIN_PW; + @JsonPropertyDescription(SCMM_USERNAME_DESCRIPTION) + private Credentials credentials; + @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION) @JsonMerge private Config.HelmConfigWithValues helm; @@ -149,9 +155,8 @@ public ScmManagerTenantConfig() { } @Override - @JsonIgnore public Credentials getCredentials() { - return new Credentials(username, password); + return credentials != null ? credentials : new Credentials(username, password); } } } diff --git a/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java b/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java index ac32a4b1e..c9630299a 100644 --- a/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java +++ b/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java @@ -69,1365 +69,1327 @@ @Slf4j public class K8sClient { - private static final TypeReference> MAP_TYPE = new TypeReference<>() { - }; - - private static final String DEFAULT_NAMESPACE = "default"; - private static final String INTERNAL_IP_TYPE = "InternalIP"; - private static final String DOCKER_CONFIG_JSON_TYPE = "kubernetes.io/dockerconfigjson"; - private static final String DOCKER_CONFIG_JSON_KEY = ".dockerconfigjson"; - private static final String NOT_FOUND_IN_NAMESPACE = " not found in namespace "; - private static final String APPLIED_PREFIX = "Applied "; - - private static final int DEFAULT_TIMEOUT_SECONDS = 60; - private static final int DEFAULT_CHECK_INTERVAL_SECONDS = 1; - private static final int FABRIC8_REQUEST_TIMEOUT_MILLIS = 60_000; - private static final int FABRIC8_CONNECTION_TIMEOUT_MILLIS = 10_000; - private static final int MILLIS_PER_SECOND = 1000; - private static final int DEFAULT_SLEEP_TIME_MILLIS = MILLIS_PER_SECOND; - private static final int DEFAULT_RETRIES = 120; - - protected int sleepTimeMillis = DEFAULT_SLEEP_TIME_MILLIS; - protected int defaultRetries = DEFAULT_RETRIES; - - /** - * -- GETTER -- - * Returns the underlying fabric8 client. - *

- *

- * -- SETTER -- - * Replaces the underlying fabric8 client, mainly for tests. - * - * @return the fabric8 client - * @param client the fabric8 client to use - */ - @Setter - @Getter - private KubernetesClient client; - /** - * -- SETTER -- - * Sets the GitOps Playground config after construction. - * - * @param gopConfig the GitOps Playground config; may be null - */ - @Setter - private com.cloudogu.gitops.config.Config gopConfig; - - /** - * Creates a client with default fabric8 configuration and no playground config. - */ - public K8sClient() { - this(null); - } - - /** - * Creates a client with default fabric8 configuration. - * - * @param gopConfig the GitOps Playground config, used e.g. to detect OpenShift mode; may be null - */ - public K8sClient(com.cloudogu.gitops.config.Config gopConfig) { - io.fabric8.kubernetes.client.Config config = new ConfigBuilder().withRequestTimeout( - FABRIC8_REQUEST_TIMEOUT_MILLIS) - .withConnectionTimeout( - FABRIC8_CONNECTION_TIMEOUT_MILLIS) - .build(); - - this.client = new KubernetesClientBuilder().withConfig(config).build(); - this.gopConfig = gopConfig; - } - - /** - * Waits for the first node in the cluster to become available. - * - * @return The name of the first available node - */ - public String waitForNode() { - log.debug("Waiting for first node of the cluster to become ready"); - - String nodeName = waitForResourceWithRetry( - "node", () -> { - NodeList nodes = client.nodes().list(); - if (nodes != null && nodes.getItems() != null && !nodes.getItems().isEmpty()) { - return nodes.getItems().get(0).getMetadata().getName(); - } - return null; - } - ); - - log.debug("First node of the cluster is ready: {}", nodeName); - return nodeName; - } - - /** - * Waits for and retrieves the internal IP address of the first node. - * - * @return the internal IP address of the first node - */ - public String waitForInternalNodeIp() { - String nodeName = waitForNode(); - log.debug("Waiting for internal IP of node {}", nodeName); - - String internalIp = waitForResourceWithRetry( - "internal IP of node " + nodeName, - () -> findInternalNodeIp(nodeName) - ); - - log.debug("Internal IP of node {}: {}", nodeName, internalIp); - return internalIp; - } - - private String findInternalNodeIp(String nodeName) { - Node node = client.nodes().withName(nodeName).get(); - if (node != null && node.getStatus() != null && node.getStatus().getAddresses() != null) { - for (NodeAddress address : node.getStatus().getAddresses()) { - if (INTERNAL_IP_TYPE.equals(address.getType())) { - return address.getAddress(); - } - } - } - return null; - } - - /** - * Waits for a service's NodePort to become available. - * - * @param serviceName name of the service to inspect - * @param namespace namespace of the service; empty means the default namespace - * @return the NodePort of the service's first port - */ - public String waitForNodePort(String serviceName, String namespace) { - log.debug("Getting node port for service {}, ns={}", serviceName, namespace); - - String nodePort = waitForResourceWithRetry( - "node port for service " + serviceName, - () -> findServiceNodePort(serviceName, namespace) - ); - - log.debug("Node port for service {}, ns={}: {}", serviceName, namespace, nodePort); - return nodePort; - } - - private String findServiceNodePort(String serviceName, String namespace) { - Service service = client.services().inNamespace(namespace).withName(serviceName).get(); - if (service != null && service.getSpec() != null && service.getSpec().getPorts() != null && !service.getSpec() - .getPorts() - .isEmpty()) { - Integer port = service.getSpec().getPorts().get(0).getNodePort(); - return port != null ? port.toString() : null; - } - return null; - } - - /** - * Creates a NodePort service (idempotent). - * - * @param name name of the service to create - * @param tcp port mapping in the form {@code port[:targetPort]} - * @param nodePort fixed node port to expose; empty for auto-assignment - * @param namespace target namespace; empty means the default namespace - */ - public void createServiceNodePort(String name, String tcp, String nodePort, String namespace) { - log.debug("Creating NodePort service {} in namespace {}", name, namespace); - - String[] ports = tcp.split(":"); - int port = Integer.parseInt(ports[0]); - int targetPort = ports.length > 1 ? Integer.parseInt(ports[1]) : port; - - ServicePort servicePort = new ServicePortBuilder().withPort(port) - .withTargetPort(new IntOrString(targetPort)) - .build(); - if (nodePort != null && !nodePort.isEmpty()) { - servicePort.setNodePort(Integer.parseInt(nodePort)); - } - - Service service = new ServiceBuilder().withNewMetadata() - .withName(name) - .withNamespace(resolveNamespace(namespace)) - .endMetadata() - .withNewSpec() - .withType("NodePort") - .withPorts(servicePort) - .endSpec() - .build(); - - executeWithErrorHandling( - "create NodePort service " + name, () -> { - client.services() - .inNamespace(resolveNamespace(namespace)) - .resource(service) - .createOr(NonDeletingOperation::update); - return null; - } - ); - - log.debug("NodePort service {} created/updated successfully", name); - } - - /** - * Patches the nodePort of a specific port in a service. - * - * @param serviceName name of the service to patch - * @param namespace namespace of the service - * @param portName name of the port entry whose nodePort is replaced - * @param newNodePort new node port value - */ - public void patchServiceNodePort(String serviceName, String namespace, String portName, int newNodePort) { - K8sClientHelper.validateServiceNodePortPatch(serviceName, namespace, portName, newNodePort); - - log.debug("Patching service {} port {} with nodePort {}", serviceName, portName, newNodePort); - - Service service = client.services().inNamespace(namespace).withName(serviceName).get(); - - if (service == null) { - throw new IllegalStateException("Service " + serviceName + NOT_FOUND_IN_NAMESPACE + namespace); - } - - List ports = service.getSpec().getPorts(); - int portIndex = -1; - for (int i = 0; i < ports.size(); i++) { - if (portName.equals(ports.get(i).getName())) { - portIndex = i; - break; - } - } - - if (portIndex == -1) { - throw new IllegalStateException("Port with name " + portName + " not found in service " + serviceName + "."); - } - - // Create JSON patch - List> patch = List.of(Map.of( - "op", - "replace", - "path", - "/spec/ports/" + portIndex + "/nodePort", - "value", - newNodePort - )); - - String patchJson = Serialization.asJson(patch); - PatchContext patchContext = new PatchContext.Builder().withPatchType(io.fabric8.kubernetes.client.dsl.base.PatchType.JSON) - .build(); - - executeWithErrorHandling( - "patch service " + serviceName, () -> { - client.services().inNamespace(namespace).withName(serviceName).patch(patchContext, patchJson); - return null; - } - ); - - log.debug( - "Service {} in namespace {} successfully patched with nodePort {} for port {}.", - serviceName, - namespace, - newNodePort, - portName - ); - } - - /** - * Creates a namespace (or an OpenShift project) if it does not already exist (idempotent). - * - * @param name name of the namespace to create - */ - public void createNamespace(String name) { - K8sClientHelper.validateNamespaceName(name); - - if (!namespaceExists(name)) { - log.debug("Namespace {} does not exist, proceeding to create.", name); - - if (runInOpenshift()) { - OpenShiftClient osClient = client.adapt(OpenShiftClient.class); - - Project project = new ProjectBuilder().withNewMetadata().withName(name).endMetadata().build(); - executeWithErrorHandling( - "create project " + name, () -> { - osClient.projects().resource(project).create(); - return null; - } - ); - log.debug("Project {} created successfully.", name); - } else { - Namespace namespace = new NamespaceBuilder().withNewMetadata().withName(name).endMetadata().build(); - - executeWithErrorHandling( - "create namespace " + name, () -> { - client.namespaces().resource(namespace).create(); - return null; - } - ); - - log.debug("Namespace {} created successfully.", name); - } - } - } - - /** - * Creates multiple namespaces. - * - * @param names names of the namespaces to create - */ - public void createNamespaces(List names) { - if (names == null) { - throw new IllegalArgumentException("Namespaces must be provided and cannot be null."); - } - for (String name : names) { - createNamespace(name); - } - } - - /** - * Checks if a namespace exists. - * - * @param namespace name of the namespace to check - * @return true if the namespace exists - */ - public boolean namespaceExists(String namespace) { - try { - Namespace ns = client.namespaces().withName(namespace).get(); - if (ns != null) { - log.debug("Namespace {} already exists.", namespace); - return true; - } - } catch (Exception e) { - log.trace("Namespace {} does not exist: {}", namespace, e.getMessage()); - } - return false; - } - - /** - * Creates or updates an empty secret in the default namespace (idempotent). - * - * @param type secret type, e.g. {@code generic} - * @param name name of the secret - */ - public void createSecret(String type, String name) { - createSecret(type, name, "", new Tuple[0]); - } - - /** - * Creates or updates an empty secret (idempotent). - * - * @param type secret type, e.g. {@code generic} - * @param name name of the secret - * @param namespace target namespace; empty means the default namespace - */ - public void createSecret(String type, String name, String namespace) { - createSecret(type, name, namespace, new Tuple[0]); - } - - /** - * Creates or updates a generic secret (idempotent). - * - * @param type secret type; {@code generic} is mapped to {@code Opaque} - * @param name name of the secret - * @param namespace target namespace; empty means the default namespace - * @param literals key-value pairs stored as string data - */ - public void createSecret(String type, String name, String namespace, Tuple... literals) { - log.debug("Creating secret {} of type {} in namespace {}", name, type, namespace); - - Map data = new HashMap<>(); - if (literals != null) { - for (Tuple tuple : literals) { - data.put(String.valueOf(tuple.getFirst()), String.valueOf(tuple.getSecond())); - } - } - - String resolvedType = "generic".equals(type) ? "Opaque" : type; - Secret secret = new SecretBuilder().withNewMetadata() - .withName(name) - .withNamespace(resolveNamespace(namespace)) - .endMetadata() - .withType(resolvedType) - .withStringData(data) - .build(); - - executeWithErrorHandling( - "create secret " + name, () -> { - client.secrets() - .inNamespace(resolveNamespace(namespace)) - .resource(secret) - .createOr(NonDeletingOperation::update); - return null; - } - ); - - log.debug("Secret {} created/updated successfully", name); - } - - /** - * Creates or updates an image pull secret in the default namespace (idempotent). - * - * @param name name of the secret - * @param host registry host the credentials belong to - * @param user registry username - * @param password registry password - */ - public void createImagePullSecret(String name, String host, String user, String password) { - createImagePullSecret(name, "", host, user, password); - } - - /** - * Creates or updates an image pull secret (idempotent). - * - * @param name name of the secret - * @param namespace target namespace; empty means the default namespace - * @param host registry host the credentials belong to - * @param user registry username - * @param password registry password - */ - public void createImagePullSecret(String name, String namespace, String host, String user, String password) { - log.debug("Creating image pull secret {} in namespace {}", name, namespace); - - String auth = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8)); - String dockerConfig = Serialization.asJson( - Map.of("auths", Map.of(host, Map.of("username", user, "password", password, "auth", auth))) - ); - - Secret secret = new SecretBuilder().withNewMetadata() - .withName(name) - .withNamespace(resolveNamespace(namespace)) - .endMetadata() - .withType(DOCKER_CONFIG_JSON_TYPE) - .addToStringData(DOCKER_CONFIG_JSON_KEY, dockerConfig) - .build(); - - executeWithErrorHandling( - "create image pull secret " + name, () -> { - client.secrets() - .inNamespace(resolveNamespace(namespace)) - .resource(secret) - .createOr(NonDeletingOperation::update); - return null; - } - ); - - log.debug("Image pull secret {} created/updated successfully", name); - } - - /** - * Retrieves the {@code namespaces} data from an ArgoCD secret, waiting for the secret to appear. - * - * @param name name of the secret - * @param namespace namespace of the secret; empty means the default namespace - * @return the base64-encoded {@code namespaces} value of the secret - */ - public String getArgoCDNamespacesSecret(String name, String namespace) { - log.debug("Getting Secret {} from namespace {}", name, namespace); - - return waitForResourceWithRetry( - "secret " + name, () -> { - Secret secret = client.secrets().inNamespace(resolveNamespace(namespace)).withName(name).get(); - - return (secret != null && secret.getData() != null && secret.getData() - .containsKey("namespaces")) ? secret.getData() - .get( - "namespaces") : null; - } - ); - } - - /** - * Extracts credentials from a secret using the default keys {@code username} and {@code - * password}. - * - * @param secretname name of the secret - * @param namespace namespace of the secret - * @return the decoded credentials - */ - public Credentials getCredentialsFromSecret(String secretname, String namespace) { - return getCredentialsFromSecret(secretname, namespace, "username", "password"); - } - - /** - * Extracts credentials from a Kubernetes secret. - * - * @param secretname name of the secret - * @param namespace namespace of the secret - * @param usernameKey data key holding the username - * @param passwordKey data key holding the password - * @return the decoded credentials - */ - public Credentials getCredentialsFromSecret( - String secretname, - String namespace, - String usernameKey, - String passwordKey) { - return executeWithErrorHandling( - "get credentials from secret " + secretname, - () -> resolveCredentialsFromSecret(secretname, namespace, usernameKey, passwordKey) - ); - } - - private Credentials resolveCredentialsFromSecret( - String secretname, - String namespace, - String usernameKey, - String passwordKey) { - Secret secret = client.secrets().inNamespace(namespace).withName(secretname).get(); - if (secret == null || secret.getData() == null) { - throw new IllegalStateException("Secret " + secretname + NOT_FOUND_IN_NAMESPACE + namespace); - } - - Map secretData = secret.getData(); - String username = new String(Base64.getDecoder().decode(secretData.get(usernameKey)), StandardCharsets.UTF_8); - String password = new String(Base64.getDecoder().decode(secretData.get(passwordKey)), StandardCharsets.UTF_8); - return new Credentials(username, password); - } - - /** - * Extracts credentials from a Kubernetes secret using a Credentials object as input. - * - * @param credentials reference describing secret name, namespace and data keys - * @return a copy of the input with username and password resolved from the secret - */ - public Credentials getCredentialsFromSecret(Credentials credentials) { - return executeWithErrorHandling( - "get credentials from secret " + credentials.getSecretName(), - () -> resolveCredentialsFromSecret(credentials) - ); - } - - private Credentials resolveCredentialsFromSecret(Credentials credentials) { - Secret secret = client.secrets() - .inNamespace(credentials.getSecretNamespace()) - .withName(credentials.getSecretName()) - .get(); - if (secret == null || secret.getData() == null) { - throw new IllegalStateException("Secret " + credentials.getSecretName() + NOT_FOUND_IN_NAMESPACE + credentials.getSecretNamespace()); - } - - Map secretData = secret.getData(); - String usernameEncoded = secretData.get(credentials.getUsernameKey()); - String username = usernameEncoded != null ? new String( - Base64.getDecoder() - .decode(usernameEncoded), StandardCharsets.UTF_8 - ) : credentials.getUsername(); - String password = new String( - Base64.getDecoder() - .decode(secretData.get(credentials.getPasswordKey())), StandardCharsets.UTF_8 - ); - - Credentials credentialsNew = new Credentials(credentials); - credentialsNew.setUsername(username); - credentialsNew.setPassword(password); - - return credentialsNew; - } - - /** - * Creates or updates a ConfigMap from a file (idempotent). - * - * @param name name of the ConfigMap - * @param namespace target namespace; empty means the default namespace - * @param filePath path of the file whose content becomes the ConfigMap data - */ - public void createConfigMapFromFile(String name, String namespace, String filePath) { - log.debug("Creating ConfigMap {} from file {} in namespace {}", name, filePath, namespace); - - File file = new File(filePath); - if (!file.exists()) { - throw new IllegalStateException("File not found: " + filePath); - } - - String fileContent; - try { - fileContent = Files.readString(file.toPath()); - } catch (IOException e) { - throw new UncheckedIOException("Failed to read file: " + filePath, e); - } - - Map data = Map.of(file.getName(), fileContent); - - ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() - .withName(name) - .withNamespace(resolveNamespace(namespace)) - .endMetadata() - .withData(data) - .build(); - - executeWithErrorHandling( - "create ConfigMap " + name + " from file", () -> { - client.configMaps() - .inNamespace(resolveNamespace(namespace)) - .resource(configMap) - .createOr(NonDeletingOperation::update); - return null; - } - ); - - log.debug("ConfigMap {} created/updated successfully", name); - } - - /** - * Retrieves a value from a ConfigMap in the current namespace. - * - * @param mapName name of the ConfigMap - * @param key data key to read - * @return the value stored under the given key - */ - public String getConfigMap(String mapName, String key) { - String namespace = getCurrentNamespace(); - - log.debug("Getting ConfigMap {}/{}, key: {}", namespace, mapName, key); - - ConfigMap configMap = client.configMaps().inNamespace(namespace).withName(mapName).get(); - - if (configMap == null) { - throw new IllegalStateException("Could not fetch configmap " + mapName + " from namespace " + namespace); - } - - if (configMap.getData() == null || !configMap.getData().containsKey(key)) { - throw new IllegalStateException("Could not fetch " + key + " within config-map " + mapName + " from namespace " + namespace); - } - - return configMap.getData().get(key); - } - - /** - * Applies YAML resources from a URL, file or directory (recursively). - * - * @param yamlLocation http(s) URL, file path or directory path containing YAML resources - * @return a summary of how many resources were applied - */ - public String applyYaml(String yamlLocation) { - log.debug("Applying YAML from {}", yamlLocation); - - if (yamlLocation.startsWith("http://") || yamlLocation.startsWith("https://")) { - try { - int appliedResources = applyYamlStream(URI.create(yamlLocation).toURL().openStream(), yamlLocation); - return APPLIED_PREFIX + appliedResources + " resource(s) from " + yamlLocation; - } catch (IOException | IllegalArgumentException e) { - throw new UncheckedIOException("Failed to apply YAML from URL: " + yamlLocation, new IOException(e)); - } - } - - File location = new File(yamlLocation); - - if (!location.exists()) { - throw new IllegalStateException("File or directory not found: " + yamlLocation); - } - - if (location.isDirectory()) { - List yamlFiles; - try (Stream stream = Files.walk(location.toPath())) { - yamlFiles = stream.filter(Files::isRegularFile) - .map(Path::toFile) - .filter(file -> file.getName().endsWith(".yaml") || file.getName().endsWith(".yml")) - .collect(Collectors.toCollection(ArrayList::new)); - } catch (IOException e) { - throw new UncheckedIOException("Failed to list YAML files in directory: " + yamlLocation, e); - } - - yamlFiles.sort(Comparator.comparing(File::getAbsolutePath)); - - int appliedResources = 0; - for (File file : yamlFiles) { - try { - appliedResources += applyYamlStream(Files.newInputStream(file.toPath()), file.getAbsolutePath()); - } catch (IOException e) { - throw new UncheckedIOException("Failed to apply YAML file: " + file.getAbsolutePath(), e); - } - } - - return APPLIED_PREFIX + appliedResources + " resource(s) from directory " + yamlLocation; - } - - try { - int appliedResources = applyYamlStream(Files.newInputStream(location.toPath()), yamlLocation); - return APPLIED_PREFIX + appliedResources + " resource(s) from " + yamlLocation; - } catch (IOException e) { - throw new UncheckedIOException("Failed to apply YAML file: " + yamlLocation, e); - } - } - - private int applyYamlStream(InputStream stream, String sourceDescription) { - List resources = executeWithErrorHandling( - "load YAML from " + sourceDescription, - () -> loadYamlItems(stream, sourceDescription) - ); - - for (HasMetadata resource : resources) { - executeWithErrorHandling( - "apply resource from " + sourceDescription, () -> { - client.resource(resource).createOr(NonDeletingOperation::update); - return null; - } - ); - } - - return resources.size(); - } - - private List loadYamlItems(InputStream stream, String sourceDescription) { - try (stream) { - return client.load(stream).items(); - } catch (IOException e) { - throw new UncheckedIOException("Failed to close YAML input stream for " + sourceDescription, e); - } - } - - /** - * Adds or removes labels on a resource in the default namespace. - * - * @param resource resource type, e.g. {@code node} - * @param name resource name; {@code --all} applies to all nodes - * @param keyValues labels to set; a key ending in {@code -} removes that label - */ - public void label(String resource, String name, Tuple... keyValues) { - label(resource, name, "", keyValues); - } - - /** - * Adds or removes labels on a resource. - * - * @param resource resource type, e.g. {@code node} - * @param name resource name; {@code --all} applies to all nodes - * @param namespace namespace of the resource; empty means the default namespace - * @param keyValues labels to set; a key ending in {@code -} removes that label - */ - public void label(String resource, String name, String namespace, Tuple... keyValues) { - if (keyValues == null || keyValues.length == 0) { - throw new IllegalArgumentException("Missing key-value-pairs"); - } - - if ("--all".equals(name)) { - NodeList nodes = client.nodes().list(); - if (nodes != null && nodes.getItems() != null) { - for (Node node : nodes.getItems()) { - label(resource, node.getMetadata().getName(), namespace, keyValues); - } - } - return; - } - - log.debug("Labeling {}/{} in namespace {}", resource, name, namespace); - - Map labelsToAdd = new HashMap<>(); - List labelsToRemove = new ArrayList<>(); - - for (Tuple tuple : keyValues) { - String key = String.valueOf(tuple.getFirst()); - String value = String.valueOf(tuple.getSecond()); - - if (key.endsWith("-")) { - labelsToRemove.add(key.substring(0, key.length() - 1)); - } else { - labelsToAdd.put(key, value); - } - } - - executeWithErrorHandling( - "label " + resource + "/" + name, () -> { - Resource resourceClient = K8sClientHelper.getResourceClient( - client, - resource, - name, - resolveNamespace(namespace) - ); - applyLabelChanges(resourceClient, resource, name, labelsToAdd, labelsToRemove); - return null; - } - ); - - log.debug("Labels updated successfully"); - } - - /** - * Fetches the resource behind {@code resourceClient}, applies the given label additions/removals - * and writes it back. Kept as a generic helper (rather than inline in {@link #label}) because - * {@code io.fabric8.kubernetes.client.dsl.Resource#replace} requires the exact type returned by - * {@code Resource#get}; a wildcard-typed local variable can't satisfy that across two separate - * calls due to Java's per-expression wildcard capture, whereas a type variable bound once for the - * whole method invocation can. - */ - private static void applyLabelChanges( - Resource resourceClient, - String resource, - String name, - Map labelsToAdd, - List labelsToRemove) { - T existingResource = resourceClient.get(); - - if (existingResource == null) { - throw new IllegalStateException("Resource " + resource + "/" + name + " not found"); - } - - Map existingLabels = existingResource.getMetadata().getLabels(); - if (existingLabels == null) { - existingLabels = new HashMap<>(); - } else { - existingLabels = new HashMap<>(existingLabels); // ensure mutable - } - - for (String key : labelsToRemove) { - existingLabels.remove(key); - } - existingLabels.putAll(labelsToAdd); - - existingResource.getMetadata().setLabels(existingLabels); - resourceClient.patch(existingResource); - } - - /** - * Removes the given labels from a resource. - * - * @param resource resource type, e.g. {@code node} - * @param name resource name; {@code --all} applies to all nodes - * @param namespace namespace of the resource; empty means the default namespace - * @param keys label keys to remove - */ - public void labelRemove(String resource, String name, String namespace, String... keys) { - Tuple[] tuples = new Tuple[keys.length]; - for (int i = 0; i < keys.length; i++) { - tuples[i] = new Tuple<>(keys[i] + "-", ""); - } - label(resource, name, namespace, tuples); - } - - /** - * Patches a resource in the default namespace using the default patch type. - * - * @param resource resource type, e.g. {@code service} - * @param name resource name - * @param yaml patch content as nested map - */ - public void patch(String resource, String name, Map yaml) { - patch(resource, name, "", "", yaml); - } - - /** - * Patches a resource using the default patch type. - * - * @param resource resource type, e.g. {@code service} - * @param name resource name - * @param namespace namespace of the resource; empty means the default namespace - * @param yaml patch content as nested map - */ - public void patch(String resource, String name, String namespace, Map yaml) { - patch(resource, name, namespace, "", yaml); - } - - /** - * Patches a resource. - * - * @param resource resource type, e.g. {@code service} - * @param name resource name - * @param namespace namespace of the resource; empty means the default namespace - * @param type patch type: {@code merge}, {@code json-merge}, {@code strategic} or {@code json} - * @param yaml patch content as nested map - */ - public void patch(String resource, String name, String namespace, String type, Map yaml) { - log.debug("Patching {}/{} in namespace {}", resource, name, namespace); - - PatchContext patchContext = K8sClientHelper.createPatchContext(type); - String patchJson = Serialization.asJson(yaml); - log.trace("Patch JSON: {}", patchJson); - - executeWithErrorHandling( - "patch " + resource + "/" + name, () -> { - Resource resourceClient = K8sClientHelper.getResourceClient( - client, - resource, - name, - resolveNamespace(namespace) - ); - resourceClient.patch(patchContext, patchJson); - return null; - } - ); - - log.debug("Resource {}/{} patched successfully", resource, name); - } - - /** - * Deletes resources by label selectors in the default namespace, see {@link #delete(String, - * String, Tuple...)}. - * - * @param resource resource type, e.g. {@code secret} - */ - public void delete(String resource) { - delete(resource, "", new Tuple[0]); - } - - /** - * Deletes resources by label selectors, see {@link #delete(String, String, Tuple...)}. - * - * @param resource resource type, e.g. {@code secret} - * @param namespace namespace to delete in; empty means the default namespace - */ - public void delete(String resource, String namespace) { - delete(resource, namespace, new Tuple[0]); - } - - /** - * Deletes all resources of a type matching the given label selectors. Failures are logged, not - * thrown, since the resources may not exist. - * - * @param resource resource type, e.g. {@code secret} - * @param namespace namespace to delete in; empty means the default namespace - * @param selectors label key-value pairs the resources must match - */ - public void delete(String resource, String namespace, Tuple... selectors) { - log.debug("Deleting {} in namespace {} with selectors", resource, namespace); - - Map labels = new HashMap<>(); - if (selectors != null) { - for (Tuple tuple : selectors) { - labels.put(String.valueOf(tuple.getFirst()), String.valueOf(tuple.getSecond())); - } - } - - try { - K8sClientHelper.deleteResourcesByType(client, resource, resolveNamespace(namespace), labels); - log.debug("Resources deleted successfully"); - } catch (Exception e) { - log.warn("Failed to delete resources (may not exist): {}", e.getMessage()); - } - } - - /** - * Deletes a single resource by name. Failures are logged, not thrown, since the resource may not - * exist. - * - * @param resource resource type, e.g. {@code secret} - * @param namespace namespace of the resource; empty means the default namespace - * @param name resource name - */ - public void delete(String resource, String namespace, String name) { - log.debug("Deleting {}/{} in namespace {}", resource, name, namespace); - - try { - Resource resourceClient = K8sClientHelper.getResourceClient( - client, - resource, - name, - resolveNamespace(namespace) - ); - resourceClient.delete(); - log.debug("Resource {}/{} deleted successfully", resource, name); - } catch (Exception e) { - log.warn("Failed to delete resource (may not exist): {}", e.getMessage()); - } - } - - /** - * Runs a pod in the default namespace, see {@link #run(String, String, String, Map, String...)}. - * - * @param name name of the pod - * @param image container image to run - * @return a status message or, with {@code --rm}-style params, the pod output - */ - public String run(String name, String image) { - return run(name, image, "", Map.of(), new String[0]); - } - - /** - * Runs a pod, see {@link #run(String, String, String, Map, String...)}. - * - * @param name name of the pod - * @param image container image to run - * @param namespace target namespace; empty means the default namespace - * @return a status message or, with {@code --rm}-style params, the pod output - */ - public String run(String name, String image, String namespace) { - return run(name, image, namespace, Map.of(), new String[0]); - } - - /** - * Runs a pod with pod-spec overrides, see {@link #run(String, String, String, Map, String...)}. - * - * @param name name of the pod - * @param image container image to run - * @param namespace target namespace; empty means the default namespace - * @param overrides pod spec fields to override, analogous to {@code kubectl run --overrides} - * @return a status message or, with {@code --rm}-style params, the pod output - */ - public String run(String name, String image, String namespace, Map overrides) { - return run(name, image, namespace, overrides, new String[0]); - } - - /** - * Runs a pod with kubectl-run-style params, see {@link #run(String, String, String, Map, - * String...)}. - * - * @param name name of the pod - * @param image container image to run - * @param namespace target namespace; empty means the default namespace - * @param params kubectl-run-style flags such as {@code --rm} or {@code --restart=Never} - * @return a status message or, with {@code --rm}-style params, the pod output - */ - public String run(String name, String image, String namespace, String... params) { - return run(name, image, namespace, Map.of(), params); - } - - /** - * Runs a pod, analogous to {@code kubectl run}. - * - * @param name name of the pod - * @param image container image to run - * @param namespace target namespace; empty means the default namespace - * @param overrides pod spec fields to override, analogous to {@code kubectl run --overrides} - * @param params kubectl-run-style flags such as {@code --rm} or {@code --restart=Never} - * @return a status message or, when the params request output collection, the pod output - */ - public String run(String name, String image, String namespace, Map overrides, String... params) { - log.debug("Running pod {} with image {} in namespace {}", name, image, namespace); - String resolvedNamespace = resolveNamespace(namespace); - List runParams = params != null ? Arrays.asList(params) : Collections.emptyList(); - - Pod pod = new PodBuilder().withNewMetadata() - .withName(name) - .withNamespace(resolvedNamespace) - .endMetadata() - .withNewSpec() - .addNewContainer() - .withName(name) - .withImage(image) - .endContainer() - .endSpec() - .build(); - - K8sClientHelper.applyRunParams(pod, runParams); - - if (overrides != null && !overrides.isEmpty()) { - log.debug("Applying overrides: {}", overrides); - pod = K8sClientHelper.applyPodOverrides(pod, overrides); - } - - final Pod finalPod = pod; - Pod createdPod = executeWithErrorHandling( - "run pod " + name, () -> client.pods() - .inNamespace(resolvedNamespace) - .resource(finalPod) - .create() - ); - - log.debug("Pod {} created successfully", name); - if (K8sClientHelper.shouldReturnPodOutput(runParams)) { - return K8sClientHelper.collectPodRunOutput( - client, - createdPod.getMetadata() - .getName(), - resolvedNamespace, - K8sClientHelper.shouldRemovePod(runParams), - defaultRetries, - sleepTimeMillis, - this - ); - } - - return "pod/" + createdPod.getMetadata().getName() + " created"; - } - - /** - * Lists custom resources of the given type across all namespaces. - * - * @param resource custom resource type, resolved via API discovery - * @return namespace/name pairs of all found resources; empty when the type is unknown or listing - * fails - */ - public List getCustomResource(String resource) { - log.debug("Getting custom resources of type {}", resource); - - try { - Map match = K8sClientHelper.findApiResourceViaDiscovery( - client, - resource.toLowerCase(Locale.ROOT), - resource - ); - ResourceDefinitionContext context = new ResourceDefinitionContext.Builder().withGroup((String) match.get( - "group")) - .withVersion((String) match.get( - "version")) - .withKind((String) match.get( - "kind")) - .withPlural((String) match.get( - "plural")) - .withNamespaced((Boolean) match.get( - "namespaced")) - .build(); - - // `apiClient`'s type is a long nested generic (MixedOperation>); spelling it out - // would hurt readability more than `var` costs, so it's kept as `var` deliberately. - var apiClient = client.genericKubernetesResources(context); - GenericKubernetesResourceList resourceList = apiClient.inAnyNamespace().list(); - - if (resourceList == null || resourceList.getItems() == null) { - return Collections.emptyList(); - } - - return resourceList.getItems().stream().map(K8sClient::toCustomResource).toList(); - } catch (Exception e) { - log.warn("Failed to get custom resources: {}", e.getMessage()); - return Collections.emptyList(); - } - } - - private static CustomResource toCustomResource(GenericKubernetesResource item) { - Map metadata = item.getMetadata() != null ? Serialization.unmarshal( - Serialization.asJson(item.getMetadata()), - MAP_TYPE - ) : Collections.emptyMap(); - String ns = metadata.containsKey("namespace") ? String.valueOf(metadata.get("namespace")) : ""; - String name = metadata.containsKey("name") ? String.valueOf(metadata.get("name")) : ""; - return new CustomResource(ns, name); - } - - /** - * Reads an annotation from a resource in the default namespace. - * - * @param resource resource type, e.g. {@code service} - * @param name resource name - * @param key annotation key to read - * @return the annotation value; may be null when the annotation is not set - */ - public String getAnnotation(String resource, String name, String key) { - return getAnnotation(resource, name, key, ""); - } - - /** - * Reads an annotation from a resource. - * - * @param resource resource type, e.g. {@code service} - * @param name resource name - * @param key annotation key to read - * @param namespace namespace of the resource; empty means the default namespace - * @return the annotation value; may be null when the annotation is not set - */ - public String getAnnotation(String resource, String name, String key, String namespace) { - log.debug("Getting annotation {} from {}/{} in namespace {}", key, resource, name, namespace); - - Resource resourceClient = K8sClientHelper.getResourceClient( - client, - resource, - name, - resolveNamespace(namespace) - ); - HasMetadata k8sResource = resourceClient.get(); - - if (k8sResource == null) { - throw new IllegalStateException("Resource " + resource + "/" + name + " not found"); - } - - Map annotations = k8sResource.getMetadata().getAnnotations(); - if (annotations == null) { - throw new IllegalStateException("No annotations found on resource " + resource + "/" + name); - } - - String value = annotations.get(key); - log.debug("getAnnotation returns = {}", value); - return value; - } - - /** - * Returns the name of the current kubeconfig context. - * - * @return the context name, or a placeholder when no context is set - */ - public String getCurrentContext() { - try { - NamedContext currentContext = client.getConfiguration().getCurrentContext(); - String context = currentContext != null ? currentContext.getName() : null; - return context != null ? context : "(current context not set)"; - } catch (Exception e) { - log.trace("Failed to get current context: {}", e.getMessage()); - return "(current context not set)"; - } - } - - /** - * Waits for a resource to reach a phase using default timeout and check interval. - * - * @param resourceType resource type, e.g. {@code pod} - * @param resourceName resource name - * @param namespace namespace of the resource; empty means the default namespace - * @param desiredPhase phase to wait for, e.g. {@code Running} - */ - public void waitForResourcePhase(String resourceType, String resourceName, String namespace, String desiredPhase) { - waitForResourcePhase( - resourceType, - resourceName, - namespace, - desiredPhase, - DEFAULT_TIMEOUT_SECONDS, - DEFAULT_CHECK_INTERVAL_SECONDS - ); - } - - /** - * Waits for a resource to reach a phase, polling in fixed intervals until the timeout expires. - * - * @param resourceType resource type, e.g. {@code pod} - * @param resourceName resource name - * @param namespace namespace of the resource; empty means the default namespace - * @param desiredPhase phase to wait for, e.g. {@code Running} - * @param timeoutSeconds maximum time to wait before failing - * @param checkIntervalSeconds pause between phase checks - */ - public void waitForResourcePhase( - String resourceType, - String resourceName, - String namespace, - String desiredPhase, - int timeoutSeconds, - int checkIntervalSeconds) { - K8sClientHelper.validateWaitForResourcePhaseParams( - resourceType, - resourceName, - namespace, - desiredPhase, - timeoutSeconds, - checkIntervalSeconds - ); - - log.debug("Waiting for {}/{} to reach phase {}", resourceType, resourceName, desiredPhase); - - long startTime = System.currentTimeMillis(); - long endTime = startTime + ((long) timeoutSeconds * MILLIS_PER_SECOND); - - while (System.currentTimeMillis() < endTime) { - if (hasReachedPhase(resourceType, resourceName, namespace, desiredPhase)) { - log.debug( - "Resource {}/{} in namespace {} reached the desired phase: {}", - resourceType, - resourceName, - namespace, - desiredPhase - ); - return; - } - - try { - Thread.sleep((long) checkIntervalSeconds * MILLIS_PER_SECOND); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while waiting for resource phase", e); - } - } - - throw new IllegalStateException("Timeout reached. Resource " + resourceType + "/" + resourceName + " in namespace " + namespace + " did not reach the desired phase: " + desiredPhase + " within " + timeoutSeconds + " seconds."); - } - - private boolean hasReachedPhase(String resourceType, String resourceName, String namespace, String desiredPhase) { - try { - Resource resourceClient = K8sClientHelper.getResourceClient( - client, - resourceType, - resourceName, - resolveNamespace(namespace) - ); - HasMetadata resource = resourceClient.get(); - if (resource == null) { - return false; - } - - String phase = extractPhase(resource); - if (desiredPhase.equals(phase)) { - return true; - } - - log.debug("Current phase: {}. Waiting for phase: {}...", phase, desiredPhase); - return false; - } catch (Exception e) { - log.trace("Error checking resource phase: {}", e.getMessage()); - return false; - } - } - - private static String extractPhase(HasMetadata resource) { - if (resource instanceof Pod pod) { - return pod.getStatus() != null ? pod.getStatus().getPhase() : null; - } - - // Generic / Custom Resources - Map status = Serialization.unmarshal(Serialization.asJson(resource), MAP_TYPE); - Map statusMap = MapUtils.asStringObjectMap(status.get("status")); - return statusMap != null ? (String) statusMap.get("phase") : null; - } - - private T waitForResourceWithRetry(String resourceDescription, Supplier fetchSupplier) { - int tryCount = 0; - T result = null; - - while (result == null && tryCount < defaultRetries) { - try { - result = fetchSupplier.get(); - } catch (Exception e) { - log.trace("Error fetching {}: {}", resourceDescription, e.getMessage()); - } - - if (result == null) { - tryCount++; - log.debug("Still waiting for {}... (try {}/{})", resourceDescription, tryCount, defaultRetries); - try { - Thread.sleep(sleepTimeMillis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while waiting", e); - } - } - } - - if (result == null) { - throw new IllegalStateException("Failed to retrieve " + resourceDescription + " after " + defaultRetries + " retries"); - } - - return result; - } - - private static T executeWithErrorHandling(String operation, Supplier supplier) { - try { - return supplier.get(); - } catch (Exception e) { - throw new RuntimeException("Failed to " + operation + ": " + e.getMessage(), e); - } - } - - private static String resolveNamespace(String namespace) { - return namespace != null && !namespace.isEmpty() ? namespace : DEFAULT_NAMESPACE; - } - - /** - * Returns the namespace the client currently operates in. - * - * @return the current namespace from the kubeconfig context - */ - public String getCurrentNamespace() { - return this.client.getNamespace(); - } - - private boolean runInOpenshift() { - return this.gopConfig != null && this.gopConfig.getApplication() != null && this.gopConfig.getApplication() - .getOpenshift(); - } - - /** - * Namespace/name coordinate of a custom resource as returned by {@link #getCustomResource}. - * - * @param namespace namespace the resource lives in; empty for cluster-scoped resources - * @param name name of the resource - */ - public record CustomResource( - String namespace, - - String name - ) { - } - - /** - * Thrown when a custom resource type cannot be resolved via Kubernetes API discovery. - */ - public static class KubernetesApiResourceNotFoundException extends RuntimeException { - /** - * Creates the exception for the given unresolvable type. - * - * @param resourceType the custom resource type that could not be found - */ - public KubernetesApiResourceNotFoundException(String resourceType) { - super("No API resource found for custom resource type '" + resourceType + "'"); - } - } + private static final TypeReference> MAP_TYPE = new TypeReference<>() { + }; + + private static final String DEFAULT_NAMESPACE = "default"; + private static final String INTERNAL_IP_TYPE = "InternalIP"; + private static final String DOCKER_CONFIG_JSON_TYPE = "kubernetes.io/dockerconfigjson"; + private static final String DOCKER_CONFIG_JSON_KEY = ".dockerconfigjson"; + private static final String NOT_FOUND_IN_NAMESPACE = " not found in namespace "; + private static final String APPLIED_PREFIX = "Applied "; + + private static final int DEFAULT_TIMEOUT_SECONDS = 60; + private static final int DEFAULT_CHECK_INTERVAL_SECONDS = 1; + private static final int FABRIC8_REQUEST_TIMEOUT_MILLIS = 60_000; + private static final int FABRIC8_CONNECTION_TIMEOUT_MILLIS = 10_000; + private static final int MILLIS_PER_SECOND = 1000; + private static final int DEFAULT_SLEEP_TIME_MILLIS = MILLIS_PER_SECOND; + private static final int DEFAULT_RETRIES = 120; + + protected int sleepTimeMillis = DEFAULT_SLEEP_TIME_MILLIS; + protected int defaultRetries = DEFAULT_RETRIES; + + /** + * -- GETTER -- + * Returns the underlying fabric8 client. + *

+ *

+ * -- SETTER -- + * Replaces the underlying fabric8 client, mainly for tests. + * + * @return the fabric8 client + * @param client the fabric8 client to use + */ + @Setter + @Getter + private KubernetesClient client; + /** + * -- SETTER -- + * Sets the GitOps Playground config after construction. + * + * @param gopConfig the GitOps Playground config; may be null + */ + @Setter + private com.cloudogu.gitops.config.Config gopConfig; + + /** + * Creates a client with default fabric8 configuration and no playground config. + */ + public K8sClient() { + this(null); + } + + /** + * Creates a client with default fabric8 configuration. + * + * @param gopConfig the GitOps Playground config, used e.g. to detect OpenShift mode; may be null + */ + public K8sClient(com.cloudogu.gitops.config.Config gopConfig) { + io.fabric8.kubernetes.client.Config config = new ConfigBuilder().withRequestTimeout( + FABRIC8_REQUEST_TIMEOUT_MILLIS) + .withConnectionTimeout( + FABRIC8_CONNECTION_TIMEOUT_MILLIS) + .build(); + + this.client = new KubernetesClientBuilder().withConfig(config).build(); + this.gopConfig = gopConfig; + } + + /** + * Waits for the first node in the cluster to become available. + * + * @return The name of the first available node + */ + public String waitForNode() { + log.debug("Waiting for first node of the cluster to become ready"); + + String nodeName = waitForResourceWithRetry( + "node", () -> { + NodeList nodes = client.nodes().list(); + if (nodes != null && nodes.getItems() != null && !nodes.getItems().isEmpty()) { + return nodes.getItems().get(0).getMetadata().getName(); + } + return null; + } + ); + + log.debug("First node of the cluster is ready: {}", nodeName); + return nodeName; + } + + /** + * Waits for and retrieves the internal IP address of the first node. + * + * @return the internal IP address of the first node + */ + public String waitForInternalNodeIp() { + String nodeName = waitForNode(); + log.debug("Waiting for internal IP of node {}", nodeName); + + String internalIp = waitForResourceWithRetry( + "internal IP of node " + nodeName, + () -> findInternalNodeIp(nodeName) + ); + + log.debug("Internal IP of node {}: {}", nodeName, internalIp); + return internalIp; + } + + private String findInternalNodeIp(String nodeName) { + Node node = client.nodes().withName(nodeName).get(); + if (node != null && node.getStatus() != null && node.getStatus().getAddresses() != null) { + for (NodeAddress address : node.getStatus().getAddresses()) { + if (INTERNAL_IP_TYPE.equals(address.getType())) { + return address.getAddress(); + } + } + } + return null; + } + + /** + * Waits for a service's NodePort to become available. + * + * @param serviceName name of the service to inspect + * @param namespace namespace of the service; empty means the default namespace + * @return the NodePort of the service's first port + */ + public String waitForNodePort(String serviceName, String namespace) { + log.debug("Getting node port for service {}, ns={}", serviceName, namespace); + + String nodePort = waitForResourceWithRetry( + "node port for service " + serviceName, + () -> findServiceNodePort(serviceName, namespace) + ); + + log.debug("Node port for service {}, ns={}: {}", serviceName, namespace, nodePort); + return nodePort; + } + + private String findServiceNodePort(String serviceName, String namespace) { + Service service = client.services().inNamespace(namespace).withName(serviceName).get(); + if (service != null && service.getSpec() != null && service.getSpec().getPorts() != null && !service.getSpec() + .getPorts() + .isEmpty()) { + Integer port = service.getSpec().getPorts().get(0).getNodePort(); + return port != null ? port.toString() : null; + } + return null; + } + + /** + * Creates a NodePort service (idempotent). + * + * @param name name of the service to create + * @param tcp port mapping in the form {@code port[:targetPort]} + * @param nodePort fixed node port to expose; empty for auto-assignment + * @param namespace target namespace; empty means the default namespace + */ + public void createServiceNodePort(String name, String tcp, String nodePort, String namespace) { + log.debug("Creating NodePort service {} in namespace {}", name, namespace); + + String[] ports = tcp.split(":"); + int port = Integer.parseInt(ports[0]); + int targetPort = ports.length > 1 ? Integer.parseInt(ports[1]) : port; + + ServicePort servicePort = new ServicePortBuilder().withPort(port) + .withTargetPort(new IntOrString(targetPort)) + .build(); + if (nodePort != null && !nodePort.isEmpty()) { + servicePort.setNodePort(Integer.parseInt(nodePort)); + } + + Service service = new ServiceBuilder().withNewMetadata() + .withName(name) + .withNamespace(resolveNamespace(namespace)) + .endMetadata() + .withNewSpec() + .withType("NodePort") + .withPorts(servicePort) + .endSpec() + .build(); + + executeWithErrorHandling( + "create NodePort service " + name, () -> { + client.services() + .inNamespace(resolveNamespace(namespace)) + .resource(service) + .createOr(NonDeletingOperation::update); + return null; + } + ); + + log.debug("NodePort service {} created/updated successfully", name); + } + + /** + * Patches the nodePort of a specific port in a service. + * + * @param serviceName name of the service to patch + * @param namespace namespace of the service + * @param portName name of the port entry whose nodePort is replaced + * @param newNodePort new node port value + */ + public void patchServiceNodePort(String serviceName, String namespace, String portName, int newNodePort) { + K8sClientHelper.validateServiceNodePortPatch(serviceName, namespace, portName, newNodePort); + + log.debug("Patching service {} port {} with nodePort {}", serviceName, portName, newNodePort); + + Service service = client.services().inNamespace(namespace).withName(serviceName).get(); + + if (service == null) { + throw new IllegalStateException("Service " + serviceName + NOT_FOUND_IN_NAMESPACE + namespace); + } + + List ports = service.getSpec().getPorts(); + int portIndex = -1; + for (int i = 0; i < ports.size(); i++) { + if (portName.equals(ports.get(i).getName())) { + portIndex = i; + break; + } + } + + if (portIndex == -1) { + throw new IllegalStateException("Port with name " + portName + " not found in service " + serviceName + "."); + } + + // Create JSON patch + List> patch = List.of(Map.of( + "op", + "replace", + "path", + "/spec/ports/" + portIndex + "/nodePort", + "value", + newNodePort + )); + + String patchJson = Serialization.asJson(patch); + PatchContext patchContext = new PatchContext.Builder().withPatchType(io.fabric8.kubernetes.client.dsl.base.PatchType.JSON) + .build(); + + executeWithErrorHandling( + "patch service " + serviceName, () -> { + client.services().inNamespace(namespace).withName(serviceName).patch(patchContext, patchJson); + return null; + } + ); + + log.debug( + "Service {} in namespace {} successfully patched with nodePort {} for port {}.", + serviceName, + namespace, + newNodePort, + portName + ); + } + + /** + * Creates a namespace (or an OpenShift project) if it does not already exist (idempotent). + * + * @param name name of the namespace to create + */ + public void createNamespace(String name) { + K8sClientHelper.validateNamespaceName(name); + + if (!namespaceExists(name)) { + log.debug("Namespace {} does not exist, proceeding to create.", name); + + if (runInOpenshift()) { + OpenShiftClient osClient = client.adapt(OpenShiftClient.class); + + Project project = new ProjectBuilder().withNewMetadata().withName(name).endMetadata().build(); + executeWithErrorHandling( + "create project " + name, () -> { + osClient.projects().resource(project).create(); + return null; + } + ); + log.debug("Project {} created successfully.", name); + } else { + Namespace namespace = new NamespaceBuilder().withNewMetadata().withName(name).endMetadata().build(); + + executeWithErrorHandling( + "create namespace " + name, () -> { + client.namespaces().resource(namespace).create(); + return null; + } + ); + + log.debug("Namespace {} created successfully.", name); + } + } + } + + /** + * Creates multiple namespaces. + * + * @param names names of the namespaces to create + */ + public void createNamespaces(List names) { + if (names == null) { + throw new IllegalArgumentException("Namespaces must be provided and cannot be null."); + } + for (String name : names) { + createNamespace(name); + } + } + + /** + * Checks if a namespace exists. + * + * @param namespace name of the namespace to check + * @return true if the namespace exists + */ + public boolean namespaceExists(String namespace) { + try { + Namespace ns = client.namespaces().withName(namespace).get(); + if (ns != null) { + log.debug("Namespace {} already exists.", namespace); + return true; + } + } catch (Exception e) { + log.trace("Namespace {} does not exist: {}", namespace, e.getMessage()); + } + return false; + } + + /** + * Creates or updates an empty secret in the default namespace (idempotent). + * + * @param type secret type, e.g. {@code generic} + * @param name name of the secret + */ + public void createSecret(String type, String name) { + createSecret(type, name, "", new Tuple[0]); + } + + /** + * Creates or updates an empty secret (idempotent). + * + * @param type secret type, e.g. {@code generic} + * @param name name of the secret + * @param namespace target namespace; empty means the default namespace + */ + public void createSecret(String type, String name, String namespace) { + createSecret(type, name, namespace, new Tuple[0]); + } + + /** + * Creates or updates a generic secret (idempotent). + * + * @param type secret type; {@code generic} is mapped to {@code Opaque} + * @param name name of the secret + * @param namespace target namespace; empty means the default namespace + * @param literals key-value pairs stored as string data + */ + public void createSecret(String type, String name, String namespace, Tuple... literals) { + log.debug("Creating secret {} of type {} in namespace {}", name, type, namespace); + + Map data = new HashMap<>(); + if (literals != null) { + for (Tuple tuple : literals) { + data.put(String.valueOf(tuple.getFirst()), String.valueOf(tuple.getSecond())); + } + } + + String resolvedType = "generic".equals(type) ? "Opaque" : type; + Secret secret = new SecretBuilder().withNewMetadata() + .withName(name) + .withNamespace(resolveNamespace(namespace)) + .endMetadata() + .withType(resolvedType) + .withStringData(data) + .build(); + + executeWithErrorHandling( + "create secret " + name, () -> { + client.secrets() + .inNamespace(resolveNamespace(namespace)) + .resource(secret) + .createOr(NonDeletingOperation::update); + return null; + } + ); + + log.debug("Secret {} created/updated successfully", name); + } + + /** + * Creates or updates an image pull secret in the default namespace (idempotent). + * + * @param name name of the secret + * @param host registry host the credentials belong to + * @param user registry username + * @param password registry password + */ + public void createImagePullSecret(String name, String host, String user, String password) { + createImagePullSecret(name, "", host, user, password); + } + + /** + * Creates or updates an image pull secret (idempotent). + * + * @param name name of the secret + * @param namespace target namespace; empty means the default namespace + * @param host registry host the credentials belong to + * @param user registry username + * @param password registry password + */ + public void createImagePullSecret(String name, String namespace, String host, String user, String password) { + log.debug("Creating image pull secret {} in namespace {}", name, namespace); + + String auth = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8)); + String dockerConfig = Serialization.asJson( + Map.of("auths", Map.of(host, Map.of("username", user, "password", password, "auth", auth))) + ); + + Secret secret = new SecretBuilder().withNewMetadata() + .withName(name) + .withNamespace(resolveNamespace(namespace)) + .endMetadata() + .withType(DOCKER_CONFIG_JSON_TYPE) + .addToStringData(DOCKER_CONFIG_JSON_KEY, dockerConfig) + .build(); + + executeWithErrorHandling( + "create image pull secret " + name, () -> { + client.secrets() + .inNamespace(resolveNamespace(namespace)) + .resource(secret) + .createOr(NonDeletingOperation::update); + return null; + } + ); + + log.debug("Image pull secret {} created/updated successfully", name); + } + + /** + * Retrieves the {@code namespaces} data from an ArgoCD secret, waiting for the secret to appear. + * + * @param name name of the secret + * @param namespace namespace of the secret; empty means the default namespace + * @return the base64-encoded {@code namespaces} value of the secret + */ + public String getArgoCDNamespacesSecret(String name, String namespace) { + log.debug("Getting Secret {} from namespace {}", name, namespace); + + return waitForResourceWithRetry( + "secret " + name, () -> { + Secret secret = client.secrets().inNamespace(resolveNamespace(namespace)).withName(name).get(); + + return (secret != null && secret.getData() != null && secret.getData() + .containsKey("namespaces")) ? secret.getData() + .get( + "namespaces") : null; + } + ); + } + + + /** + * Extracts credentials from a Kubernetes secret using a Credentials object as input. + * + * @param credentials reference describing secret name, namespace and data keys + * @return a copy of the input with username and password resolved from the secret + */ + public Credentials getCredentialsFromSecret(Credentials credentials) { + if (credentials == null) { + return null; + } + if (credentials.getSecretName() == null || credentials.getSecretName().trim().isEmpty()) { + return credentials; + } + return executeWithErrorHandling( + "get credentials from secret " + credentials.getSecretName(), + () -> resolveCredentialsFromSecret(credentials) + ); + } + + private Credentials resolveCredentialsFromSecret(Credentials credentials) { + String namespace = (credentials.getSecretNamespace() != null && !credentials.getSecretNamespace().trim().isEmpty()) + ? credentials.getSecretNamespace() + : "default"; + Secret secret = client.secrets() + .inNamespace(namespace) + .withName(credentials.getSecretName()) + .get(); + if (secret == null || secret.getData() == null) { + throw new IllegalStateException("Secret " + credentials.getSecretName() + NOT_FOUND_IN_NAMESPACE + namespace); + } + + Map secretData = secret.getData(); + String usernameEncoded = secretData.get(credentials.getUsernameKey()); + String username = usernameEncoded != null ? new String( + Base64.getDecoder() + .decode(usernameEncoded), StandardCharsets.UTF_8 + ) : credentials.getUsername(); + String password = new String( + Base64.getDecoder() + .decode(secretData.get(credentials.getPasswordKey())), StandardCharsets.UTF_8 + ); + + Credentials credentialsNew = new Credentials(credentials); + credentialsNew.setUsername(username); + credentialsNew.setPassword(password); + + return credentialsNew; + } + + /** + * Creates or updates a ConfigMap from a file (idempotent). + * + * @param name name of the ConfigMap + * @param namespace target namespace; empty means the default namespace + * @param filePath path of the file whose content becomes the ConfigMap data + */ + public void createConfigMapFromFile(String name, String namespace, String filePath) { + log.debug("Creating ConfigMap {} from file {} in namespace {}", name, filePath, namespace); + + File file = new File(filePath); + if (!file.exists()) { + throw new IllegalStateException("File not found: " + filePath); + } + + String fileContent; + try { + fileContent = Files.readString(file.toPath()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read file: " + filePath, e); + } + + Map data = Map.of(file.getName(), fileContent); + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName(name) + .withNamespace(resolveNamespace(namespace)) + .endMetadata() + .withData(data) + .build(); + + executeWithErrorHandling( + "create ConfigMap " + name + " from file", () -> { + client.configMaps() + .inNamespace(resolveNamespace(namespace)) + .resource(configMap) + .createOr(NonDeletingOperation::update); + return null; + } + ); + + log.debug("ConfigMap {} created/updated successfully", name); + } + + /** + * Retrieves a value from a ConfigMap in the current namespace. + * + * @param mapName name of the ConfigMap + * @param key data key to read + * @return the value stored under the given key + */ + public String getConfigMap(String mapName, String key) { + String namespace = getCurrentNamespace(); + + log.debug("Getting ConfigMap {}/{}, key: {}", namespace, mapName, key); + + ConfigMap configMap = client.configMaps().inNamespace(namespace).withName(mapName).get(); + + if (configMap == null) { + throw new IllegalStateException("Could not fetch configmap " + mapName + " from namespace " + namespace); + } + + if (configMap.getData() == null || !configMap.getData().containsKey(key)) { + throw new IllegalStateException("Could not fetch " + key + " within config-map " + mapName + " from namespace " + namespace); + } + + return configMap.getData().get(key); + } + + /** + * Applies YAML resources from a URL, file or directory (recursively). + * + * @param yamlLocation http(s) URL, file path or directory path containing YAML resources + * @return a summary of how many resources were applied + */ + public String applyYaml(String yamlLocation) { + log.debug("Applying YAML from {}", yamlLocation); + + if (yamlLocation.startsWith("http://") || yamlLocation.startsWith("https://")) { + try { + int appliedResources = applyYamlStream(URI.create(yamlLocation).toURL().openStream(), yamlLocation); + return APPLIED_PREFIX + appliedResources + " resource(s) from " + yamlLocation; + } catch (IOException | IllegalArgumentException e) { + throw new UncheckedIOException("Failed to apply YAML from URL: " + yamlLocation, new IOException(e)); + } + } + + File location = new File(yamlLocation); + + if (!location.exists()) { + throw new IllegalStateException("File or directory not found: " + yamlLocation); + } + + if (location.isDirectory()) { + List yamlFiles; + try (Stream stream = Files.walk(location.toPath())) { + yamlFiles = stream.filter(Files::isRegularFile) + .map(Path::toFile) + .filter(file -> file.getName().endsWith(".yaml") || file.getName().endsWith(".yml")) + .collect(Collectors.toCollection(ArrayList::new)); + } catch (IOException e) { + throw new UncheckedIOException("Failed to list YAML files in directory: " + yamlLocation, e); + } + + yamlFiles.sort(Comparator.comparing(File::getAbsolutePath)); + + int appliedResources = 0; + for (File file : yamlFiles) { + try { + appliedResources += applyYamlStream(Files.newInputStream(file.toPath()), file.getAbsolutePath()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to apply YAML file: " + file.getAbsolutePath(), e); + } + } + + return APPLIED_PREFIX + appliedResources + " resource(s) from directory " + yamlLocation; + } + + try { + int appliedResources = applyYamlStream(Files.newInputStream(location.toPath()), yamlLocation); + return APPLIED_PREFIX + appliedResources + " resource(s) from " + yamlLocation; + } catch (IOException e) { + throw new UncheckedIOException("Failed to apply YAML file: " + yamlLocation, e); + } + } + + private int applyYamlStream(InputStream stream, String sourceDescription) { + List resources = executeWithErrorHandling( + "load YAML from " + sourceDescription, + () -> loadYamlItems(stream, sourceDescription) + ); + + for (HasMetadata resource : resources) { + executeWithErrorHandling( + "apply resource from " + sourceDescription, () -> { + client.resource(resource).createOr(NonDeletingOperation::update); + return null; + } + ); + } + + return resources.size(); + } + + private List loadYamlItems(InputStream stream, String sourceDescription) { + try (stream) { + return client.load(stream).items(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to close YAML input stream for " + sourceDescription, e); + } + } + + /** + * Adds or removes labels on a resource in the default namespace. + * + * @param resource resource type, e.g. {@code node} + * @param name resource name; {@code --all} applies to all nodes + * @param keyValues labels to set; a key ending in {@code -} removes that label + */ + public void label(String resource, String name, Tuple... keyValues) { + label(resource, name, "", keyValues); + } + + /** + * Adds or removes labels on a resource. + * + * @param resource resource type, e.g. {@code node} + * @param name resource name; {@code --all} applies to all nodes + * @param namespace namespace of the resource; empty means the default namespace + * @param keyValues labels to set; a key ending in {@code -} removes that label + */ + public void label(String resource, String name, String namespace, Tuple... keyValues) { + if (keyValues == null || keyValues.length == 0) { + throw new IllegalArgumentException("Missing key-value-pairs"); + } + + if ("--all".equals(name)) { + NodeList nodes = client.nodes().list(); + if (nodes != null && nodes.getItems() != null) { + for (Node node : nodes.getItems()) { + label(resource, node.getMetadata().getName(), namespace, keyValues); + } + } + return; + } + + log.debug("Labeling {}/{} in namespace {}", resource, name, namespace); + + Map labelsToAdd = new HashMap<>(); + List labelsToRemove = new ArrayList<>(); + + for (Tuple tuple : keyValues) { + String key = String.valueOf(tuple.getFirst()); + String value = String.valueOf(tuple.getSecond()); + + if (key.endsWith("-")) { + labelsToRemove.add(key.substring(0, key.length() - 1)); + } else { + labelsToAdd.put(key, value); + } + } + + executeWithErrorHandling( + "label " + resource + "/" + name, () -> { + Resource resourceClient = K8sClientHelper.getResourceClient( + client, + resource, + name, + resolveNamespace(namespace) + ); + applyLabelChanges(resourceClient, resource, name, labelsToAdd, labelsToRemove); + return null; + } + ); + + log.debug("Labels updated successfully"); + } + + /** + * Fetches the resource behind {@code resourceClient}, applies the given label additions/removals + * and writes it back. Kept as a generic helper (rather than inline in {@link #label}) because + * {@code io.fabric8.kubernetes.client.dsl.Resource#replace} requires the exact type returned by + * {@code Resource#get}; a wildcard-typed local variable can't satisfy that across two separate + * calls due to Java's per-expression wildcard capture, whereas a type variable bound once for the + * whole method invocation can. + */ + private static void applyLabelChanges( + Resource resourceClient, + String resource, + String name, + Map labelsToAdd, + List labelsToRemove) { + T existingResource = resourceClient.get(); + + if (existingResource == null) { + throw new IllegalStateException("Resource " + resource + "/" + name + " not found"); + } + + Map existingLabels = existingResource.getMetadata().getLabels(); + if (existingLabels == null) { + existingLabels = new HashMap<>(); + } else { + existingLabels = new HashMap<>(existingLabels); // ensure mutable + } + + for (String key : labelsToRemove) { + existingLabels.remove(key); + } + existingLabels.putAll(labelsToAdd); + + existingResource.getMetadata().setLabels(existingLabels); + resourceClient.patch(existingResource); + } + + /** + * Removes the given labels from a resource. + * + * @param resource resource type, e.g. {@code node} + * @param name resource name; {@code --all} applies to all nodes + * @param namespace namespace of the resource; empty means the default namespace + * @param keys label keys to remove + */ + public void labelRemove(String resource, String name, String namespace, String... keys) { + Tuple[] tuples = new Tuple[keys.length]; + for (int i = 0; i < keys.length; i++) { + tuples[i] = new Tuple<>(keys[i] + "-", ""); + } + label(resource, name, namespace, tuples); + } + + /** + * Patches a resource in the default namespace using the default patch type. + * + * @param resource resource type, e.g. {@code service} + * @param name resource name + * @param yaml patch content as nested map + */ + public void patch(String resource, String name, Map yaml) { + patch(resource, name, "", "", yaml); + } + + /** + * Patches a resource using the default patch type. + * + * @param resource resource type, e.g. {@code service} + * @param name resource name + * @param namespace namespace of the resource; empty means the default namespace + * @param yaml patch content as nested map + */ + public void patch(String resource, String name, String namespace, Map yaml) { + patch(resource, name, namespace, "", yaml); + } + + /** + * Patches a resource. + * + * @param resource resource type, e.g. {@code service} + * @param name resource name + * @param namespace namespace of the resource; empty means the default namespace + * @param type patch type: {@code merge}, {@code json-merge}, {@code strategic} or {@code json} + * @param yaml patch content as nested map + */ + public void patch(String resource, String name, String namespace, String type, Map yaml) { + log.debug("Patching {}/{} in namespace {}", resource, name, namespace); + + PatchContext patchContext = K8sClientHelper.createPatchContext(type); + String patchJson = Serialization.asJson(yaml); + log.trace("Patch JSON: {}", patchJson); + + executeWithErrorHandling( + "patch " + resource + "/" + name, () -> { + Resource resourceClient = K8sClientHelper.getResourceClient( + client, + resource, + name, + resolveNamespace(namespace) + ); + resourceClient.patch(patchContext, patchJson); + return null; + } + ); + + log.debug("Resource {}/{} patched successfully", resource, name); + } + + /** + * Deletes resources by label selectors in the default namespace, see {@link #delete(String, + * String, Tuple...)}. + * + * @param resource resource type, e.g. {@code secret} + */ + public void delete(String resource) { + delete(resource, "", new Tuple[0]); + } + + /** + * Deletes resources by label selectors, see {@link #delete(String, String, Tuple...)}. + * + * @param resource resource type, e.g. {@code secret} + * @param namespace namespace to delete in; empty means the default namespace + */ + public void delete(String resource, String namespace) { + delete(resource, namespace, new Tuple[0]); + } + + /** + * Deletes all resources of a type matching the given label selectors. Failures are logged, not + * thrown, since the resources may not exist. + * + * @param resource resource type, e.g. {@code secret} + * @param namespace namespace to delete in; empty means the default namespace + * @param selectors label key-value pairs the resources must match + */ + public void delete(String resource, String namespace, Tuple... selectors) { + log.debug("Deleting {} in namespace {} with selectors", resource, namespace); + + Map labels = new HashMap<>(); + if (selectors != null) { + for (Tuple tuple : selectors) { + labels.put(String.valueOf(tuple.getFirst()), String.valueOf(tuple.getSecond())); + } + } + + try { + K8sClientHelper.deleteResourcesByType(client, resource, resolveNamespace(namespace), labels); + log.debug("Resources deleted successfully"); + } catch (Exception e) { + log.warn("Failed to delete resources (may not exist): {}", e.getMessage()); + } + } + + /** + * Deletes a single resource by name. Failures are logged, not thrown, since the resource may not + * exist. + * + * @param resource resource type, e.g. {@code secret} + * @param namespace namespace of the resource; empty means the default namespace + * @param name resource name + */ + public void delete(String resource, String namespace, String name) { + log.debug("Deleting {}/{} in namespace {}", resource, name, namespace); + + try { + Resource resourceClient = K8sClientHelper.getResourceClient( + client, + resource, + name, + resolveNamespace(namespace) + ); + resourceClient.delete(); + log.debug("Resource {}/{} deleted successfully", resource, name); + } catch (Exception e) { + log.warn("Failed to delete resource (may not exist): {}", e.getMessage()); + } + } + + /** + * Runs a pod in the default namespace, see {@link #run(String, String, String, Map, String...)}. + * + * @param name name of the pod + * @param image container image to run + * @return a status message or, with {@code --rm}-style params, the pod output + */ + public String run(String name, String image) { + return run(name, image, "", Map.of(), new String[0]); + } + + /** + * Runs a pod, see {@link #run(String, String, String, Map, String...)}. + * + * @param name name of the pod + * @param image container image to run + * @param namespace target namespace; empty means the default namespace + * @return a status message or, with {@code --rm}-style params, the pod output + */ + public String run(String name, String image, String namespace) { + return run(name, image, namespace, Map.of(), new String[0]); + } + + /** + * Runs a pod with pod-spec overrides, see {@link #run(String, String, String, Map, String...)}. + * + * @param name name of the pod + * @param image container image to run + * @param namespace target namespace; empty means the default namespace + * @param overrides pod spec fields to override, analogous to {@code kubectl run --overrides} + * @return a status message or, with {@code --rm}-style params, the pod output + */ + public String run(String name, String image, String namespace, Map overrides) { + return run(name, image, namespace, overrides, new String[0]); + } + + /** + * Runs a pod with kubectl-run-style params, see {@link #run(String, String, String, Map, + * String...)}. + * + * @param name name of the pod + * @param image container image to run + * @param namespace target namespace; empty means the default namespace + * @param params kubectl-run-style flags such as {@code --rm} or {@code --restart=Never} + * @return a status message or, with {@code --rm}-style params, the pod output + */ + public String run(String name, String image, String namespace, String... params) { + return run(name, image, namespace, Map.of(), params); + } + + /** + * Runs a pod, analogous to {@code kubectl run}. + * + * @param name name of the pod + * @param image container image to run + * @param namespace target namespace; empty means the default namespace + * @param overrides pod spec fields to override, analogous to {@code kubectl run --overrides} + * @param params kubectl-run-style flags such as {@code --rm} or {@code --restart=Never} + * @return a status message or, when the params request output collection, the pod output + */ + public String run(String name, String image, String namespace, Map overrides, String... params) { + log.debug("Running pod {} with image {} in namespace {}", name, image, namespace); + String resolvedNamespace = resolveNamespace(namespace); + List runParams = params != null ? Arrays.asList(params) : Collections.emptyList(); + + Pod pod = new PodBuilder().withNewMetadata() + .withName(name) + .withNamespace(resolvedNamespace) + .endMetadata() + .withNewSpec() + .addNewContainer() + .withName(name) + .withImage(image) + .endContainer() + .endSpec() + .build(); + + K8sClientHelper.applyRunParams(pod, runParams); + + if (overrides != null && !overrides.isEmpty()) { + log.debug("Applying overrides: {}", overrides); + pod = K8sClientHelper.applyPodOverrides(pod, overrides); + } + + final Pod finalPod = pod; + Pod createdPod = executeWithErrorHandling( + "run pod " + name, () -> client.pods() + .inNamespace(resolvedNamespace) + .resource(finalPod) + .create() + ); + + log.debug("Pod {} created successfully", name); + if (K8sClientHelper.shouldReturnPodOutput(runParams)) { + return K8sClientHelper.collectPodRunOutput( + client, + createdPod.getMetadata() + .getName(), + resolvedNamespace, + K8sClientHelper.shouldRemovePod(runParams), + defaultRetries, + sleepTimeMillis, + this + ); + } + + return "pod/" + createdPod.getMetadata().getName() + " created"; + } + + /** + * Lists custom resources of the given type across all namespaces. + * + * @param resource custom resource type, resolved via API discovery + * @return namespace/name pairs of all found resources; empty when the type is unknown or listing + * fails + */ + public List getCustomResource(String resource) { + log.debug("Getting custom resources of type {}", resource); + + try { + Map match = K8sClientHelper.findApiResourceViaDiscovery( + client, + resource.toLowerCase(Locale.ROOT), + resource + ); + ResourceDefinitionContext context = new ResourceDefinitionContext.Builder().withGroup((String) match.get( + "group")) + .withVersion((String) match.get( + "version")) + .withKind((String) match.get( + "kind")) + .withPlural((String) match.get( + "plural")) + .withNamespaced((Boolean) match.get( + "namespaced")) + .build(); + + // `apiClient`'s type is a long nested generic (MixedOperation>); spelling it out + // would hurt readability more than `var` costs, so it's kept as `var` deliberately. + var apiClient = client.genericKubernetesResources(context); + GenericKubernetesResourceList resourceList = apiClient.inAnyNamespace().list(); + + if (resourceList == null || resourceList.getItems() == null) { + return Collections.emptyList(); + } + + return resourceList.getItems().stream().map(K8sClient::toCustomResource).toList(); + } catch (Exception e) { + log.warn("Failed to get custom resources: {}", e.getMessage()); + return Collections.emptyList(); + } + } + + private static CustomResource toCustomResource(GenericKubernetesResource item) { + Map metadata = item.getMetadata() != null ? Serialization.unmarshal( + Serialization.asJson(item.getMetadata()), + MAP_TYPE + ) : Collections.emptyMap(); + String ns = metadata.containsKey("namespace") ? String.valueOf(metadata.get("namespace")) : ""; + String name = metadata.containsKey("name") ? String.valueOf(metadata.get("name")) : ""; + return new CustomResource(ns, name); + } + + /** + * Reads an annotation from a resource in the default namespace. + * + * @param resource resource type, e.g. {@code service} + * @param name resource name + * @param key annotation key to read + * @return the annotation value; may be null when the annotation is not set + */ + public String getAnnotation(String resource, String name, String key) { + return getAnnotation(resource, name, key, ""); + } + + /** + * Reads an annotation from a resource. + * + * @param resource resource type, e.g. {@code service} + * @param name resource name + * @param key annotation key to read + * @param namespace namespace of the resource; empty means the default namespace + * @return the annotation value; may be null when the annotation is not set + */ + public String getAnnotation(String resource, String name, String key, String namespace) { + log.debug("Getting annotation {} from {}/{} in namespace {}", key, resource, name, namespace); + + Resource resourceClient = K8sClientHelper.getResourceClient( + client, + resource, + name, + resolveNamespace(namespace) + ); + HasMetadata k8sResource = resourceClient.get(); + + if (k8sResource == null) { + throw new IllegalStateException("Resource " + resource + "/" + name + " not found"); + } + + Map annotations = k8sResource.getMetadata().getAnnotations(); + if (annotations == null) { + throw new IllegalStateException("No annotations found on resource " + resource + "/" + name); + } + + String value = annotations.get(key); + log.debug("getAnnotation returns = {}", value); + return value; + } + + /** + * Returns the name of the current kubeconfig context. + * + * @return the context name, or a placeholder when no context is set + */ + public String getCurrentContext() { + try { + NamedContext currentContext = client.getConfiguration().getCurrentContext(); + String context = currentContext != null ? currentContext.getName() : null; + return context != null ? context : "(current context not set)"; + } catch (Exception e) { + log.trace("Failed to get current context: {}", e.getMessage()); + return "(current context not set)"; + } + } + + /** + * Waits for a resource to reach a phase using default timeout and check interval. + * + * @param resourceType resource type, e.g. {@code pod} + * @param resourceName resource name + * @param namespace namespace of the resource; empty means the default namespace + * @param desiredPhase phase to wait for, e.g. {@code Running} + */ + public void waitForResourcePhase(String resourceType, String resourceName, String namespace, String desiredPhase) { + waitForResourcePhase( + resourceType, + resourceName, + namespace, + desiredPhase, + DEFAULT_TIMEOUT_SECONDS, + DEFAULT_CHECK_INTERVAL_SECONDS + ); + } + + /** + * Waits for a resource to reach a phase, polling in fixed intervals until the timeout expires. + * + * @param resourceType resource type, e.g. {@code pod} + * @param resourceName resource name + * @param namespace namespace of the resource; empty means the default namespace + * @param desiredPhase phase to wait for, e.g. {@code Running} + * @param timeoutSeconds maximum time to wait before failing + * @param checkIntervalSeconds pause between phase checks + */ + public void waitForResourcePhase( + String resourceType, + String resourceName, + String namespace, + String desiredPhase, + int timeoutSeconds, + int checkIntervalSeconds) { + K8sClientHelper.validateWaitForResourcePhaseParams( + resourceType, + resourceName, + namespace, + desiredPhase, + timeoutSeconds, + checkIntervalSeconds + ); + + log.debug("Waiting for {}/{} to reach phase {}", resourceType, resourceName, desiredPhase); + + long startTime = System.currentTimeMillis(); + long endTime = startTime + ((long) timeoutSeconds * MILLIS_PER_SECOND); + + while (System.currentTimeMillis() < endTime) { + if (hasReachedPhase(resourceType, resourceName, namespace, desiredPhase)) { + log.debug( + "Resource {}/{} in namespace {} reached the desired phase: {}", + resourceType, + resourceName, + namespace, + desiredPhase + ); + return; + } + + try { + Thread.sleep((long) checkIntervalSeconds * MILLIS_PER_SECOND); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for resource phase", e); + } + } + + throw new IllegalStateException("Timeout reached. Resource " + resourceType + "/" + resourceName + " in namespace " + namespace + " did not reach the desired phase: " + desiredPhase + " within " + timeoutSeconds + " seconds."); + } + + private boolean hasReachedPhase(String resourceType, String resourceName, String namespace, String desiredPhase) { + try { + Resource resourceClient = K8sClientHelper.getResourceClient( + client, + resourceType, + resourceName, + resolveNamespace(namespace) + ); + HasMetadata resource = resourceClient.get(); + if (resource == null) { + return false; + } + + String phase = extractPhase(resource); + if (desiredPhase.equals(phase)) { + return true; + } + + log.debug("Current phase: {}. Waiting for phase: {}...", phase, desiredPhase); + return false; + } catch (Exception e) { + log.trace("Error checking resource phase: {}", e.getMessage()); + return false; + } + } + + private static String extractPhase(HasMetadata resource) { + if (resource instanceof Pod pod) { + return pod.getStatus() != null ? pod.getStatus().getPhase() : null; + } + + // Generic / Custom Resources + Map status = Serialization.unmarshal(Serialization.asJson(resource), MAP_TYPE); + Map statusMap = MapUtils.asStringObjectMap(status.get("status")); + return statusMap != null ? (String) statusMap.get("phase") : null; + } + + private T waitForResourceWithRetry(String resourceDescription, Supplier fetchSupplier) { + int tryCount = 0; + T result = null; + + while (result == null && tryCount < defaultRetries) { + try { + result = fetchSupplier.get(); + } catch (Exception e) { + log.trace("Error fetching {}: {}", resourceDescription, e.getMessage()); + } + + if (result == null) { + tryCount++; + log.debug("Still waiting for {}... (try {}/{})", resourceDescription, tryCount, defaultRetries); + try { + Thread.sleep(sleepTimeMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting", e); + } + } + } + + if (result == null) { + throw new IllegalStateException("Failed to retrieve " + resourceDescription + " after " + defaultRetries + " retries"); + } + + return result; + } + + private static T executeWithErrorHandling(String operation, Supplier supplier) { + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException("Failed to " + operation + ": " + e.getMessage(), e); + } + } + + private static String resolveNamespace(String namespace) { + return namespace != null && !namespace.isEmpty() ? namespace : DEFAULT_NAMESPACE; + } + + /** + * Returns the namespace the client currently operates in. + * + * @return the current namespace from the kubeconfig context + */ + public String getCurrentNamespace() { + return this.client.getNamespace(); + } + + private boolean runInOpenshift() { + return this.gopConfig != null && this.gopConfig.getApplication() != null && this.gopConfig.getApplication() + .getOpenshift(); + } + + /** + * Namespace/name coordinate of a custom resource as returned by {@link #getCustomResource}. + * + * @param namespace namespace the resource lives in; empty for cluster-scoped resources + * @param name name of the resource + */ + public record CustomResource( + String namespace, + + String name + ) { + } + + /** + * Thrown when a custom resource type cannot be resolved via Kubernetes API discovery. + */ + public static class KubernetesApiResourceNotFoundException extends RuntimeException { + /** + * Creates the exception for the given unresolvable type. + * + * @param resourceType the custom resource type that could not be found + */ + public KubernetesApiResourceNotFoundException(String resourceType) { + super("No API resource found for custom resource type '" + resourceType + "'"); + } + } } diff --git a/src/main/java/com/cloudogu/gitops/tools/common/CommonToolConfig.java b/src/main/java/com/cloudogu/gitops/tools/common/CommonToolConfig.java index 3ed1b9251..a46c67089 100644 --- a/src/main/java/com/cloudogu/gitops/tools/common/CommonToolConfig.java +++ b/src/main/java/com/cloudogu/gitops/tools/common/CommonToolConfig.java @@ -1,12 +1,122 @@ package com.cloudogu.gitops.tools.common; import com.cloudogu.gitops.config.Config; +import com.cloudogu.gitops.config.Credentials; +import com.cloudogu.gitops.infrastructure.kubernetes.api.K8sClient; + +import java.util.function.Consumer; public class CommonToolConfig implements ConfigLifecycleHook { + private final K8sClient k8sClient; + + public CommonToolConfig(K8sClient k8sClient) { + this.k8sClient = k8sClient; + } + @Override public void preConfigInit(Config configToSet) { validateConfig(configToSet); + + extractCredentials(configToSet); + } + + private void extractCredentials(Config config) { + var application = config.getApplication(); + resolve( + application.getCredentials(), resolved -> { + application.setCredentials(resolved); + application.setUsername(resolved.getUsername()); + application.setPassword(resolved.getPassword()); + } + ); + + var jenkins = config.getJenkins(); + resolve( + jenkins.getCredentials(), resolved -> { + jenkins.setCredentials(resolved); + jenkins.setUsername(resolved.getUsername()); + jenkins.setPassword(resolved.getPassword()); + } + ); + resolve( + jenkins.getMetricsCredentials(), resolved -> { + jenkins.setMetricsCredentials(resolved); + jenkins.setMetricsUsername(resolved.getUsername()); + jenkins.setMetricsPassword(resolved.getPassword()); + } + ); + + var registry = config.getRegistry(); + resolve( + registry.getCredentials(), resolved -> { + registry.setCredentials(resolved); + registry.setUsername(resolved.getUsername()); + registry.setPassword(resolved.getPassword()); + } + ); + resolve( + registry.getProxyCredentials(), resolved -> { + registry.setProxyCredentials(resolved); + registry.setProxyUsername(resolved.getUsername()); + registry.setProxyPassword(resolved.getPassword()); + } + ); + var scmManager = config.getScm().getScmManager(); + + if (scmManager != null) { + resolve( + scmManager.getCredentials(), resolved -> { + scmManager.setCredentials(resolved); + scmManager.setUsername(resolved.getUsername()); + scmManager.setPassword(resolved.getPassword()); + } + ); + } + var gitlab = config.getScm().getGitlab(); + if (gitlab != null) { + resolve( + gitlab.getCredentials(), resolved -> { + gitlab.setCredentials(resolved); + gitlab.setUsername(resolved.getUsername()); + gitlab.setPassword(resolved.getPassword()); + } + ); + } + + var centralScmManager = config.getMultiTenant().getScmManager(); + if (centralScmManager != null) { + resolve( + centralScmManager.getCredentials(), resolved -> { + centralScmManager.setCredentials(resolved); + centralScmManager.setUsername(resolved.getUsername()); + centralScmManager.setPassword(resolved.getPassword()); + } + ); + } + + var centralGitlab = config.getMultiTenant().getGitlab(); + if (centralGitlab != null) { + resolve( + centralGitlab.getCredentials(), resolved -> { + centralGitlab.setCredentials(resolved); + centralGitlab.setUsername(resolved.getUsername()); + centralGitlab.setPassword(resolved.getPassword()); + } + ); + } + + } + + private void resolve( + Credentials reference, + Consumer apply) { + if (reference == null || !reference.isUsed() + || reference.getSecretName().isBlank()) { + return; + } + + apply.accept(k8sClient.getCredentialsFromSecret(reference)); } /** @@ -19,9 +129,9 @@ public void validateConfig(Config configToSet) { private static void validateMirrorReposHelmChartFolderSet(Config configToSet) { if (configToSet.getApplication().getMirrorRepos() && (configToSet.getApplication() - .getLocalHelmChartFolder() == null || configToSet.getApplication() - .getLocalHelmChartFolder() - .isEmpty())) { + .getLocalHelmChartFolder() == null || configToSet.getApplication() + .getLocalHelmChartFolder() + .isEmpty())) { // This should only happen when run outside the image, i.e. during development throw new IllegalArgumentException("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"); } diff --git a/src/test/groovy/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.groovy b/src/test/groovy/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.groovy index e7a4068ee..05d41da00 100644 --- a/src/test/groovy/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.groovy @@ -30,11 +30,11 @@ import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable class ApplicationConfiguratorTest { - static final String EXPECTED_REGISTRY_URL = 'http://my-reg' - static final int EXPECTED_REGISTRY_INTERNAL_PORT = 33333 + 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' + 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 @@ -71,9 +71,8 @@ class ApplicationConfiguratorTest { fileSystemUtils = new FileSystemUtils() applicationConfigurator = new ApplicationConfigurator() testLogger = new TestLogger(applicationConfigurator.getClass()) - commonFeatureConfig = new CommonToolConfig() - K8sClient k8sClient = Mockito.mock(K8sClient) + commonFeatureConfig = new CommonToolConfig(k8sClient) HelmClient helmClient = Mockito.mock(HelmClient) GitRepoFactory gitRepoFactory = Mockito.mock(GitRepoFactory) Deployer deployer = Mockito.mock(Deployer) diff --git a/src/test/groovy/com/cloudogu/gitops/config/schema/ConfigTest.groovy b/src/test/groovy/com/cloudogu/gitops/config/schema/ConfigTest.groovy index 3512eca60..d22990e2c 100644 --- a/src/test/groovy/com/cloudogu/gitops/config/schema/ConfigTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/config/schema/ConfigTest.groovy @@ -1,83 +1,83 @@ package com.cloudogu.gitops.config.schema -import static com.cloudogu.gitops.config.Config.* -import static org.assertj.core.api.Assertions.assertThat - import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.utils.MapUtils - import org.junit.jupiter.api.Test import picocli.CommandLine +import static com.cloudogu.gitops.config.Config.* +import static org.assertj.core.api.Assertions.assertThat + class ConfigTest { - Config testConfig = new Config(registry: new RegistrySchema(twoRegistries: true, - internalPort: 123)) + Config testConfig = new Config(registry: new RegistrySchema(twoRegistries: true, + internalPort: 123)) - @Test - void 'converts to yaml including internals'() { - String config = testConfig.toYaml(true) + @Test + void 'converts to yaml including internals'() { + String config = testConfig.toYaml(true) - assertThat(config).startsWith("""--- + assertThat(config).startsWith("""--- registry: internal: true """) - } + } - @Test - void 'converts config map to yaml'() { + @Test + void 'converts config map to yaml'() { - String config = testConfig.toYaml(false) + String config = testConfig.toYaml(false) - assertThat(config).startsWith("""--- + assertThat(config).startsWith("""--- registry: active: false """) - } - - @Test - void 'creates from schema overwriting only Map values, ignoring null values'() { - Config expectedValues = new Config(application: new ApplicationSchema(// Overwrites a default String - username: 'myUser', - // Overwrites a default Boolean - yes: true, - // Sets an otherwise empty string - namePrefix: "aPrefix"), - // Overwrites a default Integer - registry: new RegistrySchema(internalPort: 42)) - - def actualValues = fromMap(expectedValues.toMap()) - - assertThat(actualValues.application.username).isEqualTo(expectedValues.application.username) - assertThat(actualValues.application.yes).isEqualTo(expectedValues.application.yes) - assertThat(actualValues.application.namePrefix).isEqualTo(expectedValues.application.namePrefix) - assertThat(actualValues.registry.internalPort).isEqualTo(expectedValues.registry.internalPort) - } - - @Test - void 'parses lowercase vault mode from config and preserves external representation'() { - Config config = Config.fromMap([features: [secrets: [vault: [mode: 'dev']]]]) - - assertThat(config.features.secrets.vault.mode).isEqualTo(VaultMode.DEV) - - Map configMap = config.toMap() - Map features = MapUtils.asStringObjectMap(configMap.get('features')) - Map secrets = MapUtils.asStringObjectMap(features.get('secrets')) - Map vault = MapUtils.asStringObjectMap(secrets.get('vault')) - assertThat(vault.get('mode')).isEqualTo('dev') - } - - @Test - void 'parses lowercase vault mode from cli'() { - Config config = new Config() - - new CommandLine(config).parseArgs('--vault=dev') - - assertThat(config.features.secrets.vault.mode).isEqualTo(VaultMode.DEV) - } - - @Test - void 'getting Tenantname from Config'() { - testConfig.application.namePrefix = 'testprefix-' - assertThat(testConfig.application.getTenantName()).isEqualTo("testprefix") - } -} \ No newline at end of file + } + + @Test + void 'creates from schema overwriting only Map values, ignoring null values'() { + Config expectedValues = new Config(application: new ApplicationSchema(// Overwrites a default String + username: 'myUser', + // Overwrites a default Boolean + yes: true, + // Sets an otherwise empty string + namePrefix: "aPrefix"), + // Overwrites a default Integer + registry: new RegistrySchema(internalPort: 42)) + + def actualValues = fromMap(expectedValues.toMap()) + + assertThat(actualValues.application.username).isEqualTo(expectedValues.application.username) + assertThat(actualValues.application.yes).isEqualTo(expectedValues.application.yes) + assertThat(actualValues.application.namePrefix).isEqualTo(expectedValues.application.namePrefix) + assertThat(actualValues.registry.internalPort).isEqualTo(expectedValues.registry.internalPort) + } + + @Test + void 'parses lowercase vault mode from config and preserves external representation'() { + Config config = Config.fromMap([features: [secrets: [vault: [mode: 'dev']]]]) + + assertThat(config.features.secrets.vault.mode).isEqualTo(VaultMode.DEV) + + Map configMap = config.toMap() + Map features = MapUtils.asStringObjectMap(configMap.get('features')) + Map secrets = MapUtils.asStringObjectMap(features.get('secrets')) + Map vault = MapUtils.asStringObjectMap(secrets.get('vault')) + assertThat(vault.get('mode')).isEqualTo('dev') + } + + @Test + void 'parses lowercase vault mode from cli'() { + Config config = new Config() + + new CommandLine(config).parseArgs('--vault=dev') + + assertThat(config.features.secrets.vault.mode).isEqualTo(VaultMode.DEV) + } + + @Test + void 'getting Tenantname from Config'() { + testConfig.application.namePrefix = 'testprefix-' + assertThat(testConfig.application.getTenantName()).isEqualTo("testprefix") + } + +} diff --git a/src/test/groovy/com/cloudogu/gitops/config/schema/CredentialsDelegationTest.groovy b/src/test/groovy/com/cloudogu/gitops/config/schema/CredentialsDelegationTest.groovy new file mode 100644 index 000000000..9174e2d79 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/config/schema/CredentialsDelegationTest.groovy @@ -0,0 +1,100 @@ +package com.cloudogu.gitops.config.schema + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.config.scm.ScmTenantSchema +import groovy.transform.CompileDynamic +import groovy.transform.TypeChecked +import groovy.transform.TypeCheckingMode +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import picocli.CommandLine + +import static org.assertj.core.api.Assertions.assertThat + +@CompileDynamic +@TypeChecked(TypeCheckingMode.SKIP) +class CredentialsDelegationTest { + + // ── Credentials class ────────────────────────────────────────────── + + @Test + void 'copy constructor copies all fields'() { + def original = new Credentials('admin', 'secret', 'my-secret', 'prod-ns', 'user', 'pass') + + def copy = new Credentials(original) + + assertThat(copy.username).isEqualTo('admin') + assertThat(copy.password).isEqualTo('secret') + assertThat(copy.secretName).isEqualTo('my-secret') + assertThat(copy.secretNamespace).isEqualTo('prod-ns') + assertThat(copy.usernameKey).isEqualTo('user') + assertThat(copy.passwordKey).isEqualTo('pass') + } + + @Test + void 'copy constructor with null is safe'() { + def copy = new Credentials(null) + + assertThat(copy.username).isNull() + assertThat(copy.password).isNull() + assertThat(copy.secretName).isNull() + } + + @Test + void 'two-arg constructor sets defaults for keys'() { + def creds = new Credentials('user', 'pw') + + assertThat(creds.username).isEqualTo('user') + assertThat(creds.password).isEqualTo('pw') + assertThat(creds.usernameKey).isEqualTo('username') + assertThat(creds.passwordKey).isEqualTo('password') + assertThat(creds.secretName).isEmpty() + assertThat(creds.secretNamespace).isEmpty() + } + + @ParameterizedTest + @ValueSource(strings = ['registry', 'jenkins', 'application']) + void 'setting credentials stores the reference without changing plain values'(String section) { + def schema = new Config()."$section" + schema.username = 'configured-user' + schema.password = 'configured-password' + def reference = new Credentials('', '', 'secret', 'namespace') + + schema.credentials = reference + + assertThat(schema.credentials).isSameAs(reference) + assertThat(schema.username).isEqualTo('configured-user') + assertThat(schema.password).isEqualTo('configured-password') + } + + @Test + void 'parses plain credentials from CLI arguments'() { + def config = new Config() + + new CommandLine(config).parseArgs( + '--registry-username', 'registry-user', '--registry-password', 'registry-password', + '--jenkins-username', 'jenkins-user', '--jenkins-password', 'jenkins-password', + '--username', 'app-user', '--password', 'app-password', + '--smtp-user', 'mail-user', '--smtp-password', 'mail-password' + ) + + assertThat(config.registry.username).isEqualTo('registry-user') + assertThat(config.registry.password).isEqualTo('registry-password') + assertThat(config.jenkins.username).isEqualTo('jenkins-user') + assertThat(config.jenkins.password).isEqualTo('jenkins-password') + assertThat(config.application.username).isEqualTo('app-user') + assertThat(config.application.password).isEqualTo('app-password') + assertThat(config.features.mail.smtpUser).isEqualTo('mail-user') + assertThat(config.features.mail.smtpPassword).isEqualTo('mail-password') + } + + @Test + void 'ScmManagerTenantConfig uses default admin credentials'() { + def schema = new ScmTenantSchema.ScmManagerTenantConfig() + + assertThat(schema.credentials.username).isEqualTo(Config.DEFAULT_ADMIN_USER) + assertThat(schema.credentials.password).isEqualTo(Config.DEFAULT_ADMIN_PW) + } +} diff --git a/src/test/groovy/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.groovy b/src/test/groovy/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.groovy index c9bc8c51c..a0dd94348 100644 --- a/src/test/groovy/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.groovy @@ -710,7 +710,7 @@ class K8sClientTest { .once() // When - Credentials creds = k8sApiClient.getCredentialsFromSecret("my-secret", "test-ns") + Credentials creds = k8sApiClient.getCredentialsFromSecret(new Credentials(null, null, "my-secret", "test-ns")) // Then assertThat(creds.username).isEqualTo("admin") diff --git a/src/test/groovy/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.groovy b/src/test/groovy/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.groovy index 7a4e88da5..bace30671 100644 --- a/src/test/groovy/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.groovy @@ -62,9 +62,7 @@ class ScmManagerSetupTest { ingress : 'scmm.master.localhost', skipRestart : false, skipPlugins : false, - gitOpsUsername: 'gitops', - credentials : [username: 'admin', - password: 'admin']]]]) + gitOpsUsername: 'gitops']]]) @BeforeEach void setUp() {