diff --git a/README.md b/README.md index 3e1f585d0..16bc99eaa 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,9 @@ We recommend running this command as an unprivileged user, that is inside the [d - [Why not use argocd-autopilot?](#why-not-use-argocd-autopilot) - [cluster-resources](#cluster-resources) - [Jenkins](#jenkins) - - [SCM-Manager](#scm-manager) + - [SCMs](#scms) + - [SCM-Manager](#scm-manager) + - [Gitlab](#gitlab) - [Monitoring tools](#monitoring-tools) - [Secrets Management Tools](#secrets-management-tools) - [dev mode](#dev-mode) @@ -266,21 +268,54 @@ You can also find a list of all CLI/config options [here](#overview-of-all-cli-a That is, if you pass a param via CLI, for example, it will overwrite the corresponding value in the configuration. ##### Overview of all CLI and config options - +- [Application](#application) - [Registry](#registry) - [Jenkins](#jenkins) -- [Multitenant](#multitenant) -- [SCMM](#scmm) -- [Application](#application) +- [SCM](#scmtenant) + - [SCMM](#scmmtenant) + - [GITLAB](#gitlabtenant) - [Images](#images) -- [Features](#features) - - [ArgoCD](#argocd) - - [Mail](#mail) - - [Monitoring](#monitoring) - - [Secrets](#secrets) - - [Ingress Nginx](#ingress-nginx) - - [Cert Manager](#cert-manager) +- [Features](#argocd) + - [ArgoCD](#argocd) + - [Mail](#mail) + - [Monitoring](#monitoring) + - [Secrets](#secrets) + - [Ingress Nginx](#ingress-nginx) + - [Cert Manager](#cert-manager) - [Content](#content) +- [Multitenant](#multitenant) + - [SCMM](#scm-managercentral) + - [GITLAB](#gitlabcentral) + +###### Application + +| CLI | Config | Default | Type | Description | +|-----|--------|---------|------|-------------| +| `--config-file` | - | `''` | String | Config file path | +| `--config-map` | - | `''` | String | Config map name | +| `-d, --debug` | `application.debug` | - | Boolean | Enable debug mode | +| `-x, --trace` | `application.trace` | - | Boolean | Enable trace mode | +| `--output-config-file` | `application.outputConfigFile` | `false` | Boolean | Output configuration file | +| `-v, --version` | `application.versionInfoRequested` | `false` | Boolean | Display version and license info | +| `-h, --help` | `application.usageHelpRequested` | `false` | Boolean | Display help message | +| `--remote` | `application.remote` | `false` | Boolean | Expose services as LoadBalancers | +| `--insecure` | `application.insecure` | `false` | Boolean | Sets insecure-mode in cURL which skips cert validation | +| `--openshift` | `application.openshift` | `false` | Boolean | When set, openshift specific resources and configurations are applied | +| `--username` | `application.username` | `'admin'` | String | Set initial admin username | +| `--password` | `application.password` | `'admin'` | String | Set initial admin passwords | +| `-y, --yes` | `application.yes` | `false` | Boolean | Skip confirmation | +| `--name-prefix` | `application.namePrefix` | `''` | String | Set name-prefix for repos, jobs, namespaces | +| `--destroy` | `application.destroy` | `false` | Boolean | Unroll playground | +| `--pod-resources` | `application.podResources` | `false` | Boolean | Write kubernetes resource requests and limits on each pod | +| `--git-name` | `application.gitName` | `'Cloudogu'` | String | Sets git author and committer name used for initial commits | +| `--git-email` | `application.gitEmail` | `'hello@cloudogu.com'` | String | Sets git author and committer email used for initial commits | +| `--base-url` | `application.baseUrl` | `''` | String | The external base url (TLD) for all tools | +| `--url-separator-hyphen` | `application.urlSeparatorHyphen` | `false` | Boolean | Use hyphens instead of dots to separate application name from base-url | +| `--mirror-repos` | `application.mirrorRepos` | `false` | Boolean | Changes the sources of deployed tools so they work in air-gapped environments | +| `--skip-crds` | `application.skipCrds` | `false` | Boolean | Skip installation of CRDs | +| `--namespace-isolation` | `application.namespaceIsolation` | `false` | Boolean | Configure tools to work with given namespaces only | +| `--netpols` | `application.netpols` | `false` | Boolean | Sets Network Policies | + ###### Registry @@ -322,19 +357,16 @@ That is, if you pass a param via CLI, for example, it will overwrite the corresp | - | `jenkins.helm.version` | `'5.8.43'` | String | The version of the Helm chart to be installed | | - | `jenkins.helm.values` | `[:]` | Map | Helm values of the chart | -###### Multitenant +###### Scm(Tenant) -| CLI | Config | Default | Type | Description | -|-----|--------|---------|------|-------------| -| `--dedicated-internal` | `multiTenant.internal` | `false` | Boolean | SCM for Central Management is running on the same cluster | -| `--dedicated-instance` | `multiTenant.useDedicatedInstance` | `false` | Boolean | Toggles the Dedicated Instances Mode | -| `--central-scm-url` | `multiTenant.centralScmUrl` | `''` | String | URL for the centralized Management Repo | -| `--central-scm-username` | `multiTenant.username` | `''` | String | CENTRAL SCMM USERNAME | -| `--central-scm-password` | `multiTenant.password` | `''` | String | CENTRAL SCMM Password | -| `--central-argocd-namespace` | `multiTenant.centralArgocdNamespace` | `'argocd'` | String | CENTRAL Argocd Repo Namespace | -| `--central-scm-namespace` | `multiTenant.centralSCMamespace` | `'scm-manager'` | String | Central SCM namespace | +| CLI | Config | Default | Type | Description | +|------------------|---------------------------------|--------------|-------------------------|-----------------------------------------------------------------------| +| `--scm-provider` | `scmTenant.scmProviderType` | `SCM_MANAGER` | ScmProviderType | Specifies the SCM provider type. Possible values: `SCM_MANAGER`, `GITLAB`. | +| | `scmTenant.gitOpsUsername` | `''` | String | The username for the GitOps user. | +| | `scmTenant.gitlab` | `''` | GitlabTenantConfig | Configuration for GitLab, including URL, username, token, and parent group ID. | +| | `scmTenant.scmManager` | `''` | ScmManagerTenantConfig | Configuration for SCM Manager, such as internal setup or plugin handling. | -###### SCMM +###### SCMM(Tenant) | CLI | Config | Default | Type | Description | |-----|--------|---------|------|-------------| @@ -344,40 +376,22 @@ That is, if you pass a param via CLI, for example, it will overwrite the corresp | `--scmm-username` | `scmm.username` | `'admin'` | String | Mandatory when scmm-url is set | | `--scmm-password` | `scmm.password` | `'admin'` | String | Mandatory when scmm-url is set | | `--scm-root-path` | `scmm.rootPath` | `'repo'` | String | Sets the root path for the Git Repositories | -| `--scm-provider` | `scmm.provider` | `'scm-manager'` | String | Sets the scm Provider. Possible Options are "scm-manager" and "gitlab" | | - | `scmm.helm.chart` | `'scm-manager'` | String | Name of the Helm chart | | - | `scmm.helm.repoURL` | `'https://packages.scm-manager.org/repository/helm-v2-releases/'` | String | Repository url from which the Helm chart should be obtained | | - | `scmm.helm.version` | `'3.10.2'` | String | The version of the Helm chart to be installed | | - | `scmm.helm.values` | `[:]` | Map | Helm values of the chart | -###### Application -| CLI | Config | Default | Type | Description | -|-----|--------|---------|------|-------------| -| `--config-file` | - | `''` | String | Config file path | -| `--config-map` | - | `''` | String | Config map name | -| `-d, --debug` | `application.debug` | - | Boolean | Enable debug mode | -| `-x, --trace` | `application.trace` | - | Boolean | Enable trace mode | -| `--output-config-file` | `application.outputConfigFile` | `false` | Boolean | Output configuration file | -| `-v, --version` | `application.versionInfoRequested` | `false` | Boolean | Display version and license info | -| `-h, --help` | `application.usageHelpRequested` | `false` | Boolean | Display help message | -| `--remote` | `application.remote` | `false` | Boolean | Expose services as LoadBalancers | -| `--insecure` | `application.insecure` | `false` | Boolean | Sets insecure-mode in cURL which skips cert validation | -| `--openshift` | `application.openshift` | `false` | Boolean | When set, openshift specific resources and configurations are applied | -| `--username` | `application.username` | `'admin'` | String | Set initial admin username | -| `--password` | `application.password` | `'admin'` | String | Set initial admin passwords | -| `-y, --yes` | `application.yes` | `false` | Boolean | Skip confirmation | -| `--name-prefix` | `application.namePrefix` | `''` | String | Set name-prefix for repos, jobs, namespaces | -| `--destroy` | `application.destroy` | `false` | Boolean | Unroll playground | -| `--pod-resources` | `application.podResources` | `false` | Boolean | Write kubernetes resource requests and limits on each pod | -| `--git-name` | `application.gitName` | `'Cloudogu'` | String | Sets git author and committer name used for initial commits | -| `--git-email` | `application.gitEmail` | `'hello@cloudogu.com'` | String | Sets git author and committer email used for initial commits | -| `--base-url` | `application.baseUrl` | `''` | String | The external base url (TLD) for all tools | -| `--url-separator-hyphen` | `application.urlSeparatorHyphen` | `false` | Boolean | Use hyphens instead of dots to separate application name from base-url | -| `--mirror-repos` | `application.mirrorRepos` | `false` | Boolean | Changes the sources of deployed tools so they work in air-gapped environments | -| `--skip-crds` | `application.skipCrds` | `false` | Boolean | Skip installation of CRDs | -| `--namespace-isolation` | `application.namespaceIsolation` | `false` | Boolean | Configure tools to work with given namespaces only | -| `--netpols` | `application.netpols` | `false` | Boolean | Sets Network Policies | +###### Gitlab(Tenant) + +| CLI | Config | Default | Type | Description | +|-------------------|--------------------|-----------|--------|------------------------------------------------------------------------------------------------------------| +| `--gitlab-url` | `gitlabTenant.url` | `''` | String | Base URL for the GitLab instance. | +| `--gitlab-username` | `gitlabTenant.username` | `'oauth2.0'` | String | Defaults to: `oauth2.0` when a PAT token is provided. | +| `--gitlab-token` | `gitlabTenant.password` | `''` | String | PAT token for the account. | +| `--gitlab-parent-id` | `gitlabTenant.parentGroupId` | `''` | String | The numeric ID for the GitLab Group where repositories and subgroups should be created. | +| | `gitlabTenant.internal` | `false` | Boolean | Indicates if GitLab is running in the same Kubernetes cluster. Currently only external URLs are supported. | + ###### Images @@ -503,6 +517,37 @@ That is, if you pass a param via CLI, for example, it will overwrite the corresp | - | `content.repos[].overwriteMode` | `INIT` | OverwriteMode | How customer repos will be updated (INIT, RESET, UPGRADE) | | - | `content.repos[].createJenkinsJob` | `false` | Boolean | If true, creates a Jenkins job | +###### MultiTenant + +| CLI | Config | Default | Type | Description | +|------------------------------|-------------------------------------|---------------|--------------------------|----------------------------------------------------------------| +| `--dedicated-instance` | `multiTenant.useDedicatedInstance` | `false` | Boolean | Toggles the Dedicated Instances Mode. See docs for more info | +| `--central-argocd-namespace` | `multiTenant.centralArgocdNamespace`| `'argocd'` | String | Namespace for the centralized Argocd | +| `--central-scm-provider` | `multiTenant.scmProviderType` | `SCM_MANAGER` | ScmProviderType | The SCM provider type. Possible values: `SCM_MANAGER`, `GITLAB`| +| | `multiTenant.gitlab` | `` | GitlabCentralConfig | Config for GITLAB | +| | `multiTenant.scmManager` | `` | ScmManagerCentralConfig | Config for SCM Manager | + +###### Gitlab(Central) + +| CLI | Config | Default | Type | Description | +|------------------------------|--------------------------------|-------------|---------|------------------------------------------------------------------| +| `--central-gitlab-url` | `multiTenant.gitlab.url` | `''` | String | URL for external Gitlab | +| `--central-gitlab-username` | `multiTenant.gitlab.username` | `'oauth2.0'`| String | Username for GitLab 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 | +| | `multiTenant.gitlab.internal` | `false` | Boolean | SCM is running on the same cluster (only external supported now) | + +###### Scm-Manager(Central) + +| CLI | Config | Default | Type | Description | +|------------------------------|-------------------------------------|-----------------|---------|--------------------------------------------------------------------------------------| +| `--central-scmm-internal` | `multiTenant.scmManager.internal` | `false` | 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-root-path` | `multiTenant.scmManager.rootPath` | `'repo'` | String | Root path for SCM Manager | +| `--central-scmm-namespace` | `multiTenant.scmManager.namespace` | `'scm-manager'` | String | Namespace where to find the Central SCMM | + ##### Configuration file You can also use a configuration file to specify the parameters (`--config-file` or `--config-map`). @@ -669,8 +714,8 @@ To use them locally, * `--argocd` - deploy Argo CD GitOps operator > ⚠️ **Note** that switching between operators is not supported. -That is, expect errors (for example with cluster-resources) if you apply the playground once with Argo CD and the next -time without it. We recommend resetting the cluster with `init-cluster.sh` beforehand. +> That is, expect errors (for example with cluster-resources) if you apply the playground once with Argo CD and the next +> time without it. We recommend resetting the cluster with `init-cluster.sh` beforehand. ##### Deploy with local Cloudogu Ecosystem @@ -1104,6 +1149,24 @@ To apply additional global environments for jenkins you can use `--jenkins-addit Note that the [example applications](#example-applications) pipelines will only run on a Jenkins that uses agents that provide a docker host. That is, Jenkins must be able to run e.g. `docker ps` successfully on the agent. +## SCMs + +You can choose between the following Git providers: + +- SCM-Manager +- GitLab + +For configuration details, see the CLI or configuration parameters above ([SCM](#scmtenant)). + +### GitLab + +When using GitLab, you must provide a valid **parent group ID**. +This group will serve as the main group for the GOP to create and manage all required repositories. + +[![gitlab ParentID](docs/gitlab-parentid.png)](https://docs.gitlab.com/user/group/#find-the-group-id) + +To authenticate with Gitlab provide a token token as password. More information can be found [here](https://docs.gitlab.com/api/rest/authentication/) or [here](https://docs.gitlab.com/user/profile/personal_access_tokens/) +The username should remain 'oauth2.0' to access the API, unless stated otherwise by GitLab documentation. ### SCM-Manager You can set an external SCM-Manager via the following parameters when applying the playground. @@ -1333,4 +1396,4 @@ Garküche 1 or you may email hello@cloudogu.com. Your request must be sent within three years from the date you received the software from Cloudogu that is the subject of your request or, in the case of source code licensed under the AGPL/GPL/LGPL v3, for as long as Cloudogu offers spare parts or customer support -for the product, including the components or binaries that are the subject of your request. +for the product, including the components or binaries that are the subject of your request. \ No newline at end of file diff --git a/applications/cluster-resources/monitoring/prometheus-stack-helm-values.ftl.yaml b/applications/cluster-resources/monitoring/prometheus-stack-helm-values.ftl.yaml index 5181c59f3..6dc2fdb67 100644 --- a/applications/cluster-resources/monitoring/prometheus-stack-helm-values.ftl.yaml +++ b/applications/cluster-resources/monitoring/prometheus-stack-helm-values.ftl.yaml @@ -341,14 +341,16 @@ prometheus: - prometheus-metrics-creds-scmm - prometheus-metrics-creds-jenkins additionalScrapeConfigs: +<#if config.scm.scmProviderType?lower_case == "scm_manager"> - job_name: 'scm-manager' static_configs: - - targets: [ '${scmm.host}' ] - scheme: ${scmm.protocol} - metrics_path: '${scmm.path}' + - targets: [ '${scm.host}' ] + scheme: ${scm.protocol} + metrics_path: '${scm.path}' basic_auth: username: '${config.application.namePrefix}metrics' password_file: '/etc/prometheus/secrets/prometheus-metrics-creds-scmm/password' + - job_name: 'jenkins' static_configs: - targets: [ '${jenkins.host}' ] diff --git a/argocd/argocd/applications/argocd.ftl.yaml b/argocd/argocd/applications/argocd.ftl.yaml index 80e85e1e1..99a0f6a4b 100644 --- a/argocd/argocd/applications/argocd.ftl.yaml +++ b/argocd/argocd/applications/argocd.ftl.yaml @@ -19,7 +19,7 @@ spec: project: argocd source: path: ${config.features.argocd.operator?string("operator/", "argocd/")} - repoURL: ${scmm.repoUrl}argocd/argocd<#if config.scmm.provider == "gitlab">.git + repoURL: ${scm.repoUrl}argocd/argocd.git targetRevision: main # needed to sync the operator/rbac folder <#if config.features.argocd.operator> diff --git a/argocd/argocd/applications/bootstrap.ftl.yaml b/argocd/argocd/applications/bootstrap.ftl.yaml index 0785dddb6..6454ab3ad 100644 --- a/argocd/argocd/applications/bootstrap.ftl.yaml +++ b/argocd/argocd/applications/bootstrap.ftl.yaml @@ -14,7 +14,7 @@ spec: project: argocd source: path: applications/ - repoURL: ${scmm.repoUrl}argocd/argocd<#if config.scmm.provider == "gitlab">.git + repoURL: ${scm.repoUrl}argocd/argocd.git targetRevision: main directory: recurse: true diff --git a/argocd/argocd/applications/cluster-resources.ftl.yaml b/argocd/argocd/applications/cluster-resources.ftl.yaml index 200edbc37..9b4b11302 100644 --- a/argocd/argocd/applications/cluster-resources.ftl.yaml +++ b/argocd/argocd/applications/cluster-resources.ftl.yaml @@ -13,7 +13,7 @@ spec: project: argocd source: path: argocd/ - repoURL: ${scmm.repoUrl}argocd/cluster-resources<#if config.scmm.provider == "gitlab">.git + repoURL: ${scm.repoUrl}argocd/cluster-resources.git targetRevision: main directory: recurse: true diff --git a/argocd/argocd/applications/example-apps.ftl.yaml b/argocd/argocd/applications/example-apps.ftl.yaml deleted file mode 100644 index 02f9402d3..000000000 --- a/argocd/argocd/applications/example-apps.ftl.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: example-apps - namespace: ${config.application.namePrefix}argocd -# finalizer disabled, because otherwise everything under this Application would be deleted as well, if this Application is deleted by accident -# finalizers: -# - resources-finalizer.argocd.argoproj.io -spec: - destination: - namespace: ${config.application.namePrefix}argocd - server: https://kubernetes.default.svc - project: argocd - source: - path: argocd/ - repoURL: ${scmm.repoUrl}argocd/example-apps<#if config.scmm.provider == "gitlab">.git - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: false # is set to false to prevent projects to be deleted by accident - selfHeal: true diff --git a/argocd/argocd/applications/projects.ftl.yaml b/argocd/argocd/applications/projects.ftl.yaml index acdfa3b14..9c66cd88e 100644 --- a/argocd/argocd/applications/projects.ftl.yaml +++ b/argocd/argocd/applications/projects.ftl.yaml @@ -13,7 +13,7 @@ spec: project: argocd source: path: projects/ - repoURL: ${scmm.repoUrl}argocd/argocd<#if config.scmm.provider == "gitlab">.git + repoURL: ${scm.repoUrl}argocd/argocd.git targetRevision: main directory: recurse: true diff --git a/argocd/argocd/multiTenant/central/applications/argocd.ftl.yaml b/argocd/argocd/multiTenant/central/applications/argocd.ftl.yaml index 09ffecae6..994685220 100644 --- a/argocd/argocd/multiTenant/central/applications/argocd.ftl.yaml +++ b/argocd/argocd/multiTenant/central/applications/argocd.ftl.yaml @@ -19,7 +19,7 @@ spec: project: ${tenantName} source: path: ${config.features.argocd.operator?string("operator/", "argocd/")} - repoURL: ${scmm.centralScmmUrl}/repo/${config.application.namePrefix}argocd/argocd + repoURL: ${scm.centralScmUrl}argocd/argocd.git targetRevision: main # needed to sync the operator/rbac folder <#if config.features.argocd.operator??> diff --git a/argocd/argocd/multiTenant/central/applications/bootstrap.ftl.yaml b/argocd/argocd/multiTenant/central/applications/bootstrap.ftl.yaml index ad529c726..71a56c1af 100644 --- a/argocd/argocd/multiTenant/central/applications/bootstrap.ftl.yaml +++ b/argocd/argocd/multiTenant/central/applications/bootstrap.ftl.yaml @@ -14,7 +14,7 @@ spec: project: ${tenantName} source: path: multiTenant/central/applications/ - repoURL: ${scmm.centralScmmUrl}/repo/${config.application.namePrefix}argocd/argocd + repoURL: ${scm.centralScmUrl}argocd/argocd.git targetRevision: main directory: recurse: true diff --git a/argocd/argocd/multiTenant/central/applications/cluster-resources.ftl.yaml b/argocd/argocd/multiTenant/central/applications/cluster-resources.ftl.yaml index 57a671035..b27f1a8c0 100644 --- a/argocd/argocd/multiTenant/central/applications/cluster-resources.ftl.yaml +++ b/argocd/argocd/multiTenant/central/applications/cluster-resources.ftl.yaml @@ -13,7 +13,7 @@ spec: project: ${tenantName} source: path: argocd/ - repoURL: ${scmm.centralScmmUrl}/repo/${config.application.namePrefix}argocd/cluster-resources + repoURL: ${scm.centralScmUrl}argocd/cluster-resources.git targetRevision: main directory: recurse: true diff --git a/argocd/argocd/multiTenant/central/applications/projects.ftl.yaml b/argocd/argocd/multiTenant/central/applications/projects.ftl.yaml index b9eb2259e..22daaf97b 100644 --- a/argocd/argocd/multiTenant/central/applications/projects.ftl.yaml +++ b/argocd/argocd/multiTenant/central/applications/projects.ftl.yaml @@ -13,7 +13,7 @@ spec: project: ${tenantName} source: path: multiTenant/central/projects/ - repoURL: ${scmm.centralScmmUrl}/repo/${config.application.namePrefix}argocd/argocd + repoURL: ${scm.centralScmUrl}argocd/argocd.git targetRevision: main directory: recurse: true diff --git a/argocd/argocd/multiTenant/central/projects/tenant.ftl.yaml b/argocd/argocd/multiTenant/central/projects/tenant.ftl.yaml index 74d479293..12f54540f 100644 --- a/argocd/argocd/multiTenant/central/projects/tenant.ftl.yaml +++ b/argocd/argocd/multiTenant/central/projects/tenant.ftl.yaml @@ -9,15 +9,15 @@ spec: - namespace: '*' server: https://kubernetes.default.svc sourceRepos: - - ${scmm.centralScmmUrl}/repo/${config.application.namePrefix}argocd/argocd - - ${scmm.centralScmmUrl}/repo/${config.application.namePrefix}argocd/cluster-resources + - ${scm.centralScmUrl}argocd/argocd.git + - ${scm.centralScmUrl}argocd/cluster-resources.git <#if config.application.mirrorRepos> - - ${scmm.repoUrl}3rd-party-dependencies/kube-prometheus-stack<#if config.scmm.provider == "gitlab">.git - - ${scmm.repoUrl}3rd-party-dependencies/mailhog<#if config.scmm.provider == "gitlab">.git - - ${scmm.repoUrl}3rd-party-dependencies/ingress-nginx<#if config.scmm.provider == "gitlab">.git - - ${scmm.repoUrl}3rd-party-dependencies/external-secrets<#if config.scmm.provider == "gitlab">.git - - ${scmm.repoUrl}3rd-party-dependencies/vault<#if config.scmm.provider == "gitlab">.git - - ${scmm.repoUrl}3rd-party-dependencies/cert-manager<#if config.scmm.provider == "gitlab">.git + - ${scm.repoUrl}3rd-party-dependencies/kube-prometheus-stack.git + - ${scm.repoUrl}3rd-party-dependencies/mailhog.git + - ${scm.repoUrl}3rd-party-dependencies/ingress-nginx.git + - ${scm.repoUrl}3rd-party-dependencies/external-secrets.git + - ${scm.repoUrl}3rd-party-dependencies/vault.git + - ${scm.repoUrl}3rd-party-dependencies/cert-manager.git <#else> - https://prometheus-community.github.io/helm-charts - https://codecentric.github.io/helm-charts diff --git a/argocd/argocd/multiTenant/tenant/applications/bootstrap.ftl.yaml b/argocd/argocd/multiTenant/tenant/applications/bootstrap.ftl.yaml index 129b6be32..993671016 100644 --- a/argocd/argocd/multiTenant/tenant/applications/bootstrap.ftl.yaml +++ b/argocd/argocd/multiTenant/tenant/applications/bootstrap.ftl.yaml @@ -14,7 +14,7 @@ spec: project: argocd source: path: applications/ - repoURL: ${scmm.repoUrl}argocd/argocd + repoURL: ${scm.repoUrl}argocd/argocd.git targetRevision: main directory: recurse: true @@ -39,7 +39,7 @@ spec: project: argocd source: path: projects/ - repoURL: ${scmm.repoUrl}argocd/argocd + repoURL: ${scm.repoUrl}argocd/argocd.git targetRevision: main directory: recurse: true @@ -67,8 +67,8 @@ spec: - namespace: ${config.application.namePrefix}example-apps-staging server: https://kubernetes.default.svc sourceRepos: - - ${scmm.repoUrl}argocd/example-apps<#if config.scmm.provider == "gitlab">.git - - ${scmm.repoUrl}argocd/nginx-helm-umbrella<#if config.scmm.provider == "gitlab">.git + - ${scm.repoUrl}argocd/example-apps.git + - ${scm.repoUrl}argocd/nginx-helm-umbrella.git # allow to only see application resources from the specified namespace sourceNamespaces: diff --git a/argocd/argocd/operator/argocd.ftl.yaml b/argocd/argocd/operator/argocd.ftl.yaml index 5e280eef6..6506db8dd 100644 --- a/argocd/argocd/operator/argocd.ftl.yaml +++ b/argocd/argocd/operator/argocd.ftl.yaml @@ -127,11 +127,11 @@ spec: ingress: enabled: ${((!config.application.openshift) && (!config.application.insecure))?c} initialRepositories: | -<#if !(scmm.centralScmmUrl?has_content)> +<#if !(scm.centralScmUrl?has_content)> - name: argocd - url: ${scmm.repoUrl}argocd/argocd<#if config.scmm.provider == "gitlab">.git + url: ${scm.repoUrl}argocd/argocd.git - name: cluster-resources - url: ${scmm.repoUrl}argocd/cluster-resources<#if config.scmm.provider == "gitlab">.git + url: ${scm.repoUrl}argocd/cluster-resources.git - name: prometheus-community type: helm url: https://prometheus-community.github.io/helm-charts @@ -143,11 +143,11 @@ spec: url: https://kubernetes.github.io/ingress-nginx - name: example-apps - url: ${scmm.repoUrl}argocd/example-apps<#if config.scmm.provider == "gitlab">.git + url: ${scm.repoUrl}argocd/example-apps.git - name: nginx-helm-jenkins - url: ${scmm.repoUrl}argocd/nginx-helm-jenkins<#if config.scmm.provider == "gitlab">.git + url: ${scm.repoUrl}argocd/nginx-helm-jenkins.git - name: nginx-helm-umbrella - url: ${scmm.repoUrl}argocd/nginx-helm-umbrella<#if config.scmm.provider == "gitlab">.git + url: ${scm.repoUrl}argocd/nginx-helm-umbrella.git - name: bitnami type: helm url: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami diff --git a/argocd/argocd/projects/cluster-resources.ftl.yaml b/argocd/argocd/projects/cluster-resources.ftl.yaml index e237b12ab..de6579fd4 100644 --- a/argocd/argocd/projects/cluster-resources.ftl.yaml +++ b/argocd/argocd/projects/cluster-resources.ftl.yaml @@ -14,14 +14,14 @@ spec: - namespace: '*' server: https://kubernetes.default.svc sourceRepos: - - ${scmm.repoUrl}argocd/cluster-resources<#if config.scmm.provider == "gitlab">.git + - ${scm.repoUrl}argocd/cluster-resources.git <#if config.application.mirrorRepos> - - ${scmm.baseUrl}<#if config.scmm.provider == "gitlab">/3rd-party-dependencies/kube-prometheus-stack.git<#else>/repo/3rd-party-dependencies/kube-prometheus-stack - - ${scmm.baseUrl}<#if config.scmm.provider == "gitlab">/3rd-party-dependencies/mailhog.git<#else>/repo/3rd-party-dependencies/mailhog - - ${scmm.baseUrl}<#if config.scmm.provider == "gitlab">/3rd-party-dependencies/ingress-nginx.git<#else>/repo/3rd-party-dependencies/ingress-nginx - - ${scmm.baseUrl}<#if config.scmm.provider == "gitlab">/3rd-party-dependencies/external-secrets.git<#else>/repo/3rd-party-dependencies/external-secrets - - ${scmm.baseUrl}<#if config.scmm.provider == "gitlab">/3rd-party-dependencies/vault.git<#else>/repo/3rd-party-dependencies/vault - - ${scmm.baseUrl}<#if config.scmm.provider == "gitlab">/3rd-party-dependencies/cert-manager.git<#else>/repo/3rd-party-dependencies/cert-manager + - ${scm.baseUrl}<#if config.scm.scmProviderType == "GITLAB">/3rd-party-dependencies/kube-prometheus-stack.git<#else>/repo/3rd-party-dependencies/kube-prometheus-stack + - ${scm.baseUrl}<#if config.scm.scmProviderType == "GITLAB">/3rd-party-dependencies/mailhog.git<#else>/repo/3rd-party-dependencies/mailhog + - ${scm.baseUrl}<#if config.scm.scmProviderType == "GITLAB">/3rd-party-dependencies/ingress-nginx.git<#else>/repo/3rd-party-dependencies/ingress-nginx + - ${scm.baseUrl}<#if config.scm.scmProviderType == "GITLAB">/3rd-party-dependencies/external-secrets.git<#else>/repo/3rd-party-dependencies/external-secrets + - ${scm.baseUrl}<#if config.scm.scmProviderType == "GITLAB">/3rd-party-dependencies/vault.git<#else>/repo/3rd-party-dependencies/vault + - ${scm.baseUrl}<#if config.scm.scmProviderType == "GITLAB">/3rd-party-dependencies/cert-manager.git<#else>/repo/3rd-party-dependencies/cert-manager <#else> - https://prometheus-community.github.io/helm-charts - https://codecentric.github.io/helm-charts diff --git a/argocd/argocd/projects/example-apps.ftl.yaml b/argocd/argocd/projects/example-apps.ftl.yaml index e10024b03..23901dd58 100644 --- a/argocd/argocd/projects/example-apps.ftl.yaml +++ b/argocd/argocd/projects/example-apps.ftl.yaml @@ -15,8 +15,8 @@ spec: - namespace: ${config.application.namePrefix}example-apps-staging server: https://kubernetes.default.svc sourceRepos: - - ${scmm.repoUrl}argocd/example-apps<#if config.scmm.provider == "gitlab">.git - - ${scmm.repoUrl}argocd/nginx-helm-umbrella<#if config.scmm.provider == "gitlab">.git + - ${scm.repoUrl}argocd/example-apps.git + - ${scm.repoUrl}argocd/nginx-helm-umbrella.git # allow to only see application resources from the specified namespace diff --git a/argocd/cluster-resources/argocd/misc.ftl.yaml b/argocd/cluster-resources/argocd/misc.ftl.yaml index e46133870..4bba8b06a 100644 --- a/argocd/cluster-resources/argocd/misc.ftl.yaml +++ b/argocd/cluster-resources/argocd/misc.ftl.yaml @@ -13,9 +13,9 @@ spec: source: path: misc/ <#if config.multiTenant.useDedicatedInstance> - repoURL: ${scmm.centralScmmUrl}/repo/${config.application.namePrefix}argocd/cluster-resources + repoURL: ${scm.centralScmUrl}argocd/cluster-resources.git <#else> - repoURL: ${scmm.repoUrl}argocd/cluster-resources<#if config.scmm.provider == "gitlab">.git + repoURL: ${scm.repoUrl}argocd/cluster-resources.git targetRevision: main directory: diff --git a/argocd/example-apps/README.md b/argocd/example-apps/README.md deleted file mode 100644 index 143c98de3..000000000 --- a/argocd/example-apps/README.md +++ /dev/null @@ -1 +0,0 @@ -Contains examples of end-user applications \ No newline at end of file diff --git a/argocd/example-apps/apps/.gitkeep b/argocd/example-apps/apps/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/argocd/example-apps/argocd/exercise-nginx-helm.ftl.yaml b/argocd/example-apps/argocd/exercise-nginx-helm.ftl.yaml deleted file mode 100644 index 0e665aff3..000000000 --- a/argocd/example-apps/argocd/exercise-nginx-helm.ftl.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: exercise-nginx-helm-staging - namespace: ${config.application.namePrefix}argocd -<#else> - name: exercise-nginx-helm - namespace: ${config.application.namePrefix}example-apps-staging - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-staging - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/exercise-nginx-helm/staging - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true - ---- - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: exercise-nginx-helm-production - namespace: ${config.application.namePrefix}argocd -<#else> - name: exercise-nginx-helm - namespace: ${config.application.namePrefix}example-apps-production - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-production - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/exercise-nginx-helm/production - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true \ No newline at end of file diff --git a/argocd/example-apps/argocd/exercise-petclinic-helm.ftl.yaml b/argocd/example-apps/argocd/exercise-petclinic-helm.ftl.yaml deleted file mode 100644 index 12c5985c9..000000000 --- a/argocd/example-apps/argocd/exercise-petclinic-helm.ftl.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: exercise-petclinic-helm-staging - namespace: ${config.application.namePrefix}argocd -<#else> - name: exercise-petclinic-helm - namespace: ${config.application.namePrefix}example-apps-staging - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-staging - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/exercise-spring-petclinic-helm/staging - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true - ---- - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: exercise-petclinic-helm-production - namespace: ${config.application.namePrefix}argocd -<#else> - name: exercise-petclinic-helm - namespace: ${config.application.namePrefix}example-apps-production - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-production - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/exercise-spring-petclinic-helm/production - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true \ No newline at end of file diff --git a/argocd/example-apps/argocd/misc.ftl.yaml b/argocd/example-apps/argocd/misc.ftl.yaml deleted file mode 100644 index e7c99fb77..000000000 --- a/argocd/example-apps/argocd/misc.ftl.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# this misc-Application manages all resources in the misc folder in the gitops repository -# use the misc folder to deploy resources, which are needed for multiple other apps such as ServiceAccounts or shared ConfigMaps -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: misc-example-apps-production - namespace: ${config.application.namePrefix}argocd -<#else> - name: misc - namespace: ${config.application.namePrefix}example-apps-production - -spec: - project: example-apps - destination: - server: https://kubernetes.default.svc - # a namespace must be specified here, because teams are not allowed to deploy cluster-wide. - # You can specify a different namespace in the ressource YAMLs, if you are permitted to deploy in them - namespace: ${config.application.namePrefix}example-apps-production - source: - path: misc/ - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true -<#if config.multiTenant.centralSCMUrl?has_content> ---- -apiVersion: argoproj.io/v1alpha1 -kind: AppProject -metadata: - name: example-apps - namespace: ${config.application.namePrefix}argocd -spec: - description: Contains examples of end-user application - destinations: - - namespace: ${config.application.namePrefix}example-apps-production - server: https://kubernetes.default.svc - - namespace: ${config.application.namePrefix}example-apps-staging - server: https://kubernetes.default.svc - - namespace: ${config.application.namePrefix}argocd - server: https://kubernetes.default.svc - sourceRepos: - - http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm/repo/${config.application.namePrefix}argocd/example-apps - - http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm/repo/${config.application.namePrefix}argocd/nginx-helm-umbrella - - sourceNamespaces: - - ${config.application.namePrefix}example-apps-staging - - ${config.application.namePrefix}example-apps-production - - ${config.application.namePrefix}argocd - - namespaceResourceWhitelist: - - group: '*' - kind: '*' - - clusterResourceWhitelist: [] - \ No newline at end of file diff --git a/argocd/example-apps/argocd/nginx-helm-jenkins.ftl.yaml b/argocd/example-apps/argocd/nginx-helm-jenkins.ftl.yaml deleted file mode 100644 index 782304b9f..000000000 --- a/argocd/example-apps/argocd/nginx-helm-jenkins.ftl.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: nginx-helm-jenkins-staging - namespace: ${config.application.namePrefix}argocd -<#else> - name: nginx-helm-jenkins - namespace: ${config.application.namePrefix}example-apps-staging - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-staging - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/nginx-helm-jenkins/staging - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true - ---- -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: nginx-helm-jenkins-production - namespace: ${config.application.namePrefix}argocd -<#else> - name: nginx-helm-jenkins - namespace: ${config.application.namePrefix}example-apps-production - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-production - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/nginx-helm-jenkins/production - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true \ No newline at end of file diff --git a/argocd/example-apps/argocd/nginx-helm-umbrella.ftl.yaml b/argocd/example-apps/argocd/nginx-helm-umbrella.ftl.yaml deleted file mode 100644 index 419f8817e..000000000 --- a/argocd/example-apps/argocd/nginx-helm-umbrella.ftl.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: nginx-helm-umbrella -<#if config.features.argocd.operator> - namespace: ${config.application.namePrefix}argocd -<#else> - namespace: ${config.application.namePrefix}example-apps-production - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-production - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/nginx-helm-umbrella - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - syncPolicy: - automated: - prune: true - selfHeal: true \ No newline at end of file diff --git a/argocd/example-apps/argocd/petclinic-helm.ftl.yaml b/argocd/example-apps/argocd/petclinic-helm.ftl.yaml deleted file mode 100644 index 37178cae0..000000000 --- a/argocd/example-apps/argocd/petclinic-helm.ftl.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: petclinic-helm-staging - namespace: ${config.application.namePrefix}argocd -<#else> - name: petclinic-helm - namespace: ${config.application.namePrefix}example-apps-staging - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-staging - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/spring-petclinic-helm/staging - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true - ---- -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: petclinic-helm-production - namespace: ${config.application.namePrefix}argocd -<#else> - name: petclinic-helm - namespace: ${config.application.namePrefix}example-apps-production - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-production - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/spring-petclinic-helm/production - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true \ No newline at end of file diff --git a/argocd/example-apps/argocd/petclinic-plain.ftl.yaml b/argocd/example-apps/argocd/petclinic-plain.ftl.yaml deleted file mode 100644 index 89afcf2b8..000000000 --- a/argocd/example-apps/argocd/petclinic-plain.ftl.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: petclinic-plain-staging - namespace: ${config.application.namePrefix}argocd -<#else> - name: petclinic-plain - namespace: ${config.application.namePrefix}example-apps-staging - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-staging - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/spring-petclinic-plain/staging - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true - ---- - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: -<#if config.features.argocd.operator> - name: petclinic-plain-production - namespace: ${config.application.namePrefix}argocd -<#else> - name: petclinic-plain - namespace: ${config.application.namePrefix}example-apps-production - -spec: - destination: - namespace: ${config.application.namePrefix}example-apps-production - server: https://kubernetes.default.svc - project: example-apps - source: - path: apps/spring-petclinic-plain/production - repoURL: ${scmm.repoUrl}argocd/example-apps - targetRevision: main - directory: - recurse: true - syncPolicy: - automated: - prune: true - selfHeal: true \ No newline at end of file diff --git a/argocd/example-apps/misc/.gitkeep b/argocd/example-apps/misc/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/configuration.schema.json b/docs/configuration.schema.json index 53543e25a..27980ed67 100644 --- a/docs/configuration.schema.json +++ b/docs/configuration.schema.json @@ -65,6 +65,14 @@ } }, "additionalProperties" : false + }, + "ScmProviderType-nullable" : { + "anyOf" : [ { + "type" : "null" + }, { + "type" : "string", + "enum" : [ "GITLAB", "SCM_MANAGER" ] + } ] } }, "type" : "object", @@ -159,7 +167,7 @@ } }, "repos" : { - "description" : "Content repos to push into target environment", + "description" : "ContentLoader repos to push into target environment", "type" : [ "array", "null" ], "items" : { "type" : "object", @@ -208,7 +216,7 @@ "type" : "string", "enum" : [ "FOLDER_BASED", "COPY", "MIRROR" ] } ], - "description" : "Content 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.)" + "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" ], @@ -715,31 +723,69 @@ "properties" : { "centralArgocdNamespace" : { "type" : [ "string", "null" ], - "description" : "CENTRAL Argocd Repo Namespace" - }, - "centralSCMamespace" : { - "type" : [ "string", "null" ], - "description" : "CENTRAL Argocd Repo Namespace" + "description" : "Namespace for the centralized Argocd" }, - "centralScmUrl" : { - "type" : [ "string", "null" ], - "description" : "URL for the centralized Management Repo" + "gitlab" : { + "type" : [ "object", "null" ], + "properties" : { + "parentGroupId" : { + "type" : [ "string", "null" ], + "description" : "Main Group for Gitlab where the GOP creates it's groups/repos" + }, + "password" : { + "type" : [ "string", "null" ], + "description" : "Password for SCM Manager authentication" + }, + "url" : { + "type" : [ "string", "null" ], + "description" : "URL for external Gitlab" + }, + "username" : { + "type" : [ "string", "null" ], + "description" : "GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication" + } + }, + "additionalProperties" : false, + "description" : "Config for GITLAB" }, - "internal" : { - "type" : [ "boolean", "null" ], - "description" : "SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access" + "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" + }, + "rootPath" : { + "type" : [ "string", "null" ], + "description" : "Root path for SCM Manager" + }, + "url" : { + "type" : [ "string", "null" ], + "description" : "URL for the centralized Management Repo" + }, + "username" : { + "type" : [ "string", "null" ], + "description" : "CENTRAL SCMM username" + } + }, + "additionalProperties" : false, + "description" : "Config for GITLAB" }, - "password" : { - "type" : [ "string", "null" ], - "description" : "CENTRAL SCMM Password" + "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" - }, - "username" : { - "type" : [ "string", "null" ], - "description" : "CENTRAL SCMM USERNAME" + "description" : "Toggles the Dedicated Instances Mode. See docs for more info" } }, "additionalProperties" : false, @@ -827,44 +873,82 @@ "additionalProperties" : false, "description" : "Config params for repositories used within GOP" }, - "scmm" : { + "scm" : { "type" : [ "object", "null" ], "properties" : { - "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." - }, - "password" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when scmm-url is set" - }, - "provider" : { - "type" : [ "string", "null" ], - "description" : "Sets the scm Provider. Possible Options are \"scm-manager\" and \"gitlab\"" - }, - "rootPath" : { - "type" : [ "string", "null" ], - "description" : "Sets the root path for the Git Repositories. In SCM-Manager it is always \"repo\"" - }, - "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.'" + "gitlab" : { + "type" : [ "object", "null" ], + "properties" : { + "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" }, - "url" : { - "type" : [ "string", "null" ], - "description" : "The host of your external scm-manager" + "scmManager" : { + "type" : [ "object", "null" ], + "properties" : { + "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" + }, + "rootPath" : { + "type" : [ "string", "null" ], + "description" : "Sets the root path for the Git Repositories. In SCM-Manager it is always \"repo\"" + }, + "skipPlugins" : { + "type" : [ "boolean", "null" ], + "description" : "Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades." + }, + "skipRestart" : { + "type" : [ "boolean", "null" ], + "description" : "Skips restarting SCM-Manager after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'" + }, + "url" : { + "type" : [ "string", "null" ], + "description" : "The host of your external scm-manager" + }, + "username" : { + "type" : [ "string", "null" ], + "description" : "Mandatory when scmm-url is set" + } + }, + "additionalProperties" : false, + "description" : "Config for GITLAB" }, - "username" : { - "type" : [ "string", "null" ], - "description" : "Mandatory when scmm-url is set" + "scmProviderType" : { + "$ref" : "#/$defs/ScmProviderType-nullable", + "description" : "The SCM provider type. Possible values: SCM_MANAGER, GITLAB" } }, "additionalProperties" : false, - "description" : "Config parameters for SCMManager (Git repository Server, https://scm-manager.org/)" + "description" : "Config parameters for Scm" } }, "additionalProperties" : false diff --git a/docs/developers.md b/docs/developers.md index 7264f80c7..29cda7fa3 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -1026,5 +1026,4 @@ helm repo update ## Gitlab (Experimental) ### Disclaimer -The GitLab integration is for **experimental use only** and is **not fully implemented**. Features may be incomplete, unstable, or subject to change. Use at your own discretion. - +The GitLab integration is for **experimental use only** and is **not fully implemented**. Features may be incomplete, unstable, or subject to change. Use at your own discretion. \ No newline at end of file diff --git a/docs/gitlab-parentid.png b/docs/gitlab-parentid.png new file mode 100644 index 000000000..95f80514e Binary files /dev/null and b/docs/gitlab-parentid.png differ diff --git a/exercises/broken-application/README.md b/exercises/broken-application/README.md deleted file mode 100644 index a840ed786..000000000 --- a/exercises/broken-application/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Exercise: Broken Application - -This application does not run. Can you figure out how to resolve these errors? -Try applying `broken-application.yaml` with `kubectl`: - -```bash -kubectl apply -f broken-application.yaml -``` - -When you resolved all errors, you should be able to access the application. -Look into the service definition on how to access it. diff --git a/exercises/broken-application/broken-application.ftl.yaml b/exercises/broken-application/broken-application.ftl.yaml deleted file mode 100644 index e475c4f9b..000000000 --- a/exercises/broken-application/broken-application.ftl.yaml +++ /dev/null @@ -1,82 +0,0 @@ -<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']> -apiVersion: apps/v1 -kind: Deploymentz -metadata: - name: broken-application -spec: - replicas: 1 - selector: - matchLabels: - app: broken-application -<#if config.registry.createImagePullSecrets == true> - imagePullSecrets: - - name: proxy-registry - - template: - metadata: - labels: - app: broken-application - spec: - containers: - - name: broken-application - image: <#if config.images.nginx?has_content> - ${DockerImageParser.parse(config.images.nginx)} - <#else> - bitnamilegacy/nginx:1.25.1 - - ports: - - containerPort: 8080 -<#if config.application.podResources == true> - resources: - limits: - cpu: 100m - memory: 30Mi - requests: - cpu: 30m - memory: 15Mi - - ---- - -apiVersion: v1 -kind: Service -metadata: - name: broken-application - labels: - app: broken-application -spec: - type: <#if config.application.remote>LoadBalancer<#else>ClusterIP - ports: - - name: http - port: 80 - targetPort: 8080 - selector: - app: broken-application - -<#if config.features.exampleApps.nginx.baseDomain?has_content> ---- - -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: broken-application - labels: - app: broken-application -spec: - rules: - <#if config.application.urlSeparatorHyphen> - - host: broken-application-${config.features.exampleApps.nginx.baseDomain} - <#else> - - host: broken-application.${config.features.exampleApps.nginx.baseDomain} - - http: - paths: - - backend: - service: - name: broken-application - port: - name: http - path: / - pathType: Prefix - - diff --git a/exercises/nginx-validation/Jenkinsfile.ftl b/exercises/nginx-validation/Jenkinsfile.ftl index dcebcf1b5..f97db6dfa 100644 --- a/exercises/nginx-validation/Jenkinsfile.ftl +++ b/exercises/nginx-validation/Jenkinsfile.ftl @@ -104,4 +104,4 @@ node('docker') { def cesBuildLib def gitOpsBuildLib - + \ No newline at end of file diff --git a/exercises/nginx-validation/README.md b/exercises/nginx-validation/README.md deleted file mode 100644 index 3b8f5252c..000000000 --- a/exercises/nginx-validation/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Exercise: Resource validation using `gitops-build-lib` - -This repository contains an exercise on the validation utils provided by our `gitops-build-lib`. We've prepared some -broken yaml-resources for this nginx-pipeline. Your task is to eliminate all the bugs and let the `jenkins` deploy it. -Jenkins will provide you guidance using its logs. The validators will spot all the bugs for you, all you have to do is check the -logs and fix the bugs. - -The first step you have to take is to copy this repository under the namespace `argocd` in order for `jenkins` to pick it up. -You can use the export and import functions in SCM-Manager. You can export in "Settings" and import by clicking "Add Repository" diff --git a/exercises/nginx-validation/config.yamllint.yaml b/exercises/nginx-validation/config.yamllint.yaml deleted file mode 100644 index 182f60118..000000000 --- a/exercises/nginx-validation/config.yamllint.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -extends: default - -rules: - document-start: - present: false - indentation: - spaces: consistent - indent-sequences: consistent - check-multi-line-strings: false \ No newline at end of file diff --git a/exercises/nginx-validation/index.html b/exercises/nginx-validation/index.html deleted file mode 100644 index 16a5df40d..000000000 --- a/exercises/nginx-validation/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - nginx - validation - - -

yEAH - you fixed me and successfully overcame the validation errors!

- - \ No newline at end of file diff --git a/exercises/nginx-validation/k8s/values-production.ftl.yaml b/exercises/nginx-validation/k8s/values-production.ftl.yaml deleted file mode 100644 index 1970de912..000000000 --- a/exercises/nginx-validation/k8s/values-production.ftl.yaml +++ /dev/null @@ -1,12 +0,0 @@ -namespaceOverride: ${config.application.namePrefix}example-apps-production - -<#if config.features.exampleApps.nginx.baseDomain?has_content> -ingress: - enabled: true - pathType: Prefix - <#if config.application.urlSeparatorHyphen> - hostname: production-exercise-nginx-helm-${config.features.exampleApps.nginx.baseDomain} - <#else> - hostname: production.exercise-nginx-helm.${config.features.exampleApps.nginx.baseDomain} - - diff --git a/exercises/nginx-validation/k8s/values-shared.ftl.yaml b/exercises/nginx-validation/k8s/values-shared.ftl.yaml deleted file mode 100644 index 8888c5658..000000000 --- a/exercises/nginx-validation/k8s/values-shared.ftl.yaml +++ /dev/null @@ -1,44 +0,0 @@ -<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']> -<#if config.images.nginx?has_content> -<#assign nginxImageObject = DockerImageParser.parse(config.images.nginx)> -image: - registry: ${nginxImageObject.registry} - repository: ${nginxImageObject.repository} - tag: ${nginxImageObject.tag} -<#else> -image: - repository: bitnamilegacy/nginx - - -<#if config.registry.createImagePullSecrets == true> -global: - imagePullSecrets: - - proxy-registry - -service: - ports: - http: 80 - -staticSiteConfigmap: exercise-index-nginx - -extraEnvVars: - - name: LOG_LEVEL - value: debug -serverBlock: |- - server { - listen 0.0.0.0:8080; - location /hi { - return 200 "hello!"; - } - } - -<#if config.application.podResources == true> -resources: - limits: - cpu: 100m - memory: 30Mi - requests: - cpu: 30m - memory: 15Mi - - \ No newline at end of file diff --git a/exercises/nginx-validation/k8s/values-staging.ftl.yaml b/exercises/nginx-validation/k8s/values-staging.ftl.yaml deleted file mode 100644 index ffc331d6a..000000000 --- a/exercises/nginx-validation/k8s/values-staging.ftl.yaml +++ /dev/null @@ -1,12 +0,0 @@ -namespaceOverride: ${config.application.namePrefix}example-apps-staging - -<#if config.features.exampleApps.nginx.baseDomain?has_content> -ingress: - enabled: true - pathType: Prefix - <#if config.application.urlSeparatorHyphen> - hostname: staging-exercise-nginx-helm-${config.features.exampleApps.nginx.baseDomain} - <#else> - hostname: staging.exercise-nginx-helm.${config.features.exampleApps.nginx.baseDomain} - - diff --git a/exercises/petclinic-helm/Jenkinsfile.ftl b/exercises/petclinic-helm/Jenkinsfile.ftl deleted file mode 100644 index ea1102474..000000000 --- a/exercises/petclinic-helm/Jenkinsfile.ftl +++ /dev/null @@ -1,109 +0,0 @@ -#!groovy - -String getApplication() { "exercise-spring-petclinic-helm" } -String getScmManagerCredentials() { 'scmm-user' } -String getConfigRepositoryPRBaseUrl() { env.SCMM_URL } -String getConfigRepositoryPRRepo() { '${config.application.namePrefix}argocd/example-apps' } - -String getDockerRegistryBaseUrl() { env.${config.application.namePrefixForEnvVars}REGISTRY_URL } -String getDockerRegistryPath() { env.${config.application.namePrefixForEnvVars}REGISTRY_PATH } -String getDockerRegistryCredentials() { 'registry-user' } - -<#if config.registry.twoRegistries> -String getDockerRegistryProxyCredentials() { 'registry-proxy-user' } -String getDockerRegistryProxyBaseUrl() { env.${config.application.namePrefixForEnvVars}REGISTRY_PROXY_URL } - -<#noparse> -String getCesBuildLibRepo() { "${env.SCMM_URL}/repo/3rd-party-dependencies/ces-build-lib/" } -String getCesBuildLibVersion() { '2.5.0' } -String getHelmChartRepository() { "${env.SCMM_URL}/repo/3rd-party-dependencies/spring-boot-helm-chart-with-dependency" } -String getHelmChartVersion() { "1.0.0" } -String getMainBranch() { 'main' } - -cesBuildLib = library(identifier: "ces-build-lib@${cesBuildLibVersion}", - retriever: modernSCM([$class: 'GitSCMSource', remote: cesBuildLibRepo, credentialsId: scmManagerCredentials]) -).com.cloudogu.ces.cesbuildlib - -properties([ - // Don't run concurrent builds, because the ITs use the same port causing random failures on concurrent builds. - disableConcurrentBuilds() -]) - -node { - - mvn = cesBuildLib.MavenWrapper.new(this) - -<#if config.jenkins.mavenCentralMirror?has_content> - mvn.useMirrors([name: 'maven-central-mirror', mirrorOf: 'central', url: env.${config.application.namePrefixForEnvVars}MAVEN_CENTRAL_MIRROR]) - -<#noparse> - - - catchError { - - stage('Checkout') { - checkout scm - } - - stage('Build') { - mvn 'clean package -DskipTests -Dcheckstyle.skip' - archiveArtifacts artifacts: '**/target/*.jar' - } - - stage('Test') { - mvn "test -Dmaven.test.failure.ignore=true -Dcheckstyle.skip" - } - - String imageName = "" - stage('Docker') { - String imageTag = createImageTag() - -<#noparse> - String pathPrefix = !dockerRegistryPath?.trim() ? "" : "${dockerRegistryPath}/" - imageName = "${dockerRegistryBaseUrl}/${pathPrefix}${application}:${imageTag}" - -<#if config.registry.twoRegistries> -<#noparse> - docker.withRegistry("https://${dockerRegistryProxyBaseUrl}", dockerRegistryProxyCredentials) { - image = docker.build(imageName, '.') - } - -<#else> -<#noparse> - image = docker.build(imageName, '.') - - -<#noparse> - if (isBuildSuccessful()) { - docker.withRegistry("https://${dockerRegistryBaseUrl}", dockerRegistryCredentials) { - image.push() - } - } else { - echo 'Skipping docker push, because build not successful' - } - } - - stage('Deploy') { - echo 'Use our gitops-build-lib to deploy this application via helm!' - } - } - - // Archive Unit and integration test results, if any - junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml' -} - - -String createImageTag() { - def git = cesBuildLib.Git.new(this) - String branch = git.simpleBranchName - String branchSuffix = "" - - if (!"develop".equals(branch)) { - branchSuffix = "-${branch}" - } - - return "${new Date().format('yyyyMMddHHmm')}-${git.commitHashShort}${branchSuffix}" -} - -def cesBuildLib - diff --git a/exercises/petclinic-helm/README.md b/exercises/petclinic-helm/README.md deleted file mode 100644 index 8d0b6388d..000000000 --- a/exercises/petclinic-helm/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Exercise: Deployment from helm application - -This repository contains an exercise on how to use our `gitops-build-lib` to create k8s resources and deploy an application. -The `Jenkinsfile` contains stages on build, tests and building an image but is missing the deploy stage. -Your task is to add the deploy-stage creating, verifying and deploying resources to the cluster using our `gitops-build-lib`. -You can use our documentation on the `gitops-build-lib` to solve it - you can find hints (or the whole solution) in the `argocd/petclinic-helm` repository. - -The first step you have to take is to copy this repository under the namespace `argocd` in order for `jenkins` to pick it up. -You can use the export and import functions in SCM-Manager. You can export in "Settings" and import by clicking "Add Repository" diff --git a/exercises/petclinic-helm/k8s/values-production.ftl.yaml b/exercises/petclinic-helm/k8s/values-production.ftl.yaml deleted file mode 100644 index 5d007d2a8..000000000 --- a/exercises/petclinic-helm/k8s/values-production.ftl.yaml +++ /dev/null @@ -1,13 +0,0 @@ -service: - port: 80 - -<#if config.features.exampleApps.petclinic.baseDomain?has_content> -ingress: - hosts: - <#if config.application.urlSeparatorHyphen> - - host: production-exercise-petclinic-helm-${config.features.exampleApps.petclinic.baseDomain} - <#else> - - host: production.exercise-petclinic-helm.${config.features.exampleApps.petclinic.baseDomain} - - paths: ['/'] - diff --git a/exercises/petclinic-helm/k8s/values-shared.ftl.yaml b/exercises/petclinic-helm/k8s/values-shared.ftl.yaml deleted file mode 100644 index 3514effac..000000000 --- a/exercises/petclinic-helm/k8s/values-shared.ftl.yaml +++ /dev/null @@ -1,27 +0,0 @@ -extraEnv: | - - name: TZ - value: Europe/Berlin -service: - type: <#if config.application.remote>LoadBalancer<#else>ClusterIP - -ingress: - enabled: <#if config.features.exampleApps.petclinic.baseDomain?has_content>true<#else>false - -# this is a helm chart dependency -podinfo: - ui: - color: '#456456' - -<#if config.application.podResources == true> -resources: - limits: - cpu: '1' - memory: 1Gi - requests: - cpu: 300m - memory: 650Mi -<#else> -<#-- Explicitly set to null, because the chart sets memory by default - https://github.com/cloudogu/spring-boot-helm-chart/blob/0.3.2/values.yaml#L40 --> -resources: null - \ No newline at end of file diff --git a/exercises/petclinic-helm/k8s/values-staging.ftl.yaml b/exercises/petclinic-helm/k8s/values-staging.ftl.yaml deleted file mode 100644 index 0f4ae6d67..000000000 --- a/exercises/petclinic-helm/k8s/values-staging.ftl.yaml +++ /dev/null @@ -1,13 +0,0 @@ -service: - port: 80 - -<#if config.features.exampleApps.petclinic.baseDomain?has_content> -ingress: - hosts: - <#if config.application.urlSeparatorHyphen> - - host: staging-exercise-petclinic-helm-${config.features.exampleApps.petclinic.baseDomain} - <#else> - - host: staging.exercise-petclinic-helm.${config.features.exampleApps.petclinic.baseDomain} - - paths: ['/'] - diff --git a/pom.xml b/pom.xml index 109282fd4..9856fc124 100644 --- a/pom.xml +++ b/pom.xml @@ -232,6 +232,12 @@ 2.3.32 + + io.kubernetes + client-java + 24.0.0 + + io.micronaut micronaut-http-client @@ -264,13 +270,6 @@ test - - io.kubernetes - client-java - 22.0.0 - test - - org.assertj assertj-core @@ -360,7 +359,6 @@ - maven-surefire-plugin 2.22.2 @@ -551,4 +549,4 @@ - + \ No newline at end of file diff --git a/scripts/local/install-argocd-operator.sh b/scripts/local/install-argocd-operator.sh index 4dc0a0ee4..b7f2bf106 100644 --- a/scripts/local/install-argocd-operator.sh +++ b/scripts/local/install-argocd-operator.sh @@ -1,24 +1,6 @@ git clone https://github.com/argoproj-labs/argocd-operator && \ cd argocd-operator && \ -git checkout release-0.11 && \ +git checkout release-0.16 && \ +make deploy IMG=quay.io/argoprojlabs/argocd-operator:v0.15.0 -# Disable webhook by commenting out lines in config/default/kustomization.yaml -sed -i 's|^- ../webhook|# - ../webhook|' config/default/kustomization.yaml && \ -sed -i 's|^- path: manager_webhook_patch.yaml|# - path: manager_webhook_patch.yaml|' config/default/kustomization.yaml && \ - -# Change the image tag from v0.11.1 to v0.11.0 in config/manager/kustomization.yaml -sed -i 's|newTag: v0.11.1|newTag: v0.11.0|' config/manager/kustomization.yaml && \ - -# Install Prometheus CRDs -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-alertmanagerconfigs.yaml || true && \ -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-alertmanagers.yaml || true && \ -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-podmonitors.yaml || true && \ -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-probes.yaml || true && \ -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-prometheuses.yaml || true && \ -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-prometheusrules.yaml || true && \ -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-servicemonitors.yaml || true && \ -kubectl create -f https://raw.githubusercontent.com/prometheus-community/helm-charts/main/charts/kube-prometheus-stack/charts/crds/crds/crd-thanosrulers.yaml || true && \ - -# Install ArgoCD Operator CRDs and components -kubectl kustomize config/default | kubectl create -f - || true rm -Rf ../argocd-operator/ \ No newline at end of file diff --git a/scripts/scm-manager/init-scmm.sh b/scripts/scm-manager/init-scmm.sh index 7013ef930..de958a691 100755 --- a/scripts/scm-manager/init-scmm.sh +++ b/scripts/scm-manager/init-scmm.sh @@ -34,16 +34,8 @@ function initSCMM() { if [[ ${SCM_PROVIDER} == "scm-manager" ]]; then configureScmmManager fi - - if [[ $CONTENT_EXAMPLES == true ]]; then - pushHelmChartRepo "3rd-party-dependencies/spring-boot-helm-chart" - pushHelmChartRepoWithDependency "3rd-party-dependencies/spring-boot-helm-chart-with-dependency" - pushRepoMirror "${GITOPS_BUILD_LIB_REPO}" "3rd-party-dependencies/gitops-build-lib" - pushRepoMirror "${CES_BUILD_LIB_REPO}" "3rd-party-dependencies/ces-build-lib" 'develop' - fi } - function pushHelmChartRepo() { TARGET_REPO_SCMM="$1" @@ -179,63 +171,6 @@ function configureScmmManager() { addUser "${METRICS_USERNAME}" "${METRICS_PASSWORD}" "changeme@test.local" setPermissionForUser "${METRICS_USERNAME}" "metrics:read" - USE_CENTRAL_SCM=$([[ -n "${CENTRAL_SCM_URL// /}" ]] && echo true || echo false) - echo "Using central SCM: ${USE_CENTRAL_SCM}, URL: '${CENTRAL_SCM_URL}'" - - ### ArgoCD Repos - if [[ $INSTALL_ARGOCD == true ]]; then - - addRepo "${NAME_PREFIX}argocd" "argocd" "GitOps repo for administration of ArgoCD" "$USE_CENTRAL_SCM" - setPermission "${NAME_PREFIX}argocd" "argocd" "${GITOPS_USERNAME}" "WRITE" - - addRepo "${NAME_PREFIX}argocd" "cluster-resources" "GitOps repo for basic cluster-resources" "$USE_CENTRAL_SCM" - setPermission "${NAME_PREFIX}argocd" "cluster-resources" "${GITOPS_USERNAME}" "WRITE" - - setPermissionForNamespace "${NAME_PREFIX}argocd" "${GITOPS_USERNAME}" "CI-SERVER" - - if [[ $USE_CENTRAL_SCM == true ]]; then - addRepo "${NAME_PREFIX}argocd" "argocd" "Bootstrap repo for applications" - setPermission "${NAME_PREFIX}argocd" "argocd" "${GITOPS_USERNAME}" "WRITE" - fi - fi - - if [[ $CONTENT_EXAMPLES == true ]]; then - addRepo "${NAME_PREFIX}argocd" "nginx-helm-jenkins" "3rd Party app (NGINX) with helm, templated in Jenkins (gitops-build-lib)" - setPermission "${NAME_PREFIX}argocd" "nginx-helm-jenkins" "${GITOPS_USERNAME}" "WRITE" - - addRepo "${NAME_PREFIX}argocd" "petclinic-plain" "Java app with plain k8s resources" - setPermission "${NAME_PREFIX}argocd" "petclinic-plain" "${GITOPS_USERNAME}" "WRITE" - - addRepo "${NAME_PREFIX}argocd" "petclinic-helm" "Java app with custom helm chart" - setPermission "${NAME_PREFIX}argocd" "petclinic-helm" "${GITOPS_USERNAME}" "WRITE" - - addRepo "${NAME_PREFIX}argocd" "example-apps" "GitOps repo for examples of end-user applications" - setPermission "${NAME_PREFIX}argocd" "example-apps" "${GITOPS_USERNAME}" "WRITE" - - ### Repos with replicated dependencies - addRepo "3rd-party-dependencies" "spring-boot-helm-chart" - setPermission "3rd-party-dependencies" "spring-boot-helm-chart" "${GITOPS_USERNAME}" "WRITE" - - addRepo "3rd-party-dependencies" "spring-boot-helm-chart-with-dependency" - setPermission "3rd-party-dependencies" "spring-boot-helm-chart-with-dependency" "${GITOPS_USERNAME}" "WRITE" - - addRepo "3rd-party-dependencies" "gitops-build-lib" "Jenkins pipeline shared library for automating deployments via GitOps " - setPermission "3rd-party-dependencies" "gitops-build-lib" "${GITOPS_USERNAME}" "WRITE" - - addRepo "3rd-party-dependencies" "ces-build-lib" "Jenkins pipeline shared library adding features for Maven, Gradle, Docker, SonarQube, Git and others" - setPermission "3rd-party-dependencies" "ces-build-lib" "${GITOPS_USERNAME}" "WRITE" - - ### Exercise Repos - addRepo "${NAME_PREFIX}exercises" "petclinic-helm" - setPermission "${NAME_PREFIX}exercises" "petclinic-helm" "${GITOPS_USERNAME}" "WRITE" - - addRepo "${NAME_PREFIX}exercises" "nginx-validation" - setPermission "${NAME_PREFIX}exercises" "nginx-validation" "${GITOPS_USERNAME}" "WRITE" - - addRepo "${NAME_PREFIX}exercises" "broken-application" - setPermission "${NAME_PREFIX}exercises" "broken-application" "${GITOPS_USERNAME}" "WRITE" - fi - # Install necessary plugins installScmmPlugins diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy index 1d55b4695..ebda4feae 100644 --- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy +++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy @@ -14,6 +14,7 @@ import com.cloudogu.gitops.destroy.Destroyer import com.cloudogu.gitops.utils.CommandExecutor import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient +import com.cloudogu.gitops.utils.NetworkingUtils import groovy.util.logging.Slf4j import groovy.yaml.YamlSlurper import io.micronaut.context.ApplicationContext diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy index f7ced089a..03751179f 100644 --- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy +++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy @@ -12,13 +12,9 @@ import com.cloudogu.gitops.features.argocd.ArgoCD import com.cloudogu.gitops.features.deployment.ArgoCdApplicationStrategy import com.cloudogu.gitops.features.deployment.Deployer import com.cloudogu.gitops.features.deployment.HelmStrategy -import com.cloudogu.gitops.jenkins.GlobalPropertyManager -import com.cloudogu.gitops.jenkins.JenkinsApiClient -import com.cloudogu.gitops.jenkins.JobManager -import com.cloudogu.gitops.jenkins.PrometheusConfigurator -import com.cloudogu.gitops.jenkins.UserManager -import com.cloudogu.gitops.scmm.ScmmRepoProvider -import com.cloudogu.gitops.scmm.api.ScmmApiClient +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepoFactory +import com.cloudogu.gitops.jenkins.* import com.cloudogu.gitops.utils.* import groovy.util.logging.Slf4j import io.micronaut.context.ApplicationContext @@ -47,6 +43,7 @@ class GitopsPlaygroundCliMainScripted { def fileSystemUtils = new FileSystemUtils() def executor = new CommandExecutor() + def networkingUtils = new NetworkingUtils() def k8sClient = new K8sClient(executor, fileSystemUtils, new Provider() { @Override Config get() { @@ -57,52 +54,46 @@ class GitopsPlaygroundCliMainScripted { def httpClientFactory = new HttpClientFactory() - def scmmRepoProvider = new ScmmRepoProvider(config, fileSystemUtils) + def scmmRepoProvider = new GitRepoFactory(config, fileSystemUtils) - def insecureSslContextProvider = new Provider() { - @Override - HttpClientFactory.InsecureSslContext get() { - return httpClientFactory.insecureSslContext() - } - } - def httpClientScmm = httpClientFactory.okHttpClientScmm(httpClientFactory.createLoggingInterceptor(), config, insecureSslContextProvider) - def scmmApiClient = new ScmmApiClient(config, httpClientScmm) + def helmStrategy = new HelmStrategy(config, helmClient) + + def gitHandler = new GitHandler(config, helmStrategy, fileSystemUtils, k8sClient, networkingUtils) def jenkinsApiClient = new JenkinsApiClient(config, - httpClientFactory.okHttpClientJenkins(httpClientFactory.createLoggingInterceptor(), config, insecureSslContextProvider)) + httpClientFactory.okHttpClientJenkins(config)) context.registerSingleton(k8sClient) if (config.application.destroy) { context.registerSingleton(new Destroyer([ - new ArgoCDDestructionHandler(config, k8sClient, scmmRepoProvider, helmClient, fileSystemUtils), - new ScmmDestructionHandler(config, scmmApiClient), + new ArgoCDDestructionHandler(config, k8sClient, scmmRepoProvider, helmClient, fileSystemUtils, gitHandler), + new ScmmDestructionHandler(config), new JenkinsDestructionHandler(new JobManager(jenkinsApiClient), config, new GlobalPropertyManager(jenkinsApiClient)) ])) } else { - def helmStrategy = new HelmStrategy(config, helmClient) - - def deployer = new Deployer(config, new ArgoCdApplicationStrategy(config, fileSystemUtils, scmmRepoProvider), helmStrategy) + def deployer = new Deployer(config, new ArgoCdApplicationStrategy(config, fileSystemUtils, scmmRepoProvider, gitHandler), helmStrategy) - def airGappedUtils = new AirGappedUtils(config, scmmRepoProvider, scmmApiClient, fileSystemUtils, helmClient) - def networkingUtils = new NetworkingUtils() + def airGappedUtils = new AirGappedUtils(config, scmmRepoProvider, fileSystemUtils, helmClient, gitHandler) def jenkins = new Jenkins(config, executor, fileSystemUtils, new GlobalPropertyManager(jenkinsApiClient), new JobManager(jenkinsApiClient), new UserManager(jenkinsApiClient), - new PrometheusConfigurator(jenkinsApiClient), helmStrategy, k8sClient, networkingUtils) + new PrometheusConfigurator(jenkinsApiClient), helmStrategy, k8sClient, networkingUtils,gitHandler) + // make sure the order of features is in same order as the @Order values context.registerSingleton(new Application(config, [ new Registry(config, fileSystemUtils, k8sClient, helmStrategy), - new ScmManager(config, executor, fileSystemUtils, helmStrategy, k8sClient, networkingUtils), + new ScmManagerSetup(config, executor, fileSystemUtils, helmStrategy, k8sClient, networkingUtils), + gitHandler, jenkins, - new ArgoCD(config, k8sClient, helmClient, fileSystemUtils, scmmRepoProvider), - new IngressNginx(config, fileSystemUtils, deployer, k8sClient, airGappedUtils), - new CertManager(config, fileSystemUtils, deployer, k8sClient, airGappedUtils), - new Mailhog(config, fileSystemUtils, deployer, k8sClient, airGappedUtils), - new PrometheusStack(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, scmmRepoProvider), - new ExternalSecretsOperator(config, fileSystemUtils, deployer, k8sClient, airGappedUtils), - new Vault(config, fileSystemUtils, k8sClient, deployer, airGappedUtils), - new Content(config, k8sClient, scmmRepoProvider, scmmApiClient, jenkins), + new ArgoCD(config, k8sClient, helmClient, fileSystemUtils, scmmRepoProvider, gitHandler), + new IngressNginx(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler), + new CertManager(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler), + new Mailhog(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler), + new PrometheusStack(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, scmmRepoProvider, gitHandler), + new ExternalSecretsOperator(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler), + new Vault(config, fileSystemUtils, k8sClient, deployer, airGappedUtils, gitHandler), + new ContentLoader(config, k8sClient, scmmRepoProvider, jenkins, gitHandler), ])) } } diff --git a/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy b/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy index 7d0c54a4a..a5b0c773f 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy @@ -1,5 +1,6 @@ package com.cloudogu.gitops.config + import com.cloudogu.gitops.utils.FileSystemUtils import groovy.util.logging.Slf4j @@ -23,7 +24,7 @@ class ApplicationConfigurator { addAdditionalApplicationConfig(newConfig) addNamePrefix(newConfig) - addScmmConfig(newConfig) + addScmConfig(newConfig) addRegistryConfig(newConfig) @@ -56,13 +57,6 @@ class ApplicationConfigurator { if (newConfig.features.ingressNginx.active && !newConfig.application.baseUrl) { log.warn("Ingress-controller is activated without baseUrl parameter. Services will not be accessible by hostnames. To avoid this use baseUrl with ingress. ") } - if (newConfig.content.examples) { - if (!newConfig.registry.active) { - throw new RuntimeException("content.examples requires either registry.active or registry.url") - } - String prefix = newConfig.application.namePrefix - newConfig.content.namespaces += [prefix + "example-apps-staging", prefix + "example-apps-production"] - } } private void addNamePrefix(Config newConfig) { @@ -117,40 +111,38 @@ class ApplicationConfigurator { } } - private void addScmmConfig(Config newConfig) { - log.debug("Adding additional config for SCM-Manager") + private void addScmConfig(Config newConfig) { + log.debug("Adding additional config for SCM") - newConfig.scmm.gitOpsUsername = "${newConfig.application.namePrefix}gitops" - - if (newConfig.scmm.url) { + if (newConfig.scm.scmManager.url) { log.debug("Setting external scmm config") - newConfig.scmm.internal = false - newConfig.scmm.urlForJenkins = newConfig.scmm.url + newConfig.scm.scmManager.internal = false + newConfig.scm.scmManager.urlForJenkins = newConfig.scm.scmManager.url } else { log.debug("Setting configs for internal SCM-Manager") // We use the K8s service as default name here, because it is the only option: - // "scmm.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9091) + // "scmm.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9091) // will not work on Windows and MacOS. - newConfig.scmm.urlForJenkins = + newConfig.scm.scmManager.urlForJenkins = "http://scmm.${newConfig.application.namePrefix}scm-manager.svc.cluster.local/scm" - // More internal fields are set lazily in ScmManger.groovy (after SCMM is deployed and ports are known) + // More internal fields are set lazily in ScmManger.groovy (after SCMM is deployed and ports are known) } // We probably could get rid of some of the complexity by refactoring url, host and ingress into a single var if (newConfig.application.baseUrl) { - newConfig.scmm.ingress = new URL(injectSubdomain("scmm", + //TODO check, do we need ingerss? During ScmManager setup --> redesign by oop concept + newConfig.scm.scmManager.ingress = new URL(injectSubdomain("scmm", newConfig.application.baseUrl as String, newConfig.application.urlSeparatorHyphen as Boolean)).host } // When specific user/pw are not set, set them to global values - if (newConfig.scmm.password === Config.DEFAULT_ADMIN_PW) { - newConfig.scmm.password = newConfig.application.password + if (newConfig.scm.scmManager.password === Config.DEFAULT_ADMIN_PW) { + newConfig.scm.scmManager.password = newConfig.application.password } - if (newConfig.scmm.username === Config.DEFAULT_ADMIN_USER) { - newConfig.scmm.username = newConfig.application.username + if (newConfig.scm.scmManager.username === Config.DEFAULT_ADMIN_USER) { + newConfig.scm.scmManager.username = newConfig.application.username } - } private void addJenkinsConfig(Config newConfig) { @@ -159,13 +151,13 @@ class ApplicationConfigurator { log.debug("Setting external jenkins config") newConfig.jenkins.active = true newConfig.jenkins.internal = false - newConfig.jenkins.urlForScmm = newConfig.jenkins.url + newConfig.jenkins.urlForScm = newConfig.jenkins.url } else if (newConfig.jenkins.active) { log.debug("Setting configs for internal jenkins") // We use the K8s service as default name here, because it is the only option: // "jenkins.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9090) // will not work on Windows and MacOS. - newConfig.jenkins.urlForScmm = "http://jenkins.${newConfig.application.namePrefix}jenkins.svc.cluster.local" + newConfig.jenkins.urlForScm = "http://jenkins.${newConfig.application.namePrefix}jenkins.svc.cluster.local" // More internal fields are set lazily in Jenkins.groovy (after Jenkins is deployed and ports are known) } else { @@ -215,41 +207,26 @@ class ApplicationConfigurator { log.debug("Setting Vault URL ${vault.url}") } - if (!newConfig.features.exampleApps.petclinic.baseDomain) { - // This param only requires the host / domain - newConfig.features.exampleApps.petclinic.baseDomain = - new URL(injectSubdomain('petclinic', baseUrl, urlSeparatorHyphen)).host - log.debug("Setting Petclinic URL ${newConfig.features.exampleApps.petclinic.baseDomain}") - } - if (!newConfig.features.exampleApps.nginx.baseDomain) { - // This param only requires the host / domain - newConfig.features.exampleApps.nginx.baseDomain = - new URL(injectSubdomain('nginx', baseUrl, urlSeparatorHyphen)).host - log.debug("Setting Nginx URL ${newConfig.features.exampleApps.nginx.baseDomain}") - } } + // TODO: Anna check all condig.multitenant.* void setMultiTenantModeConfig(Config newConfig) { if (newConfig.multiTenant.useDedicatedInstance) { if (!newConfig.application.namePrefix) { throw new RuntimeException('To enable Central Multi-Tenant mode, you must define a name prefix to distinguish between instances.') } - if (!newConfig.multiTenant.username || !newConfig.multiTenant.password) { - throw new RuntimeException('To use Central Multi Tenant mode define the username and password for the central SCM instance.') - } - if (!newConfig.features.argocd.operator) { newConfig.features.argocd.operator = true } // Removes trailing slash from the input URL to avoid duplicated slashes in further URL handling - if (newConfig.multiTenant.centralScmUrl) { - String urlString = newConfig.multiTenant.centralScmUrl.toString() + if (newConfig.multiTenant.scmManager.url) { + String urlString = newConfig.multiTenant.scmManager.url.toString() if (urlString.endsWith("/")) { urlString = urlString[0..-2] } - newConfig.multiTenant.centralScmUrl= urlString + newConfig.multiTenant.scmManager.url = urlString } //Disabling IngressNginx in DedicatedInstances Mode for now. @@ -304,7 +281,7 @@ class ApplicationConfigurator { static void validateContent(Config config) { config.content.repos.each { repo -> - + if (!repo.url) { throw new RuntimeException("content.repos requires a url parameter.") } @@ -345,8 +322,8 @@ class ApplicationConfigurator { private void validateScmmAndJenkinsAreBothSet(Config configToSet) { if (configToSet.jenkins.active && - (configToSet.scmm.url && !configToSet.jenkins.url || - !configToSet.scmm.url && configToSet.jenkins.url)) { + (configToSet.scm.scmManager.url && !configToSet.jenkins.url || + !configToSet.scm.scmManager.url && configToSet.jenkins.url)) { throw new RuntimeException('When setting jenkins URL, scmm URL must also be set and the other way round') } } diff --git a/src/main/groovy/com/cloudogu/gitops/config/Config.groovy b/src/main/groovy/com/cloudogu/gitops/config/Config.groovy index 2c39d8f00..59f44cd51 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/Config.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/Config.groovy @@ -1,6 +1,6 @@ package com.cloudogu.gitops.config -import com.cloudogu.gitops.utils.NetworkingUtils +import com.cloudogu.gitops.features.git.config.ScmTenantSchema import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonPropertyDescription import com.fasterxml.jackson.core.JsonGenerator @@ -65,11 +65,11 @@ class Config { @JsonPropertyDescription(MULTITENANT_DESCRIPTION) @Mixin - MultiTentantSchema multiTenant = new MultiTentantSchema() + MultiTenantSchema multiTenant = new MultiTenantSchema() - @JsonPropertyDescription(SCMM_DESCRIPTION) + @JsonPropertyDescription(SCM_DESCRIPTION) @Mixin - ScmmSchema scmm = new ScmmSchema() + ScmTenantSchema scm = new ScmTenantSchema() @JsonPropertyDescription(APPLICATION_DESCRIPTION) @Mixin @@ -229,7 +229,7 @@ class Config { This is the URL configured in SCMM inside the Jenkins Plugin, e.g. at http://scmm.localhost/scm/admin/settings/jenkins See addJenkinsConfig() and the comment at scmm.urlForJenkins */ - String urlForScmm = '' + String urlForScm = '' String ingress = '' // Bash image used with internal Jenkins only String internalBashImage = 'bash:5' @@ -289,94 +289,6 @@ class Config { version: '5.8.43') } - static class ScmmSchema { - Boolean internal = true - String gitOpsUsername = '' - /* When installing from via Docker we have to distinguish scmm.url (which is a local IP address) from - the SCMM URL used by jenkins. - - This is necessary to make the build on push feature (webhooks from SCMM to Jenkins that trigger builds) work - in k3d. - The webhook contains repository URLs that start with the "Base URL" Setting of SCMM. - Jenkins checks these repo URLs and triggers all builds that match repo URLs. - - This value is set as "Base URL" in SCMM Settings and in Jenkins Job. - - See ApplicationConfigurator.addScmmConfig() and the comment at jenkins.urlForScmm */ - String urlForJenkins = '' - @JsonIgnore String getHost() { return NetworkingUtils.getHost(url)} - @JsonIgnore String getProtocol() { return NetworkingUtils.getProtocol(url)} - String ingress = '' - - @Option(names = ['--scmm-skip-restart'], description = SCMM_SKIP_RESTART_DESCRIPTION) - @JsonPropertyDescription(SCMM_SKIP_RESTART_DESCRIPTION) - Boolean skipRestart = false - - @Option(names = ['--scmm-skip-plugins'], description = SCMM_SKIP_PLUGINS_DESCRIPTION) - @JsonPropertyDescription(SCMM_SKIP_PLUGINS_DESCRIPTION) - Boolean skipPlugins = false - - @Option(names = ['--scmm-url'], description = SCMM_URL_DESCRIPTION) - @JsonPropertyDescription(SCMM_URL_DESCRIPTION) - String url = '' - - @Option(names = ['--scmm-username'], description = SCMM_USERNAME_DESCRIPTION) - @JsonPropertyDescription(SCMM_USERNAME_DESCRIPTION) - String username = DEFAULT_ADMIN_USER - - @Option(names = ['--scmm-password'], description = SCMM_PASSWORD_DESCRIPTION) - @JsonPropertyDescription(SCMM_PASSWORD_DESCRIPTION) - String password = DEFAULT_ADMIN_PW - - @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION) - HelmConfigWithValues helm = new HelmConfigWithValues( - chart: 'scm-manager', - repoURL: 'https://packages.scm-manager.org/repository/helm-v2-releases/', - version: '3.11.0', - values: [:] - ) - - @Option(names = ['--scm-root-path'], description = SCM_ROOT_PATH_DESCRIPTION) - @JsonPropertyDescription(SCM_ROOT_PATH_DESCRIPTION) - String rootPath = 'repo' - - @Option(names = ['--scm-provider'], description = SCM_PROVIDER_DESCRIPTION) - @JsonPropertyDescription(SCM_PROVIDER_DESCRIPTION) - String provider = 'scm-manager' - - } - - static class MultiTentantSchema { - - @Option(names = ['--dedicated-internal'], description = CENTRAL_SCM_INTERNAL_DESCRIPTION) - @JsonPropertyDescription(CENTRAL_SCM_INTERNAL_DESCRIPTION) - Boolean internal = false - - @Option(names = ['--dedicated-instance'], description = CENTRAL_USEDEDICATED_DESCRIPTION) - @JsonPropertyDescription(CENTRAL_USEDEDICATED_DESCRIPTION) - Boolean useDedicatedInstance = false - - @Option(names = ['--central-scm-url'], description = CENTRAL_MGMT_REPO_DESCRIPTION) - @JsonPropertyDescription(CENTRAL_MGMT_REPO_DESCRIPTION) - String centralScmUrl = '' - - @Option(names = ['--central-scm-username'], description = CENTRAL_SCMM_USERNAME_DESCRIPTION) - @JsonPropertyDescription(CENTRAL_SCMM_USERNAME_DESCRIPTION) - String username = '' - - @Option(names = ['--central-scm-password'], description = CENTRAL_SCMM_PASSWORD_DESCRIPTION) - @JsonPropertyDescription(CENTRAL_SCMM_PASSWORD_DESCRIPTION) - String password = '' - - @Option(names = ['--central-argocd-namespace'], description = CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION) - @JsonPropertyDescription(CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION) - String centralArgocdNamespace = 'argocd' - - @Option(names = ['--central-scm-namespace'], description = CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION) - @JsonPropertyDescription(CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION) - String centralSCMamespace = 'scm-manager' - } - static class ApplicationSchema { Boolean runningInsideK8s = false String namePrefixForEnvVars = '' diff --git a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy index a9a1acdde..05e6777fc 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy @@ -27,10 +27,10 @@ interface ConfigConstants { String CONTENT_DESCRIPTION = 'Config parameters for content, i.e. end-user or tenant applications as opposed to cluster-resources' - // Content + // ContentLoader String CONTENT_EXAMPLES_DESCRIPTION = 'Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project' String CONTENT_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' - String CONTENT_REPO_DESCRIPTION = "Content repos to push into target environment" + String CONTENT_REPO_DESCRIPTION = "ContentLoader repos to push into target environment" String CONTENT_REPO_URL_DESCRIPTION = "URL of the content repo. Mandatory for each type." String CONTENT_REPO_PATH_DESCRIPTION = "Path within the content repo to process" String CONTENT_REPO_REF_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!" @@ -38,7 +38,7 @@ interface ConfigConstants { String CONTENT_REPO_USERNAME_DESCRIPTION = "Username to authenticate against content repo" String CONTENT_REPO_PASSWORD_DESCRIPTION = "Password to authenticate against content repo" String CONTENT_REPO_TEMPLATING_DESCRIPTION = "When true, template all files ending in .ftl within the repo" - String CONTENT_REPO_TYPE_DESCRIPTION = "Content 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.)" + String CONTENT_REPO_TYPE_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.)" String CONTENT_REPO_TARGET_DESCRIPTION = "Target repo for the repository in the for of namespace/name. Must contain one slash to separate namespace from name." String CONTENT_REPO_TARGET_OVERWRITE_MODE_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." String CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION = "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches." @@ -58,30 +58,16 @@ interface ConfigConstants { String JENKINS_ADDITIONAL_ENVS_DESCRIPTION = 'Set additional environments to Jenkins' // group scmm - String SCMM_DESCRIPTION = 'Config parameters for SCMManager (Git repository Server, https://scm-manager.org/)' - String SCMM_SKIP_RESTART_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.\'' - String SCMM_SKIP_PLUGINS_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.' - String SCMM_URL_DESCRIPTION = 'The host of your external scm-manager' - String SCMM_USERNAME_DESCRIPTION = 'Mandatory when scmm-url is set' - String SCMM_PASSWORD_DESCRIPTION = 'Mandatory when scmm-url is set' + String SCM_DESCRIPTION = 'Config parameters for Scm' String GIT_NAME_DESCRIPTION = 'Sets git author and committer name used for initial commits' String GIT_EMAIL_DESCRIPTION = 'Sets git author and committer email used for initial commits' - String SCM_ROOT_PATH_DESCRIPTION = 'Sets the root path for the Git Repositories. In SCM-Manager it is always "repo"' - String SCM_PROVIDER_DESCRIPTION = 'Sets the scm Provider. Possible Options are "scm-manager" and "gitlab"' //MutliTentant - String CENTRAL_USEDEDICATED_DESCRIPTION = "Toggles the Dedicated Instances Mode" - String CENTRAL_SCM_INTERNAL_DESCRIPTION = 'SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access' String MULTITENANT_DESCRIPTION = 'Multi Tenant Configs' - String CENTRAL_MGMT_REPO_DESCRIPTION = 'URL for the centralized Management Repo' - String CENTRAL_SCMM_USERNAME_DESCRIPTION = 'CENTRAL SCMM USERNAME' - String CENTRAL_SCMM_PASSWORD_DESCRIPTION = 'CENTRAL SCMM Password' - String CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION = 'CENTRAL Argocd Repo Namespace' // group remote String REMOTE_DESCRIPTION = 'Expose services as LoadBalancers' String INSECURE_DESCRIPTION = 'Sets insecure-mode in cURL which skips cert validation' - String LOCAL_HELM_CHART_FOLDER_DESCRIPTION = 'A local folder (within the GOP image mostly) where the local mirrors of all helm charts are loaded from when mirror-Repos is active. This is mostly needed for development.' // group tool configuration String APPLICATION_DESCRIPTION = 'Application configuration parameter for GOP' diff --git a/src/main/groovy/com/cloudogu/gitops/config/Credentials.groovy b/src/main/groovy/com/cloudogu/gitops/config/Credentials.groovy new file mode 100644 index 000000000..57dd3629f --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/config/Credentials.groovy @@ -0,0 +1,11 @@ +package com.cloudogu.gitops.config + +class Credentials { + String username + String password + + Credentials(String username, String password) { + this.username = username + this.password = password + } +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/config/MultiTenantSchema.groovy b/src/main/groovy/com/cloudogu/gitops/config/MultiTenantSchema.groovy new file mode 100644 index 000000000..d2a7646b3 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/config/MultiTenantSchema.groovy @@ -0,0 +1,42 @@ +package com.cloudogu.gitops.config + +import com.cloudogu.gitops.features.git.config.ScmCentralSchema.GitlabCentralConfig +import com.cloudogu.gitops.features.git.config.ScmCentralSchema.ScmManagerCentralConfig +import com.cloudogu.gitops.features.git.config.util.ScmProviderType +import com.fasterxml.jackson.annotation.JsonPropertyDescription +import picocli.CommandLine.Mixin +import picocli.CommandLine.Option + +class MultiTenantSchema { + + static final String SCM_PROVIDER_TYPE_DESCRIPTION = 'The SCM provider type. Possible values: SCM_MANAGER, GITLAB' + static final String GITLAB_CONFIG_DESCRIPTION = 'Config for GITLAB' + static final String SCMM_CONFIG_DESCRIPTION = 'Config for GITLAB' + static final String CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION = 'Namespace for the centralized Argocd' + static final String CENTRAL_USEDEDICATED_DESCRIPTION = 'Toggles the Dedicated Instances Mode. See docs for more info' + + @Option( + names = ['--central-scm-provider'], + description = SCM_PROVIDER_TYPE_DESCRIPTION, + defaultValue = "SCM_MANAGER" + ) + @JsonPropertyDescription(SCM_PROVIDER_TYPE_DESCRIPTION) + ScmProviderType scmProviderType = ScmProviderType.SCM_MANAGER + + @JsonPropertyDescription(GITLAB_CONFIG_DESCRIPTION) + @Mixin + GitlabCentralConfig gitlab + + @JsonPropertyDescription(SCMM_CONFIG_DESCRIPTION) + @Mixin + ScmManagerCentralConfig scmManager + + @Option(names = ['--central-argocd-namespace'], description = CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION) + String centralArgocdNamespace = 'argocd' + + @Option(names = ['--dedicated-instance'], description = CENTRAL_USEDEDICATED_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_USEDEDICATED_DESCRIPTION) + Boolean useDedicatedInstance = false + +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy b/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy index 036b07185..585a1fa85 100644 --- a/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy +++ b/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy @@ -1,13 +1,12 @@ package com.cloudogu.gitops.dependencyinjection import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.git.providers.scmmanager.api.AuthorizationInterceptor import com.cloudogu.gitops.okhttp.RetryInterceptor -import com.cloudogu.gitops.scmm.api.AuthorizationInterceptor import groovy.transform.TupleConstructor import io.micronaut.context.annotation.Factory -import io.micronaut.context.annotation.Prototype import jakarta.inject.Named -import jakarta.inject.Provider import jakarta.inject.Singleton import okhttp3.JavaNetCookieJar import okhttp3.OkHttpClient @@ -24,40 +23,38 @@ import java.security.cert.X509Certificate @Factory class HttpClientFactory { - @Singleton - @Named("jenkins") - OkHttpClient okHttpClientJenkins(HttpLoggingInterceptor httpLoggingInterceptor, Config config, Provider insecureSslContextProvider) { + + static OkHttpClient buildOkHttpClient(Credentials credentials, Boolean isInsecure) { def builder = new OkHttpClient.Builder() - .cookieJar(new JavaNetCookieJar(new CookieManager())) - .addInterceptor(httpLoggingInterceptor) + .addInterceptor(new AuthorizationInterceptor(credentials.username, credentials.password)) + .addInterceptor(createLoggingInterceptor()) .addInterceptor(new RetryInterceptor()) - if (config.application.insecure) { - def context = insecureSslContextProvider.get() + if (isInsecure) { + def context = insecureSslContext() builder.sslSocketFactory(context.socketFactory, context.trustManager) } return builder.build() } - + @Singleton - @Named("scmm") - OkHttpClient okHttpClientScmm(HttpLoggingInterceptor loggingInterceptor, Config config, Provider insecureSslContext) { + @Named("jenkins") + OkHttpClient okHttpClientJenkins(Config config) { def builder = new OkHttpClient.Builder() - .addInterceptor(new AuthorizationInterceptor(config.scmm.username, config.scmm.password)) - .addInterceptor(loggingInterceptor) + .cookieJar(new JavaNetCookieJar(new CookieManager())) + .addInterceptor(createLoggingInterceptor()) .addInterceptor(new RetryInterceptor()) if (config.application.insecure) { - def context = insecureSslContext.get() + def context = insecureSslContext() builder.sslSocketFactory(context.socketFactory, context.trustManager) } return builder.build() } - @Singleton - HttpLoggingInterceptor createLoggingInterceptor() { + static HttpLoggingInterceptor createLoggingInterceptor() { def logger = LoggerFactory.getLogger("com.cloudogu.gitops.HttpClient") def ret = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { @@ -73,8 +70,7 @@ class HttpClientFactory { return ret } - @Prototype - InsecureSslContext insecureSslContext() { + static InsecureSslContext insecureSslContext() { def noCheckTrustManager = new X509TrustManager() { @Override void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { @@ -100,4 +96,4 @@ class HttpClientFactory { final SSLSocketFactory socketFactory final X509TrustManager trustManager } -} +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy b/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy index 8e81468d6..5d2f3f8b1 100644 --- a/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy +++ b/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy @@ -1,8 +1,9 @@ package com.cloudogu.gitops.destroy import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.ScmmRepoProvider +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.git.GitRepoFactory import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.HelmClient import com.cloudogu.gitops.utils.K8sClient @@ -15,29 +16,31 @@ import java.nio.file.Path @Order(100) class ArgoCDDestructionHandler implements DestructionHandler { private K8sClient k8sClient - private ScmmRepoProvider repoProvider + private GitRepoFactory repoProvider private HelmClient helmClient private Config config private FileSystemUtils fileSystemUtils - + private GitHandler gitHandler ArgoCDDestructionHandler( Config config, K8sClient k8sClient, - ScmmRepoProvider repoProvider, + GitRepoFactory repoProvider, HelmClient helmClient, - FileSystemUtils fileSystemUtils + FileSystemUtils fileSystemUtils, + GitHandler gitHandler ) { this.k8sClient = k8sClient this.repoProvider = repoProvider this.helmClient = helmClient this.config = config this.fileSystemUtils = fileSystemUtils + this.gitHandler = gitHandler } @Override void destroy() { - def repo = repoProvider.getRepo("argocd/argocd") + def repo = repoProvider.getRepo("argocd/argocd", gitHandler.resourcesScm) repo.cloneRepo() for (def app in k8sClient.getCustomResource("app")) { @@ -82,10 +85,10 @@ class ArgoCDDestructionHandler implements DestructionHandler { k8sClient.delete("app", 'argocd', "argocd") k8sClient.delete('secret', 'default', 'jenkins-credentials') - k8sClient.delete('secret', 'default', 'argocd-repo-creds-scmm') + k8sClient.delete('secret', 'default', 'argocd-repo-creds-scm') } - void installArgoCDViaHelm(ScmmRepo repo) { + void installArgoCDViaHelm(GitRepo repo) { // this is a hack to be able to uninstall using helm def namePrefix = config.application.namePrefix @@ -98,4 +101,4 @@ class ArgoCDDestructionHandler implements DestructionHandler { helmClient.dependencyBuild(umbrellaChartPath) helmClient.upgrade('argocd', umbrellaChartPath, [namespace: "${namePrefix}argocd"]) } -} +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy b/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy index 6c34b5f83..8fa0aa603 100644 --- a/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy +++ b/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy @@ -1,19 +1,18 @@ package com.cloudogu.gitops.destroy import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.api.ScmmApiClient +import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApiClient import io.micronaut.core.annotation.Order import jakarta.inject.Singleton @Singleton @Order(200) class ScmmDestructionHandler implements DestructionHandler { - private ScmmApiClient scmmApiClient + private ScmManagerApiClient scmmApiClient private Config config ScmmDestructionHandler( - Config config, - ScmmApiClient scmmApiClient + Config config ) { this.config = config this.scmmApiClient = scmmApiClient @@ -53,4 +52,4 @@ class ScmmDestructionHandler implements DestructionHandler { throw new RuntimeException("Could not delete user $name (${response.code()} ${response.message()}): ${response.errorBody().string()}") } } -} +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy b/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy index 46c96da35..66b7759c0 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy @@ -4,7 +4,7 @@ import com.cloudogu.gitops.Feature import com.cloudogu.gitops.FeatureWithImage import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.scmm.ScmUrlResolver +import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient @@ -30,19 +30,22 @@ class CertManager extends Feature implements FeatureWithImage { final K8sClient k8sClient final Config config final String namespace = "${config.application.namePrefix}cert-manager" + GitHandler gitHandler CertManager( Config config, FileSystemUtils fileSystemUtils, DeploymentStrategy deployer, K8sClient k8sClient, - AirGappedUtils airGappedUtils + AirGappedUtils airGappedUtils, + GitHandler gitHandler ) { this.deployer = deployer this.config = config this.fileSystemUtils = fileSystemUtils this.k8sClient = k8sClient this.airGappedUtils = airGappedUtils + this.gitHandler = gitHandler } @Override @@ -61,7 +64,6 @@ class CertManager extends Feature implements FeatureWithImage { .getStaticModels(), ]) as Map - def valuesFromConfig = config.features.certManager.helm.values def mergedMap = MapUtils.deepMerge(valuesFromConfig, templatedMap) @@ -79,7 +81,7 @@ class CertManager extends Feature implements FeatureWithImage { 'Chart.yaml'))['version'] deployer.deployFeature( - ScmUrlResolver.scmmRepoUrl(config, repoNamespaceAndName), + gitHandler.getResourcesScm().repoUrl(repoNamespaceAndName), 'cert-manager', '.', certManagerVersion, diff --git a/src/main/groovy/com/cloudogu/gitops/features/Content.groovy b/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy similarity index 96% rename from src/main/groovy/com/cloudogu/gitops/features/Content.groovy rename to src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy index 8920b9329..b3a9a957f 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/Content.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy @@ -3,9 +3,9 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.Feature import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.config.Config.OverwriteMode -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.ScmmRepoProvider -import com.cloudogu.gitops.scmm.api.ScmmApiClient +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.git.GitRepoFactory import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient import com.cloudogu.gitops.utils.TemplatingEngine @@ -34,12 +34,11 @@ import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositoryS @Singleton @Order(999) // We want to evaluate content last, to allow for changing all other repos -class Content extends Feature { +class ContentLoader extends Feature { private Config config private K8sClient k8sClient - private ScmmRepoProvider repoProvider - private ScmmApiClient scmmApiClient + private GitRepoFactory repoProvider private Jenkins jenkins // set by lazy initialisation private TemplatingEngine templatingEngine @@ -47,14 +46,16 @@ class Content extends Feature { private List cachedRepoCoordinates = new ArrayList<>() private File mergedReposFolder - Content( - Config config, K8sClient k8sClient, ScmmRepoProvider repoProvider, ScmmApiClient scmmApiClient, Jenkins jenkins + private GitHandler gitHandler + + ContentLoader( + Config config, K8sClient k8sClient, GitRepoFactory repoProvider, Jenkins jenkins, GitHandler gitHandler ) { this.config = config this.k8sClient = k8sClient this.repoProvider = repoProvider - this.scmmApiClient = scmmApiClient this.jenkins = jenkins + this.gitHandler = gitHandler } @Override @@ -239,7 +240,6 @@ class Content extends Feature { } } - private void cloneToLocalFolder(ContentRepositorySchema repoConfig, File repoTmpDir) { def cloneCommand = gitClone() @@ -303,8 +303,9 @@ class Content extends Feature { private void pushTargetRepos(List repoCoordinates) { repoCoordinates.each { repoCoordinate -> - ScmmRepo targetRepo = repoProvider.getRepo(repoCoordinate.fullRepoName) - def isNewRepo = targetRepo.create('', scmmApiClient, false) + GitRepo targetRepo = repoProvider.getRepo(repoCoordinate.fullRepoName,this.gitHandler.tenant) + boolean isNewRepo = targetRepo.createRepositoryAndSetPermission(repoCoordinate.fullRepoName, "", false) + if (isValidForPush(isNewRepo, repoCoordinate)) { targetRepo.cloneRepo() @@ -333,7 +334,7 @@ class Content extends Feature { * Copies repoCoordinate to targetRepo, commits and pushes * Same logic for both FOLDER_BASED and COPY repo types. */ - private static void handleRepoCopyingOrFolderBased(RepoCoordinate repoCoordinate, ScmmRepo targetRepo, boolean isNewRepo) { + private static void handleRepoCopyingOrFolderBased(RepoCoordinate repoCoordinate, GitRepo targetRepo, boolean isNewRepo) { if (!isNewRepo) { clearTargetRepoIfApplicable(repoCoordinate, targetRepo) } @@ -364,7 +365,7 @@ class Content extends Feature { refSpec } - private static void clearTargetRepoIfApplicable(RepoCoordinate repoCoordinate, ScmmRepo targetRepo) { + private static void clearTargetRepoIfApplicable(RepoCoordinate repoCoordinate, GitRepo targetRepo) { if (OverwriteMode.INIT != repoCoordinate.repoConfig.overwriteMode) { if (OverwriteMode.RESET == repoCoordinate.repoConfig.overwriteMode) { log.info("OverwriteMode ${OverwriteMode.RESET} set for repo '${repoCoordinate.fullRepoName}': " + @@ -380,7 +381,7 @@ class Content extends Feature { /** * Force pushes repoCoordinate.repoConfig.ref or all refs to targetRepo */ - private static void handleRepoMirroring(RepoCoordinate repoCoordinate, ScmmRepo targetRepo) { + private static void handleRepoMirroring(RepoCoordinate repoCoordinate, GitRepo targetRepo) { try (def targetGit = Git.open(new File(targetRepo.absoluteLocalRepoTmpDir))) { def remoteUrl = targetGit.repository.config.getString('remote', 'origin', 'url') @@ -423,7 +424,7 @@ class Content extends Feature { } } - private void createJenkinsJobIfApplicable(RepoCoordinate repoCoordinate, ScmmRepo repo) { + private void createJenkinsJobIfApplicable(RepoCoordinate repoCoordinate, GitRepo repo) { if (repoCoordinate.repoConfig.createJenkinsJob && jenkins.isEnabled()) { if (existFileInSomeBranch(repo.absoluteLocalRepoTmpDir, 'Jenkinsfile')) { jenkins.createJenkinsjob(repoCoordinate.namespace, repoCoordinate.namespace) diff --git a/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy b/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy index 25a1ff91b..57fe90849 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy @@ -4,7 +4,7 @@ import com.cloudogu.gitops.Feature import com.cloudogu.gitops.FeatureWithImage import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.scmm.ScmUrlResolver +import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient @@ -31,19 +31,22 @@ class ExternalSecretsOperator extends Feature implements FeatureWithImage { private FileSystemUtils fileSystemUtils private DeploymentStrategy deployer private AirGappedUtils airGappedUtils + private GitHandler gitHandler ExternalSecretsOperator( Config config, FileSystemUtils fileSystemUtils, DeploymentStrategy deployer, K8sClient k8sClient, - AirGappedUtils airGappedUtils + AirGappedUtils airGappedUtils, + GitHandler gitHandler ) { this.deployer = deployer this.config = config this.fileSystemUtils = fileSystemUtils this.k8sClient = k8sClient this.airGappedUtils = airGappedUtils + this.gitHandler=gitHandler } @Override @@ -59,7 +62,7 @@ class ExternalSecretsOperator extends Feature implements FeatureWithImage { // Allow for using static classes inside the templates statics: new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels() ]) - + def helmConfig = config.features.secrets.externalSecrets.helm def mergedMap = MapUtils.deepMerge(helmConfig.values, templatedMap) def tempValuesPath = fileSystemUtils.writeTempFile(mergedMap) @@ -75,7 +78,7 @@ class ExternalSecretsOperator extends Feature implements FeatureWithImage { 'Chart.yaml'))['version'] deployer.deployFeature( - ScmUrlResolver.scmmRepoUrl(config, repoNamespaceAndName), + this.gitHandler.resourcesScm.repoUrl(repoNamespaceAndName), "external-secrets", '.', externalSecretsVersion, diff --git a/src/main/groovy/com/cloudogu/gitops/features/IngressNginx.groovy b/src/main/groovy/com/cloudogu/gitops/features/IngressNginx.groovy index f72aa906f..a01473971 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/IngressNginx.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/IngressNginx.groovy @@ -4,7 +4,7 @@ import com.cloudogu.gitops.Feature import com.cloudogu.gitops.FeatureWithImage import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.scmm.ScmUrlResolver +import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient @@ -31,19 +31,22 @@ class IngressNginx extends Feature implements FeatureWithImage { private FileSystemUtils fileSystemUtils private DeploymentStrategy deployer private AirGappedUtils airGappedUtils + private GitHandler gitHandler IngressNginx( Config config, FileSystemUtils fileSystemUtils, DeploymentStrategy deployer, K8sClient k8sClient, - AirGappedUtils airGappedUtils + AirGappedUtils airGappedUtils, + GitHandler gitHandler ) { this.deployer = deployer this.config = config this.fileSystemUtils = fileSystemUtils this.k8sClient = k8sClient this.airGappedUtils = airGappedUtils + this.gitHandler = gitHandler } @Override @@ -74,7 +77,7 @@ class IngressNginx extends Feature implements FeatureWithImage { 'Chart.yaml'))['version'] deployer.deployFeature( - ScmUrlResolver.scmmRepoUrl(config, repoNamespaceAndName), + gitHandler.resourcesScm.repoUrl(repoNamespaceAndName), 'ingress-nginx', '.', ingressNginxVersion, diff --git a/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy b/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy index 1fc412ea2..0bb492d4b 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy @@ -1,19 +1,16 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.Feature -import com.cloudogu.gitops.config.ApplicationConfigurator import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.DeploymentStrategy import com.cloudogu.gitops.features.deployment.HelmStrategy +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.features.git.config.util.ScmProviderType import com.cloudogu.gitops.jenkins.GlobalPropertyManager import com.cloudogu.gitops.jenkins.JobManager import com.cloudogu.gitops.jenkins.PrometheusConfigurator import com.cloudogu.gitops.jenkins.UserManager -import com.cloudogu.gitops.utils.CommandExecutor -import com.cloudogu.gitops.utils.FileSystemUtils -import com.cloudogu.gitops.utils.K8sClient -import com.cloudogu.gitops.utils.MapUtils -import com.cloudogu.gitops.utils.NetworkingUtils +import com.cloudogu.gitops.utils.* import freemarker.template.Configuration import freemarker.template.DefaultObjectWrapperBuilder import groovy.util.logging.Slf4j @@ -38,6 +35,7 @@ class Jenkins extends Feature { private DeploymentStrategy deployer private K8sClient k8sClient private NetworkingUtils networkingUtils + private GitHandler gitHandler Jenkins( Config config, @@ -49,7 +47,8 @@ class Jenkins extends Feature { PrometheusConfigurator prometheusConfigurator, HelmStrategy deployer, K8sClient k8sClient, - NetworkingUtils networkingUtils + NetworkingUtils networkingUtils, + GitHandler gitHandler ) { this.config = config this.commandExecutor = commandExecutor @@ -61,8 +60,9 @@ class Jenkins extends Feature { this.deployer = deployer this.k8sClient = k8sClient this.networkingUtils = networkingUtils + this.gitHandler = gitHandler - if(config.jenkins.internal) { + if (config.jenkins.internal) { this.namespace = "${config.application.namePrefix}jenkins" } } @@ -72,6 +72,7 @@ class Jenkins extends Feature { return config.jenkins.active } + @Override void enable() { @@ -85,7 +86,6 @@ class Jenkins extends Feature { def nodeName = k8sClient.waitForNode().replace('node/', '') k8sClient.label('node', nodeName, new Tuple2('node', 'jenkins')) - k8sClient.createSecret('generic', 'jenkins-credentials', namespace, new Tuple2('jenkins-admin-user', config.jenkins.username), new Tuple2('jenkins-admin-password', config.jenkins.password)) @@ -137,9 +137,9 @@ class Jenkins extends Feature { JENKINS_PASSWORD : config.jenkins.password, // Used indirectly in utils.sh 😬 REMOTE_CLUSTER : config.application.remote, - SCMM_URL : config.scmm.urlForJenkins, - SCMM_PASSWORD : config.scmm.password, - SCM_PROVIDER : config.scmm.provider, + SCMM_URL : this.gitHandler.tenant.url, + SCMM_PASSWORD : this.gitHandler.tenant.credentials.password, + SCM_PROVIDER : config.scm.scmProviderType, INSTALL_ARGOCD : config.features.argocd.active, NAME_PREFIX : config.application.namePrefix, INSECURE : config.application.insecure, @@ -147,7 +147,7 @@ class Jenkins extends Feature { SKIP_PLUGINS : config.jenkins.skipPlugins ]) - globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}SCMM_URL", config.scmm.urlForJenkins) + globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}SCM_URL", this.gitHandler.tenant.url) if (config.jenkins.additionalEnvs) { for (entry in (config.jenkins.additionalEnvs as Map).entrySet()) { @@ -186,40 +186,34 @@ class Jenkins extends Feature { prometheusConfigurator.enableAuthentication() } - if (config.content.examples) { - - String jobName = "example-apps" - String namespace = "argocd" - createJenkinsjob(namespace, jobName) - - } - } void createJenkinsjob(String namespace, String repoName) { - def credentialId = "scmm-user" + def credentialId = "scm-user" String prefixedNamespace = "${config.application.namePrefix}${namespace}" String jobName = "${config.application.namePrefix}${repoName}" + jobManager.createJob(jobName, - config.scmm.urlForJenkins, + this.gitHandler.tenant.url, prefixedNamespace, credentialId) - if (config.scmm.provider == 'scm-manager') { + + if (config.scm.scmProviderType == ScmProviderType.SCM_MANAGER) { jobManager.createCredential( jobName, credentialId, "${config.application.namePrefix}gitops", - "${config.scmm.password}", + "${config.scm.getScmManager().password}", 'credentials for accessing scm-manager') } - if (config.scmm.provider == 'gitlab') { + if (config.scm.scmProviderType == ScmProviderType.GITLAB) { jobManager.createCredential( jobName, credentialId, - "${config.scmm.username}", - "${config.scmm.password}", + "${config.scm.getGitlab().username}", + "${config.scm.getGitlab().password}", 'credentials for accessing gitlab') } diff --git a/src/main/groovy/com/cloudogu/gitops/features/Mailhog.groovy b/src/main/groovy/com/cloudogu/gitops/features/Mailhog.groovy index a6681e122..80ec1c0b3 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/Mailhog.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/Mailhog.groovy @@ -3,9 +3,8 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.Feature import com.cloudogu.gitops.FeatureWithImage import com.cloudogu.gitops.config.Config - import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.scmm.ScmUrlResolver +import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient @@ -29,19 +28,21 @@ class Mailhog extends Feature implements FeatureWithImage { String namespace = "${config.application.namePrefix}monitoring" Config config K8sClient k8sClient - + private String username private String password private FileSystemUtils fileSystemUtils private DeploymentStrategy deployer private AirGappedUtils airGappedUtils + private GitHandler gitHandler Mailhog( Config config, FileSystemUtils fileSystemUtils, DeploymentStrategy deployer, K8sClient k8sClient, - AirGappedUtils airGappedUtils + AirGappedUtils airGappedUtils, + GitHandler gitHandler ) { this.deployer = deployer this.config = config @@ -49,6 +50,7 @@ class Mailhog extends Feature implements FeatureWithImage { this.k8sClient = k8sClient this.fileSystemUtils = fileSystemUtils this.airGappedUtils = airGappedUtils + this.gitHandler = gitHandler } @@ -64,12 +66,12 @@ class Mailhog extends Feature implements FeatureWithImage { def templatedMap = templateToMap(HELM_VALUES_PATH, [ mail : [ // Note that passing the URL object here leads to problems in Graal Native image, see Git history - host: config.features.mail.mailhogUrl ? new URL(config.features.mail.mailhogUrl ).host : "", + host: config.features.mail.mailhogUrl ? new URL(config.features.mail.mailhogUrl).host : "", ], passwordCrypt: bcryptMailhogPassword, - config : config, + config : config, // Allow for using static classes inside the templates - statics: new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels() + statics : new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels() ]) def helmConfig = config.features.mail.helm @@ -88,7 +90,7 @@ class Mailhog extends Feature implements FeatureWithImage { 'Chart.yaml'))['version'] deployer.deployFeature( - ScmUrlResolver.scmmRepoUrl(config, repoNamespaceAndName), + "${this.gitHandler.resourcesScm.repoUrl(repoNamespaceAndName)}", 'mailhog', '.', mailhogVersion, @@ -97,10 +99,10 @@ class Mailhog extends Feature implements FeatureWithImage { tempValuesPath, DeploymentStrategy.RepoType.GIT) } else { deployer.deployFeature( - helmConfig.repoURL , + helmConfig.repoURL, 'mailhog', - helmConfig.chart , - helmConfig.version , + helmConfig.chart, + helmConfig.version, namespace, 'mailhog', tempValuesPath) diff --git a/src/main/groovy/com/cloudogu/gitops/features/PrometheusStack.groovy b/src/main/groovy/com/cloudogu/gitops/features/PrometheusStack.groovy index 2cf7e2d20..bccf68b6c 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/PrometheusStack.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/PrometheusStack.groovy @@ -4,10 +4,10 @@ import com.cloudogu.gitops.Feature import com.cloudogu.gitops.FeatureWithImage import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.ScmmRepoProvider +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.git.GitRepoFactory import com.cloudogu.gitops.utils.* -import com.cloudogu.gitops.scmm.ScmUrlResolver import freemarker.template.DefaultObjectWrapperBuilder import groovy.util.logging.Slf4j import groovy.yaml.YamlSlurper @@ -31,10 +31,11 @@ class PrometheusStack extends Feature implements FeatureWithImage { Config config K8sClient k8sClient - ScmmRepoProvider scmmRepoProvider + GitRepoFactory scmRepoProvider private FileSystemUtils fileSystemUtils private DeploymentStrategy deployer private AirGappedUtils airGappedUtils + private GitHandler gitHandler PrometheusStack( Config config, @@ -42,14 +43,16 @@ class PrometheusStack extends Feature implements FeatureWithImage { DeploymentStrategy deployer, K8sClient k8sClient, AirGappedUtils airGappedUtils, - ScmmRepoProvider scmmRepoProvider + GitRepoFactory scmRepoProvider, + GitHandler gitHandler ) { this.deployer = deployer this.config = config this.fileSystemUtils = fileSystemUtils this.k8sClient = k8sClient this.airGappedUtils = airGappedUtils - this.scmmRepoProvider = scmmRepoProvider + this.scmRepoProvider = scmRepoProvider + this.gitHandler = gitHandler } @Override @@ -68,7 +71,7 @@ class PrometheusStack extends Feature implements FeatureWithImage { Map templateModel = buildTemplateValues(config, uid) - def values = templateToMap(HELM_VALUES_PATH, templateModel) + def values = templateToMap(HELM_VALUES_PATH, templateModel) def helmConfig = config.features.monitoring.helm def mergedMap = MapUtils.deepMerge(helmConfig.values, values) @@ -99,7 +102,7 @@ class PrometheusStack extends Feature implements FeatureWithImage { } if (config.application.namespaceIsolation || config.application.netpols) { - ScmmRepo clusterResourcesRepo = scmmRepoProvider.getRepo('argocd/cluster-resources', config.multiTenant.useDedicatedInstance) + GitRepo clusterResourcesRepo = scmRepoProvider.getRepo('argocd/cluster-resources', this.gitHandler.resourcesScm) clusterResourcesRepo.cloneRepo() for (String currentNamespace : config.application.namespaces.activeNamespaces) { @@ -134,7 +137,7 @@ class PrometheusStack extends Feature implements FeatureWithImage { 'Chart.yaml'))['version'] deployer.deployFeature( - ScmUrlResolver.scmmRepoUrl(config, repoNamespaceAndName), + this.gitHandler.resourcesScm.repoUrl(repoNamespaceAndName), 'prometheusstack', '.', prometheusVersion, @@ -154,27 +157,27 @@ class PrometheusStack extends Feature implements FeatureWithImage { } } - private Map buildTemplateValues(Config config, String uid){ + private Map buildTemplateValues(Config config, String uid) { def model = [ monitoring: [grafana: [host: config.features.monitoring.grafanaUrl ? new URL(config.features.monitoring.grafanaUrl).host : ""]], namespaces: (config.application.namespaces.activeNamespaces ?: []) as LinkedHashSet, - scmm : scmConfigurationMetrics(), + scm : scmConfigurationMetrics(), jenkins : jenkinsConfigurationMetrics(), uid : uid, config : config, // Allow for using static classes inside the templates - statics : new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels() + statics : new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels() ] as Map return model } private Map scmConfigurationMetrics() { - def uri = ScmUrlResolver.scmmBaseUri(config).resolve("api/v2/metrics/prometheus") + def uri = this.gitHandler.resourcesScm.prometheusMetricsEndpoint() [ - protocol: uri.scheme ?: "", - host : uri.authority ?: "", - path : uri.path ?: "" + protocol: uri?.scheme ?: "", + host : uri?.authority ?: "", + path : uri?.path ?: "" ] } diff --git a/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy b/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy index 85ccfac1f..80af58847 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy @@ -15,7 +15,7 @@ import java.nio.file.Path @Slf4j @Singleton -@Order(50) +@Order(40) class Registry extends Feature { /** diff --git a/src/main/groovy/com/cloudogu/gitops/features/ScmManager.groovy b/src/main/groovy/com/cloudogu/gitops/features/ScmManager.groovy deleted file mode 100644 index 66e52c8a5..000000000 --- a/src/main/groovy/com/cloudogu/gitops/features/ScmManager.groovy +++ /dev/null @@ -1,303 +0,0 @@ -package com.cloudogu.gitops.features - -import com.cloudogu.gitops.Feature -import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.features.deployment.HelmStrategy -import com.cloudogu.gitops.utils.* -import groovy.util.logging.Slf4j -import io.micronaut.core.annotation.Order -import jakarta.inject.Singleton -import org.gitlab4j.api.GitLabApi -import org.gitlab4j.api.models.Group -import org.gitlab4j.api.models.Project - -import java.util.function.Supplier -import java.util.logging.Level - -@Slf4j -@Singleton -@Order(60) -class ScmManager extends Feature { - - static final String HELM_VALUES_PATH = "scm-manager/values.ftl.yaml" - - String namespace - private Config config - private CommandExecutor commandExecutor - private FileSystemUtils fileSystemUtils - private DeploymentStrategy deployer - private GitLabApi gitlabApi - private K8sClient k8sClient - private NetworkingUtils networkingUtils - String centralSCMUrl - - ScmManager( - Config config, - CommandExecutor commandExecutor, - FileSystemUtils fileSystemUtils, - // For now we deploy imperatively using helm to avoid order problems. In future we could deploy via argocd. - HelmStrategy deployer, - K8sClient k8sClient, - NetworkingUtils networkingUtils - ) { - this.config = config - this.commandExecutor = commandExecutor - this.fileSystemUtils = fileSystemUtils - this.deployer = deployer - this.gitlabApi = new GitLabApi(config.scmm.url, config.scmm.password) - this.gitlabApi.enableRequestResponseLogging(Level.ALL) - this.k8sClient = k8sClient - this.networkingUtils = networkingUtils - this.centralSCMUrl = config.multiTenant.centralScmUrl - - if(config.scmm.internal) { - this.namespace = "${config.application.namePrefix}scm-manager" - } - } - - @Override - boolean isEnabled() { - return true // For now, we either deploy an internal or configure an external instance - } - - @Override - void enable() { - if (config.multiTenant.useDedicatedInstance) { - this.centralSCMUrl = !config.multiTenant.internal ? config.multiTenant.centralScmUrl : "http://scmm.scm-manager.svc.cluster.local/scm" - } - - if (config.scmm.internal) { - String releaseName = 'scmm' - - k8sClient.createNamespace(namespace) - - def helmConfig = config.scmm.helm - - def templatedMap = templateToMap(HELM_VALUES_PATH, [ - host : config.scmm.ingress, - remote : config.application.remote, - username : config.scmm.username, - password : config.scmm.password, - helm : config.scmm.helm, - releaseName: releaseName - ]) - - def mergedMap = MapUtils.deepMerge(helmConfig.values, templatedMap) - def tempValuesPath = fileSystemUtils.writeTempFile(mergedMap) - - deployer.deployFeature( - helmConfig.repoURL, - 'scm-manager', - helmConfig.chart, - helmConfig.version, - namespace, - 'scmm', - tempValuesPath - ) - - // Update scmm.url after it is deployed (and ports are known) - // Defined here: https://github.com/scm-manager/scm-manager/blob/3.2.1/scm-packaging/helm/src/main/chart/templates/_helpers.tpl#L14-L25 - String contentPath = "/scm" - - if (config.application.runningInsideK8s) { - log.debug("Setting scmm url to k8s service, since installation is running inside k8s") - config.scmm.url = networkingUtils.createUrl("${releaseName}.${namespace}.svc.cluster.local", "80", contentPath) - } else { - log.debug("Setting internal configs for local single node cluster with internal scmm. Waiting for NodePort...") - def port = k8sClient.waitForNodePort(releaseName, namespace) - String clusterBindAddress = networkingUtils.findClusterBindAddress() - config.scmm.url = networkingUtils.createUrl(clusterBindAddress, port, contentPath) - - if (config.multiTenant.useDedicatedInstance && config.multiTenant.internal) { - log.debug("Setting internal configs for local single node cluster with internal central scmm. Waiting for NodePort...") - def portCentralScm = k8sClient.waitForNodePort(releaseName, "scm-manager") - centralSCMUrl = networkingUtils.createUrl(clusterBindAddress, portCentralScm, contentPath) - } - } - } - - // NOTE: This code is experimental and not intended for production use. - // Please use with caution and ensure proper testing before deployment. - - if (config.scmm.provider == "gitlab") { - configureGitlab() - } - - commandExecutor.execute("${fileSystemUtils.rootDir}/scripts/scm-manager/init-scmm.sh", [ - - GIT_COMMITTER_NAME : config.application.gitName, - GIT_COMMITTER_EMAIL : config.application.gitEmail, - GIT_AUTHOR_NAME : config.application.gitName, - GIT_AUTHOR_EMAIL : config.application.gitEmail, - GITOPS_USERNAME : config.scmm.gitOpsUsername, - TRACE : config.application.trace, - SCMM_URL : config.scmm.url, - SCMM_USERNAME : config.scmm.username, - SCMM_PASSWORD : config.scmm.password, - JENKINS_URL : config.jenkins.url, - INTERNAL_SCMM : config.scmm.internal, - JENKINS_URL_FOR_SCMM : config.jenkins.urlForScmm, - SCMM_URL_FOR_JENKINS : config.scmm.urlForJenkins, - // Used indirectly in utils.sh 😬 - REMOTE_CLUSTER : config.application.remote, - INSTALL_ARGOCD : config.features.argocd.active, - SPRING_BOOT_HELM_CHART_COMMIT: config.repositories.springBootHelmChart.ref, - SPRING_BOOT_HELM_CHART_REPO : config.repositories.springBootHelmChart.url, - GITOPS_BUILD_LIB_REPO : config.repositories.gitopsBuildLib.url, - CES_BUILD_LIB_REPO : config.repositories.cesBuildLib.url, - NAME_PREFIX : config.application.namePrefix, - INSECURE : config.application.insecure, - SCM_ROOT_PATH : config.scmm.rootPath, - SCM_PROVIDER : config.scmm.provider, - CONTENT_EXAMPLES : config.content.examples, - SKIP_RESTART : config.scmm.skipRestart, - SKIP_PLUGINS : config.scmm.skipPlugins, - CENTRAL_SCM_URL : centralSCMUrl, - CENTRAL_SCM_USERNAME : config.multiTenant.username, - CENTRAL_SCM_PASSWORD : config.multiTenant.password - ]) - } - - void configureGitlab() { - log.info("Gitlab init") - - createGroups() - } - - - void createGroups() { - log.info("Creating Gitlab Groups") - def mainGroupName = "${config.application.namePrefix}scm".toString() - Group mainSCMGroup = this.gitlabApi.groupApi.getGroup(mainGroupName) - if (!mainSCMGroup) { - def tempGroup = new Group() - .withName(mainGroupName) - .withPath(mainGroupName.toLowerCase()) - .withParentId(null) - - mainSCMGroup = this.gitlabApi.groupApi.addGroup(tempGroup) - } - - - String argoCDGroupName = 'argocd' - Optional argoCDGroup = getGroup("${mainGroupName}/${argoCDGroupName}") - if (argoCDGroup.isEmpty()) { - def tempGroup = new Group() - .withName(argoCDGroupName) - .withPath(argoCDGroupName.toLowerCase()) - .withParentId(mainSCMGroup.id) - - argoCDGroup = addGroup(tempGroup) - } - - argoCDGroup.ifPresent(this.&createArgoCDRepos) - - String dependencysGroupName = '3rd-party-dependencies' - Optional dependencysGroup = getGroup("${mainGroupName}/${dependencysGroupName}") - if (dependencysGroup.isEmpty()) { - def tempGroup = new Group() - .withName(dependencysGroupName) - .withPath(dependencysGroupName.toLowerCase()) - .withParentId(mainSCMGroup.id) - - addGroup(tempGroup) - } - - String exercisesGroupName = 'exercises' - Optional exercisesGroup = getGroup("${mainGroupName}/${exercisesGroupName}") - if (exercisesGroup.isEmpty()) { - def tempGroup = new Group() - .withName(exercisesGroupName) - .withPath(exercisesGroupName.toLowerCase()) - .withParentId(mainSCMGroup.id) - - exercisesGroup = addGroup(tempGroup) - } - - exercisesGroup.ifPresent(this.&createExercisesRepos) - } - - void createExercisesRepos(Group exercisesGroup) { - log.info("Creating GitlabRepos for ${exercisesGroup}") - createRepo("petclinic-helm", "petclinic-helm", exercisesGroup) - createRepo("nginx-validation", "nginx-validation", exercisesGroup) - createRepo("broken-application", "broken-application", exercisesGroup) - } - - void createArgoCDRepos(Group argoCDGroup) { - log.info("Creating GitlabRepos for ${argoCDGroup}") - createRepo("cluster-resources", "GitOps repo for basic cluster-resources", argoCDGroup) - createRepo("petclinic-helm", "Java app with custom helm chart", argoCDGroup) - createRepo("petclinic-plain", "Java app with plain k8s resources", argoCDGroup) - createRepo("nginx-helm-jenkins", "3rd Party app (NGINX) with helm, templated in Jenkins (gitops-build-lib)", argoCDGroup) - createRepo("argocd", "GitOps repo for administration of ArgoCD", argoCDGroup) - createRepo("example-apps", "GitOps repo for examples of end-user applications", argoCDGroup) - - } - - - void removeBranchProtection(Project project) { - try { - this.gitlabApi.getProtectedBranchesApi().unprotectBranch(project.getId(), project.getDefaultBranch()) - log.info("Unprotected default branch: " + project.getDefaultBranch()) - } catch (Exception ex) { - log.error("Failed Unprotecting branch for repo ${project}") - } - } - - - void createRepo(String name, String description, Group parentGroup) { - - Optional project = getProject("${parentGroup.getFullPath()}/${name}".toString()) - if (project.isEmpty()) { - Project projectSpec = new Project() - .withName(name) - .withDescription(description) - .withIssuesEnabled(true) - .withMergeRequestsEnabled(true) - .withWikiEnabled(true) - .withSnippetsEnabled(true) - .withPublic(false) - .withNamespaceId(parentGroup.getId()) - .withInitializeWithReadme(true) - - log.info("Project ${projectSpec} created!") - project = Optional.ofNullable(this.gitlabApi.projectApi.createProject(projectSpec)) - } - removeBranchProtection(project.get()) - } - - //to bundle the 3 functions down below - private Optional executeGitlabApiCall(Supplier apiCall) { - try { - return Optional.ofNullable(apiCall.get()) - } catch (Exception e) { - return Optional.empty() - } - } - - private Optional getGroup(String groupName) { - try { - return Optional.ofNullable(this.gitlabApi.groupApi.getGroup(groupName)) - } catch (Exception e) { - return Optional.empty() - } - } - - private Optional addGroup(Group group) { - try { - return Optional.ofNullable(this.gitlabApi.groupApi.addGroup(group)) - } catch (Exception e) { - return Optional.empty() - } - } - - private Optional getProject(String projectPath) { - try { - return Optional.ofNullable(this.gitlabApi.projectApi.getProject(projectPath)) - } catch (Exception e) { - return Optional.empty() - } - } -} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/ScmManagerSetup.groovy b/src/main/groovy/com/cloudogu/gitops/features/ScmManagerSetup.groovy new file mode 100644 index 000000000..e0772da95 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/ScmManagerSetup.groovy @@ -0,0 +1,143 @@ +package com.cloudogu.gitops.features + +import com.cloudogu.gitops.Feature +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.features.deployment.HelmStrategy +import com.cloudogu.gitops.features.git.config.util.ScmProviderType +import com.cloudogu.gitops.utils.* +import groovy.util.logging.Slf4j +import io.micronaut.core.annotation.Order +import jakarta.inject.Singleton + +@Slf4j +@Singleton +@Order(50) +class ScmManagerSetup extends Feature { + + static final String HELM_VALUES_PATH = "scm-manager/values.ftl.yaml" + + String namespace + private Config config + private CommandExecutor commandExecutor + private FileSystemUtils fileSystemUtils + private DeploymentStrategy deployer + private K8sClient k8sClient + private NetworkingUtils networkingUtils + String centralSCMUrl + + ScmManagerSetup( + Config config, + CommandExecutor commandExecutor, + FileSystemUtils fileSystemUtils, + // For now we deploy imperatively using helm to avoid order problems. In future we could deploy via argocd. + HelmStrategy deployer, + K8sClient k8sClient, + NetworkingUtils networkingUtils + ) { + this.config = config + this.commandExecutor = commandExecutor + this.fileSystemUtils = fileSystemUtils + this.deployer = deployer + this.k8sClient = k8sClient + this.networkingUtils = networkingUtils + + if (config.scm.internal) { + this.namespace = "${config.application.namePrefix}scm-manager" + } + } + + @Override + boolean isEnabled() { + return config.scm.scmProviderType == ScmProviderType.SCM_MANAGER + } + + @Override + void enable() { + if (config.scm.scmManager.internal) { + String releaseName = 'scmm' + + k8sClient.createNamespace(namespace) + + def helmConfig = config.scm.scmManager.helm + + def templatedMap = templateToMap(HELM_VALUES_PATH, [ + host : config.scm.scmManager.ingress, + remote : config.application.remote, + username : config.scm.scmManager.username, + password : config.scm.scmManager.password, + helm : config.scm.scmManager.helm, + releaseName: releaseName + ]) + + def mergedMap = MapUtils.deepMerge(helmConfig.values, templatedMap) + def tempValuesPath = fileSystemUtils.writeTempFile(mergedMap) + + deployer.deployFeature( + helmConfig.repoURL, + 'scm-manager', + helmConfig.chart, + helmConfig.version, + namespace, + 'scmm', + tempValuesPath + ) + + // Update scmm.url after it is deployed (and ports are known) + // Defined here: https://github.com/scm-manager/scm-manager/blob/3.2.1/scm-packaging/helm/src/main/chart/templates/_helpers.tpl#L14-L25 + String contentPath = "/scm" + + + if (config.application.runningInsideK8s) { + log.debug("Setting scmm url to k8s service, since installation is running inside k8s") + config.scm.scmManager.url = networkingUtils.createUrl("${releaseName}.${namespace}.svc.cluster.local", "80", contentPath) + } else { + log.debug("Setting internal configs for local single node cluster with internal scmm. Waiting for NodePort...") + def port = k8sClient.waitForNodePort(releaseName, namespace) + String clusterBindAddress = networkingUtils.findClusterBindAddress() + config.scm.scmManager.url = networkingUtils.createUrl(clusterBindAddress, port, contentPath) + + if (config.multiTenant.useDedicatedInstance && config.multiTenant.scmProviderType == ScmProviderType.SCM_MANAGER) { + log.debug("Setting internal configs for local single node cluster with internal central scmm. Waiting for NodePort...") + def portCentralScm = k8sClient.waitForNodePort(releaseName, config.multiTenant.scmManager.namespace) + centralSCMUrl = networkingUtils.createUrl(clusterBindAddress, portCentralScm, contentPath) + } + } + } + + //disable setup for faster testing + commandExecutor.execute("${fileSystemUtils.rootDir}/scripts/scm-manager/init-scmm.sh", [ + + GIT_COMMITTER_NAME : config.application.gitName, + GIT_COMMITTER_EMAIL : config.application.gitEmail, + GIT_AUTHOR_NAME : config.application.gitName, + GIT_AUTHOR_EMAIL : config.application.gitEmail, + GITOPS_USERNAME : config.scm.scmManager.gitOpsUsername, + TRACE : config.application.trace, + SCMM_URL : config.scm.scmManager.url, + SCMM_USERNAME : config.scm.scmManager.username, + SCMM_PASSWORD : config.scm.scmManager.password, + JENKINS_URL : config.jenkins.url, + INTERNAL_SCMM : config.scm.scmManager.internal, + JENKINS_URL_FOR_SCMM : config.jenkins.urlForScm, + SCMM_URL_FOR_JENKINS : config.scm.scmManager.urlForJenkins, + // Used indirectly in utils.sh 😬 + REMOTE_CLUSTER : config.application.remote, + INSTALL_ARGOCD : config.features.argocd.active, + SPRING_BOOT_HELM_CHART_COMMIT: config.repositories.springBootHelmChart.ref, + SPRING_BOOT_HELM_CHART_REPO : config.repositories.springBootHelmChart.url, + GITOPS_BUILD_LIB_REPO : config.repositories.gitopsBuildLib.url, + CES_BUILD_LIB_REPO : config.repositories.cesBuildLib.url, + NAME_PREFIX : config.application.namePrefix, + INSECURE : config.application.insecure, + SCM_ROOT_PATH : config.scm.scmManager.rootPath, + SCM_PROVIDER : 'scm-manager', + CONTENT_EXAMPLES : false, + SKIP_RESTART : config.scm.scmManager.skipRestart, + SKIP_PLUGINS : config.scm.scmManager.skipPlugins, + CENTRAL_SCM_URL : config.multiTenant.scmManager.url, + CENTRAL_SCM_USERNAME : config.multiTenant.scmManager.username, + CENTRAL_SCM_PASSWORD : config.multiTenant.scmManager.password + ]) + } +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy b/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy index e7ccd9254..a55b782cc 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy @@ -3,9 +3,8 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.Feature import com.cloudogu.gitops.FeatureWithImage import com.cloudogu.gitops.config.Config - import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.scmm.ScmUrlResolver +import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.utils.* import freemarker.template.DefaultObjectWrapperBuilder import groovy.util.logging.Slf4j @@ -22,26 +21,29 @@ class Vault extends Feature implements FeatureWithImage { static final String VAULT_START_SCRIPT_PATH = '/applications/cluster-resources/secrets/vault/dev-post-start.ftl.sh' static final String HELM_VALUES_PATH = 'applications/cluster-resources/secrets/vault/values.ftl.yaml' - String namespace = "${config.application.namePrefix}secrets" + String namespace = "${config.application.namePrefix}secrets" Config config K8sClient k8sClient private FileSystemUtils fileSystemUtils private DeploymentStrategy deployer private AirGappedUtils airGappedUtils + private GitHandler gitHandler Vault( Config config, FileSystemUtils fileSystemUtils, K8sClient k8sClient, DeploymentStrategy deployer, - AirGappedUtils airGappedUtils + AirGappedUtils airGappedUtils, + GitHandler gitHandler ) { this.deployer = deployer this.config = config this.fileSystemUtils = fileSystemUtils this.k8sClient = k8sClient this.airGappedUtils = airGappedUtils + this.gitHandler = gitHandler } @Override @@ -133,7 +135,7 @@ class Vault extends Feature implements FeatureWithImage { 'Chart.yaml'))['version'] deployer.deployFeature( - ScmUrlResolver.scmmRepoUrl(config, repoNamespaceAndName), + this.gitHandler.resourcesScm.repoUrl(repoNamespaceAndName), 'vault', '.', vaultVersion, diff --git a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy index 826bafb6d..243ecf2d6 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy @@ -2,20 +2,17 @@ package com.cloudogu.gitops.features.argocd import com.cloudogu.gitops.Feature import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.kubernetes.argocd.ArgoApplication +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepoFactory +import com.cloudogu.gitops.git.providers.GitProvider import com.cloudogu.gitops.kubernetes.rbac.RbacDefinition import com.cloudogu.gitops.kubernetes.rbac.Role -import com.cloudogu.gitops.scmm.ScmUrlResolver -import com.cloudogu.gitops.scmm.ScmmRepoProvider import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.HelmClient import com.cloudogu.gitops.utils.K8sClient -import com.cloudogu.gitops.utils.TemplatingEngine import groovy.util.logging.Slf4j import io.micronaut.core.annotation.Order import jakarta.inject.Singleton -import org.eclipse.jgit.api.CloneCommand -import org.eclipse.jgit.api.Git import org.springframework.security.crypto.bcrypt.BCrypt import java.nio.file.Path @@ -41,33 +38,31 @@ class ArgoCD extends Feature { protected RepoInitializationAction argocdRepoInitializationAction protected RepoInitializationAction clusterResourcesInitializationAction - protected RepoInitializationAction exampleAppsInitializationAction - protected RepoInitializationAction nginxHelmJenkinsInitializationAction - protected RepoInitializationAction nginxValidationInitializationAction - protected RepoInitializationAction brokenApplicationInitializationAction protected RepoInitializationAction tenantBootstrapInitializationAction protected File remotePetClinicRepoTmpDir - protected List petClinicInitializationActions = [] protected K8sClient k8sClient protected HelmClient helmClient protected FileSystemUtils fileSystemUtils - private ScmmRepoProvider repoProvider + private GitRepoFactory repoProvider + + GitHandler gitHandler ArgoCD( Config config, K8sClient k8sClient, HelmClient helmClient, FileSystemUtils fileSystemUtils, - ScmmRepoProvider repoProvider + GitRepoFactory repoProvider, + GitHandler gitHandler ) { this.repoProvider = repoProvider this.config = config this.k8sClient = k8sClient this.helmClient = helmClient this.fileSystemUtils = fileSystemUtils - + this.gitHandler = gitHandler this.password = this.config.application.password } @@ -78,25 +73,11 @@ class ArgoCD extends Feature { @Override void enable() { - initRepos() - - log.debug('Cloning Repositories') - if (config.content.examples) { - def petclinicInitAction = createRepoInitializationAction('applications/argocd/petclinic/plain-k8s', 'argocd/petclinic-plain') - petClinicInitializationActions += petclinicInitAction - gitRepos += petclinicInitAction - - petclinicInitAction = createRepoInitializationAction('applications/argocd/petclinic/helm', 'argocd/petclinic-helm') - petClinicInitializationActions += petclinicInitAction - gitRepos += petclinicInitAction - - petclinicInitAction = createRepoInitializationAction('exercises/petclinic-helm', 'exercises/petclinic-helm') - petClinicInitializationActions += petclinicInitAction - gitRepos += petclinicInitAction - - cloneRemotePetclinicRepo() - } + initTenantRepos() + initCentralRepos() + + log.debug('Cloning Repositories') gitRepos.forEach(repoInitializationAction -> { repoInitializationAction.initLocalRepo() @@ -104,11 +85,6 @@ class ArgoCD extends Feature { prepareGitOpsRepos() - if (config.content.examples) { - prepareApplicationNginxHelmJenkins() - preparePetClinicRepos() - } - gitRepos.forEach(repoInitializationAction -> { repoInitializationAction.repo.commitAndPush('Initial Commit') }) @@ -164,50 +140,25 @@ class ArgoCD extends Feature { new Tuple2('owner', 'helm'), new Tuple2('name', 'argocd')) } - protected initRepos() { - argocdRepoInitializationAction = createRepoInitializationAction('argocd/argocd', 'argocd/argocd', config.multiTenant.useDedicatedInstance) - - clusterResourcesInitializationAction = createRepoInitializationAction('argocd/cluster-resources', 'argocd/cluster-resources', config.multiTenant.useDedicatedInstance) - gitRepos += clusterResourcesInitializationAction + protected initTenantRepos() { + if (!config.multiTenant.useDedicatedInstance) { + argocdRepoInitializationAction = createRepoInitializationAction('argocd/argocd', 'argocd/argocd', this.gitHandler.tenant) - if (config.multiTenant.useDedicatedInstance) { - tenantBootstrapInitializationAction = createRepoInitializationAction('argocd/argocd/multiTenant/tenant', 'argocd/argocd') + clusterResourcesInitializationAction = createRepoInitializationAction('argocd/cluster-resources', 'argocd/cluster-resources', this.gitHandler.tenant) + gitRepos += clusterResourcesInitializationAction + } else { + tenantBootstrapInitializationAction = createRepoInitializationAction('argocd/argocd/multiTenant/tenant', 'argocd/argocd', this.gitHandler.tenant) gitRepos += tenantBootstrapInitializationAction } - - if (config.content.examples) { - exampleAppsInitializationAction = createRepoInitializationAction('argocd/example-apps', 'argocd/example-apps') - gitRepos += exampleAppsInitializationAction - - nginxHelmJenkinsInitializationAction = createRepoInitializationAction('applications/argocd/nginx/helm-jenkins', 'argocd/nginx-helm-jenkins') - gitRepos += nginxHelmJenkinsInitializationAction - - nginxValidationInitializationAction = createRepoInitializationAction('exercises/nginx-validation', 'exercises/nginx-validation') - gitRepos += nginxValidationInitializationAction - - brokenApplicationInitializationAction = createRepoInitializationAction('exercises/broken-application', 'exercises/broken-application') - gitRepos += brokenApplicationInitializationAction - - remotePetClinicRepoTmpDir = File.createTempDir('gitops-playground-petclinic') - } } - private void cloneRemotePetclinicRepo() { - log.debug("Cloning petclinic base repo, revision ${config.repositories.springPetclinic.ref}," + - " from ${config.repositories.springPetclinic.url}") - Git git = gitClone() - .setURI(config.repositories.springPetclinic.url) - .setDirectory(remotePetClinicRepoTmpDir) - .call() - git.checkout().setName(config.repositories.springPetclinic.ref).call() - log.debug('Finished cloning petclinic base repo') - } + protected initCentralRepos() { + if (config.multiTenant.useDedicatedInstance) { + argocdRepoInitializationAction = createRepoInitializationAction('argocd/argocd', 'argocd/argocd', true) - /** - * Overwrite for testing purposes - */ - protected CloneCommand gitClone() { - Git.cloneRepository() + clusterResourcesInitializationAction = createRepoInitializationAction('argocd/cluster-resources', 'argocd/cluster-resources', true) + gitRepos += clusterResourcesInitializationAction + } } private void prepareGitOpsRepos() { @@ -225,57 +176,17 @@ class ArgoCD extends Feature { FileSystemUtils.deleteFile clusterResourcesInitializationAction.repo.getAbsoluteLocalRepoTmpDir() + MONITORING_RESOURCES_PATH + 'ingress-nginx-dashboard-requests-handling.yaml' } - if (!config.scmm.internal) { - String externalScmmUrl = ScmUrlResolver.externalHost(config) - log.debug("Configuring all yaml files in gitops repos to use the external scmm url: ${externalScmmUrl}") - replaceFileContentInYamls(new File(clusterResourcesInitializationAction.repo.getAbsoluteLocalRepoTmpDir()), scmmUrlInternal, externalScmmUrl) - + //TODO Anna do we need this? Or just pass the correct URL directly? + /*if (!config.scm.isInternal) { + String externalScmUrl = ScmmRepo.createScmmUrl(config) + log.debug("Configuring all yaml files in gitops repos to use the external scm url: ${externalScmUrl}") + replaceFileContentInYamls(new File(clusterResourcesInitializationAction.repo.getAbsoluteLocalRepoTmpDir()), scmmUrlInternal, externalScmUrl) + if (config.content.examples) { - replaceFileContentInYamls(new File(exampleAppsInitializationAction.repo.getAbsoluteLocalRepoTmpDir()), scmmUrlInternal, externalScmmUrl) + replaceFileContentInYamls(new File(exampleAppsInitializationAction.repo.getAbsoluteLocalRepoTmpDir()), scmmUrlInternal, externalScmUrl) } - } + } */ - if (config.content.examples) { - fileSystemUtils.copyDirectory("${fileSystemUtils.rootDir}/applications/argocd/nginx/helm-umbrella", - Path.of(exampleAppsInitializationAction.repo.getAbsoluteLocalRepoTmpDir(), 'apps/nginx-helm-umbrella/').toString()) - exampleAppsInitializationAction.replaceTemplates() - - //generating the bootstrap application in a app of app pattern for example apps in the /argocd applications folder - if (config.multiTenant.useDedicatedInstance) { - new ArgoApplication( - 'example-apps', - ScmUrlResolver.tenantBaseUrl(config)+'argocd/example-apps', - namespace, - namespace, - 'argocd/', - config.application.getTenantName()) - .generate(tenantBootstrapInitializationAction.repo, 'applications') - } - } - } - - private void prepareApplicationNginxHelmJenkins() { - if (!config.features.secrets.active) { - // External Secrets are not needed in example - FileSystemUtils.deleteFile nginxHelmJenkinsInitializationAction.repo.getAbsoluteLocalRepoTmpDir() + '/k8s/staging/external-secret.yaml' - FileSystemUtils.deleteFile nginxHelmJenkinsInitializationAction.repo.getAbsoluteLocalRepoTmpDir() + '/k8s/production/external-secret.yaml' - } - } - - private void preparePetClinicRepos() { - for (def repoInitAction : petClinicInitializationActions) { - def tmpDir = repoInitAction.repo.getAbsoluteLocalRepoTmpDir() - - log.debug("Copying original petclinic files for petclinic repo: $tmpDir") - fileSystemUtils.copyDirectory(remotePetClinicRepoTmpDir.toString(), tmpDir, new FileSystemUtils.IgnoreDotGitFolderFilter()) - fileSystemUtils.deleteEmptyFiles(Path.of(tmpDir), ~/k8s\/.*\.yaml/) - - new TemplatingEngine().template( - new File("${fileSystemUtils.getRootDir()}/applications/argocd/petclinic/Dockerfile.ftl"), - new File("${tmpDir}/Dockerfile"), - [baseImage: config.images.petclinic as String] - ) - } } private void deployWithHelm() { @@ -434,33 +345,32 @@ class ArgoCD extends Feature { protected void createSCMCredentialsSecret() { - log.debug('Creating repo credential secret that is used by argocd to access repos in SCM-Manager') + log.debug("Creating repo credential secret that is used by argocd to access repos in ${config.scm.scmProviderType.toString()}") // Create secret imperatively here instead of values.yaml, because we don't want it to show in git repo - def repoTemplateSecretName = 'argocd-repo-creds-scmm' + def repoTemplateSecretName = 'argocd-repo-creds-scm' - String scmmUrlForArgoCD = config.scmm.internal ? scmmUrlInternal : ScmUrlResolver.externalHost(config) k8sClient.createSecret('generic', repoTemplateSecretName, namespace, - new Tuple2('url', scmmUrlForArgoCD), - new Tuple2('username', config.scmm.username), - new Tuple2('password', config.scmm.password) + new Tuple2('url', this.gitHandler.tenant.url), + new Tuple2('username', this.gitHandler.tenant.credentials.username), + new Tuple2('password', this.gitHandler.tenant.credentials.password) ) k8sClient.label('secret', repoTemplateSecretName, namespace, - new Tuple2(' argocd.argoproj.io/secret-type', 'repo-creds')) + new Tuple2('argocd.argoproj.io/secret-type', 'repo-creds')) if (config.multiTenant.useDedicatedInstance) { - log.debug('Creating central repo credential secret that is used by argocd to access repos in SCM-Manager') + log.debug("Creating central repo credential secret that is used by argocd to access repos in ${config.scm.scmProviderType.toString()}") // Create secret imperatively here instead of values.yaml, because we don't want it to show in git repo - def centralRepoTemplateSecretName = 'argocd-repo-creds-central-scmm' + def centralRepoTemplateSecretName = 'argocd-repo-creds-central-scm' k8sClient.createSecret('generic', centralRepoTemplateSecretName, config.multiTenant.centralArgocdNamespace, - new Tuple2('url', config.multiTenant.centralScmUrl), - new Tuple2('username', config.multiTenant.username), - new Tuple2('password', config.multiTenant.password) + new Tuple2('url', this.gitHandler.central.url), + new Tuple2('username', this.gitHandler.central.credentials.username), + new Tuple2('password', this.gitHandler.central.credentials.password) ) k8sClient.label('secret', centralRepoTemplateSecretName, config.multiTenant.centralArgocdNamespace, - new Tuple2(' argocd.argoproj.io/secret-type', 'repo-creds')) + new Tuple2('argocd.argoproj.io/secret-type', 'repo-creds')) } } @@ -488,34 +398,31 @@ class ArgoCD extends Feature { FileSystemUtils.deleteDir argocdRepoInitializationAction.repo.getAbsoluteLocalRepoTmpDir() + '/multiTenant' } + /* TODO do we need this? if (!config.scmm.internal) { String externalScmmUrl = ScmUrlResolver.externalHost(config) log.debug("Configuring all yaml files in argocd repo to use the external scmm url: ${externalScmmUrl}") replaceFileContentInYamls(new File(argocdRepoInitializationAction.repo.getAbsoluteLocalRepoTmpDir()), scmmUrlInternal, externalScmmUrl) } + */ if (!config.application.netpols) { log.debug("Deleting argocd netpols.") FileSystemUtils.deleteFile argocdRepoInitializationAction.repo.getAbsoluteLocalRepoTmpDir() + '/argocd/templates/allow-namespaces.yaml' } - if (!config.content.examples) { - FileSystemUtils.deleteFile argocdRepoInitializationAction.repo.getAbsoluteLocalRepoTmpDir() + '/applications/example-apps.yaml' - FileSystemUtils.deleteFile argocdRepoInitializationAction.repo.getAbsoluteLocalRepoTmpDir() + '/projects/example-apps.yaml' - } - argocdRepoInitializationAction.repo.commitAndPush("Initial Commit") } - protected RepoInitializationAction createRepoInitializationAction(String localSrcDir, String scmmRepoTarget) { - new RepoInitializationAction(config, repoProvider.getRepo(scmmRepoTarget), localSrcDir) + protected RepoInitializationAction createRepoInitializationAction(String localSrcDir, String scmRepoTarget, Boolean isCentral) { + GitProvider provider = (Boolean.TRUE == isCentral) ? gitHandler.central : gitHandler.tenant + new RepoInitializationAction(config, repoProvider.getRepo(scmRepoTarget, provider), this.gitHandler, localSrcDir) } - protected RepoInitializationAction createRepoInitializationAction(String localSrcDir, String scmmRepoTarget, Boolean isCentralRepo) { - new RepoInitializationAction(config, repoProvider.getRepo(scmmRepoTarget, isCentralRepo), localSrcDir) + protected RepoInitializationAction createRepoInitializationAction(String localSrcDir, String scmRepoTarget, GitProvider gitProvider) { + new RepoInitializationAction(config, repoProvider.getRepo(scmRepoTarget, gitProvider), this.gitHandler, localSrcDir) } - private void replaceFileContentInYamls(File folder, String from, String to) { fileSystemUtils.getAllFilesFromDirectoryWithEnding(folder.absolutePath, ".yaml").forEach(file -> { fileSystemUtils.replaceFileContent(file.absolutePath, from, to) diff --git a/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy b/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy index 285f48d5e..f6eba87df 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy @@ -1,19 +1,21 @@ package com.cloudogu.gitops.features.argocd import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmUrlResolver -import com.cloudogu.gitops.scmm.ScmmRepo +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo import freemarker.template.DefaultObjectWrapperBuilder class RepoInitializationAction { - private ScmmRepo repo + private GitRepo repo private String copyFromDirectory private Config config + private GitHandler gitHandler - RepoInitializationAction(Config config, ScmmRepo repo, String copyFromDirectory) { + RepoInitializationAction(Config config, GitRepo repo,GitHandler gitHandler, String copyFromDirectory) { this.config = config this.repo = repo this.copyFromDirectory = copyFromDirectory + this.gitHandler = gitHandler } /** @@ -30,32 +32,27 @@ class RepoInitializationAction { repo.replaceTemplates(templateModel) } - ScmmRepo getRepo() { + GitRepo getRepo() { return repo } - private static Map buildTemplateValues(Config config){ + private Map buildTemplateValues(Config config) { def model = [ - tenantName: tenantName(config.application.namePrefix), - argocd : [host: config.features.argocd.url ? new URL(config.features.argocd.url).host : ""], - scmm : [ - baseUrl : config.scmm.internal ? "http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm" : ScmUrlResolver.externalHost(config), - host : config.scmm.internal ? "http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local" : config.scmm.host, - protocol : config.scmm.internal ? 'http' : config.scmm.protocol, - repoUrl : ScmUrlResolver.tenantBaseUrl(config), - centralScmmUrl: !config.multiTenant.internal ? config.multiTenant.centralScmUrl : "http://scmm.scm-manager.svc.cluster.local/scm" + tenantName: config.application.tenantName, + argocd : [host: config.features.argocd.url ? new URL(config.features.argocd.url).host : ""], //TODO move this to argocd class and get the url from there + scm : [ + baseUrl : this.repo.gitProvider.url, + host : this.repo.gitProvider.host, + protocol: this.repo.gitProvider.protocol, + repoUrl : this.repo.gitProvider.repoPrefix(), + centralScmUrl: this.gitHandler.central?.repoPrefix() ?: '' ], config : config, // Allow for using static classes inside the templates - statics : new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels() + statics : new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels() ] as Map return model } - private static String tenantName(String namePrefix) { - if (!namePrefix) return "" - return namePrefix.replaceAll(/-$/, "") - } - } \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy b/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy index 5782fbdbf..cb3d3b2d5 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy @@ -1,8 +1,9 @@ package com.cloudogu.gitops.features.deployment import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.ScmmRepoProvider +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.git.GitRepoFactory import com.cloudogu.gitops.utils.FileSystemUtils import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator import com.fasterxml.jackson.dataformat.yaml.YAMLMapper @@ -16,16 +17,20 @@ import java.nio.file.Path class ArgoCdApplicationStrategy implements DeploymentStrategy { private FileSystemUtils fileSystemUtils private Config config - private final ScmmRepoProvider scmmRepoProvider + private final GitRepoFactory gitRepoProvider + + private GitHandler gitHandler ArgoCdApplicationStrategy( Config config, FileSystemUtils fileSystemUtils, - ScmmRepoProvider scmmRepoProvider + GitRepoFactory gitRepoProvider, + GitHandler gitHandler ) { - this.scmmRepoProvider = scmmRepoProvider + this.gitRepoProvider = gitRepoProvider this.fileSystemUtils = fileSystemUtils this.config = config + this.gitHandler = gitHandler } @Override @@ -37,7 +42,7 @@ class ArgoCdApplicationStrategy implements DeploymentStrategy { def namePrefix = config.application.namePrefix def shallCreateNamespace = config.features['argocd']['operator'] ? "CreateNamespace=false" : "CreateNamespace=true" - ScmmRepo clusterResourcesRepo = scmmRepoProvider.getRepo('argocd/cluster-resources', config.multiTenant.useDedicatedInstance) + GitRepo clusterResourcesRepo = gitRepoProvider.getRepo('argocd/cluster-resources', this.gitHandler.resourcesScm) clusterResourcesRepo.cloneRepo() // Inline values from tmpHelmValues file into ArgoCD Application YAML @@ -72,10 +77,10 @@ class ArgoCdApplicationStrategy implements DeploymentStrategy { ], project : project, sources : [[ - repoURL : repoURL, + repoURL : repoURL, "${chooseKeyChartOrPath(repoType)}": chartOrPath, - targetRevision : version, - helm : [ + targetRevision : version, + helm : [ releaseName: releaseName, values : inlineValues ] diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/GitHandler.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/GitHandler.groovy new file mode 100644 index 000000000..0cd89931d --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/git/GitHandler.groovy @@ -0,0 +1,152 @@ +package com.cloudogu.gitops.features.git + +import com.cloudogu.gitops.Feature +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.features.deployment.HelmStrategy +import com.cloudogu.gitops.features.git.config.util.ScmProviderType +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.gitlab.Gitlab +import com.cloudogu.gitops.git.providers.scmmanager.ScmManager +import com.cloudogu.gitops.utils.FileSystemUtils +import com.cloudogu.gitops.utils.K8sClient +import com.cloudogu.gitops.utils.NetworkingUtils +import groovy.util.logging.Slf4j +import io.micronaut.core.annotation.Order +import jakarta.inject.Singleton + +@Slf4j +@Singleton +@Order(60) +class GitHandler extends Feature { + + Config config + + NetworkingUtils networkingUtils + HelmStrategy helmStrategy + FileSystemUtils fileSystemUtils + K8sClient k8sClient + + GitProvider tenant + GitProvider central + + + GitHandler(Config config, HelmStrategy helmStrategy, FileSystemUtils fileSystemUtils, K8sClient k8sClient, NetworkingUtils networkingUtils) { + this.config = config + this.helmStrategy = helmStrategy + this.fileSystemUtils = fileSystemUtils + this.k8sClient = k8sClient + this.networkingUtils = networkingUtils + } + + @Override + boolean isEnabled() { + return true + } + + void validate() { + if (config.scm.scmManager.url) { + config.scm.scmManager.internal = false + config.scm.scmManager.urlForJenkins = config.scm.scmManager.url + } else { + log.debug("Setting configs for internal SCM-Manager") + // We use the K8s service as default name here, because it is the only option: + // "scmm.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9091) + // will not work on Windows and MacOS. + config.scm.scmManager.urlForJenkins = + "http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm" + + // More internal fields are set lazily in ScmManger.groovy (after SCMM is deployed and ports are known) + } + config.scm.scmManager.gitOpsUsername="${config.application.namePrefix}gitops" + + if (config.scm.gitlab.url) { + config.scm.scmProviderType = ScmProviderType.GITLAB + config.scm.scmManager = null + if (!config.scm.gitlab.password || !config.scm.gitlab.parentGroupId) { + throw new RuntimeException('SCMProviderType is Gitlab but no PAT Token is given') + } + } + } + + //Retrieves the appropriate SCM for cluster resources depending on whether the environment is multi-tenant or not. + GitProvider getResourcesScm() { + if (central) { + return central + } else if (tenant) { + return tenant + } else { + throw new IllegalStateException("No SCM provider found.") + } + } + + @Override + void enable() { + //TenantSCM + switch (config.scm.scmProviderType) { + case ScmProviderType.GITLAB: + this.tenant = new Gitlab(this.config, this.config.scm.gitlab) + break + case ScmProviderType.SCM_MANAGER: + def prefixedNamespace = "${config.application.namePrefix}scm-manager".toString() + config.scm.scmManager.namespace = prefixedNamespace + this.tenant = new ScmManager(this.config, config.scm.scmManager, k8sClient, networkingUtils) + // this.tenant.setup() setup will be here in future + break + default: + throw new IllegalArgumentException("Unsupported SCM provider found in TenantSCM") + } + + if (config.multiTenant.useDedicatedInstance) { + switch (config.multiTenant.scmProviderType) { + case ScmProviderType.GITLAB: + this.central = new Gitlab(this.config, this.config.multiTenant.gitlab) + break + case ScmProviderType.SCM_MANAGER: + this.central = new ScmManager(this.config, config.multiTenant.scmManager, k8sClient, networkingUtils) + break + default: + throw new IllegalArgumentException("Unsupported SCM-Central provider: ${config.scm.scmProviderType}") + } + } + + //can be removed if we combine argocd and cluster-resources + final String namePrefix = (config?.application?.namePrefix ?: "").trim() + if (this.central) { + setupRepos(this.central, namePrefix) + setupRepos(this.tenant, namePrefix, false) + } else { + setupRepos(this.tenant, namePrefix, true) + } + create3thPartyDependencies(this.tenant, namePrefix) + } + + // includeClusterResources = true => also create the argocd/cluster-resources repository + static void setupRepos(GitProvider gitProvider, String namePrefix = "", boolean includeClusterResources = true) { + gitProvider.createRepository( + withOrgPrefix(namePrefix, "argocd/argocd"), + "GitOps repo for administration of ArgoCD" + ) + if (includeClusterResources) { + gitProvider.createRepository( + withOrgPrefix(namePrefix, "argocd/cluster-resources"), + "GitOps repo for basic cluster-resources" + ) + } + } + + static create3thPartyDependencies(GitProvider gitProvider, String namePrefix = "") { + gitProvider.createRepository(withOrgPrefix(namePrefix, "3rd-party-dependencies/spring-boot-helm-chart"), "spring-boot-helm-chart") + gitProvider.createRepository(withOrgPrefix(namePrefix, "3rd-party-dependencies/spring-boot-helm-chart-with-dependency"), "spring-boot-helm-chart-with-dependency") + gitProvider.createRepository(withOrgPrefix(namePrefix, "3rd-party-dependencies/gitops-build-lib"), "Jenkins pipeline shared library for automating deployments via GitOps") + gitProvider.createRepository(withOrgPrefix(namePrefix, "3rd-party-dependencies/ces-build-lib"), "Jenkins pipeline shared library adding features for Maven, Gradle, Docker, SonarQube, Git and others") + } + + /** + * Adds a prefix to the group/namespace part (before the first '/'): + * Example: "argocd/argocd" + "foo-" => "foo-argocd/argocd" + */ + static String withOrgPrefix(String prefix, String repoPath) { + if (!prefix) return repoPath + return prefix + repoPath + } +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmCentralSchema.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmCentralSchema.groovy new file mode 100644 index 000000000..660af0327 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmCentralSchema.groovy @@ -0,0 +1,93 @@ +package com.cloudogu.gitops.features.git.config + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.features.git.config.util.GitlabConfig +import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig +import com.fasterxml.jackson.annotation.JsonPropertyDescription +import picocli.CommandLine.Option + +class ScmCentralSchema { + + static class GitlabCentralConfig implements GitlabConfig { + + public static final String CENTRAL_GITLAB_URL_DESCRIPTION = "URL for external Gitlab" + public static final String CENTRAL_GITLAB_USERNAME_DESCRIPTION = "GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication" + public static final String CENTRAL_GITLAB_PASSWORD_DESCRIPTION = "Password for SCM Manager authentication" + public static final String CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION = "Main Group for Gitlab where the GOP creates it's groups/repos" + + // Only supports external Gitlab for now + Boolean internal = false + + @Option(names = ['--central-gitlab-url'], description = CENTRAL_GITLAB_URL_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_GITLAB_URL_DESCRIPTION) + String url = '' + + @Option(names = ['--central-gitlab-username'], description = CENTRAL_GITLAB_USERNAME_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_GITLAB_USERNAME_DESCRIPTION) + String username = 'oauth2.0' + + @Option(names = ['--central-gitlab-token'], description = CENTRAL_GITLAB_PASSWORD_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_GITLAB_PASSWORD_DESCRIPTION) + String password = '' + + @Option(names = ['--central-gitlab-group-id'], description = CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION) + String parentGroupId = '' + + Credentials getCredentials() { + return new Credentials(username, password) + } + } + + static class ScmManagerCentralConfig implements ScmManagerConfig { + + public static final String CENTRAL_SCMM_INTERNAL_DESCRIPTION = 'SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access' + public static final String CENTRAL_SCMM_URL_DESCRIPTION = 'URL for the centralized Management Repo' + public static final String CENTRAL_SCMM_USERNAME_DESCRIPTION = 'CENTRAL SCMM username' + public static final String CENTRAL_SCMM_PASSWORD_DESCRIPTION = 'CENTRAL SCMM password' + public static final String CENTRAL_SCMM_PATH_DESCRIPTION = 'Root path for SCM Manager' + public static final String CENTRAL_SCMM_NAMESPACE_DESCRIPTION = 'Namespace where to find the Central SCMM' + + @Option(names = ['--central-scmm-internal'], description = CENTRAL_SCMM_INTERNAL_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_SCMM_INTERNAL_DESCRIPTION) + Boolean internal = false + + @Option(names = ['--central-scmm-url'], description = CENTRAL_SCMM_URL_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_SCMM_URL_DESCRIPTION) + String url = '' + + @Option(names = ['--central-scmm-username'], description = CENTRAL_SCMM_USERNAME_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_SCMM_USERNAME_DESCRIPTION) + String username = '' + + @Option(names = ['--central-scmm-password'], description = CENTRAL_SCMM_PASSWORD_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_SCMM_PASSWORD_DESCRIPTION) + String password = '' + + @Option(names = ['--central-scmm-root-path'], description = CENTRAL_SCMM_PATH_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_SCMM_PATH_DESCRIPTION) + String rootPath = 'repo' + + @Option(names = ['--central-scmm-namespace'], description = CENTRAL_SCMM_NAMESPACE_DESCRIPTION) + @JsonPropertyDescription(CENTRAL_SCMM_NAMESPACE_DESCRIPTION) + String namespace = 'scm-manager' + + @Override + String getIngress() { + return null //Needed for setup + } + + @Override + Config.HelmConfigWithValues getHelm() { + return null //Needed for setup + } + + Credentials getCredentials() { + return new Credentials(username, password) + } + + String gitOpsUsername = '' + + } +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmTenantSchema.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmTenantSchema.groovy new file mode 100644 index 000000000..0e1f1b353 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmTenantSchema.groovy @@ -0,0 +1,155 @@ +package com.cloudogu.gitops.features.git.config + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.features.git.config.util.GitlabConfig +import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig +import com.cloudogu.gitops.features.git.config.util.ScmProviderType +import com.cloudogu.gitops.utils.NetworkingUtils +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonMerge +import com.fasterxml.jackson.annotation.JsonPropertyDescription +import picocli.CommandLine.Mixin +import picocli.CommandLine.Option + +import static com.cloudogu.gitops.config.ConfigConstants.HELM_CONFIG_DESCRIPTION + +class ScmTenantSchema { + + static final String GITLAB_CONFIG_DESCRIPTION = 'Config for GITLAB' + static final String SCMM_CONFIG_DESCRIPTION = 'Config for GITLAB' + static final String SCM_PROVIDER_TYPE_DESCRIPTION = 'The SCM provider type. Possible values: SCM_MANAGER, GITLAB' + static final String GITOPSUSERNAME_DESCRIPTION = 'Username for the Gitops User' + + @Option( + names = ['--scm-provider'], + description = SCM_PROVIDER_TYPE_DESCRIPTION, + defaultValue = "SCM_MANAGER" + ) + @JsonPropertyDescription(SCM_PROVIDER_TYPE_DESCRIPTION) + ScmProviderType scmProviderType = ScmProviderType.SCM_MANAGER + + @JsonPropertyDescription(GITLAB_CONFIG_DESCRIPTION) + @Mixin + GitlabTenantConfig gitlab + + @JsonPropertyDescription(SCMM_CONFIG_DESCRIPTION) + @Mixin + ScmManagerTenantConfig scmManager + + @JsonIgnore + Boolean internal = { -> + return (gitlab.internal || scmManager.internal) + } + + + static class GitlabTenantConfig implements GitlabConfig { + + static final String GITLAB_INTERNAL_DESCRIPTION = 'True if Gitlab is running in the same K8s cluster. For now we only support access by external URL' + static final String GITLAB_URL_DESCRIPTION = "Base URL for the Gitlab instance" + static final String GITLAB_USERNAME_DESCRIPTION = 'Defaults to: oauth2.0 when PAT token is given.' + static final String GITLAB_TOKEN_DESCRIPTION = 'PAT Token for the account. Needs read/write repo permissions. See docs for mor information' + static final String GITLAB_PARENT_GROUP_ID = 'Number for the Gitlab Group where the repos and subgroups should be created' + + @JsonPropertyDescription(GITLAB_INTERNAL_DESCRIPTION) + Boolean internal = false + + @Option(names = ['--gitlab-url'], description = GITLAB_URL_DESCRIPTION) + @JsonPropertyDescription(GITLAB_URL_DESCRIPTION) + String url = '' + + @Option(names = ['--gitlab-username'], description = GITLAB_USERNAME_DESCRIPTION) + @JsonPropertyDescription(GITLAB_USERNAME_DESCRIPTION) + String username = 'oauth2.0' + + @Option(names = ['--gitlab-token'], description = GITLAB_TOKEN_DESCRIPTION) + @JsonPropertyDescription(GITLAB_TOKEN_DESCRIPTION) + String password = '' + + @Option(names = ['--gitlab-parent-id'], description = GITLAB_PARENT_GROUP_ID) + @JsonPropertyDescription(GITLAB_PARENT_GROUP_ID) + String parentGroupId = '' + + Credentials getCredentials() { + return new Credentials(username, password) + } + + } + + static class ScmManagerTenantConfig implements ScmManagerConfig { + + static final String SCMM_SKIP_RESTART_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.\'' + static final String SCMM_SKIP_PLUGINS_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.' + static final String SCMM_URL_DESCRIPTION = 'The host of your external scm-manager' + static final String SCMM_USERNAME_DESCRIPTION = 'Mandatory when scmm-url is set' + static final String SCMM_PASSWORD_DESCRIPTION = 'Mandatory when scmm-url is set' + static final String SCMM_ROOT_PATH_DESCRIPTION = 'Sets the root path for the Git Repositories. In SCM-Manager it is always "repo"' + static final String SCMM_NAMESPACE_DESCRIPTION = 'Namespace where SCM-Manager should run' + + Boolean internal = true + + @Option(names = ['--scmm-url'], description = SCMM_URL_DESCRIPTION) + @JsonPropertyDescription(SCMM_URL_DESCRIPTION) + String url = '' + + @Option(names = ['--scmm-namespace'], description = SCMM_NAMESPACE_DESCRIPTION) + @JsonPropertyDescription(SCMM_NAMESPACE_DESCRIPTION) + String namespace = 'scm-manager' + + @Option(names = ['--scmm-username'], description = SCMM_USERNAME_DESCRIPTION) + @JsonPropertyDescription(SCMM_USERNAME_DESCRIPTION) + String username = Config.DEFAULT_ADMIN_USER + + @Option(names = ['--scmm-password'], description = SCMM_PASSWORD_DESCRIPTION) + @JsonPropertyDescription(SCMM_PASSWORD_DESCRIPTION) + String password = Config.DEFAULT_ADMIN_PW + + @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION) + @JsonMerge + Config.HelmConfigWithValues helm = new Config.HelmConfigWithValues( + chart: 'scm-manager', + repoURL: 'https://packages.scm-manager.org/repository/helm-v2-releases/', + version: '3.11.0', + values: [:] + ) + + @Option(names = ['--scmm-root-path'], description = SCMM_ROOT_PATH_DESCRIPTION) + @JsonPropertyDescription(SCMM_ROOT_PATH_DESCRIPTION) + String rootPath = 'repo' + + /* When installing from via Docker we have to distinguish scmm.url (which is a local IP address) from + the SCMM URL used by jenkins. + + This is necessary to make the build on push feature (webhooks from SCMM to Jenkins that trigger builds) work + in k3d. + The webhook contains repository URLs that start with the "Base URL" Setting of SCMM. + Jenkins checks these repo URLs and triggers all builds that match repo URLs. + + This value is set as "Base URL" in SCMM Settings and in Jenkins Job. + + See ApplicationConfigurator.addScmmConfig() and the comment at jenkins.urlForScmm */ + + String urlForJenkins = '' + + @JsonIgnore + String getHost() { return NetworkingUtils.getHost(url) } + + @JsonIgnore + String getProtocol() { return NetworkingUtils.getProtocol(url) } + String ingress = '' + + @Option(names = ['--scmm-skip-restart'], description = SCMM_SKIP_RESTART_DESCRIPTION) + @JsonPropertyDescription(SCMM_SKIP_RESTART_DESCRIPTION) + Boolean skipRestart = false + + @Option(names = ['--scmm-skip-plugins'], description = SCMM_SKIP_PLUGINS_DESCRIPTION) + @JsonPropertyDescription(SCMM_SKIP_PLUGINS_DESCRIPTION) + Boolean skipPlugins = false + + String gitOpsUsername = '' + + Credentials getCredentials() { + return new Credentials(username, password) + } + } +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/GitlabConfig.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/GitlabConfig.groovy new file mode 100644 index 000000000..ecffa3d47 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/GitlabConfig.groovy @@ -0,0 +1,12 @@ +package com.cloudogu.gitops.features.git.config.util + +import com.cloudogu.gitops.config.Credentials + +interface GitlabConfig { + String url + String parentGroupId + String defaultVisibility + String gitOpsUsername + + Credentials getCredentials() +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmManagerConfig.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmManagerConfig.groovy new file mode 100644 index 000000000..ffdd63813 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmManagerConfig.groovy @@ -0,0 +1,20 @@ +package com.cloudogu.gitops.features.git.config.util + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials + + +interface ScmManagerConfig { + Boolean getInternal() + + String getUrl() + String getUsername() + String getPassword() + String getNamespace() + String getIngress() + Config.HelmConfigWithValues getHelm() + String getRootPath() + String getGitOpsUsername() + + Credentials getCredentials() +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmProviderType.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmProviderType.groovy new file mode 100644 index 000000000..5685e2e71 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmProviderType.groovy @@ -0,0 +1,6 @@ +package com.cloudogu.gitops.features.git.config.util + +enum ScmProviderType { + GITLAB, + SCM_MANAGER +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/GitRepo.groovy b/src/main/groovy/com/cloudogu/gitops/git/GitRepo.groovy new file mode 100644 index 000000000..7543f32eb --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/GitRepo.groovy @@ -0,0 +1,213 @@ +package com.cloudogu.gitops.git + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.git.jgit.helpers.InsecureCredentialProvider +import com.cloudogu.gitops.git.providers.AccessRole +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.RepoUrlScope +import com.cloudogu.gitops.git.providers.Scope +import com.cloudogu.gitops.utils.FileSystemUtils +import com.cloudogu.gitops.utils.TemplatingEngine +import groovy.util.logging.Slf4j +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.PushCommand +import org.eclipse.jgit.transport.ChainingCredentialsProvider +import org.eclipse.jgit.transport.CredentialsProvider +import org.eclipse.jgit.transport.RefSpec +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider + +@Slf4j +class GitRepo { + + static final String NAMESPACE_3RD_PARTY_DEPENDENCIES = '3rd-party-dependencies' + + private final Config config + public GitProvider gitProvider + private final FileSystemUtils fileSystemUtils + + private final String repoTarget + private final boolean insecure + private final String gitName + private final String gitEmail + + private Git gitMemoization + private final String absoluteLocalRepoTmpDir + + GitRepo(Config config, + GitProvider gitProvider, + String repoTarget, + FileSystemUtils fileSystemUtils) { + def tmpDir = File.createTempDir() + tmpDir.deleteOnExit() + this.absoluteLocalRepoTmpDir = tmpDir.absolutePath + this.config = config + this.gitProvider = gitProvider + this.fileSystemUtils = fileSystemUtils + + this.repoTarget = repoTarget.startsWith(NAMESPACE_3RD_PARTY_DEPENDENCIES) ? repoTarget : + "${config.application.namePrefix}${repoTarget}" + + this.insecure = config.application.insecure + this.gitName = config.application.gitName + this.gitEmail = config.application.gitEmail + } + + String getRepoTarget() { + return repoTarget + } + + boolean createRepositoryAndSetPermission(String repoTarget, String description, boolean initialize = true) { + def isNewRepo = this.gitProvider.createRepository(repoTarget, description, initialize) + if (isNewRepo && gitProvider.getGitOpsUsername()) { + gitProvider.setRepositoryPermission( + repoTarget, + gitProvider.getGitOpsUsername(), + AccessRole.WRITE, + Scope.USER + ) + } + return isNewRepo + + } + + String getAbsoluteLocalRepoTmpDir() { + return absoluteLocalRepoTmpDir + } + + void cloneRepo() { + def cloneUrl = getGitRepositoryUrl() + log.debug("Cloning ${repoTarget}, Origin: ${cloneUrl}") + Git.cloneRepository() + .setURI(cloneUrl) + .setDirectory(new File(absoluteLocalRepoTmpDir)) + .setCredentialsProvider(getCredentialProvider()) + .call() + } + + void commitAndPush(String message, String tag) { + commitAndPush(message, tag, 'HEAD:refs/heads/main') + } + + + void commitAndPush(String commitMessage, String tag, String refSpec) { + log.debug("Adding files to ${repoTarget}") + def git = getGit() + git.add().addFilepattern(".").call() + + if (git.status().call().hasUncommittedChanges()) { + log.debug("Commiting ${repoTarget}") + git.commit() + .setSign(false) + .setMessage(commitMessage) + .setAuthor(gitName, gitEmail) + .setCommitter(gitName, gitEmail) + .call() + + def pushCommand = createPushCommand(refSpec) + + if (tag) { + log.debug("Setting tag '${tag}' on repo: ${repoTarget}") + // Delete existing tags first to get idempotence + git.tagDelete().setTags(tag).call() + git.tag() + .setName(tag) + .call() + pushCommand.setPushTags() + } + + log.debug("Pushing repo: ${repoTarget}, refSpec: ${refSpec}") + pushCommand.call() + } else { + log.debug("No changes after add, nothing to commit or push on repo: ${repoTarget}") + } + } + + + void commitAndPush(String commitMessage) { + commitAndPush(commitMessage, null, 'HEAD:refs/heads/main') + } + /** + * Push all refs, i.e. all tags and branches + */ + + void pushAll(boolean force) { + createPushCommand('refs/*:refs/*').setForce(force).call() + } + + + void pushRef(String ref, boolean force) { + pushRef(ref, ref, force) + } + + + void pushRef(String ref, String targetRef, boolean force) { + createPushCommand("${ref}:${targetRef}").setForce(force).call() + } + + + /** + * Delete all files in this repository + */ + void clearRepo() { + fileSystemUtils.deleteFilesExcept(new File(absoluteLocalRepoTmpDir), ".git") + } + + + void copyDirectoryContents(String srcDir) { + copyDirectoryContents(srcDir, (FileFilter) null) + } + + + void copyDirectoryContents(String srcDir, FileFilter fileFilter) { + if (!srcDir) { + log.warn("Source directory is not defined. Nothing to copy?") + return + } + + log.debug("Initializing repo $repoTarget from $srcDir") + String absoluteSrcDirLocation = new File(srcDir).isAbsolute() + ? srcDir + : "${fileSystemUtils.getRootDir()}/${srcDir}" + fileSystemUtils.copyDirectory(absoluteSrcDirLocation, absoluteLocalRepoTmpDir, fileFilter) + } + + + void writeFile(String path, String content) { + def file = new File("$absoluteLocalRepoTmpDir/$path") + fileSystemUtils.createDirectory(file.parent) + file.createNewFile() + file.text = content + } + + + void replaceTemplates(Map parameters) { + new TemplatingEngine().replaceTemplates(new File(absoluteLocalRepoTmpDir), parameters) + } + + private PushCommand createPushCommand(String refSpec) { + getGit() + .push() + .setRemote(getGitRepositoryUrl()) + .setRefSpecs(new RefSpec(refSpec)) + .setCredentialsProvider(getCredentialProvider()) + } + + String getGitRepositoryUrl() { + return this.gitProvider.repoUrl(repoTarget, RepoUrlScope.CLIENT) + } + + private Git getGit() { + if (gitMemoization != null) { + return gitMemoization + } + + return gitMemoization = Git.open(new File(absoluteLocalRepoTmpDir)) + } + + private CredentialsProvider getCredentialProvider() { + def auth = this.gitProvider.getCredentials() + def passwordAuthentication = new UsernamePasswordCredentialsProvider(auth.username, auth.password) + return insecure ? new ChainingCredentialsProvider(new InsecureCredentialProvider(), passwordAuthentication) : passwordAuthentication + } + +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/GitRepoFactory.groovy b/src/main/groovy/com/cloudogu/gitops/git/GitRepoFactory.groovy new file mode 100644 index 000000000..31e0b3221 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/GitRepoFactory.groovy @@ -0,0 +1,22 @@ +package com.cloudogu.gitops.git + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.utils.FileSystemUtils +import jakarta.inject.Singleton + +@Singleton +class GitRepoFactory { + protected final Config config + protected final FileSystemUtils fileSystemUtils + + GitRepoFactory(Config config, FileSystemUtils fileSystemUtils) { + this.fileSystemUtils = fileSystemUtils + this.config = config + } + + GitRepo getRepo(String repoTarget, GitProvider gitProvider) { + return new GitRepo(config, gitProvider, repoTarget, fileSystemUtils) + } + +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/jgit/InsecureCredentialProvider.groovy b/src/main/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProvider.groovy similarity index 97% rename from src/main/groovy/com/cloudogu/gitops/scmm/jgit/InsecureCredentialProvider.groovy rename to src/main/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProvider.groovy index 8009eaf04..6dfc9c866 100644 --- a/src/main/groovy/com/cloudogu/gitops/scmm/jgit/InsecureCredentialProvider.groovy +++ b/src/main/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProvider.groovy @@ -1,4 +1,4 @@ -package com.cloudogu.gitops.scmm.jgit +package com.cloudogu.gitops.git.jgit.helpers import org.eclipse.jgit.errors.UnsupportedCredentialItem import org.eclipse.jgit.transport.CredentialItem @@ -47,4 +47,4 @@ class InsecureCredentialProvider extends CredentialsProvider { return true } -} +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/GitProvider.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/GitProvider.groovy new file mode 100644 index 000000000..6d2568e51 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/GitProvider.groovy @@ -0,0 +1,67 @@ +package com.cloudogu.gitops.git.providers + +import com.cloudogu.gitops.config.Credentials + +interface GitProvider { + + default boolean createRepository(String repoTarget, String description) { + return createRepository(repoTarget, description, true); + } + + boolean createRepository(String repoTarget, String description, boolean initialize) + + void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) + + default String repoUrl(String repoTarget) { + return repoUrl(repoTarget, RepoUrlScope.IN_CLUSTER); + } + + String repoUrl(String repoTarget, RepoUrlScope scope); + + String repoPrefix() + + Credentials getCredentials() + + URI prometheusMetricsEndpoint() + + //TODO implement + void deleteRepository(String namespace, String repository, boolean prefixNamespace) + + //TODO implement + void deleteUser(String name) + + //TODO implement + void setDefaultBranch(String repoTarget, String branch) + + String getUrl() + + String getProtocol() + + String getHost() //TODO? can we maybe get this via helper and config? + + String getGitOpsUsername() + +} + +enum AccessRole { + READ, WRITE, MAINTAIN, ADMIN, OWNER +} + +enum Scope { + USER, GROUP +} + +/** + * IN_CLUSTER: URLs intended for workloads running inside the Kubernetes cluster + * (e.g., ArgoCD, Jobs, in-cluster automation). + * + * CLIENT : URLs intended for interactive or CI clients performing push/clone operations, + * regardless of their location. + * If the application itself runs inside Kubernetes, the Service DNS is used; + * otherwise, NodePort (for internal installations) or externalBase (for external ones) + * is selected automatically. + */ +enum RepoUrlScope { + IN_CLUSTER, + CLIENT +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/gitlab/Gitlab.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/gitlab/Gitlab.groovy new file mode 100644 index 000000000..48bdf7482 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/gitlab/Gitlab.groovy @@ -0,0 +1,390 @@ +package com.cloudogu.gitops.git.providers.gitlab + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.features.git.config.util.GitlabConfig +import com.cloudogu.gitops.git.providers.AccessRole +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.RepoUrlScope +import com.cloudogu.gitops.git.providers.Scope +import groovy.util.logging.Slf4j +import org.gitlab4j.api.GitLabApi +import org.gitlab4j.api.GitLabApiException +import org.gitlab4j.api.models.AccessLevel +import org.gitlab4j.api.models.Group +import org.gitlab4j.api.models.Project +import org.gitlab4j.api.models.Visibility + +import java.util.logging.Level + +@Slf4j +class Gitlab implements GitProvider { + + private final Config config + private final GitLabApi api + private GitlabConfig gitlabConfig + + Gitlab(Config config, GitlabConfig gitlabConfig) { + this.config = config + this.gitlabConfig = gitlabConfig + this.api = new GitLabApi(gitlabConfig.url, credentials.password) + this.api.enableRequestResponseLogging(Level.ALL) + } + + @Override + boolean createRepository(String repoTarget, String description, boolean initialize) { + def repoNamespace = repoTarget.split('/', 2)[0] + def repoName = repoTarget.split('/', 2)[1] + +// def repoNamespacePrefixed = config.application.namePrefix + repoNamespace + // 1) Resolve parent by numeric ID (do NOT treat the ID as a path!) + Group parent = parentGroup() + String repoNamespacePath = repoNamespace.toLowerCase() + String projectPath = repoName.toLowerCase() + + long subgroupId = ensureSubgroupUnderParentId(parent, repoNamespacePath) + String fullProjectPath = "${parentFullPath()}/${repoNamespacePath}/${projectPath}" + + + if (findProject(fullProjectPath).present) { + log.info("GitLab project already exists: ${fullProjectPath}") + return false + } + + def project = new Project() + .withName(repoName) + .withPath(projectPath) + .withDescription(description ?: "") + .withIssuesEnabled(false) + .withMergeRequestsEnabled(false) + .withWikiEnabled(false) + .withSnippetsEnabled(false) + .withNamespaceId(subgroupId) + .withInitializeWithReadme(initialize) + project.visibility = toVisibility(gitlabConfig.defaultVisibility) + + def created = api.projectApi.createProject(project) + log.info("Created GitLab project ${created.getPathWithNamespace()} (id=${created.id})") + return true + } + + @Override + void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) { + String fullPath = resolveFullPath(repoTarget) + Project project = findProjectOrThrow(fullPath) + AccessLevel level = toAccessLevel(role, scope) + if (scope == Scope.GROUP) { + def group = api.groupApi.getGroups(principal) + .find { it.fullPath == principal || it.path == principal || it.name == principal } + if (!group) throw new IllegalArgumentException("Group '${principal}' not found") + api.projectApi.shareProject(project.id, group.id, level, null) + } else { + def user = api.userApi.findUsers(principal) + .find { it.username == principal || it.email == principal } + if (!user) throw new IllegalArgumentException("User '${principal}' not found") + api.projectApi.addMember(project.id, user.id, level) + } + } + + @Override + String repoUrl(String repoTarget, RepoUrlScope scope) { + String base = gitlabConfig.url.strip() + return "${base}/${parentFullPath()}/${repoTarget}.git" + } + + @Override + String repoPrefix() { + String base = gitlabConfig.url.strip() + def prefix = (config.application.namePrefix ?: "").strip() + return "${base}/${parentFullPath()}/${prefix}" + + } + + //TODo getCredentials + @Override + Credentials getCredentials() { + return this.gitlabConfig.credentials + } + + @Override + String getProtocol() { + return gitlabConfig.url + } + + String getHost() { + return gitlabConfig.url + } + + @Override + String getGitOpsUsername() { + return gitlabConfig.gitOpsUsername + } + + @Override + String getUrl() { + return this.gitlabConfig.url + } + + //TODO do we dee + @Override + URI prometheusMetricsEndpoint() { + return null + } + + //TODO implement + @Override + void deleteRepository(String namespace, String repository, boolean prefixNamespace) { + + } + + //TODO implement + @Override + void deleteUser(String name) { + + } + + //TODO implement + @Override + void setDefaultBranch(String repoTarget, String branch) { + + } + + + private Group parentGroup() { + try { + return api.groupApi.getGroup(gitlabConfig.parentGroupId as Long) + } catch (GitLabApiException e) { + throw new IllegalStateException( + "Parent group '${gitlabConfig.parentGroupId}' not found or inaccessible: ${e.message}", e) + } + } + + + private String parentFullPath() { + parentGroup().fullPath + } + + /** Ensure a single-level subgroup exists under 'parent'; return its namespace (group) ID. */ + private long ensureSubgroupUnderParentId(Group parent, String segPath) { + // 1) Already there? + Group existing = findDirectSubgroupByPath(parent.id as Long, segPath) + if (existing != null) return existing.id as Long + + + // 2) Guard against project/subgroup name collision in the same parent + Project collision = findDirectProjectByPath(parent.id as Long, segPath) + if (collision != null) { + throw new IllegalStateException( + "Cannot create subgroup '${segPath}' under '${parent.fullPath}': " + + "a project with that path already exists at '${parent.fullPath}/${segPath}'. " + + "Rename/transfer the project first or choose a different subgroup name." + ) + } + + // 3) Create subgroup + Group toCreate = new Group() + .withName(segPath) // display name + .withPath(segPath) // (lowercase etc.) + .withParentId(parent.id) + + + try { + Group created = api.groupApi.addGroup(toCreate) + log.info("Created group {}", created.fullPath) + return created.id as Long + } catch (GitLabApiException e) { + // If someone created it in parallel, treat 400/409 as "exists" and re-fetch + if (e.httpStatus in [400, 409]) { + Group retry = findDirectSubgroupByPath(parent.id as Long, segPath) + if (retry != null) return retry.id as Long + } + def ve = e.hasValidationErrors() ? e.getValidationErrors() : null + log.error("addGroup failed (parent={}, segPath={}, status={}, message={}, validationErrors={})", + parent.fullPath, segPath, e.httpStatus, e.getMessage(), ve) + throw e + } + } + + + /** Find a direct subgroup of 'parentId' with the exact path . */ + private Group findDirectSubgroupByPath(Long parentId, String segPath) { + // uses the overload: getSubGroups(Object idOrPath) + List subGroups = api.groupApi.getSubGroups(parentId) + return subGroups?.find { Group subGroup -> subGroup.path == segPath } + } + + + /** Find a direct project of 'parentId' with the exact path . */ + private Project findDirectProjectByPath(Long parentId, String path) { + // uses the overload: getProjects(Object idOrPath) + List projects = api.groupApi.getProjects(parentId) + return projects?.find { Project project -> project.path == path } + } + + + // ---- Helpers ---- + private Optional findProject(String fullPath) { + try { + return Optional.ofNullable(api.projectApi.getProject(fullPath)) + } catch (Exception ignore) { + return Optional.empty() + } + } + + private Project findProjectOrThrow(String fullPath) { + return findProject(fullPath).orElseThrow { + new IllegalStateException("GitLab project '${fullPath}' not found") + } + } + + private String resolveFullPath(String repoTarget) { + if (!gitlabConfig.parentGroupId) { + throw new IllegalStateException("gitlab.parentGroup is not set") + } + return "${gitlabConfig.parentGroupId}/${repoTarget}" + } + + + private static Visibility toVisibility(String s) { + switch ((s ?: "private").toLowerCase()) { + case "public": return Visibility.PUBLIC + case "internal": return Visibility.INTERNAL + default: return Visibility.PRIVATE + } + } + +// provider-agnostic AccessRole → GitLab AccessLevel + private static AccessLevel toAccessLevel(AccessRole role, Scope scope) { + switch (role) { + case AccessRole.READ: + // GitLab: Guests usually can't read private repo code; Reporter can. + return AccessLevel.REPORTER + case AccessRole.WRITE: + // Typical push/merge permissions + return AccessLevel.DEVELOPER + case AccessRole.MAINTAIN: + return AccessLevel.MAINTAINER + case AccessRole.ADMIN: + // No separate project-level "admin" → cap at Maintainer + return AccessLevel.MAINTAINER + case AccessRole.OWNER: + // OWNER is meaningful for groups/namespaces; for users on a project we cap to MAINTAINER + return (scope == Scope.GROUP) ? AccessLevel.OWNER : AccessLevel.MAINTAINER + default: + throw new IllegalArgumentException("Unknown role: ${role}") + } + } + + + //TODO when git abctraction feature is ready, we will create before merge to main a branch, that + // contain this code as preservation for oop + /* ================================= SETUP CODE ==================================== + void setup() { + log.info("Creating Gitlab Groups") + def mainGroupName = "${config.application.namePrefix}scm".toString() + Group mainSCMGroup = this.gitlabApi.groupApi.getGroup(mainGroupName) + if (!mainSCMGroup) { + def tempGroup = new Group() + .withName(mainGroupName) + .withPath(mainGroupName.toLowerCase()) + .withParentId(null) + + mainSCMGroup = this.gitlabApi.groupApi.addGroup(tempGroup) + } + + String argoCDGroupName = 'argocd' + Optional argoCDGroup = getGroup("${mainGroupName}/${argoCDGroupName}") + if (argoCDGroup.isEmpty()) { + def tempGroup = new Group() + .withName(argoCDGroupName) + .withPath(argoCDGroupName.toLowerCase()) + .withParentId(mainSCMGroup.id) + + argoCDGroup = addGroup(tempGroup) + } + + argoCDGroup.ifPresent(this.&createArgoCDRepos) + + String dependencysGroupName = '3rd-party-dependencies' + Optional dependencysGroup = getGroup("${mainGroupName}/${dependencysGroupName}") + if (dependencysGroup.isEmpty()) { + def tempGroup = new Group() + .withName(dependencysGroupName) + .withPath(dependencysGroupName.toLowerCase()) + .withParentId(mainSCMGroup.id) + + addGroup(tempGroup) + } + + String exercisesGroupName = 'exercises' + Optional exercisesGroup = getGroup("${mainGroupName}/${exercisesGroupName}") + if (exercisesGroup.isEmpty()) { + def tempGroup = new Group() + .withName(exercisesGroupName) + .withPath(exercisesGroupName.toLowerCase()) + .withParentId(mainSCMGroup.id) + + exercisesGroup = addGroup(tempGroup) + } + + exercisesGroup.ifPresent(this.&createExercisesRepos) + } + + void createRepo(String name, String description) { + Optional project = getProject("${parentGroup.getFullPath()}/${name}".toString()) + if (project.isEmpty()) { + Project projectSpec = new Project() + .withName(name) + .withDescription(description) + .withIssuesEnabled(true) + .withMergeRequestsEnabled(true) + .withWikiEnabled(true) + .withSnippetsEnabled(true) + .withPublic(false) + .withNamespaceId(this.gitlabConfig.parentGroup.toLong()) + .withInitializeWithReadme(true) + + project = Optional.ofNullable(this.gitlabApi.projectApi.createProject(projectSpec)) + log.info("Project ${projectSpec} created in Gitlab!") + } + removeBranchProtection(project.get()) + } + + void removeBranchProtection(Project project) { + try { + this.gitlabApi.getProtectedBranchesApi().unprotectBranch(project.getId(), project.getDefaultBranch()) + log.debug("Unprotected default branch: " + project.getDefaultBranch()) + } catch (Exception ex) { + log.error("Failed to unprotect default branch '${project.getDefaultBranch()}' for project '${project.getName()}' (ID: ${project.getId()})", ex) + } + } + + + private Optional getGroup(String groupName) { + try { + return Optional.ofNullable(this.gitlabApi.groupApi.getGroup(groupName)) + } catch (Exception e) { + return Optional.empty() + } + } + + private Optional addGroup(Group group) { + try { + return Optional.ofNullable(this.gitlabApi.groupApi.addGroup(group)) + } catch (Exception e) { + return Optional.empty() + } + } + + private Optional getProject(String projectPath) { + try { + return Optional.ofNullable(this.gitlabApi.projectApi.getProject(projectPath)) + } catch (Exception e) { + return Optional.empty() + + + } + } + */ + +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/api/Permission.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/Permission.groovy similarity index 91% rename from src/main/groovy/com/cloudogu/gitops/scmm/api/Permission.groovy rename to src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/Permission.groovy index d70c99f97..ffe623bcd 100644 --- a/src/main/groovy/com/cloudogu/gitops/scmm/api/Permission.groovy +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/Permission.groovy @@ -1,4 +1,4 @@ -package com.cloudogu.gitops.scmm.api +package com.cloudogu.gitops.git.providers.scmmanager class Permission { final String name diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManager.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManager.groovy new file mode 100644 index 000000000..16f1059ca --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManager.groovy @@ -0,0 +1,284 @@ +package com.cloudogu.gitops.git.providers.scmmanager + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig +import com.cloudogu.gitops.git.providers.AccessRole +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.RepoUrlScope +import com.cloudogu.gitops.git.providers.Scope +import com.cloudogu.gitops.git.providers.scmmanager.api.Repository +import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApiClient +import com.cloudogu.gitops.utils.K8sClient +import com.cloudogu.gitops.utils.NetworkingUtils +import groovy.util.logging.Slf4j +import retrofit2.Response + +@Slf4j +class ScmManager implements GitProvider { + + private final ScmManagerUrlResolver urls + private final ScmManagerApiClient apiClient + private final ScmManagerConfig scmmConfig + + ScmManager(Config config, ScmManagerConfig scmmConfig, K8sClient k8sClient, NetworkingUtils networkingUtils) { + this.scmmConfig = scmmConfig + this.urls = new ScmManagerUrlResolver(config, scmmConfig, k8sClient, networkingUtils) + this.apiClient = new ScmManagerApiClient(urls.clientApiBase().toString(), scmmConfig.credentials, config.application.insecure) + } + + + // --- Git operations --- + @Override + boolean createRepository(String repoTarget, String description, boolean initialize) { + def repoNamespace = repoTarget.split('/', 2)[0] + def repoName = repoTarget.split('/', 2)[1] + def repo = new Repository(repoNamespace, repoName, description ?: "") + Response response = apiClient.repositoryApi().create(repo, initialize).execute() + return handle201or409(response, "Repository ${repoNamespace}/${repoName}") + } + + @Override + void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) { + def repoNamespace = repoTarget.split('/', 2)[0] + def repoName = repoTarget.split('/', 2)[1] + + boolean isGroup = (scope == Scope.GROUP) + Permission.Role scmManagerRole = mapToScmManager(role) + def permission = new Permission(principal, scmManagerRole, isGroup) + + Response response = apiClient.repositoryApi().createPermission(repoNamespace, repoName, permission).execute() + handle201or409(response, "Permission on ${repoNamespace}/${repoName}") + } + + @Override + Credentials getCredentials() { + return this.scmmConfig.credentials + } + + + //TODO implement + @Override + void setDefaultBranch(String repoTarget, String branch) { + + } + + //TODO implement + @Override + void deleteRepository(String namespace, String repository, boolean prefixNamespace) { + + } + //TODO implement + @Override + void deleteUser(String name) { + + } + + @Override + String getGitOpsUsername() { + return scmmConfig.gitOpsUsername + } + + // --- In-cluster / Endpoints --- + /** In-cluster base …/scm (without trailing slash) */ + @Override + String getUrl() { + return urls.inClusterBase().toString() + } + + /** In-cluster repo prefix: …/scm//[] */ + @Override + String repoPrefix() { + return urls.inClusterRepoPrefix() + } + + + /** …/scm/// */ + @Override + String repoUrl(String repoTarget, RepoUrlScope scope) { + switch (scope) { + case RepoUrlScope.CLIENT: + return urls.clientRepoUrl(repoTarget) + case RepoUrlScope.IN_CLUSTER: + return urls.inClusterRepoUrl(repoTarget) + default: + return urls.inClusterRepoUrl(repoTarget) + } + } + + @Override + String getProtocol() { + return urls.inClusterBase().scheme // e.g. "http" + } + + @Override + String getHost() { + return urls.inClusterBase().host // e.g. "scmm.ns.svc.cluster.local" + } + + + /** …/scm/api/v2/metrics/prometheus — client-side, typically scraped externally */ + @Override + URI prometheusMetricsEndpoint() { + return urls.prometheusEndpoint() + } + + // --- helpers --- + private static Permission.Role mapToScmManager(AccessRole role) { + switch (role) { + case AccessRole.READ: return Permission.Role.READ + case AccessRole.WRITE: return Permission.Role.WRITE + case AccessRole.MAINTAIN: + // SCM-manager doesn't know MAINTAIN -> downgrade to WRITE + log.warn("SCM-Manager: Mapping MAINTAIN → WRITE") + return Permission.Role.WRITE + case AccessRole.ADMIN: return Permission.Role.OWNER + case AccessRole.OWNER: return Permission.Role.OWNER + } + } + + private static boolean handle201or409(Response response, String what) { + int code = response.code() + if (code == 409) { + log.debug("${what} already exists — ignoring (HTTP 409)") + return false + } else if (code != 201) { + throw new RuntimeException("Could not create ${what}" + + "HTTP Details: ${response.code()} ${response.message()}: ${response.errorBody().string()}") + } + return true// because its created + } + + /** Test-only constructor (package-private on purpose). */ + ScmManager(Config config, ScmManagerConfig scmmConfig, + ScmManagerUrlResolver urls, + ScmManagerApiClient apiClient) { + this.scmmConfig = Objects.requireNonNull(scmmConfig, "scmmConfig must not be null") + this.urls = Objects.requireNonNull(urls, "urls must not be null") + this.apiClient = apiClient ?: new ScmManagerApiClient( + urls.clientApiBase().toString(), + scmmConfig.credentials, + Objects.requireNonNull(config, "config must not be null").application.insecure + ) + } + + //TODO when git abctraction feature is ready, we will create before merge to main a branch, that + // contain this code as preservation for oop + /* ============================= SETUP FOR LATER =========================================== +void waitForScmmAvailable(int timeoutSeconds = 60, int intervalMillis = 2000) { + long startTime = System.currentTimeMillis() + long timeoutMillis = timeoutSeconds * 1000L + + while (System.currentTimeMillis() - startTime < timeoutMillis) { + try { + def call = this.scmmApiClient.generalApi().checkScmmAvailable() + def response = call.execute() + + if (response.successful) { + return + } else { + println "SCM-Manager not ready yet: HTTP ${response.code()}" + } + } catch (Exception e) { + println "Waiting for SCM-Manager... Error: ${e.message}" + } + + sleep(intervalMillis) + } + throw new RuntimeException("Timeout: SCM-Manager did not respond with 200 OK within ${timeoutSeconds} seconds") +} + void setup(){ + setupInternalScm(this.namespace) + setupHelm() + installScmmPlugins() + configureJenkinsPlugin() +} + +void setupInternalScm(String namespace) { + this.namespace = namespace + setInternalUrl() +} + +//TODO URL handling by object +String setInternalUrl() { + this.url="http://scmm.${namespace}.svc.cluster.local/scm" +} + +void setupHelm() { + def templatedMap = templateToMap(HELM_VALUES_PATH, [ + host : scmmConfig.ingress, + remote : config.application.remote, + username : this.scmmConfig.credentials.username, + password : this.scmmConfig.credentials.password, + helm : this.scmmConfig.helm, + releaseName: releaseName + ]) + + def helmConfig = this.scmmConfig.helm + def mergedMap = MapUtils.deepMerge(helmConfig.values, templatedMap) + def tempValuesPath = fileSystemUtils.writeTempFile(mergedMap) + + this.deployer.deployFeature( + helmConfig.repoURL, + 'scm-manager', + helmConfig.chart, + helmConfig.version, + namespace, + releaseName, + tempValuesPath + ) + waitForScmmAvailable() +} + +//TODO System.env to config Object +def installScmmPlugins(Boolean restart = true) { + + if (System.getenv('SKIP_PLUGINS')?.toLowerCase() == 'true') { + log.info("Skipping SCM plugin installation due to SKIP_PLUGINS=true") + return + } + + if (System.getenv('SKIP_RESTART')?.toLowerCase() == 'true') { + log.info("Skipping SCMM restart due to SKIP_RESTART=true") + restart = false + } + + def pluginNames = [ + "scm-mail-plugin", + "scm-review-plugin", + "scm-code-editor-plugin", + "scm-editor-plugin", + "scm-landingpage-plugin", + "scm-el-plugin", + "scm-readme-plugin", + "scm-webhook-plugin", + "scm-ci-plugin", + "scm-metrics-prometheus-plugin" + ] + def jenkinsUrl = System.getenv('JENKINS_URL_FOR_SCMM') + if (jenkinsUrl) { + pluginNames.add("scm-jenkins-plugin") + } + + for (String pluginName : pluginNames) { + log.info("Installing Plugin ${pluginName} ...") + + try { + def response = scmmApiClient.pluginApi().install(pluginName, restart).execute() + + if (!response.isSuccessful()) { + def message = "Installing Plugin '${pluginName}' failed with status: ${response.code()} - ${response.message()}" + log.error(message) + throw new RuntimeException(message) + } else { + log.info("Successfully installed plugin '${pluginName}'") + } + } catch (Exception e) { + log.error("Installing Plugin '${pluginName}' failed with error: ${e.message}", e) + throw new RuntimeException("Installing Plugin '${pluginName}' failed", e) + } + } +} + +*/ +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolver.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolver.groovy new file mode 100644 index 000000000..033d02ba1 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolver.groovy @@ -0,0 +1,128 @@ +package com.cloudogu.gitops.git.providers.scmmanager + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig +import com.cloudogu.gitops.utils.K8sClient +import com.cloudogu.gitops.utils.NetworkingUtils +import groovy.util.logging.Slf4j + +@Slf4j +class ScmManagerUrlResolver { + + private final Config config + private final ScmManagerConfig scmm + private final K8sClient k8s + private final NetworkingUtils net + + private final String releaseName = 'scmm' + private URI cachedClusterBind + + ScmManagerUrlResolver(Config config, ScmManagerConfig scmm, K8sClient k8s, NetworkingUtils net) { + this.config = config + this.scmm = scmm + this.k8s = k8s + this.net = net + } + + + // ---------- Public API used by ScmManager ---------- + + /** Client base …/scm (no trailing slash) */ + URI clientBase() { noTrailSlash(ensureScm(clientBaseRaw())) } + + /** Client API base …/scm/api/ */ + URI clientApiBase() { withSlash(clientBase()).resolve("api/") } + + /** Client repo base …/scm/repo (no trailing slash) */ + URI clientRepoBase() { noTrailSlash(withSlash(clientBase()).resolve("${root()}/")) } + + + /** In-cluster base …/scm (no trailing slash) */ + URI inClusterBase() { noTrailSlash(ensureScm(inClusterBaseRaw())) } + + /** In-cluster repo prefix …/scm/repo/[] */ + String inClusterRepoPrefix() { + def prefix = (config.application.namePrefix ?: "").strip() + def base = withSlash(inClusterBase()) + def url = withSlash(base.resolve(root())) + + return URI.create(url.toString() + prefix).toString() + } + + /** In-cluster repo URL …/scm/repo// */ + String inClusterRepoUrl(String repoTarget) { + def repo = repoTarget.strip() + noTrailSlash(withSlash(inClusterBase()).resolve("${root()}/${repo}/")).toString() + } + + /** Client repo URL …/scm/repo// (no trailing slash) */ + String clientRepoUrl(String repoTarget) { + def repo = repoTarget.strip() + noTrailSlash(withSlash(clientRepoBase()).resolve("${repo}/")).toString() + } + + /** …/scm/api/v2/metrics/prometheus */ + URI prometheusEndpoint() { withSlash(clientBase()).resolve("api/v2/metrics/prometheus") } + + // ---------- Base resolution ---------- + + private URI clientBaseRaw() { + if (Boolean.TRUE == scmm.internal) + return config.application.runningInsideK8s ? serviceDnsBase() : nodePortBase() + return externalBase() + } + + private URI inClusterBaseRaw() { + return scmm.internal ? serviceDnsBase() : externalBase() + } + + private URI serviceDnsBase() { + System.out.println("serviceDnsBase namespace: " + scmm.namespace) + def namespace = (scmm.namespace ?: "scm-manager").strip() + + + URI.create("http://scmm.${namespace}.svc.cluster.local") + } + + private URI externalBase() { + def url = (scmm.url ?: "").strip() + if (url) return URI.create(url) + + def ingress = (scmm.ingress ?: "").strip() + if (ingress) return URI.create("http://${ingress}") + throw new IllegalArgumentException("Either scmm.url or scmm.ingress must be set when internal=false") + } + + private URI nodePortBase() { + if (cachedClusterBind) return cachedClusterBind + + def namespace = (scmm.namespace ?: "scm-manager").strip() + + final def port = k8s.waitForNodePort(releaseName, namespace) + final def host = net.findClusterBindAddress() + cachedClusterBind = new URI("http://${host}:${port}") + return cachedClusterBind + } + + // ---------- Helpers ---------- + + private String root() { + (scmm.rootPath ?: "repo").strip() + } + + private static URI ensureScm(URI u) { + def us = withSlash(u) + def path = us.path ?: "" + path.endsWith("/scm/") ? us : us.resolve("scm/") + } + + private static URI withSlash(URI u) { + def s = u.toString() + s.endsWith('/') ? u : URI.create(s + '/') + } + + private static URI noTrailSlash(URI u) { + def s = u.toString() + s.endsWith('/') ? URI.create(s.substring(0, s.length() - 1)) : u + } +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/api/AuthorizationInterceptor.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/AuthorizationInterceptor.groovy similarity index 91% rename from src/main/groovy/com/cloudogu/gitops/scmm/api/AuthorizationInterceptor.groovy rename to src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/AuthorizationInterceptor.groovy index 2396a7b56..28d799bf3 100644 --- a/src/main/groovy/com/cloudogu/gitops/scmm/api/AuthorizationInterceptor.groovy +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/AuthorizationInterceptor.groovy @@ -1,4 +1,4 @@ -package com.cloudogu.gitops.scmm.api +package com.cloudogu.gitops.git.providers.scmmanager.api import okhttp3.Credentials @@ -23,4 +23,4 @@ class AuthorizationInterceptor implements Interceptor { return chain.proceed(newRequest) } -} +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/PluginApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/PluginApi.groovy new file mode 100644 index 000000000..76450db8b --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/PluginApi.groovy @@ -0,0 +1,19 @@ +package com.cloudogu.gitops.git.providers.scmmanager.api + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +interface PluginApi { + @POST("v2/plugins/available/{name}/install") + Call install(@Path("name") String name, @Query("restart") Boolean restart) + + @PUT("api/v2/config/jenkins/") + @Headers("Content-Type: application/json") + Call configureJenkinsPlugin(@Body Map config) +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/api/Repository.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/Repository.groovy similarity index 92% rename from src/main/groovy/com/cloudogu/gitops/scmm/api/Repository.groovy rename to src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/Repository.groovy index bb9c5f62c..9dea6dd14 100644 --- a/src/main/groovy/com/cloudogu/gitops/scmm/api/Repository.groovy +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/Repository.groovy @@ -1,4 +1,4 @@ -package com.cloudogu.gitops.scmm.api +package com.cloudogu.gitops.git.providers.scmmanager.api class Repository { final String name diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/api/RepositoryApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/RepositoryApi.groovy similarity index 85% rename from src/main/groovy/com/cloudogu/gitops/scmm/api/RepositoryApi.groovy rename to src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/RepositoryApi.groovy index 6db6a981e..59871b48c 100644 --- a/src/main/groovy/com/cloudogu/gitops/scmm/api/RepositoryApi.groovy +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/RepositoryApi.groovy @@ -1,5 +1,6 @@ -package com.cloudogu.gitops.scmm.api +package com.cloudogu.gitops.git.providers.scmmanager.api +import com.cloudogu.gitops.git.providers.scmmanager.Permission import okhttp3.ResponseBody import retrofit2.Call import retrofit2.http.* @@ -15,4 +16,4 @@ interface RepositoryApi { @POST("v2/repositories/{namespace}/{name}/permissions/") @Headers("Content-Type: application/vnd.scmm-repositoryPermission+json") Call createPermission(@Path("namespace") String namespace, @Path("name") String name, @Body Permission permission) -} +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApi.groovy new file mode 100644 index 000000000..e974b3cd6 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApi.groovy @@ -0,0 +1,17 @@ +package com.cloudogu.gitops.git.providers.scmmanager.api + +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.PUT + +interface ScmManagerApi { + + @GET("api/v2") + Call checkScmmAvailable() + + @PUT("api/v2/config") + @Headers("Content-Type: application/vnd.scmm-config+json;v=2") + Call setConfig(@Body Map config) +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApiClient.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApiClient.groovy new file mode 100644 index 000000000..f6d058485 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApiClient.groovy @@ -0,0 +1,48 @@ +package com.cloudogu.gitops.git.providers.scmmanager.api + + +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.dependencyinjection.HttpClientFactory +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory + +/** + * Parent class for all SCMM Apis that lazily creates the APIs + */ +class ScmManagerApiClient { + Credentials credentials + OkHttpClient okHttpClient + String url + + ScmManagerApiClient(String url, Credentials credentials, Boolean isInsecure) { + this.url = url + this.credentials = credentials + this.okHttpClient = HttpClientFactory.buildOkHttpClient(credentials, isInsecure) + } + + UsersApi usersApi() { + return retrofit().create(UsersApi) + } + + RepositoryApi repositoryApi() { + return retrofit().create(RepositoryApi) + } + + ScmManagerApi generalApi() { + return retrofit().create(ScmManagerApi) + } + + PluginApi pluginApi() { + return retrofit().create(PluginApi) + } + + protected Retrofit retrofit() { + return new Retrofit.Builder() + .baseUrl(this.url) + .client(okHttpClient) + // Converts HTTP body objects from groovy to JSON + .addConverterFactory(JacksonConverterFactory.create()) + .build() + } +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerUser.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerUser.groovy new file mode 100644 index 000000000..c33af243a --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerUser.groovy @@ -0,0 +1,11 @@ +package com.cloudogu.gitops.git.providers.scmmanager.api + +class ScmManagerUser { + String name + String displayName + String mail + boolean external = false + String password + boolean active = true + Map _links = [:] +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApi.groovy new file mode 100644 index 000000000..671996d89 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApi.groovy @@ -0,0 +1,18 @@ +package com.cloudogu.gitops.git.providers.scmmanager.api + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Path + +interface UsersApi { + @DELETE("v2/users/{id}") + Call delete(@Path("id") String id) + + @Headers(["Content-Type: application/vnd.scmm-user+json;v=2"]) + @POST("/api/v2/users") + Call addUser(@Body ScmManagerUser user) +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy index 45addc202..686653777 100644 --- a/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy +++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy @@ -1,7 +1,7 @@ package com.cloudogu.gitops.kubernetes.argocd -import com.cloudogu.gitops.scmm.ScmmRepo +import com.cloudogu.gitops.git.GitRepo import com.cloudogu.gitops.utils.TemplatingEngine import groovy.util.logging.Slf4j @@ -50,7 +50,7 @@ class ArgoApplication { return new File(outputDir, filename) } - void generate(ScmmRepo repo, String subfolder) { + void generate(GitRepo repo, String subfolder) { log.debug("Generating ArgoCDApplication for name='${name}', namespace='${namespace}''") def outputDir = Path.of(repo.absoluteLocalRepoTmpDir, subfolder).toFile() diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy index c86c693dd..adbba381d 100644 --- a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy +++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy @@ -1,7 +1,7 @@ package com.cloudogu.gitops.kubernetes.rbac import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo +import com.cloudogu.gitops.git.GitRepo import com.cloudogu.gitops.utils.TemplatingEngine import groovy.util.logging.Slf4j @@ -15,7 +15,7 @@ class RbacDefinition { private String namespace private List serviceAccounts = [] private String subfolder = "rbac" - private ScmmRepo repo + private GitRepo repo private Config config private final TemplatingEngine templater = new TemplatingEngine() @@ -48,7 +48,7 @@ class RbacDefinition { return this } - RbacDefinition withRepo(ScmmRepo repo) { + RbacDefinition withRepo(GitRepo repo) { this.repo = repo return this } @@ -84,4 +84,4 @@ class RbacDefinition { ) } -} +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/okhttp/ScmManagerAPI.groovy b/src/main/groovy/com/cloudogu/gitops/okhttp/ScmManagerAPI.groovy new file mode 100644 index 000000000..7d677ac7f --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/okhttp/ScmManagerAPI.groovy @@ -0,0 +1,4 @@ +package com.cloudogu.gitops.okhttp + +class ScmManagerAPI { +} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/ScmUrlResolver.groovy b/src/main/groovy/com/cloudogu/gitops/scmm/ScmUrlResolver.groovy deleted file mode 100644 index ede122dd0..000000000 --- a/src/main/groovy/com/cloudogu/gitops/scmm/ScmUrlResolver.groovy +++ /dev/null @@ -1,86 +0,0 @@ -package com.cloudogu.gitops.scmm - -import com.cloudogu.gitops.config.Config - -class ScmUrlResolver { - /** - * Returns the tenant/namespace base URL **without** a trailing slash. - * - * Why no trailing slash? - * - Callers often append further segments (e.g., "/repo//" or ".git"). - * Returning a slash here easily leads to double slashes ("//") or brittle - * template logic. - */ - static String tenantBaseUrl(Config config) { - switch (config.scmm.provider) { - case "scm-manager": - // scmmBaseUri ends with /scm/ - return scmmBaseUri(config).resolve("${config.scmm.rootPath}/${config.application.namePrefix}").toString() - case "gitlab": - // for GitLab, do not append /scm/ - return externalHost(config).resolve("${config.application.namePrefix}${config.scmm.rootPath}").toString() - default: - throw new IllegalArgumentException("Unknown SCM provider: ${config.scmm.provider}") - } - } - - /** - * External host base URL, ALWAYS ending with "/". - * - * Source: - * - Uses config.scmm.url if present; otherwise builds from config.scmm.protocol + "://" + config.scmm.host. - * - * Notes: - * - This method does NOT strip paths (e.g., "/scm"). If config.scmm.url includes a path, it will be preserved; - * only the trailing "/" is enforced. - * - Intended as a base for later URI.resolve() calls. - * - */ - static URI externalHost(Config config) { - def urlString = (config.scmm?.url ?: "${config.scmm.protocol}://${config.scmm.host}" as String).strip() - def uri = URI.create(urlString) - if (uri.toString().endsWith("/")) { - return uri - } else { - return URI.create(uri.toString() + "/") - } - } - - /** - * Service base URL (SCM-Manager incl. "/scm/", ALWAYS ending with "/"). - * - * Source: - * - Internal: "http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm/" - * - External: config.scmm.url (must already include the context path, e.g., "/scm") - * - * Notes: - * - Ensures a trailing "/" so java.net.URI.resolve() preserves the "/scm" segment; without it, a base may be treated as a file. - * - Does NOT strip paths from config.scmm.url; any provided path is preserved—only the trailing "/" is enforced. - * - Intended as a base for subsequent URI.resolve(...) calls. - * - Guarantee: always ends with "/". - * - * Throws: - * - IllegalArgumentException if external mode is selected (config.scmm.internal = false) but config.scmm.url is empty. - */ - static URI scmmBaseUri(Config config) { - if (config.scmm.internal) { - return new URI("http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm/") - } - - def urlString = config.scmm?.url?.strip() ?: "" - if (!urlString) { - throw new IllegalArgumentException("config.scmm.url must be set when config.scmm.internal = false") - } - def uri = URI.create(urlString) - // ensure a trailing slash - if (uri.toString().endsWith("/")) { - return uri - } else { - return URI.create(uri.toString() + "/") - } - } - - static String scmmRepoUrl(Config config, String repoNamespaceAndName) { - return scmmBaseUri(config).resolve("${config.scmm.rootPath}/${repoNamespaceAndName}").toString() - } -} diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/ScmmRepo.groovy b/src/main/groovy/com/cloudogu/gitops/scmm/ScmmRepo.groovy deleted file mode 100644 index 1736992c0..000000000 --- a/src/main/groovy/com/cloudogu/gitops/scmm/ScmmRepo.groovy +++ /dev/null @@ -1,234 +0,0 @@ -package com.cloudogu.gitops.scmm - -import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.api.Permission -import com.cloudogu.gitops.scmm.api.Repository -import com.cloudogu.gitops.scmm.api.ScmmApiClient -import com.cloudogu.gitops.scmm.jgit.InsecureCredentialProvider -import com.cloudogu.gitops.utils.FileSystemUtils -import com.cloudogu.gitops.utils.TemplatingEngine -import groovy.util.logging.Slf4j -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.api.PushCommand -import org.eclipse.jgit.transport.ChainingCredentialsProvider -import org.eclipse.jgit.transport.CredentialsProvider -import org.eclipse.jgit.transport.RefSpec -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider -import retrofit2.Response - -@Slf4j -class ScmmRepo { - - static final String NAMESPACE_3RD_PARTY_DEPENDENCIES = '3rd-party-dependencies' - - private String scmmRepoTarget - private String username - private String password - private String scmmUrl - private String absoluteLocalRepoTmpDir - protected FileSystemUtils fileSystemUtils - private boolean insecure - private Git gitMemoization = null - private String gitName - private String gitEmail - private String rootPath - private String scmProvider - private Config config - - Boolean isCentralRepo - - ScmmRepo(Config config, String scmmRepoTarget, FileSystemUtils fileSystemUtils, Boolean isCentralRepo = false) { - def tmpDir = File.createTempDir() - tmpDir.deleteOnExit() - this.isCentralRepo = isCentralRepo - this.username = !this.isCentralRepo ? config.scmm.username : config.multiTenant.username - this.password = !this.isCentralRepo ? config.scmm.password : config.multiTenant.password - - //switching from normal scm path to the central path - this.scmmUrl = "${config.scmm.protocol}://${config.scmm.host}" - if(this.isCentralRepo) { - boolean useInternal = config.multiTenant.internal - String internalUrl = "http://scmm.${config.multiTenant.centralSCMamespace}.svc.cluster.local/scm" - String externalUrl = config.multiTenant.centralScmUrl.toString() - - this.scmmUrl = useInternal ? internalUrl : externalUrl - } - - - this.scmmRepoTarget = scmmRepoTarget.startsWith(NAMESPACE_3RD_PARTY_DEPENDENCIES) ? scmmRepoTarget : - "${config.application.namePrefix}${scmmRepoTarget}" - - this.absoluteLocalRepoTmpDir = tmpDir.absolutePath - this.fileSystemUtils = fileSystemUtils - this.insecure = config.application.insecure - this.gitName = config.application.gitName - this.gitEmail = config.application.gitEmail - this.scmProvider = config.scmm.provider - this.rootPath = config.scmm.rootPath - this.config = config - } - - String getAbsoluteLocalRepoTmpDir() { - return absoluteLocalRepoTmpDir - } - - String getScmmRepoTarget() { - return scmmRepoTarget - } - - void cloneRepo() { - log.debug("Cloning $scmmRepoTarget repo") - Git.cloneRepository() - .setURI(getGitRepositoryUrl()) - .setDirectory(new File(absoluteLocalRepoTmpDir)) - .setCredentialsProvider(getCredentialProvider()) - .call() - } - - /** - * @return true if created, false if already exists. Throw exception on all other errors - */ - boolean create(String description, ScmmApiClient scmmApiClient, boolean initialize = true) { - def namespace = scmmRepoTarget.split('/', 2)[0] - def repoName = scmmRepoTarget.split('/', 2)[1] - - def repositoryApi = scmmApiClient.repositoryApi() - def repo = new Repository(namespace, repoName, description) - log.debug("Creating repo: ${namespace}/${repoName}") - def createResponse = repositoryApi.create(repo, initialize).execute() - handleResponse(createResponse, repo) - - def permission = new Permission(config.scmm.gitOpsUsername as String, Permission.Role.WRITE) - def permissionResponse = repositoryApi.createPermission(namespace, repoName, permission).execute() - return handleResponse(permissionResponse, permission, "for repo $namespace/$repoName") - } - - private static boolean handleResponse(Response response, Object body, String additionalMessage = '') { - if (response.code() == 409) { - // Here, we could consider sending another request for changing the existing object to become proper idempotent - log.debug("${body.class.simpleName} already exists ${additionalMessage}, ignoring: ${body}") - return false // because repo exists - } else if (response.code() != 201) { - throw new RuntimeException("Could not create ${body.class.simpleName} ${additionalMessage}.\n${body}\n" + - "HTTP Details: ${response.code()} ${response.message()}: ${response.errorBody().string()}") - } - return true// because its created - } - - void writeFile(String path, String content) { - def file = new File("$absoluteLocalRepoTmpDir/$path") - fileSystemUtils.createDirectory(file.parent) - file.createNewFile() - file.text = content - } - - void copyDirectoryContents(String srcDir, FileFilter fileFilter = null) { - if (!srcDir) { - println "Source directory is not defined. Nothing to copy?" - return - } - - log.debug("Initializing repo $scmmRepoTarget with content of folder $srcDir") - String absoluteSrcDirLocation = srcDir - if (!new File(absoluteSrcDirLocation).isAbsolute()) { - absoluteSrcDirLocation = fileSystemUtils.getRootDir() + "/" + srcDir - } - fileSystemUtils.copyDirectory(absoluteSrcDirLocation, absoluteLocalRepoTmpDir, fileFilter) - } - - void replaceTemplates(Map parameters) { - new TemplatingEngine().replaceTemplates(new File(absoluteLocalRepoTmpDir), parameters) - } - - def commitAndPush(String commitMessage, String tag = null, String refSpec = 'HEAD:refs/heads/main') { - log.debug("Adding files to repo: ${scmmRepoTarget}") - getGit() - .add() - .addFilepattern(".") - .call() - - if (getGit().status().call().hasUncommittedChanges()) { - log.debug("Committing repo: ${scmmRepoTarget}") - getGit() - .commit() - .setSign(false) - .setMessage(commitMessage) - .setAuthor(gitName, gitEmail) - .setCommitter(gitName, gitEmail) - .call() - - def pushCommand = createPushCommand(refSpec) - - if (tag) { - log.debug("Setting tag '${tag}' on repo: ${scmmRepoTarget}") - // Delete existing tags first to get idempotence - getGit().tagDelete().setTags(tag).call() - getGit() - .tag() - .setName(tag) - .call() - - pushCommand.setPushTags() - } - - log.debug("Pushing repo: ${scmmRepoTarget}, refSpec: ${refSpec}") - pushCommand.call() - } else { - log.debug("No changes after add, nothing to commit or push on repo: ${scmmRepoTarget}") - } - } - - /** - * Push all refs, i.e. all tags and branches - */ - def pushAll(boolean force = false) { - createPushCommand('refs/*:refs/*').setForce(force).call() - } - - def pushRef(String ref, String targetRef, boolean force = false) { - createPushCommand("${ref}:${targetRef}").setForce(force).call() - } - - def pushRef(String ref, boolean force = false) { - pushRef(ref, ref, force) - } - - private PushCommand createPushCommand(String refSpec) { - getGit() - .push() - .setRemote(getGitRepositoryUrl()) - .setRefSpecs(new RefSpec(refSpec)) - .setCredentialsProvider(getCredentialProvider()) - } - - private CredentialsProvider getCredentialProvider() { - if (scmProvider == "gitlab") { - username = "oauth2" - } - def passwordAuthentication = new UsernamePasswordCredentialsProvider(username, password) - - if (!insecure) { - return passwordAuthentication - } - - return new ChainingCredentialsProvider(new InsecureCredentialProvider(), passwordAuthentication) - } - - private Git getGit() { - if (gitMemoization != null) { - return gitMemoization - } - - return gitMemoization = Git.open(new File(absoluteLocalRepoTmpDir)) - } - - String getGitRepositoryUrl() { - return "${scmmUrl}/${rootPath}/${scmmRepoTarget}" - } - /** - * Delete all files in this repository - */ - void clearRepo() { - fileSystemUtils.deleteFilesExcept(new File(absoluteLocalRepoTmpDir), ".git") - } -} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/ScmmRepoProvider.groovy b/src/main/groovy/com/cloudogu/gitops/scmm/ScmmRepoProvider.groovy deleted file mode 100644 index a5d3e68ae..000000000 --- a/src/main/groovy/com/cloudogu/gitops/scmm/ScmmRepoProvider.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package com.cloudogu.gitops.scmm - -import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.utils.FileSystemUtils -import jakarta.inject.Singleton - -@Singleton -class ScmmRepoProvider { - protected final Config config - protected final FileSystemUtils fileSystemUtils - - ScmmRepoProvider(Config config, FileSystemUtils fileSystemUtils) { - this.fileSystemUtils = fileSystemUtils - this.config = config - } - - ScmmRepo getRepo(String repoTarget) { - return new ScmmRepo(config ,repoTarget, fileSystemUtils) - } - - ScmmRepo getRepo(String repoTarget, Boolean isCentralRepo) { - return new ScmmRepo(config ,repoTarget, fileSystemUtils, isCentralRepo) - } -} \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/api/ScmmApiClient.groovy b/src/main/groovy/com/cloudogu/gitops/scmm/api/ScmmApiClient.groovy deleted file mode 100644 index 811b5c5ec..000000000 --- a/src/main/groovy/com/cloudogu/gitops/scmm/api/ScmmApiClient.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package com.cloudogu.gitops.scmm.api - -import com.cloudogu.gitops.config.Config -import jakarta.inject.Named -import jakarta.inject.Singleton -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.jackson.JacksonConverterFactory - -/** - * Parent class for all SCMM Apis that lazily creates the APIs, so the latest SCMM-URL is used - */ -@Singleton -class ScmmApiClient { - Config config - OkHttpClient okHttpClient - - ScmmApiClient(Config config, @Named("scmm") OkHttpClient okHttpClient) { - this.config = config - this.okHttpClient = okHttpClient - } - - UsersApi usersApi() { - return retrofit().create(UsersApi) - } - - RepositoryApi repositoryApi() { - return retrofit().create(RepositoryApi) - } - - protected Retrofit retrofit() { - return new Retrofit.Builder() - .baseUrl(config.scmm.url + '/api/') - .client(okHttpClient) - // Converts HTTP body objects from groovy to JSON - .addConverterFactory(JacksonConverterFactory.create()) - .build() - } -} diff --git a/src/main/groovy/com/cloudogu/gitops/scmm/api/UsersApi.groovy b/src/main/groovy/com/cloudogu/gitops/scmm/api/UsersApi.groovy deleted file mode 100644 index 2120be3a9..000000000 --- a/src/main/groovy/com/cloudogu/gitops/scmm/api/UsersApi.groovy +++ /dev/null @@ -1,11 +0,0 @@ -package com.cloudogu.gitops.scmm.api - -import okhttp3.ResponseBody -import retrofit2.Call -import retrofit2.http.DELETE -import retrofit2.http.Path - -interface UsersApi { - @DELETE("v2/users/{id}") - Call delete(@Path("id") String id) -} diff --git a/src/main/groovy/com/cloudogu/gitops/utils/AirGappedUtils.groovy b/src/main/groovy/com/cloudogu/gitops/utils/AirGappedUtils.groovy index befa33700..e21a56d22 100644 --- a/src/main/groovy/com/cloudogu/gitops/utils/AirGappedUtils.groovy +++ b/src/main/groovy/com/cloudogu/gitops/utils/AirGappedUtils.groovy @@ -1,13 +1,13 @@ package com.cloudogu.gitops.utils import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.ScmmRepoProvider -import com.cloudogu.gitops.scmm.api.ScmmApiClient +import com.cloudogu.gitops.config.Config.HelmConfig +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.git.GitRepoFactory import groovy.util.logging.Slf4j import groovy.yaml.YamlSlurper import jakarta.inject.Singleton - import java.nio.file.Path @Slf4j @@ -15,37 +15,39 @@ import java.nio.file.Path class AirGappedUtils { private Config config - private ScmmRepoProvider repoProvider - private ScmmApiClient scmmApiClient + private GitRepoFactory repoProvider private FileSystemUtils fileSystemUtils private HelmClient helmClient + private GitHandler gitHandler - AirGappedUtils(Config config, ScmmRepoProvider repoProvider, ScmmApiClient scmmApiClient, - FileSystemUtils fileSystemUtils, HelmClient helmClient) { + AirGappedUtils(Config config, GitRepoFactory repoProvider, + FileSystemUtils fileSystemUtils, HelmClient helmClient, GitHandler gitHandler) { this.config = config this.repoProvider = repoProvider - this.scmmApiClient = scmmApiClient this.fileSystemUtils = fileSystemUtils this.helmClient = helmClient + this.gitHandler = gitHandler } /** * In air-gapped mode, the chart's dependencies can't be resolved. * As helm does not provide an option for changing them interactively, we push the charts into a separate repo. * We alter these repos to resolve dependencies locally from SCM. - * + * * @return the repo namespace and name */ - String mirrorHelmRepoToGit(Config.HelmConfig helmConfig) { + String mirrorHelmRepoToGit(HelmConfig helmConfig) { String repoName = helmConfig.chart - String namespace = ScmmRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES - def repoNamespaceAndName = "${namespace}/${repoName}" - def localHelmChartFolder = "${config.application.localHelmChartFolder}/${repoName}" + String namespace = GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES + String repoNamespaceAndName = "${namespace}/${repoName}" + String localHelmChartFolder = "${config.application.localHelmChartFolder}/${repoName}" validateChart(repoNamespaceAndName, localHelmChartFolder, repoName) - ScmmRepo repo = repoProvider.getRepo(repoNamespaceAndName) - repo.create("Mirror of Helm chart $repoName from ${helmConfig.repoURL}", scmmApiClient) + GitRepo repo = repoProvider.getRepo(repoNamespaceAndName, gitHandler.tenant) + + repo.createRepositoryAndSetPermission(repoNamespaceAndName, "Mirror of Helm chart $repoName from ${helmConfig.repoURL}", false) + repo.cloneRepo() repo.copyDirectoryContents(localHelmChartFolder) @@ -72,17 +74,17 @@ class AirGappedUtils { } } - private Map localizeChartYaml(ScmmRepo scmmRepo) { - log.debug("Preparing repo ${scmmRepo.scmmRepoTarget} for air-gapped use: Changing Chart.yaml to resolve depencies locally") + private Map localizeChartYaml(GitRepo gitRepo) { + log.debug("Preparing repo ${gitRepo.repoTarget} for air-gapped use: Changing Chart.yaml to resolve depencies locally") - def chartYamlPath = Path.of(scmmRepo.absoluteLocalRepoTmpDir, 'Chart.yaml') + def chartYamlPath = Path.of(gitRepo.absoluteLocalRepoTmpDir, 'Chart.yaml') Map chartYaml = new YamlSlurper().parse(chartYamlPath) as Map - Map chartLock = parseChartLockIfExists(scmmRepo) + Map chartLock = parseChartLockIfExists(gitRepo) List dependencies = chartYaml.dependencies as List ?: [] for (Map chartYamlDep : dependencies) { - resolveDependencyVersion(chartLock, chartYamlDep, scmmRepo) + resolveDependencyVersion(chartLock, chartYamlDep, gitRepo) // Remove link to external repo, to force using local one chartYamlDep.repository = '' @@ -91,7 +93,7 @@ class AirGappedUtils { return chartYaml } - private static Map parseChartLockIfExists(ScmmRepo scmmRepo) { + private static Map parseChartLockIfExists(GitRepo scmmRepo) { def chartLock = Path.of(scmmRepo.absoluteLocalRepoTmpDir, 'Chart.lock') if (!chartLock.toFile().exists()) { return [:] @@ -102,13 +104,13 @@ class AirGappedUtils { /** * Resolve proper dependency version from Chart.lock, e.g. 5.18.* -> 5.18.1 */ - private void resolveDependencyVersion(Map chartLock, Map chartYamlDep, ScmmRepo scmmRepo) { + private void resolveDependencyVersion(Map chartLock, Map chartYamlDep, GitRepo gitRepo) { def chartLockDep = findByName(chartLock.dependencies as List, chartYamlDep.name as String) if (chartLockDep) { chartYamlDep.version = chartLockDep.version } else if ((chartYamlDep.version as String).contains('*')) { throw new RuntimeException("Unable to determine proper version for dependency " + - "${chartYamlDep.name} (version: ${chartYamlDep.version}) from repo ${scmmRepo.scmmRepoTarget}") + "${chartYamlDep.name} (version: ${chartYamlDep.version}) from repo ${gitRepo.repoTarget}") } } diff --git a/src/main/groovy/com/cloudogu/gitops/utils/K8sClient.groovy b/src/main/groovy/com/cloudogu/gitops/utils/K8sClient.groovy index c31220526..ac7cd3cbd 100644 --- a/src/main/groovy/com/cloudogu/gitops/utils/K8sClient.groovy +++ b/src/main/groovy/com/cloudogu/gitops/utils/K8sClient.groovy @@ -193,7 +193,7 @@ class K8sClient { } String getArgoCDNamespacesSecret(String name, String namespace = '') { - String[] command = ["kubectl", "get", 'secret',name,"-n${namespace}", '-ojsonpath={.data.namespaces}'] + String[] command = ["kubectl", "get", 'secret', name, "-n", "${namespace}", '-ojsonpath={.data.namespaces}'] String output = waitForOutput( command, "Getting Secret from Cluster", diff --git a/src/main/groovy/com/cloudogu/gitops/utils/NetworkingUtils.groovy b/src/main/groovy/com/cloudogu/gitops/utils/NetworkingUtils.groovy index 1fb624a38..d04426b6f 100644 --- a/src/main/groovy/com/cloudogu/gitops/utils/NetworkingUtils.groovy +++ b/src/main/groovy/com/cloudogu/gitops/utils/NetworkingUtils.groovy @@ -34,7 +34,7 @@ class NetworkingUtils { log.debug("Local address: " + localAddress) log.debug("Cluster address: " + potentialClusterBindAddress) - if(!potentialClusterBindAddress) { + if (!potentialClusterBindAddress) { throw new RuntimeException("Could not connect to kubernetes cluster: no cluster bind address") } @@ -57,7 +57,7 @@ class NetworkingUtils { */ String getLocalAddress() { try { - List sortedInterfaces = + List sortedInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()).sort { it.index } for (NetworkInterface anInterface : sortedInterfaces) { @@ -103,4 +103,4 @@ class NetworkingUtils { return "http" return '' } -} +} \ No newline at end of file diff --git a/src/main/resources/proxy-config.json b/src/main/resources/proxy-config.json index b67a47767..b87283c63 100644 --- a/src/main/resources/proxy-config.json +++ b/src/main/resources/proxy-config.json @@ -3,8 +3,7 @@ ["java.util.function.Function"], ["java.util.function.Predicate"], ["java.util.function.Supplier"], - ["com.cloudogu.gitops.scmm.api.UsersApi"], - ["com.cloudogu.gitops.scmm.api.RepositoryApi"], + ["com.cloudogu.gitops.git.providers.scmmanager.apiUsersApi"], + ["com.cloudogu.gitops.git.providers.scmmanager.api.RepositoryApi"], ["java.io.FileFilter"] - ] diff --git a/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy b/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy index e2a603a95..7662c6fb0 100644 --- a/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy @@ -2,6 +2,7 @@ package com.cloudogu.gitops import com.cloudogu.gitops.config.ApplicationConfigurator import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.features.git.config.ScmTenantSchema import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.TestLogger import org.junit.jupiter.api.BeforeEach @@ -24,37 +25,44 @@ class ApplicationConfiguratorTest { private TestLogger testLogger Config testConfig = Config.fromMap([ application: [ - localHelmChartFolder : 'someValue', - namePrefix : '' + localHelmChartFolder: 'someValue', + namePrefix : '' ], registry : [ - url : EXPECTED_REGISTRY_URL, - proxyUrl: "proxy-$EXPECTED_REGISTRY_URL", + url : EXPECTED_REGISTRY_URL, + proxyUrl : "proxy-$EXPECTED_REGISTRY_URL", proxyUsername: "proxy-user", proxyPassword: "proxy-pw", - internalPort: EXPECTED_REGISTRY_INTERNAL_PORT, + internalPort : EXPECTED_REGISTRY_INTERNAL_PORT, ], jenkins : [ - url : EXPECTED_JENKINS_URL + url: EXPECTED_JENKINS_URL ], - scmm : [ - url : EXPECTED_SCMM_URL, + scm : [ + scmManager: [ + url: EXPECTED_SCMM_URL + ], + ], + multiTenant: [ + scmManager: [ + url: '' + ] ], - features : [ - secrets : [ - vault : [ - mode : EXPECTED_VAULT_MODE + features : [ + secrets: [ + vault: [ + mode: EXPECTED_VAULT_MODE ] ], ] ]) - // We have to set this value using env vars, which makes tests complicated, so ignore it - Config almostEmptyConfig = Config.fromMap([ - application: [ - localHelmChartFolder : 'someValue', - ], - ]) +// // We have to set this value using env vars, which makes tests complicated, so ignore it +// Config almostEmptyConfig = Config.fromMap([ +// application: [ +// localHelmChartFolder: 'someValue', +// ], +// ]) @BeforeEach void setup() { @@ -83,28 +91,28 @@ class ApplicationConfiguratorTest { assertThat(actualConfig.application.runningInsideK8s).isEqualTo(true) } } - + @Test void 'Sets jenkins active if external url is set'() { testConfig.jenkins.url = 'external' def actualConfig = applicationConfigurator.initConfig(testConfig) assertThat(actualConfig.jenkins.active).isEqualTo(true) } - + @Test void 'Leaves Jenkins urlForScmm empty, if not active'() { testConfig.jenkins.url = '' testConfig.jenkins.active = false - + def actualConfig = applicationConfigurator.initConfig(testConfig) - assertThat(actualConfig.jenkins.urlForScmm).isEmpty() + assertThat(actualConfig.jenkins.urlForScm).isEmpty() } - + @Test void 'Fails if jenkins is external and scmm is internal or the other way round'() { testConfig.jenkins.active = true testConfig.jenkins.url = 'external' - testConfig.scmm.url = '' + testConfig.scm.scmManager.url = '' def exception = shouldFail(RuntimeException) { applicationConfigurator.validateConfig(testConfig) @@ -112,14 +120,14 @@ class ApplicationConfiguratorTest { assertThat(exception.message).isEqualTo('When setting jenkins URL, scmm URL must also be set and the other way round') testConfig.jenkins.url = '' - testConfig.scmm.url = 'external' + testConfig.scm.scmManager.url = 'external' exception = shouldFail(RuntimeException) { applicationConfigurator.validateConfig(testConfig) } assertThat(exception.message).isEqualTo('When setting jenkins URL, scmm URL must also be set and the other way round') - - + + testConfig.jenkins.active = false applicationConfigurator.validateConfig(testConfig) // no exception when jenkins is not active @@ -157,8 +165,8 @@ class ApplicationConfiguratorTest { applicationConfigurator.validateConfig(testConfig) } assertThat(exception.message).isEqualTo('content.repos requires a url parameter.') - - + + testConfig.content.repos = [ new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.COPY, target: "missing_slash"), ] @@ -178,7 +186,7 @@ class ApplicationConfiguratorTest { } assertThat(exception.message).isEqualTo('content.repos.type COPY requires content.repos.target to be set. Repo: abc') } - + @Test void 'Fails if FOLDER_BASED repo has target parameter'() { testConfig.content.repos = [ @@ -188,8 +196,8 @@ class ApplicationConfiguratorTest { applicationConfigurator.validateConfig(testConfig) } assertThat(exception.message).isEqualTo('content.repos.type FOLDER_BASED does not support target parameter. Repo: abc') - - + + testConfig.content.repos = [ new Config.ContentSchema.ContentRepositorySchema(url: 'abc', type: Config.ContentRepoType.FOLDER_BASED, targetRef: 'someRef'), ] @@ -231,29 +239,6 @@ class ApplicationConfiguratorTest { assertThat(exception.message).isEqualTo('content.repos.type MIRROR does not support templating. Repo: abc') } - @Test - void 'Adds content example namespaces'() { - testConfig.content.examples = true - - def actualConfig = applicationConfigurator.initConfig(testConfig) - - assertThat(actualConfig.content.namespaces).containsExactlyInAnyOrder('example-apps-staging', 'example-apps-production') - } - - - @Test - void 'Fails if example Content is active but registry is not active'() { - testConfig.content.examples = true - testConfig.registry.internal = false - testConfig.registry.url = '' - - - def exception = shouldFail(RuntimeException) { - applicationConfigurator.initConfig(testConfig) - } - assertThat(exception.message).isEqualTo('content.examples requires either registry.active or registry.url') - } - @Test void 'Ignores empty localHemlChartFolder, if mirrorRepos is not set'() { testConfig.application.mirrorRepos = false @@ -265,20 +250,12 @@ class ApplicationConfiguratorTest { @Test void "Certain properties are read from env"() { - withEnvironmentVariable('SPRING_BOOT_HELM_CHART_REPO', 'value1').execute { - def actualConfig = new ApplicationConfigurator(fileSystemUtils).initConfig(new Config()) - assertThat(actualConfig.repositories.springBootHelmChart.url).isEqualTo('value1') - } - withEnvironmentVariable('SPRING_PETCLINIC_REPO', 'value2').execute { - def actualConfig = new ApplicationConfigurator(fileSystemUtils).initConfig(new Config()) - assertThat(actualConfig.repositories.springPetclinic.url).isEqualTo('value2') - } withEnvironmentVariable('GITOPS_BUILD_LIB_REPO', 'value3').execute { - def actualConfig = new ApplicationConfigurator(fileSystemUtils).initConfig(new Config()) + def actualConfig = new ApplicationConfigurator(fileSystemUtils).initConfig(minimalConfig()) assertThat(actualConfig.repositories.gitopsBuildLib.url).isEqualTo('value3') } withEnvironmentVariable('CES_BUILD_LIB_REPO', 'value4').execute { - def actualConfig = new ApplicationConfigurator(fileSystemUtils).initConfig(new Config()) + def actualConfig = new ApplicationConfigurator(fileSystemUtils).initConfig(minimalConfig()) assertThat(actualConfig.repositories.cesBuildLib.url).isEqualTo('value4') } } @@ -298,9 +275,7 @@ class ApplicationConfiguratorTest { assertThat(actualConfig.features.mail.mailhogUrl).isEqualTo("http://mailhog.localhost") assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("http://grafana.localhost") assertThat(actualConfig.features.secrets.vault.url).isEqualTo("http://vault.localhost") - assertThat(actualConfig.features.exampleApps.petclinic.baseDomain).isEqualTo("petclinic.localhost") - assertThat(actualConfig.features.exampleApps.nginx.baseDomain).isEqualTo("nginx.localhost") - assertThat(actualConfig.scmm.ingress).isEqualTo("scmm.localhost") + assertThat(actualConfig.scm.scmManager.ingress).isEqualTo("scmm.localhost") assertThat(actualConfig.jenkins.ingress).isEqualTo("jenkins.localhost") } @@ -320,9 +295,7 @@ class ApplicationConfiguratorTest { assertThat(actualConfig.features.mail.mailhogUrl).isEqualTo("http://mailhog-localhost") assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("http://grafana-localhost") assertThat(actualConfig.features.secrets.vault.url).isEqualTo("http://vault-localhost") - assertThat(actualConfig.features.exampleApps.petclinic.baseDomain).isEqualTo("petclinic-localhost") - assertThat(actualConfig.features.exampleApps.nginx.baseDomain).isEqualTo("nginx-localhost") - assertThat(actualConfig.scmm.ingress).isEqualTo("scmm-localhost") + assertThat(actualConfig.scm.scmManager.ingress).isEqualTo("scmm-localhost") assertThat(actualConfig.jenkins.ingress).isEqualTo("jenkins-localhost") } @@ -377,8 +350,6 @@ class ApplicationConfiguratorTest { testConfig.features.mail.mailhogUrl = 'mailhog' testConfig.features.monitoring.grafanaUrl = 'grafana' testConfig.features.secrets.vault.url = 'vault' - testConfig.features.exampleApps.petclinic.baseDomain = 'petclinic' - testConfig.features.exampleApps.nginx.baseDomain = 'nginx' def actualConfig = applicationConfigurator.initConfig(testConfig) @@ -386,8 +357,6 @@ class ApplicationConfiguratorTest { assertThat(actualConfig.features.mail.mailhogUrl).isEqualTo("mailhog") assertThat(actualConfig.features.monitoring.grafanaUrl).isEqualTo("grafana") assertThat(actualConfig.features.secrets.vault.url).isEqualTo("vault") - assertThat(actualConfig.features.exampleApps.petclinic.baseDomain).isEqualTo("petclinic") - assertThat(actualConfig.features.exampleApps.nginx.baseDomain).isEqualTo("nginx") } @Test @@ -447,7 +416,7 @@ class ApplicationConfiguratorTest { testConfig.features.argocd.operator = true testConfig.features.argocd.resourceInclusionsCluster = 'https://100.125.0.1:443' testConfig.features.argocd.env = [ - [name: "ENV_VAR_1", value: "value1"] , + [name: "ENV_VAR_1", value: "value1"], [name: "ENV_VAR_2", value: "value2"] ] as List> @@ -581,13 +550,12 @@ class ApplicationConfiguratorTest { } } - @Test - void "MultiTenant Mode Central SCM Url"(){ - testConfig.multiTenant.centralScmUrl="scmm.localhost/scm" - testConfig.application.namePrefix="foo" + void "MultiTenant Mode Central SCM Url"() { + testConfig.multiTenant.scmManager.url = "scmm.localhost/scm" + testConfig.application.namePrefix = "foo" applicationConfigurator.initConfig(testConfig) - assertThat(testConfig.multiTenant.centralScmUrl).toString() == "scmm.localhost/scm/" + assertThat(testConfig.multiTenant.scmManager.url).toString() == "scmm.localhost/scm/" } @Test @@ -662,4 +630,18 @@ class ApplicationConfiguratorTest { } return keysList } + + private static Config minimalConfig() { + def config = new Config() + config.application = new Config.ApplicationSchema( + localHelmChartFolder: 'someValue', + namePrefix: '' + ) + config.scm = new ScmTenantSchema( + scmManager: new ScmTenantSchema.ScmManagerTenantConfig( + url: '' + ) + ) + return config + } } \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy b/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy index 532c84447..14b34afe3 100644 --- a/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy @@ -5,12 +5,10 @@ import io.micronaut.context.ApplicationContext import org.junit.jupiter.api.Test import static org.assertj.core.api.Assertions.assertThat -import static com.cloudogu.gitops.config.Config.* class ApplicationTest { - Config config = new Config( - scmm: new ScmmSchema(url: 'http://localhost')) + Config config = new Config() @Test void 'feature\'s ordering is correct'() { @@ -18,8 +16,8 @@ class ApplicationTest { .registerSingleton(config) .getBean(Application) def features = application.features.collect { it.class.simpleName } - - assertThat(features).isEqualTo(["Registry", "ScmManager", "Jenkins", "ArgoCD", "IngressNginx", "CertManager", "Mailhog", "PrometheusStack", "ExternalSecretsOperator", "Vault", "Content"]) + + assertThat(features).isEqualTo(["Registry", "ScmManagerSetup", "GitHandler" ,"Jenkins", "ArgoCD", "IngressNginx", "CertManager", "Mailhog", "PrometheusStack", "ExternalSecretsOperator", "Vault", "ContentLoader"]) } @Test @@ -51,7 +49,7 @@ class ApplicationTest { application.setNamespaceListToConfig(config) assertThat(config.application.namespaces.getActiveNamespaces()).containsExactlyInAnyOrderElementsOf(namespaceList) } - + @Test void 'get active namespaces correctly in Openshift'() { config.registry.active = true @@ -98,7 +96,7 @@ class ApplicationTest { "example-apps-production", ]) } - + @Test void 'handles empty content namespaces'() { def application = ApplicationContext.run() diff --git a/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScriptedTest.groovy b/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScriptedTest.groovy index 34c9a54c6..043e4211d 100644 --- a/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScriptedTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScriptedTest.groovy @@ -5,6 +5,8 @@ import com.cloudogu.gitops.Feature import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.destroy.Destroyer import com.cloudogu.gitops.destroy.DestructionHandler +import com.cloudogu.gitops.features.git.config.ScmTenantSchema +import com.cloudogu.gitops.features.git.config.ScmTenantSchema.ScmManagerTenantConfig import io.github.classgraph.ClassGraph import io.github.classgraph.ClassInfo import io.micronaut.context.ApplicationContext @@ -27,7 +29,8 @@ class GitopsPlaygroundCliMainScriptedTest { GitopsPlaygroundCliScriptedForTest gitopsPlaygroundCliScripted = new GitopsPlaygroundCliScriptedForTest() Config config = new Config( jenkins: new JenkinsSchema(url: 'http://jenkins'), - scmm: new ScmmSchema(url: 'http://scmm') + scm: new ScmTenantSchema( + scmManager: new ScmManagerTenantConfig(url: 'http://scmm')) ) /** @@ -80,6 +83,14 @@ class GitopsPlaygroundCliMainScriptedTest { if (classInfo.extendsSuperclass(parentClass) || (parentIsInterface && classInfo.implementsInterface(parentClass))) { + + // ignore test classes + String location = classInfo.loadClass().protectionDomain?.codeSource?.location?.path ?: "" + if (location == null) location = "" + if (location =~ /[\\/]test-classes[\\/]/ || location =~ /[\\/]classes[\\/]java[\\/]test[\\/]/) { + return + } + def orderAnnotation = classInfo.getAnnotationInfo(Order) if (orderAnnotation) { def orderValue = orderAnnotation.getParameterValues().getValue('value') as int @@ -103,4 +114,4 @@ class GitopsPlaygroundCliMainScriptedTest { applicationContext = super.createApplicationContext() } } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliTest.groovy b/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliTest.groovy index 4c051360b..d8b04ce15 100644 --- a/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliTest.groovy @@ -263,39 +263,40 @@ class GitopsPlaygroundCliTest { @Test void 'ensure helm defaults are used, if not set'() { - // this test sets only a few values for helm configuration and expect, that defaults are used. + // this test sets only a few values for helm configuration and expect, that defaults are used. def fileConfig = [ - jenkins: [ + jenkins : [ helm: [ - version: '5.8.1' + version: '5.8.1' ] ], - scmm: [ - helm: [ - values: [ - initialDelaySeconds: 120 - ] + scm : [ + scmManager: [ + helm: [ + values: [ + initialDelaySeconds: 120 + ] + ] ] - ] - , + ], features: [ - monitoring: [ + monitoring : [ helm: [ - version: '66.2.1', + version : '66.2.1', grafanaImage: 'localhost:30000/proxy/grafana:latest' ] ], - secrets: [ + secrets : [ externalSecrets: [ - helm: [ - chart: 'my-secrets' - ] + helm: [ + chart: 'my-secrets' + ] ], - vault: [ - helm: [ - repoURL: 'localhost:3000/proxy/vault:latest' - ] + vault : [ + helm: [ + repoURL: 'localhost:3000/proxy/vault:latest' + ] ], ], certManager: [ @@ -311,17 +312,16 @@ class GitopsPlaygroundCliTest { configFile.text = toYaml(fileConfig) - cli.run("--config-file=${configFile}","--yes") + cli.run("--config-file=${configFile}", "--yes") def myconfig = cli.lastSchema; assertThat(myconfig.jenkins.helm.chart).isEqualTo('jenkins') assertThat(myconfig.jenkins.helm.repoURL).isEqualTo('https://charts.jenkins.io') assertThat(myconfig.jenkins.helm.version).isEqualTo('5.8.1') // overridden - - assertThat(myconfig.scmm.helm.chart).isEqualTo('scm-manager') - assertThat(myconfig.scmm.helm.repoURL).isEqualTo('https://packages.scm-manager.org/repository/helm-v2-releases/') - assertThat(myconfig.scmm.helm.version).isEqualTo('3.11.0') - assertThat(myconfig.scmm.helm.values.initialDelaySeconds).isEqualTo(120) // overridden + assertThat(myconfig.scm.scmManager.helm.chart).isEqualTo('scm-manager') + assertThat(myconfig.scm.scmManager.helm.repoURL).isEqualTo('https://packages.scm-manager.org/repository/helm-v2-releases/') + assertThat(myconfig.scm.scmManager.helm.version).isEqualTo('3.11.0') + assertThat(myconfig.scm.scmManager.helm.values.initialDelaySeconds).isEqualTo(120) // overridden assertThat(cli.lastSchema.features.monitoring.helm.chart).isEqualTo('kube-prometheus-stack') assertThat(cli.lastSchema.features.monitoring.helm.repoURL).isEqualTo('https://prometheus-community.github.io/helm-charts') @@ -349,6 +349,7 @@ class GitopsPlaygroundCliTest { assertThat(cli.lastSchema.features.certManager.helm.acmeSolverImage).isEqualTo('') assertThat(cli.lastSchema.features.certManager.helm.image).isEqualTo('localhost:30000/proxy/cert-manager-controller:latest') } + static String getLoggingPattern() { loggingEncoder.pattern } diff --git a/src/test/groovy/com/cloudogu/gitops/destroy/DestroyerDependencyInjectionTest.groovy b/src/test/groovy/com/cloudogu/gitops/destroy/DestroyerDependencyInjectionTest.groovy index 925533522..40ccb59c1 100644 --- a/src/test/groovy/com/cloudogu/gitops/destroy/DestroyerDependencyInjectionTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/destroy/DestroyerDependencyInjectionTest.groovy @@ -1,7 +1,6 @@ package com.cloudogu.gitops.destroy import com.cloudogu.gitops.config.Config - import io.micronaut.context.ApplicationContext import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test @@ -10,14 +9,16 @@ class DestroyerDependencyInjectionTest { @Test void 'can create bean'() { def destroyer = ApplicationContext.run() - .registerSingleton(Config.fromMap( [ - scmm: [ - url: 'http://localhost:9091/scm', - username: 'admin', - password: 'admin', + .registerSingleton(Config.fromMap([ + scm : [ + scmManager: [ + url : 'http://localhost:9091/scm', + username: 'admin', + password: 'admin' + ] ], - jenkins: [ - url: 'http://localhost:9090', + jenkins : [ + url : 'http://localhost:9090', username: 'admin', password: 'admin', ], diff --git a/src/test/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandlerTest.groovy b/src/test/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandlerTest.groovy deleted file mode 100644 index 6cd5becc5..000000000 --- a/src/test/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandlerTest.groovy +++ /dev/null @@ -1,59 +0,0 @@ -package com.cloudogu.gitops.destroy - -import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.api.ScmmApiClient -import okhttp3.OkHttpClient -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.mockito.Mockito -import retrofit2.Retrofit - -import static org.assertj.core.api.Assertions.assertThat - -class ScmmDestructionHandlerTest { - - private ScmmApiClient scmmApiClient - private MockWebServer mockWebServer - - @BeforeEach - void setUp() { - mockWebServer = new MockWebServer() - def retrofit = new Retrofit.Builder() - .baseUrl(mockWebServer.url("")) - .build() - scmmApiClient = new ScmmApiClient(Mockito.mock(Config), Mockito.mock(OkHttpClient)) { - @Override - protected Retrofit retrofit() { - return retrofit - } - } - } - - @AfterEach - void tearDown() { - mockWebServer.shutdown() - } - - @Test - void 'destroys all'() { - def destructionHandler = new ScmmDestructionHandler(Config.fromMap( [application: [namePrefix: 'foo-']]), scmmApiClient) - - for (def i = 0; i < 1 /* user */ + 13 /* repositories */; i++) { - mockWebServer.enqueue(new MockResponse().setResponseCode(204)) - } - destructionHandler.destroy() - - def request = mockWebServer.takeRequest() - assertThat(request.requestUrl.encodedPath()).isEqualTo("/v2/users/foo-gitops") - assertThat(request.method).isEqualTo("DELETE") - - for (def i = 0; i < 13; ++i) { - request = mockWebServer.takeRequest() - assertThat(request.method).isEqualTo("DELETE") - assertThat(request.requestUrl.encodedPath()).matches(~/^\/v2\/repositories\/.*\/.*$/) - } - } -} diff --git a/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy index e8aa75d5c..5aa7106a4 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy @@ -1,31 +1,38 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config - import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.providers.GitProvider import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClientForTest import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension + import java.nio.file.Files import java.nio.file.Path import static org.assertj.core.api.Assertions.assertThat import static org.mockito.ArgumentMatchers.any -import static org.mockito.Mockito.* +import static org.mockito.Mockito.verify +import static org.mockito.Mockito.when +@ExtendWith(MockitoExtension.class) class CertManagerTest { String chartVersion = "1.16.1" Config config = Config.fromMap([ - features : [ + features: [ certManager: [ active: true, helm : [ - chart : 'cert-manager', - repoURL : 'https://charts.jetstack.io', - version : chartVersion, + chart : 'cert-manager', + repoURL: 'https://charts.jetstack.io', + version: chartVersion, ], ], ], @@ -33,8 +40,15 @@ class CertManagerTest { Path temporaryYamlFile FileSystemUtils fileSystemUtils = new FileSystemUtils() - DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) - AirGappedUtils airGappedUtils = mock(AirGappedUtils) + + @Mock + DeploymentStrategy deploymentStrategy + @Mock + AirGappedUtils airGappedUtils + @Mock + GitHandler gitHandler + @Mock + GitProvider gitProvider @Test void 'Helm release is installed'() { @@ -65,6 +79,9 @@ class CertManagerTest { @Test void 'helm release is installed in air-gapped mode'() { + when(gitHandler.getResourcesScm()).thenReturn(gitProvider) + when(gitProvider.repoUrl(any())).thenReturn("http://scmm.scm-manager.svc.cluster.local/scm/repo/a/b") + config.application.mirrorRepos = true when(airGappedUtils.mirrorHelmRepoToGit(any(Config.HelmConfig))).thenReturn('a/b') @@ -94,6 +111,8 @@ class CertManagerTest { @Test void 'check images are overriddes'() { + when(gitHandler.getResourcesScm()).thenReturn(gitProvider) + when(gitProvider.repoUrl(any())).thenReturn("http://test") // Prep config.application.mirrorRepos = true @@ -143,7 +162,7 @@ class CertManagerTest { temporaryYamlFile = Path.of(ret.toString().replace(".ftl", "")) return ret } - }, deploymentStrategy, new K8sClientForTest(config), airGappedUtils) + }, deploymentStrategy, new K8sClientForTest(config), airGappedUtils, gitHandler) } private Map parseActualYaml() { diff --git a/src/test/groovy/com/cloudogu/gitops/features/ContentTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/ContentLoaderTest.groovy similarity index 93% rename from src/test/groovy/com/cloudogu/gitops/features/ContentTest.groovy rename to src/test/groovy/com/cloudogu/gitops/features/ContentLoaderTest.groovy index f705ea56a..6c148ff2e 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/ContentTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/ContentLoaderTest.groovy @@ -1,8 +1,12 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepoProvider -import com.cloudogu.gitops.scmm.api.ScmmApiClient +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepoFactory +import com.cloudogu.gitops.utils.git.GitHandlerForTests +import com.cloudogu.gitops.utils.git.TestGitRepoFactory +import com.cloudogu.gitops.utils.git.ScmManagerMock +import com.cloudogu.gitops.utils.git.TestScmManagerApiClient import com.cloudogu.gitops.utils.* import groovy.util.logging.Slf4j import groovy.yaml.YamlSlurper @@ -17,9 +21,10 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import org.mockito.ArgumentCaptor -import static com.cloudogu.gitops.config.Config.* +import static ContentLoader.RepoCoordinate +import static com.cloudogu.gitops.config.Config.ContentRepoType import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema -import static com.cloudogu.gitops.features.Content.RepoCoordinate +import static com.cloudogu.gitops.config.Config.OverwriteMode import static groovy.test.GroovyAssert.shouldFail import static org.assertj.core.api.Assertions.assertThat import static org.mockito.ArgumentMatchers.any @@ -27,25 +32,36 @@ import static org.mockito.ArgumentMatchers.eq import static org.mockito.Mockito.* @Slf4j -class ContentTest { +class ContentLoaderTest { // bareRepo static List foldersToDelete = new ArrayList() - Config config = new Config( - application: new ApplicationSchema( - namePrefix: 'foo-'), - registry: new RegistrySchema( - url: 'reg-url', - path: 'reg-path', - username: 'reg-user', - password: 'reg-pw', - createImagePullSecrets: false,)) + Config config = new Config([ + application: [ + namePrefix: 'foo-' + ], + scm : [ + scmManager: [ + url: '' + ] + ], + registry : [ + url : 'reg-url', + path : 'reg-path', + username : 'reg-user', + password : 'reg-pw', + createImagePullSecrets: false + ] + ]) + CommandExecutorForTest k8sCommands = new CommandExecutorForTest() K8sClientForTest k8sClient = new K8sClientForTest(config, k8sCommands) - TestScmmRepoProvider scmmRepoProvider = new TestScmmRepoProvider(config, new FileSystemUtils()) - TestScmmApiClient scmmApiClient = new TestScmmApiClient(config) + TestGitRepoFactory scmmRepoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) + TestScmManagerApiClient scmmApiClient = new TestScmManagerApiClient(config) Jenkins jenkins = mock(Jenkins.class) + ScmManagerMock scmManagerMock = new ScmManagerMock() + GitHandler gitHandler = new GitHandlerForTests(config, scmManagerMock) @TempDir File tmpDir @@ -149,7 +165,7 @@ class ContentTest { config.content.repos = contentRepos - def repos =createContent().cloneContentRepos() + def repos = createContent().cloneContentRepos() expectedTargetRepos.each { expected -> assertThat(new File(findRoot(repos), "${expected.namespace}/${expected.repoName}/file")).exists().isFile() @@ -172,13 +188,12 @@ class ContentTest { @Test void 'supports content variables'() { - config.content.repos = [ new ContentRepositorySchema(url: createContentRepo('folderBasedRepo1'), type: ContentRepoType.FOLDER_BASED, templating: true) ] config.content.variables.someapp = [somevalue: 'this is a custom variable'] - def repos =createContent().cloneContentRepos() + def repos = createContent().cloneContentRepos() // Assert Templating assertThat(new File(findRoot(repos), "common/repo/some.yaml")).exists() @@ -212,7 +227,7 @@ class ContentTest { new ContentRepositorySchema(url: createContentRepo('', 'git-repository-with-branches-tags'), ref: 'someBranch', type: ContentRepoType.COPY, target: 'common/branch') ] - def repos =createContent().cloneContentRepos() + def repos = createContent().cloneContentRepos() assertThat(new File(findRoot(repos), "common/tag/README.md")).exists().isFile() assertThat(new File(findRoot(repos), "common/tag/README.md").text).contains("someTag") @@ -230,7 +245,7 @@ class ContentTest { new ContentRepositorySchema(url: createContentRepo('', 'git-repo-different-default-branch'), target: 'common/default', type: ContentRepoType.COPY), ] - def repos =createContent().cloneContentRepos() + def repos = createContent().cloneContentRepos() assertThat(new File(findRoot(repos), "common/default/README.md")).exists().isFile() assertThat(new File(findRoot(repos), "common/default/README.md").text).contains("different") @@ -244,7 +259,7 @@ class ContentTest { ] def exception = shouldFail(RuntimeException) { - createContent().cloneContentRepos() + createContent().cloneContentRepos() } assertThat(exception.message).startsWith("Reference 'does/not/exist' not found in content repository") } @@ -259,7 +274,7 @@ class ContentTest { new ContentRepositorySchema(url: createContentRepo('copyRepo1'), ref: 'main', type: ContentRepoType.COPY, target: 'common/repo'), ] - def repos =createContent().cloneContentRepos() + def repos = createContent().cloneContentRepos() assertThat(new File(findRoot(repos), "common/repo/file").text).contains("copyRepo1") // Last repo "wins" @@ -566,19 +581,18 @@ class ContentTest { new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath') ] - scmmApiClient.mockRepoApiBehaviour() - + def expectedRepo = 'common/repo' + scmManagerMock.initOnceRepo('common/repo') createContent().install() - def expectedRepo = 'common/repo' - def repo = scmmRepoProvider.getRepo(expectedRepo) + def repo = scmmRepoProvider.getRepo(expectedRepo, scmManagerMock) - def url = repo.getGitRepositoryUrl() + String url = repo.getGitRepositoryUrl() // clone repo, to ensure, changes in remote repo. try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { - verify(repo).create(eq(''), any(ScmmApiClient), eq(false)) + verify(repo).createRepositoryAndSetPermission(eq(expectedRepo), any(String.class), eq(false)) def commitMsg = git.log().call().iterator().next().getFullMessage() assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) @@ -597,6 +611,7 @@ class ContentTest { ] createContent().install() + scmManagerMock.clearInitOnce() def folderAfterReset = File.createTempDir('second-cloned-repo') folderAfterReset.deleteOnExit() @@ -611,6 +626,7 @@ class ContentTest { } + } @Test @@ -632,14 +648,14 @@ class ContentTest { createContent().install() def expectedRepo = 'common/repo' - def repo = scmmRepoProvider.getRepo(expectedRepo) + def repo = scmmRepoProvider.getRepo(expectedRepo, new ScmManagerMock()) def url = repo.getGitRepositoryUrl() // clone repo, to ensure, changes in remote repo. try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { - verify(repo).create(eq(''), any(ScmmApiClient), eq(false)) + verify(repo).createRepositoryAndSetPermission(eq(expectedRepo), any(String.class), eq(false)) def commitMsg = git.log().call().iterator().next().getFullMessage() assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) @@ -689,18 +705,17 @@ class ContentTest { new ContentRepositorySchema(url: createContentRepo('copyRepo2'), type: ContentRepoType.COPY, target: 'common/repo', path: 'subPath') ] - scmmApiClient.mockRepoApiBehaviour() - + def expectedRepo = 'common/repo' + scmManagerMock.initOnceRepo('common/repo') createContent().install() - def expectedRepo = 'common/repo' - def repo = scmmRepoProvider.getRepo(expectedRepo) + def repo = scmmRepoProvider.getRepo(expectedRepo, scmManagerMock) def url = repo.getGitRepositoryUrl() // clone repo, to ensure, changes in remote repo. try (def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(tmpDir).call()) { - verify(repo).create(eq(''), any(ScmmApiClient), eq(false)) + verify(repo).createRepositoryAndSetPermission(eq(expectedRepo), any(String.class), eq(false)) def commitMsg = git.log().call().iterator().next().getFullMessage() assertThat(commitMsg).isEqualTo("Initialize content repo ${expectedRepo}".toString()) @@ -720,6 +735,7 @@ class ContentTest { ] createContent().install() + scmManagerMock.clearInitOnce() def folderAfterReset = File.createTempDir('second-cloned-repo') folderAfterReset.deleteOnExit() @@ -733,6 +749,7 @@ class ContentTest { assertThat(new File(folderAfterReset, "copyRepo2").exists()).isTrue() } + } @Test @@ -851,8 +868,8 @@ class ContentTest { } } - private ContentForTest createContent() { - new ContentForTest(config, k8sClient, scmmRepoProvider, scmmApiClient, jenkins) + private ContentLoaderForTest createContent() { + new ContentLoaderForTest(config, k8sClient, scmmRepoProvider, jenkins, gitHandler) } private static parseActualYaml(File pathToYamlFile) { @@ -867,7 +884,7 @@ class ContentTest { } Git cloneRepo(String expectedRepo, File repoFolder) { - def repo = scmmRepoProvider.getRepo(expectedRepo) + def repo = scmmRepoProvider.getRepo(expectedRepo, new ScmManagerMock()) def url = repo.getGitRepositoryUrl() def git = Git.cloneRepository().setURI(url).setBranch('main').setDirectory(repoFolder).call() @@ -905,11 +922,12 @@ class ContentTest { assertThat(new File(repoFolder, "README.md").text).contains(expectedReadmeContent) } } - class ContentForTest extends Content { + + class ContentLoaderForTest extends ContentLoader { CloneCommand cloneSpy - ContentForTest(Config config, K8sClient k8sClient, ScmmRepoProvider repoProvider, ScmmApiClient scmmApiClient, Jenkins jenkins) { - super(config, k8sClient, repoProvider, scmmApiClient, jenkins) + ContentLoaderForTest(Config config, K8sClient k8sClient, GitRepoFactory repoProvider, Jenkins jenkins, GitHandler gitHandler) { + super(config, k8sClient, repoProvider, jenkins, gitHandler) } @Override diff --git a/src/test/groovy/com/cloudogu/gitops/features/ExternalSecretsOperatorTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/ExternalSecretsOperatorTest.groovy index 59090d0bb..352320475 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/ExternalSecretsOperatorTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/ExternalSecretsOperatorTest.groovy @@ -1,15 +1,20 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config - import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.providers.GitProvider import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.CommandExecutorForTest import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClientForTest import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension + import java.nio.file.Files import java.nio.file.Path @@ -17,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat import static org.mockito.ArgumentMatchers.any import static org.mockito.Mockito.* +@ExtendWith(MockitoExtension.class) class ExternalSecretsOperatorTest { Config config = new Config( @@ -27,11 +33,18 @@ class ExternalSecretsOperatorTest { CommandExecutorForTest commandExecutor = new CommandExecutorForTest() K8sClientForTest k8sClient = new K8sClientForTest(config) - DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) - AirGappedUtils airGappedUtils = mock(AirGappedUtils) FileSystemUtils fileSystemUtils = new FileSystemUtils() Path temporaryYamlFile + @Mock + DeploymentStrategy deploymentStrategy + @Mock + AirGappedUtils airGappedUtils + @Mock + GitHandler gitHandler + @Mock + GitProvider gitProvider + @Test void "is disabled via active flag"() { config.features.secrets.active = false @@ -72,7 +85,7 @@ class ExternalSecretsOperatorTest { @Test void 'helm release is installed with custom images'() { - config.features.secrets.externalSecrets.helm = new Config.SecretsSchema.ESOSchema.ESOHelmSchema([ + config.features.secrets.externalSecrets.helm = new Config.SecretsSchema.ESOSchema.ESOHelmSchema([ image : 'localhost:5000/external-secrets/external-secrets:v0.6.1', certControllerImage: 'localhost:5000/external-secrets/external-secrets-certcontroller:v0.6.1', webhookImage : 'localhost:5000/external-secrets/external-secrets-webhook:v0.6.1' @@ -104,9 +117,12 @@ class ExternalSecretsOperatorTest { @Test void 'helm release is installed in air-gapped mode'() { - config.application.mirrorRepos = true + when(gitHandler.getResourcesScm()).thenReturn(gitProvider) + when(gitProvider.repoUrl(any())).thenReturn("http://scmm.foo-scm-manager.svc.cluster.local/scm/repo/a/b") when(airGappedUtils.mirrorHelmRepoToGit(any(Config.HelmConfig))).thenReturn('a/b') + config.application.mirrorRepos = true + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) config.application.localHelmChartFolder = rootChartsFolder.toString() @@ -136,7 +152,7 @@ class ExternalSecretsOperatorTest { config.registry.proxyUsername = 'proxy-user' config.registry.proxyPassword = 'proxy-pw' config.registry.proxyPassword = 'proxy-pw' - config.features.secrets.externalSecrets.helm = new Config.SecretsSchema.ESOSchema.ESOHelmSchema( [ + config.features.secrets.externalSecrets.helm = new Config.SecretsSchema.ESOSchema.ESOHelmSchema([ certControllerImage: 'some:thing', webhookImage : 'some:thing' ]) @@ -162,7 +178,7 @@ class ExternalSecretsOperatorTest { // Path after template invocation return ret } - }, deploymentStrategy, k8sClient, airGappedUtils) + }, deploymentStrategy, k8sClient, airGappedUtils, gitHandler) } private Map parseActualYaml() { diff --git a/src/test/groovy/com/cloudogu/gitops/features/IngressNginxTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/IngressNginxTest.groovy index ad3635a95..87966cb24 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/IngressNginxTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/IngressNginxTest.groovy @@ -1,14 +1,19 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config - import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.providers.GitProvider import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClientForTest import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension + import java.nio.file.Files import java.nio.file.Path @@ -16,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat import static org.mockito.ArgumentMatchers.any import static org.mockito.Mockito.* +@ExtendWith(MockitoExtension.class) class IngressNginxTest { // setting default config values with ingress nginx active @@ -28,10 +34,17 @@ class IngressNginxTest { )) Path temporaryYamlFile FileSystemUtils fileSystemUtils = new FileSystemUtils() - DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) - AirGappedUtils airGappedUtils = mock(AirGappedUtils) + K8sClientForTest k8sClient = new K8sClientForTest(config) + @Mock + DeploymentStrategy deploymentStrategy + @Mock + AirGappedUtils airGappedUtils + @Mock + GitHandler gitHandler + @Mock + GitProvider gitProvider @Test void 'Helm release is installed'() { @@ -42,7 +55,7 @@ class IngressNginxTest { assertThat(actual['controller']['replicaCount']).isEqualTo(2) verify(deploymentStrategy).deployFeature(config.features.ingressNginx.helm.repoURL, 'ingress-nginx', - config.features.ingressNginx.helm.chart, config.features.ingressNginx.helm.version,'foo-ingress-nginx', + config.features.ingressNginx.helm.chart, config.features.ingressNginx.helm.version, 'foo-ingress-nginx', 'ingress-nginx', temporaryYamlFile) assertThat(parseActualYaml()['controller']['resources']).isNull() assertThat(parseActualYaml()['controller']['metrics']).isNull() @@ -74,8 +87,8 @@ class IngressNginxTest { config.features.ingressNginx.helm.values = [ controller: [ replicaCount: 42, - span: '7,5', - ] + span : '7,5', + ] ] createIngressNginx().install() @@ -88,16 +101,19 @@ class IngressNginxTest { @Test void 'helm release is installed in air-gapped mode'() { - config.application.mirrorRepos = true + when(gitHandler.getResourcesScm()).thenReturn(gitProvider) + when(gitProvider.repoUrl(any())).thenReturn("http://scmm.foo-scm-manager.svc.cluster.local/scm/repo/a/b") when(airGappedUtils.mirrorHelmRepoToGit(any(Config.HelmConfig))).thenReturn('a/b') + config.application.mirrorRepos = true + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) config.application.localHelmChartFolder = rootChartsFolder.toString() Path SourceChart = rootChartsFolder.resolve('ingress-nginx') Files.createDirectories(SourceChart) - Map ChartYaml = [ version : '1.2.3' ] + Map ChartYaml = [version: '1.2.3'] fileSystemUtils.writeYaml(ChartYaml, SourceChart.resolve('Chart.yaml').toFile()) createIngressNginx().install() @@ -109,7 +125,7 @@ class IngressNginxTest { assertThat(helmConfig.value.version).isEqualTo('4.12.1') verify(deploymentStrategy).deployFeature( 'http://scmm.foo-scm-manager.svc.cluster.local/scm/repo/a/b', - 'ingress-nginx', '.', '1.2.3','foo-ingress-nginx', + 'ingress-nginx', '.', '1.2.3', 'foo-ingress-nginx', 'ingress-nginx', temporaryYamlFile, DeploymentStrategy.RepoType.GIT) } @@ -128,7 +144,7 @@ class IngressNginxTest { } @Test - void 'Activates network policies'(){ + void 'Activates network policies'() { config.application.netpols = true createIngressNginx().install() @@ -171,7 +187,7 @@ class IngressNginxTest { config.features.ingressNginx.active = false assertThat(createIngressNginx().getActiveNamespaceFromFeature()).isEqualTo(null) } - + private IngressNginx createIngressNginx() { // We use the real FileSystemUtils and not a mock to make sure file editing works as expected new IngressNginx(config, new FileSystemUtils() { @@ -182,7 +198,7 @@ class IngressNginxTest { // Path after template invocation return ret } - }, deploymentStrategy, k8sClient, airGappedUtils) + }, deploymentStrategy, k8sClient, airGappedUtils, gitHandler) } private Map parseActualYaml() { diff --git a/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy index 30f6aa070..c4dd62b31 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/JenkinsTest.groovy @@ -2,6 +2,7 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.HelmStrategy +import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.jenkins.GlobalPropertyManager import com.cloudogu.gitops.jenkins.JobManager import com.cloudogu.gitops.jenkins.PrometheusConfigurator @@ -10,10 +11,14 @@ import com.cloudogu.gitops.utils.CommandExecutorForTest import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient import com.cloudogu.gitops.utils.NetworkingUtils +import com.cloudogu.gitops.utils.git.GitHandlerForTests +import com.cloudogu.gitops.utils.git.ScmManagerMock import groovy.yaml.YamlSlurper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.ArgumentCaptor +import org.mockito.Mock + import java.nio.file.Path import static org.assertj.core.api.Assertions.assertThat @@ -21,10 +26,15 @@ import static org.mockito.ArgumentMatchers.* import static org.mockito.Mockito.* class JenkinsTest { - Config config = new Config( jenkins: new Config.JenkinsSchema(active: true)) + Config config = new Config( + scm: [ + scmManager: [ + urlForJenkins: "testUrlJenkins" + ]], + jenkins: new Config.JenkinsSchema(active: true)) String expectedNodeName = 'something' - + CommandExecutorForTest commandExecutor = new CommandExecutorForTest() GlobalPropertyManager globalPropertyManager = mock(GlobalPropertyManager) JobManager jobManger = mock(JobManager) @@ -35,17 +45,21 @@ class JenkinsTest { NetworkingUtils networkingUtils = mock(NetworkingUtils.class) K8sClient k8sClient = mock(K8sClient) + @Mock + ScmManagerMock scmManagerMock = new ScmManagerMock() + GitHandler gitHandler = new GitHandlerForTests(config, scmManagerMock) + @BeforeEach void setup() { // waitForInternalNodeIp -> waitForNode() when(k8sClient.waitForNode()).thenReturn("node/${expectedNodeName}".toString()) when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any())).thenReturn('') } - + @Test void 'Installs Jenkins'() { def jenkins = createJenkins() - + config.jenkins.url = 'http://jenkins' config.jenkins.helm.chart = 'jen-chart' config.jenkins.helm.repoURL = 'https://jen-repo' @@ -54,7 +68,7 @@ class JenkinsTest { config.jenkins.password = 'jenpw' config.jenkins.internalBashImage = 'bash:42' config.jenkins.internalDockerClientVersion = '23' - + when(k8sClient.run(anyString(), anyString(), anyString(), anyMap(), any())).thenReturn(''' root:x:0: daemon:x:1: @@ -62,7 +76,7 @@ docker:x:42:me me:x:1000:''') jenkins.install() - + verify(deploymentStrategy).deployFeature('https://jen-repo', 'jenkins', 'jen-chart', '4.8.1', 'jenkins', 'jenkins', temporaryYamlFile) @@ -71,11 +85,11 @@ me:x:1000:''') verify(k8sClient).createSecret('generic', 'jenkins-credentials', 'jenkins', new Tuple2('jenkins-admin-user', 'jenusr'), new Tuple2('jenkins-admin-password', 'jenpw')) - + assertThat(parseActualYaml()['dockerClientVersion'].toString()).isEqualTo('23') - + assertThat(parseActualYaml()['controller']['image']['tag']).isEqualTo('4.8.1') - + assertThat(parseActualYaml()['controller']['jenkinsUrl']).isEqualTo('http://jenkins') assertThat(parseActualYaml()['controller']['serviceType']).isEqualTo('NodePort') @@ -83,7 +97,7 @@ me:x:1000:''') List customInitContainers = parseActualYaml()['controller']['customInitContainers'] as List assertThat(customInitContainers[0]['image']).isEqualTo('bash:42') - + assertThat(parseActualYaml()['agent']['runAsUser']).isEqualTo(1000) assertThat(parseActualYaml()['agent']['runAsGroup']).isEqualTo(42) @@ -110,11 +124,11 @@ me:x:1000:''') @Test void 'Installs only if internal'() { config.jenkins.internal = false - - createJenkins().install() - verify(deploymentStrategy, never()).deployFeature(anyString(), anyString(), anyString(), anyString(), + + createJenkins().install() + verify(deploymentStrategy, never()).deployFeature(anyString(), anyString(), anyString(), anyString(), anyString(), anyString(), any(Path)) - + assertThat(temporaryYamlFile).isNull() } @@ -122,7 +136,7 @@ me:x:1000:''') void 'Additional helm values are merged with default values'() { config.jenkins.helm.values = [ controller: [ - nodePort: 42 + nodePort: 42 ] ] @@ -135,7 +149,7 @@ me:x:1000:''') void 'Enables ingress when baseUrl is set'() { config.jenkins.ingress = 'jenkins.localhost' config.application.baseUrl = 'someBaseUrl' - + createJenkins().install() assertThat(parseActualYaml()['controller']['ingress']['enabled']).isEqualTo(true) @@ -149,17 +163,16 @@ me:x:1000:''') assertThat(parseActualYaml()['controller']['serviceType']).isEqualTo('LoadBalancer') } - + @Test void 'Maps config properly'() { config.application.remote = true config.application.trace = true config.features.argocd.active = true config.content.examples = true - config.scmm.url = 'http://scmm' - config.scmm.urlForJenkins ='http://scmm/scm' - config.scmm.username = 'scmm-usr' - config.scmm.password = 'scmm-pw' + config.scm.scmManager.url = 'http://scmm.scm-manager.svc.cluster.local/scm' + config.scm.scmManager.username = 'scmm-usr' + config.scm.scmManager.password = 'scmm-pw' config.application.namePrefix = 'my-prefix-' config.application.namePrefixForEnvVars = 'MY_PREFIX_' config.registry.url = 'reg-url' @@ -196,14 +209,14 @@ me:x:1000:''') assertThat(env['NAME_PREFIX']).isEqualTo('my-prefix-') assertThat(env['INSECURE']).isEqualTo('false') - assertThat(env['SCMM_URL']).isEqualTo('http://scmm/scm') - assertThat(env['SCMM_PASSWORD']).isEqualTo('scmm-pw') + assertThat(env['SCMM_URL']).isEqualTo('http://scmm.scm-manager.svc.cluster.local/scm') + assertThat(env['SCMM_PASSWORD']).isEqualTo(scmManagerMock.credentials.password) assertThat(env['INSTALL_ARGOCD']).isEqualTo('true') assertThat(env['SKIP_PLUGINS']).isEqualTo('true') assertThat(env['SKIP_RESTART']).isEqualTo('true') - verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_SCMM_URL', 'http://scmm/scm') + verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_SCM_URL', 'http://scmm.scm-manager.svc.cluster.local/scm') verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_K8S_VERSION', Config.K8S_VERSION) verify(globalPropertyManager).setGlobalProperty('MY_PREFIX_REGISTRY_URL', 'reg-url') @@ -213,47 +226,33 @@ me:x:1000:''') verify(userManager).createUser('metrics-usr', 'metrics-pw') verify(userManager).grantPermission('metrics-usr', UserManager.Permissions.METRICS_VIEW) - - verify(jobManger).createCredential('my-prefix-example-apps', 'scmm-user', - 'my-prefix-gitops', 'scmm-pw', 'credentials for accessing scm-manager') - - verify(jobManger).startJob('my-prefix-example-apps') - verify(jobManger).createJob('my-prefix-example-apps', 'http://scmm/scm', - "my-prefix-argocd", 'scmm-user') - - verify(jobManger).createCredential('my-prefix-example-apps', 'registry-user', - 'reg-usr', 'reg-pw', 'credentials for accessing the docker-registry for writing images built on jenkins') - verify(jobManger, never()).createCredential(eq('my-prefix-example-apps'), eq('registry-proxy-user'), - anyString(), anyString(), anyString()) - verify(jobManger, never()).createCredential(eq('my-prefix-example-apps'), eq('registry-proxy-user'), - anyString(), anyString(), anyString()) } @Test void 'Does not configure prometheus when external Jenkins'() { config.features.monitoring.active = true config.jenkins.internal = false - + createJenkins().install() verify(prometheusConfigurator, never()).enableAuthentication() } - + @Test void 'Does not configure prometheus when monitoring off'() { config.features.monitoring.active = false config.jenkins.internal = true - + createJenkins().install() verify(prometheusConfigurator, never()).enableAuthentication() } - + @Test void 'Configures prometheus'() { config.features.monitoring.active = true config.jenkins.internal = true - + createJenkins().install() verify(prometheusConfigurator).enableAuthentication() @@ -263,7 +262,7 @@ me:x:1000:''') void "URL: Use k8s service name if running as k8s pod"() { config.jenkins.internal = true config.application.runningInsideK8s = true - + createJenkins().install() assertThat(config.jenkins.url).isEqualTo("http://jenkins.jenkins.svc.cluster.local:80") } @@ -279,14 +278,14 @@ me:x:1000:''') createJenkins().install() assertThat(config.jenkins.url).endsWith('192.168.16.2:42') } - + @Test void 'Handles two registries'() { config.registry.twoRegistries = true config.content.examples = true config.application.namePrefix = 'my-prefix-' config.application.namePrefixForEnvVars = 'MY_PREFIX_' - + config.registry.url = 'reg-url' config.registry.path = 'reg-path' config.registry.username = 'reg-usr' @@ -302,12 +301,6 @@ me:x:1000:''') verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_REGISTRY_URL'), anyString()) verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_REGISTRY_PATH'), anyString()) - verify(jobManger).createCredential('my-prefix-example-apps', 'registry-user', - 'reg-usr', 'reg-pw', - 'credentials for accessing the docker-registry for writing images built on jenkins') - verify(jobManger).createCredential('my-prefix-example-apps', 'registry-proxy-user', - 'reg-proxy-usr', 'reg-proxy-pw', - 'credentials for accessing the docker-registry that contains 3rd party or base images') } @Test @@ -354,7 +347,7 @@ me:x:1000:''') config.registry.url = 'some value' config.jenkins.mavenCentralMirror = 'http://test' config.application.namePrefixForEnvVars = 'MY_PREFIX_' - + createJenkins().install() verify(globalPropertyManager).setGlobalProperty(eq('MY_PREFIX_MAVEN_CENTRAL_MIRROR'), eq("http://test")) @@ -375,7 +368,7 @@ me:x:1000:''') // Path after template invocation return ret } - }, globalPropertyManager, jobManger, userManager, prometheusConfigurator, deploymentStrategy, k8sClient, networkingUtils) + }, globalPropertyManager, jobManger, userManager, prometheusConfigurator, deploymentStrategy, k8sClient, networkingUtils, gitHandler) } private Map parseActualYaml() { diff --git a/src/test/groovy/com/cloudogu/gitops/features/MailhogTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/MailhogTest.groovy index 0e8312685..3da6f0f61 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/MailhogTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/MailhogTest.groovy @@ -1,15 +1,18 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config - import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.utils.git.ScmManagerMock import com.cloudogu.gitops.utils.AirGappedUtils import com.cloudogu.gitops.utils.FileSystemUtils +import com.cloudogu.gitops.utils.git.GitHandlerForTests import com.cloudogu.gitops.utils.K8sClientForTest import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test import org.mockito.ArgumentCaptor import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder + import java.nio.file.Files import java.nio.file.Path @@ -19,19 +22,24 @@ import static org.mockito.Mockito.* class MailhogTest { - Config config = new Config( - application: new Config.ApplicationSchema( - namePrefix: "foo-"), - features: new Config.FeaturesSchema( - mail: new Config.MailSchema( - mailhog: true) - )) + Config config = Config.fromMap([ + application: [ + namePrefix: "foo-" + ], + features : [ + mail: [ + mailhog: true + ] + ] + ]) + DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) AirGappedUtils airGappedUtils = mock(AirGappedUtils) Path temporaryYamlFile = null FileSystemUtils fileSystemUtils = new FileSystemUtils() K8sClientForTest k8sClient = new K8sClientForTest(config) + GitHandler gitHandler = new GitHandlerForTests(config, new ScmManagerMock()) @Test void "is disabled via active flag"() { @@ -183,7 +191,7 @@ class MailhogTest { assertThat(helmConfig.value.repoURL).isEqualTo('https://codecentric.github.io/helm-charts') assertThat(helmConfig.value.version).isEqualTo('5.0.1') verify(deploymentStrategy).deployFeature( - 'http://scmm.foo-scm-manager.svc.cluster.local/scm/repo/a/b', + 'http://scmm.scm-manager.svc.cluster.local/scm/repo/a/b', 'mailhog', '.', '1.2.3', 'foo-monitoring', 'mailhog', temporaryYamlFile, DeploymentStrategy.RepoType.GIT) } @@ -223,7 +231,7 @@ class MailhogTest { // Path after template invocation return ret } - }, deploymentStrategy, k8sClient, airGappedUtils) + }, deploymentStrategy, k8sClient, airGappedUtils, gitHandler) } private Map parseActualYaml() { diff --git a/src/test/groovy/com/cloudogu/gitops/features/PrometheusStackTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/PrometheusStackTest.groovy index 500948c5e..c4f0fcdb2 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/PrometheusStackTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/PrometheusStackTest.groovy @@ -2,9 +2,14 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config import com.cloudogu.gitops.features.deployment.DeploymentStrategy -import com.cloudogu.gitops.scmm.ScmmRepo +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.utils.git.TestGitRepoFactory +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.utils.git.ScmManagerMock import com.cloudogu.gitops.utils.* import groovy.yaml.YamlSlurper +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.ArgumentCaptor @@ -17,61 +22,72 @@ import static org.mockito.ArgumentMatchers.any import static org.mockito.Mockito.* class PrometheusStackTest { - Config config = new Config( - registry: new Config.RegistrySchema( - internal: true, + Config config = Config.fromMap( + registry: [ + internal : true, createImagePullSecrets: false - ), - scmm: new Config.ScmmSchema( - internal: true - ), - jenkins: new Config.JenkinsSchema(internal: true, + ], + scm: [ + scmManager: [ + internal: true + ] + ], + jenkins: [ + internal : true, metricsUsername: 'metrics', - metricsPassword: 'metrics',), - application: new Config.ApplicationSchema( - username: 'abc', - password: '123', - remote: false, - openshift: false, - namePrefix: "foo-", - mirrorRepos: false, - podResources: false, - skipCrds: false, + metricsPassword: 'metrics' + ], + application: [ + username : 'abc', + password : '123', + remote : false, + openshift : false, + namePrefix : 'foo-', + mirrorRepos : false, + podResources : false, + skipCrds : false, namespaceIsolation: false, - gitName: 'Cloudogu', - gitEmail: 'hello@cloudogu.com', - netpols: false, - namespaces: new Config.ApplicationSchema.NamespaceSchema( - dedicatedNamespaces: new LinkedHashSet([ + gitName : 'Cloudogu', + gitEmail : 'hello@cloudogu.com', + netpols : false, + namespaces : [ + dedicatedNamespaces: [ "test1-default", "test1-argocd", "test1-monitoring", "test1-secrets" - ]), - tenantNamespaces: new LinkedHashSet(["test1-example-apps-staging", - "test1-example-apps-production"]) - )), - features: new Config.FeaturesSchema( - argocd: new Config.ArgoCDSchema(active: true), - monitoring: new Config.MonitoringSchema( - active: true, - grafanaUrl: '', + ] as LinkedHashSet, + tenantNamespaces : [ + "test1-example-apps-staging", + "test1-example-apps-production" + ] as LinkedHashSet + ] + ], + features: [ + argocd : [ + active: true + ], + monitoring : [ + active : true, + grafanaUrl : '', grafanaEmailFrom: 'grafana@example.org', - grafanaEmailTo: 'infra@example.org', - helm: new Config.MonitoringSchema.MonitoringHelmSchema( - chart: 'kube-prometheus-stack', + grafanaEmailTo : 'infra@example.org', + helm : [ + chart : 'kube-prometheus-stack', repoURL: 'https://prom', - version: '19.2.2', - - )), - secrets: new Config.SecretsSchema(active: true), - ingressNginx: new Config.IngressNginxSchema(active: true), - mail: new Config.MailSchema( - mailhog: true, - ), - - ), - ) + version: '19.2.2' + ] + ], + secrets : [ + active: true + ], + ingressNginx: [ + active: true + ], + mail : [ + mailhog: true + ] + ]) K8sClientForTest k8sClient = new K8sClientForTest(config) CommandExecutorForTest k8sCommandExecutor = k8sClient.commandExecutorForTest @@ -81,10 +97,19 @@ class PrometheusStackTest { FileSystemUtils fileSystemUtils = new FileSystemUtils() File clusterResourcesRepoDir + GitHandler gitHandler = mock(GitHandler.class) + ScmManagerMock scmManagerMock + + @BeforeEach + void setup() { + scmManagerMock = new ScmManagerMock() + } + + @Test void "is disabled via active flag"() { config.features.monitoring.active = false - createStack().install() + createStack(scmManagerMock).install() assertThat(temporaryYamlFilePrometheus).isNull() assertThat(k8sCommandExecutor.actualCommands).isEmpty() verifyNoMoreInteractions(deploymentStrategy) @@ -94,7 +119,7 @@ class PrometheusStackTest { void 'When mailhog disabled: Does not include mail configurations into cluster resources'() { config.features.mail.active = null // user should not do this in real. config.features.mail.mailhog = false - createStack().install() + createStack(scmManagerMock).install() def yaml = parseActualYaml() assertThat(yaml['grafana']['notifiers']).isNull() @@ -103,7 +128,7 @@ class PrometheusStackTest { @Test void 'When mailhog enabled: Includes mail configurations into cluster resources'() { config.features.mail.active = true - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['notifiers']).isNotNull() } @@ -112,7 +137,7 @@ class PrometheusStackTest { config.features.mail.active = true config.features.monitoring.grafanaEmailFrom = 'grafana@example.com' config.features.monitoring.grafanaEmailTo = 'infra@example.com' - createStack().install() + createStack(scmManagerMock).install() def notifiersYaml = parseActualYaml()['grafana']['notifiers']['notifiers.yaml']['notifiers']['settings'] as List assertThat(notifiersYaml[0]['addresses']).isEqualTo('infra@example.com') @@ -122,7 +147,7 @@ class PrometheusStackTest { @Test void "When Email Addresses is NOT set"() { config.features.mail.active = true - createStack().install() + createStack(scmManagerMock).install() def notifiersYaml = parseActualYaml()['grafana']['notifiers']['notifiers.yaml']['notifiers']['settings'] as List assertThat(notifiersYaml[0]['addresses']).isEqualTo('infra@example.org') @@ -137,7 +162,7 @@ class PrometheusStackTest { config.features.monitoring.grafanaEmailTo = 'grafana@example.com' // needed to check that yaml is inserted correctly - createStack().install() + createStack(scmManagerMock).install() def contactPointsYaml = parseActualYaml() assertThat(contactPointsYaml['grafana']['alerting']['contactpoints.yaml']).isEqualTo(new YamlSlurper().parseText( @@ -177,7 +202,7 @@ policies: config.features.mail.smtpAddress = 'smtp.example.com' config.features.mail.smtpUser = 'mailserver@example.com' - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['smtp']['existingSecret']).isEqualTo('grafana-email-secret') k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user=mailserver@example.com --from-literal password=') @@ -189,7 +214,7 @@ policies: config.features.mail.smtpAddress = 'smtp.example.com' config.features.mail.smtpPassword = '1101ABCabc&/+*~' - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['smtp']['existingSecret']).isEqualTo('grafana-email-secret') k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user= --from-literal password=1101ABCabc&/+*~') } @@ -199,7 +224,7 @@ policies: config.features.mail.active = true config.features.mail.smtpAddress = 'smtp.example.com' - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['valuesFrom']).isNull() assertThat(parseActualYaml()['grafana']['smtp']).isNull() @@ -213,7 +238,7 @@ policies: config.features.mail.smtpUser = 'grafana@example.com' config.features.mail.smtpPassword = '1101ABCabc&/+*~' - createStack().install() + createStack(scmManagerMock).install() k8sCommandExecutor.assertExecuted('kubectl create secret generic grafana-email-secret -n foo-monitoring --from-literal user=grafana@example.com --from-literal password=1101ABCabc&/+*~') } @@ -223,7 +248,7 @@ policies: config.features.mail.active = true config.features.mail.smtpAddress = 'smtp.example.com' - createStack().install() + createStack(scmManagerMock).install() def contactPointsYaml = parseActualYaml() assertThat(contactPointsYaml['grafana']['env']['GF_SMTP_HOST']).isEqualTo('smtp.example.com') @@ -233,7 +258,7 @@ policies: void 'When external Mailserver is NOT set'() { config.features.mail.active = null // user should not do this in real. config.features.mail.mailhog = false - createStack().install() + createStack(scmManagerMock).install() def contactPointsYaml = parseActualYaml() assertThat(contactPointsYaml['grafana']['alerting']).isNull() @@ -242,7 +267,7 @@ policies: @Test void "service type LoadBalancer when run remotely"() { config.application.remote = true - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['service']['type']).isEqualTo('LoadBalancer') assertThat(parseActualYaml()['grafana']['service']['nodePort']).isNull() @@ -252,7 +277,7 @@ policies: void "configures admin user if requested"() { config.application.username = "my-user" config.application.password = "hunter2" - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['adminUser']).isEqualTo('my-user') assertThat(parseActualYaml()['grafana']['adminPassword']).isEqualTo('hunter2') @@ -261,7 +286,7 @@ policies: @Test void 'service type ClusterIP when not run remotely'() { config.application.remote = false - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['service']['type']).isEqualTo('ClusterIP') } @@ -269,7 +294,7 @@ policies: @Test void 'uses ingress if enabled'() { config.features.monitoring.grafanaUrl = 'http://grafana.local' - createStack().install() + createStack(scmManagerMock).install() def ingressYaml = parseActualYaml()['grafana']['ingress'] @@ -279,22 +304,19 @@ policies: @Test void 'does not use ingress by default'() { - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana'] as Map).doesNotContainKey('ingress') } @Test void 'uses remote scmm url if requested'() { - config.scmm.internal = false - config.scmm.url = 'https://localhost:9091/prefix' - createStack().install() - + createStack(scmManagerMock).install() def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List - assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:9091') - assertThat(additionalScrapeConfigs[0]['metrics_path']).isEqualTo('/prefix/api/v2/metrics/prometheus') - assertThat(additionalScrapeConfigs[0]['scheme']).isEqualTo('https') + assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:8080') + assertThat(additionalScrapeConfigs[0]['metrics_path']).isEqualTo('/scm/api/v2/metrics/prometheus') + assertThat(additionalScrapeConfigs[0]['scheme']).isEqualTo('http') // scrape config for jenkins is unchanged assertThat(((additionalScrapeConfigs[1]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('jenkins.foo-jenkins.svc.cluster.local') @@ -306,25 +328,25 @@ policies: void 'uses remote jenkins url if requested'() { config.jenkins["internal"] = false config.jenkins["url"] = 'https://localhost:9090/jenkins' - createStack().install() - - + createStack(scmManagerMock).install() def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List - assertThat(((additionalScrapeConfigs[1]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:9090') - assertThat(additionalScrapeConfigs[1]['metrics_path']).isEqualTo('/jenkins/prometheus') - assertThat(additionalScrapeConfigs[1]['scheme']).isEqualTo('https') // scrape config for scmm is unchanged - assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('scmm.foo-scm-manager.svc.cluster.local') + assertThat(((additionalScrapeConfigs[0]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:8080') assertThat(additionalScrapeConfigs[0]['scheme']).isEqualTo('http') assertThat(additionalScrapeConfigs[0]['metrics_path']).isEqualTo('/scm/api/v2/metrics/prometheus') + + + assertThat(((additionalScrapeConfigs[1]['static_configs'] as List)[0]['targets'] as List)[0]).isEqualTo('localhost:9090') + assertThat(additionalScrapeConfigs[1]['metrics_path']).isEqualTo('/jenkins/prometheus') + assertThat(additionalScrapeConfigs[1]['scheme']).isEqualTo('https') } @Test void 'configures custom metrics user for jenkins'() { config.jenkins["metricsUsername"] = 'external-metrics-username' config.jenkins["metricsPassword"] = 'hunter2' - createStack().install() + createStack(scmManagerMock).install() assertThat(k8sCommandExecutor.actualCommands[1]).isEqualTo("kubectl create secret generic prometheus-metrics-creds-jenkins -n foo-monitoring --from-literal password=hunter2 --dry-run=client -oyaml | kubectl apply -f-") def additionalScrapeConfigs = parseActualYaml()['prometheus']['prometheusSpec']['additionalScrapeConfigs'] as List @@ -334,7 +356,7 @@ policies: @Test void "configures custom image for grafana"() { config.features.monitoring.helm.grafanaImage = "localhost:5000/grafana/grafana:the-tag" - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['image']['registry']).isEqualTo('localhost:5000') assertThat(parseActualYaml()['grafana']['image']['repository']).isEqualTo('grafana/grafana') @@ -344,7 +366,7 @@ policies: @Test void "configures custom image for grafana-sidecar"() { config.features.monitoring.helm.grafanaSidecarImage = "localhost:5000/grafana/sidecar:the-tag" - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['grafana']['sidecar']['image']['registry']).isEqualTo('localhost:5000') assertThat(parseActualYaml()['grafana']['sidecar']['image']['repository']).isEqualTo('grafana/sidecar') @@ -357,7 +379,7 @@ policies: config.features.monitoring.helm.prometheusOperatorImage = "localhost:5000/prometheus-operator/prometheus-operator:v2" config.features.monitoring.helm.prometheusConfigReloaderImage = "localhost:5000/prometheus-operator/prometheus-config-reloader:v3" - createStack().install() + createStack(scmManagerMock).install() def actualYaml = parseActualYaml() assertThat(actualYaml['prometheus']['prometheusSpec']['image']['registry']).isEqualTo('localhost:5000') @@ -378,7 +400,7 @@ policies: config.registry.proxyUsername = 'proxy-user' config.registry.proxyPassword = 'proxy-pw' - createStack().install() + createStack(scmManagerMock).install() k8sClient.commandExecutorForTest.assertExecuted( 'kubectl create secret docker-registry proxy-registry -n foo-monitoring' + @@ -388,7 +410,7 @@ policies: @Test void 'helm release is installed'() { - createStack().install() + createStack(scmManagerMock).install() assertThat(k8sCommandExecutor.actualCommands[0].trim()).isEqualTo( 'kubectl create secret generic prometheus-metrics-creds-scmm -n foo-monitoring --from-literal password=123 --dry-run=client -oyaml | kubectl apply -f-') @@ -433,7 +455,7 @@ policies: void 'Skips CRDs'() { config.application.skipCrds = true - createStack().install() + createStack(scmManagerMock).install() assertThat(parseActualYaml()['crds']['enabled']).isEqualTo(false) } @@ -442,7 +464,7 @@ policies: void 'Sets pod resource limits and requests'() { config.application.podResources = true - createStack().install() + createStack(scmManagerMock).install() def yaml = parseActualYaml() assertThat(yaml['prometheusOperator']['resources'] as Map).containsKeys('limits', 'requests') @@ -459,7 +481,7 @@ policies: String realoutput = '{"app.kubernetes.io/created-by":"Internal OpenShift","openshift.io/description":"","openshift.io/display-name":"","openshift.io/requester":"myUser@mydomain.de","openshift.io/sa.scc.mcs":"s0:c30,c25","openshift.io/sa.scc.supplemental-groups":"1000920000/10000","openshift.io/sa.scc.uid-range":"1000920000/10000","project-type":"customer"}' k8sCommandExecutor.enqueueOutput(new CommandExecutor.Output('', realoutput, 0)) - createStack().install() + createStack(scmManagerMock).install() def yaml = parseActualYaml() assertThat(yaml['prometheusOperator']['securityContext']).isNotNull() @@ -482,7 +504,7 @@ policies: void 'works with namespaceIsolation'() { config.application.namespaceIsolation = true - def prometheusStack = createStack() + def prometheusStack = createStack(scmManagerMock) prometheusStack.install() def yaml = parseActualYaml() @@ -508,7 +530,7 @@ policies: void 'network policies are created for prometheus'() { config.application.netpols = true //config.application.namespaces.dedicatedNamespaces = ["testnamespace1", "testnamespace2"] - def prometheusStack = createStack() + def prometheusStack = createStack(scmManagerMock) prometheusStack.install() for (String namespace : config.application.namespaces.getActiveNamespaces()) { @@ -531,7 +553,8 @@ policies: Map prometheusChartYaml = [version: '1.2.3'] fileSystemUtils.writeYaml(prometheusChartYaml, prometheusSourceChart.resolve('Chart.yaml').toFile()) - createStack().install() + scmManagerMock.inClusterBase = new URI("http://scmm.foo-scm-manager.svc.cluster.local/scm") + createStack(scmManagerMock).install() def helmConfig = ArgumentCaptor.forClass(Config.HelmConfig) verify(airGappedUtils).mirrorHelmRepoToGit(helmConfig.capture()) @@ -558,7 +581,7 @@ policies: ] ] - createStack().install() + createStack(scmManagerMock).install() def actual = parseActualYaml() assertThat(actual['key']['some']).isEqualTo('thing') @@ -580,7 +603,7 @@ policies: "test1-secrets" ] config.application.namespaces.dedicatedNamespaces = namespaceList - createStack().install() + createStack(scmManagerMock).install() def actual = parseActualYaml() assertThat(actual['prometheus']['prometheusSpec']['serviceMonitorNamespaceSelector']).isEqualTo(new YamlSlurper().parseText(''' @@ -597,22 +620,14 @@ matchExpressions: )) } - private PrometheusStack createStack() { + private PrometheusStack createStack(ScmManagerMock scmManagerMock) { // We use the real FileSystemUtils and not a mock to make sure file editing works as expected - + when(gitHandler.getResourcesScm()).thenReturn(scmManagerMock) def configuration = config - def repoProvider = new TestScmmRepoProvider(config, new FileSystemUtils()) { - @Override - ScmmRepo getRepo(String repoTarget) { - def repo = super.getRepo(repoTarget) - clusterResourcesRepoDir = new File(repo.getAbsoluteLocalRepoTmpDir()) - - return repo - } - + TestGitRepoFactory repoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) { @Override - ScmmRepo getRepo(String repoTarget, Boolean isCentralRepo) { - def repo = super.getRepo(repoTarget, isCentralRepo) + GitRepo getRepo(String repoTarget,GitProvider scm) { + def repo = super.getRepo(repoTarget, scmManagerMock) clusterResourcesRepoDir = new File(repo.getAbsoluteLocalRepoTmpDir()) return repo @@ -627,7 +642,7 @@ matchExpressions: temporaryYamlFilePrometheus = Path.of(ret.toString().replace(".ftl", "")) return ret } - }, deploymentStrategy, k8sClient, airGappedUtils, repoProvider) + }, deploymentStrategy, k8sClient, airGappedUtils, repoProvider, gitHandler) } private Map parseActualYaml() { diff --git a/src/test/groovy/com/cloudogu/gitops/features/ScmManagerTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/ScmManagerSetupTest.groovy similarity index 76% rename from src/test/groovy/com/cloudogu/gitops/features/ScmManagerTest.groovy rename to src/test/groovy/com/cloudogu/gitops/features/ScmManagerSetupTest.groovy index ae0f49468..ffd0728a5 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/ScmManagerTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/ScmManagerSetupTest.groovy @@ -1,7 +1,11 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.MultiTenantSchema import com.cloudogu.gitops.features.deployment.HelmStrategy +import com.cloudogu.gitops.features.git.config.ScmCentralSchema +import com.cloudogu.gitops.features.git.config.ScmTenantSchema +import com.cloudogu.gitops.features.git.config.ScmTenantSchema.ScmManagerTenantConfig import com.cloudogu.gitops.utils.* import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test @@ -11,9 +15,9 @@ import java.nio.file.Path import static org.assertj.core.api.Assertions.assertThat import static org.mockito.ArgumentMatchers.anyString import static org.mockito.Mockito.mock -import static org.mockito.Mockito.when +import static org.mockito.Mockito.when -class ScmManagerTest { +class ScmManagerSetupTest { Config config = new Config( application: new Config.ApplicationSchema( @@ -24,27 +28,34 @@ class ScmManagerTest { insecure: false, gitName: 'Cloudogu', gitEmail: 'hello@cloudogu.com', - runningInsideK8s : true + runningInsideK8s: true ), - scmm: new Config.ScmmSchema( - url: 'http://scmm', - internal: true, - ingress: 'scmm.localhost', - username: 'scmm-usr', - password: 'scmm-pw', - gitOpsUsername: 'foo-gitops', - urlForJenkins: 'http://scmm4jenkins', - helm: new Config.HelmConfigWithValues( - chart: 'scm-manager-chart', - version: '2.47.0', - repoURL: 'https://packages.scm-manager.org/repository/helm-v2-releases/', - values: [:] + multiTenant: new MultiTenantSchema( + scmManager: new ScmCentralSchema.ScmManagerCentralConfig( + username: 'scmm-usr' + ) + ), + scm: new ScmTenantSchema( + scmManager: new ScmManagerTenantConfig( + url: 'http://scmm', + internal: true, + ingress: 'scmm.localhost', + username: 'scmm-usr', + password: 'scmm-pw', + gitOpsUsername: 'foo-gitops', + urlForJenkins: 'http://scmm4jenkins', + helm: new Config.HelmConfigWithValues( + chart: 'scm-manager-chart', + version: '2.47.0', + repoURL: 'https://packages.scm-manager.org/repository/helm-v2-releases/', + values: [:] + ) ) ), jenkins: new Config.JenkinsSchema( internal: true, url: 'http://jenkins', - urlForScmm: 'http://jenkins4scm' + urlForScm: 'http://jenkins4scm' ), repositories: new Config.RepositoriesSchema( springBootHelmChart: new Config.RepositorySchemaWithRef( @@ -59,6 +70,7 @@ class ScmManagerTest { ) ) ) + CommandExecutorForTest commandExecutor = new CommandExecutorForTest() Path temporaryYamlFile @@ -69,11 +81,11 @@ class ScmManagerTest { @Test void 'Installs SCMM and calls script with proper params'() { - config.scmm.username = 'scmm-usr' + config.multiTenant.scmManager.username = 'scmm-usr' config.features.ingressNginx.active = true config.features.argocd.active = true - config.scmm.skipPlugins = true - config.scmm.skipRestart = true + config.scm.scmManager.skipPlugins = true + config.scm.scmManager.skipRestart = true createScmManager().install() assertThat(parseActualYaml()['extraEnv'] as String).contains('SCM_WEBAPP_INITIALUSER\n value: "scmm-usr"') @@ -120,7 +132,7 @@ class ScmManagerTest { @Test void 'Sets service and host only if enabled'() { config.application.remote = true - config.scmm.ingress = '' + config.scm.scmManager.ingress = '' createScmManager().install() Map actualYaml = parseActualYaml() as Map @@ -131,7 +143,7 @@ class ScmManagerTest { @Test void 'Installs only if internal'() { - config.scmm.internal = false + config.scm.scmManager.internal = false createScmManager().install() assertThat(temporaryYamlFile).isNull() @@ -139,7 +151,7 @@ class ScmManagerTest { @Test void 'initialDelaySeconds is set properly'() { - config.scmm.helm.values = [ + config.scm.scmManager.helm.values = [ livenessProbe: [ initialDelaySeconds: 140 ] @@ -151,25 +163,25 @@ class ScmManagerTest { @Test void "URL: Use k8s service name if running as k8s pod"() { - config.scmm.internal = true + config.scm.scmManager.internal = true config.application.runningInsideK8s = true - + createScmManager().install() - assertThat(config.scmm.url).isEqualTo("http://scmm.foo-scm-manager.svc.cluster.local:80/scm") + assertThat(config.scm.scmManager.url).isEqualTo("http://scmm.foo-scm-manager.svc.cluster.local:80/scm") } @Test void "URL: Use local ip and nodePort when outside of k8s"() { - config.scmm.internal = true + config.scm.scmManager.internal = true config.application.runningInsideK8s = false when(networkingUtils.findClusterBindAddress()).thenReturn('192.168.16.2') when(k8sClient.waitForNodePort(anyString(), anyString())).thenReturn('42') - + createScmManager().install() - assertThat(config.scmm.url).endsWith('192.168.16.2:42/scm') + assertThat(config.scm.scmManager.url).endsWith('192.168.16.2:42/scm') } - + protected Map getEnvAsMap() { commandExecutor.environment.collectEntries { it.split('=') } } @@ -179,10 +191,10 @@ class ScmManagerTest { return ys.parse(temporaryYamlFile) as Map } - private ScmManager createScmManager() { + private ScmManagerSetup createScmManager() { when(networkingUtils.createUrl(anyString(), anyString(), anyString())).thenCallRealMethod() when(networkingUtils.createUrl(anyString(), anyString())).thenCallRealMethod() - new ScmManager(config, commandExecutor, new FileSystemUtils() { + new ScmManagerSetup(config, commandExecutor, new FileSystemUtils() { @Override Path writeTempFile(Map mapValues) { def ret = super.writeTempFile(mapValues) diff --git a/src/test/groovy/com/cloudogu/gitops/features/VaultTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/VaultTest.groovy index ab7c3c8dd..81889d91a 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/VaultTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/VaultTest.groovy @@ -1,20 +1,21 @@ package com.cloudogu.gitops.features import com.cloudogu.gitops.config.Config - import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.utils.git.GitHandlerForTests +import com.cloudogu.gitops.utils.git.ScmManagerMock import com.cloudogu.gitops.utils.* import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test import org.mockito.ArgumentCaptor + import java.nio.file.Files import java.nio.file.Path import static org.assertj.core.api.Assertions.assertThat import static org.mockito.ArgumentMatchers.any -import static org.mockito.Mockito.mock -import static org.mockito.Mockito.verify -import static org.mockito.Mockito.when +import static org.mockito.Mockito.* class VaultTest { @@ -34,6 +35,7 @@ class VaultTest { DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) AirGappedUtils airGappedUtils = mock(AirGappedUtils) K8sClientForTest k8sClient = new K8sClientForTest(config) + GitHandler gitHandler = new GitHandlerForTests(config, new ScmManagerMock()) Path temporaryYamlFile @Test @@ -69,7 +71,7 @@ class VaultTest { assertThat(ingressYaml['enabled']).isEqualTo(true) assertThat((ingressYaml['hosts'] as List)[0]['host']).isEqualTo('vault.local') } - + @Test void 'uses ingress if enabled and image set'() { config.features.secrets.vault.url = 'http://vault.local' @@ -128,7 +130,7 @@ class VaultTest { assertThat(k8sClient.commandExecutorForTest.actualCommands[0]).contains('kubectl get namespace foo-secrets') assertThat(k8sClient.commandExecutorForTest.actualCommands[1]).contains('kubectl create namespace foo-secrets') - def createdConfigMapName = ((k8sClient.commandExecutorForTest.actualCommands[2] =~ /kubectl create configmap (\S*) .*/)[0] as List) [1] + def createdConfigMapName = ((k8sClient.commandExecutorForTest.actualCommands[2] =~ /kubectl create configmap (\S*) .*/)[0] as List)[1] assertThat(actualVolumes[0]['configMap']['name']).isEqualTo(createdConfigMapName) assertThat(k8sClient.commandExecutorForTest.actualCommands[2]).contains('-n foo-secrets') @@ -218,7 +220,7 @@ class VaultTest { assertThat(helmConfig.value.repoURL).isEqualTo('https://vault-reg') assertThat(helmConfig.value.version).isEqualTo('42.23.0') verify(deploymentStrategy).deployFeature( - 'http://scmm.foo-scm-manager.svc.cluster.local/scm/repo/a/b', + 'http://scmm.scm-manager.svc.cluster.local/scm/repo/a/b', 'vault', '.', '1.2.3', 'foo-secrets', 'vault', temporaryYamlFile, DeploymentStrategy.RepoType.GIT) } @@ -258,7 +260,7 @@ class VaultTest { temporaryYamlFile = Path.of(ret.toString().replace(".ftl", "")) return ret } - }, k8sClient, deploymentStrategy, airGappedUtils) + }, k8sClient, deploymentStrategy, airGappedUtils, gitHandler) } private Map parseActualYaml() { diff --git a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy index 4e66d15ce..673c96411 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy @@ -1,14 +1,17 @@ package com.cloudogu.gitops.features.argocd import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.utils.git.GitHandlerForTests +import com.cloudogu.gitops.utils.git.TestGitProvider +import com.cloudogu.gitops.utils.git.TestGitRepoFactory +import com.cloudogu.gitops.git.providers.GitProvider import com.cloudogu.gitops.utils.* import groovy.io.FileType import groovy.json.JsonSlurper import groovy.yaml.YamlSlurper -import org.eclipse.jgit.api.CheckoutCommand -import org.eclipse.jgit.api.CloneCommand import org.junit.jupiter.api.Test +import org.mockito.Spy import org.springframework.security.crypto.bcrypt.BCrypt import java.nio.file.Files @@ -19,9 +22,6 @@ import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironment import static org.assertj.core.api.Assertions.assertThat import static org.assertj.core.api.Assertions.fail import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode -import static org.mockito.ArgumentMatchers.any -import static org.mockito.ArgumentMatchers.anyString -import static org.mockito.Mockito.* class ArgoCDTest { Map buildImages = [ @@ -47,24 +47,28 @@ class ArgoCDTest { tenantNamespaces : ["example-apps-staging", "example-apps-production"] ] ], - scmm: [ - internal: true, - url : 'https://abc' + scm: [ + scmManager: [ + internal: true], + gitlab : [ + url: '' + ] ], - images: buildImages + [petclinic: 'petclinic-value'], - repositories: [ - springBootHelmChart: [ - url: 'https://github.com/cloudogu/spring-boot-helm-chart.git', - ref: '0.3.0' + multiTenant: [ + scmManager : [ + url: '' ], - springPetclinic : [ - url: 'https://github.com/cloudogu/spring-petclinic.git', - ref: '32c8653' + gitlab : [ + url: '' ], - gitopsBuildLib : [ + useDedicatedInstance: false + ], + images: buildImages + [petclinic: 'petclinic-value'], + repositories: [ + gitopsBuildLib: [ url: "https://github.com/cloudogu/gitops-build-lib.git", ], - cesBuildLib : [ + cesBuildLib : [ url: 'https://github.com/cloudogu/ces-build-lib.git', ] ], @@ -101,29 +105,31 @@ class ArgoCDTest { ] ) + @Spy + CommandExecutor test = new CommandExecutor() + CommandExecutorForTest k8sCommands = new CommandExecutorForTest() CommandExecutorForTest helmCommands = new CommandExecutorForTest() - ScmmRepo argocdRepo + GitRepo argocdRepo String actualHelmValuesFile - ScmmRepo clusterResourcesRepo - ScmmRepo exampleAppsRepo - ScmmRepo nginxHelmJenkinsRepo - ScmmRepo nginxValidationRepo - ScmmRepo brokenApplicationRepo - ScmmRepo tenantBootstrap - List petClinicRepos = [] - CloneCommand gitCloneMock = mock(CloneCommand.class, RETURNS_DEEP_STUBS) + GitRepo clusterResourcesRepo + GitRepo nginxHelmJenkinsRepo + GitRepo tenantBootstrap + List petClinicRepos = [] String prefixPathCentral = '/multiTenant/central/' ArgoCD argocd @Test void 'Installs argoCD'() { - def argocd = createArgoCD() - // Simulate argocd Namespace does not exist k8sCommands.enqueueOutput(new CommandExecutor.Output('namespace not found', '', 1)) + def argocd = createArgoCD() argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) k8sCommands.assertExecuted('kubectl create namespace argocd') @@ -140,8 +146,8 @@ class ArgoCDTest { assertThat(parseActualYaml(actualHelmValuesFile)['global']).isNull() // check repoTemplateSecretName - k8sCommands.assertExecuted('kubectl create secret generic argocd-repo-creds-scmm -n argocd') - k8sCommands.assertExecuted('kubectl label secret argocd-repo-creds-scmm -n argocd') + k8sCommands.assertExecuted('kubectl create secret generic argocd-repo-creds-scm -n argocd') + k8sCommands.assertExecuted('kubectl label secret argocd-repo-creds-scm -n argocd') // Check dependency build and helm install assertThat(helmCommands.actualCommands[0].trim()).isEqualTo('helm repo add argo https://argoproj.github.io/argo-helm') @@ -184,32 +190,23 @@ class ArgoCDTest { def argocdYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/argocd.yaml') assertThat(argocdYaml['spec']['source']['directory']).isNull() assertThat(argocdYaml['spec']['source']['path']).isEqualTo('argocd/') - // The other application files should be validated here as well! - - // Content examples - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/example-apps.yaml')).exists() - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/example-apps.yaml')).exists() - } - - @Test - void 'Disables example content'() { - config.content.examples = false - - def argocd = createArgoCD() - argocd.install() - - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/example-apps.yaml')).doesNotExist() - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/example-apps.yaml')).doesNotExist() } @Test void 'Installs argoCD for remote and external Scmm'() { config.application.remote = true - config.scmm.internal = false + config.scm.scmManager.internal = false + config.scm.scmManager.url = "https://abc" config.features.argocd.url = 'https://argo.cd' def argocd = createArgoCD() argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + + List filesWithInternalSCMM = findFilesContaining(new File(argocdRepo.getAbsoluteLocalRepoTmpDir()), argocd.scmmUrlInternal) assertThat(filesWithInternalSCMM).isEmpty() List filesWithExternalSCMM = findFilesContaining(new File(argocdRepo.getAbsoluteLocalRepoTmpDir()), "https://abc") @@ -226,7 +223,9 @@ class ArgoCDTest { void 'When monitoring disabled: Does not push path monitoring to cluster resources'() { config.features.monitoring.active = false - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + ArgoCD.MONITORING_RESOURCES_PATH)).doesNotExist() } @@ -235,7 +234,9 @@ class ArgoCDTest { void 'When monitoring enabled: Does push path monitoring to cluster resources'() { config.features.monitoring.active = true - createArgoCD().install() + def argo = createArgoCD() + argo.install() + this.clusterResourcesRepo = (argo as ArgoCDForTest).clusterResourcesRepo assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + ArgoCD.MONITORING_RESOURCES_PATH)).exists() @@ -264,7 +265,10 @@ class ArgoCDTest { void 'When ingressNginx disabled: Does not push monitoring dashboard resources'() { config.features.monitoring.active = true config.features.ingressNginx.active = false - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + ArgoCD.MONITORING_RESOURCES_PATH)).exists() assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + "/misc/ingress-nginx-dashboard.yaml")).doesNotExist() assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + "/misc/ingress-nginx-dashboard-requests-handling.yaml")).doesNotExist() @@ -272,10 +276,15 @@ class ArgoCDTest { @Test void 'When mailhog disabled: Does not include mail configurations into cluster resources'() { - config.features.mail.active = false config.features.mail.mailhog = false - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + def valuesYaml = parseActualYaml(actualHelmValuesFile) assertThat(valuesYaml['argo-cd']['notifications']['enabled']).isEqualTo(false) assertThat(valuesYaml['argo-cd']['notifications']['notifiers']).isNull() @@ -284,7 +293,13 @@ class ArgoCDTest { @Test void 'When mailhog enabled: Includes mail configurations into cluster resources'() { config.features.mail.active = true - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + def valuesYaml = parseActualYaml(actualHelmValuesFile) assertThat(valuesYaml['argo-cd']['notifications']['enabled']).isEqualTo(true) assertThat(valuesYaml['argo-cd']['notifications']['notifiers']).isNotNull() @@ -296,7 +311,15 @@ class ArgoCDTest { config.features.argocd.emailFrom = 'argocd@example.com' config.features.argocd.emailToUser = 'app-team@example.com' config.features.argocd.emailToAdmin = 'argocd@example.com' - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + + def valuesYaml = parseActualYaml(actualHelmValuesFile) def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') def argocdYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/argocd.yaml') @@ -314,7 +337,14 @@ class ArgoCDTest { void 'When emailaddress is NOT set: Use default email addresses in configurations'() { config.features.mail.active = true - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + def valuesYaml = parseActualYaml(actualHelmValuesFile) def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') def argocdYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/argocd.yaml') @@ -336,7 +366,13 @@ class ArgoCDTest { config.features.mail.smtpUser = 'argo@example.com' config.features.mail.smtpPassword = '1101:ABCabc&/+*~' - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + def serviceEmail = new YamlSlurper().parseText( parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['notifiers']['service.email'] as String) @@ -381,7 +417,13 @@ class ArgoCDTest { config.features.mail.active = true config.features.mail.smtpAddress = 'smtp.example.com' - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + def serviceEmail = new YamlSlurper().parseText( parseActualYaml(actualHelmValuesFile)['argo-cd']['notifications']['notifiers']['service.email'] as String) @@ -396,7 +438,13 @@ class ArgoCDTest { @Test void 'When external Mailserver is NOT set'() { config.features.mail.active = true - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) + def valuesYaml = parseActualYaml(actualHelmValuesFile) assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)['host']) doesNotHaveToString('mailhog.*monitoring.svc.cluster.local') @@ -405,102 +453,14 @@ class ArgoCDTest { assertThat(new YamlSlurper().parseText(valuesYaml['argo-cd']['notifications']['notifiers']['service.email'] as String)) doesNotHaveToString('password') } - @Test - void 'When vault enabled: Pushes external secret, and mounts into example app'() { - createArgoCD().install() - def valuesYaml = new YamlSlurper().parse(Path.of nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir(), 'k8s/values-shared.yaml') - - assertThat((valuesYaml['extraVolumeMounts'] as List)).hasSize(2) - assertThat((valuesYaml['extraVolumes'] as List)).hasSize(2) - - assertThat(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir() + "/k8s/staging/external-secret.yaml")).exists() - assertThat(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir() + "/k8s/production/external-secret.yaml")).exists() - assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + "/misc/secrets")).exists() - } - - @Test - void 'When vault disabled: Does not push ExternalSecret and not mount into example app'() { - config.features.secrets.active = false - createArgoCD().install() - def valuesYaml = new YamlSlurper().parse(Path.of nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir(), 'k8s/values-shared.yaml') - assertThat((valuesYaml['extraVolumeMounts'] as List)).hasSize(1) - assertThat((valuesYaml['extraVolumes'] as List)).hasSize(1) - assertThat((valuesYaml['extraVolumeMounts'] as List)[0]['name']).isEqualTo('index') - assertThat((valuesYaml['extraVolumes'] as List)[0]['name']).isEqualTo('index') - - assertThat(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir() + "/k8s/staging/external-secret.yaml")).doesNotExist() - assertThat(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir() + "/k8s/production/external-secret.yaml")).doesNotExist() - } - @Test void 'When vault disabled: Does not push path "secrets" to cluster resources'() { config.features.secrets.active = false - createArgoCD().install() - assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + "/misc/secrets")).doesNotExist() - } - - @Test - void 'Pushes example repos for local'() { - config.application.remote = false def argocd = createArgoCD() - - def setUriMock = mock(CloneCommand.class, RETURNS_DEEP_STUBS) - def checkoutMock = mock(CheckoutCommand.class, RETURNS_DEEP_STUBS) - when(gitCloneMock.setURI(anyString())).thenReturn(setUriMock) - when(setUriMock.setDirectory(any(File.class)).call().checkout()).thenReturn(checkoutMock) - argocd.install() - def valuesYaml = parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-shared.yaml') - assertThat(valuesYaml['service']['type']).isEqualTo('ClusterIP') - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-production.yaml')).doesNotContainKey('ingress') - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-staging.yaml')).doesNotContainKey('ingress') - assertThat(valuesYaml).doesNotContainKey('resources') - - valuesYaml = parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml') - assertThat(valuesYaml['nginx']['service']['type']).isEqualTo('ClusterIP') - assertThat(valuesYaml['nginx'] as Map).doesNotContainKey('ingress') - assertThat(valuesYaml['nginx'] as Map).doesNotContainKey('resources') - - assertThat((parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[1]['spec']['type'])) - .isEqualTo('ClusterIP') - assertThat((parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[0]['spec']['template']['spec']['containers'] as List)[0]['resources']) - .isNull() - - assertThat(new File(nginxValidationRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).doesNotContain('resources:') + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo - // Assert Petclinic repo cloned - verify(gitCloneMock).setURI('https://github.com/cloudogu/spring-petclinic.git') - verify(setUriMock).setDirectory(argocd.remotePetClinicRepoTmpDir) - verify(checkoutMock).setName('32c8653') - - assertPetClinicRepos('ClusterIP', 'LoadBalancer', '') - } - - @Test - void 'Pushes example repos for remote'() { - config.application.remote = true - config.features.exampleApps.petclinic.baseDomain = 'petclinic.local' - config.features.exampleApps.nginx.baseDomain = 'nginx.local' - - createArgoCD().install() - - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-shared.yaml').toString()) - .doesNotContain('ClusterIP') - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-production.yaml')['ingress']['hostname']).isEqualTo('production.nginx-helm.nginx.local') - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-staging.yaml')['ingress']['hostname']).isEqualTo('staging.nginx-helm.nginx.local') - - assertThat(parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml').toString()) - .doesNotContain('ClusterIP') - - def valuesYaml = parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml') - assertThat(valuesYaml['nginx']['ingress']['hostname'] as String).isEqualTo('production.nginx-helm-umbrella.nginx.local') - - assertThat((parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[2]['spec']['rules'] as List)[0]['host']) - .isEqualTo('broken-application.nginx.local') - assertThat((parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[1]['spec']['type'])) - .isEqualTo('LoadBalancer') - - assertPetClinicRepos('LoadBalancer', 'ClusterIP', 'petclinic.local') + assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + "/misc/secrets")).doesNotExist() } @Test @@ -508,7 +468,10 @@ class ArgoCDTest { config.features.monitoring.active = false config.application.mirrorRepos = true - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( @@ -542,77 +505,40 @@ class ArgoCDTest { k8sCommands.assertExecuted("kubectl apply -f https://raw.githubusercontent.com/prometheus-community/helm-charts/kube-prometheus-stack-42.0.3/charts/kube-prometheus-stack/charts/crds/crds/crd-servicemonitors.yaml") } - @Test - void 'If urlSeparatorHyphen is set, ensure that hostnames are build correctly '() { - config.application.remote = true - config.features.exampleApps.petclinic.baseDomain = 'petclinic-local' - config.features.exampleApps.nginx.baseDomain = 'nginx-local' - config.application.urlSeparatorHyphen = true - - createArgoCD().install() - - def valuesYaml = parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml') - assertThat(valuesYaml['nginx']['ingress']['hostname'] as String).isEqualTo('production-nginx-helm-umbrella-nginx-local') - - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-production.yaml')['ingress']['hostname']).isEqualTo('production-nginx-helm-nginx-local') - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-staging.yaml')['ingress']['hostname']).isEqualTo('staging-nginx-helm-nginx-local') - - assertThat((parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[2]['spec']['rules'] as List)[0]['host']) - .isEqualTo('broken-application-nginx-local') - - assertPetClinicRepos('LoadBalancer', 'ClusterIP', 'petclinic-local') - } - - @Test - void 'If urlSeparatorHyphen is NOT set, ensure that hostnames are build correctly '() { - config.application.remote = true - config.features.exampleApps.petclinic.baseDomain = 'petclinic.local' - config.features.exampleApps.nginx.baseDomain = 'nginx.local' - config.application.urlSeparatorHyphen = false - - createArgoCD().install() - - def valuesYaml = parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml') - assertThat(valuesYaml['nginx']['ingress']['hostname'] as String).isEqualTo('production.nginx-helm-umbrella.nginx.local') - - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-production.yaml')['ingress']['hostname']).isEqualTo('production.nginx-helm.nginx.local') - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-staging.yaml')['ingress']['hostname']).isEqualTo('staging.nginx-helm.nginx.local') - assertPetClinicRepos('LoadBalancer', 'ClusterIP', 'petclinic.local') - } - @Test void 'For internal SCMM: Use service address in gitops repos'() { def argocd = createArgoCD() argocd.install() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + List filesWithInternalSCMM = findFilesContaining(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir()), argocd.scmmUrlInternal) assertThat(filesWithInternalSCMM).isNotEmpty() - filesWithInternalSCMM = findFilesContaining(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), argocd.scmmUrlInternal) - assertThat(filesWithInternalSCMM).isNotEmpty() } @Test void 'For external SCMM: Use external address in gitops repos'() { - config.scmm.internal = false + config.scm.internal = false + config.scm.scmManager.url = "https://abc" def argocd = createArgoCD() argocd.install() + + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + List filesWithInternalSCMM = findFilesContaining(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir()), argocd.scmmUrlInternal) assertThat(filesWithInternalSCMM).isEmpty() - filesWithInternalSCMM = findFilesContaining(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), argocd.scmmUrlInternal) - assertThat(filesWithInternalSCMM).isEmpty() List filesWithExternalSCMM = findFilesContaining(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir()), "https://abc") assertThat(filesWithExternalSCMM).isNotEmpty() - filesWithExternalSCMM = findFilesContaining(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), "https://abc") - assertThat(filesWithExternalSCMM).isNotEmpty() } @Test void 'Pushes repos with empty name-prefix'() { def argocd = createArgoCD() argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo assertArgoCdYamlPrefixes(argocd.scmmUrlInternal, '') - assertJenkinsEnvironmentVariablesPrefixes('') } @Test @@ -620,20 +546,21 @@ class ArgoCDTest { config.registry.twoRegistries = true createArgoCD().install() - assertJenkinsEnvironmentVariablesPrefixes('') assertJenkinsfileRegistryCredentials() } @Test void 'Pushes repos with name-prefix'() { config.application.namePrefix = 'abc-' - config.application.namePrefixForEnvVars = 'ABC_' def argocd = createArgoCD() argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + assertArgoCdYamlPrefixes(argocd.scmmUrlInternal, 'abc-') - assertJenkinsEnvironmentVariablesPrefixes('ABC_') + } @Test @@ -642,76 +569,27 @@ class ArgoCDTest { createArgoCD().install() for (def petclinicRepo : petClinicRepos) { - if (petclinicRepo.scmmRepoTarget.contains('argocd/petclinic-plain')) { + if (petclinicRepo.repoTarget.contains('argocd/petclinic-plain')) { assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/staging/deployment.yaml').text).contains('runAsUser: null') assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/staging/deployment.yaml').text).contains('runAsGroup: null') } - if (petclinicRepo.scmmRepoTarget.contains('argocd/petclinic-helm')) { + if (petclinicRepo.repoTarget.contains('argocd/petclinic-helm')) { assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).contains('runAsUser: null') assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).contains('runAsGroup: null') } } } - - @Test - void 'configures custom nginx image'() { - config.images.nginx = 'localhost:5000/nginx/nginx:latest' - createArgoCD().install() - - def image = parseActualYaml(nginxHelmJenkinsRepo.absoluteLocalRepoTmpDir + '/k8s/values-shared.yaml')['image'] - assertThat(image['registry']).isEqualTo('localhost:5000') - assertThat(image['repository']).isEqualTo('nginx/nginx') - assertThat(image['tag']).isEqualTo('latest') - - image = parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml')['nginx']['image'] - assertThat(image['registry']).isEqualTo('localhost:5000') - assertThat(image['repository']).isEqualTo('nginx/nginx') - assertThat(image['tag']).isEqualTo('latest') - - def deployment = parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[0] - assertThat(deployment['kind']).as("Did not correctly fetch deployment from broken-application.yaml").isEqualTo("Deploymentz") - assertThat((deployment['spec']['template']['spec']['containers'] as List)[0]['image']).isEqualTo('localhost:5000/nginx/nginx:latest') - - def yamlString = new File(nginxValidationRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text - assertThat(yamlString).startsWith("""image: - registry: localhost:5000 - repository: nginx/nginx - tag: latest -""") - } - - @Test - void 'Sets image pull secrets for nginx'() { - config.registry.createImagePullSecrets = true - config.registry.twoRegistries = true - config.registry.proxyUrl = 'proxy-url' - config.registry.proxyUsername = 'proxy-user' - config.registry.proxyPassword = 'proxy-pw' - - createArgoCD().install() - - assertThat(parseActualYaml(nginxHelmJenkinsRepo.absoluteLocalRepoTmpDir + '/k8s/values-shared.yaml')['global']['imagePullSecrets']) - .isEqualTo(['proxy-registry']) - - assertThat(parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml')['nginx']['global']['imagePullSecrets']) - .isEqualTo(['proxy-registry']) - - def deployment = parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[0] - assertThat(deployment['spec']['imagePullSecrets']).isEqualTo([[name: 'proxy-registry']]) - - assertThat(new File(nginxValidationRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text) - .contains("""global: - imagePullSecrets: - - proxy-registry -""") - } - @Test void 'Skips CRDs for argo cd'() { config.application.skipCrds = true - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['crds']['install']).isEqualTo(false) } @@ -744,7 +622,7 @@ class ArgoCDTest { createArgoCD().install() for (def petclinicRepo : petClinicRepos) { - if (petclinicRepo.scmmRepoTarget.contains('argocd/petclinic-plain')) { + if (petclinicRepo.repoTarget.contains('argocd/petclinic-plain')) { assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, 'Jenkinsfile').text).contains('mvn = cesBuildLib.MavenInDocker.new(this, \'maven:latest\')') } } @@ -758,39 +636,23 @@ class ArgoCDTest { createArgoCD().install() for (def petclinicRepo : petClinicRepos) { - if (petclinicRepo.scmmRepoTarget.contains('argocd/petclinic-plain')) { + if (petclinicRepo.repoTarget.contains('argocd/petclinic-plain')) { assertThat(new File(petclinicRepo.absoluteLocalRepoTmpDir, 'Jenkinsfile').text).contains('mvn = cesBuildLib.MavenInDocker.new(this, \'latest\', dockerRegistryProxyCredentials)') } } - } - @Test - void 'Sets pod resource limits and requests'() { - config.application.podResources = true - - createArgoCD().install() - - assertThat(parseActualYaml(new File(nginxHelmJenkinsRepo.getAbsoluteLocalRepoTmpDir()), 'k8s/values-shared.yaml')['resources'] as Map) - .containsKeys('limits', 'requests') - - assertThat(parseActualYaml(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'apps/nginx-helm-umbrella/values.yaml')['nginx']['resources'] as Map) - .containsKeys('limits', 'requests') - - assertThat(new File(nginxValidationRepo.absoluteLocalRepoTmpDir, '/k8s/values-shared.yaml').text).contains('limits:', 'resources:') - - assertThat((parseActualYaml(brokenApplicationRepo.absoluteLocalRepoTmpDir + '/broken-application.yaml')[0]['spec']['template']['spec']['containers'] as List)[0]['resources'] as Map) - .containsKeys('limits', 'requests') - - assertPetClinicRepos('ClusterIP', 'LoadBalancer', '') - } - @Test void 'ArgoCD with active network policies'() { config.application.netpols = true - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), + ArgoCD.HELM_VALUES_PATH + ) assertThat(parseActualYaml(actualHelmValuesFile)['argo-cd']['global']['networkPolicy']['create']).isEqualTo(true) assertThat(new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), '/argocd/values.yaml').text.contains("namespace: monitoring")) @@ -843,6 +705,7 @@ class ArgoCDTest { } private void assertArgoCdYamlPrefixes(String scmmUrl, String expectedPrefix) { + assertAllYamlFiles(new File(argocdRepo.getAbsoluteLocalRepoTmpDir()), 'projects', 4) { Path file -> def yaml = parseActualYaml(file.toString()) List sourceRepos = yaml['spec']['sourceRepos'] as List @@ -876,7 +739,7 @@ class ArgoCDTest { } } - assertAllYamlFiles(new File(argocdRepo.getAbsoluteLocalRepoTmpDir()), 'applications', 5) { Path file -> + assertAllYamlFiles(new File(argocdRepo.getAbsoluteLocalRepoTmpDir()), 'applications', 4) { Path file -> def yaml = parseActualYaml(file.toString()) assertThat(yaml['spec']['source']['repoURL'] as String) .as("$file repoURL have name prefix") @@ -891,16 +754,6 @@ class ArgoCDTest { .isEqualTo("${expectedPrefix}argocd".toString()) } - assertAllYamlFiles(new File(exampleAppsRepo.getAbsoluteLocalRepoTmpDir()), 'argocd', 7) { Path it -> - def yaml = parseActualYaml(it.toString()) - List yamlDocuments = yaml instanceof List ? yaml : [yaml] - for (def document in yamlDocuments) { - assertThat(document['spec']['source']['repoURL'] as String) - .as("$it repoURL have name prefix") - .startsWith("${scmmUrl}/repo/${expectedPrefix}argocd") - } - } - assertAllYamlFiles(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir()), 'argocd', 1) { Path it -> def yaml = parseActualYaml(it.toString()) @@ -982,7 +835,7 @@ class ArgoCDTest { } ArgoCD createArgoCD() { - def argoCD = new ArgoCDForTest(config, k8sCommands, helmCommands) + def argoCD = ArgoCDForTest.newWithAutoProviders(config, k8sCommands, helmCommands) return argoCD } @@ -1011,14 +864,14 @@ class ArgoCDTest { boolean separatorHyphen = config.application.urlSeparatorHyphen boolean podResources = config.application.podResources - for (ScmmRepo repo : petClinicRepos) { + for (GitRepo repo : petClinicRepos) { def tmpDir = repo.absoluteLocalRepoTmpDir def jenkinsfile = new File(tmpDir, 'Jenkinsfile') assertThat(jenkinsfile).exists() assertJenkinsfileRegistryCredentials() - if (repo.scmmRepoTarget == 'argocd/petclinic-plain') { + if (repo.repoTarget == 'argocd/petclinic-plain') { assertBuildImagesInJenkinsfileReplaced(jenkinsfile) assertThat(new File(tmpDir, 'Dockerfile').text).startsWith('FROM petclinic-value') @@ -1056,7 +909,7 @@ class ArgoCDTest { } } - } else if (repo.scmmRepoTarget == 'argocd/petclinic-helm') { + } else if (repo.repoTarget == 'argocd/petclinic-helm') { assertBuildImagesInJenkinsfileReplaced(jenkinsfile) assertThat(new File(tmpDir, 'k8s/values-shared.yaml').text).contains("type: ${expectedServiceType}") assertThat(new File(tmpDir, 'k8s/values-shared.yaml').text).doesNotContain("type: ${unexpectedServiceType}") @@ -1085,7 +938,7 @@ class ArgoCDTest { } } - } else if (repo.scmmRepoTarget == 'exercises/petclinic-helm') { + } else if (repo.repoTarget == 'exercises/petclinic-helm') { // Does not contain the gitops build lib call, so no build images to replace assertThat(new File(tmpDir, 'k8s/values-shared.yaml').text).contains("type: ${expectedServiceType}") assertThat(new File(tmpDir, 'k8s/values-shared.yaml').text).doesNotContain("type: ${unexpectedServiceType}") @@ -1151,6 +1004,7 @@ class ArgoCDTest { def argocd = setupOperatorTest() argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) def rbacConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_RBAC_PATH) @@ -1165,7 +1019,9 @@ class ArgoCDTest { @Test void 'No files for operator when operator is false'() { - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) def rbacConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_RBAC_PATH) @@ -1176,9 +1032,10 @@ class ArgoCDTest { @Test void 'Deploys with operator without OpenShift configuration'() { - def argoCD = setupOperatorTest(openshift: false) + def argocd = setupOperatorTest(openshift: false) + argocd.install() - argoCD.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) k8sCommands.assertExecuted("kubectl apply -f ${argocdConfigPath}") @@ -1213,8 +1070,9 @@ class ArgoCDTest { "example-apps-production" ]) - def argoCD = setupOperatorTest(openshift: false) - argoCD.install() + def argocd = setupOperatorTest(openshift: false) + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo File rbacPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_RBAC_PATH).toFile() @@ -1257,9 +1115,10 @@ class ArgoCDTest { @Test void 'Deploys with operator with OpenShift configuration'() { - def argoCD = setupOperatorTest(openshift: true) + def argocd = setupOperatorTest(openshift: true) - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) k8sCommands.assertExecuted("kubectl apply -f ${argocdConfigPath}") @@ -1276,12 +1135,13 @@ class ArgoCDTest { @Test void 'Correctly sets resourceInclusions from config'() { - def argoCD = setupOperatorTest() + def argocd = setupOperatorTest() // Set the config to a custom resourceInclusionsCluster value config.features.argocd.resourceInclusionsCluster = 'https://192.168.0.1:6443' - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) @@ -1301,7 +1161,7 @@ class ArgoCDTest { @Test void 'resourceInclusionsCluster from config file trumps ENVs'() { - def argoCD = setupOperatorTest() + def argocd = setupOperatorTest() // Set the config to a custom internalKubernetesApiUrl value config.application.internalKubernetesApiUrl = 'https://192.168.0.1:6443' @@ -1310,9 +1170,11 @@ class ArgoCDTest { withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "100.125.0.1") .and("KUBERNETES_SERVICE_PORT", "443") .execute { - argoCD.install() + argocd.install() } + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) @@ -1333,7 +1195,7 @@ class ArgoCDTest { @Test void 'Sets env variables in ArgoCD components when provided'() { - def argoCD = setupOperatorTest() + def argocd = setupOperatorTest() // Set environment variables for ArgoCD config.features.argocd.env = [ @@ -1341,7 +1203,8 @@ class ArgoCDTest { [name: "ENV_VAR_2", value: "value2"] ] as List - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) @@ -1361,12 +1224,13 @@ class ArgoCDTest { @Test void 'Does not set env variables when none are provided'() { - def argoCD = setupOperatorTest() + def argocd = setupOperatorTest() // Ensure env is an empty list (default) config.features.argocd.env = [] - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) @@ -1382,14 +1246,15 @@ class ArgoCDTest { @Test void 'Sets single env variable in ArgoCD components when provided'() { - def argoCD = setupOperatorTest() + def argocd = setupOperatorTest() // Set a single environment variable for ArgoCD config.features.argocd.env = [ [name: "ENV_VAR_SINGLE", value: "singleValue"] ] as List - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def argocdConfigPath = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH) def yaml = parseActualYaml(argocdConfigPath.toFile().toString()) @@ -1420,9 +1285,10 @@ class ArgoCDTest { @Test void 'Operator config sets server insecure to true when insecure is set'() { config.application.insecure = true - def argoCD = setupOperatorTest() + def argocd = setupOperatorTest() - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def yaml = parseActualYaml(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH).toString()) assertThat(yaml['spec']['server']['insecure']).isEqualTo(true) @@ -1430,9 +1296,10 @@ class ArgoCDTest { @Test void 'Operator config sets server_insecure to false when insecure is not set'() { - def argoCD = setupOperatorTest() + def argocd = setupOperatorTest() - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def yaml = parseActualYaml(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH).toString()) assertThat(yaml['spec']['server']['insecure']).isEqualTo(false) @@ -1442,9 +1309,10 @@ class ArgoCDTest { void 'Generates correct ingress yaml with expected host when insecure is true and not on OpenShift'() { config.application.insecure = true config.features.argocd.url = "http://argocd.localhost" - def argoCD = setupOperatorTest(openshift: false) + def argocd = setupOperatorTest(openshift: false) - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") assertThat(ingressFile) @@ -1463,9 +1331,9 @@ class ArgoCDTest { @Test void 'Does not generate ingress yaml when insecure is false'() { config.application.insecure = false - def argoCD = setupOperatorTest(openshift: false) - - argoCD.install() + def argocd = setupOperatorTest(openshift: false) + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") assertThat(ingressFile) @@ -1476,9 +1344,10 @@ class ArgoCDTest { @Test void 'Does not generate ingress yaml when running on OpenShift'() { config.application.insecure = true - def argoCD = setupOperatorTest(openshift: true) + def argocd = setupOperatorTest(openshift: true) - argoCD.install() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") assertThat(ingressFile) @@ -1489,9 +1358,10 @@ class ArgoCDTest { @Test void 'Does not generate ingress yaml when insecure is false and OpenShift is true'() { config.application.insecure = false - def argoCD = setupOperatorTest(openshift: true) + def argocd = setupOperatorTest(openshift: true) + argocd.install() - argoCD.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") assertThat(ingressFile) @@ -1501,17 +1371,17 @@ class ArgoCDTest { @Test void 'Central Bootstrapping for Tenant Applications'() { - setup() + setupDedicatedInstanceMode() +// this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") assertThat(ingressFile) .as("Ingress file should not be generated when both flags are false") .doesNotExist() } - @Test void 'GOP DedicatedInstances Central templating works correctly'() { - setup() + setupDedicatedInstanceMode() //Central Applications assertThat(new File(argocdRepo.getAbsoluteLocalRepoTmpDir() + "${prefixPathCentral}applications/argocd.yaml")).exists() assertThat(new File(argocdRepo.getAbsoluteLocalRepoTmpDir() + "${prefixPathCentral}applications/bootstrap.yaml")).exists() @@ -1531,7 +1401,7 @@ class ArgoCDTest { assertThat(bootstrapYaml['metadata']['name']).isEqualTo('testPrefix-bootstrap') assertThat(bootstrapYaml['metadata']['namespace']).isEqualTo('argocd') assertThat(bootstrapYaml['spec']['project']).isEqualTo('testPrefix') - assertThat(bootstrapYaml['spec']['source']['repoURL']).isEqualTo("scmm.testhost/scm/repo/testPrefix-argocd/argocd") + assertThat(bootstrapYaml['spec']['source']['repoURL']).isEqualTo("scmm.testhost/scm/repo/testPrefix-argocd/argocd.git") assertThat(clusterResourcesYaml['metadata']['name']).isEqualTo('testPrefix-cluster-resources') assertThat(clusterResourcesYaml['metadata']['namespace']).isEqualTo('argocd') @@ -1549,54 +1419,39 @@ class ArgoCDTest { assertThat(tenantProject['metadata']['name']).isEqualTo('testPrefix') assertThat(tenantProject['metadata']['namespace']).isEqualTo('argocd') def sourceRepos = (List) tenantProject['spec']['sourceRepos'] - assertThat(sourceRepos[0]).isEqualTo('scmm.testhost/scm/repo/testPrefix-argocd/argocd') - assertThat(sourceRepos[1]).isEqualTo('scmm.testhost/scm/repo/testPrefix-argocd/cluster-resources') + assertThat(sourceRepos[0]).isEqualTo('scmm.testhost/scm/repo/testPrefix-argocd/argocd.git') + assertThat(sourceRepos[1]).isEqualTo('scmm.testhost/scm/repo/testPrefix-argocd/cluster-resources.git') } @Test void 'Cluster Resource Misc templating'() { - setup() + setupDedicatedInstanceMode() + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo assertThat(new File(clusterResourcesRepo.getAbsoluteLocalRepoTmpDir() + "/argocd/misc.yaml")).exists() def miscYaml = new YamlSlurper().parse(Path.of clusterResourcesRepo.getAbsoluteLocalRepoTmpDir(), "/argocd/misc.yaml") assertThat(miscYaml['metadata']['name']).isEqualTo('testPrefix-misc') assertThat(miscYaml['metadata']['namespace']).isEqualTo('argocd') } - @Test - void 'generate example-apps bootstrapping application via ArgoApplication when true'() { - setup() - assertThat(new File(tenantBootstrap.getAbsoluteLocalRepoTmpDir() + "/applications/bootstrap.yaml")).exists() - assertThat(new File(tenantBootstrap.getAbsoluteLocalRepoTmpDir() + "/applications/argocd-application-example-apps-testPrefix-argocd.yaml")).exists() - } - - @Test - void 'not generating example-apps bootstrapping application via ArgoApplication when false'() { - config.content.examples = false - setup() - assertThat(new File(tenantBootstrap.getAbsoluteLocalRepoTmpDir() + "/applications/bootstrap.yaml")).exists() - assertThat(new File(tenantBootstrap.getAbsoluteLocalRepoTmpDir() + "/applications/argocd-application-example-apps-testPrefix-argocd.yaml")).doesNotExist() - } - @Test void 'Append namespaces to Argocd argocd-default-cluster-config secrets'() { config.application.namespaces.dedicatedNamespaces = new LinkedHashSet(['dedi-test1', 'dedi-test2', 'dedi-test3']) config.application.namespaces.tenantNamespaces = new LinkedHashSet(['tenant-test1', 'tenant-test2', 'tenant-test3']) - setup() - k8sCommands.assertExecuted('kubectl get secret argocd-default-cluster-config -nargocd -ojsonpath={.data.namespaces}') + setupDedicatedInstanceMode() + k8sCommands.assertExecuted('kubectl get secret argocd-default-cluster-config -n argocd -ojsonpath={.data.namespaces}') k8sCommands.assertExecuted('kubectl patch secret argocd-default-cluster-config -n argocd --patch-file=/tmp/gitops-playground-patch-') } - @Test void 'multiTenant folder gets deleted correctly if not in dedicated mode'() { config.multiTenant.useDedicatedInstance = false def argocd = createArgoCD() argocd.install() - - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'multiTenant/')).doesNotExist() - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/')).exists() - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/')).exists() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + assertThat(Path.of(this.argocdRepo.getAbsoluteLocalRepoTmpDir(), 'multiTenant/')).doesNotExist() + assertThat(Path.of(this.argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/')).exists() + assertThat(Path.of(this.argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/')).exists() } @Test @@ -1605,15 +1460,16 @@ class ArgoCDTest { def argocd = createArgoCD() argocd.install() - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'multiTenant/')).exists() - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/')).doesNotExist() - assertThat(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/')).doesNotExist() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + assertThat(Path.of(this.argocdRepo.getAbsoluteLocalRepoTmpDir(), 'multiTenant/')).exists() + assertThat(Path.of(this.argocdRepo.getAbsoluteLocalRepoTmpDir(), 'applications/')).doesNotExist() + assertThat(Path.of(this.argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/')).doesNotExist() } @Test void 'RBACs generated correctly'() { config.application.namespaces.tenantNamespaces = new LinkedHashSet(['testprefix-tenant-test1', 'testprefix-tenant-test2', 'testprefix-tenant-test3']) - setup() + setupDedicatedInstanceMode() File rbacFolder = new File(argocdRepo.getAbsoluteLocalRepoTmpDir() + "/operator/rbac") File rbacTenantFolder = new File(argocdRepo.getAbsoluteLocalRepoTmpDir() + "/operator/rbac/tenant") @@ -1652,8 +1508,9 @@ class ArgoCDTest { void 'Operator RBAC includes node access rules when not on OpenShift'() { config.application.namePrefix = "testprefix-" - def argoCD = setupOperatorTest(openshift: false) - argoCD.install() + def argocd = setupOperatorTest(openshift: false) + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo print config.toMap() @@ -1673,8 +1530,9 @@ class ArgoCDTest { void 'Operator RBAC does not include node access rules when on OpenShift'() { config.application.namePrefix = "testprefix-" - def argoCD = setupOperatorTest(openshift: true) - argoCD.install() + def argocd = setupOperatorTest(openshift: true) + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo File rbacDir = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_RBAC_PATH).toFile() File roleFile = new File(rbacDir, "role-argocd-testprefix-monitoring.yaml") @@ -1693,7 +1551,12 @@ class ArgoCDTest { void 'If not using mirror, ensure source repos in cluster-resources got right URL'() { config.application.mirrorRepos = false - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') clusterRessourcesYaml['spec']['sourceRepos'] @@ -1729,7 +1592,11 @@ class ArgoCDTest { void 'If using mirror, ensure source repos in cluster-resources got right URL'() { config.application.mirrorRepos = true - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') clusterRessourcesYaml['spec']['sourceRepos'] @@ -1756,50 +1623,46 @@ class ArgoCDTest { @Test void 'If using mirror with GitLab, ensure source repos in cluster-resources got right URL'() { config.application.mirrorRepos = true - config.scmm.provider = 'gitlab' - - createArgoCD().install() + config.scm.scmProviderType = 'GITLAB' + config.scm.gitlab.url = 'https://testGitLab.com/testgroup' + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') clusterRessourcesYaml['spec']['sourceRepos'] assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/mailhog.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/ingress-nginx.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', - 'http://scmm.scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git' - ) - assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/kube-prometheus-stack', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/mailhog', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/ingress-nginx', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/external-secrets', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/vault', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/repo/3rd-party-dependencies/cert-manager' + 'https://testGitLab.com/testgroup/3rd-party-dependencies/kube-prometheus-stack.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/mailhog.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/ingress-nginx.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/external-secrets.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/vault.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/cert-manager.git' ) } - @Test void 'If using mirror with GitLab with prefix, ensure source repos in cluster-resources got right URL'() { config.application.mirrorRepos = true - config.scmm.provider = 'gitlab' + config.scm.scmProviderType = 'GITLAB' + config.scm.gitlab.url = "https://testGitLab.com/testgroup" config.application.namePrefix = 'test1-' - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') clusterRessourcesYaml['spec']['sourceRepos'] assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).contains( - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/kube-prometheus-stack.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/mailhog.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/ingress-nginx.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/external-secrets.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/vault.git', - 'http://scmm.test1-scm-manager.svc.cluster.local/scm/3rd-party-dependencies/cert-manager.git' + 'https://testGitLab.com/testgroup/3rd-party-dependencies/kube-prometheus-stack.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/mailhog.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/ingress-nginx.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/external-secrets.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/vault.git', + 'https://testGitLab.com/testgroup/3rd-party-dependencies/cert-manager.git' ) assertThat(clusterRessourcesYaml['spec']['sourceRepos'] as List).doesNotContain( @@ -1817,7 +1680,9 @@ class ArgoCDTest { config.application.mirrorRepos = true config.application.namePrefix = 'test1-' - createArgoCD().install() + def argocd = createArgoCD() + argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo def clusterRessourcesYaml = new YamlSlurper().parse(Path.of argocdRepo.getAbsoluteLocalRepoTmpDir(), 'projects/cluster-resources.yaml') clusterRessourcesYaml['spec']['sourceRepos'] @@ -1842,16 +1707,17 @@ class ArgoCDTest { ) } - - void setup() { + void setupDedicatedInstanceMode() { config.application.namePrefix = 'testPrefix-' - config.multiTenant.centralScmUrl = 'scmm.testhost/scm' - config.multiTenant.username = 'testUserName' - config.multiTenant.password = 'testPassword' + config.multiTenant.scmManager.url = 'scmm.testhost/scm' + config.multiTenant.scmManager.username = 'testUserName' + config.multiTenant.scmManager.password = 'testPassword' config.multiTenant.useDedicatedInstance = true - this.argocd = setupOperatorTest() argocd.install() + this.argocdRepo = (argocd as ArgoCDForTest).argocdRepo + this.clusterResourcesRepo = (argocd as ArgoCDForTest).clusterResourcesRepo + } protected ArgoCD setupOperatorTest(Map options = [:]) { @@ -1946,43 +1812,69 @@ class ArgoCDTest { } - class ArgoCDForTest extends ArgoCD { - ArgoCDForTest(Config config, CommandExecutorForTest k8sCommands, CommandExecutorForTest helmCommands) { - super(config, new K8sClientForTest(config, k8sCommands), new HelmClient(helmCommands), new FileSystemUtils(), - new TestScmmRepoProvider(config, new FileSystemUtils())) - mockPrefixActiveNamespaces(config) + static class ArgoCDForTest extends ArgoCD { + final Config cfg + GitProvider tenantMock + + // **Factory** + static ArgoCDForTest newWithAutoProviders(Config cfg, + CommandExecutorForTest k8sCommands, + CommandExecutorForTest helmCommands) { + + def prov = TestGitProvider.buildProviders(cfg) + return new ArgoCDForTest( + cfg, + k8sCommands, + helmCommands, + prov.tenant as GitProvider, + prov.central as GitProvider) } - @Override - protected initRepos() { - super.initRepos() - - argocdRepo = argocdRepoInitializationAction.repo - actualHelmValuesFile = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), HELM_VALUES_PATH) - clusterResourcesRepo = clusterResourcesInitializationAction.repo - if (config.multiTenant.useDedicatedInstance) { - tenantBootstrap = tenantBootstrapInitializationAction.repo - } + // ---- Only keep the real constructor ---- + ArgoCDForTest(Config cfg, + CommandExecutorForTest k8sCommands, + CommandExecutorForTest helmCommands, + GitProvider tenantProvider, + GitProvider centralProvider) { + super( + cfg, + new K8sClientForTest(cfg, k8sCommands), + new HelmClient(helmCommands), + new FileSystemUtils(), + new TestGitRepoFactory(cfg, new FileSystemUtils()), + new GitHandlerForTests(cfg, tenantProvider, centralProvider) + ) + this.cfg = cfg + this.tenantMock = tenantProvider + mockPrefixActiveNamespaces(cfg) + } - if (config.content.examples) { - exampleAppsRepo = exampleAppsInitializationAction.repo - nginxHelmJenkinsRepo = nginxHelmJenkinsInitializationAction.repo - nginxValidationRepo = nginxValidationInitializationAction.repo - brokenApplicationRepo = brokenApplicationInitializationAction.repo - petClinicRepos = petClinicInitializationActions.collect { it.repo } - } + GitRepo getArgocdRepo() { + return this.argocdRepoInitializationAction?.repo + } + + GitRepo getClusterResourcesRepo() { + return this.clusterResourcesInitializationAction?.repo + } + + GitRepo getTenantBootstrapRepo() { + return this.tenantBootstrapInitializationAction?.repo } @Override - protected CloneCommand gitClone() { - return gitCloneMock + protected initCentralRepos() { + super.initCentralRepos() + } + + + @Override + protected initTenantRepos() { + super.initTenantRepos() } - } - private Map parseActualYaml(File folder, String file) { - return parseActualYaml(Path.of(folder.absolutePath, file).toString()) } + private Map parseActualYaml(String pathToYamlFile) { File yamlFile = new File(pathToYamlFile) def ys = new YamlSlurper() diff --git a/src/test/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategyTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategyTest.groovy index 7e5d07e9f..95c680322 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategyTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategyTest.groovy @@ -1,9 +1,15 @@ package com.cloudogu.gitops.features.deployment import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.features.git.config.ScmTenantSchema +import com.cloudogu.gitops.features.git.config.ScmTenantSchema.ScmManagerTenantConfig +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.utils.git.ScmManagerMock import com.cloudogu.gitops.utils.FileSystemUtils -import com.cloudogu.gitops.utils.TestScmmRepoProvider +import com.cloudogu.gitops.utils.git.GitHandlerForTests +import com.cloudogu.gitops.utils.git.TestGitRepoFactory import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test @@ -11,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat class ArgoCdApplicationStrategyTest { private File localTempDir + GitHandler gitHandler = new GitHandlerForTests(new Config(), new ScmManagerMock()) @Test void 'deploys feature using argo CD'() { @@ -106,9 +113,11 @@ spec: gitName: 'Cloudogu', gitEmail: 'hello@cloudogu.com' ), - scmm: new Config.ScmmSchema( - username: "dont-care-username", - password: "dont-care-password", + scm: new ScmTenantSchema( + scmManager: new ScmManagerTenantConfig( + username: "dont-care-username", + password: "dont-care-password" + ) ), features: new Config.FeaturesSchema( argocd: new Config.ArgoCDSchema( @@ -117,25 +126,16 @@ spec: ) ) - - def repoProvider = new TestScmmRepoProvider(config, new FileSystemUtils()) { - @Override - ScmmRepo getRepo(String repoTarget) { - def repo = super.getRepo(repoTarget) - localTempDir = new File(repo.getAbsoluteLocalRepoTmpDir()) - - return repo - } - + def repoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) { @Override - ScmmRepo getRepo(String repoTarget, Boolean isCentralRepo) { - def repo = super.getRepo(repoTarget, isCentralRepo) + GitRepo getRepo(String repoTarget, GitProvider gitProvider) { + def repo = super.getRepo(repoTarget, gitProvider) localTempDir = new File(repo.getAbsoluteLocalRepoTmpDir()) return repo } } - return new ArgoCdApplicationStrategy(config, new FileSystemUtils(), repoProvider) + return new ArgoCdApplicationStrategy(config, new FileSystemUtils(), repoProvider, gitHandler) } } \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/features/git/GitHandlerTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/git/GitHandlerTest.groovy new file mode 100644 index 000000000..5d173ae8c --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/features/git/GitHandlerTest.groovy @@ -0,0 +1,239 @@ +package com.cloudogu.gitops.features.git + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.features.deployment.HelmStrategy +import com.cloudogu.gitops.features.git.config.util.ScmProviderType +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.scmmanager.ScmManager +import com.cloudogu.gitops.utils.FileSystemUtils +import com.cloudogu.gitops.utils.K8sClient +import com.cloudogu.gitops.utils.NetworkingUtils +import com.cloudogu.gitops.utils.git.GitHandlerForTests +import com.cloudogu.gitops.utils.git.GitlabMock +import com.cloudogu.gitops.utils.git.ScmManagerMock +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.* +import static org.mockito.Mockito.* + +class GitHandlerTest { + + private static Config config(Map overrides = [:]) { + Map base = [ + application: [ + namePrefix: '' + ], + scm : [ + scmProviderType: ScmProviderType.SCM_MANAGER, // default + scmManager : [ + internal: true + ], + gitlab : [ + url: '' + ] + ], + multiTenant: [ + scmManager : [ url: '' ], + gitlab : [ url: '' ], + useDedicatedInstance: false + ] + ] + Map merged = deepMerge(base, overrides) + return new Config().fromMap(merged) + } + + /** simple deep merge for nested maps */ + @SuppressWarnings('unchecked') + private static Map deepMerge(Map left, Map right) { + Map out = [:] + left + right.each { k, v -> + if (v instanceof Map && left[k] instanceof Map) { + out[k] = deepMerge((Map) left[k], (Map) v) + } else { + out[k] = v + } + } + return out + } + + private static GitHandler handler(Config cfg) { + return new GitHandler( + cfg, + mock(HelmStrategy), + mock(FileSystemUtils), + mock(K8sClient), + mock(NetworkingUtils) + ) + } + + // ---------- validate() ------------------------------------------------------------ + + @Test + void 'validate(): ScmManager external url sets internal=false and urlForJenkins equals url'() { + def cfg = config([ + application: [namePrefix: 'fv40-'], + scm: [ + scmManager: [url: 'https://scmm.example.com/scm', internal: true] + ] + ]) + def gh = handler(cfg) + + gh.validate() + + assertFalse(cfg.scm.scmManager.internal) + assertEquals('https://scmm.example.com/scm', cfg.scm.scmManager.urlForJenkins) + } + + + @Test + void 'validate(): GitLab chosen, provider switched, scmm nulled, missing PAT or parentGroupId throws'() { + def cfg = config([ + scm: [ + gitlab: [url: 'https://gitlab.example.com'] + ] + ]) + def gh = handler(cfg) + + def ex = assertThrows(RuntimeException) { gh.validate() } + assertTrue(ex.message.toLowerCase().contains('gitlab')) + assertEquals(ScmProviderType.GITLAB, cfg.scm.scmProviderType) + assertNull(cfg.scm.scmManager) + } + + // ---------- getResourcesScm() ----------------------------------------------------- + + @Test + void 'getResourcesScm(): central wins over tenant'() { + def cfg = config() + def gitHandler = handler(cfg) + + gitHandler.tenant = mock(GitProvider, 'tenant') + gitHandler.central = mock(GitProvider, 'central') + + assertSame(gitHandler.central, gitHandler.getResourcesScm()) + } + + @Test + void 'getResourcesScm(): tenant returned when central absent, throws when none'() { + def cfg = config() + def gitHandler = handler(cfg) + + gitHandler.tenant = mock(GitProvider) + assertSame(gitHandler.tenant, gitHandler.getResourcesScm()) + + gitHandler.tenant = null + def ex = assertThrows(IllegalStateException) { gitHandler.getResourcesScm() } + assertTrue(ex.message.contains('No SCM provider')) + } + + // ---------- enable(): SCM_MANAGER tenant only ------------------------------------ + @Test + void 'ScmManager tenant-only: tenant gets 6 repositories'() { + def cfg = new Config().fromMap([ + scm:[scmManager:[internal:true], gitlab:[url:'']], + multiTenant:[useDedicatedInstance:false] + ]) + + def tenant = new ScmManagerMock() + def gitHandler = new GitHandlerForTests(cfg, tenant) + + gitHandler.enable() + + assertEquals('scm-manager', cfg.scm.scmManager.namespace) + + assertTrue(tenant.createdRepos.contains('argocd/argocd')) + assertTrue(tenant.createdRepos.contains('argocd/cluster-resources')) + assertTrue(tenant.createdRepos.contains('3rd-party-dependencies/spring-boot-helm-chart')) + assertTrue(tenant.createdRepos.contains('3rd-party-dependencies/spring-boot-helm-chart-with-dependency')) + assertTrue(tenant.createdRepos.contains('3rd-party-dependencies/gitops-build-lib')) + assertTrue(tenant.createdRepos.contains('3rd-party-dependencies/ces-build-lib')) + assertEquals(6, tenant.createdRepos.size()) + + // No central provider in tenant-only scenario + assertNull(gitHandler.getCentral()) + } + + @Test + void 'ScmManager dedicated: central gets 2 repos: tenant gets 1 and 4 dependencies'() { + def cfg = config([ + application: [namePrefix: 'fv40-'], + scm : [ + scmProviderType: ScmProviderType.SCM_MANAGER, + scmManager : [internal: true], + gitlab : [url: ''] + ], + multiTenant: [ + useDedicatedInstance: true, + scmManager: [url: ''], + gitlab : [url: ''] + ] + ]) + + def tenant = new ScmManagerMock(namePrefix: 'fv40-') + def central = new ScmManagerMock(namePrefix: 'fv40-') + def gitHandler = new GitHandlerForTests(cfg, tenant, central) + + gitHandler.enable() + + // Central: argocd + cluster-resources = 2 + assertTrue(central.createdRepos.contains('fv40-argocd/argocd')) + assertTrue(central.createdRepos.contains('fv40-argocd/cluster-resources')) + assertEquals(2, central.createdRepos.size()) + + // Tenant: only argocd + 4 dependencies = 5 (no cluster-resources) + assertTrue(tenant.createdRepos.contains('fv40-argocd/argocd')) + assertFalse(tenant.createdRepos.contains('fv40-argocd/cluster-resources')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/spring-boot-helm-chart')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/spring-boot-helm-chart-with-dependency')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/gitops-build-lib')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/ces-build-lib')) + assertEquals(5, tenant.createdRepos.size()) + } + + @Test + void 'Gitlab dedicated: same layout as ScmManager dedicated'() { + def cfg = config([ + application: [namePrefix: 'fv40-'], + scm : [ + scmProviderType: ScmProviderType.GITLAB, + gitlab : [url: 'https://gitlab.example.com', password: 'pat', parentGroupId: 123], + scmManager : [internal: true] + ], + multiTenant: [ + useDedicatedInstance: true, + gitlab: [url: 'https://gitlab.example.com', password: 'pat2', parentGroupId: 456], + scmManager: [url: ''] + ] + ]) + + // Assumes your GitlabMock has a similar contract to ScmManagerMock (collects createdRepos) + def tenant = new GitlabMock(base: new URI(cfg.scm.gitlab.url), namePrefix: 'fv40-') + def central = new GitlabMock(base: new URI(cfg.multiTenant.gitlab.url), namePrefix: 'fv40-') + def gitHandler = new GitHandlerForTests(cfg, tenant, central) + + gitHandler.enable() + + // Central: argocd + cluster-resources + assertTrue(central.createdRepos.contains('fv40-argocd/argocd')) + assertTrue(central.createdRepos.contains('fv40-argocd/cluster-resources')) + assertEquals(2, central.createdRepos.size()) + + // Tenant: argocd only + 4 dependencies + assertTrue(tenant.createdRepos.contains('fv40-argocd/argocd')) + assertFalse(tenant.createdRepos.contains('fv40-argocd/cluster-resources')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/spring-boot-helm-chart')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/spring-boot-helm-chart-with-dependency')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/gitops-build-lib')) + assertTrue(tenant.createdRepos.contains('fv40-3rd-party-dependencies/ces-build-lib')) + assertEquals(5, tenant.createdRepos.size()) + } + + @Test + void 'withOrgPrefix helper behaves as expected'() { + assertEquals('argocd/argocd', GitHandler.withOrgPrefix('', 'argocd/argocd')) + assertEquals('argocd/argocd', GitHandler.withOrgPrefix(null, 'argocd/argocd')) + assertEquals('fv40-argocd/argocd', GitHandler.withOrgPrefix('fv40-', 'argocd/argocd')) + } + + +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/git/GitRepoTest.groovy b/src/test/groovy/com/cloudogu/gitops/git/GitRepoTest.groovy new file mode 100644 index 000000000..c7edecf30 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/git/GitRepoTest.groovy @@ -0,0 +1,240 @@ +package com.cloudogu.gitops.git + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.git.providers.AccessRole +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.Scope +import com.cloudogu.gitops.utils.git.ScmManagerMock +import com.cloudogu.gitops.utils.FileSystemUtils +import com.cloudogu.gitops.utils.git.TestGitRepoFactory +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock + +import static groovy.test.GroovyAssert.shouldFail +import static org.assertj.core.api.Assertions.assertThat + +class GitRepoTest { + + + public static final String expectedNamespace = "namespace" + public static final String expectedRepo = "repo" + Config config = Config.fromMap([ + application: [ + gitName : "Cloudogu", + gitEmail: "hello@cloudogu.com" + ], + scm : [ + scmManager: [ + username: "dont-care-username", + password: "dont-care-password" + ] + ] + ]) + + TestGitRepoFactory repoProvider = new TestGitRepoFactory(config, new FileSystemUtils()) + + @Mock + GitProvider gitProvider + + ScmManagerMock scmManagerMock + + @BeforeEach + void setup() { + scmManagerMock = new ScmManagerMock() + } + + + @Test + void "writes file"() { + def repo = createRepo("", scmManagerMock) + repo.writeFile("test.txt", "the file's content") + + def expectedFile = new File("$repo.absoluteLocalRepoTmpDir/test.txt") + assertThat(expectedFile.getText()).is("the file's content") + } + + @Test + void "overwrites file"() { + def repo = createRepo("", scmManagerMock) + def tempDir = repo.absoluteLocalRepoTmpDir + + def existingFile = new File("$tempDir/already-exists.txt") + existingFile.createNewFile() + existingFile.text = "already existing content" + + repo.writeFile("already-exists.txt", "overwritten content") + + def expectedFile = new File("$tempDir/already-exists.txt") + assertThat(expectedFile.getText()).is("overwritten content") + } + + @Test + void "writes file and creates subdirectory"() { + def repo = createRepo("", scmManagerMock) + def tempDir = repo.absoluteLocalRepoTmpDir + repo.writeFile("subdirectory/test.txt", "the file's content") + + def expectedFile = new File("$tempDir/subdirectory/test.txt") + assertThat(expectedFile.getText()).is("the file's content") + } + + @Test + void "throws error when directory conflicts with existing file"() { + def repo = createRepo("", scmManagerMock) + def tempDir = repo.absoluteLocalRepoTmpDir + new File("$tempDir/test.txt").mkdir() + + shouldFail(FileNotFoundException) { + repo.writeFile("test.txt", "the file's content") + } + } + + @Test + void 'Creates repo with empty name-prefix'() { + def repo = createRepo('expectedRepoTarget', scmManagerMock) + assertThat(repo.repoTarget).isEqualTo('expectedRepoTarget') + } + + @Test + void 'Creates repo with name-prefix'() { + config.application.namePrefix = 'abc-' + def repo = createRepo('expectedRepoTarget', scmManagerMock) + assertThat(repo.repoTarget).isEqualTo('abc-expectedRepoTarget') + } + + @Test + void 'Creates repo without name-prefix when in namespace 3rd-party-deps'() { + + config.application.namePrefix = 'abc-' + def repo = createRepo("${GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/foo", scmManagerMock) + assertThat(repo.repoTarget).isEqualTo("${GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/foo".toString()) + } + + @Test + void 'Clones and checks out main'() { + def repo = createRepo("", scmManagerMock) + + repo.cloneRepo() + def HEAD = new File(repo.absoluteLocalRepoTmpDir, '.git/HEAD') + assertThat(HEAD.text).isEqualTo("ref: refs/heads/main\n") + assertThat(new File(repo.absoluteLocalRepoTmpDir, 'README.md')).exists() + } + + @Test + void 'pushes changes to remote directory'() { + def repo = createRepo("", scmManagerMock) + + repo.cloneRepo() + def readme = new File(repo.absoluteLocalRepoTmpDir, 'README.md') + readme.text = 'This text should be in the readme afterwards' + repo.commitAndPush("The commit message") + + def commits = Git.open(new File(repo.absoluteLocalRepoTmpDir)).log().setMaxCount(1).all().call().collect() + assertThat(commits.size()).isEqualTo(1) + assertThat(commits[0].fullMessage).isEqualTo("The commit message") + assertThat(commits[0].authorIdent.emailAddress).isEqualTo('hello@cloudogu.com') + assertThat(commits[0].authorIdent.name).isEqualTo('Cloudogu') + assertThat(commits[0].committerIdent.emailAddress).isEqualTo('hello@cloudogu.com') + assertThat(commits[0].committerIdent.name).isEqualTo("Cloudogu") + + List tags = Git.open(new File(repo.absoluteLocalRepoTmpDir)).tagList().call() + assertThat(tags.size()).isEqualTo(0) + } + + @Test + void 'pushes changes to remote directory with tag'() { + def repo = createRepo("", scmManagerMock) + def expectedTag = '1.0' + + repo.cloneRepo() + def readme = new File(repo.absoluteLocalRepoTmpDir, 'README.md') + readme.text = 'This text should be in the readme afterwards' + // Create existing tag to test for idempotence + Git.open(new File(repo.absoluteLocalRepoTmpDir)).tag().setName(expectedTag).call() + + repo.commitAndPush("The commit message", expectedTag) + + + List tags = Git.open(new File(repo.absoluteLocalRepoTmpDir)).tagList().call() + assertThat(tags.size()).isEqualTo(1) + assertThat(tags[0].name).isEqualTo("refs/tags/$expectedTag".toString()) + // It would be a good idea to check if the git tag is set on the commit. + // However, it's extremely complicated with jgit + // The "official" example code throws an exception here: Ref peeledRef = repository.getRefDatabase().peel(ref) + // https://github.com/centic9/jgit-cookbook/blob/d923e18b2ce2e55761858fd2e8e402dd252e0766/src/main/java/org/dstadler/jgit/porcelain/ListTags.java + // 🤷 + } + + @Test + void 'creates repository and sets permission when new and username present'() { + + def repoTarget = "foo/bar" + def repo = createRepo(repoTarget, scmManagerMock) + scmManagerMock.nextCreateResults = [true] // simulate "new repo" + scmManagerMock.gitOpsUsername = 'foo-gitops' // username available + + def created = repo.createRepositoryAndSetPermission(repoTarget, 'testdescription', true) + + assertThat(created).isTrue() + + // Verify that repo was created + assertThat(scmManagerMock.createdRepos).containsExactly(repoTarget) + + // Verify permission call + assertThat(scmManagerMock.permissionCalls).hasSize(1) + def call = scmManagerMock.permissionCalls[0] + assertThat(call.repoTarget).isEqualTo(repoTarget) + assertThat(call.principal).isEqualTo('foo-gitops') + assertThat(call.role).isEqualTo(AccessRole.WRITE) + assertThat(call.scope).isEqualTo(Scope.USER) + } + + + @Test + void 'does not set permission when repository already exists'() { + def repoTarget = "foo/bar" + def repo = createRepo(repoTarget, scmManagerMock) + + scmManagerMock.nextCreateResults = [false] // simulate "already exists" + scmManagerMock.gitOpsUsername = 'foo-gitops' // even with username, no permission should be set + + def created = repo.createRepositoryAndSetPermission(repoTarget, 'desc', true) + + assertThat(created).isFalse() + + // Created was attempted once + assertThat(scmManagerMock.createdRepos).containsExactly(repoTarget) + + // No permission calls + assertThat(scmManagerMock.permissionCalls).isEmpty() + } + + + @Test + void 'does not set permission when no GitOps username is configured'() { + def repoTarget = "foo/bar" + def scmManagerMock = new ScmManagerMock() + def repo = createRepo(repoTarget, scmManagerMock) + + scmManagerMock.nextCreateResults = [true] // repo is new + scmManagerMock.gitOpsUsername = null // no username + + def created = repo.createRepositoryAndSetPermission(repoTarget, 'desc', true) + + assertThat(created).isTrue() + + // Repo created + assertThat(scmManagerMock.createdRepos).containsExactly(repoTarget) + + // No permission calls because username missing + assertThat(scmManagerMock.permissionCalls).isEmpty() + } + + + private GitRepo createRepo(String repoTarget = "${expectedNamespace}/${expectedRepo}", ScmManagerMock scmManagerMock) { + return repoProvider.getRepo(repoTarget, scmManagerMock) + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/scmm/jgit/InsecureCredentialProviderTest.groovy b/src/test/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProviderTest.groovy similarity index 97% rename from src/test/groovy/com/cloudogu/gitops/scmm/jgit/InsecureCredentialProviderTest.groovy rename to src/test/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProviderTest.groovy index 7d54d87b0..1d17dec05 100644 --- a/src/test/groovy/com/cloudogu/gitops/scmm/jgit/InsecureCredentialProviderTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProviderTest.groovy @@ -1,4 +1,4 @@ -package com.cloudogu.gitops.scmm.jgit +package com.cloudogu.gitops.git.jgit.helpers import org.eclipse.jgit.transport.CredentialItem @@ -44,4 +44,4 @@ class InsecureCredentialProviderTest { assertThat(skipRepository.value).isTrue() assertThat(skipAlways.value).isFalse() } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerTest.groovy b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerTest.groovy new file mode 100644 index 000000000..c501f5a42 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerTest.groovy @@ -0,0 +1,166 @@ +package com.cloudogu.gitops.git.providers.scmmanager + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig +import com.cloudogu.gitops.git.providers.AccessRole +import com.cloudogu.gitops.git.providers.RepoUrlScope +import com.cloudogu.gitops.git.providers.Scope +import com.cloudogu.gitops.git.providers.scmmanager.api.Repository +import com.cloudogu.gitops.git.providers.scmmanager.api.RepositoryApi +import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApiClient +import com.cloudogu.gitops.utils.K8sClient +import com.cloudogu.gitops.utils.NetworkingUtils +import okhttp3.internal.http.RealResponseBody +import okio.BufferedSource +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import retrofit2.Call +import retrofit2.Response + +import static org.junit.jupiter.api.Assertions.* +import static org.mockito.ArgumentMatchers.* +import static org.mockito.Mockito.* +import org.junit.jupiter.api.function.Executable + +@ExtendWith(MockitoExtension) +class ScmManagerTest { + + private Config config + + @Mock ScmManagerConfig scmmCfg + @Mock K8sClient k8s + @Mock NetworkingUtils net + @Mock ScmManagerUrlResolver urls + @Mock ScmManagerApiClient apiClient + @Mock RepositoryApi repoApi + + @BeforeEach + void setup() { + config = new Config( + application: new Config.ApplicationSchema( + insecure: false, + namePrefix: "fv40-", + runningInsideK8s: true + ) + ) + + lenient().when(scmmCfg.getCredentials()).thenReturn(new Credentials("user","password")) + lenient(). when(scmmCfg.getGitOpsUsername()).thenReturn("gitops-bot") + + lenient().when(urls.inClusterBase()).thenReturn(new URI("http://scmm.ns.svc.cluster.local/scm")) + lenient().when(urls.inClusterRepoPrefix()).thenReturn("http://scmm.ns.svc.cluster.local/scm/repo/fv40-") + lenient().when(urls.clientApiBase()).thenReturn(new URI("http://nodeport/scm/api/v2/")) + + lenient().when(apiClient.repositoryApi()).thenReturn(repoApi) + } + + private ScmManager newSchManager() { + return new ScmManager(config, scmmCfg, urls, apiClient) + } + + private static Call callReturningSuccess(int code) { + def call = mock(Call) + when(call.execute()).thenReturn(Response.success(code, null)) + call + } + private static Call callReturningError(int code) { + def call = mock(Call) + def body = new RealResponseBody('ignored', 0, mock(BufferedSource)) + when(call.execute()).thenReturn(Response.error(code, body)) + call + } + + + @Test + void 'createRepository returns true on 201 and false on subsequent 409 for the same repo'() { + def scmManager = newSchManager() + + def created = callReturningSuccess(201) + def conflict = callReturningError(409) + def seen = new HashSet() + + when(repoApi.create(any(Repository), anyBoolean())) + .thenAnswer(inv -> { + Repository r = inv.getArgument(0) + if (seen.contains(r.fullRepoName)) return conflict + seen.add(r.fullRepoName) + return created + }) + + assertTrue(scmManager.createRepository("team/demo", "Demo repo", true)) + assertFalse(scmManager.createRepository("team/demo", "Demo repo", true)) // 409 + assertTrue(scmManager.createRepository("team/other", null, false)) // neuer Name -> 201 + + verify(repoApi, times(3)).create(any(Repository), anyBoolean()) + } + + @Test + void 'setRepositoryPermission maps MAINTAIN to WRITE and handles 201 409'() { + def scmManager = newSchManager() + + def created = callReturningSuccess(201) + def conflict = callReturningError(409) + def seen = new HashSet() // key: ns/name + + when(repoApi.createPermission(anyString(), anyString(), any(Permission))) + .thenAnswer(inv -> { + String namespace = inv.getArgument(0) + String repoName = inv.getArgument(1) + String key = namespace + "/" + repoName + if (seen.contains(key)) return conflict + seen.add(key) + return created + }) + + assertDoesNotThrow({ -> + scmManager.setRepositoryPermission("namespace/repo1", "devs", AccessRole.MAINTAIN, Scope.GROUP) + } as Executable) + + assertDoesNotThrow({ -> + scmManager.setRepositoryPermission("namespace/repo1", "devs", AccessRole.MAINTAIN, Scope.GROUP) + } as Executable) + verify(repoApi, atLeastOnce()) + .createPermission(eq("namespace"), eq("repo1"), argThat { Permission p -> p.groupPermission && p.role == Permission.Role.WRITE }) + } + + + @Test + void 'url, repoPrefix, repoUrl variants, protocol and host come from UrlResolver'() { + when(urls.inClusterRepoUrl(anyString())).thenAnswer(a -> "http://scmm.ns.svc.cluster.local/scm/repo/" + a.getArgument(0)) + when(urls.clientRepoUrl(anyString())).thenAnswer(a -> "http://nodeport/scm/repo/" + a.getArgument(0)) + + def scmManager = newSchManager() + + assertEquals("http://scmm.ns.svc.cluster.local/scm", scmManager.url) + assertEquals("http://scmm.ns.svc.cluster.local/scm/repo/fv40-", scmManager.repoPrefix()) + + assertEquals("http://scmm.ns.svc.cluster.local/scm/repo/team/app", + scmManager.repoUrl("team/app", RepoUrlScope.IN_CLUSTER)) + assertEquals("http://nodeport/scm/repo/team/app", + scmManager.repoUrl("team/app", RepoUrlScope.CLIENT)) + + assertEquals("http", scmManager.protocol) + assertEquals("scmm.ns.svc.cluster.local", scmManager.host) + } + + @Test + void 'prometheusMetricsEndpoint is delegated to UrlResolver'() { + when(urls.prometheusEndpoint()).thenReturn(new URI("http://nodeport/scm/api/v2/metrics/prometheus")) + def scmManager = newSchManager() + assertEquals(new URI("http://nodeport/scm/api/v2/metrics/prometheus"), scmManager.prometheusMetricsEndpoint()) + } + + // Credentials & GitOps-User + @Test + void 'credentials and gitOpsUsername come from ScmManagerConfig'() { + def scmManager = newSchManager() + assertEquals("user", scmManager.credentials.username) + assertEquals("password", scmManager.credentials.password) + assertEquals("gitops-bot", scmManager.gitOpsUsername) + } + +} diff --git a/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolverTest.groovy b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolverTest.groovy new file mode 100644 index 000000000..75868c2a4 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolverTest.groovy @@ -0,0 +1,166 @@ +package com.cloudogu.gitops.git.providers.scmmanager + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.features.git.config.ScmTenantSchema +import com.cloudogu.gitops.utils.K8sClient +import com.cloudogu.gitops.utils.NetworkingUtils +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension + +import static org.junit.jupiter.api.Assertions.* +import static org.mockito.ArgumentMatchers.any +import static org.mockito.ArgumentMatchers.eq +import static org.mockito.Mockito.* + +@ExtendWith(MockitoExtension.class) +class ScmManagerUrlResolverTest { + private Config config + + @Mock + private K8sClient k8s + @Mock + private NetworkingUtils net + + @BeforeEach + void setUp() { + config = new Config( + application: new Config.ApplicationSchema( + namePrefix: 'fv40-', + runningInsideK8s: false + ) + ) + } + + private ScmManagerUrlResolver resolverWith(Map args = [:]) { + def scmmCofig = new ScmTenantSchema.ScmManagerTenantConfig() + scmmCofig.internal = (args.containsKey('internal') ? args.internal : true) + scmmCofig.namespace = (args.containsKey('namespace') ? args.namespace : "scm-manager") + scmmCofig.rootPath = (args.containsKey('rootPath') ? args.rootPath : "repo") + scmmCofig.url = (args.containsKey('url') ? args.url : "") + scmmCofig.ingress = (args.containsKey('ingress') ? args.ingress : "") + + return new ScmManagerUrlResolver(config, scmmCofig, k8s, net) + } + + // ---------- Client base & API ---------- + @Test + void "clientBase(): internal + outside K8s uses NodePort and appends 'scm' (no trailing slash) and only resolves NodePort once"() { + when(k8s.waitForNodePort(eq('scmm'), any())).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn("10.0.0.1") + + def r = resolverWith() + URI base1 = r.clientBase() + URI base2 = r.clientBase() + + assertEquals("http://10.0.0.1:30080/scm", base1.toString()) + assertEquals(base1, base2) + + verify(k8s, times(1)).waitForNodePort("scmm", "scm-manager") + verify(net, times(1)).findClusterBindAddress() + verifyNoMoreInteractions(k8s, net) + } + + @Test + void "clientApiBase(): appends 'api' to the client base"() { + when(k8s.waitForNodePort("scmm", "scm-manager")).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn("10.0.0.1") + + var r = resolverWith() + assertEquals("http://10.0.0.1:30080/scm/api/", r.clientApiBase().toString()) + } + + // ---------- Repo base & URLs ---------- + @Test + void "clientRepoUrl(): trims repoTarget and removes trailing slash"() { + when(k8s.waitForNodePort("scmm", "scm-manager")).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn("10.0.0.1") + + var r = resolverWith() + assertEquals("http://10.0.0.1:30080/scm/repo/ns/project", + r.clientRepoUrl(" ns/project ")) + } + + // ---------- In-cluster base & URLs ---------- + @Test + void "inClusterBase(): internal uses service DNS "() { + def r = resolverWith(namespace: "custom-ns", internal: true) + assertEquals("http://scmm.custom-ns.svc.cluster.local/scm", r.inClusterBase().toString()) + } + + + @Test + void "inClusterBase(): external uses external base + 'scm'"() { + var r = resolverWith(internal: false, url: "https://scmm.external") + assertEquals("https://scmm.external/scm", r.inClusterBase().toString()) + } + + + @Test + void "inClusterRepoUrl(): builds full in-cluster repo URL without trailing slash"() { + var r = resolverWith() + assertEquals("http://scmm.scm-manager.svc.cluster.local/scm/repo/admin/admin", + r.inClusterRepoUrl("admin/admin")) + } + + @Test + void "inClusterRepoPrefix(): includes configured namePrefix (empty prefix yields base path)"() { + // with non-empty namePrefix + config.application.namePrefix = 'fv40-' + def r1 = resolverWith() + assertEquals('http://scmm.scm-manager.svc.cluster.local/scm/repo/fv40-', r1.inClusterRepoPrefix()) + + // with empty/blank namePrefix + config.application.namePrefix = ' ' + def r2 = resolverWith() + assertEquals('http://scmm.scm-manager.svc.cluster.local/scm/repo/', r2.inClusterRepoPrefix()) + } + + // ---------- externalBase selection & error ---------- + @Test + void "externalBase(): prefers 'url' over 'ingress'"() { + def r = resolverWith(internal: false, url: 'https://scmm.external', ingress: 'ingress.example.org') + assertEquals('https://scmm.external/scm', r.inClusterBase().toString()) + } + + @Test + void "externalBase(): uses 'ingress' when 'url' is missing"() { + def r = resolverWith(internal: false, url: null, ingress: 'ingress.example.org') + assertEquals('http://ingress.example.org/scm', r.inClusterBase().toString()) + } + + @Test + void "externalBase(): throws when neither 'url' nor 'ingress' is set"() { + def r = resolverWith(internal: false, url: null, ingress: null) + def ex = assertThrows(IllegalArgumentException) { r.inClusterBase() } + assertTrue(ex.message.contains('Either scmm.url or scmm.ingress must be set when internal=false')) + } + + + @Test + void "nodePortBase(): falls back to default namespace 'scm-manager' when none provided"() { + when(k8s.waitForNodePort(eq('scmm'), eq('scm-manager'))).thenReturn("30080") + when(net.findClusterBindAddress()).thenReturn('10.0.0.1') + + def r = resolverWith(namespace: null) + assertEquals('http://10.0.0.1:30080/scm', r.clientBase().toString()) + } + + // ---------- helpers behavior ---------- + @Test + void "ensureScm(): adds 'scm' if missing and keeps it if present"() { + def r1 = resolverWith(internal: false, url: 'https://scmm.localhost') + assertEquals('https://scmm.localhost/scm', r1.clientBase().toString()) + } + + + // ---------- prometheus endpoint ---------- + @Test + void "prometheusEndpoint(): resolves "() { + def r = resolverWith(internal: false, url: 'https://scmm.localhost') + assertEquals('https://scmm.localhost/scm/api/v2/metrics/prometheus', r.prometheusEndpoint().toString()) + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApiTest.groovy b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApiTest.groovy new file mode 100644 index 000000000..07b698d34 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApiTest.groovy @@ -0,0 +1,59 @@ +package com.cloudogu.gitops.git.providers.scmmanager.api + +import com.cloudogu.gitops.common.MockWebServerHttpsFactory +import com.cloudogu.gitops.config.Credentials +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test + +import javax.net.ssl.SSLHandshakeException + +import static groovy.test.GroovyAssert.shouldFail +import static org.assertj.core.api.Assertions.assertThat + +class UsersApiTest { + private MockWebServer webServer = new MockWebServer() + private Credentials credentials = new Credentials("user", "pass") + + @AfterEach + void tearDown() { + webServer.shutdown() + } + + @Test + void 'allows self-signed certificates when using insecure option'() { + webServer.useHttps(MockWebServerHttpsFactory.createSocketFactory().sslSocketFactory(), false) + + def api = usersApi(true) + webServer.enqueue(new MockResponse().setResponseCode(204)) + + def resp = api.delete('test-user').execute() + + assertThat(resp.isSuccessful()).isTrue() + assertThat(webServer.requestCount).isEqualTo(1) + } + + @Test + void 'does not allow self-signed certificates by default'() { + webServer.useHttps(MockWebServerHttpsFactory.createSocketFactory().sslSocketFactory(), false) + + def api = usersApi(false) + + shouldFail(SSLHandshakeException) { + api.delete('test-user').execute() + } + assertThat(webServer.requestCount).isEqualTo(0) + } + + + private UsersApi usersApi(boolean insecure) { + def client = new ScmManagerApiClient(apiBaseUrl(), credentials, insecure) + return client.usersApi() + } + + private String apiBaseUrl() { + return "${webServer.url('scm')}/api/" + } + +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/integration/features/ArgoCdTestIT.groovy b/src/test/groovy/com/cloudogu/gitops/integration/features/ArgoCdTestIT.groovy index 0469a345f..244968e9e 100644 --- a/src/test/groovy/com/cloudogu/gitops/integration/features/ArgoCdTestIT.groovy +++ b/src/test/groovy/com/cloudogu/gitops/integration/features/ArgoCdTestIT.groovy @@ -25,7 +25,6 @@ class ArgoCdTestIT extends KubenetesApiTestSetup { assertThat(namespaces.getItems().isEmpty()).isFalse() def namespace = namespaces.getItems().find { namespace.equals(it.getMetadata().name) } assertThat(namespace).isNotNull() - } /** @@ -59,4 +58,4 @@ class ArgoCdTestIT extends KubenetesApiTestSetup { } return false } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/integration/features/KubenetesApiTestSetup.groovy b/src/test/groovy/com/cloudogu/gitops/integration/features/KubenetesApiTestSetup.groovy index 73be67bc8..f36732135 100644 --- a/src/test/groovy/com/cloudogu/gitops/integration/features/KubenetesApiTestSetup.groovy +++ b/src/test/groovy/com/cloudogu/gitops/integration/features/KubenetesApiTestSetup.groovy @@ -19,7 +19,7 @@ abstract class KubenetesApiTestSetup { static String kubeConfigPath CoreV1Api api int TIME_TO_WAIT = 12 - int RETRY_SECONDS = 15 + int RETRY_SECONDS = 30 /** * Gets path to kubeconfig @@ -32,6 +32,7 @@ abstract class KubenetesApiTestSetup { } assertThat(kubeConfigPath) isNotBlank() } + /** * establish connection to kubernetes and create API to use. */ @@ -85,4 +86,4 @@ abstract class KubenetesApiTestSetup { */ abstract boolean isReadyToStartTests() -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/integration/features/PrometheusStackTestIT.groovy b/src/test/groovy/com/cloudogu/gitops/integration/features/PrometheusStackTestIT.groovy index 351f877e7..7ac1b9661 100644 --- a/src/test/groovy/com/cloudogu/gitops/integration/features/PrometheusStackTestIT.groovy +++ b/src/test/groovy/com/cloudogu/gitops/integration/features/PrometheusStackTestIT.groovy @@ -33,6 +33,7 @@ class PrometheusStackTestIT extends KubenetesApiTestSetup { } return false; } + @BeforeAll static void labelTest() { println "###### PROMETHEUS ######" @@ -90,4 +91,4 @@ class PrometheusStackTestIT extends KubenetesApiTestSetup { def pods = api.listNamespacedPod(namespace).execute() assertThat(pods.getItems().size()).isEqualTo(3) } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy index ca7873b1f..7252b73b0 100644 --- a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/ArgocdApplicationTest.groovy @@ -1,8 +1,8 @@ package com.cloudogu.gitops.kubernetes.rbac import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.git.GitRepo import com.cloudogu.gitops.kubernetes.argocd.ArgoApplication -import com.cloudogu.gitops.scmm.ScmmRepo import com.cloudogu.gitops.utils.FileSystemUtils import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test @@ -13,13 +13,16 @@ class ArgocdApplicationTest { Config config = Config.fromMap([ - scmm : [ - username: 'user', - password: 'pass', - protocol: 'http', - host : 'localhost', - provider: 'scm-manager', - rootPath: 'scm' + scm : [ + scmManager: [username: 'user', + password: 'pass', + host : 'localhost', + rootPath: 'scm' + ], + gitlab : [username: 'user', + password: 'pass', + + ] ], application: [ namePrefix: '', @@ -29,11 +32,10 @@ class ArgocdApplicationTest { ] ]) - @Test void 'simple ArgoCD Application with common values'() { - ScmmRepo repo = new ScmmRepo(config, "my-repo", new FileSystemUtils()) + GitRepo repo = new GitRepo(config, null, "my-repo", new FileSystemUtils()) new ArgoApplication( 'example-apps', diff --git a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy index 4df2eedd9..db1a4cf5a 100644 --- a/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinitionTest.groovy @@ -1,7 +1,7 @@ package com.cloudogu.gitops.kubernetes.rbac import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo +import com.cloudogu.gitops.git.GitRepo import com.cloudogu.gitops.utils.FileSystemUtils import groovy.yaml.YamlSlurper import org.junit.jupiter.api.Test @@ -12,23 +12,24 @@ import static org.junit.jupiter.api.Assertions.assertThrows class RbacDefinitionTest { private final Config config = Config.fromMap([ - scmm: [ - username: 'user', - password: 'pass', - protocol: 'http', - host: 'localhost', - provider: 'scm-manager', - rootPath: 'scm' + scm : [ + scmManager: [ + username: 'user', + password: 'pass', + protocol: 'http', + host : 'localhost', + rootPath: 'scm' + ], ], application: [ namePrefix: '', - insecure: false, - gitName: 'Test User', - gitEmail: 'test@example.com' + insecure : false, + gitName : 'Test User', + gitEmail : 'test@example.com' ] ]) - private final ScmmRepo repo = new ScmmRepo(config, "my-repo", new FileSystemUtils()) + private final GitRepo repo = new GitRepo(config, null, "my-repo", new FileSystemUtils()) @Test void 'generates at least one RBAC YAML file'() { @@ -245,7 +246,7 @@ class RbacDefinitionTest { void 'renders node access rules in argocd-role only when not on OpenShift'() { config.application.openshift = false - ScmmRepo tempRepo = new ScmmRepo(config, "rbac-test", new FileSystemUtils()) + GitRepo tempRepo = new GitRepo(config, null, "rbac-test", new FileSystemUtils()) new RbacDefinition(Role.Variant.ARGOCD) .withName("nodecheck") @@ -271,7 +272,7 @@ class RbacDefinitionTest { void 'does not render node access rules in argocd-role when on OpenShift'() { config.application.openshift = true - ScmmRepo tempRepo = new ScmmRepo(config, "rbac-test", new FileSystemUtils()) + GitRepo tempRepo = new GitRepo(config, null, "rbac-test", new FileSystemUtils()) new RbacDefinition(Role.Variant.ARGOCD) .withName("nodecheck") @@ -302,7 +303,8 @@ class RbacDefinitionTest { .generate() } - assertThat(ex.message).contains("Config must not be null") // oder je nach deiner tatsächlichen Exception-Message + assertThat(ex.message).contains("Config must not be null") + // oder je nach deiner tatsächlichen Exception-Message } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/scmm/ScmUrlResolverTest.groovy b/src/test/groovy/com/cloudogu/gitops/scmm/ScmUrlResolverTest.groovy deleted file mode 100644 index 46817d4a1..000000000 --- a/src/test/groovy/com/cloudogu/gitops/scmm/ScmUrlResolverTest.groovy +++ /dev/null @@ -1,133 +0,0 @@ -package com.cloudogu.gitops.scmm - -import com.cloudogu.gitops.config.Config -import org.junit.jupiter.api.Test -import static org.junit.jupiter.api.Assertions.* - -class ScmUrlResolverTest { - - // ---------- externalHost ---------- - @Test - void 'externalHost uses scmm url trims whitespace and enforces trailing slash'() { - Config config = new Config( - scmm: new Config.ScmmSchema(url: 'http://scmm ')) - URI uri = ScmUrlResolver.externalHost(config) - assertEquals("http://scmm/", uri.toString()) - } - - @Test - void 'externalHost preserve existing path and only appends trailing slash'() { - Config config = new Config( - scmm: new Config.ScmmSchema( - url: 'https://git.example.com/gitlab' - ) - ) - URI uri = ScmUrlResolver.externalHost(config) - assertEquals("https://git.example.com/gitlab/",uri.toString()) - - } - // ---------- scmmBaseUri ---------- - @Test - void 'scmmBaseUri internal true returns svc cluster local url including scm'() { - Config config = new Config( - application: new Config.ApplicationSchema( - namePrefix: 'dev-', - ), - scmm: new Config.ScmmSchema( - internal: true - ) - ) - URI uri = ScmUrlResolver.scmmBaseUri(config); - assertEquals("http://scmm.dev-scm-manager.svc.cluster.local/scm/", uri.toString()); - } - - @Test - void 'scmmBaseUri external throws when url missing'() { - Config config = new Config( - scmm: new Config.ScmmSchema( - internal: false, - ) - ) - assertThrows(IllegalArgumentException.class, () -> ScmUrlResolver.scmmBaseUri(config)) - } - - @Test - void 'scmmBaseUri external preserves path and ensures trailing slash'() { - Config config = new Config( - scmm: new Config.ScmmSchema( - internal: false, - url: 'http://scmm.example.com/scm' - ) - ) - URI uri = ScmUrlResolver.scmmBaseUri(config); - assertEquals("http://scmm.example.com/scm/", uri.toString()); - - } - - // ---------- tenantBaseUrl ---------- - @Test - void 'tenantBaseUrl scmManager based on scmmBaseUri no trailing slash'() { - Config config = new Config( - application: new Config.ApplicationSchema( - namePrefix: 'tenant-', - ), - scmm: new Config.ScmmSchema( - internal: false, - url: 'http://scmm.example.com/scm', - rootPath: "repo" - ) - ) - String url = ScmUrlResolver.tenantBaseUrl(config); - assertEquals("http://scmm.example.com/scm/repo/tenant-", url); - assertFalse(url.endsWith("/")); - } - - @Test - void 'tenantBaseUrl gitlab based on externalHost no trailing slash'() { - Config config = new Config( - application: new Config.ApplicationSchema( - namePrefix: 'tenant-', - ), - scmm: new Config.ScmmSchema( - provider: 'gitlab', - url: 'http://gitlab.example.com', - rootPath: "group" - ) - ) - String url = ScmUrlResolver.tenantBaseUrl(config); - - assertEquals("http://gitlab.example.com/tenant-group", url); - assertFalse(url.endsWith("/")); - } - - @Test - void 'tenantBaseUrl unknown provider throws'() { - Config config = new Config( - application: new Config.ApplicationSchema( - namePrefix: 'tenant-', - ), - scmm: new Config.ScmmSchema( - provider: 'gitlabb', - url: 'http://gitlab.example.com', - rootPath: "group" - ) - ) - assertThrows(IllegalArgumentException.class, () -> ScmUrlResolver.tenantBaseUrl(config)); - } - - // ---------- scmmRepoUrl ---------- - @Test - void 'scmmRepoUrl appends root path and namespaceName after scmmBaseUri'() { - Config config = new Config( - application: new Config.ApplicationSchema( - namePrefix: 'dev-', - ), - scmm: new Config.ScmmSchema( - internal: true, - rootPath: 'repo', - ) - ) - String url = ScmUrlResolver.scmmRepoUrl(config, "my-ns/my-repo"); - assertEquals("http://scmm.dev-scm-manager.svc.cluster.local/scm/repo/my-ns/my-repo", url); - } -} diff --git a/src/test/groovy/com/cloudogu/gitops/scmm/api/UsersApiTest.groovy b/src/test/groovy/com/cloudogu/gitops/scmm/api/UsersApiTest.groovy deleted file mode 100644 index 8ee2b12f0..000000000 --- a/src/test/groovy/com/cloudogu/gitops/scmm/api/UsersApiTest.groovy +++ /dev/null @@ -1,58 +0,0 @@ -package com.cloudogu.gitops.scmm.api - -import com.cloudogu.gitops.common.MockWebServerHttpsFactory -import com.cloudogu.gitops.config.Config -import io.micronaut.context.ApplicationContext -import io.micronaut.inject.qualifiers.Qualifiers -import okhttp3.OkHttpClient -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import retrofit2.Retrofit - -import javax.net.ssl.SSLHandshakeException - -import static groovy.test.GroovyAssert.shouldFail -import static org.assertj.core.api.Assertions.assertThat - -class UsersApiTest { - private MockWebServer webServer = new MockWebServer() - - @AfterEach - void tearDown() { - webServer.shutdown() - } - - @Test - void 'allows self-signed certificates when using insecure option'() { - webServer.useHttps(MockWebServerHttpsFactory.createSocketFactory().sslSocketFactory(), false) - - OkHttpClient okHttpClient = ApplicationContext.run() - .registerSingleton(new Config(application: new Config.ApplicationSchema(insecure: true))) - .getBean(OkHttpClient.class, Qualifiers.byName("scmm")) - def usersApi = retrofit(okHttpClient).create(UsersApi) - - webServer.enqueue(new MockResponse()) - usersApi.delete('test-user').execute() - assertThat(webServer.requestCount).isEqualTo(1) - } - - @Test - void 'does not allow self-signed certificates by default'() { - webServer.useHttps(MockWebServerHttpsFactory.createSocketFactory().sslSocketFactory(), false) - - def usersApi = retrofit().create(UsersApi) - shouldFail(SSLHandshakeException) { - usersApi.delete('test-user').execute() - } - } - - private Retrofit retrofit(OkHttpClient okHttpClient = null) { - def builder = new Retrofit.Builder() - .baseUrl("${webServer.url("scm")}/api/") - if (okHttpClient) - builder = builder.client(okHttpClient) - builder.build() - } -} diff --git a/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy b/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy index 0276576bb..f1be29f47 100644 --- a/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/utils/AirGappedUtilsTest.groovy @@ -1,10 +1,14 @@ package com.cloudogu.gitops.utils import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.api.Permission -import com.cloudogu.gitops.scmm.api.Repository -import com.cloudogu.gitops.scmm.api.ScmmApiClient +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.utils.git.GitHandlerForTests +import com.cloudogu.gitops.utils.git.TestGitRepoFactory +import com.cloudogu.gitops.git.providers.scmmanager.Permission +import com.cloudogu.gitops.utils.git.ScmManagerMock +import com.cloudogu.gitops.git.providers.scmmanager.api.Repository +import com.cloudogu.gitops.utils.git.TestScmManagerApiClient import groovy.yaml.YamlSlurper import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Ref @@ -21,25 +25,29 @@ import static org.mockito.Mockito.* class AirGappedUtilsTest { - Config config = new Config( - application: new Config.ApplicationSchema( - localHelmChartFolder : '', - gitName : 'Cloudogu', - gitEmail : 'hello@cloudogu.com', - ) - ) + Config config = Config.fromMap([ + application: [ + localHelmChartFolder: '', + gitName : 'Cloudogu', + gitEmail : 'hello@cloudogu.com'], + scm : [ + scmManager: [ + url: ''] + ] + ]) - Config.HelmConfig helmConfig = new Config.HelmConfig( [ + Config.HelmConfig helmConfig = new Config.HelmConfig([ chart : 'kube-prometheus-stack', repoURL: 'https://kube-prometheus-stack-repo-url', version: '58.2.1' ]) - + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) - TestScmmRepoProvider scmmRepoProvider = new TestScmmRepoProvider(config, new FileSystemUtils()) + TestGitRepoFactory gitRepoFactory = new TestGitRepoFactory(config, new FileSystemUtils()) FileSystemUtils fileSystemUtils = new FileSystemUtils() - TestScmmApiClient scmmApiClient = new TestScmmApiClient(config) + TestScmManagerApiClient scmmApiClient = new TestScmManagerApiClient(config) HelmClient helmClient = mock(HelmClient) + GitHandler gitHandler = new GitHandlerForTests(config, new ScmManagerMock()) @BeforeEach void setUp() { @@ -48,15 +56,15 @@ class AirGappedUtilsTest { when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response) } - + @Test void 'Prepares repos for air-gapped use'() { setupForAirgappedUse() def actualRepoNamespaceAndName = createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) - + assertThat(actualRepoNamespaceAndName).isEqualTo( - "${ScmmRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/kube-prometheus-stack".toString()) + "${GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/kube-prometheus-stack".toString()) assertAirGapped() } @@ -78,13 +86,13 @@ class AirGappedUtilsTest { setupForAirgappedUse(null, []) createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) - ScmmRepo prometheusRepo = scmmRepoProvider.repos['3rd-party-dependencies/kube-prometheus-stack'] + GitRepo prometheusRepo = gitRepoFactory.repos['3rd-party-dependencies/kube-prometheus-stack'] def actualPrometheusChartYaml = new YamlSlurper().parse(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.yaml')) - def dependencies = actualPrometheusChartYaml['dependencies'] + def dependencies = actualPrometheusChartYaml['dependencies'] assertThat(dependencies).isNull() } - + @Test void 'Fails for invalid helm charts'() { setupForAirgappedUse() @@ -95,7 +103,7 @@ class AirGappedUtilsTest { def exception = shouldFail(RuntimeException) { createAirGappedUtils().mirrorHelmRepoToGit(helmConfig) } - + assertThat(exception.getMessage()).isEqualTo( "Helm chart in folder ${rootChartsFolder}/kube-prometheus-stack seems invalid.".toString()) assertThat(exception.getCause()).isSameAs(expectedException) @@ -109,10 +117,10 @@ class AirGappedUtilsTest { name : 'kube-prometheus-stack-chart', dependencies: [ [ - condition: 'crds.enabled', - name: 'crds', + condition : 'crds.enabled', + name : 'crds', repository: '', - version: '0.0.0' + version : '0.0.0' ], [ condition : 'grafana.enabled', @@ -122,7 +130,7 @@ class AirGappedUtilsTest { ] ] ] - + if (dependencies != null) { if (dependencies.isEmpty()) { prometheusChartYaml.remove('dependencies') @@ -130,21 +138,21 @@ class AirGappedUtilsTest { prometheusChartYaml.dependencies = dependencies } } - + fileSystemUtils.writeYaml(prometheusChartYaml, sourceChart.resolve('Chart.yaml').toFile()) - if(chartLock == null) { + if (chartLock == null) { chartLock = [ dependencies: [ [ - name: 'crds', + name : 'crds', repository: "", - version: '0.0.0' + version : '0.0.0' ], [ - name: 'grafana', + name : 'grafana', repository: 'https://grafana.github.io/helm-charts', - version: '7.3.9' + version : '7.3.9' ] ] ] @@ -155,7 +163,7 @@ class AirGappedUtilsTest { } protected void assertAirGapped() { - ScmmRepo prometheusRepo = scmmRepoProvider.repos['3rd-party-dependencies/kube-prometheus-stack'] + GitRepo prometheusRepo = gitRepoFactory.repos['3rd-party-dependencies/kube-prometheus-stack'] assertThat(prometheusRepo).isNotNull() assertThat(Path.of(prometheusRepo.absoluteLocalRepoTmpDir, 'Chart.lock')).doesNotExist() @@ -175,11 +183,15 @@ class AirGappedUtilsTest { assertHelmRepoCommits(prometheusRepo, '1.2.3', 'Chart kube-prometheus-stack-chart, version: 1.2.3\n\n' + 'Source: https://kube-prometheus-stack-repo-url\nDependencies localized to run in air-gapped environments') - verify(prometheusRepo).create(eq('Mirror of Helm chart kube-prometheus-stack from https://kube-prometheus-stack-repo-url'), any(ScmmApiClient)) + verify(prometheusRepo).createRepositoryAndSetPermission( + eq("${GitRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/kube-prometheus-stack".toString()), + eq("Mirror of Helm chart kube-prometheus-stack from https://kube-prometheus-stack-repo-url"), + eq(false) + ) } - void assertHelmRepoCommits(ScmmRepo repo, String expectedTag, String expectedCommitMessage) { + void assertHelmRepoCommits(GitRepo repo, String expectedTag, String expectedCommitMessage) { def commits = Git.open(new File(repo.absoluteLocalRepoTmpDir)).log().setMaxCount(1).all().call().collect() assertThat(commits.size()).isEqualTo(1) assertThat(commits[0].fullMessage).isEqualTo(expectedCommitMessage) @@ -190,6 +202,6 @@ class AirGappedUtilsTest { } AirGappedUtils createAirGappedUtils() { - new AirGappedUtils(config, scmmRepoProvider, scmmApiClient, fileSystemUtils, helmClient) + new AirGappedUtils(config, gitRepoFactory, fileSystemUtils, helmClient, gitHandler) } -} +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/K8sClientTest.groovy b/src/test/groovy/com/cloudogu/gitops/utils/K8sClientTest.groovy index de0e17807..5cec3a8d8 100644 --- a/src/test/groovy/com/cloudogu/gitops/utils/K8sClientTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/utils/K8sClientTest.groovy @@ -59,7 +59,7 @@ class K8sClientTest { k8sClient.getArgoCDNamespacesSecret('my-secret', 'my-ns') assertThat(commandExecutor.actualCommands[0]).isEqualTo( - "kubectl get secret my-secret -nmy-ns -ojsonpath={.data.namespaces}") + "kubectl get secret my-secret -n my-ns -ojsonpath={.data.namespaces}") } @Test diff --git a/src/test/groovy/com/cloudogu/gitops/utils/ScmmRepoTest.groovy b/src/test/groovy/com/cloudogu/gitops/utils/ScmmRepoTest.groovy deleted file mode 100644 index c35978dcf..000000000 --- a/src/test/groovy/com/cloudogu/gitops/utils/ScmmRepoTest.groovy +++ /dev/null @@ -1,254 +0,0 @@ -package com.cloudogu.gitops.utils - -import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.api.Permission -import com.cloudogu.gitops.scmm.api.Repository -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.lib.Ref -import org.junit.jupiter.api.Test -import org.mockito.ArgumentCaptor -import retrofit2.Call - -import static groovy.test.GroovyAssert.shouldFail -import static org.assertj.core.api.Assertions.assertThat -import static org.mockito.ArgumentMatchers.* -import static org.mockito.Mockito.* - -class ScmmRepoTest { - - - public static final String expectedNamespace = "namespace" - public static final String expectedRepo = "repo" - Config config = new Config( - application: new Config.ApplicationSchema( - gitName: "Cloudogu", - gitEmail: "hello@cloudogu.com",) - , - scmm: new Config.ScmmSchema( - username: "dont-care-username", - password: "dont-care-password", - gitOpsUsername: 'foo-gitops' - )) - TestScmmRepoProvider scmmRepoProvider = new TestScmmRepoProvider(config, new FileSystemUtils()) - TestScmmApiClient scmmApiClient = new TestScmmApiClient(config) - Call response201 = TestScmmApiClient.mockSuccessfulResponse(201) - Call response409 = scmmApiClient.mockErrorResponse(409) - Call response500 = scmmApiClient.mockErrorResponse(500) - - @Test - void "writes file"() { - def repo = createRepo() - repo.writeFile("test.txt", "the file's content") - - def expectedFile = new File("$repo.absoluteLocalRepoTmpDir/test.txt") - assertThat(expectedFile.getText()).is("the file's content") - } - - @Test - void "overwrites file"() { - def repo = createRepo() - def tempDir = repo.absoluteLocalRepoTmpDir - - def existingFile = new File("$tempDir/already-exists.txt") - existingFile.createNewFile() - existingFile.text = "already existing content" - - repo.writeFile("already-exists.txt", "overwritten content") - - def expectedFile = new File("$tempDir/already-exists.txt") - assertThat(expectedFile.getText()).is("overwritten content") - } - - @Test - void "writes file and creates subdirectory"() { - def repo = createRepo() - def tempDir = repo.absoluteLocalRepoTmpDir - repo.writeFile("subdirectory/test.txt", "the file's content") - - def expectedFile = new File("$tempDir/subdirectory/test.txt") - assertThat(expectedFile.getText()).is("the file's content") - } - - @Test - void "throws error when directory conflicts with existing file"() { - def repo = createRepo() - def tempDir = repo.absoluteLocalRepoTmpDir - new File("$tempDir/test.txt").mkdir() - - shouldFail(FileNotFoundException) { - repo.writeFile("test.txt", "the file's content") - } - } - - @Test - void 'Creates repo with empty name-prefix'() { - def repo = createRepo('expectedRepoTarget') - assertThat(repo.scmmRepoTarget).isEqualTo('expectedRepoTarget') - } - - @Test - void 'Creates repo with name-prefix'() { - config.application.namePrefix = 'abc-' - def repo = createRepo('expectedRepoTarget') - assertThat(repo.scmmRepoTarget).isEqualTo('abc-expectedRepoTarget') - } - - @Test - void 'Creates repo without name-prefix when in namespace 3rd-party-deps'() { - config.application.namePrefix = 'abc-' - def repo = createRepo("${ScmmRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/foo") - assertThat(repo.scmmRepoTarget).isEqualTo("${ScmmRepo.NAMESPACE_3RD_PARTY_DEPENDENCIES}/foo".toString()) - } - - @Test - void 'Clones and checks out main'() { - def repo = createRepo() - - repo.cloneRepo() - def HEAD = new File(repo.absoluteLocalRepoTmpDir, '.git/HEAD') - assertThat(HEAD.text).isEqualTo("ref: refs/heads/main\n") - assertThat(new File(repo.absoluteLocalRepoTmpDir, 'README.md')).exists() - } - - @Test - void 'pushes changes to remote directory'() { - def repo = createRepo() - - repo.cloneRepo() - def readme = new File(repo.absoluteLocalRepoTmpDir, 'README.md') - readme.text = 'This text should be in the readme afterwards' - repo.commitAndPush("The commit message") - - def commits = Git.open(new File(repo.absoluteLocalRepoTmpDir)).log().setMaxCount(1).all().call().collect() - assertThat(commits.size()).isEqualTo(1) - assertThat(commits[0].fullMessage).isEqualTo("The commit message") - assertThat(commits[0].authorIdent.emailAddress).isEqualTo('hello@cloudogu.com') - assertThat(commits[0].authorIdent.name).isEqualTo('Cloudogu') - assertThat(commits[0].committerIdent.emailAddress).isEqualTo('hello@cloudogu.com') - assertThat(commits[0].committerIdent.name).isEqualTo("Cloudogu") - - List tags = Git.open(new File(repo.absoluteLocalRepoTmpDir)).tagList().call() - assertThat(tags.size()).isEqualTo(0) - } - - @Test - void 'pushes changes to remote directory with tag'() { - def repo = createRepo() - def expectedTag = '1.0' - - repo.cloneRepo() - def readme = new File(repo.absoluteLocalRepoTmpDir, 'README.md') - readme.text = 'This text should be in the readme afterwards' - // Create existing tag to test for idempotence - Git.open(new File(repo.absoluteLocalRepoTmpDir)).tag().setName(expectedTag).call() - - repo.commitAndPush("The commit message", expectedTag) - - - List tags = Git.open(new File(repo.absoluteLocalRepoTmpDir)).tagList().call() - assertThat(tags.size()).isEqualTo(1) - assertThat(tags[0].name).isEqualTo("refs/tags/$expectedTag".toString()) - // It would be a good idea to check if the git tag is set on the commit. - // However, it's extremely complicated with jgit - // The "official" example code throws an exception here: Ref peeledRef = repository.getRefDatabase().peel(ref) - // https://github.com/centic9/jgit-cookbook/blob/d923e18b2ce2e55761858fd2e8e402dd252e0766/src/main/java/org/dstadler/jgit/porcelain/ListTags.java - // 🤷 - } - - @Test - void 'Create repo'() { - def repo = createRepo() - - when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response201) - when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response201) - - repo.create('description', scmmApiClient) - - assertCreatedRepo() - } - - @Test - void 'Create repo: Ignores existing Repos'() { - def repo = createRepo() - - when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response409) - when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response201) - - repo.create('description', scmmApiClient) - - assertCreatedRepo() - } - - @Test - void 'Create repo: Ignore existing Repos'() { - def repo = createRepo() - - when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response409) - when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response201) - - repo.create('description', scmmApiClient) - - assertCreatedRepo() - } - - @Test - void 'Create repo: Ignore existing Permissions'() { - def repo = createRepo() - - when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response201) - when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response409) - - repo.create('description', scmmApiClient) - - assertCreatedRepo() - } - - @Test - void 'Create repo: Handle failures to SCMM-API for Repos'() { - def repo = createRepo() - - when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response500) - - def exception = shouldFail(RuntimeException) { - repo.create('description', scmmApiClient) - } - assertThat(exception.message).startsWith('Could not create Repository') - assertThat(exception.message).contains(expectedNamespace) - assertThat(exception.message).contains(expectedRepo) - assertThat(exception.message).contains('500') - } - - @Test - void 'Create repo: Handle failures to SCMM-API for Permissions'() { - def repo = createRepo() - - when(scmmApiClient.repositoryApi.create(any(Repository), anyBoolean())).thenReturn(response201) - when(scmmApiClient.repositoryApi.createPermission(anyString(), anyString(), any(Permission))).thenReturn(response500) - - def exception = shouldFail(RuntimeException) { - repo.create('description', scmmApiClient) - } - assertThat(exception.message).startsWith("Could not create Permission for repo $expectedNamespace/$expectedRepo") - assertThat(exception.message).contains('foo-gitops') - assertThat(exception.message).contains(Permission.Role.WRITE.name()) - assertThat(exception.message).contains('500') - } - - protected void assertCreatedRepo() { - def repoCreateArgument = ArgumentCaptor.forClass(Repository) - verify(scmmApiClient.repositoryApi, times(1)).create(repoCreateArgument.capture(), eq(true)) - assertThat(repoCreateArgument.allValues[0].namespace).isEqualTo(expectedNamespace) - assertThat(repoCreateArgument.allValues[0].name).isEqualTo(expectedRepo) - assertThat(repoCreateArgument.allValues[0].description).isEqualTo('description') - - def permissionCreateArgument = ArgumentCaptor.forClass(Permission) - verify(scmmApiClient.repositoryApi, times(1)).createPermission(anyString(), anyString(), permissionCreateArgument.capture()) - assertThat(permissionCreateArgument.allValues[0].name).isEqualTo('foo-gitops') - assertThat(permissionCreateArgument.allValues[0].role).isEqualTo(Permission.Role.WRITE) - } - - private ScmmRepo createRepo(String repoTarget = "${expectedNamespace}/${expectedRepo}") { - return scmmRepoProvider.getRepo(repoTarget) - } -} diff --git a/src/test/groovy/com/cloudogu/gitops/utils/TestScmmRepoProvider.groovy b/src/test/groovy/com/cloudogu/gitops/utils/TestScmmRepoProvider.groovy deleted file mode 100644 index 4643dddcc..000000000 --- a/src/test/groovy/com/cloudogu/gitops/utils/TestScmmRepoProvider.groovy +++ /dev/null @@ -1,53 +0,0 @@ -package com.cloudogu.gitops.utils - -import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.ScmmRepo -import com.cloudogu.gitops.scmm.ScmmRepoProvider -import org.apache.commons.io.FileUtils - -import static org.mockito.Mockito.spy - -class TestScmmRepoProvider extends ScmmRepoProvider { - Map repos = [:] - - TestScmmRepoProvider(Config config, FileSystemUtils fileSystemUtils) { - super(config, fileSystemUtils) - } - @Override - ScmmRepo getRepo(String repoTarget){ - return getRepo(repoTarget,false) - } - - @Override - ScmmRepo getRepo(String repoTarget, Boolean centralRepo) { - // Check if we already have a mock for this repo - ScmmRepo repo = repos[repoTarget] - // Check if we already have a mock for this repo - if (repo != null && repo.isCentralRepo == centralRepo) { - return repo - } - - ScmmRepo repoNew = new ScmmRepo(config, repoTarget, fileSystemUtils, centralRepo) { - String remoteGitRepopUrl = '' - - @Override - String getGitRepositoryUrl() { - if (!remoteGitRepopUrl) { - - def tempDir = File.createTempDir('gitops-playground-repocopy') - tempDir.deleteOnExit() - def originalRepo = System.getProperty("user.dir") + "/src/test/groovy/com/cloudogu/gitops/utils/data/git-repository/" - - FileUtils.copyDirectory(new File(originalRepo), tempDir) - remoteGitRepopUrl = 'file://' + tempDir.absolutePath - } - return remoteGitRepopUrl - } - - } - // Create a spy to enable verification while keeping real behavior - ScmmRepo spyRepo = spy(repoNew) - repos.put(repoTarget, spyRepo) - return spyRepo - } -} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/git/GitHandlerForTests.groovy b/src/test/groovy/com/cloudogu/gitops/utils/git/GitHandlerForTests.groovy new file mode 100644 index 000000000..c61909176 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/utils/git/GitHandlerForTests.groovy @@ -0,0 +1,58 @@ +package com.cloudogu.gitops.utils.git + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.features.deployment.HelmStrategy +import com.cloudogu.gitops.features.git.GitHandler +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.utils.FileSystemUtils +import com.cloudogu.gitops.utils.K8sClientForTest +import com.cloudogu.gitops.utils.NetworkingUtils + +import static org.mockito.Mockito.mock + +class GitHandlerForTests extends GitHandler { + private final GitProvider tenantProvider + private final GitProvider centralProvider + + GitHandlerForTests(Config config, GitProvider tenantProvider, GitProvider centralProvider = null) { + super(config, mock(HelmStrategy), new FileSystemUtils(), new K8sClientForTest(config), new NetworkingUtils()) + this.tenantProvider = tenantProvider + this.centralProvider = centralProvider + } + + @Override + void enable() { + // Inject the test providers into the base class before running the real logic + this.tenant = tenantProvider + this.central = centralProvider + + // Mirror the production side effect: set namespace for internal SCMM + if (this.config?.scm?.scmManager != null) { + this.config.scm.scmManager.namespace = "${config.application.namePrefix}scm-manager".toString() + } + + // === Run ONLY the repo setup logic (NO provider construction here) === + final String namePrefix = (config?.application?.namePrefix ?: "").trim() + if (this.central) { + setupRepos(this.central, namePrefix) + setupRepos(this.tenant, namePrefix, false) + } else { + setupRepos(this.tenant, namePrefix, true) + } + create3thPartyDependencies(this.tenant, namePrefix) + + } + + @Override + void validate() {} + + @Override + GitProvider getTenant() { return tenantProvider } + + @Override + GitProvider getCentral() { return centralProvider } + + @Override + GitProvider getResourcesScm() { return centralProvider ?: tenantProvider } + +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/git/GitlabMock.groovy b/src/test/groovy/com/cloudogu/gitops/utils/git/GitlabMock.groovy new file mode 100644 index 000000000..dcec9d857 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/utils/git/GitlabMock.groovy @@ -0,0 +1,57 @@ +package com.cloudogu.gitops.utils.git + +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.git.providers.AccessRole +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.RepoUrlScope +import com.cloudogu.gitops.git.providers.Scope + +class GitlabMock implements GitProvider { + URI base = new URI("https://example.com/group") // from config.scm.gitlab.url + String namePrefix = "" // prefix if you use tenant mode + + final List createdRepos = [] + final List permissionCalls = [] + + @Override + boolean createRepository(String repoTarget, String description, boolean initialize) { + createdRepos << repoTarget + return true + } + + @Override + boolean createRepository(String repoTarget, String description) { + return createRepository(repoTarget, description, true) + } + + @Override + void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) { + permissionCalls << [repoTarget: repoTarget, principal: principal, role: role, scope: scope] + } + + @Override + String repoUrl(String repoTarget, RepoUrlScope scope) { + def cleaned = base.toString().replaceAll('/+$','') + return "${cleaned}/${repoTarget}.git" + } + + + @Override + String repoPrefix() { + def cleaned = base.toString().replaceAll('/+$','') + return "${cleaned}/${namePrefix?:''}".toString() + } + + // trivial passthroughs + @Override URI prometheusMetricsEndpoint() { return base } + @Override Credentials getCredentials() { return new Credentials("gitops","gitops") } + @Override void deleteRepository(String n, String r, boolean p) {} + @Override void deleteUser(String name) {} + @Override void setDefaultBranch(String target, String branch) {} + @Override String getUrl() { return base.toString() } + @Override String getProtocol() { return base.scheme } + @Override String getHost() { return base.host } + @Override String getGitOpsUsername() { return "gitops" } + + +} diff --git a/src/test/groovy/com/cloudogu/gitops/utils/git/ScmManagerMock.groovy b/src/test/groovy/com/cloudogu/gitops/utils/git/ScmManagerMock.groovy new file mode 100644 index 000000000..e0cad3851 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/utils/git/ScmManagerMock.groovy @@ -0,0 +1,129 @@ +package com.cloudogu.gitops.utils.git + +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.git.providers.AccessRole +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.git.providers.RepoUrlScope +import com.cloudogu.gitops.git.providers.Scope + + +/** + * Lightweight test double for ScmManager/GitProvider. + * - Configurable in-cluster and client bases + * - Optional namePrefix to model “tenant” behavior + * - Records createRepository / setRepositoryPermission calls for assertions + */ +class ScmManagerMock implements GitProvider { + + private final Set initOnceRepos = [] as Set + private final Map createCalls = [:].withDefault{0} + + void initOnceRepo(String fullName) { initOnceRepos << fullName } + void clearInitOnce() { initOnceRepos.clear(); createCalls.clear() } + + + // --- configurable --- + URI inClusterBase = new URI("http://scmm.scm-manager.svc.cluster.local/scm") + URI clientBase = new URI("http://localhost:8080/scm") + String rootPath = "repo" // SCMM rootPath + String namePrefix = "" // e.g., "fv40-" for tenant mode + Credentials credentials = new Credentials("gitops", "gitops") + String gitOpsUsername = "gitops" + URI prometheus = new URI("http://localhost:8080/scm/api/v2/metrics/prometheus") + + // --- call recordings for assertions --- + final List createdRepos = [] + final List permissionCalls = [] + /** Optional sequence to control createRepository() return values per call */ + List nextCreateResults = [] // empty -> default true + + @Override + boolean createRepository(String repoTarget, String description, boolean initialize) { + if (initOnceRepos.contains(repoTarget)) { + return ++createCalls[repoTarget] == 1 // 1. call true, then false + } + createdRepos << repoTarget + // Pretend repository was created successfully. + // If you need idempotency checks, examine createdRepos.count(repoTarget) in your tests. + return nextCreateResults ? nextCreateResults.remove(0) : true + } + + @Override + void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) { + permissionCalls << [ + repoTarget: repoTarget, + principal : principal, + role : role, + scope : scope + ] + } + + /** …/scm/// */ + @Override + String repoUrl(String repoTarget, RepoUrlScope scope) { + URI base = (scope == RepoUrlScope.CLIENT) ? clientBase : inClusterBase + def cleanedBase = withoutTrailingSlash(base).toString() + return "${cleanedBase}/${rootPath}/${repoTarget}" + } + + /** In-cluster repo prefix: …/scm//[] */ + @Override + String repoPrefix() { + def base = withoutTrailingSlash(inClusterBase).toString() + def prefix = (namePrefix ?: "").strip() + return "${base}/${rootPath}/${prefix}" + } + + @Override + Credentials getCredentials() { + return credentials + } + + + /** …/scm/api/v2/metrics/prometheus */ + @Override + URI prometheusMetricsEndpoint() { + return prometheus + } + + @Override + void deleteRepository(String namespace, String repository, boolean prefixNamespace) { + + } + + @Override + void deleteUser(String name) { + + } + + @Override + void setDefaultBranch(String repoTarget, String branch) { + + } + + /** In-cluster base …/scm (without trailing slash) */ + @Override + String getUrl() { + return inClusterBase.toString() + } + + @Override + String getProtocol() { + return inClusterBase.scheme // e.g., "http" + } + + @Override + String getHost() { + return inClusterBase.host // e.g., "scmm.ns.svc.cluster.local" + } + + @Override + String getGitOpsUsername() { + return gitOpsUsername + } + // --- helpers --- + private static URI withoutTrailingSlash(URI uri) { + def s = uri.toString() + return new URI(s.endsWith("/") ? s.substring(0, s.length() - 1) : s) + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/git/TestGitProvider.groovy b/src/test/groovy/com/cloudogu/gitops/utils/git/TestGitProvider.groovy new file mode 100644 index 000000000..615f9bcd3 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/utils/git/TestGitProvider.groovy @@ -0,0 +1,29 @@ +package com.cloudogu.gitops.utils.git + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.git.providers.GitProvider + +class TestGitProvider { + static Map buildProviders(Config cfg) { + if (cfg.scm.scmProviderType?.toString() == 'GITLAB') { + def gitlab = new GitlabMock( + base: new URI(cfg.scm.gitlab.url), + namePrefix: cfg.application.namePrefix + ) + return [tenant: gitlab, central: cfg.multiTenant.useDedicatedInstance ? gitlab : null] + } + + def serviceDns = "http://scmm.${cfg.application.namePrefix}scm-manager.svc.cluster.local/scm" + String tenantInCluster = (cfg.scm.scmManager?.url ?: serviceDns) as String + String centralInCluster = (cfg.multiTenant.scmManager?.url ?: tenantInCluster) as String + + def tenant = new ScmManagerMock(inClusterBase: new URI(tenantInCluster), namePrefix: cfg.application.namePrefix) + def central = cfg.multiTenant.useDedicatedInstance + ? new ScmManagerMock(inClusterBase: new URI(centralInCluster), namePrefix: cfg.application.namePrefix) + : null + return [tenant: tenant, central: central] + } +} + + + diff --git a/src/test/groovy/com/cloudogu/gitops/utils/git/TestGitRepoFactory.groovy b/src/test/groovy/com/cloudogu/gitops/utils/git/TestGitRepoFactory.groovy new file mode 100644 index 000000000..846a00897 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/utils/git/TestGitRepoFactory.groovy @@ -0,0 +1,68 @@ +package com.cloudogu.gitops.utils.git + +import com.cloudogu.gitops.config.Config +import com.cloudogu.gitops.git.GitRepo +import com.cloudogu.gitops.git.GitRepoFactory +import com.cloudogu.gitops.git.providers.GitProvider +import com.cloudogu.gitops.utils.FileSystemUtils +import org.apache.commons.io.FileUtils + +import static org.mockito.Mockito.doAnswer +import static org.mockito.Mockito.spy + + +class TestGitRepoFactory extends GitRepoFactory { + Map repos = [:] + GitProvider defaultProvider + + TestGitRepoFactory(Config config, FileSystemUtils fileSystemUtils) { + super(config, fileSystemUtils) + } + + @Override + GitRepo getRepo(String repoTarget, GitProvider scm) { + def effectiveProvider = scm ?: defaultProvider + + if (!effectiveProvider) { + throw new IllegalStateException( + "No GitProvider provided for repo '${repoTarget}' and defaultProvider is null." + ) + } + + if (repos[repoTarget]) { + return repos[repoTarget] + } + + GitRepo repoNew = new GitRepo(config, scm, repoTarget, fileSystemUtils) { + String remoteGitRepoUrl = '' + + @Override + String getGitRepositoryUrl() { + if (!remoteGitRepoUrl) { + + def tempDir = File.createTempDir('gitops-playground-repocopy') + tempDir.deleteOnExit() + def originalRepo = System.getProperty("user.dir") + "/src/test/groovy/com/cloudogu/gitops/utils/data/git-repository/" + + FileUtils.copyDirectory(new File(originalRepo), tempDir) + remoteGitRepoUrl = 'file://' + tempDir.absolutePath + } + return remoteGitRepoUrl + } + } + + + GitRepo spyRepo = spy(repoNew) + + // Test-only: remove local clone target before cloning to avoid "not empty" errors + doAnswer { invocation -> + File target = new File(spyRepo.absoluteLocalRepoTmpDir) + if (target?.exists()) { + FileUtils.deleteDirectory(target) + } + invocation.callRealMethod() + }.when(spyRepo).cloneRepo() + repos.put(repoTarget, spyRepo) + return spyRepo + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/TestScmmApiClient.groovy b/src/test/groovy/com/cloudogu/gitops/utils/git/TestScmManagerApiClient.groovy similarity index 74% rename from src/test/groovy/com/cloudogu/gitops/utils/TestScmmApiClient.groovy rename to src/test/groovy/com/cloudogu/gitops/utils/git/TestScmManagerApiClient.groovy index 59e4376a2..be30b219f 100644 --- a/src/test/groovy/com/cloudogu/gitops/utils/TestScmmApiClient.groovy +++ b/src/test/groovy/com/cloudogu/gitops/utils/git/TestScmManagerApiClient.groovy @@ -1,27 +1,29 @@ -package com.cloudogu.gitops.utils +package com.cloudogu.gitops.utils.git import com.cloudogu.gitops.config.Config -import com.cloudogu.gitops.scmm.api.Permission -import com.cloudogu.gitops.scmm.api.Repository -import com.cloudogu.gitops.scmm.api.RepositoryApi -import com.cloudogu.gitops.scmm.api.ScmmApiClient +import com.cloudogu.gitops.config.Credentials +import com.cloudogu.gitops.git.providers.scmmanager.Permission +import com.cloudogu.gitops.git.providers.scmmanager.api.Repository +import com.cloudogu.gitops.git.providers.scmmanager.api.RepositoryApi +import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApiClient import okhttp3.internal.http.RealResponseBody import okio.BufferedSource +import org.mockito.ArgumentMatchers import retrofit2.Call import retrofit2.Response import static org.mockito.ArgumentMatchers.* import static org.mockito.Mockito.mock -import static org.mockito.Mockito.when +import static org.mockito.Mockito.when -class TestScmmApiClient extends ScmmApiClient { +class TestScmManagerApiClient extends ScmManagerApiClient { RepositoryApi repositoryApi = mock(RepositoryApi) Set createdRepos = new HashSet<>() Set createdPermissions = new HashSet<>() - TestScmmApiClient(Config config) { - super(config, null) + TestScmManagerApiClient(Config config) { + super(config.scm.scmManager.url, new Credentials(config.scm.scmManager.username, config.scm.scmManager.password), null) } @Override @@ -36,7 +38,7 @@ class TestScmmApiClient extends ScmmApiClient { def responseCreated = mockSuccessfulResponse(201) def responseExists = mockErrorResponse(409) - when(repositoryApi.create(any(Repository), anyBoolean())) + when(repositoryApi.create(ArgumentMatchers.any(Repository), anyBoolean())) .thenAnswer { invocation -> Repository repo = invocation.getArgument(0) if (createdRepos.contains(repo.fullRepoName)) { @@ -48,7 +50,7 @@ class TestScmmApiClient extends ScmmApiClient { } when(repositoryApi.createPermission(anyString(), anyString(), any(Permission))) .thenAnswer { invocation -> - String namespace= invocation.getArgument(0) + String namespace = invocation.getArgument(0) String name = invocation.getArgument(1) if (createdPermissions.contains("${namespace}/${name}".toString())) { return responseExists @@ -72,4 +74,4 @@ class TestScmmApiClient extends ScmmApiClient { when(expectedCall.execute()).thenReturn(errorResponse) expectedCall } -} +} \ No newline at end of file diff --git a/src/test/resources/testMainConfig.yaml b/src/test/resources/testMainConfig.yaml index f551dad57..25b8ab00e 100644 --- a/src/test/resources/testMainConfig.yaml +++ b/src/test/resources/testMainConfig.yaml @@ -23,14 +23,16 @@ jenkins: mavenCentralMirror: "" helm: values: {} -scmm: - url: "http://172.18.0.2:9091/scm" - username: "admin" - password: "admin" - helm: - chart: "scm-manager" - repoURL: "https://packages.scm-manager.org/repository/helm-v2-releases/" - version: "3.2.1" +scm: + scmProviderType: "SCM_MANAGER" + scmManager: + url: "http://172.18.0.2:9091/scm" + username: "admin" + password: "admin" + helm: + chart: "scm-manager" + repoURL: "https://packages.scm-manager.org/repository/helm-v2-releases/" + version: "3.2.1" application: remote: false insecure: false