diff --git a/.catalog-onboard-pipeline.yaml b/.catalog-onboard-pipeline.yaml new file mode 100644 index 00000000..026c4f6b --- /dev/null +++ b/.catalog-onboard-pipeline.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +offerings: + - name: deploy-arch-ibm-eso + kind: solution + catalog_id: 7df1e4ca-d54c-4fd0-82ce-3d13247308cd + offering_id: 3745fd94-b05c-4720-9d0f-fc97e76d4850 + variations: + - name: fully-configurable + mark_ready: true + install_type: fullstack + pre_validation: "tests/scripts/pre-validation-eso.sh" + post_validation: "tests/scripts/post-validation-eso.sh" diff --git a/.gitignore b/.gitignore index 7bee4643..0103195a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,9 @@ Brewfile.lock.json testpod*.yaml precommit.txt +# drawio temporary files +**/.*.drawio.bkp + # VS Code state .vscode/ *.code-workspace diff --git a/.releaserc b/.releaserc index 708916f7..622ce915 100644 --- a/.releaserc +++ b/.releaserc @@ -10,6 +10,9 @@ }], ["@semantic-release/exec", { "successCmd": "echo \"SEMVER_VERSION=${nextRelease.version}\" >> $GITHUB_ENV" + }], + ["@semantic-release/exec", { + "publishCmd": "./ci/trigger-catalog-onboarding-pipeline.sh --version=v${nextRelease.version}" }] ] } diff --git a/README.md b/README.md index 70da8109..374e99e4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This module automates the installation and configuration of the [External Secret -## external-secrets-operator-module +## External Secrets Operator module External Secrets Operator synchronizes secrets in the Kubernetes cluster with secrets that are mapped in [Secrets Manager](https://cloud.ibm.com/docs/secrets-manager). @@ -288,11 +288,11 @@ For more information about IAM Trusted profiles and ESO Multitenancy configurati - [Setup of ESO as a Service from RedHat](https://cloud.redhat.com/blog/how-to-setup-external-secrets-operator-eso-as-a-service) - [ESO Multitenancy configuration from ESO Docs](https://external-secrets.io/latest/guides/multi-tenancy/) -### _Important current limitation of ESO deployment_ +### _Important current architectural limitation of ESO deployment_ The current ESO version doesn't allow to customise the default IAM endpoint (https://iam.cloud.ibm.com) it uses when authenticating through apikey (`api_key` authentication) for both ClusterSecretStore and SecretStore APIs. -As a direct effect of this limitation, for a standard OCP cluster topology as defined by GoldenEye design (3 workers zones `edge` `private` and `transit`), an ESO deployment with `api_key` authentication configuration needs to be performed on the workers pool with access to the public network (`dedicated: edge` label in GE usual topology) to work fine. If the ESO deployment is performed on a workers pool without access to public network (i.e. to https://iam.cloud.ibm.com) the apikey authentication is expected to fail. +As a direct effect of this limitation, for an OCP cluster topology designed with three different subnet layers `edge` `private` and `transit`, where only `edge` one has access to the public network, `private` is for business workload and `transit` for private networking, an ESO deployment with `api_key` authentication configuration needs to be performed on the workers pool with access to the public network (`dedicated: edge` label in GE usual topology) to work fine. If the ESO deployment is performed on a workers pool without access to public network (i.e. to https://iam.cloud.ibm.com) the apikey authentication is expected to fail, unless ESO is enrolled into RedHat Service Mesh (this module allows to add the expected resources annotations but the Mesh gateways configuration is out of the scope of the module) or a different networking solution is implemented. ### Pod Reloader @@ -476,6 +476,18 @@ module "external_secrets_operator" { } ``` +## Required IAM access policies +You need the following permissions to run this module. + +- Account Management + - IAM Services + - **Secrets Manager** service + - `Administrator` platform access + - `Manager` service access + - **Kubernetes** service + - `Administrator` platform access + - `Manager` service access + ### Requirements @@ -509,9 +521,9 @@ module "external_secrets_operator" { | [eso\_enroll\_in\_servicemesh](#input\_eso\_enroll\_in\_servicemesh) | Flag to enroll ESO into istio servicemesh | `bool` | `false` | no | | [eso\_image](#input\_eso\_image) | The External Secrets Operator image in the format of `[registry-url]/[namespace]/[image]`. | `string` | `"ghcr.io/external-secrets/external-secrets"` | no | | [eso\_image\_version](#input\_eso\_image\_version) | The version or digest for the external secrets image to deploy. If changing the value, ensure it is compatible with the chart version set in eso\_chart\_version. | `string` | `"v0.17.0-ubi@sha256:5c9f7750fb922fb09cfc3b430d5916923b85f17ba5099b244173344ab3046b53"` | no | -| [eso\_namespace](#input\_eso\_namespace) | Namespace to create and be used to install ESO components including helm releases. If eso\_store\_scope == cluster, this will also be used to deploy ClusterSecretStore/cluster\_store in it | `string` | `null` | no | +| [eso\_namespace](#input\_eso\_namespace) | Namespace to create and be used to install ESO components including helm releases. | `string` | `null` | no | | [eso\_pod\_configuration](#input\_eso\_pod\_configuration) | Configuration to use to customise ESO deployment on specific pods. Setting appropriate values will result in customising ESO helm release. Default value is {} to keep ESO standard deployment. Ignore the key if not required. |
object({
annotations = optional(object({
# The annotations for external secret controller pods.
external_secrets = optional(map(string), {})
# The annotations for external secret cert controller pods.
external_secrets_cert_controller = optional(map(string), {})
# The annotations for external secret controller pods.
external_secrets_webhook = optional(map(string), {})
}), {})

labels = optional(object({
# The labels for external secret controller pods.
external_secrets = optional(map(string), {})
# The labels for external secret cert controller pods.
external_secrets_cert_controller = optional(map(string), {})
# The labels for external secret controller pods.
external_secrets_webhook = optional(map(string), {})
}), {})
})
| `{}` | no | -| [existing\_eso\_namespace](#input\_existing\_eso\_namespace) | Existing Namespace to be used to install ESO components including helm releases. If eso\_store\_scope == cluster, this will also be used to deploy ClusterSecretStore/cluster\_store in it | `string` | `null` | no | +| [existing\_eso\_namespace](#input\_existing\_eso\_namespace) | Existing Namespace to be used to install ESO components including helm releases. | `string` | `null` | no | | [reloader\_chart\_location](#input\_reloader\_chart\_location) | The location of the Reloader Helm chart. | `string` | `"https://stakater.github.io/stakater-charts"` | no | | [reloader\_chart\_version](#input\_reloader\_chart\_version) | The version of the Reloader Helm chart. Ensure that the chart version is compatible with the image version specified in reloader\_image\_version. | `string` | `"2.1.4"` | no | | [reloader\_custom\_values](#input\_reloader\_custom\_values) | String containing custom values to be used for reloader helm chart. See https://github.com/stakater/Reloader/blob/master/deployments/kubernetes/chart/reloader/values.yaml | `string` | `null` | no | diff --git a/deploy-arch-ibm-eso.svg b/deploy-arch-ibm-eso.svg new file mode 100644 index 00000000..1a8538c7 --- /dev/null +++ b/deploy-arch-ibm-eso.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/all-combined/main.tf b/examples/all-combined/main.tf index 7cb276e5..994c1081 100644 --- a/examples/all-combined/main.tf +++ b/examples/all-combined/main.tf @@ -52,7 +52,7 @@ locals { machine_type = "bx2.4x16" workers_per_zone = 1 labels = { "dedicated" : "default" } - operating_system = "REDHAT_8_64" + operating_system = "RHEL_9_64" } ] diff --git a/examples/basic/main.tf b/examples/basic/main.tf index f6610ec0..383daf03 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -64,7 +64,7 @@ locals { machine_type = "bx2.4x16" workers_per_zone = 1 labels = { "dedicated" : "default" } - operating_system = "REDHAT_8_64" + operating_system = "RHEL_9_64" } ] diff --git a/ibm_catalog.json b/ibm_catalog.json new file mode 100644 index 00000000..f2d5e50e --- /dev/null +++ b/ibm_catalog.json @@ -0,0 +1,276 @@ + +{ + "products": [ + { + "name": "deploy-arch-ibm-eso", + "label": "Cloud automation for External Secrets Operator", + "product_kind": "solution", + "tags": [ + "ibm_created", + "target_terraform", + "terraform", + "solution", + "security" + ], + "keywords": [ + "Secrets", + "Secrets Manager", + "IaC", + "infrastructure as code", + "terraform", + "solution" + ], + "short_description": "Deploys the External Secrets Operator (ESO) on an IBM Cloud Kubernetes Service (IKS) OpenShift cluster.", + "long_description": "This architecture allows to deploy the External Secrets Operator (ESO) and the related configuration on an IBM Cloud OpenShift Cluster to manage the secrets deployed on the cluster through IBM Cloud Secrets Manager. For more information about the External Secrets Operator, please refer to the [External Secrets Operator documentation](https://external-secrets.io/latest/).", + "offering_docs_url": "https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/blob/main/solutions/fully-configurable/README.md", + "offering_icon_url": "https://raw.githubusercontent.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/refs/heads/main/deploy-arch-ibm-eso.svg", + "provider_name": "IBM", + "features": [ + { + "title": "Deploys the External Secrets Operator (ESO) in existing cluster", + "description": "This architecture allows to deploy the External Secrets Operator on an existing IBM Cloud OpenShift Cluster.
For more details about the features and the options available please refer to this [page](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/blob/main/solutions/fully-configurable/DA-details.md)" + }, + { + "title": "Configures the External Secrets Operator (ESO) Cluster Secrets Stores and Secrets Stores with the related ServiceIDs, Secrets Groups and authentication methods", + "description": "Deploy and configure ESO Cluster Secret Store resources for cluster scoped secrets store and ESO Secret Store resources for namespace scoped secrets store.
For more details about Cluster Secret Store please refer to this [documentation](https://external-secrets.io/latest/api/clustersecretstore/).
For more details about Secret Store please refer to this [documentation](https://external-secrets.io/latest/api/secretstore/).
Both the Secret Store types support two different authentication methods, IAM API key and Truster Profile.
For more details about configuring the Stores through this architecture input please refer to this [page](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/blob/main/solutions/fully-configurable/DA-eso-configuration.md)" + }, + { + "title": "Deploys and configures Stakater Reloader into the cluster", + "description": "The architecture allows to optionally deploy Stakater Reloader into the cluster that helps with refreshing the cluster's secrets values by reloading pods when needed.
For more information about Stakater Reloader, please refer to the [Stakater Reloader documentation](https://github.com/stakater/Reloader)." + } + ], + "support_details": "This product is in the community registry, as such support is handled through the originated repo. If you experience issues please open an issue [in this repository](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/issues). Please note this product is not supported via the IBM Cloud Support Center.", + "flavors": [ + { + "label": "Fully configurable", + "name": "fully-configurable", + "install_type": "fullstack", + "working_directory": "solutions/fully-configurable", + "iam_permissions": [ + { + "role_crns": [ + "crn:v1:bluemix:public:iam::::role:Administrator" + ], + "service_name": "iam-identity" + }, + { + "service_name": "secrets-manager", + "role_crns": [ + "crn:v1:bluemix:public:iam::::serviceRole:Administrator", + "crn:v1:bluemix:public:iam::::serviceRole:Manager" + ] + }, + { + "service_name": "containers-kubernetes", + "role_crns": [ + "crn:v1:bluemix:public:iam::::serviceRole:Manager", + "crn:v1:bluemix:public:iam::::role:Editor" + ] + } + ], + "configuration": [ + { + "key": "ibmcloud_api_key" + }, + { + "key": "prefix", + "required": true + }, + { + "key": "provider_visibility", + "hidden": true, + "options": [ + { + "displayname": "private", + "value": "private" + }, + { + "displayname": "public", + "value": "public" + }, + { + "displayname": "public-and-private", + "value": "public-and-private" + } + ] + }, + { + "key": "existing_cluster_crn", + "required": true + }, + { + "key": "existing_secrets_manager_crn", + "required": true + }, + { + "key": "secrets_manager_ibmcloud_api_key" + }, + { + "key": "eso_namespace" + }, + { + "key": "existing_eso_namespace" + }, + { + "key": "eso_cluster_nodes_configuration" + }, + { + "key": "eso_pod_configuration" + }, + { + "key": "eso_image" + }, + { + "key": "eso_image_version" + }, + { + "key": "eso_chart_location" + }, + { + "key": "eso_chart_version" + }, + { + "key": "eso_enroll_in_servicemesh" + }, + { + "key": "reloader_deployed" + }, + { + "key": "reloader_reload_strategy" + }, + { + "key": "reloader_namespaces_to_ignore", + "type": "string", + "custom_config": { + "type": "array", + "grouping": "deployment", + "original_grouping": "deployment", + "config_constraints": { + "type": "string" + } + } + }, + { + "key": "reloader_resources_to_ignore", + "custom_config": { + "type": "array", + "grouping": "deployment", + "original_grouping": "deployment", + "config_constraints": { + "type": "string" + } + } + }, + { + "key": "reloader_namespaces_selector", + "custom_config": { + "type": "array", + "grouping": "deployment", + "original_grouping": "deployment", + "config_constraints": { + "type": "string" + } + } + }, + { + "key": "reloader_resource_label_selector", + "custom_config": { + "type": "array", + "grouping": "deployment", + "original_grouping": "deployment", + "config_constraints": { + "type": "string" + } + } + }, + { + "key": "reloader_ignore_secrets" + }, + { + "key": "reloader_ignore_configmaps" + }, + { + "key": "reloader_is_openshift" + }, + { + "key": "reloader_is_argo_rollouts" + }, + { + "key": "reloader_reload_on_create" + }, + { + "key": "reloader_sync_after_restart" + }, + { + "key": "reloader_pod_monitor_metrics" + }, + { + "key": "reloader_log_format" + }, + { + "key": "reloader_custom_values" + }, + { + "key": "reloader_image" + }, + { + "key": "reloader_image_version" + }, + { + "key": "reloader_chart_location" + }, + { + "key": "reloader_chart_version" + }, + { + "key": "eso_secretsstores_configuration", + "type": "string", + "custom_config": { + "type": "json_editor", + "grouping": "deployment", + "original_grouping": "deployment", + "config_constraints": { + "type": "string" + } + } + }, + { + "key": "service_endpoints", + "options": [ + { + "displayname": "Public", + "value": "public" + }, + { + "displayname": "Private", + "value": "private" + } + ] + } + ], + "architecture": { + "features": [ + { + "title": " ", + "description": "Configured to use IBM secure by default standards, but can be edited to fit your use case." + } + ], + "diagrams": [ + { + "diagram": { + "caption": "External Secrets Operator architecture on IBM Cloud OpenShift cluster", + "url": "https://raw.githubusercontent.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/main/reference-architecture/reference-architecture/eso.svg", + "type": "image/svg+xml" + }, + "description": "This architecture supports deploying External Secrets Operator on IBM Cloud OpenShift cluster." + } + ] + }, + "dependencies": [], + "dependency_version_2": true, + "terraform_version": "1.10.5" + } + ] + } + ] + } diff --git a/modules/eso-clusterstore/README.md b/modules/eso-clusterstore/README.md index f48ea924..1f099d1f 100644 --- a/modules/eso-clusterstore/README.md +++ b/modules/eso-clusterstore/README.md @@ -35,14 +35,14 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [clusterstore\_helm\_rls\_name](#input\_clusterstore\_helm\_rls\_name) | Name of helm release for clusterstore | `string` | `"cluster-secret-store"` | no | -| [clusterstore\_name](#input\_clusterstore\_name) | Name of the ESO secret store to be used/created for cluster scope. | `string` | `"clustersecret-store"` | no | -| [clusterstore\_secret\_apikey](#input\_clusterstore\_secret\_apikey) | APIkey to be configured in the clusterstore\_secret\_name secret in the ESO clusterstore. One between clusterstore\_secret\_apikey and clusterstore\_trusted\_profile\_name must be filled | `string` | `null` | no | -| [clusterstore\_secret\_name](#input\_clusterstore\_secret\_name) | Secret name to be used/referenced in the ESO clusterstore to pull from Secrets Manager | `string` | `"ibm-secret"` | no | -| [clusterstore\_secrets\_manager\_guid](#input\_clusterstore\_secrets\_manager\_guid) | Secrets manager instance GUID for clusterstore where secrets will be stored or fetched from | `string` | n/a | yes | -| [clusterstore\_trusted\_profile\_name](#input\_clusterstore\_trusted\_profile\_name) | The name of the trusted profile to use for clusterstore scope. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance | `string` | `null` | no | +| [clusterstore\_helm\_rls\_name](#input\_clusterstore\_helm\_rls\_name) | Name of helm release for cluster secrets store | `string` | `"cluster-secret-store"` | no | +| [clusterstore\_name](#input\_clusterstore\_name) | Name of the ESO cluster secrets store to be used/created for cluster scope. | `string` | `"clustersecret-store"` | no | +| [clusterstore\_secret\_apikey](#input\_clusterstore\_secret\_apikey) | APIkey to be configured in the clusterstore\_secret\_name secret in the ESO cluster secrets store. One between clusterstore\_secret\_apikey and clusterstore\_trusted\_profile\_name must be filled | `string` | `null` | no | +| [clusterstore\_secret\_name](#input\_clusterstore\_secret\_name) | Secret name to be used/referenced in the ESO cluster secrets store to pull from Secrets Manager | `string` | `"ibm-secret"` | no | +| [clusterstore\_secrets\_manager\_guid](#input\_clusterstore\_secrets\_manager\_guid) | Secrets manager instance GUID for cluster secrets store where secrets will be stored or fetched from | `string` | n/a | yes | +| [clusterstore\_trusted\_profile\_name](#input\_clusterstore\_trusted\_profile\_name) | The name of the trusted profile to use for cluster secrets store scope. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance | `string` | `null` | no | | [eso\_authentication](#input\_eso\_authentication) | Authentication method, Possible values are api\_key or/and trusted\_profile. | `string` | `"trusted_profile"` | no | -| [eso\_namespace](#input\_eso\_namespace) | Namespace where the ESO is deployed. It will be used to deploy the ClusterStore | `string` | n/a | yes | +| [eso\_namespace](#input\_eso\_namespace) | Namespace where the ESO is deployed. It will be used to deploy the cluster secrets store | `string` | n/a | yes | | [region](#input\_region) | Region where Secrets Manager is deployed. It will be used to build the regional URL to the service | `string` | n/a | yes | | [service\_endpoints](#input\_service\_endpoints) | The service endpoint type to communicate with the provided secrets manager instance. Possible values are `public` or `private`. This also will set the iam endpoint for containerAuth when enabling Trusted Profile/CR based authentication. | `string` | `"public"` | no | diff --git a/modules/eso-clusterstore/variables.tf b/modules/eso-clusterstore/variables.tf index a7b865b5..06c97791 100644 --- a/modules/eso-clusterstore/variables.tf +++ b/modules/eso-clusterstore/variables.tf @@ -1,7 +1,7 @@ ######## eso clusterstore configuration variable "eso_namespace" { - description = "Namespace where the ESO is deployed. It will be used to deploy the ClusterStore" + description = "Namespace where the ESO is deployed. It will be used to deploy the cluster secrets store" type = string } @@ -16,24 +16,28 @@ variable "service_endpoints" { default = "public" validation { condition = contains(["public", "private"], var.service_endpoints) - error_message = "The specified service_endpoints is not a valid selection!" + error_message = "The value for var.service_endpoints must be either public or private." } } variable "clusterstore_name" { - description = "Name of the ESO secret store to be used/created for cluster scope." + description = "Name of the ESO cluster secrets store to be used/created for cluster scope." default = "clustersecret-store" type = string + validation { + condition = can(regex("^([a-z][-a-z0-9]*[a-z0-9])$", var.clusterstore_name)) + error_message = "The cluster secrets store name must start with a lowercase letter, can contain lowercase letters, numbers and hyphens, and must end with a lowercase letter." + } } variable "clusterstore_helm_rls_name" { - description = "Name of helm release for clusterstore" + description = "Name of helm release for cluster secrets store" type = string default = "cluster-secret-store" } ############################################################################## -# Authentication configuration for clusterstore that can be one of api_key or trusted_profile +# Authentication configuration for cluster secrets store that can be one of api_key or trusted_profile ############################################################################## variable "eso_authentication" { type = string @@ -50,14 +54,14 @@ variable "eso_authentication" { } variable "clusterstore_secret_name" { - description = "Secret name to be used/referenced in the ESO clusterstore to pull from Secrets Manager" + description = "Secret name to be used/referenced in the ESO cluster secrets store to pull from Secrets Manager" default = "ibm-secret" type = string } variable "clusterstore_secret_apikey" { type = string - description = "APIkey to be configured in the clusterstore_secret_name secret in the ESO clusterstore. One between clusterstore_secret_apikey and clusterstore_trusted_profile_name must be filled" + description = "APIkey to be configured in the clusterstore_secret_name secret in the ESO cluster secrets store. One between clusterstore_secret_apikey and clusterstore_trusted_profile_name must be filled" sensitive = true default = null validation { @@ -74,7 +78,7 @@ variable "clusterstore_secret_apikey" { variable "clusterstore_trusted_profile_name" { type = string - description = "The name of the trusted profile to use for clusterstore scope. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance" + description = "The name of the trusted profile to use for cluster secrets store scope. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance" default = null } @@ -82,5 +86,5 @@ variable "clusterstore_trusted_profile_name" { variable "clusterstore_secrets_manager_guid" { type = string - description = "Secrets manager instance GUID for clusterstore where secrets will be stored or fetched from" + description = "Secrets manager instance GUID for cluster secrets store where secrets will be stored or fetched from" } diff --git a/modules/eso-secretstore/README.md b/modules/eso-secretstore/README.md index 1931a38f..51f38357 100644 --- a/modules/eso-secretstore/README.md +++ b/modules/eso-secretstore/README.md @@ -38,13 +38,13 @@ No modules. | [eso\_authentication](#input\_eso\_authentication) | Authentication method, Possible values are api\_key or/and trusted\_profile. | `string` | `"trusted_profile"` | no | | [region](#input\_region) | Region where Secrets Manager is deployed. It will be used to build the regional URL to the service | `string` | n/a | yes | | [service\_endpoints](#input\_service\_endpoints) | The service endpoint type to communicate with the provided secrets manager instance. Possible values are `public` or `private`. This also will set the iam endpoint for containerAuth when enabling Trusted Profile/CR based authentication. | `string` | `"public"` | no | -| [sstore\_helm\_rls\_name](#input\_sstore\_helm\_rls\_name) | Name of helm release for external secret | `string` | `"external-secret-store"` | no | -| [sstore\_namespace](#input\_sstore\_namespace) | Namespace to create the SecretStore. The namespace must exist as it is not created by this module | `string` | n/a | yes | -| [sstore\_secret\_apikey](#input\_sstore\_secret\_apikey) | APIkey to be stored into sstore\_secret\_name to authenticate on Secrets Manager instance | `string` | `null` | no | -| [sstore\_secret\_name](#input\_sstore\_secret\_name) | Secret name to be used/referenced in the ESO secretstore to pull from Secrets Manager | `string` | `"ibm-secret"` | no | -| [sstore\_secrets\_manager\_guid](#input\_sstore\_secrets\_manager\_guid) | Secrets manager instance GUID for secretstore where secrets will be stored or fetched from | `string` | n/a | yes | +| [sstore\_helm\_rls\_name](#input\_sstore\_helm\_rls\_name) | Name of helm release for the secrets store | `string` | `"external-secret-store"` | no | +| [sstore\_namespace](#input\_sstore\_namespace) | Namespace to create the secret store. The namespace must exist as it is not created by this module | `string` | n/a | yes | +| [sstore\_secret\_apikey](#input\_sstore\_secret\_apikey) | APIkey to be stored into var.sstore\_secret\_name secret to authenticate with Secrets Manager instance | `string` | `null` | no | +| [sstore\_secret\_name](#input\_sstore\_secret\_name) | Secret name to be used/referenced in the ESO secretsstore to pull from Secrets Manager | `string` | `"ibm-secret"` | no | +| [sstore\_secrets\_manager\_guid](#input\_sstore\_secrets\_manager\_guid) | Secrets manager instance GUID for secrets store where secrets will be stored or fetched from | `string` | n/a | yes | | [sstore\_store\_name](#input\_sstore\_store\_name) | Name of the SecretStore to create | `string` | n/a | yes | -| [sstore\_trusted\_profile\_name](#input\_sstore\_trusted\_profile\_name) | The name of the trusted profile to use for the secretstore. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance | `string` | `null` | no | +| [sstore\_trusted\_profile\_name](#input\_sstore\_trusted\_profile\_name) | The name of the trusted profile to use for the secrets store. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance | `string` | `null` | no | ### Outputs diff --git a/modules/eso-secretstore/variables.tf b/modules/eso-secretstore/variables.tf index 768ca6b1..86035d49 100644 --- a/modules/eso-secretstore/variables.tf +++ b/modules/eso-secretstore/variables.tf @@ -5,12 +5,6 @@ variable "region" { type = string } -variable "sstore_helm_rls_name" { - description = "Name of helm release for external secret" - type = string - default = "external-secret-store" -} - variable "service_endpoints" { type = string description = "The service endpoint type to communicate with the provided secrets manager instance. Possible values are `public` or `private`. This also will set the iam endpoint for containerAuth when enabling Trusted Profile/CR based authentication." @@ -22,7 +16,7 @@ variable "service_endpoints" { } ############################################################################## -# Authentication configuration for clusterstore that can be one of api_key or trusted_profile +# Authentication configuration for secretsstore that can be one of api_key or trusted_profile ############################################################################## variable "eso_authentication" { type = string @@ -41,13 +35,13 @@ variable "eso_authentication" { ####### apikey authentication variable "sstore_secret_name" { - description = "Secret name to be used/referenced in the ESO secretstore to pull from Secrets Manager" + description = "Secret name to be used/referenced in the ESO secretsstore to pull from Secrets Manager" default = "ibm-secret" type = string } variable "sstore_secret_apikey" { - description = "APIkey to be stored into sstore_secret_name to authenticate on Secrets Manager instance" + description = "APIkey to be stored into var.sstore_secret_name secret to authenticate with Secrets Manager instance" type = string default = null validation { @@ -60,25 +54,37 @@ variable "sstore_secret_apikey" { } } -####### trusted profile +####### trusted profile authentication variable "sstore_trusted_profile_name" { type = string - description = "The name of the trusted profile to use for the secretstore. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance" + description = "The name of the trusted profile to use for the secrets store. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance" default = null } +####### secrets store configuration + variable "sstore_secrets_manager_guid" { type = string - description = "Secrets manager instance GUID for secretstore where secrets will be stored or fetched from" + description = "Secrets manager instance GUID for secrets store where secrets will be stored or fetched from" } variable "sstore_namespace" { type = string - description = "Namespace to create the SecretStore. The namespace must exist as it is not created by this module" + description = "Namespace to create the secret store. The namespace must exist as it is not created by this module" } variable "sstore_store_name" { type = string description = "Name of the SecretStore to create" + validation { + condition = can(regex("^([a-z][-a-z0-9]*[a-z0-9])$", var.sstore_store_name)) + error_message = "The secrets store name must start with a lowercase letter, can contain lowercase letters, numbers and hyphens, and must end with a lowercase letter." + } +} + +variable "sstore_helm_rls_name" { + description = "Name of helm release for the secrets store" + type = string + default = "external-secret-store" } diff --git a/reference-architecture/eso.svg b/reference-architecture/eso.svg new file mode 100644 index 00000000..9f219546 --- /dev/null +++ b/reference-architecture/eso.svg @@ -0,0 +1,4 @@ + + + +
IBM Cloud
Existing Resource Group
Secrets Manager
Existing IBM VPC
Existing OpenShift Cluster
eso operator namespace
External Secrets Operator
Stakater Reloader
\ No newline at end of file diff --git a/solutions/fully-configurable/DA-details.md b/solutions/fully-configurable/DA-details.md new file mode 100644 index 00000000..2c4cf667 --- /dev/null +++ b/solutions/fully-configurable/DA-details.md @@ -0,0 +1,31 @@ +# Terraform IBM External Secrets Operator + +This architecture allows to deploy [External Secrets Operator](https://external-secrets.io/latest/) (also known as ESO) on an existing IBM Cloud OpenShift Cluster + +External Secrets Operator synchronizes secrets in the Kubernetes cluster with secrets that are mapped in [Secrets Manager](https://cloud.ibm.com/docs/secrets-manager). + +The architecture provides the following features: +- Install and configure External Secrets Operator (ESO). +- Customise External Secret Operator deployment on specific cluster workers by configuration approriate NodeSelector and Tolerations in the ESO helm release [More details below](#customise-eso-deployment-on-specific-cluster-nodes) +- Deploy and configure [ClusterSecretStore](https://external-secrets.io/latest/api/clustersecretstore/) resources for cluster scope secrets store +- Deploy and configure [SecretStore](https://external-secrets.io/latest/api/secretstore/) resources for namespace scope secrets store +- Leverage on two authentication methods to be configured on the single stores instances: + - IAM apikey standard authentication + - IAM Trusted profile + +The current version of the architecture supports multitenants configuration by setting up "ESO as a service" (ref. https://cloud.redhat.com/blog/how-to-setup-external-secrets-operator-eso-as-a-service) for both authentication methods
+[More details](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator#example-of-multitenancy-configuration-example-in-namespaced-externalsecrets-stores) + +### Pod Reloader + +The architecture allows also to deploy optionally Stakater Reloader](https://github.com/stakater/Reloader): when secrets are updated, depending on you configuration pods may need to be restarted to pick up the new secrets. To do this you can use it. +By default, the module deploys this to watch for changes in secrets and configmaps and trigger a rolling update of the related pods. +To have Reloader watch a secret or configMap add the annotation `reloader.stakater.com/auto: "true"` to the secret or configMap, the same annotation can be added to deployments to have them restarted when the secret or configMap changes. + +This can be further configured as needed, for more details see https://github.com/stakater/Reloader By default it watches all namespaces. +If you do not need it please set `reloader_deployed = false` in the input variable value. + +### Output content and Secrets configuration + +This architecture doesn't provide support for configuring the Secrets and the ESO external-secrets structures needed to synchronize the secret with Secrets Manager. +However its output provides, for each Cluster Secrets Store and Secrets Store configured in input, the IDs for the ServiceIDs, for the Account and Service Secrets Groups and so on: these output structures can be easily used in a terraform template to configure and deploy the secrets on the cluster. diff --git a/solutions/fully-configurable/DA-eso-configuration.md b/solutions/fully-configurable/DA-eso-configuration.md new file mode 100644 index 00000000..ae1d0cf8 --- /dev/null +++ b/solutions/fully-configurable/DA-eso-configuration.md @@ -0,0 +1,127 @@ +# Configuring External Secrets Operator stores + +In the Architecture configuration it is possible to configure the set of [Cluster Secrets Stores](https://external-secrets.io/latest/api/clustersecretstore/) and [Secrets Stores](https://external-secrets.io/latest/api/clustersecretstore/) to deploy on the cluster after the External Secrets Operator deployment and that are responsible to handle the integration of the operator components with the IBM Cloud Secrets Manager instance. +The configuration of these stores is performed through the complex object `eso_secretsstores_configuration` and by specifying this input parameter when configuring the deployable architecture. + +The input variable `eso_secretsstores_configuration` is a map with two keys, `cluster_secrets_stores` and `secrets_stores`, that respectively collect the list of objects to define the cluster secrets stores and secrets stores. Each object in the two map's elements represent a secrets store. Each store's key is used also as name of the store itself. Each store has the following attributes to define its configuration: + - `namespace` is the namespace where the store is to be created, and `create_namespace` can be used to control if the namespace is to be created or it is already available on the cluster. + - `serviceid_name` and `serviceid_description` provide the details to create the ServiceID to be entitled to read the secrets from Secrets Manager. As alternative, if already available, it is possible to specify `existing_serviceid_id` (no ServiceID is created in this case). The ServiceID (newly created or existing) will be provided with the IAM the policies to read the secrets for each of the Secrets Groups associated with the store. + - `account_secrets_group_name` and `account_secrets_group_description` provide the details to create the Secrets Group (named Account Secrets Group in the context of ESO) where to create the secret to store the API key (called account API key starting from now) configured in the cluster to pull the secrets from Secrets Manager (and owned by the ServiceID defined through `serviceid_name` and `serviceid_description`). As alternative is possible to provide the ID of an already existing Account ServiceID through `existing_account_secrets_group_id`. In this case no Account ServiceID is created. + - `service_secrets_groups_list` is the list of Secrets Groups to create for the store where to create the secrets to be managed by the store. Each element of the list has two fields `name` and `description` to configure the Secret Group. + - `existing_service_secrets_group_id_list` is the list of already existing Secrets Group where to create the secrets to be managed by the store. This list will be merged to the list of the ones created through the `service_secrets_groups_list` and the final list will be used to create the policies to allow the ServiceID owner of the account API key to read the secrets in these Secrets Groups + - `trusted_profile_name` and `trusted_profile_description` provide the details to authenticate pull the secrets from Secrets Manager through trusted profile authentication as alternative to API key one. + - the logic to authenticate on Secrets Manager for the store to pull secrets is the following: + - if a trusted profile name is provided, the trusted profile is created and the related authentication is used to pull secrets from Secrets Manager for the store. If no trusted profile is provided + - if an existing ServiceID is provided, it will be used to pull secrets from Secrets Manager (and will be provided with the entitlement to read secrets for all the Service Secrets Groups) + - if an existing ServiceID isn't provided, the ServiceID created through `serviceid_name` and `serviceid_description` will be used + +Below an example that sets this input variable: + +``` +{ + cluster_secrets_stores = { + "cluster-secrets-store-1" = { + namespace = "eso-namespace-cs1" + create_namespace = true + existing_serviceid_id = null + serviceid_name = "esoda-test-cluster-secrets-store-1-serviceid" + serviceid_description = "esoda-test-cluster-secrets-store-1-serviceid description" + existing_account_secrets_group_id = "" + account_secrets_group_name = "esoda-test-cs-account-secrets-group-1" + account_secrets_group_description = "esoda-test-cs-account-secrets-group-1 description" + trusted_profile_name = "" + trusted_profile_description = null + existing_service_secrets_group_id_list = [] + service_secrets_groups_list = [{ + name = "esoda-test-cs-service1-secrets-group" + description = "Secrets group for secrets used by the ESO" + },{ + name = "esoda-test-cs-service2-secrets-group" + description = "Secrets group 2 for secrets used by the ESO" + }] + }, + "cluster-secrets-store-2" = { + namespace = "eso-namespace-cs2" + create_namespace = true + existing_serviceid_id = null + serviceid_name = "" + serviceid_description = "" + existing_account_secrets_group_id = "23cc32a3-d29e-688b-abe2-d7f7129f73f3" + account_secrets_group_name = "esoda-test-cs-account-secrets-group-2" + account_secrets_group_description = "esoda-test-cs-account-secrets-group-2 description" + trusted_profile_name = null + trusted_profile_description = null + existing_service_secrets_group_id_list = [] + service_secrets_groups_list = [{ + name = "esoda-test-cs-service3-secrets-group" + description = "Secrets group 3 for secrets used by the ESO" + },{ + name = "esoda-test-cs-service4-secrets-group" + description = "Secrets group 4 for secrets used by the ESO" + }] + }, + "cluster-secrets-store-3" = { + namespace = "eso-namespace-cs3" + create_namespace = true + existing_serviceid_id = null + serviceid_name = "" + serviceid_description = "" + existing_account_secrets_group_id = "" + account_secrets_group_name = "esoda-test-cs-account-secrets-group-2" + account_secrets_group_description = "esoda-test-cs-account-secrets-group-2 description" + trusted_profile_name = "cs3-trustedprofile" + trusted_profile_description = "Trusted profile to authenticate cs3" + existing_service_secrets_group_id_list = [] + service_secrets_groups_list = [{ + name = "esoda-test-cs-service5-secrets-group" + description = "Secrets group 5 for secrets used by the ESO" + },{ + name = "esoda-test-cs-service6-secrets-group" + description = "Secrets group 6 for secrets used by the ESO" + }] + } + } + secrets_stores = { + "secrets-store-1" = { + namespace = "eso-namespace-ss1" + create_namespace = true + existing_serviceid_id = "ServiceId-0ec46d95-c28d-4768-a912-5fcf73d4959e" + serviceid_name = "" + serviceid_description = "" + existing_account_secrets_group_id = "" + account_secrets_group_name = "esoda-test-ss-account-secrets-group-1" + account_secrets_group_description = "esoda-test-ss-account-secrets-group-1 description" + trusted_profile_name = "" + trusted_profile_description = null + existing_service_secrets_group_id_list = [] + service_secrets_groups_list = [{ + name = "esoda-test-ss-service1-secrets-group" + description = "Secrets group 1 for secrets used by the ESO" + },{ + name = "esoda-test-ss-service2-secrets-group" + description = "Secrets group 2 for secrets used by the ESO" + }] + }, + "secrets-store-2" = { + namespace = "eso-namespace-ss2" + create_namespace = true + existing_serviceid_id = null + serviceid_name = "esoda-test-secrets-store-2-serviceid" + serviceid_description = "esoda-test-secrets-store-2-serviceid description" + existing_account_secrets_group_id = "" + account_secrets_group_name = "esoda-test-ss-account-secrets-group-2" + account_secrets_group_description = "esoda-test-ss-account-secrets-group-2 description" + trusted_profile_name = null + trusted_profile_description = null + existing_service_secrets_group_id_list = [] + service_secrets_groups_list = [{ + name = "esoda-test-ss-service3-secrets-group" + description = "Secrets group 3 for secrets used by the ESO" + },{ + name = "esoda-test-ss-service4-secrets-group" + description = "Secrets group 4 for secrets used by the ESO" + }] + } + } +} +``` diff --git a/solutions/fully-configurable/README.md b/solutions/fully-configurable/README.md new file mode 100644 index 00000000..7f7f8e6f --- /dev/null +++ b/solutions/fully-configurable/README.md @@ -0,0 +1,3 @@ +# Cloud automation for External Secrets Operator (Fully configurable) + +:exclamation: **Important:** This solution is not intended to be called by other modules because it contains a provider configuration and is not compatible with the `for_each`, `count`, and `depends_on` arguments. For more information, see [Providers Within Modules](https://developer.hashicorp.com/terraform/language/modules/develop/providers). diff --git a/solutions/fully-configurable/catalogValidationValues.json.template b/solutions/fully-configurable/catalogValidationValues.json.template new file mode 100644 index 00000000..f48a7e33 --- /dev/null +++ b/solutions/fully-configurable/catalogValidationValues.json.template @@ -0,0 +1,3 @@ +{ + "ibmcloud_api_key": $VALIDATION_APIKEY +} diff --git a/solutions/fully-configurable/example-secrets-configuration.md b/solutions/fully-configurable/example-secrets-configuration.md new file mode 100644 index 00000000..1f8e337c --- /dev/null +++ b/solutions/fully-configurable/example-secrets-configuration.md @@ -0,0 +1,61 @@ +## Example of leveraging the deployment of External Secrets Operator using this DA, the secrets configuration on IBM Cloud Secrets Manager and their binding on Cluster Secret Store on a cluster + +The code below is an example of generating a username/password secret on Secrets Manager to deploy a dockerjson cluster secret for each Cluster Secrets Store: + +``` +################################################################## +# creation of generic username/password secret +# (for example to store artifactory username and API key) +################################################################## + +locals { + # secret value for sm_userpass_secret + userpass_apikey = sensitive("password-payload-example") +} + +# Create username_password secret and store in secret manager +module "sm_userpass_secret" { + for_each = local.cluster_secrets_stores_account_secrets_groups + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.7.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = each.value.secrets_group.secret_group_id + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${each.key}-usernamepassword-secret" # checkov:skip=CKV_SECRET_6 + secret_description = "example secret for ${each.value.name}" #tfsec:ignore:general-secrets-no-plaintext-exposure # checkov:skip=CKV_SECRET_6 + secret_payload_password = local.userpass_apikey # pragma: allowlist secret + secret_type = "username_password" #checkov:skip=CKV_SECRET_6 + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_username = "artifactory-user" # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_auto_rotation = false + secret_auto_rotation_interval = 0 + secret_auto_rotation_unit = null + providers = { + ibm = ibm.ibm-sm + } +} + +################################################################## +# ESO externalsecrets with cluster scope and apikey authentication +################################################################## + +# ESO externalsecret with cluster scope creating a dockerconfigjson type secret +module "external_secret_usr_pass" { + for_each = local.cluster_secrets_store_account_serviceid_apikey_secrets + depends_on = [module.eso_clustersecretsstore] + source = "../../modules/eso-external-secret" + es_kubernetes_secret_type = "dockerconfigjson" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "username_password" #checkov:skip=CKV_SECRET_6 + sm_secret_id = each.value.secrets_manager_secret.secret_id + es_kubernetes_namespace = var.eso_secretsstores_configuration.cluster_secrets_stores[each.key].namespace + eso_store_name = each.key + es_container_registry = "example-registry-local.artifactory.com" + es_kubernetes_secret_name = "dockerconfigjson-uc" #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "es-docker-uc" +} + +output "sm_userpass_secret" { + value = module.sm_userpass_secret +} +``` diff --git a/solutions/fully-configurable/main.tf b/solutions/fully-configurable/main.tf new file mode 100644 index 00000000..946230ae --- /dev/null +++ b/solutions/fully-configurable/main.tf @@ -0,0 +1,648 @@ +locals { + prefix = var.prefix != null ? (var.prefix != "" ? var.prefix : null) : null +} + +# parsing cluster crn to collect the cluster ID and the region it is deployed into +module "crn_parser_cluster" { + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.1.0" + crn = var.existing_cluster_crn +} + +# parsing secrets manager crn to collect the secrets manager ID and its region +module "crn_parser_sm" { + source = "terraform-ibm-modules/common-utilities/ibm//modules/crn-parser" + version = "1.1.0" + crn = var.existing_secrets_manager_crn +} + +locals { + cluster_id = module.crn_parser_cluster.service_instance + cluster_region = module.crn_parser_cluster.region + sm_region = module.crn_parser_sm.region + sm_guid = module.crn_parser_sm.service_instance + sm_ibmcloud_api_key = var.secrets_manager_ibmcloud_api_key == null ? var.ibmcloud_api_key : var.secrets_manager_ibmcloud_api_key +} + +data "ibm_container_cluster_config" "cluster_config" { + cluster_name_id = local.cluster_id +} + +################################################################## +# ESO deployment configuration +# Configures ESO and reloader deployments +################################################################## + +locals { + # converting list of strings to comma separated values as expected by the module + reloader_namespaces_to_ignore = length(var.reloader_namespaces_to_ignore) != 0 ? join(", ", var.reloader_namespaces_to_ignore) : null + reloader_resources_to_ignore = length(var.reloader_resources_to_ignore) != 0 ? join(", ", var.reloader_resources_to_ignore) : null + reloader_namespaces_selector = length(var.reloader_namespaces_selector) != 0 ? join(", ", var.reloader_namespaces_selector) : null + reloader_resource_label_selector = length(var.reloader_resource_label_selector) != 0 ? join(", ", var.reloader_resource_label_selector) : null +} + +module "external_secrets_operator" { + source = "../../" + eso_namespace = var.eso_namespace + existing_eso_namespace = var.existing_eso_namespace + eso_enroll_in_servicemesh = var.eso_enroll_in_servicemesh + # ESO configuration + eso_cluster_nodes_configuration = var.eso_cluster_nodes_configuration + eso_pod_configuration = var.eso_pod_configuration + eso_image = var.eso_image + eso_image_version = var.eso_image_version + eso_chart_location = var.eso_chart_location + eso_chart_version = var.eso_chart_version + # reloader configuration + reloader_deployed = var.reloader_deployed + reloader_reload_strategy = var.reloader_reload_strategy + reloader_namespaces_to_ignore = local.reloader_namespaces_to_ignore + reloader_resources_to_ignore = local.reloader_resources_to_ignore + reloader_namespaces_selector = local.reloader_namespaces_selector + reloader_resource_label_selector = local.reloader_resource_label_selector + reloader_ignore_secrets = var.reloader_ignore_secrets + reloader_ignore_configmaps = var.reloader_ignore_configmaps + reloader_is_openshift = var.reloader_is_openshift + reloader_is_argo_rollouts = var.reloader_is_argo_rollouts + reloader_reload_on_create = var.reloader_reload_on_create + reloader_sync_after_restart = var.reloader_sync_after_restart + reloader_pod_monitor_metrics = var.reloader_pod_monitor_metrics + reloader_log_format = var.reloader_log_format + reloader_custom_values = var.reloader_custom_values + reloader_image = var.reloader_image + reloader_image_version = var.reloader_image_version + reloader_chart_location = var.reloader_chart_location + reloader_chart_version = var.reloader_chart_version +} + +################################################################## +# ESO Cluster secrets stores management +################################################################## + +# for each element of cluster_secrets_stores going to create +# 1. service secrets groups (the secrets groups to contain the secrets read by the ESO) to create if any +# 2. account secrets group (the secrets group to store the secrets used by the ESO to connect to the secrets manager and pull the secrets values) to create if any +# 3. the trusted profile to create if any +# 4. the service id to read the secrets from the secrets manager if any + +locals { + + # list of service secrets groups to create for each cluster secrets store - each element of the map has a key with the name of the clustersecretsstore contatenated to the secrets group name (using "." as separator) to keep the keys unique + # flatten ensures that this local value is a flat list of objects, rather than a list of lists of objects + cluster_secrets_stores_service_secrets_groups_list = flatten([ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : [ + for service_secrets_group_key, service_secrets_group in cluster_secrets_store.service_secrets_groups_list : { + key = "${cluster_secrets_store_key}.${service_secrets_group.name}" + name = try("${local.prefix}-${service_secrets_group.name}", service_secrets_group.name) + description = service_secrets_group.description + } + ] + ]) + +} + +# service secrets groups for the cluster secrets stores +module "cluster_secrets_stores_service_secrets_groups" { + for_each = tomap({ + for idx, element in local.cluster_secrets_stores_service_secrets_groups_list : element.key => element + }) + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.3.4" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = each.value.name # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = each.value.description #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +locals { + # map of cluster secrets stores service secrets groups enriched with the created secrets groups details + cluster_secrets_stores_service_secrets_groups = { + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => [ + for service_secrets_group_key, service_secrets_group in cluster_secrets_store.service_secrets_groups_list : { + key = "${cluster_secrets_store_key}.${service_secrets_group.name}" + name = try("${local.prefix}-${service_secrets_group.name}", service_secrets_group.name) + description = service_secrets_group.description + secrets_group = module.cluster_secrets_stores_service_secrets_groups["${cluster_secrets_store_key}.${service_secrets_group.name}"] + } + ] + } +} + +# trusted profile authentication for the cluster secrets stores +locals { + # putting together the service secrets groups IDs to use for each cluster secrets store with the trusted profile to read them + cluster_secrets_stores_trusted_profile_to_create = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "trusted_profile_name" : try("${local.prefix}-${cluster_secrets_store.trusted_profile_name}", cluster_secrets_store.trusted_profile_name) + "trusted_profile_description" : cluster_secrets_store.trusted_profile_description != null ? cluster_secrets_store.trusted_profile_description : "Trusted profile for the secrets store ${cluster_secrets_store_key}" + "trusted_profile_service_secrets_groups_IDs" : local.cluster_secrets_stores_service_secrets_groups_fulllist[cluster_secrets_store_key] + } if(cluster_secrets_store.trusted_profile_name != null && cluster_secrets_store.trusted_profile_name != "") + }) +} + +# creating trusted profiles for the secrets groups created with module tp_clusterstore_secrets_manager_group +module "cluster_secrets_store_trusted_profile" { + for_each = local.cluster_secrets_stores_trusted_profile_to_create + source = "../../modules/eso-trusted-profile" + trusted_profile_name = each.value.trusted_profile_name + secrets_manager_guid = local.sm_guid + secret_groups_id = each.value.trusted_profile_service_secrets_groups_IDs + tp_cluster_crn = var.existing_cluster_crn + trusted_profile_claim_rule_type = "ROKS_SA" + tp_namespace = var.eso_namespace +} + +# account secrets groups for the cluster secrets stores +module "cluster_secrets_stores_account_secrets_groups" { + for_each = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "name" : try("${local.prefix}-${cluster_secrets_store.account_secrets_group_name}", cluster_secrets_store.account_secrets_group_name) + "description" : cluster_secrets_store.account_secrets_group_description + } if(cluster_secrets_store.existing_account_secrets_group_id == null || cluster_secrets_store.existing_account_secrets_group_id == "") && cluster_secrets_store.account_secrets_group_name != null + }) + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.3.4" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = each.value.name # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = each.value.description #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +locals { + # map of cluster secrets stores account secrets groups enriched with the created secrets groups details + cluster_secrets_stores_account_secrets_groups = { + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + name = try("${local.prefix}-${cluster_secrets_store.account_secrets_group_name}", cluster_secrets_store.account_secrets_group_name) + secrets_group = module.cluster_secrets_stores_account_secrets_groups[cluster_secrets_store_key] + + } + } +} + +# for each cluster secrets store creating the service id to pull secrets if existing service id is not provided +resource "ibm_iam_service_id" "cluster_secrets_stores_secret_puller" { + for_each = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "name" : try("${local.prefix}-${cluster_secrets_store.serviceid_name}", cluster_secrets_store.serviceid_name) + "description" : cluster_secrets_store.serviceid_description + } if(cluster_secrets_store.existing_serviceid_id == null || cluster_secrets_store.existing_serviceid_id == "") + }) + name = each.value.name + description = each.value.description +} + +locals { + # map of serviceIDs details owning the secrets to pull from Secrets Manager for each cluster secrets stores + cluster_secrets_stores_secret_puller_service_ids = { + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "name" : try("${local.prefix}-${cluster_secrets_store.serviceid_name}", cluster_secrets_store.serviceid_name) + "service_id" : ibm_iam_service_id.cluster_secrets_stores_secret_puller[cluster_secrets_store_key] + } if(cluster_secrets_store.existing_serviceid_id == null || cluster_secrets_store.existing_serviceid_id == "") + } +} + +# cluster secrets stores namespaces creation +module "cluster_secrets_store_namespace" { + for_each = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "namespace" : cluster_secrets_store.namespace + } if cluster_secrets_store.create_namespace == true + }) + source = "terraform-ibm-modules/namespace/ibm" + version = "1.0.3" + namespaces = [ + { + name = each.value.namespace + metadata = { + name = each.value.namespace + labels = {} + annotations = {} + } + } + ] +} + +locals { + # generating the list of service secrets groups ids to use with the cluster secrets store authentication configuration + + # putting together the service secrets groups IDs to use for each cluster secrets store + cluster_secrets_stores_service_secrets_groups_fulllist = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => concat( + cluster_secrets_store.existing_service_secrets_group_id_list, + [for service_secrets_group_key, service_secrets_group in cluster_secrets_store.service_secrets_groups_list : module.cluster_secrets_stores_service_secrets_groups["${cluster_secrets_store_key}.${service_secrets_group.name}"].secret_group_id] + ) + }) + + # putting together the service secrets groups IDs to use for each cluster secrets store with the account secrets group ID to read them + cluster_secrets_stores_policies_to_create = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + # if the existing_serviceid_id is null it collects the service id created otherwise will use the existing one + "accountServiceID" : (cluster_secrets_store.existing_serviceid_id == null || cluster_secrets_store.existing_serviceid_id == "") ? ibm_iam_service_id.cluster_secrets_stores_secret_puller[cluster_secrets_store_key].id : cluster_secrets_store.existing_serviceid_id + "service_secrets_groups_IDs" : local.cluster_secrets_stores_service_secrets_groups_fulllist[cluster_secrets_store_key] + } + }) + + # temporary step to create a final map to process to create the policies from the account secrets group ID to each of the service secrets groups IDs + cluster_secrets_stores_policies_to_create_temp = flatten([ + for cluster_secrets_store_key, cluster_store_element in local.cluster_secrets_stores_policies_to_create : [ + for index, service_secrets_group_id in cluster_store_element.service_secrets_groups_IDs : { + # creating the key value as the combination of the cluster secrets store key and the service secrets group ID to avoid duplicates in the next map + cluster_secrets_store_key = cluster_secrets_store_key # keeping this key needed during cluster secrets store creation + key = "${cluster_secrets_store_key}.csg${index}" + accountServiceID = cluster_store_element.accountServiceID + service_secrets_group_ID = service_secrets_group_id + } + ] + ]) + + # final flat map to process to create the policies using for_each, using as key of the map the combination of the cluster secrets store key and the service secrets group ID to avoid duplicates in the map + cluster_secrets_stores_policies_to_create_map = tomap({ + for idx, element in local.cluster_secrets_stores_policies_to_create_temp : element.key => element + }) +} + +# Create policy to allow new service id to pull secrets from secrets manager +resource "ibm_iam_service_policy" "cluster_secrets_store_secrets_puller_policy" { + for_each = local.cluster_secrets_stores_policies_to_create_map + iam_service_id = each.value.accountServiceID + roles = ["Viewer", "SecretsReader"] + resources { + service = "secrets-manager" + resource_instance_id = local.sm_guid + resource_type = "secret-group" + resource = each.value.service_secrets_group_ID + } +} + +# create for each Service ID the relative API key and add it to secret manager +module "cluster_secrets_store_account_serviceid_apikey" { + for_each = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "accountServiceID" : (cluster_secrets_store.existing_serviceid_id == null || cluster_secrets_store.existing_serviceid_id == "") ? ibm_iam_service_id.cluster_secrets_stores_secret_puller[cluster_secrets_store_key].id : cluster_secrets_store.existing_serviceid_id + "secretGroupID" : cluster_secrets_store.existing_account_secrets_group_id != null && cluster_secrets_store.existing_account_secrets_group_id != "" ? cluster_secrets_store.existing_account_secrets_group_id : module.cluster_secrets_stores_account_secrets_groups[cluster_secrets_store_key].secret_group_id + } + }) + source = "terraform-ibm-modules/iam-serviceid-apikey-secrets-manager/ibm" + version = "1.1.1" + region = local.sm_region + #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_iam_secret_name = try("${local.prefix}-${each.key}-${each.value.accountServiceID}-apikey", "${each.key}-${each.value.accountServiceID}-apikey") + sm_iam_secret_description = "API key for serviceID ${each.value.accountServiceID}" #tfsec:ignore:general-secrets-no-plaintext-exposure + serviceid_id = each.value.accountServiceID + secrets_manager_guid = local.sm_guid + secret_group_id = each.value.secretGroupID + providers = { + ibm = ibm.ibm-sm + } +} + +# data source to get the API key to pull secrets from secrets manager +data "ibm_sm_iam_credentials_secret" "cluster_secrets_store_account_serviceid_apikey" { + # for_each = local.cluster_secrets_stores_policies_to_create_map + for_each = var.eso_secretsstores_configuration.cluster_secrets_stores + instance_id = local.sm_guid + #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static type + secret_id = module.cluster_secrets_store_account_serviceid_apikey[each.key].secret_id + provider = ibm.ibm-sm +} + +locals { + # map of account ServiceIDs enriched with the created secrets manager secret details + cluster_secrets_store_account_serviceid_apikey_secrets = { + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "account_service_id" : (cluster_secrets_store.existing_serviceid_id == null || cluster_secrets_store.existing_serviceid_id == "") ? ibm_iam_service_id.cluster_secrets_stores_secret_puller[cluster_secrets_store_key].id : cluster_secrets_store.existing_serviceid_id + "secrets_group_id" : cluster_secrets_store.existing_account_secrets_group_id != null && cluster_secrets_store.existing_account_secrets_group_id != "" ? cluster_secrets_store.existing_account_secrets_group_id : module.cluster_secrets_stores_account_secrets_groups[cluster_secrets_store_key].secret_group_id + "secrets_manager_secret" : module.cluster_secrets_store_account_serviceid_apikey[cluster_secrets_store_key] + } + } +} + +################################################################## +# ESO Secrets stores management +################################################################## + +# for each element of secrets_stores going to create +# 1. service secrets groups (the secrets groups to contain the secrets read by the ESO) to create if any +# 2. account secrets group (the secrets group to store the secrets used by the ESO to connect to the secrets manager and pull the secrets values) to create if any +# 3. the trusted profile to create if any +# 4. the service id to read the secrets from the secrets manager if any + +locals { + # list of service secrets groups to create for each secrets store - each element of the map has a key with the name of the secretsstore contatenated to the secrets group name (using "." as separator) to keep the keys unique + # flatten ensures that this local value is a flat list of objects, rather than a list of lists of objects + + secrets_stores_service_secrets_groups_list = flatten([ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : [ + for service_secrets_group_key, service_secrets_group in secrets_store.service_secrets_groups_list : { + key = "${secrets_store_key}.${service_secrets_group.name}" + name = try("${local.prefix}-${service_secrets_group.name}", service_secrets_group.name) + description = service_secrets_group.description + } + ] + ]) + +} + +# service secrets groups for the secrets stores +module "secrets_stores_service_secrets_groups" { + for_each = tomap({ + for idx, element in local.secrets_stores_service_secrets_groups_list : element.key => element + }) + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.3.4" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = each.value.name # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = each.value.description #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +locals { + # map of service secrets groups details for each secrets store + secrets_stores_service_secrets_groups = { + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => [ + for service_secrets_group_key, service_secrets_group in secrets_store.service_secrets_groups_list : { + key = "${secrets_store_key}.${service_secrets_group.name}" + name = try("${local.prefix}-${service_secrets_group.name}", service_secrets_group.name) + description = service_secrets_group.description + secrets_group = module.secrets_stores_service_secrets_groups["${secrets_store_key}.${service_secrets_group.name}"] + } + ] + } +} + +# trusted profile authentication for secrets stores +locals { + # putting together the service secrets groups IDs to use for each secrets store with the trusted profile to read them + secrets_stores_trusted_profile_to_create = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "trusted_profile_name" : try("${local.prefix}-${secrets_store.trusted_profile_name}", secrets_store.trusted_profile_name) + "trusted_profile_description" : secrets_store.trusted_profile_description != null ? secrets_store.trusted_profile_description : "Trusted profile for the secrets store ${secrets_store_key}" + "trusted_profile_service_secrets_groups_IDs" : local.secrets_stores_service_secrets_groups_fulllist[secrets_store_key] + } if(secrets_store.trusted_profile_name != null && secrets_store.trusted_profile_name != "") + }) +} + +# creating trusted profiles for the secrets groups created with module tp_secrets_manager_groups +module "secrets_stores_trusted_profiles" { + for_each = local.secrets_stores_trusted_profile_to_create + source = "../../modules/eso-trusted-profile" + trusted_profile_name = each.value.trusted_profile_name + secrets_manager_guid = local.sm_guid + secret_groups_id = each.value.trusted_profile_service_secrets_groups_IDs + tp_cluster_crn = var.existing_cluster_crn + trusted_profile_claim_rule_type = "ROKS_SA" + tp_namespace = var.eso_namespace +} + +# account secrets group for the secrets stores +module "secrets_stores_account_secrets_groups" { + for_each = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "name" : try("${local.prefix}-${secrets_store.account_secrets_group_name}", secrets_store.account_secrets_group_name) + "description" : secrets_store.account_secrets_group_description + } if(secrets_store.existing_account_secrets_group_id == null || secrets_store.existing_account_secrets_group_id == "") && secrets_store.account_secrets_group_name != null + }) + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.3.4" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = each.value.name # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = each.value.description #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +locals { + + # map of account secrets groups details for each secrets stores + secrets_stores_account_secrets_groups = { + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + name = try("${local.prefix}-${secrets_store.account_secrets_group_name}", secrets_store.account_secrets_group_name) + secrets_group = module.secrets_stores_account_secrets_groups[secrets_store_key] + } + } +} + +# for each secrets store creating the service id to pull secrets if existing service id is not provided +resource "ibm_iam_service_id" "secrets_stores_secret_puller" { + for_each = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "name" : try("${local.prefix}-${secrets_store.serviceid_name}", secrets_store.serviceid_name) + "description" : secrets_store.serviceid_description + } if(secrets_store.existing_serviceid_id == null || secrets_store.existing_serviceid_id == "") + }) + name = each.value.name + description = each.value.description +} + +locals { + # map of serviceID details for pulling secrets from Secrets Manager for each secrets stores + secrets_stores_secret_puller_service_ids = { + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "name" : try("${local.prefix}-${secrets_store.serviceid_name}", secrets_store.serviceid_name) + "service_id" : ibm_iam_service_id.secrets_stores_secret_puller[secrets_store_key] + } if(secrets_store.existing_serviceid_id == null || secrets_store.existing_serviceid_id == "") + } +} + +# cluster secrets stores namespaces creation +module "secrets_store_namespace" { + for_each = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "namespace" : secrets_store.namespace + } if secrets_store.create_namespace == true + }) + source = "terraform-ibm-modules/namespace/ibm" + version = "1.0.3" + namespaces = [ + { + name = each.value.namespace + metadata = { + name = each.value.namespace + labels = {} + annotations = {} + } + } + ] +} + +locals { + # generating the list of service secrets groups ids to use with the secrets store authentication configuration + + # putting together the service secrets groups IDs to use for each secrets store + secrets_stores_service_secrets_groups_fulllist = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => concat( + secrets_store.existing_service_secrets_group_id_list, + [for service_secrets_group_key, service_secrets_group in secrets_store.service_secrets_groups_list : module.secrets_stores_service_secrets_groups["${secrets_store_key}.${service_secrets_group.name}"].secret_group_id] + ) + }) + + # putting together the service secrets groups IDs to use for each secrets store with the account secrets group ID to read them + secrets_stores_policies_to_create = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + # if the existing_serviceid_id is null it collects the service id created otherwise will use the existing one + "accountServiceID" : (secrets_store.existing_serviceid_id == null || secrets_store.existing_serviceid_id == "") ? ibm_iam_service_id.secrets_stores_secret_puller[secrets_store_key].id : secrets_store.existing_serviceid_id + "service_secrets_groups_IDs" : local.secrets_stores_service_secrets_groups_fulllist[secrets_store_key] + } + }) + + # temporary step to create a final map to process to create the policies from the account secrets group ID to each of the service secrets groups IDs + secrets_stores_policies_to_create_temp = flatten([ + for secrets_store_key, store_element in local.secrets_stores_policies_to_create : [ + for index, service_secrets_group_id in store_element.service_secrets_groups_IDs : { + # creating the key value as the combination of the secrets store key and the service secrets group ID to avoid duplicates in the next map + secrets_store_key = secrets_store_key # keeping this key needed during secrets store creation + key = "${secrets_store_key}.ssg${index}" + accountServiceID = store_element.accountServiceID + service_secrets_group_ID = service_secrets_group_id + } + ] + ]) + + # final flat map to process to create the policies using for_each, using as key of the map the combination of the secrets store key and the service secrets group ID to avoid duplicates in the map + secrets_stores_policies_to_create_map = tomap({ + for idx, element in local.secrets_stores_policies_to_create_temp : element.key => element + }) +} + +# Create policy to allow new service id to pull secrets from secrets manager +resource "ibm_iam_service_policy" "secrets_store_secrets_puller_policy" { + for_each = local.secrets_stores_policies_to_create_map + iam_service_id = each.value.accountServiceID + roles = ["Viewer", "SecretsReader"] + resources { + service = "secrets-manager" + resource_instance_id = local.sm_guid + resource_type = "secret-group" + resource = each.value.service_secrets_group_ID + } +} + +# create for each Service ID the relative API key and add it to secret manager +module "secrets_store_account_serviceid_apikey" { + for_each = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "accountServiceID" : (secrets_store.existing_serviceid_id == null || secrets_store.existing_serviceid_id == "") ? ibm_iam_service_id.secrets_stores_secret_puller[secrets_store_key].id : secrets_store.existing_serviceid_id + "secretGroupID" : secrets_store.existing_account_secrets_group_id != null && secrets_store.existing_account_secrets_group_id != "" ? secrets_store.existing_account_secrets_group_id : module.secrets_stores_account_secrets_groups[secrets_store_key].secret_group_id + } + }) + source = "terraform-ibm-modules/iam-serviceid-apikey-secrets-manager/ibm" + version = "1.1.1" + region = local.sm_region + #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_iam_secret_name = try("${local.prefix}-${each.key}-${each.value.accountServiceID}-apikey", "${each.key}-${each.value.accountServiceID}-apikey") + sm_iam_secret_description = "API key for serviceID ${each.value.accountServiceID}" #tfsec:ignore:general-secrets-no-plaintext-exposure + serviceid_id = each.value.accountServiceID + secrets_manager_guid = local.sm_guid + secret_group_id = each.value.secretGroupID + providers = { + ibm = ibm.ibm-sm + } +} + +locals { + # map of Secrets Manager secrets details for pulling secrets from Secrets Manager for each secrets stores + secrets_store_account_serviceid_apikey_secrets = { + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "account_service_id" : (secrets_store.existing_serviceid_id == null || secrets_store.existing_serviceid_id == null) ? ibm_iam_service_id.secrets_stores_secret_puller[secrets_store_key].id : secrets_store.existing_serviceid_id + "secrets_group_id" : secrets_store.existing_account_secrets_group_id != null && secrets_store.existing_account_secrets_group_id != "" ? secrets_store.existing_account_secrets_group_id : module.secrets_stores_account_secrets_groups[secrets_store_key].secret_group_id + "secrets_manager_secret" : module.secrets_store_account_serviceid_apikey[secrets_store_key] + } + } +} + +# # data source to get the API key to pull secrets from secrets manager +data "ibm_sm_iam_credentials_secret" "secrets_store_account_serviceid_apikey" { + for_each = var.eso_secretsstores_configuration.secrets_stores + instance_id = local.sm_guid + #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static type + secret_id = module.secrets_store_account_serviceid_apikey[each.key].secret_id + provider = ibm.ibm-sm +} + +# creation of the ESO ClusterStore (cluster wide scope) + +module "eso_clustersecretsstore" { + for_each = tomap({ + for cluster_secrets_store_key, cluster_secrets_store in var.eso_secretsstores_configuration.cluster_secrets_stores : + cluster_secrets_store_key => { + "name" : cluster_secrets_store_key + "authentication" : cluster_secrets_store.trusted_profile_name != null && cluster_secrets_store.trusted_profile_name != "" ? "trusted_profile" : "api_key" + "secret_apikey" : data.ibm_sm_iam_credentials_secret.cluster_secrets_store_account_serviceid_apikey[cluster_secrets_store_key].api_key != null ? data.ibm_sm_iam_credentials_secret.cluster_secrets_store_account_serviceid_apikey[cluster_secrets_store_key].api_key : null + "trusted_profile_name" : cluster_secrets_store.trusted_profile_name != null && cluster_secrets_store.trusted_profile_name != "" ? try("${local.prefix}-${cluster_secrets_store.trusted_profile_name}", cluster_secrets_store.trusted_profile_name) : null + "namespace" : cluster_secrets_store.namespace + } + }) + source = "../../modules/eso-clusterstore" + eso_authentication = each.value.authentication + clusterstore_secret_apikey = each.value.secret_apikey + region = local.sm_region + clusterstore_helm_rls_name = "${each.value.name}-helmrelease" + clusterstore_secret_name = each.value.secret_apikey != null ? "${each.value.name}-auth-apikey" : null #checkov:skip=CKV_SECRET_6 + clusterstore_name = each.value.name + clusterstore_secrets_manager_guid = local.sm_guid + eso_namespace = each.value.namespace + service_endpoints = var.service_endpoints + clusterstore_trusted_profile_name = each.value.trusted_profile_name != null && each.value.trusted_profile_name != "" ? each.value.trusted_profile_name : null + depends_on = [ + module.external_secrets_operator + ] +} + +# creation of namespace scoped secrets store +module "eso_secretsstore" { + for_each = tomap({ + for secrets_store_key, secrets_store in var.eso_secretsstores_configuration.secrets_stores : + secrets_store_key => { + "name" : secrets_store_key + "authentication" : secrets_store.trusted_profile_name != null && secrets_store.trusted_profile_name != "" ? "trusted_profile" : "api_key" + "secret_apikey" : data.ibm_sm_iam_credentials_secret.secrets_store_account_serviceid_apikey[secrets_store_key].api_key != null ? data.ibm_sm_iam_credentials_secret.secrets_store_account_serviceid_apikey[secrets_store_key].api_key : null + "trusted_profile_name" : secrets_store.trusted_profile_name != null && secrets_store.trusted_profile_name != "" ? try("${local.prefix}-${secrets_store.trusted_profile_name}", secrets_store.trusted_profile_name) : null + "namespace" : secrets_store.namespace + } + }) + depends_on = [module.external_secrets_operator] + source = "../../modules/eso-secretstore" + eso_authentication = each.value.authentication + region = local.sm_region + sstore_namespace = each.value.namespace + sstore_secrets_manager_guid = local.sm_guid + sstore_store_name = each.value.name + sstore_secret_apikey = each.value.secret_apikey + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "${each.value.name}-helmrelease" + sstore_trusted_profile_name = each.value.trusted_profile_name != null && each.value.trusted_profile_name != "" ? each.value.trusted_profile_name : null + sstore_secret_name = each.value.secret_apikey != null ? "${each.value.name}-auth-apikey" : null #checkov:skip=CKV_SECRET_6 +} diff --git a/solutions/fully-configurable/outputs.tf b/solutions/fully-configurable/outputs.tf new file mode 100644 index 00000000..336b659c --- /dev/null +++ b/solutions/fully-configurable/outputs.tf @@ -0,0 +1,43 @@ +# cluster secrets store created resources + +output "cluster_secrets_stores_service_secrets_groups" { + description = "Secrets groups created for each cluster secrets store to store the secrets managed through by ESO" + value = local.cluster_secrets_stores_service_secrets_groups +} + +output "cluster_secrets_stores_account_secrets_groups" { + description = "Secrets groups created for each cluster secrets store and used by this to managed the store secrets" + value = local.cluster_secrets_stores_account_secrets_groups +} + +output "cluster_secrets_stores_secret_puller_service_ids" { + description = "ServiceIDs created for each cluster secrets store to pull secrets from Secrets Manager" + value = local.cluster_secrets_stores_secret_puller_service_ids +} + +output "cluster_secrets_store_account_serviceid_apikey_secrets" { + description = "Secrets Manager secret created for each cluster secrets store and the related serviceID for the API key to pull secrets from Secrets Manager" + value = local.cluster_secrets_store_account_serviceid_apikey_secrets +} + +# secrets store created resources + +output "secrets_stores_service_secrets_groups" { + description = "Secrets groups created for each secrets store to store the secrets managed through by ESO" + value = local.secrets_stores_service_secrets_groups +} + +output "secrets_stores_account_secrets_groups" { + description = "Secrets groups created for each secrets store and used by this to managed the store secrets" + value = local.secrets_stores_account_secrets_groups +} + +output "secrets_stores_secret_puller_service_ids" { + description = "ServiceIDs created for each secrets store to pull secrets from Secrets Manager" + value = local.secrets_stores_secret_puller_service_ids +} + +output "secrets_store_account_serviceid_apikey_secrets" { + description = "Secrets Manager secret created for each secrets store and the related serviceID for the API key to pull secrets from Secrets Manager" + value = local.secrets_store_account_serviceid_apikey_secrets +} diff --git a/solutions/fully-configurable/provider.tf b/solutions/fully-configurable/provider.tf new file mode 100644 index 00000000..0033f25b --- /dev/null +++ b/solutions/fully-configurable/provider.tf @@ -0,0 +1,28 @@ +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + visibility = var.provider_visibility + region = local.cluster_region + private_endpoint_type = (var.provider_visibility == "private" && local.cluster_region == "ca-mon") ? "vpe" : null +} + +provider "ibm" { + ibmcloud_api_key = local.sm_ibmcloud_api_key + visibility = var.provider_visibility + region = local.sm_region + alias = "ibm-sm" + private_endpoint_type = (var.provider_visibility == "private" && local.sm_region == "ca-mon") ? "vpe" : null +} + +provider "kubernetes" { + host = data.ibm_container_cluster_config.cluster_config.host + token = data.ibm_container_cluster_config.cluster_config.token + cluster_ca_certificate = data.ibm_container_cluster_config.cluster_config.ca_certificate +} + +provider "helm" { + kubernetes { + host = data.ibm_container_cluster_config.cluster_config.host + token = data.ibm_container_cluster_config.cluster_config.token + cluster_ca_certificate = data.ibm_container_cluster_config.cluster_config.ca_certificate + } +} diff --git a/solutions/fully-configurable/variables.tf b/solutions/fully-configurable/variables.tf new file mode 100644 index 00000000..42189ca5 --- /dev/null +++ b/solutions/fully-configurable/variables.tf @@ -0,0 +1,362 @@ +####################################################################### +# Generic +####################################################################### + +variable "ibmcloud_api_key" { + type = string + description = "IBM Cloud API Key" + sensitive = true +} + +variable "secrets_manager_ibmcloud_api_key" { + type = string + description = "API key to authenticate on Secrets Manager instance. If null the ibmcloud_api_key will be used." + default = null +} + +variable "provider_visibility" { + description = "Set the visibility value for the IBM terraform provider. Supported values are `public`, `private`, `public-and-private`. [Learn more](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/guides/custom-service-endpoints)." + type = string + default = "private" + + validation { + condition = contains(["public", "private", "public-and-private"], var.provider_visibility) + error_message = "Invalid visibility option. Allowed values are 'public', 'private', or 'public-and-private'." + } +} + +variable "prefix" { + type = string + nullable = true + description = "The prefix to be added to all resources created by this solution. To skip using a prefix, set this value to null or an empty string. The prefix must begin with a lowercase letter and may contain only lowercase letters, digits, and hyphens '-'. It should not exceed 16 characters, must not end with a hyphen('-'), and can not contain consecutive hyphens ('--'). Example: prod-0205. [Learn more](https://terraform-ibm-modules.github.io/documentation/#/prefix.md)." + + validation { + # - null and empty string is allowed + # - Must not contain consecutive hyphens (--): length(regexall("--", var.prefix)) == 0 + # - Starts with a lowercase letter: [a-z] + # - Contains only lowercase letters (a–z), digits (0–9), and hyphens (-) + # - Must not end with a hyphen (-): [a-z0-9] + condition = (var.prefix == null || var.prefix == "" ? true : + alltrue([ + can(regex("^[a-z][-a-z0-9]*[a-z0-9]$", var.prefix)), + length(regexall("--", var.prefix)) == 0 + ]) + ) + error_message = "Prefix must begin with a lowercase letter and may contain only lowercase letters, digits, and hyphens '-'. It must not end with a hyphen('-'), and cannot contain consecutive hyphens ('--')." + } + + validation { + # must not exceed 16 characters in length + condition = length(var.prefix) <= 16 + error_message = "Prefix must not exceed 16 characters." + } +} + +variable "existing_cluster_crn" { + type = string + description = "The CRN of the to deploy ESO operator onto. This value cannot be null." + nullable = false +} + +variable "existing_secrets_manager_crn" { + type = string + description = "The CRN of the existing Secrets Manager instance to use. This value cannot be null." + nullable = false +} + +############################################################################################################ +# ESO DEPLOYMENT CONFIGURATION +############################################################################################################ + +variable "eso_namespace" { + type = string + description = "Cluster namespace to create and to deploy the External secrets Operator and Reloader into." + default = "es-operator" + validation { + condition = var.eso_namespace == null || can(regex("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", var.eso_namespace)) + error_message = "The value of the eso_namespace must be a valid Kubernetes namespace name" + } +} + +variable "existing_eso_namespace" { + type = string + description = "Existing cluster namespace to deploy the External secrets Operator and Reloader into. If eso_namespace is not null, this value will be ignored." + default = null + validation { + condition = var.existing_eso_namespace == null || can(regex("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", var.existing_eso_namespace)) + error_message = "The value of the existing_eso_namespace must be a valid Kubernetes namespace name." + } + validation { + condition = var.existing_eso_namespace == null && var.eso_namespace == null ? false : true + error_message = "The values of var.existing_eso_namespace and var.eso_namespace cannot be null at the same time." + } +} + +variable "eso_cluster_nodes_configuration" { + description = "Configuration to use to customise ESO deployment on specific cluster nodes. Default value is null to keep ESO standard deployment. Learn more [here](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator#customise-eso-deployment-on-specific-cluster-nodes)" + type = object({ + nodeSelector = object({ + label = string + value = string + }) + tolerations = object({ + key = string + operator = string + value = string + effect = string + }) + }) + default = null +} + +# ESO deployment cluster pods configuration +variable "eso_pod_configuration" { + description = "Configuration to use to customise ESO deployment on specific pods. Default value is {} to keep ESO standard deployment. Ignore if not needed." + type = object({ + annotations = optional(object({ + # The annotations for external secret controller pods. + external_secrets = optional(map(string), {}) + # The annotations for external secret cert controller pods. + external_secrets_cert_controller = optional(map(string), {}) + # The annotations for external secret controller pods. + external_secrets_webhook = optional(map(string), {}) + }), {}) + + labels = optional(object({ + # The labels for external secret controller pods. + external_secrets = optional(map(string), {}) + # The labels for external secret cert controller pods. + external_secrets_cert_controller = optional(map(string), {}) + # The labels for external secret controller pods. + external_secrets_webhook = optional(map(string), {}) + }), {}) + }) + default = {} +} + +# external secrets operator image and helm charts references +variable "eso_image" { + type = string + description = "The External Secrets Operator image in the format of `[registry-url]/[namespace]/[image]`." + default = "ghcr.io/external-secrets/external-secrets" + nullable = false +} + +variable "eso_image_version" { + type = string + description = "The version or digest for the external secrets image to deploy. If changing the value, ensure it is compatible with the chart version set in eso_chart_version." + default = "v0.16.1-ubi@sha256:329ecbb3b0f1e70d9fa81a6de403325eae39ef95f89b1633cc3bc627ba3204b5" # datasource: ghcr.io/external-secrets/external-secrets + nullable = false + validation { + condition = can(regex("(^v\\d+\\.\\d+.\\d+(\\-\\w+)?(\\@sha256\\:\\w+){0,1})$", var.eso_image_version)) + error_message = "The value of the external secrets image version must match classic version or the tag and sha256 image digest format" + } +} + +variable "eso_chart_location" { + type = string + description = "The location of the External Secrets Operator Helm chart." + default = "https://charts.external-secrets.io" + nullable = false +} + +variable "eso_chart_version" { + type = string + description = "The version of the External Secrets Operator Helm chart. Ensure that the chart version is compatible with the image version specified in eso_image_version." + default = "0.16.1" # registryUrl: charts.external-secrets.io + nullable = false +} + +# ESO +variable "eso_enroll_in_servicemesh" { + description = "Flag to enroll the External Secrets Operator into RedHat Service Mesh adding the istio-injection annotation to the ESO namespace and to ESO pods. Default to false." + type = bool + default = false +} + +############################################################################################################ +# RELOADER DEPLOYMENT CONFIGURATION +############################################################################################################ + +variable "reloader_deployed" { + description = "Flag to enable the deployment of [reloader](https://github.com/stakater/Reloader) along with ESO. Learn more [here](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator#pod-reloader)" + type = bool + default = true +} + +variable "reloader_reload_strategy" { + description = "The reload strategy to use for reloader. Possible values are `env-vars` or `annotations`. Default value is `annotations`" + type = string + default = "annotations" + validation { + condition = contains(["env-vars", "annotations"], var.reloader_reload_strategy) + error_message = "The specified reloader_reload_strategy is not a valid selection! Valid values are `env-vars` or `annotations`" + } +} + +variable "reloader_namespaces_to_ignore" { + description = "List of comma separated namespaces to ignore for reloader. If multiple are provided they are combined with the AND operator" + type = list(string) + default = [] +} + +variable "reloader_resources_to_ignore" { + description = "List of comma separated resources to ignore for reloader. If multiple are provided they are combined with the AND operator" + type = list(string) + default = [] +} + +variable "reloader_namespaces_selector" { + description = "List of comma separated label selectors, if multiple are provided they are combined with the AND operator" + type = list(string) + default = [] +} + +variable "reloader_resource_label_selector" { + description = "List of comma separated label selectors, if multiple are provided they are combined with the AND operator" + type = list(string) + default = [] +} + +variable "reloader_ignore_secrets" { + description = "Whether to ignore secret changes or not" + type = bool + default = false +} + +variable "reloader_ignore_configmaps" { + description = "Whether to ignore configmap changes or not" + type = bool + default = false +} + +variable "reloader_is_openshift" { + description = "Enable OpenShift DeploymentConfigs" + type = bool + default = true +} + +variable "reloader_is_argo_rollouts" { + description = "Enable Argo Rollouts" + type = bool + default = false +} + +variable "reloader_reload_on_create" { + description = "Enable reload on create events" + type = bool + default = true + +} +variable "reloader_sync_after_restart" { + description = "Enable sync after Reloader restarts for Add events, works only when reloadOnCreate is true" + type = bool + default = true +} + +variable "reloader_pod_monitor_metrics" { + description = "Enable to scrape Reloader's Prometheus metrics" + type = bool + default = false +} + +variable "reloader_log_format" { + description = "The log format to use for reloader. Possible values are `json` or `text`. Default value is `json`" + type = string + default = "text" + validation { + condition = contains(["json", "text"], var.reloader_log_format) + error_message = "The specified reloader_log_format is not a valid selection! Valid values are `json` or `text`" + } +} + +variable "reloader_custom_values" { + description = "String containing custom values to be used for reloader helm chart. More details [here](https://github.com/stakater/Reloader/blob/master/deployments/kubernetes/chart/reloader/values.yaml)" + type = string + default = null +} + +# reloader image and helm charts references +variable "reloader_image" { + type = string + description = "The reloader image repository in the format of `[registry-url]/[namespace]/[image]`." + default = "ghcr.io/stakater/reloader" + nullable = false +} + +variable "reloader_image_version" { + type = string + description = "The version or digest for the reloader image to deploy. If changing the value, ensure it is compatible with the chart version set in reloader_chart_version." + default = "v1.4.2-ubi@sha256:e80c47260ed11f44a3b81331486f218955c4e36e82aa121d3bfdbfcab28b2055" # datasource: ghcr.io/stakater/reloader + nullable = false + validation { + condition = can(regex("(^v\\d+\\.\\d+.\\d+(\\-\\w+)?(\\@sha256\\:\\w+){0,1})$", var.reloader_image_version)) + error_message = "The value of the reloader image version must match classic version or the tag and sha256 image digest format" + } +} + +variable "reloader_chart_location" { + type = string + description = "The location of the Reloader Helm chart." + default = "https://stakater.github.io/stakater-charts" + nullable = false +} + +variable "reloader_chart_version" { + type = string + description = "The version of the Reloader Helm chart. Ensure that the chart version is compatible with the image version specified in reloader_image_version." + default = "2.1.3" # registryUrl: stakater.github.io/stakater-charts + nullable = false +} + +# secrets stores configuration + +variable "eso_secretsstores_configuration" { + description = "Configuration of the [cluster secrets stores](https://external-secrets.io/latest/api/clustersecretstore/) and [secrets stores](https://external-secrets.io/latest/api/secretstore/) to create. Learn more about this configuration [here](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/blob/main/solutions/fully-configurable/DA-eso-configuration.md)" + type = object({ + cluster_secrets_stores = map(object({ + namespace = string + create_namespace = bool + existing_serviceid_id = optional(string, null) + serviceid_name = optional(string, null) + serviceid_description = optional(string, null) + existing_account_secrets_group_id = optional(string, null) + account_secrets_group_name = optional(string, null) + account_secrets_group_description = optional(string, null) + trusted_profile_name = optional(string, null) # if both the trusted_profile_name and the serviceid_name/existing_serviceid_id are set, the trusted_profile_name will be used + trusted_profile_description = optional(string, null) + existing_service_secrets_group_id_list = optional(list(string), []) + service_secrets_groups_list = optional(list(object({ + name = string + description = string + })), []) + })) + secrets_stores = map(object({ + create_namespace = bool + namespace = optional(string, null) + existing_serviceid_id = optional(string, null) + serviceid_name = optional(string, null) + serviceid_description = optional(string, null) + existing_account_secrets_group_id = optional(string, null) + account_secrets_group_name = optional(string, null) + account_secrets_group_description = optional(string, null) + trusted_profile_name = optional(string, null) # if both the trusted_profile_name and the serviceid_name/existing_serviceid_id are set, the trusted_profile_name will be used + trusted_profile_description = optional(string, null) + existing_service_secrets_group_id_list = optional(list(string), []) + service_secrets_groups_list = optional(list(object({ + name = string + description = string + })), []) + })) + }) + default = { + cluster_secrets_stores = {} + secrets_stores = {} + } +} + +variable "service_endpoints" { + type = string + description = "The service endpoint type to communicate with the provided secrets manager instance. Possible values are `public` or `private`. This also will set the iam endpoint for containerAuth when enabling Trusted Profile/CR based authentication." + default = "private" +} diff --git a/solutions/fully-configurable/version.tf b/solutions/fully-configurable/version.tf new file mode 100644 index 00000000..75b5592f --- /dev/null +++ b/solutions/fully-configurable/version.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.9.0" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.37.1" + } + helm = { + source = "hashicorp/helm" + version = "2.17.0" + } + ibm = { + source = "IBM-Cloud/ibm" + version = "1.79.2" + } + } +} diff --git a/tests/existing-resources/main.tf b/tests/existing-resources/main.tf new file mode 100644 index 00000000..d9969774 --- /dev/null +++ b/tests/existing-resources/main.tf @@ -0,0 +1,199 @@ +################################################################## +# Resource Group +################################################################## + +module "resource_group" { + source = "terraform-ibm-modules/resource-group/ibm" + version = "1.2.0" + # if an existing resource group is not set (null) create a new one using prefix + resource_group_name = var.resource_group == null ? "${var.prefix}-resource-group" : null + existing_resource_group_name = var.resource_group +} + +################################################################## +# Create VPC, public gateway and subnets +################################################################## + +locals { + + subnet_prefix = flatten([ + for k, v in module.zone_subnet_addrs : [ + for zone, cidr in v.network_cidr_blocks : { + cidr = cidr + label = k + zone = zone + zone_index = split("-", zone)[1] + } + ] + ]) + + + merged_subnets = [ + for subnet in module.subnets : + merge( + subnet, + { + label = lookup([for sk in local.subnet_prefix : sk if sk.cidr == subnet.subnet_ipv4_cidr][0], "label", "") + zone = lookup([for sk in local.subnet_prefix : sk if sk.cidr == subnet.subnet_ipv4_cidr][0], "zone", "") + } + ) + ] + + subnets = { + default = [for subnet in local.merged_subnets : { id = subnet.subnet_id, cidr_block = subnet.subnet_ipv4_cidr, zone = subnet.zone } if subnet.label == "default"], + } + + ocp_worker_pools = [ + { + subnet_prefix = "default" + pool_name = "default" + machine_type = "bx2.4x16" + workers_per_zone = 1 + labels = { "dedicated" : "default" } + operating_system = "RHEL_9_64" + } + ] + +} + + +resource "null_resource" "subnet_mappings" { + count = length(var.zones) + + triggers = { + name = "${var.region}-${var.zones[count.index]}" + new_bits = 2 + } +} + +module "zone_subnet_addrs" { + source = "git::https://github.com/hashicorp/terraform-cidr-subnets.git?ref=v1.0.0" + for_each = var.cidr_bases + + base_cidr_block = each.value + + networks = null_resource.subnet_mappings[*].triggers +} + +module "vpc" { + source = "terraform-ibm-modules/vpc/ibm" + version = "1.5.1" + vpc_name = "${var.prefix}-vpc" + resource_group_id = module.resource_group.resource_group_id + locations = [] + vpc_tags = var.resource_tags + subnet_name_prefix = "${var.prefix}-subnet" + default_network_acl_name = "${var.prefix}-nacl" + default_routing_table_name = "${var.prefix}-routing-table" + default_security_group_name = "${var.prefix}-sg" + create_gateway = true + public_gateway_name_prefix = "${var.prefix}-pw" + number_of_addresses = 16 + auto_assign_address_prefix = false +} + +module "subnet_prefix" { + source = "terraform-ibm-modules/vpc/ibm//modules/vpc-address-prefix" + version = "1.5.1" + count = length(local.subnet_prefix) + name = "${var.prefix}-z-${local.subnet_prefix[count.index].label}-${split("-", local.subnet_prefix[count.index].zone)[2]}" + location = local.subnet_prefix[count.index].zone + vpc_id = module.vpc.vpc.vpc_id + ip_range = local.subnet_prefix[count.index].cidr +} + + +module "subnets" { + depends_on = [module.subnet_prefix] + source = "terraform-ibm-modules/vpc/ibm//modules/subnet" + version = "1.5.1" + count = length(local.subnet_prefix) + location = local.subnet_prefix[count.index].zone + vpc_id = module.vpc.vpc.vpc_id + ip_range = local.subnet_prefix[count.index].cidr + name = "${var.prefix}-subnet-${local.subnet_prefix[count.index].label}-${split("-", local.subnet_prefix[count.index].zone)[2]}" + public_gateway = local.subnet_prefix[count.index].label == "default" ? module.public_gateways[split("-", local.subnet_prefix[count.index].zone)[2] - 1].public_gateway_id : null + subnet_access_control_list = module.network_acl.network_acl_id +} + +module "public_gateways" { + source = "terraform-ibm-modules/vpc/ibm//modules/public-gateway" + version = "1.5.1" + count = length(var.zones) + vpc_id = module.vpc.vpc.vpc_id + location = "${var.region}-${var.zones[count.index]}" + name = "${var.prefix}-vpc-gateway-${var.zones[count.index]}" + resource_group_id = module.resource_group.resource_group_id +} + +module "security_group" { + source = "terraform-ibm-modules/vpc/ibm//modules/security-group" + version = "1.5.1" + depends_on = [module.vpc] + create_security_group = false + resource_group_id = module.resource_group.resource_group_id + security_group = "${var.prefix}-sg" + security_group_rules = [ + { + name = "allow_all_inbound" + remote = "0.0.0.0/0" + direction = "inbound" + } + ] +} + +locals { + allow_subnet_cidr_inbound_rules = [ + for k, v in module.zone_subnet_addrs : + { + name = "allow-traffic-subnet-${k}-inbound" + action = "allow" + source = v.base_cidr_block + destination = "0.0.0.0/0" + direction = "inbound" + } + ] + allow_subnet_cidr_outbound_rules = [ + for k, v in module.zone_subnet_addrs : + { + name = "allow-traffic-subnet-${k}-outbound" + action = "allow" + source = "0.0.0.0/0" + destination = v.base_cidr_block + direction = "outbound" + } + ] + acl_rules = flatten( + [ + local.allow_subnet_cidr_inbound_rules, + local.allow_subnet_cidr_outbound_rules, + var.acl_rules_list + ] + ) +} + +module "network_acl" { + source = "terraform-ibm-modules/vpc/ibm//modules/network-acl" + version = "1.5.1" + name = "${var.prefix}-vpc-acl" + vpc_id = module.vpc.vpc.vpc_id + resource_group_id = module.resource_group.resource_group_id + rules = local.acl_rules +} + +# OCP CLUSTER creation +module "ocp_base" { + source = "terraform-ibm-modules/base-ocp-vpc/ibm" + version = "3.46.16" + cluster_name = "${var.prefix}-vpc" + resource_group_id = module.resource_group.resource_group_id + region = var.region + force_delete_storage = true + vpc_id = module.vpc.vpc.vpc_id + vpc_subnets = local.subnets + worker_pools = local.ocp_worker_pools + tags = [] + use_existing_cos = false + # outbound required by cluster proxy + disable_outbound_traffic_protection = true +} diff --git a/tests/existing-resources/outputs.tf b/tests/existing-resources/outputs.tf new file mode 100644 index 00000000..c227d49a --- /dev/null +++ b/tests/existing-resources/outputs.tf @@ -0,0 +1,13 @@ +############################################################################## +# Outputs +############################################################################## + +output "cluster_crn" { + description = "CRN of the cluster deployed" + value = module.ocp_base.cluster_crn +} + +output "prefix" { + value = var.prefix + description = "prefix" +} diff --git a/tests/existing-resources/provider.tf b/tests/existing-resources/provider.tf new file mode 100644 index 00000000..df45ef50 --- /dev/null +++ b/tests/existing-resources/provider.tf @@ -0,0 +1,4 @@ +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + region = var.region +} diff --git a/tests/existing-resources/variables.tf b/tests/existing-resources/variables.tf new file mode 100644 index 00000000..e9b30e86 --- /dev/null +++ b/tests/existing-resources/variables.tf @@ -0,0 +1,146 @@ +####################################################################### +# Generic +####################################################################### + +variable "prefix" { + description = "Prefix for name of all resource created by this example" + type = string + default = "eso-clusterfull-test" +} + +variable "region" { + type = string + description = "Region where resources will be created." + default = "us-south" +} + +variable "ibmcloud_api_key" { + type = string + description = "APIkey that's associated with the account to use, set via environment variable TF_VAR_ibmcloud_api_key or .tfvars file." + sensitive = true +} + +variable "resource_group" { + type = string + description = "An existing resource group name to use for this example, if unset a new resource group will be created" + default = null +} + +# tflint-ignore: terraform_unused_declarations +variable "resource_tags" { + type = list(string) + description = "Optional list of tags to be added to created resources" + default = [] +} + +variable "zones" { + description = "List of zones" + type = list(string) + default = ["1", "2", "3"] +} + +variable "cidr_bases" { + description = "A list of base CIDR blocks for each network zone" + type = map(string) + default = { + default = "192.168.32.0/20" + } +} + + +variable "acl_rules_list" { + description = "List of rules that are to be attached to the Network ACL" + type = list(object({ + name = string + action = string + source = string + destination = string + direction = string + icmp = optional(object({ + code = number + type = number + })) + tcp = optional(object({ + port_max = number + port_min = number + source_port_max = number + source_port_min = number + })) + udp = optional(object({ + port_max = number + port_min = number + source_port_max = number + source_port_min = number + })) + })) + default = [ + { + name = "iks-create-worker-nodes-inbound" + action = "allow" + source = "161.26.0.0/16" + destination = "0.0.0.0/0" + direction = "inbound" + }, + { + name = "iks-nodes-to-master-inbound" + action = "allow" + source = "166.8.0.0/14" + destination = "0.0.0.0/0" + direction = "inbound" + }, + { + name = "iks-create-worker-nodes-outbound" + action = "allow" + source = "0.0.0.0/0" + destination = "161.26.0.0/16" + direction = "outbound" + }, + { + name = "iks-worker-to-master-outbound" + action = "allow" + source = "0.0.0.0/0" + destination = "166.8.0.0/14" + direction = "outbound" + }, + { + name = "allow-all-https-inbound" + source = "0.0.0.0/0" + action = "allow" + destination = "0.0.0.0/0" + direction = "inbound" + tcp = { + source_port_min = 443 + source_port_max = 443 + port_min = 1 + port_max = 65535 + } + }, + { + name = "allow-all-https-outbound" + source = "0.0.0.0/0" + action = "allow" + destination = "0.0.0.0/0" + direction = "outbound" + tcp = { + source_port_min = 1 + source_port_max = 65535 + port_min = 443 + port_max = 443 + } + }, + { + name = "deny-all-outbound" + action = "deny" + source = "0.0.0.0/0" + destination = "0.0.0.0/0" + direction = "outbound" + }, + { + name = "deny-all-inbound" + action = "deny" + source = "0.0.0.0/0" + destination = "0.0.0.0/0" + direction = "inbound" + } + ] +} diff --git a/tests/existing-resources/version.tf b/tests/existing-resources/version.tf new file mode 100644 index 00000000..fc71646f --- /dev/null +++ b/tests/existing-resources/version.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.9.0" + required_providers { + ibm = { + source = "IBM-Cloud/ibm" + version = ">= 1.62.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.2.1, < 4.0.0" + } + } +} diff --git a/tests/pr_test.go b/tests/pr_test.go index e0991107..f66ef5fd 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -11,10 +11,13 @@ import ( "time" "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/gruntwork-io/terratest/modules/logger" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/cloudinfo" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/common" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/testhelper" + "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/testschematic" "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -23,6 +26,10 @@ const resourceGroup = "geretain-test-ext-secrets-sync" const defaultExampleTerraformDir = "examples/all-combined" const basicExampleTerraformDir = "examples/basic" +// schematics DA consts +const fullConfigSolutionDir = "solutions/fully-configurable" +const existingResourcesTerraformDir = "tests/existing-resources" + // Define a struct with fields that match the structure of the YAML data const yamlLocation = "../common-dev-assets/common-go-assets/common-permanent-resources.yaml" @@ -543,3 +550,240 @@ func extractDefaultValueFromFile(lines []string, variableName string) string { } return "" } + +// Schematics DA test + +func setupOptionsSchematics(t *testing.T, prefix string, dir string) *testhelper.TestOptions { + // Verify ibmcloud_api_key variable is set + checkVariable := "TF_VAR_ibmcloud_api_key" + val, present := os.LookupEnv(checkVariable) + require.True(t, present, checkVariable+" environment variable not set") + require.NotEqual(t, "", val, checkVariable+" environment variable is empty") + + logger.Log(t, "variable "+checkVariable+" correctly set") + + // Verify region variable is set, otherwise it computes it + region := "" + checkRegion := "TF_VAR_region" + valRegion, presentRegion := os.LookupEnv(checkRegion) + if presentRegion { + region = valRegion + } else { + // Programmatically determine region to use based on availability + region, _ = testhelper.GetBestVpcRegion(val, "../common-dev-assets/common-go-assets/cloudinfo-region-vpc-gen2-prefs.yaml", "eu-de") + } + + logger.Log(t, "Using region: ", region) + + options := testhelper.TestOptionsDefaultWithVars(&testhelper.TestOptions{ + Testing: t, + TerraformDir: dir, + Prefix: prefix, + IgnoreUpdates: testhelper.Exemptions{ // Ignore for consistency check + List: []string{}, + }, + Region: region, + ResourceGroup: resourceGroup, + }) + return options +} + +// sets up options for solutions through schematics +func setupSolutionSchematicOptions(t *testing.T, prefix string, dir string) *testschematic.TestSchematicOptions { + + logger.Log(t, "setupSolutionSchematicOptions - Using prefix: ", prefix) + + options := testschematic.TestSchematicOptionsDefault(&testschematic.TestSchematicOptions{ + Testing: t, + TarIncludePatterns: []string{ + "*.tf", + "chart/*.yaml", + "chart/raw/*.yaml", + "chart/raw/templates/*.yaml", + "chart/raw/templates/*.tpl", + "modules/eso-clusterstore/*.tf", + "modules/eso-secretstore/*.tf", + "modules/eso-trusted-profile/*.tf", + "modules/eso-external-secret/*.tf", + dir + "/*.tf", + }, + TemplateFolder: dir, + Tags: []string{"test-esoda-schematic"}, + Prefix: prefix, + DeleteWorkspaceOnFail: false, + WaitJobCompleteMinutes: 60, + }) + + return options +} + +// helper function to set up inputs for full config solution test, will help keep it consistent +// between normal and upgrade tests +func getFullConfigSolutionTestVariables(mainOptions *testschematic.TestSchematicOptions, existingOptions *testhelper.TestOptions) []testschematic.TestSchematicTerraformVar { + + eso_secretsstores_configuration := map[string]any{ + "cluster_secrets_stores": map[string]any{ + + "css-1": map[string]any{ + "namespace": "eso-namespace-cs1", + "create_namespace": true, + // "existing_serviceid_id": "", + "serviceid_name": "esoda-test-css-1-serviceid", + "serviceid_description": "esoda-test-css-1-serviceid description", + // "existing_account_secrets_group_id": "", + "account_secrets_group_name": "esoda-test-cs-accsg-1", + "account_secrets_group_description": "esoda-test-cs-accsg-1 description", + "trusted_profile_name": "", + "trusted_profile_description": "", + "existing_service_secrets_group_id_list": []string{}, + "service_secrets_groups_list": []map[string]any{ + { + "name": "esoda-test-cs-s1-sg", + "description": "Secrets group 1 for secrets used by the ESO", + }, + { + "name": "esoda-test-cs-s2-sg", + "description": "Secrets group 2 for secrets used by the ESO", + }, + }, + }, + "css-2": map[string]any{ + "namespace": "eso-namespace-cs2", + "create_namespace": true, + "existing_serviceid_id": "", + "serviceid_name": "esoda-test-css-3-serviceid", + "serviceid_description": "esoda-test-css-3-serviceid description", + "existing_account_secrets_group_id": "", + "account_secrets_group_name": "esoda-test-cs-accsg-3", + "account_secrets_group_description": "esoda-test-cs-accsg-3 description", + // "trusted_profile_name": "", + // "trusted_profile_description": "", + "existing_service_secrets_group_id_list": []string{}, + "service_secrets_groups_list": []map[string]any{ + { + "name": "esoda-test-cs-s3-sg", + "description": "Secrets group 3 for secrets used by the ESO", + }, + { + "name": "esoda-test-cs-s4-sg", + "description": "Secrets group 4 for secrets used by the ESO", + }, + }, + }, + }, + "secrets_stores": map[string]any{ + + "ss-1": map[string]any{ + "namespace": "eso-namespace-ss1", + "create_namespace": true, + "existing_serviceid_id": "", + "serviceid_name": "esoda-test-ss-1-serviceid", + "serviceid_description": "esoda-test-ss-1-serviceid description", + "existing_account_secrets_group_id": "", + "account_secrets_group_name": "esoda-test-ss-accsg-1", + "account_secrets_group_description": "esoda-test-ss-accsg-1 description", + // "trusted_profile_name": "", + // "trusted_profile_description": "", + "existing_service_secrets_group_id_list": []string{}, + "service_secrets_groups_list": []map[string]any{ + { + "name": "esoda-test-ss-s1-sg", + "description": "Secrets group 1 for secrets used by the ESO", + }, + { + "name": "esoda-test-ss-s2-sg", + "description": "Secrets group 2 for secrets used by the ESO", + }, + }, + }, + "ss-2": map[string]any{ + "namespace": "eso-namespace-ss2", + "create_namespace": true, + // "existing_serviceid_id": "", + "serviceid_name": "esoda-test-ss-2-serviceid", + "serviceid_description": "esoda-test-ss-2-serviceid description", + // "existing_account_secrets_group_id": "", + "account_secrets_group_name": "esoda-test-ss-accsg-2", + "account_secrets_group_description": "esoda-test-ss-accsg-2 description", + "trusted_profile_name": "", + "trusted_profile_description": "", + "existing_service_secrets_group_id_list": []string{}, + "service_secrets_groups_list": []map[string]any{ + { + "name": "esoda-test-ss-s3-sg", + "description": "Secrets group 3 for secrets used by the ESO", + }, + { + "name": "esoda-test-ss-s4-sg", + "description": "Secrets group 4 for secrets used by the ESO", + }, + }, + }, + }, + } + + logger.Log(mainOptions.Testing, "setupSolutionSchematicOptions - Using mainOptions.Prefix: ", mainOptions.Prefix) + + vars := []testschematic.TestSchematicTerraformVar{ + {Name: "ibmcloud_api_key", Value: mainOptions.RequiredEnvironmentVars["TF_VAR_ibmcloud_api_key"], DataType: "string", Secure: true}, + {Name: "prefix", Value: mainOptions.Prefix, DataType: "string"}, + {Name: "existing_secrets_manager_crn", Value: smCRN, DataType: "string"}, + {Name: "existing_cluster_crn", Value: existingOptions.LastTestTerraformOutputs["cluster_crn"], DataType: "string"}, + + {Name: "eso_secretsstores_configuration", Value: eso_secretsstores_configuration, DataType: "object"}, + } + + return vars +} + +func TestRunFullConfigSolutionSchematics(t *testing.T) { + + // set up the options for existing resource deployment + // needed by solution + existingResourceOptions := setupOptionsSchematics(t, "eso-cluster-full", existingResourcesTerraformDir) + + // Creates temp dirs and runs InitAndApply for existing resources + // outputs will be in options after apply + + existingResourceOptions.SkipTestTearDown = true + _, existDeployErr := existingResourceOptions.RunTest() + + defer existingResourceOptions.TestTearDown() // public function ignores skip above + + // immediately fail and exit test if existing deployment failed (tear down is in a defer) + require.NoError(t, existDeployErr, "error creating needed existing resources") + + // start main schematics test + options := setupSolutionSchematicOptions(t, "eso-full", fullConfigSolutionDir) + + options.TerraformVars = getFullConfigSolutionTestVariables(options, existingResourceOptions) + + err := options.RunSchematicTest() + assert.Nil(t, err, "This should not have errored") + +} + +func TestRunFullConfigSolutionUpgradeSchematics(t *testing.T) { + + // set up the options for existing resource deployment + // needed by solution + existingResourceOptions := setupOptionsSchematics(t, "eso-cluster-fupg", existingResourcesTerraformDir) + + // Creates temp dirs and runs InitAndApply for existing resources + // outputs will be in options after apply + + existingResourceOptions.SkipTestTearDown = true + _, existDeployErr := existingResourceOptions.RunTest() + defer existingResourceOptions.TestTearDown() // public function ignores skip above + + // immediately fail and exit test if existing deployment failed (tear down is in a defer) + require.NoError(t, existDeployErr, "error creating needed existing VPC resources") + + // start main schematics test + options := setupSolutionSchematicOptions(t, "eso-fupg", fullConfigSolutionDir) + + options.TerraformVars = getFullConfigSolutionTestVariables(options, existingResourceOptions) + + err := options.RunSchematicUpgradeTest() + assert.Nil(t, err, "This should not have errored") +} diff --git a/tests/scripts/post-validation-eso.sh b/tests/scripts/post-validation-eso.sh new file mode 100755 index 00000000..e221196a --- /dev/null +++ b/tests/scripts/post-validation-eso.sh @@ -0,0 +1,19 @@ +#! /bin/bash + +######################################################################################################################## +## This script is used by the catalog pipeline to destroy the SLZ ROKS Cluster, which was provisioned as a ## +## prerequisite for the WAS extension that is published to the catalog ## +######################################################################################################################## + +set -e + +TERRAFORM_SOURCE_DIR="tests/resources" +TF_VARS_FILE="terraform.tfvars" + +( + cd ${TERRAFORM_SOURCE_DIR} + echo "Destroying prerequisite ESO DA Cluster .." + terraform destroy -input=false -auto-approve -var-file=${TF_VARS_FILE} || exit 1 + + echo "Post-validation complete successfully" +) diff --git a/tests/scripts/pre-validation-eso.sh b/tests/scripts/pre-validation-eso.sh new file mode 100755 index 00000000..47af456a --- /dev/null +++ b/tests/scripts/pre-validation-eso.sh @@ -0,0 +1,50 @@ +#! /bin/bash + +######################################################################################################################## +## This script is used by the catalog pipeline to deploy the SLZ ROKS, which is a prerequisite for the WAS operator ## +## landing zone extension, after catalog validation has completed. ## +######################################################################################################################## + +set -e + +DA_DIR="solutions/fully-configurable" +TERRAFORM_SOURCE_DIR="tests/existing-resources" +JSON_FILE="${DA_DIR}/catalogValidationValues.json" +REGION="us-south" +RESOURCE_GROUP="geretain-test-ext-secrets-sync" +EXISTING_SECRETS_MANAGER_CRN="crn:v1:bluemix:public:secrets-manager:us-south:a/abac0df06b644a9cabc6e44f55b3880e:79c6d411-c18f-4670-b009-b0044a238667::" +TF_VARS_FILE="terraform.tfvars" + +( + cwd=$(pwd) + cd ${TERRAFORM_SOURCE_DIR} + echo "Provisioning prerequisite..." + terraform init || exit 1 + # $VALIDATION_APIKEY is available in the catalog runtime + { + echo "ibmcloud_api_key=\"${VALIDATION_APIKEY}\"" + echo "prefix=\"eso-da-$(openssl rand -hex 2)\"" + echo "region=\"${REGION}\"" + echo "resource_group=\"${RESOURCE_GROUP}\"" + } >> "${TF_VARS_FILE}" + terraform apply -input=false -auto-approve -var-file="${TF_VARS_FILE}" || exit 1 + + existing_secrets_manager_crn_var_name="existing_secrets_manager_crn" + existing_cluster_crn_var_name="existing_cluster_crn" + prefix_var_name="prefix" + prefix_var_value="$(terraform output -state=terraform.tfstate -raw prefix)" + existing_cluster_crn_var_value="$(terraform output -state=terraform.tfstate -raw cluster_crn)" + + echo "Appending '${prefix_var_name}', '${existing_cluster_crn_var_name}', '${existing_secrets_manager_crn_var_name}' input variable values to ${JSON_FILE}..." + + cd "${cwd}" + jq -r --arg prefix_var_name "${prefix_var_name}" \ + --arg prefix_var_value "${prefix_var_value}" \ + --arg existing_cluster_crn_var_name "${existing_cluster_crn_var_name}" \ + --arg existing_cluster_crn_var_value "${existing_cluster_crn_var_value}" \ + --arg existing_secrets_manager_crn_var_name "${existing_secrets_manager_crn_var_name}" \ + --arg existing_secrets_manager_crn_var_value "${EXISTING_SECRETS_MANAGER_CRN}" \ + '. + {($prefix_var_name): $prefix_var_value, ($existing_cluster_crn_var_name): $existing_cluster_crn_var_value, ($existing_secrets_manager_crn_var_name): $existing_secrets_manager_crn_var_value}' "${JSON_FILE}" > tmpfile && mv tmpfile "${JSON_FILE}" || exit 1 + + echo "Pre-validation complete successfully" +) diff --git a/variables.tf b/variables.tf index 3ff89fa8..5f0a9652 100644 --- a/variables.tf +++ b/variables.tf @@ -3,13 +3,13 @@ ############################################################################################################ variable "eso_namespace" { - description = "Namespace to create and be used to install ESO components including helm releases. If eso_store_scope == cluster, this will also be used to deploy ClusterSecretStore/cluster_store in it" + description = "Namespace to create and be used to install ESO components including helm releases." type = string default = null } variable "existing_eso_namespace" { - description = "Existing Namespace to be used to install ESO components including helm releases. If eso_store_scope == cluster, this will also be used to deploy ClusterSecretStore/cluster_store in it" + description = "Existing Namespace to be used to install ESO components including helm releases." type = string default = null }