diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1f65b1e3..aeac75fd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Primary owner should be listed first in list of global owners, followed by any secondary owners -* @SirSpidey @ocofaigh +* @vbontempi @daniel-butler-irl diff --git a/.github/settings.yml b/.github/settings.yml index f1a90940..6dbad9cf 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -25,4 +25,4 @@ repository: description: "Synchronizes secrets between Secrets Manager and an IBM Cloud OpenShift cluster" # Use a comma-separated list of topics to set on the repo (ensure not to use any caps in the topic string). - topics: terraform, ibm-cloud, terraform-module, core-team + topics: terraform, ibm-cloud, terraform-module, core-team, external-secret diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 577cfcfa..14502e48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: issue_comment: types: - created - jobs: call-terraform-ci-pipeline: uses: terraform-ibm-modules/common-pipeline-assets/.github/workflows/common-terraform-module-ci-v2.yml@v1.22.5 diff --git a/.gitignore b/.gitignore index 223ebaa2..7bee4643 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Local .terraform directories **/.terraform/* - +testpod*.* # .tfstate files *.tfstate *.tfstate.* @@ -40,6 +40,9 @@ terraform.rc # Ignore .tfsec .tfsec/ +# Ignore brew lock +Brewfile.lock.json + # Ignore Mac files .DS_Store @@ -49,25 +52,9 @@ terraform.rc # Node modules /node_modules -# Visual Studio Code -.vscode/ - -### Go ### -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +testpod*.yaml +precommit.txt -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work +# VS Code state +.vscode/ +*.code-workspace diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32155edc..9d1e8bd6 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -common-dev-assets/module-assets/.pre-commit-config.yaml \ No newline at end of file +./common-dev-assets/module-assets/.pre-commit-config.yaml \ No newline at end of file diff --git a/.secrets.baseline b/.secrets.baseline index 83c2fe0a..ad443f6f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2023-12-09T06:39:44Z", + "generated_at": "2023-12-11T12:45:00Z", "plugins_used": [ { "name": "AWSKeyDetector" diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..e69de29b diff --git a/Makefile b/Makefile index fa1444d3..b753a4a6 120000 --- a/Makefile +++ b/Makefile @@ -1 +1 @@ -common-dev-assets/module-assets/Makefile \ No newline at end of file +./common-dev-assets/module-assets/Makefile \ No newline at end of file diff --git a/README.md b/README.md index 83e2d757..40a3efa7 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,555 @@ - -# Terraform modules template project - - -[![Incubating (Not yet consumable)](https://img.shields.io/badge/status-Incubating%20(Not%20yet%20consumable)-red)](https://terraform-ibm-modules.github.io/documentation/#/badge-status) +## Terraform IBM External Secrets Operator module + +[![Graduated (Supported)](https://img.shields.io/badge/Status-Graduated%20(Supported)-brightgreen)](https://terraform-ibm-modules.github.io/documentation/#/badge-status) [![latest release](https://img.shields.io/github/v/release/terraform-ibm-modules/terraform-ibm-external-secrets-operator?logo=GitHub&sort=semver)](https://github.com/terraform-ibm-modules/terraform-ibm-external-secrets-operator/releases/latest) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com/) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) - - -TODO: Replace this with a description of the modules in this repo. +This module automates the installation and configuration of the [External Secrets Operator](https://external-secrets.io/) in a cluster. - - + ## Overview * [terraform-ibm-external-secrets-operator](#terraform-ibm-external-secrets-operator) +* [Submodules](./modules) + * [eso-clusterstore](./modules/eso-clusterstore) + * [eso-external-secret](./modules/eso-external-secret) + * [eso-secretstore](./modules/eso-secretstore) + * [eso-trusted-profile](./modules/eso-trusted-profile) * [Examples](./examples) - * [Advanced example](./examples/advanced) - * [Basic example](./examples/basic) + * [Basic Example](./examples/basic) + * [Example that uses trusted profiles (container authentication)](./examples/trusted-profiles-authentication) + * [Example to deploy the External Secret Operator and to create a different set of resources in terms of secrets, secret groups, stores and auth configurations](./examples/all-combined) + * [ImagePull API key Secrets Manager](./examples/all-combined/imagepull-apikey-secrets-manager) * [Contributing](#contributing) + +## 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). + +The module 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) - - +The submodules automate the configuration of an operator, providing the following features: +- Deploy and configure [ClusterSecretStore](https://external-secrets.io/latest/api/clustersecretstore/) resources for cluster scope secrets store [eso-clusterstore](./eso-clusterstore/README.md) +- Deploy and configure [SecretStore](https://external-secrets.io/latest/api/secretstore/) resources for namespace scope secrets store [eso-secretstore](./eso-secretstore/README.md) +- Leverage on two authentication methods to be configured on the single stores instances: + - IAM apikey standard authentication + - IAM Trusted profile: in conjunction with [`eso-trusted-profile`](./eso-trusted-profile/README.md) submodule, which allows to create one or more trusted profiles to use with the ESO module for trusted profile authentication. +- Configure the [ExternalSecret](https://external-secrets.io/latest/api/externalsecret/) resources to be bound to the expected secrets store (according to the visibility you need) and to configure the target secret details + - The following secret types of [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/#secret-types) are currently supported: + - `Opaque` (`opaque` in this module) + - `kubernetes.io/dockerconfigjson` (`dockerconfigjson` in this module) +The current version of the module 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 below](#example-of-multitenancy-configuration-example-in-namespaced-externalsecrets-stores) - -## terraform-ibm-external-secrets-operator +The following combinations of Kubernetes Secrets and Secrets Manager secrets are used with given [External-Secret type](https://external-secrets.io/latest/provider/ibm-secrets-manager/). -### Usage +| es_kubernetes_secret_type[^1] | sm_secret_type[^2] | external_secret_type[^3] | +|-------------------------------|--------------------|--------------------------| +| dockerconfigjson | arbitrary | arbitrary | +| dockerconfigjson | iam_credentials | iam_credentials | +| dockerconfigjson | username_password | username_password | +| opaque | arbitrary | arbitrary | +| opaque | iam_credentials | iam_credentials | +| opaque | username_password | username_password | +| opaque | kv | kv | +| tls | imported_cert | imported_cert | +| tls | public_cert | public_cert | +| tls | private_cert | private_cert | - +[^2]: [sm_secret_type](#input_sm_secret_type): IBM Cloud Secrets Manager secret type that is used as source data by ESO. + +[^3]: [external_secret_type](https://external-secrets.io/latest/provider/ibm-secrets-manager/#secret-types): The secret type that is used by ESO. + +### Customise ESO deployment on specific cluster nodes + +In order to customise the NodeSelector and the tolerations to make the External Secret Operator deployed on specific cluster nodes it is possible to configure the following input variable with the appropriate values: ```hcl +variable "eso_cluster_nodes_configuration" { + description = "Configuration to use to customise ESO deployment on specific cluster nodes. Setting appropriate values will result in customising ESO helm release. Default value is null to keep ESO standard deployment." + type = object({ + nodeSelector = object({ + label = string + value = string + }) + tolerations = object({ + key = string + operator = string + value = string + effect = string + }) + }) + default = null +} +``` + +For example: +```hcl +module "external_secrets_operator" { + (...) + eso_cluster_nodes_configuration = { + nodeSelector = { + label = "dedicated" + value = "edge" + } + tolerations = { + key = "dedicated" + operator = "Equal" + value = "edge" + effect = "NoExecute" + } + } + (...) ``` -### Required access policies +will make the External Secret Operator to run on clusters nodes labeled with `dedicated: edge`. + +The resulting helm release configuration, according to the `terraform plan` output would be like + +```bash + +(...) +# module.external_secrets_operator.helm_release.external_secrets_operator[0] will be created + + resource "helm_release" "external_secrets_operator" { + + atomic = false + + chart = "external-secrets" + + cleanup_on_fail = false + + create_namespace = false + + dependency_update = false + + disable_crd_hooks = false + + disable_openapi_validation = false + + disable_webhooks = false + + force_update = false + + id = (known after apply) + + lint = false + + manifest = (known after apply) + + max_history = 0 + + metadata = (known after apply) + + name = "external-secrets" + + namespace = "es-operator" + + pass_credentials = false + + recreate_pods = false + + render_subchart_notes = true + + replace = false + + repository = "https://charts.external-secrets.io" + + reset_values = false + + reuse_values = false + + skip_crds = false + + status = "deployed" + + timeout = 300 + + values = [ + + <<-EOT + installCRDs: true + extraVolumes: + - name: sa-token + projected: + defaultMode: 0644 + sources: + - serviceAccountToken: + path: sa-token + expirationSeconds: 3600 + audience: iam + extraVolumeMounts: + - mountPath: /var/run/secrets/tokens + name: sa-token + webhook: + extraVolumes: + - name: sa-token + projected: + defaultMode: 0644 + sources: + - serviceAccountToken: + path: sa-token + expirationSeconds: 3600 + audience: iam + extraVolumeMounts: + - mountPath: /var/run/secrets/tokens + name: sa-token + EOT, + + <<-EOT + nodeSelector: { dedicated: edge } + tolerations: + - key: dedicated + operator: Equal + value: edge + effect: NoExecute + webhook: + nodeSelector: { dedicated: edge } + tolerations: + - key: dedicated + operator: Equal + value: edge + effect: NoExecute + certController: + nodeSelector: { dedicated: edge } + tolerations: + - key: dedicated + operator: Equal + value: edge + effect: NoExecute + EOT, + ] + + verify = false + + version = "0.9.7" + + wait = true + + wait_for_jobs = false + } +(...) + +``` + +Similarly exporting this environment variable + +```bash +export TF_VAR_eso_cluster_nodes_configuration="{\"nodeSelector\": {\"label\": \"dedicated\", \"value\": \"transit\"}, \"tolerations\": {\"key\": \"dedicated\", \"operator\": \"Equal\", \"value\": \"transit\", \"effect\": \"NoExecute\"}}" +``` + +will make the External Secret Operator to run on clusters nodes labeled with `dedicated: transit`. + +The default `null` value keeps the default ESO behaviour. + +### Example of Multitenancy configuration example in namespaced externalsecrets stores + +To configure a set of tenants to be configured in their proper namespace (to achieve tenant isolation) you need simply to follow these steps: + +- deploy ESO in the cluster + +```hcl +module "external_secrets_operator" { + source = "terraform-ibm-modules/external-secrets-operator/ibm" + version = "1.0.0" + eso_namespace = var.eso_namespace # namespace to deploy ESO + service_endpoints = var.service_endpoints # use public or private endpoints for IAM and Secrets Manager + eso_cluster_nodes_configuration = <> +} +``` + +- create multiple `SecretStore`(s) in the proper namespaces + +With `api_key` authentication mode + +```hcl + +module "eso_namespace_secretstore_1" { + depends_on = [ + module.external_secrets_operator + ] + source = "../modules/eso-secretstore" + eso_authentication = "api_key" + region = local.sm_region # SM region + sstore_namespace = var.es_kubernetes_namespaces[2] # namespace to create the secret store + sstore_secrets_manager_guid = local.sm_guid # the guid of the secrets manager instance to use + sstore_store_name = "${var.es_kubernetes_namespaces[2]}-store" # store name + # to pull the secrets from SM + sstore_secret_apikey = data.ibm_sm_iam_credentials_secret.secret_puller_secret.api_key # pragma: allowlist secret + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store" # helm release name suffix to use for the store + sstore_secret_name = "generic-cluster-api-key" #checkov:skip=CKV_SECRET_6 +} + +``` + +With `trusted_profile` authentication mode + +```hcl +module "eso_namespace_secretstores" { + depends_on = [ + module.external_secrets_operator + ] + source = "../modules/eso-secretstore" + eso_authentication = "trusted_profile" + region = local.sm_region # SM region + sstore_namespace = kubernetes_namespace.examples[count.index].metadata[0].name # namespace to create the secret store + sstore_secrets_manager_guid = local.sm_guid # the guid of the secrets manager instance to use + sstore_store_name = "${kubernetes_namespace.examples[count.index].metadata[0].name}-store" # store name + sstore_trusted_profile_name = module.external_secrets_trusted_profiles[count.index].trusted_profile_name # trusted profile name to use into this secret store + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store-${count.index}" # helm release name suffix to use for the store + sstore_secret_name = "secretstore-api-key" #checkov:skip=CKV_SECRET_6 +} + +``` + +More details can be found in the examples linked below. + +### More information links + +For more information about IAM Trusted profiles and ESO Multitenancy configuration please refer to +- [IBM IAM Trusted profiles article](https://www.ibm.com/cloud/blog/announcements/use-trusted-profiles-to-simplify-user-and-access-management) +- [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_ + +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. + + +### Pod 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 the [Stakater Reloader](https://github.com/stakater/Reloader). +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. +When using the [eso-external-secret](modules/eso-external-secret) submodule, use the `reloader_watching` variable to have the annotation added to the secret. - +### Troubleshooting - +In the case of problems with secrets synchronization a good start point to the investigation is to list the externalsecrets resources in the cluster: - +```bash +oc get externalsecrets -A +NAMESPACE NAME AGE STATUS CAPABILITIES READY +apikeynspace3 secretstore.external-secrets.io/apikeynspace3-store 32m Valid ReadOnly True +apikeynspace4 secretstore.external-secrets.io/apikeynspace4-store 32m Valid ReadOnly True +tpnspace1 secretstore.external-secrets.io/tpnspace1-store 32m Valid ReadOnly True +tpnspace2 secretstore.external-secrets.io/tpnspace2-store 32m Valid ReadOnly True +NAMESPACE NAME AGE STATUS CAPABILITIES READY + clustersecretstore.external-secrets.io/cluster-store 32m Valid ReadOnly True + +NAMESPACE NAME STORE REFRESH INTERVAL STATUS READY +apikeynspace1 externalsecret.external-secrets.io/dockerconfigjson-uc cluster-store 1m SecretSyncedError False +apikeynspace2 externalsecret.external-secrets.io/cloudant-opaque-arb cluster-store 5m SecretSyncedError False +apikeynspace3 externalsecret.external-secrets.io/dockerconfigjson-arb apikeynspace3-store 1h SecretSyncedError False +apikeynspace4 externalsecret.external-secrets.io/dockerconfigjson-iam apikeynspace4-store 1h SecretSyncedError False +tpnspace1 externalsecret.external-secrets.io/geretain-tesoall-arbitrary-arb-tp-0 tpnspace1-store 5m SecretSynced True +tpnspace2 externalsecret.external-secrets.io/geretain-tesoall-arbitrary-arb-tp-1 tpnspace2-store 5m SecretSynced True +``` + +In the example above some of the externalsecrets are experiencing secrets synchronization errors. +By describing them you should be able to identify the error: + +```bash +oc describe externalsecret dockerconfigjson-uc -n apikeynspace1 +Name: dockerconfigjson-uc +Namespace: apikeynspace1 +Labels: app=raw + app.kubernetes.io/managed-by=Helm + chart=raw-v0.2.5 + heritage=Helm + release=apikeynspace1-es-docker-uc +Annotations: meta.helm.sh/release-name: apikeynspace1-es-docker-uc + meta.helm.sh/release-namespace: apikeynspace1 +API Version: external-secrets.io/v1beta1 +Kind: ExternalSecret +Metadata: + (...) +Status: + Conditions: + Last Transition Time: 2023-06-27T15:18:31Z + Message: could not get secret data from provider + Reason: SecretSyncedError + Status: False + Type: Ready + Refresh Time: +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + (...) + Warning UpdateFailed 119s (x13 over 28m) external-secrets An error occurred while performing the 'authenticate' step: Post "https://iam.cloud.ibm.com/identity/token": context deadline exceeded (Client.Timeout exceeded while awaiting headers) +``` + +In the output above there is a problem with reaching IAM endpoint (verify that the pods where ESO is running are able to reach that endpoint) + +### _Important note_ + +If you taint or destroy or simply make a change that needs the helm_release resource of the ESO operator to be deleted and recreated, this would make terraform to destroy the operator itself, including all the CRDs, which would destroy all the secrets synched through ESO, even if the helm_release resource of these CRDs aren't directly touched and terraform wouldn't be able to identify such a change. +So in the case you plan to make changes to the operator helm_release once deployed, run preliminary a `terraform plan` to be sure that the release isn't destroyed and recreated. + +### Examples of the secrets format and layout + +
dockerconfigjson from arbitrary or iam_credentials + +Secret Body > + +``` +apiVersion: v1 +kind: Secret +metadata: + name: dockerconfigjson-iam + namespace: test-ns +data: + .dockerconfigjson: [BASE64ENCODED] +type: kubernetes.io/dockerconfigjson +``` + +Base64 Decoded `.dockerconfigson` > + +``` +{ + "auths": { + "us.icr.io": { + "username": "iamapikey", + "password": "APIKEYVALUE", # pragma: allowlist secret + "email": "terraform@ibm.com" + } + } +} +``` + +
+ +
dockerconfigjson from username_password + +Secret Body > +``` +apiVersion: v1 +kind: Secret +metadata: + name: dockerconfigjson-uc + namespace: test-ns +data: + .dockerconfigjson: [BASE64ENCODED] +type: kubernetes.io/dockerconfigjson +``` + +Base64 Decoded `.dockerconfigson` > + +``` +{ + "auths": { + "xx.artifactory.xyz-devops.com": { + "username": "artifactoryuser@org.com", + "password": "APIKEYVALUE" # pragma: allowlist secret + } + } +} +``` + +
+ +
opaque from arbitrary or iam_credentials + +Secret Body > +``` +apiVersion: v1 +kind: Secret +metadata: + name: opaque-arb + namespace: test-ns +type: Opaque +data: + apikey: APIKEYVALUE # pragma: allowlist secret + +``` + +
+ +
opaque from username_password + +Secret Body > +``` +apiVersion: v1 +kind: Secret +metadata: + name: opaque-uc + namespace: test-ns +type: Opaque +data: + username: test-user + password: PASSWORDVALUE # pragma: allowlist secret + +``` + +
+ +## Usage + +```hcl +module "es_kubernetes_secret" { + source = "../modules/eso-external-secret" + es_kubernetes_secret_type = "dockerconfigjson" + sm_secret_type = "iam_credentials" + sm_secret_id = module.docker_config.serviceid_apikey_secret_id + eso_setup = true + es_kubernetes_namespaces = var.es_kubernetes_namespaces + es_docker_email = "terraform@ibm.com" + eso_generic_secret_apikey = data.ibm_secrets_manager_secret.secret_puller_secret.api_key # pragma: allowlist secret + secrets_manager_guid = module.secrets_manager_iam_configuration.secrets_manager_guid + region = "us-south" + es_kubernetes_secret_name = "dockerconfigjson-iam" + depends_on = [ + kubernetes_namespace.cluster_namespaces + ] + es_kubernetes_secret_data_key = "apiKey" + es_helm_rls_name = "es-docker-iam" +} +``` - ### Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [helm](#requirement\_helm) | >= 2.11.0, < 3.0.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.16.1, < 3.0.0 | ### Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [eso\_namespace](#module\_eso\_namespace) | terraform-ibm-modules/namespace/ibm | 1.0.2 | ### Resources -No resources. +| Name | Type | +|------|------| +| [helm_release.external_secrets_operator](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.pod_reloader](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [kubernetes_namespace.existing_eso_namespace](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/namespace) | data source | ### Inputs -No inputs. +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [eso\_chart\_location](#input\_eso\_chart\_location) | The location of the External Secrets Operator Helm chart. | `string` | `"https://charts.external-secrets.io"` | no | +| [eso\_chart\_version](#input\_eso\_chart\_version) | 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. | `string` | `"0.12.1"` | no | +| [eso\_cluster\_nodes\_configuration](#input\_eso\_cluster\_nodes\_configuration) | Configuration to use to customise ESO deployment on specific cluster nodes. Setting appropriate values will result in customising ESO helm release. Default value is null to keep ESO standard deployment. |
object({
nodeSelector = object({
label = string
value = string
})
tolerations = object({
key = string
operator = string
value = string
effect = string
})
})
| `null` | no | +| [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.12.1-ubi@sha256:d38834043de0a4e4feeac8a08d0bc96b71ddd7fe1d4c8583ee3751badeaeb01d"` | 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\_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 | +| [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` | `"1.2.1"` | 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 | +| [reloader\_deployed](#input\_reloader\_deployed) | Whether to deploy reloader or not https://github.com/stakater/Reloader | `bool` | `true` | no | +| [reloader\_ignore\_configmaps](#input\_reloader\_ignore\_configmaps) | Whether to ignore configmap changes or not | `bool` | `false` | no | +| [reloader\_ignore\_secrets](#input\_reloader\_ignore\_secrets) | Whether to ignore secret changes or not | `bool` | `false` | no | +| [reloader\_image](#input\_reloader\_image) | The reloader image in the format of `[registry-url]/[namespace]/[image]`. | `string` | `"ghcr.io/stakater/reloader"` | no | +| [reloader\_image\_version](#input\_reloader\_image\_version) | 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. | `string` | `"v1.2.1-ubi@sha256:80a557100c6835c7e3c9842194250c9c4ca78f43200bc3a93a32e5b105ad11bb"` | no | +| [reloader\_is\_argo\_rollouts](#input\_reloader\_is\_argo\_rollouts) | Enable Argo Rollouts | `bool` | `false` | no | +| [reloader\_is\_openshift](#input\_reloader\_is\_openshift) | Enable OpenShift DeploymentConfigs | `bool` | `true` | no | +| [reloader\_log\_format](#input\_reloader\_log\_format) | The log format to use for reloader. Possible values are `json` or `text`. Default value is `json` | `string` | `"text"` | no | +| [reloader\_namespaces\_selector](#input\_reloader\_namespaces\_selector) | List of comma separated label selectors, if multiple are provided they are combined with the AND operator | `string` | `null` | no | +| [reloader\_namespaces\_to\_ignore](#input\_reloader\_namespaces\_to\_ignore) | List of comma separated namespaces to ignore for reloader. If multiple are provided they are combined with the AND operator | `string` | `null` | no | +| [reloader\_pod\_monitor\_metrics](#input\_reloader\_pod\_monitor\_metrics) | Enable to scrape Reloader's Prometheus metrics | `bool` | `false` | no | +| [reloader\_reload\_on\_create](#input\_reloader\_reload\_on\_create) | Enable reload on create events | `bool` | `true` | no | +| [reloader\_reload\_strategy](#input\_reloader\_reload\_strategy) | The reload strategy to use for reloader. Possible values are `env-vars` or `annotations`. Default value is `annotations` | `string` | `"annotations"` | no | +| [reloader\_resource\_label\_selector](#input\_reloader\_resource\_label\_selector) | List of comma separated label selectors, if multiple are provided they are combined with the AND operator | `string` | `null` | no | +| [reloader\_resources\_to\_ignore](#input\_reloader\_resources\_to\_ignore) | List of comma separated resources to ignore for reloader. If multiple are provided they are combined with the AND operator | `string` | `null` | no | +| [reloader\_sync\_after\_restart](#input\_reloader\_sync\_after\_restart) | Enable sync after Reloader restarts for Add events, works only when reloadOnCreate is true | `bool` | `true` | no | ### Outputs No outputs. - - + ## Contributing You can report issues and request features for this module in GitHub issues in the module repo. See [Report an issue or request a feature](https://github.com/terraform-ibm-modules/.github/blob/main/.github/SUPPORT.md). diff --git a/chart/raw/Chart.yaml b/chart/raw/Chart.yaml new file mode 100644 index 00000000..0b7b2da6 --- /dev/null +++ b/chart/raw/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +description: A place for all the Kubernetes resources which don't already have a home +name: raw +version: v0.2.5 +kubeVersion: ">=1.16.0-0" +home: https://github.com/itscontained/charts/blob/master/itscontained/raw +keywords: + - raw + - incubator + - incubator-raw +sources: + - https://github.com/helm/charts/blob/master/incubator/raw +maintainers: + - name: DirtyCajunRice + email: nick@cajun.pro diff --git a/chart/raw/README.md b/chart/raw/README.md new file mode 100644 index 00000000..b79f84a8 --- /dev/null +++ b/chart/raw/README.md @@ -0,0 +1,60 @@ +# itscontained/raw + +The `itscontained/raw` chart takes a list of Kubernetes resources and +merges each resource with a default `metadata.labels` map and installs +the result. + +The Kubernetes resources can be "raw" ones defined under the `resources` key, or "templated" ones defined under the `templates` key. + +Some use cases for this chart include Helm-based installation and +maintenance of resources of kinds: +- LimitRange +- PriorityClass +- Secret + +## Usage + +### Raw resources + +#### STEP 1: Create a yaml file containing your raw resources. + +``` +# raw-priority-classes.yaml +resources: + - apiVersion: scheduling.k8s.io/v1beta1 + kind: PriorityClass + metadata: + name: common-critical + value: 100000000 + globalDefault: false + description: "This priority class should only be used for critical priority common pods." +``` + +#### STEP 2: Install your raw resources. + +``` +helm install raw-priority-classes itscontained/raw -f raw-priority-classes.yaml +``` + +### Templated resources + +#### STEP 1: Create a yaml file containing your templated resources. + +``` +# values.yaml + +templates: +- | + apiVersion: v1 + kind: Secret + metadata: + name: common-secret + stringData: + mykey: {{ .Values.mysecret }} +``` + +#### STEP 2: Install your templated resources. + +``` +helm install mysecret itscontained/raw -f values.yaml +``` diff --git a/chart/raw/templates/_helpers.tpl b/chart/raw/templates/_helpers.tpl new file mode 100644 index 00000000..f916d275 --- /dev/null +++ b/chart/raw/templates/_helpers.tpl @@ -0,0 +1,45 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "raw.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "raw.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "raw.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +raw.resource will create a resource template that can be +merged with each item in `.Values.resources`. +*/}} +{{- define "raw.resource" -}} +metadata: + labels: + app: {{ template "raw.name" . }} + chart: {{ template "raw.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- end }} diff --git a/chart/raw/templates/resources.yaml b/chart/raw/templates/resources.yaml new file mode 100644 index 00000000..83c900de --- /dev/null +++ b/chart/raw/templates/resources.yaml @@ -0,0 +1,9 @@ +{{- $template := fromYaml (include "raw.resource" .) -}} +{{- range .Values.resources }} +--- +{{ toYaml (merge . $template) -}} +{{- end }} +{{- range $i, $t := .Values.templates }} +--- +{{ toYaml (merge (tpl $t $ | fromYaml) $template) -}} +{{- end }} diff --git a/chart/raw/values.yaml b/chart/raw/values.yaml new file mode 100644 index 00000000..586f7276 --- /dev/null +++ b/chart/raw/values.yaml @@ -0,0 +1,17 @@ +resources: [] +# - apiVersion: scheduling.k8s.io/v1beta1 +# kind: PriorityClass +# metadata: +# name: app-low +# value: 70000 +# globalDefault: false +# description: "This priority class should only be used for low priority app pods." + +templates: [] +# - | +# apiVersion: v1 +# kind: Secret +# metadata: +# name: common-secret +# stringData: +# mykey: {{ .Values.mysecret }} diff --git a/ci b/ci index f9d799a5..f591e15b 120000 --- a/ci +++ b/ci @@ -1 +1 @@ -common-dev-assets/module-assets/ci \ No newline at end of file +./common-dev-assets/module-assets/ci \ No newline at end of file diff --git a/cra-config.yaml b/cra-config.yaml index 02d79f03..27499937 100644 --- a/cra-config.yaml +++ b/cra-config.yaml @@ -1,11 +1,10 @@ -# More info about this file at https://github.com/terraform-ibm-modules/common-pipeline-assets/blob/main/.github/workflows/terraform-test-pipeline.md#cra-config-yaml version: "v1" CRA_TARGETS: - - CRA_TARGET: "examples/advanced" # Target directory for CRA scan. If not provided, the CRA Scan will not be run. - CRA_IGNORE_RULES_FILE: "cra-tf-validate-ignore-rules.json" # CRA Ignore file to use. If not provided, it checks the repo root directory for `cra-tf-validate-ignore-rules.json` - PROFILE_ID: "0e6e7b5a-817d-4344-ab6f-e5d7a9c49520" # SCC profile ID (currently set to the FSCloud 1.4.0 profile). + - CRA_TARGET: "examples/all-combined" + CRA_IGNORE_RULES_FILE: "cra-tf-validate-ignore-rules.json" + CRA_ENVIRONMENT_VARIABLES: # An optional map of environment variables for CRA, where the key is the variable name and value is the value. Useful for providing TF_VARs. + TF_VAR_existing_cis_instance_name: "geretain-permanent-cis" + TF_VAR_existing_cis_instance_resource_group_id: "6deff3aad31a48b696cb1b173e54c41d" # pragma: allowlist secret # SCC_INSTANCE_ID: "" # The SCC instance ID to use to download profile for CRA scan. If not provided, a default global value will be used. # SCC_REGION: "" # The IBM Cloud region that the SCC instance is in. If not provided, a default global value will be used. - # CRA_ENVIRONMENT_VARIABLES: # An optional map of environment variables for CRA, where the key is the variable name and value is the value. Useful for providing TF_VARs. - # TF_VAR_sample: "sample value" - # TF_VAR_other: "another value" + # PROFILE_ID: "" # The Profile ID input for CRA SCC scan. Ensure to use a US-specific ID. If not provided, a default global value will be used. diff --git a/cra-tf-validate-ignore-rules.json b/cra-tf-validate-ignore-rules.json index adbff6e0..e963b906 100644 --- a/cra-tf-validate-ignore-rules.json +++ b/cra-tf-validate-ignore-rules.json @@ -1,3 +1,40 @@ { - "scc_rules": [] + "scc_rules": [ + { + "scc_rule_id": "rule-216e2449-27d7-4afc-929a-b66e196a9cf9", + "description": "Check whether Flow Logs for VPC are enabled", + "ignore_reason": "This rule is not relevant to the module itself, just the VPC resource that is used in the example that is scanned", + "is_valid": false + }, + { + "scc_rule_id": "rule-64c0bea0-8760-4a6b-a56c-ee375a48961e", + "description": "Check whether Virtual Private Cloud (VPC) has no public gateways attached", + "ignore_reason": "This rule is not relevant to the module itself, just the VPC resource that is used in the example that is scanned", + "is_valid": false + }, + { + "scc_rule_id": "rule-2325054a-c338-474a-9740-0b7034487e40", + "description": "Check whether OpenShift clusters are accessible only by using private endpoints", + "ignore_reason": "This rule is not relevant to the module itself, just the OCP cluster resource that is used in the example that is scanned", + "is_valid": false + }, + { + "scc_rule_id": "rule-4d86c074-097e-4ff3-a763-ccff128388e2", + "description": "Check whether multifactor authentication (MFA) is enabled at the account level", + "ignore_reason": "This is an account based rule, so unrelated to this module itself", + "is_valid": false + }, + { + "scc_rule_id": "rule-0704e840-e443-4781-b9be-ec57469d09c1", + "description": "Check whether permissions for API key creation are limited and configured in IAM settings for the account owner", + "ignore_reason": "This is an account based rule, so unrelated to this module itself", + "is_valid": false + }, + { + "scc_rule_id": "rule-0244c010-fde6-4db3-95aa-8952bd292ac3", + "description": "Check whether permissions for service ID creation are limited and configured in IAM settings for the account owner", + "ignore_reason": "This is an account based rule, so unrelated to this module itself", + "is_valid": false + } + ] } diff --git a/examples/advanced/README.md b/examples/advanced/README.md deleted file mode 100644 index d52511a3..00000000 --- a/examples/advanced/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Advanced example - - - diff --git a/examples/advanced/outputs.tf b/examples/advanced/outputs.tf deleted file mode 100644 index addadea6..00000000 --- a/examples/advanced/outputs.tf +++ /dev/null @@ -1,23 +0,0 @@ -############################################################################## -# Outputs -############################################################################## - -output "region" { - description = "The region all resources were provisioned in" - value = var.region -} - -output "prefix" { - description = "The prefix used to name all provisioned resources" - value = var.prefix -} - -output "resource_group_name" { - description = "The name of the resource group used" - value = var.resource_group -} - -output "resource_tags" { - description = "List of resource tags" - value = var.resource_tags -} diff --git a/examples/advanced/variables.tf b/examples/advanced/variables.tf deleted file mode 100644 index 170a5abc..00000000 --- a/examples/advanced/variables.tf +++ /dev/null @@ -1,29 +0,0 @@ -variable "ibmcloud_api_key" { - type = string - description = "The IBM Cloud API Key" - sensitive = true -} - -variable "region" { - type = string - description = "Region to provision all resources created by this example" - default = "us-south" -} - -variable "prefix" { - type = string - description = "Prefix to append to all resources created by this example" - default = "complete" -} - -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 -} - -variable "resource_tags" { - type = list(string) - description = "Optional list of tags to be added to created resources" - default = [] -} diff --git a/examples/advanced/version.tf b/examples/advanced/version.tf deleted file mode 100644 index 398bd442..00000000 --- a/examples/advanced/version.tf +++ /dev/null @@ -1,12 +0,0 @@ -terraform { - required_version = ">= 1.3.0" - - # Ensure that there is always 1 example locked into the lowest provider version of the range defined in the main - # module's version.tf (usually a basic example), and 1 example that will always use the latest provider version. - required_providers { - ibm = { - source = "IBM-Cloud/ibm" - version = ">= 1.49.0, < 2.0.0" - } - } -} diff --git a/examples/all-combined/README.md b/examples/all-combined/README.md new file mode 100644 index 00000000..8b914d13 --- /dev/null +++ b/examples/all-combined/README.md @@ -0,0 +1,82 @@ +# Example to deploy the External Secret Operator and to create a different set of resources in terms of secrets, secret groups, stores and auth configurations + +This end-to-end example performs the following actions +- Loads an existing resource group or creates a new one +- Provisions a OCP infrastructure with VPC, COS instance and an OpenShift cluster +- Configures an hybrid ESO configuration with a set of different stores to cover use-cases + - a ClusterSecretStore with API key authentication + - a ClusterSecretStore with Trusted profile authentication + - two namespaced SecretStore with API key authentication + - two namespaced SecretStore with Trusted Profile authentication based on a policy restricted to a single secrets group + - one namespaced SecretStore with Trusted Profile authentication based on a policy restricted to multiple secrets groups + - one namespaced SecretStore with Trusted Profile authentication based on a policy not restricted to any secrets group +- Creates/Loads the following resources to complete the mentioned use-cases + - Loads an existing Secrets Manager instance or creates a new one + - Creates Secrets Manager IAM engine configuration and secret group(s) + - Creates service ID (secret-puller) configured with IAM policies to pull secrets from SM + - Deploys a dockerconfigjson secret for an artifactory registry + - Creates username_password Secrets Manager secret to store artifactory credentials + - Deploys external secrets in designated namespace + - Creates a ClusterSecretStore using API key authentication to access secrets from designated namespace + - Installs and configures external secret operator + - Deploys another dockerconfigjson secret for an artifactory registry + - Deploys external secrets in designated namespace + - Creates a ClusterSecretStore using Trusted Profile authentication to access secrets from designated namespace + - Deploys a dockerconfigjson secret from an arbitrary secret to authenticate in container registry + - Creates arbitrary Secrets Manager secret to store existing API key + - Deploys external secrets in designated namespaces + - Creates a SecretStore (using API key authentication) to access secrets from designated namespace + - Deploys a dockerconfigjson secret from a dynamic IAM secret to authenticate in container registry + - Creates ServiceID (imagePull) with IAM policies to read from container registry namespace + - Creates IAM Secrets Manager secret and dynamic API key that is associated with a imagePull Service ID + - Deploys external secrets in designated namespaces + - Creates a SecretStore(using API key authentication) to access secrets from designated namespace + - Deploys a dockerconfigjson secret from a set of dynamic IAM secrets to authenticate in a set of container registries + - Creates a set of ServiceIDs (imagePull) with IAM policies to read from container registry namespace for each secret to create (`${var.prefix}-image-pull-service-id-chain-sec-1/2/3`) + - Creates a set of IAM Secrets Manager secrets and dynamic API key that are associated with the imagePull ServiceIDs created at the step above + - Deploys external secrets resource in designated namespaces for the dockerjsonconfig secret building the secrets chain and using an existing secrets store + - Creates and deploys a set of arbitrary secrets to cover the different use-cases for namespaced SecretStores + - Creates and deploys a public certificate through CIS integration and public certificate engine module + - Creates and deploys a private certificate and private certificate engine module + - Loads certificate components stored on Secrets Manager as arbitrary secrets and then use these to create and deploy an imported certificate with public and intermediate certificates and public certificate private key + - Creates and deploys a key-value secret with single key-value couple + - Creates and deploys a key-value secret with multiple key-value couples + + + +In order to create the intermediate certificate the following parameters are needed: +- imported_certificate_sm_id: Secrets Manager ID where the componenents for the imported certificate are stored +- imported_certificate_sm_region: region of the Secrets Manager instance where the componenents for the imported certificate are stored +- imported_certificate_intermediate_secret_id: secret ID to load the intermediate certificate component for the imported certificate +- imported_certificate_public_secret_id: secret ID to load the public certificate component for the imported certificate +- imported_certificate_private_secret_id: secret ID to load the private key component for the imported certificate + +In order to generate the public certificate the following mutually exclusive parameters are needed: +1. `acme_letsencrypt_private_key`: the Let's Encrypt ACME private key value (more details available [here](https://cloud.ibm.com/docs/secrets-manager?topic=secrets-manager-prepare-order-certificates#create-acme-account)) +2. Let's Encrypt ACME Secrets Manager secret details: + - `acme_letsencrypt_private_key_secret_id`: secret ID to load the Let's Encrypt acme private key for the public certificate generation + - `acme_letsencrypt_private_key_sm_id`: Secrets Manager ID where the Let's Encrypt acme private key for the public certificate generation is stored + - `acme_letsencrypt_private_key_sm_region`: region of the Secrets Manager instance where the Let's Encrypt acme private key for the public certificate generation is stored + +If a value is provided for `acme_letsencrypt_private_key` the other ones are not needed and not used, otherwise if it is **null** all the remaining ones (Secrets Manager ID, region and secret ID for Let's Encrypt ACME key) are needed to create a public certificate. + +In the case all the mentioned parameters are left with their default **null** value, the related secrets will not be created. + +The example is split into separated templates related with their specific scope: +- main.tf for the VPC, cluster, VPE, ESO operator deployment and namespaces preliminary creation +- clusterstore.tf for ESO ClusterSecretsStore configuration with API key authentication, including two different externalsecrets and secret types configuration (username/password and arbitrary) +- secretstore.tf for ESO secretstore configuration with API key authentication and namespace isolation, including two different externalsecrets and secrets configuration (arbitrary and image pull API key secret using imagepull-apikey-secrets-manager) +- secretsmanager.tf for Secrets Manager instance configuration, along with IAM serviceID and API keys and secrets groups +- kv.tf for key-value (single and multiple keys) secrets +- publiccertificate.tf for public certificate management +- privatecertificate.tf for private certificate management +- importedcertificate.tf for imported certificate management +- tpauth_namespaced_sstore.tf for ESO SecretsStore and externalsecret configuration with Trusted Profile authentication and namespace isolation +- tpauth_cluster_sstore.tf for ESO ClusterSecretsStore and externalsecret configuration with Trusted Profile authentication + + +## Important note about input region and existing SecretManager region parameters + +The test is currently using the existing SecretManager region to deploy the VPC and the cluster if this value is not null. Instead if null it follows what set through `var.region` + +This logic is achieved through the local `sm_region` variable that is then used to create resources. diff --git a/examples/all-combined/clusterstore.tf b/examples/all-combined/clusterstore.tf new file mode 100644 index 00000000..23218c75 --- /dev/null +++ b/examples/all-combined/clusterstore.tf @@ -0,0 +1,77 @@ +############################################################################## +# This template shows how to create an ESO clusterstore, a secrets store cluster scoped, with api key authentication +# Two different secret types +# - username & password +# - arbitrary (for Cloudant resource key) +# are created and configured in two different externalsecret resources in two different namespaces +# leveraging on the same ESO clusterstore +# The username/password secret is stored into a dockerconfigjson K8s secret type +# The arbitrary secret is stored into an opaque K8s secret type +############################################################################## + +# creation of the ESO ClusterStore (cluster wide scope) with apikey authentication +module "eso_clusterstore" { + source = "../../modules/eso-clusterstore" + eso_authentication = "api_key" + clusterstore_secret_apikey = data.ibm_sm_iam_credentials_secret.secret_puller_secret.api_key + region = local.sm_region + clusterstore_helm_rls_name = "cluster-store" + clusterstore_secret_name = "generic-cluster-api-key" #checkov:skip=CKV_SECRET_6 + clusterstore_name = "cluster-store" + clusterstore_secrets_manager_guid = local.sm_guid + eso_namespace = var.eso_namespace + service_endpoints = var.service_endpoints + depends_on = [ + module.external_secrets_operator + ] +} + +################################################################## +# 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" { + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.secrets_manager_group.secret_group_id + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-usernamepassword-secret" # checkov:skip=CKV_SECRET_6 + secret_description = "example secret in existing secret manager instance" #tfsec:ignore:general-secrets-no-plaintext-exposure # checkov:skip=CKV_SECRET_6 + secret_payload_password = local.userpass_apikey + 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" { + depends_on = [module.eso_clusterstore] + 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 = module.sm_userpass_secret.secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[0].metadata[0].name + eso_store_name = "cluster-store" + 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" +} diff --git a/examples/all-combined/imagepull-apikey-secrets-manager/README.md b/examples/all-combined/imagepull-apikey-secrets-manager/README.md new file mode 100644 index 00000000..1a901128 --- /dev/null +++ b/examples/all-combined/imagepull-apikey-secrets-manager/README.md @@ -0,0 +1,50 @@ +# ImagePull API key Secrets Manager + +This module generate and store a service ID API key in IBM Cloud Secrets Manager that can be used in an imagePullSecret for pulling images from an IBM Container Registry. For more information about image pull secrets, see Creating an image pull secret in "Setting up an image registry" in Cloud docs. + + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= v1.0.0 | +| [ibm](#requirement\_ibm) | >= 1.51.0, < 2.0.0 | +| [time](#requirement\_time) | >= 0.9.1, < 1.0.0 | + +### Modules + +| Name | Source | Version | +|------|--------|---------| +| [dynamic\_serviceid\_apikey](#module\_dynamic\_serviceid\_apikey) | terraform-ibm-modules/iam-serviceid-apikey-secrets-manager/ibm | 1.1.1 | + +### Resources + +| Name | Type | +|------|------| +| [ibm_iam_service_id.image_secret_pull_service_id](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_service_id) | resource | +| [ibm_iam_service_policy.cr_policy](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_service_policy) | resource | +| [time_sleep.wait_30_seconds_for_creation](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [time_sleep.wait_30_seconds_for_destruction](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [cr\_namespace\_name](#input\_cr\_namespace\_name) | Container registry namespace name to be configured in IAM policy. | `string` | n/a | yes | +| [region](#input\_region) | Region where resources will be sourced / created | `string` | n/a | yes | +| [resource\_group\_id](#input\_resource\_group\_id) | The resource group ID in which the container registry namespace exists (used in IAM policy configuration). | `string` | n/a | yes | +| [secrets\_manager\_guid](#input\_secrets\_manager\_guid) | Secrets manager instance GUID where secrets will be stored or fetched from | `string` | n/a | yes | +| [service\_id\_description](#input\_service\_id\_description) | Description to be used for ServiceID. | `string` | `"ServiceId used to access container registry"` | no | +| [service\_id\_name](#input\_service\_id\_name) | Name to be used for ServiceID. | `string` | `"sid:0.0.1:image-secret-pull:automated:simple-service:container-registry:"` | no | +| [service\_id\_secret\_description](#input\_service\_id\_secret\_description) | Description to be used for ServiceID API Key. | `string` | `"API Key associated with image pull serviceid"` | no | +| [service\_id\_secret\_group\_id](#input\_service\_id\_secret\_group\_id) | Secret Group ID of SM IAM secret where Service ID apikey will be stored. Leave default (null) to add in default secret-group. | `string` | `null` | no | +| [service\_id\_secret\_name](#input\_service\_id\_secret\_name) | Name of SM IAM secret (dynamic ServiceID API Key) to be created. | `string` | `"image-pull-iam-secret"` | no | + +### Outputs + +| Name | Description | +|------|-------------| +| [secret\_manager\_guid](#output\_secret\_manager\_guid) | GUI of Secrets-Manager containing secret | +| [serviceid\_apikey\_secret\_id](#output\_serviceid\_apikey\_secret\_id) | ID of the Secret Manager Secret containing ServiceID API Key | +| [serviceid\_name](#output\_serviceid\_name) | Name of the ServiceID created to access Container Registry | + diff --git a/examples/all-combined/imagepull-apikey-secrets-manager/main.tf b/examples/all-combined/imagepull-apikey-secrets-manager/main.tf new file mode 100644 index 00000000..c5d52542 --- /dev/null +++ b/examples/all-combined/imagepull-apikey-secrets-manager/main.tf @@ -0,0 +1,57 @@ +############################################################################## +# Create ServiceID +############################################################################## + +resource "ibm_iam_service_id" "image_secret_pull_service_id" { + name = var.service_id_name + description = var.service_id_description +} + +############################################################################## +# Create IAM policy +############################################################################## + +resource "ibm_iam_service_policy" "cr_policy" { + + + iam_service_id = ibm_iam_service_id.image_secret_pull_service_id.id + roles = ["Reader"] + + resources { + service = "container-registry" + resource_type = "namespace" + resource = var.cr_namespace_name + resource_group_id = var.resource_group_id + } +} + +# wait time to acknowledge / finish serviceID creation +resource "time_sleep" "wait_30_seconds_for_creation" { + depends_on = [ibm_iam_service_policy.cr_policy] + create_duration = "30s" +} + + +############################################################################## +# Create Secrets-Manager IAM secret/API key +############################################################################## + +module "dynamic_serviceid_apikey" { + source = "terraform-ibm-modules/iam-serviceid-apikey-secrets-manager/ibm" + version = "1.1.1" + region = var.region + #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_iam_secret_name = var.service_id_secret_name + sm_iam_secret_description = var.service_id_secret_description #tfsec:ignore:general-secrets-no-plaintext-exposure + serviceid_id = ibm_iam_service_id.image_secret_pull_service_id.id + secrets_manager_guid = var.secrets_manager_guid + sm_iam_secret_api_key_persistence = true + secret_group_id = var.service_id_secret_group_id + depends_on = [time_sleep.wait_30_seconds_for_creation] +} + +## Wait 30 sec after APIKey is deleted to ensure proper processing +resource "time_sleep" "wait_30_seconds_for_destruction" { + depends_on = [module.dynamic_serviceid_apikey] + destroy_duration = "30s" +} diff --git a/examples/all-combined/imagepull-apikey-secrets-manager/outputs.tf b/examples/all-combined/imagepull-apikey-secrets-manager/outputs.tf new file mode 100644 index 00000000..cce7ead2 --- /dev/null +++ b/examples/all-combined/imagepull-apikey-secrets-manager/outputs.tf @@ -0,0 +1,18 @@ +############################################################################## +# Outputs +############################################################################## + +output "secret_manager_guid" { + value = var.secrets_manager_guid + description = "GUI of Secrets-Manager containing secret" +} + +output "serviceid_name" { + description = "Name of the ServiceID created to access Container Registry" + value = ibm_iam_service_id.image_secret_pull_service_id.name +} + +output "serviceid_apikey_secret_id" { + description = "ID of the Secret Manager Secret containing ServiceID API Key" + value = module.dynamic_serviceid_apikey.secret_id +} diff --git a/examples/all-combined/imagepull-apikey-secrets-manager/variables.tf b/examples/all-combined/imagepull-apikey-secrets-manager/variables.tf new file mode 100644 index 00000000..eac10c0d --- /dev/null +++ b/examples/all-combined/imagepull-apikey-secrets-manager/variables.tf @@ -0,0 +1,62 @@ +############################################################################## +# Input Variables for module +############################################################################## + +############################################################################## +# Common vars +############################################################################## +variable "region" { + description = "Region where resources will be sourced / created" + type = string +} + +variable "secrets_manager_guid" { + type = string + description = "Secrets manager instance GUID where secrets will be stored or fetched from" +} + +############################################################################## +# Service ID setup vars +############################################################################## +variable "cr_namespace_name" { + type = string + description = "Container registry namespace name to be configured in IAM policy." +} + +variable "resource_group_id" { + type = string + description = "The resource group ID in which the container registry namespace exists (used in IAM policy configuration)." +} + +variable "service_id_name" { + type = string + default = "sid:0.0.1:image-secret-pull:automated:simple-service:container-registry:" + description = "Name to be used for ServiceID." +} + +variable "service_id_description" { + type = string + default = "ServiceId used to access container registry" + description = "Description to be used for ServiceID." +} + +############################################################################## +# Service ID SM secret setup vars +############################################################################## +variable "service_id_secret_group_id" { + type = string + description = "Secret Group ID of SM IAM secret where Service ID apikey will be stored. Leave default (null) to add in default secret-group." + default = null +} + +variable "service_id_secret_name" { + type = string + description = "Name of SM IAM secret (dynamic ServiceID API Key) to be created." + default = "image-pull-iam-secret" #tfsec:ignore:general-secrets-no-plaintext-exposure +} + +variable "service_id_secret_description" { + type = string + default = "API Key associated with image pull serviceid" #tfsec:ignore:general-secrets-no-plaintext-exposure + description = "Description to be used for ServiceID API Key." +} diff --git a/examples/all-combined/imagepull-apikey-secrets-manager/version.tf b/examples/all-combined/imagepull-apikey-secrets-manager/version.tf new file mode 100644 index 00000000..4f2be558 --- /dev/null +++ b/examples/all-combined/imagepull-apikey-secrets-manager/version.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= v1.0.0" + required_providers { + # Use "greater than or equal to" range in modules + ibm = { + source = "IBM-Cloud/ibm" + version = ">= 1.51.0, < 2.0.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.9.1, < 1.0.0" + } + } +} diff --git a/examples/all-combined/importedcertificate.tf b/examples/all-combined/importedcertificate.tf new file mode 100644 index 00000000..8ef5cf35 --- /dev/null +++ b/examples/all-combined/importedcertificate.tf @@ -0,0 +1,88 @@ +################################################################## +# imported certificate for secrets manager +################################################################## + +locals { + + # validation for secrets manager region to be set for existing secrets manager instance + validate_imported_sm_region_cnd = var.imported_certificate_sm_id != null && var.imported_certificate_sm_region == null + validate_imported_sm_region_msg = "imported_certificate_sm_region must also be set when value given for imported_certificate_sm_id" + # tflint-ignore: terraform_unused_declarations + validate_imported_sm_region_chk = regex( + "^${local.validate_imported_sm_region_msg}$", + (!local.validate_imported_sm_region_cnd + ? local.validate_imported_sm_region_msg + : "")) + + validate_imported_sm_cnd = (var.imported_certificate_public_secret_id != null && var.imported_certificate_private_secret_id != null) && var.imported_certificate_sm_id == null + validate_imported_sm_msg = "If imported_certificate_public_secret_id and imported_certificate_private_secret_id to create an imported certificate also imported_certificate_sm_id must be set" + # tflint-ignore: terraform_unused_declarations + validate_imported_sm_chk = regex( + "^${local.validate_imported_sm_msg}$", + (!local.validate_imported_sm_cnd + ? local.validate_imported_sm_msg + : "")) +} + +# loading from Secrets Manager the three components (private key, intermediate and public cert) composing the imported certificate +data "ibm_sm_arbitrary_secret" "imported_certificate_intermediate" { + count = var.imported_certificate_intermediate_secret_id != null ? 1 : 0 + region = var.imported_certificate_sm_region + instance_id = var.imported_certificate_sm_id + secret_id = var.imported_certificate_intermediate_secret_id +} + +data "ibm_sm_arbitrary_secret" "imported_certificate_private" { + count = var.imported_certificate_private_secret_id != null ? 1 : 0 + region = var.imported_certificate_sm_region + instance_id = var.imported_certificate_sm_id + secret_id = var.imported_certificate_private_secret_id +} + +data "ibm_sm_arbitrary_secret" "imported_certificate_public" { + count = var.imported_certificate_public_secret_id != null ? 1 : 0 + region = var.imported_certificate_sm_region + instance_id = var.imported_certificate_sm_id + secret_id = var.imported_certificate_public_secret_id +} + +# composing the imported certificate +resource "ibm_sm_imported_certificate" "secrets_manager_imported_certificate" { + count = var.imported_certificate_public_secret_id != null && var.imported_certificate_private_secret_id != null ? 1 : 0 + instance_id = local.sm_guid + region = local.sm_region + name = "${var.prefix}-sm-imported-cert" + custom_metadata = { "key" : "value" } + description = "Imported certificate for ${var.prefix}-sm-imported-cert" + secret_group_id = module.secrets_manager_group.secret_group_id + certificate = data.ibm_sm_arbitrary_secret.imported_certificate_public[0].payload + intermediate = var.imported_certificate_intermediate_secret_id != null ? data.ibm_sm_arbitrary_secret.imported_certificate_intermediate[0].payload : null + private_key = data.ibm_sm_arbitrary_secret.imported_certificate_private[0].payload + provider = ibm.ibm-sm +} + +# definition of the flag handling the intermediate section +locals { + imported_certificate_has_intermediate = var.imported_certificate_intermediate_secret_id != null ? true : false +} + +# eso externalsecret object for imported certificate in secrets store with apikey authentication +module "external_secret_imported_certificate" { + count = var.imported_certificate_public_secret_id != null && var.imported_certificate_private_secret_id != null ? 1 : 0 + depends_on = [ + module.eso_apikey_namespace_secretstore_1 + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_secret_type = "tls" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "imported_cert" #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_secret_id = ibm_sm_imported_certificate.secrets_manager_imported_certificate[0].secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[2].metadata[0].name + eso_store_name = "${var.es_namespaces_apikey[2]}-store" + es_refresh_interval = var.es_refresh_interval + es_kubernetes_secret_name = "impcertificate-tls" #tfsec:ignore:general-secrets-no-plaintext-exposure #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "es-impcertificate" + sm_certificate_has_intermediate = local.imported_certificate_has_intermediate + # setting the value to false as the secret's components on SM are split in "not bundled" format + sm_certificate_bundle = false +} diff --git a/examples/all-combined/kv.tf b/examples/all-combined/kv.tf new file mode 100644 index 00000000..80b0e57b --- /dev/null +++ b/examples/all-combined/kv.tf @@ -0,0 +1,70 @@ +################################################################## +# Management of kv secrets +################################################################## + +################################################################## +# Single key kv secret +################################################################## + +resource "ibm_sm_kv_secret" "secrets_manager_kv_secret_singlekey" { + instance_id = local.sm_guid + region = local.sm_region + custom_metadata = { "meta_key" : "meta_value" } + data = { "secret_key" : "secret_value" } + description = "Extended description for this secret." + labels = ["my-label"] + secret_group_id = module.secrets_manager_group.secret_group_id + name = "${var.prefix}-sm-kv-secret" + provider = ibm.ibm-sm +} + +################################################################## +# Multiple keys kv secret +################################################################## + +resource "ibm_sm_kv_secret" "secrets_manager_kv_secret_multiplekeys" { + instance_id = local.sm_guid + region = local.sm_region + custom_metadata = { "meta_key" : "meta_value" } + data = { "secret_key1" : "secret_value1", "secret_key2" : "secret_value2", "secret_key3" : "secret_value3" } # checkov:skip=CKV_SECRET_6 + description = "Extended description for this secret." + labels = ["my-label"] + secret_group_id = module.secrets_manager_group.secret_group_id + name = "${var.prefix}-sm-kv-multikeys-secret" + provider = ibm.ibm-sm +} + +# eso externalsecret object for single key kv secret configured with apikey authentication secretstore +module "external_secret_kv_singlekey" { + depends_on = [ + module.eso_apikey_namespace_secretstore_2 + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_secret_type = "opaque" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "kv" #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_secret_id = ibm_sm_kv_secret.secrets_manager_kv_secret_singlekey.secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[3].metadata[0].name + eso_store_name = "${var.es_namespaces_apikey[3]}-store" + es_refresh_interval = var.es_refresh_interval + es_kubernetes_secret_name = "kv-single-key" #tfsec:ignore:general-secrets-no-plaintext-exposure #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "kv-single-key" + sm_kv_keyid = "secret_key" +} + +# eso externalsecret object for multiple keys kv secret configured with apikey authentication secretstore +module "external_secret_kv_multiplekeys" { + depends_on = [ + module.eso_apikey_namespace_secretstore_2 + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_secret_type = "opaque" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "kv" #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_secret_id = ibm_sm_kv_secret.secrets_manager_kv_secret_multiplekeys.secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[3].metadata[0].name + eso_store_name = "${var.es_namespaces_apikey[3]}-store" + es_refresh_interval = var.es_refresh_interval + es_kubernetes_secret_name = "kv-multiple-keys" #tfsec:ignore:general-secrets-no-plaintext-exposure #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "kv-multiple-keys" +} diff --git a/examples/all-combined/main.tf b/examples/all-combined/main.tf new file mode 100644 index 00000000..2b4c7cbc --- /dev/null +++ b/examples/all-combined/main.tf @@ -0,0 +1,298 @@ +################################################################## +# Resource Group +################################################################## + +module "resource_group" { + source = "terraform-ibm-modules/resource-group/ibm" + version = "1.1.6" + # 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 = "REDHAT_8_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.0" + 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.0" + 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.0" + 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.0" + 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.0" + 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.0" + 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.35.10" + 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 + import_default_worker_pool_on_create = false +} + + +############################################################################## +# Init cluster config for helm and kubernetes providers +############################################################################## + +data "ibm_container_cluster_config" "cluster_config" { + cluster_name_id = module.ocp_base.cluster_id + resource_group_id = module.resource_group.resource_group_id +} + +# Wait time to allow cluster refreshes components after provisioning +resource "time_sleep" "wait_45_seconds" { + depends_on = [data.ibm_container_cluster_config.cluster_config] + create_duration = "45s" +} + +######################################## +## CIS to own public certificate +######################################## + +data "ibm_cis" "cis_instance" { + name = var.existing_cis_instance_name + resource_group_id = var.existing_cis_instance_resource_group_id +} + +###################################### +# VPEs creation in the case of private for service endpoint +###################################### + +module "vpes" { + source = "terraform-ibm-modules/vpe-gateway/ibm" + version = "4.3.0" + count = var.service_endpoints == "private" ? 1 : 0 + region = var.region + prefix = "vpe" + vpc_name = "${var.prefix}-vpc" + vpc_id = module.vpc.vpc.vpc_id + subnet_zone_list = [ + for index, subnet in local.subnets.default : { + name = "${local.sm_region}-${index}" + zone = subnet.zone + id = subnet.id + acl_name = "acl" + public_gateway = true + } + ] + resource_group_id = module.resource_group.resource_group_id # pragma: allowlist secret + cloud_services = [] + cloud_service_by_crn = [ + { + name = "iam-${var.region}" + crn = "crn:v1:bluemix:public:iam-svcs:global:::endpoint:private.iam.cloud.ibm.com" + }, + { + name = "sm-${var.region}" + crn = local.sm_crn + } + ] + service_endpoints = "private" + depends_on = [ibm_resource_instance.secrets_manager] +} + +################################################################## +# ESO deployment +################################################################## + +module "external_secrets_operator" { + source = "../../" + eso_namespace = var.eso_namespace + depends_on = [ + kubernetes_namespace.apikey_namespaces, kubernetes_namespace.tp_namespaces + ] +} + +################################################################## +# Preliminary creation of namespaces to use for +# clusterstore and namespaced secretstores (to be configured with apikey authentication) +################################################################## + +# Creating the namespaces for apikey authentication secrets stores +resource "kubernetes_namespace" "apikey_namespaces" { + count = length(var.es_namespaces_apikey) + metadata { + name = var.es_namespaces_apikey[count.index] + } + lifecycle { + ignore_changes = [ + metadata[0].annotations, + metadata[0].labels + ] + } + depends_on = [ + time_sleep.wait_45_seconds + ] +} diff --git a/examples/all-combined/outputs.tf b/examples/all-combined/outputs.tf new file mode 100644 index 00000000..a06c9202 --- /dev/null +++ b/examples/all-combined/outputs.tf @@ -0,0 +1,14 @@ +############################################################################## +# Outputs +############################################################################## + + +output "cluster_id" { + description = "ID of the cluster deployed" + value = module.ocp_base.cluster_id +} + +output "subnets" { + description = "List of subnet" + value = local.subnets +} diff --git a/examples/all-combined/privatecertificate.tf b/examples/all-combined/privatecertificate.tf new file mode 100644 index 00000000..d9d039c7 --- /dev/null +++ b/examples/all-combined/privatecertificate.tf @@ -0,0 +1,61 @@ +################################################################## +# Private certificate secret configuration +################################################################## + +# private certificate common name, Certificate Authority common name and certificate template name definition +locals { + # generating certificate common name + pvt_cert_common_name = "pvt-${var.prefix}.${var.pvt_cert_common_name}" + pvt_root_ca_common_name = "pvt-${var.prefix}.${var.pvt_root_ca_common_name}" + pvt_certificate_template_name = var.pvt_certificate_template_name != null ? var.pvt_certificate_template_name : "pvt-${var.prefix}-cert-template" +} + +# private certificate engine +module "secrets_manager_private_secret_engine" { + source = "terraform-ibm-modules/secrets-manager-private-cert-engine/ibm" + version = "1.3.5" + secrets_manager_guid = local.sm_guid + region = local.sm_region + root_ca_name = var.pvt_ca_name != null ? var.pvt_ca_name : "pvt-${var.prefix}-project-root-ca" + root_ca_common_name = local.pvt_root_ca_common_name + root_ca_max_ttl = var.pvt_ca_max_ttl + intermediate_ca_name = "pvt-${var.prefix}-project-intermediate-ca" + certificate_template_name = local.pvt_certificate_template_name + providers = { + ibm = ibm.ibm-sm + } +} + +# private certificate generation +module "secrets_manager_private_certificate" { + depends_on = [module.secrets_manager_private_secret_engine] + source = "terraform-ibm-modules/secrets-manager-private-cert/ibm" + version = "1.3.2" + cert_name = "${var.prefix}-sm-private-cert" + cert_description = "Private certificate for ${local.pvt_cert_common_name}" + cert_secrets_group_id = module.secrets_manager_group.secret_group_id + cert_template = local.pvt_certificate_template_name + cert_common_name = local.pvt_cert_common_name + secrets_manager_guid = local.sm_guid + secrets_manager_region = local.sm_region + providers = { + ibm = ibm.ibm-sm + } +} + +# eso externalsecret object for private certificate in secrets store with apikey authentication +module "external_secret_private_certificate" { + depends_on = [ + module.eso_apikey_namespace_secretstore_1 + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_secret_type = "tls" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "private_cert" #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_secret_id = module.secrets_manager_private_certificate.secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[2].metadata[0].name + eso_store_name = "${var.es_namespaces_apikey[2]}-store" + es_refresh_interval = var.es_refresh_interval + es_kubernetes_secret_name = "pvtcertificate-tls" #tfsec:ignore:general-secrets-no-plaintext-exposure #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "es-pvtcertificate" +} diff --git a/examples/all-combined/provider.tf b/examples/all-combined/provider.tf new file mode 100644 index 00000000..97dc8d6c --- /dev/null +++ b/examples/all-combined/provider.tf @@ -0,0 +1,26 @@ +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + region = var.region +} + +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + region = var.existing_sm_instance_region == null ? var.region : var.existing_sm_instance_region + alias = "ibm-sm" +} + + +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/examples/all-combined/publiccertificate.tf b/examples/all-combined/publiccertificate.tf new file mode 100644 index 00000000..52a1ccaa --- /dev/null +++ b/examples/all-combined/publiccertificate.tf @@ -0,0 +1,68 @@ +################################################################## +# Public certificate secret configuration +################################################################## + +# A public certificate engine, consisting of a certificate authority (LetEncrypt) +# and a DNS provider authorisation (CIS) are configured as a pre-requisite to +# secrets manager generating certificates +module "secrets_manager_public_cert_engine" { + count = (var.acme_letsencrypt_private_key != null || (var.acme_letsencrypt_private_key_sm_id != null && var.acme_letsencrypt_private_key_secret_id != null && var.acme_letsencrypt_private_key_sm_region != null)) ? 1 : 0 + source = "terraform-ibm-modules/secrets-manager-public-cert-engine/ibm" + version = "1.0.2" + secrets_manager_guid = local.sm_guid + region = local.sm_region + internet_services_crn = data.ibm_cis.cis_instance.id + ca_config_name = var.ca_name != null ? var.ca_name : "${var.prefix}-project-ca" + dns_config_name = var.dns_provider_name != null ? var.dns_provider_name : "${var.prefix}-project-dns" + private_key_secrets_manager_instance_guid = var.acme_letsencrypt_private_key_sm_id + private_key_secrets_manager_secret_id = var.acme_letsencrypt_private_key_secret_id + private_key_secrets_manager_region = var.acme_letsencrypt_private_key_sm_region + acme_letsencrypt_private_key = var.acme_letsencrypt_private_key + skip_iam_authorization_policy = var.skip_iam_authorization_policy + providers = { + ibm = ibm.ibm-sm + ibm.secret-store = ibm.ibm-sm + } +} + +# public certificate common name definition +locals { + # generating certificate common name + cert_common_name = "pub-${var.prefix}.${var.cert_common_name}" +} + +# public certificate creation +module "secrets_manager_public_certificate" { + count = (var.acme_letsencrypt_private_key != null || (var.acme_letsencrypt_private_key_sm_id != null && var.acme_letsencrypt_private_key_secret_id != null && var.acme_letsencrypt_private_key_sm_region != null)) ? 1 : 0 + depends_on = [module.secrets_manager_public_cert_engine] + source = "terraform-ibm-modules/secrets-manager-public-cert/ibm" + version = "1.2.1" + cert_common_name = local.cert_common_name + cert_description = "Certificate for ${local.cert_common_name}" + cert_name = "${var.prefix}-sm-public-cert" + cert_secrets_group_id = module.secrets_manager_group.secret_group_id + secrets_manager_ca_name = var.ca_name != null ? var.ca_name : "${var.prefix}-project-ca" + secrets_manager_dns_provider_name = var.dns_provider_name != null ? var.dns_provider_name : "${var.prefix}-project-dns" + secrets_manager_guid = local.sm_guid + secrets_manager_region = local.sm_region + bundle_certs = var.public_certificate_bundle +} + +# eso externalsecret object for public certificate in secrets store with apikey authentication +module "external_secret_public_certificate" { + count = (var.acme_letsencrypt_private_key != null || (var.acme_letsencrypt_private_key_sm_id != null && var.acme_letsencrypt_private_key_secret_id != null && var.acme_letsencrypt_private_key_sm_region != null)) ? 1 : 0 + depends_on = [ + module.eso_apikey_namespace_secretstore_1 + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_secret_type = "tls" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "public_cert" #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_secret_id = module.secrets_manager_public_certificate[0].secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[2].metadata[0].name + eso_store_name = "${var.es_namespaces_apikey[2]}-store" + es_refresh_interval = var.es_refresh_interval + es_kubernetes_secret_name = "pubcertificate-tls" #tfsec:ignore:general-secrets-no-plaintext-exposure #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "es-pubcertificate" + sm_certificate_bundle = var.public_certificate_bundle +} diff --git a/examples/all-combined/secretsmanager.tf b/examples/all-combined/secretsmanager.tf new file mode 100644 index 00000000..ef72cd9e --- /dev/null +++ b/examples/all-combined/secretsmanager.tf @@ -0,0 +1,152 @@ +############################################################################## +# Secrets manager validations +############################################################################## + +locals { + + # validation for secrets manager region to be set for existing secrets manager instance + validate_sm_region_cnd = var.existing_sm_instance_guid != null && var.existing_sm_instance_region == null + validate_sm_region_msg = "existing_sm_instance_region must also be set when value given for existing_sm_instance_guid." + # tflint-ignore: terraform_unused_declarations + validate_sm_region_chk = regex( + "^${local.validate_sm_region_msg}$", + (!local.validate_sm_region_cnd + ? local.validate_sm_region_msg + : "")) + + # validation for secrets manager crn to be set for existing secrets manager instance if using private service endpoints + validate_sm_crn_cnd = var.existing_sm_instance_guid != null && var.existing_sm_instance_crn == null && var.service_endpoints == "private" + validate_sm_crn_msg = "existing_sm_instance_crn must also be set when value given for existing_sm_instance_guid if service_endpoints is private." + # tflint-ignore: terraform_unused_declarations + validate_sm_crn_chk = regex( + "^${local.validate_sm_crn_msg}$", + (!local.validate_sm_crn_cnd + ? local.validate_sm_crn_msg + : "")) + + # setting the secrets manager resource id to use + sm_guid = var.existing_sm_instance_guid == null ? ibm_resource_instance.secrets_manager[0].guid : var.existing_sm_instance_guid + + # if service_endpoints is not private the crn for SM is not needed because of VPE creation is not needed + sm_crn = var.existing_sm_instance_crn == null ? (var.service_endpoints == "private" ? ibm_resource_instance.secrets_manager[0].crn : "") : var.existing_sm_instance_crn + + + sm_region = var.existing_sm_instance_region == null ? var.region : var.existing_sm_instance_region + sm_acct_id = var.existing_sm_instance_guid == null ? module.iam_secrets_engine[0].acct_secret_group_id : module.secrets_manager_group_acct[0].secret_group_id +} + + +######################################## +# Secrets-Manager and IAM configuration +######################################## + +# IAM user policy, Secret Manager instance, Service ID for IAM engine, IAM service ID policies, associated Service ID API key stored in a secret object in account level secret-group and IAM engine configuration +resource "ibm_resource_instance" "secrets_manager" { + count = var.existing_sm_instance_guid == null ? 1 : 0 + name = "${var.prefix}-sm" + service = "secrets-manager" + plan = var.sm_service_plan + location = local.sm_region + tags = var.resource_tags + resource_group_id = module.resource_group.resource_group_id + timeouts { + create = "30m" # Extending provisioning time to 30 minutes + } + provider = ibm.ibm-sm +} + +# Configure IAM secrets engine +module "iam_secrets_engine" { + count = var.existing_sm_instance_guid == null ? 1 : 0 + source = "terraform-ibm-modules/secrets-manager-iam-engine/ibm" + version = "1.2.6" + region = local.sm_region + secrets_manager_guid = ibm_resource_instance.secrets_manager[0].guid + iam_secret_generator_service_id_name = "${var.prefix}-sid:0.0.1:${ibm_resource_instance.secrets_manager[0].name}-iam-secret-generator:automated:simple-service:secret-manager:" + iam_secret_generator_apikey_name = "${var.prefix}-iam-secret-generator-apikey" + new_secret_group_name = "${var.prefix}-account-secret-group" + iam_secret_generator_apikey_secret_name = "${var.prefix}-iam-secret-generator-apikey-secret" + iam_engine_name = "iam-engine" + endpoint_type = var.service_endpoints + providers = { + ibm = ibm.ibm-sm + } +} + +# create secrets group for secrets +module "secrets_manager_group" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = "${var.prefix}-secret-group" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group for storing account credentials" #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +# additional secrets manager secret group for service level secrets +module "secrets_manager_group_acct" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + count = var.existing_sm_instance_guid == null ? 0 : 1 + region = local.sm_region + secrets_manager_guid = local.sm_guid + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_group_name = "${var.prefix}-account-secret-group" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group for storing account credentials" #tfsec:ignore:general-secrets-no-plaintext-exposure + depends_on = [module.iam_secrets_engine] + providers = { + ibm = ibm.ibm-sm + } +} + +################################################################## +# Create IAM serviceId, IAM policy and IAM API key to pull secrets from secret manager +################################################################## + +# Create service-id +resource "ibm_iam_service_id" "secret_puller" { + name = "sid:0.0.1:${var.prefix}-secret-puller:automated:simple-service:secret-manager:" + description = "ServiceID that can pull secrets from Secret Manager" +} + +# Create policy to allow new service id to pull secrets from secrets manager +resource "ibm_iam_service_policy" "secret_puller_policy" { + iam_service_id = ibm_iam_service_id.secret_puller.id + roles = ["Viewer", "SecretsReader"] + + resources { + service = "secrets-manager" + resource_instance_id = local.sm_guid + resource_type = "secret-group" + resource = module.secrets_manager_group.secret_group_id + } +} + +# create dynamic Service ID API key and add to secret manager +module "dynamic_serviceid_apikey1" { + 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 = "${var.prefix}-${var.sm_iam_secret_name}" + sm_iam_secret_description = "Example of dynamic IAM secret / apikey" #tfsec:ignore:general-secrets-no-plaintext-exposure + serviceid_id = ibm_iam_service_id.secret_puller.id + secrets_manager_guid = local.sm_guid + secret_group_id = local.sm_acct_id + depends_on = [module.iam_secrets_engine, ibm_iam_service_policy.secret_puller_policy, ibm_iam_service_id.secret_puller] + providers = { + ibm = ibm.ibm-sm + } +} + + +# data source to get the API key to pull secrets from secrets manager +data "ibm_sm_iam_credentials_secret" "secret_puller_secret" { + instance_id = local.sm_guid + #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static type + secret_id = module.dynamic_serviceid_apikey1.secret_id + provider = ibm.ibm-sm +} diff --git a/examples/all-combined/secretstore.tf b/examples/all-combined/secretstore.tf new file mode 100644 index 00000000..e744721e --- /dev/null +++ b/examples/all-combined/secretstore.tf @@ -0,0 +1,201 @@ +############################################################################## +# This template shows how to create ESO secretstores, a secrets store namespace scoped, with api key authentication +# Two different secretstores are created in two different namespaces, allowing to achive namespace isolation +# Two different secret types +# - arbitrary (to store image pull API key) +# - Image pull API key secret using imagepull-apikey-secrets-manager-module +# are created and configured in two different externalsecret resources configured with the two different secretstores +# Both the secrets are store into a dockerconfigjson K8s secret type +# The Image pull API key secret is stored into an opaque K8s secret type +############################################################################## + +# creation of namespace scoped secretstore with apikey authentication +module "eso_apikey_namespace_secretstore_1" { + depends_on = [module.external_secrets_operator] + source = "../../modules/eso-secretstore" + eso_authentication = "api_key" + region = local.sm_region + sstore_namespace = kubernetes_namespace.apikey_namespaces[2].metadata[0].name + sstore_secrets_manager_guid = local.sm_guid + sstore_store_name = "${var.es_namespaces_apikey[2]}-store" + sstore_secret_apikey = data.ibm_sm_iam_credentials_secret.secret_puller_secret.api_key + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store" + sstore_secret_name = "generic-cluster-api-key" #checkov:skip=CKV_SECRET_6 +} + +module "eso_apikey_namespace_secretstore_2" { + depends_on = [module.external_secrets_operator] + source = "../../modules/eso-secretstore" + eso_authentication = "api_key" + region = local.sm_region + sstore_namespace = kubernetes_namespace.apikey_namespaces[3].metadata[0].name + sstore_secrets_manager_guid = local.sm_guid + sstore_store_name = "${var.es_namespaces_apikey[3]}-store" + sstore_secret_apikey = data.ibm_sm_iam_credentials_secret.secret_puller_secret.api_key + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store" + sstore_secret_name = "generic-cluster-api-key" #checkov:skip=CKV_SECRET_6 +} + +################################################################## +# Arbitrary secret (for example image pull secret) +################################################################## + +locals { + # image pull API key value for sm_arbitrary_imagepull_secret + imagepull_apikey = sensitive("imagepull-payload-example") +} + +# create the arbitrary secret and store in secret manager +module "sm_arbitrary_imagepull_secret" { + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.secrets_manager_group.secret_group_id + secret_type = "arbitrary" + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-imagepull-apikey-secret" #checkov:skip=CKV_SECRET_6 + secret_description = "example secret for provided image pull API key" #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_payload_password = local.imagepull_apikey + providers = { + ibm = ibm.ibm-sm + } +} + +# creation of externalsecret for the arbitrary secret in namespaced secretstore with API key authentication +# secrets stored in K8s dockerconfigjson secret type +module "external_secret_arbitrary_cr_registry" { + depends_on = [ + module.eso_apikey_namespace_secretstore_1 + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_secret_type = "dockerconfigjson" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "arbitrary" + sm_secret_id = module.sm_arbitrary_imagepull_secret.secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[2].metadata[0].name + es_container_registry = "test.icr.com" + es_container_registry_email = "terraform@ibm.com" + eso_store_name = "${var.es_namespaces_apikey[2]}-store" + es_refresh_interval = var.es_refresh_interval + es_kubernetes_secret_name = "dockerconfigjson-arb" #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "es-docker-arb" +} + +################################################################## +# Image pull API key secret using imagepull-apikey-secrets-manager-module +################################################################## + +# create image pull serviceID and secret and store in secrets manager +module "image_pull" { + source = "./imagepull-apikey-secrets-manager" + resource_group_id = module.resource_group.resource_group_id + secrets_manager_guid = local.sm_guid + cr_namespace_name = var.cr_namespace_name + region = local.sm_region + #tfsec:ignore:general-secrets-no-plaintext-exposure + service_id_secret_name = "${var.prefix}-image-pull-service-id" + service_id_secret_group_id = module.secrets_manager_group.secret_group_id + depends_on = [module.iam_secrets_engine, module.secrets_manager_group] + providers = { + ibm = ibm.ibm-sm + } +} + +# ESO external secret storing the secret in the cluster as dockerconfigson type (from image pull IAM dynamic credential/secret) using namespaced secretstore +module "external_secret_secret_image_pull" { + source = "../../modules/eso-external-secret" + depends_on = [ + module.eso_apikey_namespace_secretstore_2, + ] + eso_store_scope = "namespace" + es_kubernetes_secret_type = "dockerconfigjson" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "iam_credentials" #tfsec:ignore:general-secrets-no-plaintext-exposure + sm_secret_id = module.image_pull.serviceid_apikey_secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[3].metadata[0].name + es_container_registry = "test.icr.com" + es_container_registry_email = "terraform@ibm.com" + es_refresh_interval = var.es_refresh_interval + eso_store_name = "${var.es_namespaces_apikey[3]}-store" + es_kubernetes_secret_name = "dockerconfigjson-iam" #tfsec:ignore:general-secrets-no-plaintext-exposure #checkov:skip=CKV_SECRET_6 + es_helm_rls_name = "es-docker-iam" +} + +################################################################## +# Image pull API key secrets using imagepull-apikey-secrets-manager-module +# to be configured in a single chain of secrets +################################################################## + +# create image pull serviceID and secret and store in secrets manager +module "image_pull_chain_secret_1" { + source = "./imagepull-apikey-secrets-manager" + resource_group_id = module.resource_group.resource_group_id + secrets_manager_guid = local.sm_guid + cr_namespace_name = var.cr_namespace_name + region = local.sm_region + #tfsec:ignore:general-secrets-no-plaintext-exposure + service_id_secret_name = "${var.prefix}-image-pull-service-id-chain-sec-1" + service_id_secret_group_id = module.secrets_manager_group.secret_group_id + depends_on = [module.iam_secrets_engine, module.secrets_manager_group] + providers = { + ibm = ibm.ibm-sm + } +} + +# create image pull serviceID and secret and store in secrets manager +module "image_pull_chain_secret_2" { + source = "./imagepull-apikey-secrets-manager" + resource_group_id = module.resource_group.resource_group_id + secrets_manager_guid = local.sm_guid + cr_namespace_name = var.cr_namespace_name + region = local.sm_region + #tfsec:ignore:general-secrets-no-plaintext-exposure + service_id_secret_name = "${var.prefix}-image-pull-service-id-chain-sec-2" + service_id_secret_group_id = module.secrets_manager_group.secret_group_id + depends_on = [module.iam_secrets_engine, module.secrets_manager_group] + providers = { + ibm = ibm.ibm-sm + } +} + +# create image pull serviceID and secret and store in secrets manager +module "image_pull_chain_secret_3" { + source = "./imagepull-apikey-secrets-manager" + resource_group_id = module.resource_group.resource_group_id + secrets_manager_guid = local.sm_guid + cr_namespace_name = var.cr_namespace_name + region = local.sm_region + #tfsec:ignore:general-secrets-no-plaintext-exposure + service_id_secret_name = "${var.prefix}-image-pull-service-id-chain-sec-3" + service_id_secret_group_id = module.secrets_manager_group.secret_group_id + depends_on = [module.iam_secrets_engine, module.secrets_manager_group] + providers = { + ibm = ibm.ibm-sm + } +} + +module "external_secret_secret_image_pull_chain" { + source = "../../modules/eso-external-secret" + depends_on = [module.eso_apikey_namespace_secretstore_2, ] + eso_store_scope = "namespace" + es_kubernetes_secret_type = "dockerconfigjson" # checkov:skip=CKV_SECRET_6 + sm_secret_type = "iam_credentials" + eso_store_name = "${var.es_namespaces_apikey[3]}-store" + es_kubernetes_secret_name = "dockerconfigjson-chain" + es_helm_rls_name = "es-docker-iam-chain" + es_kubernetes_namespace = kubernetes_namespace.apikey_namespaces[3].metadata[0].name + sm_secret_id = null # null is accepted only in the case of a dockerjsonconfig secret with secrets chain + es_container_registry_secrets_chain = [ + { + "es_container_registry" : "test1.icr.com", "sm_secret_id" : module.image_pull_chain_secret_1.serviceid_apikey_secret_id, "es_container_registry_email" : "terraform1@ibm.com" + }, + { + "es_container_registry" : "test2.icr.com", "sm_secret_id" : module.image_pull_chain_secret_2.serviceid_apikey_secret_id, "es_container_registry_email" : null + }, + { + "es_container_registry" : "test3.icr.com", "sm_secret_id" : module.image_pull_chain_secret_3.serviceid_apikey_secret_id, "es_container_registry_email" : "" + } + ] +} diff --git a/examples/all-combined/tpauth_cluster_sstore.tf b/examples/all-combined/tpauth_cluster_sstore.tf new file mode 100644 index 00000000..51b003f9 --- /dev/null +++ b/examples/all-combined/tpauth_cluster_sstore.tf @@ -0,0 +1,102 @@ +#################################################################################### +# clustersecretstore with trusted profile auth +# creation of clustersecretstore, secretsgroup and externalsecrets resource +# for csecretstore supporting authentication by trusted profile +#################################################################################### + +# creating a secrets group for clustersecretstore with trustedprofile auth +module "tp_clusterstore_secrets_manager_group" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = "${var.prefix}-cpstore-tp-secret-group" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group for storing account credentials for clusterstore tp authentication" #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +locals { + cstore_store_name = "cluster-store-tpauth" + cstore_trusted_profile_name = "${var.prefix}-eso-cstore-tp" + cstore_tp_namespace = "eso-cstore-tp-namespace" +} + +# creating trusted profiles for the secrets groups created with module tp_clusterstore_secrets_manager_group +module "external_secrets_clusterstore_trusted_profile" { + source = "../../modules/eso-trusted-profile" + trusted_profile_name = local.cstore_trusted_profile_name + secrets_manager_guid = local.sm_guid + secret_groups_id = [module.tp_clusterstore_secrets_manager_group.secret_group_id] + tp_cluster_crn = module.ocp_base.cluster_crn + trusted_profile_claim_rule_type = "ROKS_SA" + tp_namespace = var.eso_namespace +} + +# creation of the ESO ClusterStore (cluster wide scope) with trustedprofile authentication +module "eso_clusterstore_tpauth" { + source = "../../modules/eso-clusterstore" + eso_authentication = "trusted_profile" + clusterstore_trusted_profile_name = local.cstore_trusted_profile_name + region = local.sm_region + clusterstore_helm_rls_name = "${local.cstore_store_name}-rls" + clusterstore_name = local.cstore_store_name + clusterstore_secrets_manager_guid = local.sm_guid + eso_namespace = var.eso_namespace + service_endpoints = var.service_endpoints + depends_on = [ + module.external_secrets_operator + ] +} + +# arbitrary secret to be synched through the clustersecretstore with TP authentication +module "sm_cstore_arbitrary_secret_tp" { + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.tp_clusterstore_secrets_manager_group.secret_group_id + secret_type = "arbitrary" + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-eso-test-dummy-secret-cstore-tp" #checkov:skip=CKV_SECRET_6 + secret_description = "eso_test_dummy_cstore_secret_tp example secret in existing secret manager instance" #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_payload_password = "dummy_secret_value_eso_test_dummy_cstore_secret_tp" # pragma: allowlist secret + providers = { + ibm = ibm.ibm-sm + } +} + +# Creating the namespaces for TP authentication clusterstore to store the secrets +resource "kubernetes_namespace" "clusterstore_tpauth_secrets_namespace" { + metadata { + name = local.cstore_tp_namespace + } + lifecycle { + ignore_changes = [ + metadata[0].annotations, + metadata[0].labels + ] + } + depends_on = [ + time_sleep.wait_45_seconds + ] +} + +# eso externalsecret object with clustersecretstore trusted profiles authentication +module "cstore_external_secret_tp" { + depends_on = [module.eso_clusterstore_tpauth] + source = "../../modules/eso-external-secret" + eso_store_scope = "cluster" + es_kubernetes_namespace = kubernetes_namespace.clusterstore_tpauth_secrets_namespace.metadata[0].name + es_kubernetes_secret_name = "${var.prefix}-arbitrary-arb-cstore-tp" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "arbitrary" #checkov:skip=CKV_SECRET_6 + sm_secret_id = module.sm_cstore_arbitrary_secret_tp.secret_id #checkov:skip=CKV_SECRET_6 + es_kubernetes_secret_type = "opaque" + es_kubernetes_secret_data_key = "apikey" + es_refresh_interval = "5m" + eso_store_name = local.cstore_store_name # each store created with the name of the namespace with "-store" as suffix + es_container_registry = "us.icr.io" + es_container_registry_email = "user@company.com" + es_helm_rls_name = "es-tp" +} diff --git a/examples/all-combined/tpauth_namespaced_sstore.tf b/examples/all-combined/tpauth_namespaced_sstore.tf new file mode 100644 index 00000000..0f52f9aa --- /dev/null +++ b/examples/all-combined/tpauth_namespaced_sstore.tf @@ -0,0 +1,344 @@ +#################################################################################### +# namespaced secretstores with trusted profile auth: +# creation of namespaces, secretstores, secretsgroups and externalsecrets resources +# for namespaced secretstores supporting authentication by trusted profiles +#################################################################################### + +# Create namespaces for trusted profile auth secretsstores +resource "kubernetes_namespace" "tp_namespaces" { + count = length(var.es_namespaces_tp) + metadata { + name = var.es_namespaces_tp[count.index] + } + lifecycle { + ignore_changes = [ + metadata[0].annotations, + metadata[0].labels + ] + } + depends_on = [ + time_sleep.wait_45_seconds + ] +} + +# SecretStore with trustedprofile auth for each namespace +module "eso_tp_namespace_secretstores" { + depends_on = [module.external_secrets_operator] + count = length(var.es_namespaces_tp) + source = "../../modules/eso-secretstore" + eso_authentication = "trusted_profile" + region = local.sm_region + sstore_namespace = kubernetes_namespace.tp_namespaces[count.index].metadata[0].name + sstore_secrets_manager_guid = local.sm_guid + sstore_store_name = "${var.es_namespaces_tp[count.index]}-store" # each store created with the name of the namespace with "-store" as suffix + sstore_trusted_profile_name = module.external_secrets_trusted_profiles[count.index].trusted_profile_name + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store-${count.index}" + sstore_secret_name = "secretstore-tp-${count.index}" #checkov:skip=CKV_SECRET_6 +} + +# creating a secrets group for each namespace to be used for namespaced secretstores with trustedprofile auth +module "tp_secrets_manager_groups" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + count = length(var.es_namespaces_tp) + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = "${var.prefix}-tp-secret-group-${count.index}" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group for storing account credentials for tp authentication" #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +# creating trusted profiles for the secrets groups created with module tp_secrets_manager_groups +module "external_secrets_trusted_profiles" { + count = length(var.es_namespaces_tp) + source = "../../modules/eso-trusted-profile" + trusted_profile_name = "${var.prefix}-eso-tp-${count.index}" + secrets_manager_guid = local.sm_guid + secret_groups_id = [module.tp_secrets_manager_groups[count.index].secret_group_id] + tp_cluster_crn = module.ocp_base.cluster_crn + trusted_profile_claim_rule_type = "ROKS_SA" + tp_namespace = var.eso_namespace +} + +# arbitrary secrets in each namespace and each related secrets group +module "sm_arbitrary_secrets_tp" { + count = length(var.es_namespaces_tp) + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.tp_secrets_manager_groups[count.index].secret_group_id + secret_type = "arbitrary" + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-eso-test-dummy-secret-tp-${count.index}" #checkov:skip=CKV_SECRET_6 + secret_description = "eso_test_dummy_secret_tp example secret in existing secret manager instance" #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_payload_password = "dummy_secret_value_eso_test_dummy_secret_tp" # pragma: allowlist secret + providers = { + ibm = ibm.ibm-sm + } +} + +# eso externalsecret object with trusted profiles authentication for each namespace +module "external_secret_tp" { + depends_on = [ + module.eso_tp_namespace_secretstores + ] + count = length(var.es_namespaces_tp) + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_namespace = kubernetes_namespace.tp_namespaces[count.index].metadata[0].name + es_kubernetes_secret_name = "${var.prefix}-arbitrary-arb-tp-${count.index}" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "arbitrary" #checkov:skip=CKV_SECRET_6 + sm_secret_id = module.sm_arbitrary_secrets_tp[count.index].secret_id #checkov:skip=CKV_SECRET_6 + es_kubernetes_secret_type = "opaque" + es_kubernetes_secret_data_key = "apikey" + es_refresh_interval = "5m" + eso_store_name = "${var.es_namespaces_tp[count.index]}-store" # each store created with the name of the namespace with "-store" as suffix + es_container_registry = "us.icr.io" + es_container_registry_email = "user@company.com" + es_helm_rls_name = "es-tp" +} + +###################################################################################################### +# namespaced secretstores with trusted profile authentication and policy bound to multiple secrets groups +###################################################################################################### + +# Create namespace for tp auth with policy for multiple secrets groups +resource "kubernetes_namespace" "tp_namespace_multisg" { + metadata { + name = var.es_namespace_tp_multi_sg + } + lifecycle { + ignore_changes = [ + metadata[0].annotations, + metadata[0].labels + ] + } + depends_on = [ + time_sleep.wait_45_seconds + ] +} + +# SecretStore with trustedprofile auth and TP policy bound to multiple secrets groups +module "eso_tp_namespace_secretstore_multisg" { + depends_on = [module.external_secrets_operator] + source = "../../modules/eso-secretstore" + eso_authentication = "trusted_profile" + region = local.sm_region + sstore_namespace = kubernetes_namespace.tp_namespace_multisg.metadata[0].name + sstore_secrets_manager_guid = local.sm_guid + sstore_store_name = "${var.es_namespace_tp_multi_sg}-store" # each store created with the name of the namespace with "-store" as suffix + sstore_trusted_profile_name = module.external_secrets_trusted_profile_multisg.trusted_profile_name + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store-tp-multisg" + sstore_secret_name = "secretstore-tp-multisg" #checkov:skip=CKV_SECRET_6 +} + +# creating two secrets groups for a single namespace to test trusted profile policy on multiple secrets groups +module "tp_secrets_manager_group_multi_1" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = "${var.prefix}-tp-secret-group-multisg-1" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group n.1 for storing account credentials for tp authentication with multi secrets group policy" #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +module "tp_secrets_manager_group_multi_2" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = "${var.prefix}-tp-secret-group-multisg-21" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group n.2 for storing account credentials for tp authentication with multi secrets group policy" #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +# arbitrary secret for secrets group 1 +module "sm_arbitrary_secret_tp_multisg_1" { + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.tp_secrets_manager_group_multi_1.secret_group_id + secret_type = "arbitrary" + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-eso-test-dummy-secret-tp-multisg-1" #checkov:skip=CKV_SECRET_6 + secret_description = "eso_test_dummy_secret_tp_multisg_1 example secret in existing secret manager instance for tp auth for secrets group n.1" #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_payload_password = "dummy_secret_value_eso_test_dummy_secret_tp" # pragma: allowlist secret + providers = { + ibm = ibm.ibm-sm + } +} + +# arbitrary secret for secrets group 2 +module "sm_arbitrary_secret_tp_multisg_2" { + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.tp_secrets_manager_group_multi_2.secret_group_id + secret_type = "arbitrary" + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-eso-test-dummy-secret-tp-multisg-2" #checkov:skip=CKV_SECRET_6 + secret_description = "eso_test_dummy_secret_tp_multisg_2 example secret in existing secret manager instance for tp auth for secrets group n.2" #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_payload_password = "dummy_secret_value_eso_test_dummy_secret_tp" # pragma: allowlist secret + providers = { + ibm = ibm.ibm-sm + } +} + +# creating trusted profile with TP policy bound to multiple secrets groups +module "external_secrets_trusted_profile_multisg" { + source = "../../modules/eso-trusted-profile" + trusted_profile_name = "${var.prefix}-eso-tp-multisg" + secrets_manager_guid = local.sm_guid + secret_groups_id = [module.tp_secrets_manager_group_multi_1.secret_group_id, module.tp_secrets_manager_group_multi_2.secret_group_id] + tp_cluster_crn = module.ocp_base.cluster_crn + trusted_profile_claim_rule_type = "ROKS_SA" + tp_namespace = var.eso_namespace +} + +# first eso externalsecret object with trusted profile authentication and TP policy bound to multiple secrets groups +module "external_secret_tp_multisg_1" { + depends_on = [ + module.eso_tp_namespace_secretstore_multisg + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_namespace = kubernetes_namespace.tp_namespace_multisg.metadata[0].name + es_kubernetes_secret_name = "${var.prefix}-arbitrary-arb-tp-multisg-1" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "arbitrary" #checkov:skip=CKV_SECRET_6 + sm_secret_id = module.sm_arbitrary_secret_tp_multisg_1.secret_id #checkov:skip=CKV_SECRET_6 + es_kubernetes_secret_type = "opaque" + es_kubernetes_secret_data_key = "apikey" + es_refresh_interval = "5m" + eso_store_name = "${var.es_namespace_tp_multi_sg}-store" # each store created with the name of the namespace with "-store" as suffix + es_container_registry = "us.icr.io" + es_container_registry_email = "user@company.com" + es_helm_rls_name = "es-tp-multisg-1" +} + +# second eso externalsecret object with trusted profile authentication and TP policy bound to multiple secrets groups +module "external_secret_tp_multisg_2" { + depends_on = [ + module.eso_tp_namespace_secretstore_multisg + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_namespace = kubernetes_namespace.tp_namespace_multisg.metadata[0].name + es_kubernetes_secret_name = "${var.prefix}-arbitrary-arb-tp-multisg-2" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "arbitrary" #checkov:skip=CKV_SECRET_6 + sm_secret_id = module.sm_arbitrary_secret_tp_multisg_2.secret_id #checkov:skip=CKV_SECRET_6 + es_kubernetes_secret_type = "opaque" + es_kubernetes_secret_data_key = "apikey" + es_refresh_interval = "5m" + eso_store_name = "${var.es_namespace_tp_multi_sg}-store" # each store created with the name of the namespace with "-store" as suffix + es_container_registry = "us.icr.io" + es_container_registry_email = "user@company.com" + es_helm_rls_name = "es-tp-multisg-2" +} + +###################################################################################################### +# namespaced secretstores with trusted profile authentication and policy not bound to any secrets group +###################################################################################################### + +# Create namespace for trusted profile authentication with policy without secrets group +resource "kubernetes_namespace" "tp_namespace_tpnosg" { + metadata { + name = var.es_namespace_tp_no_sg + } + lifecycle { + ignore_changes = [ + metadata[0].annotations, + metadata[0].labels + ] + } + depends_on = [ + time_sleep.wait_45_seconds + ] +} + +# SecretStore with trusted profile authentication and TP policy without secrets group +module "eso_tp_namespace_secretstore_nosecgroup" { + depends_on = [module.external_secrets_operator] + source = "../../modules/eso-secretstore" + eso_authentication = "trusted_profile" + region = local.sm_region + sstore_namespace = kubernetes_namespace.tp_namespace_tpnosg.metadata[0].name + sstore_secrets_manager_guid = local.sm_guid + sstore_store_name = "${var.es_namespace_tp_no_sg}-store" # each store created with the name of the namespace with "-store" as suffix + sstore_trusted_profile_name = module.external_secrets_trusted_profile_nosecgroup.trusted_profile_name + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store-tp-nosg" + sstore_secret_name = "secretstore-tp-nosg" #checkov:skip=CKV_SECRET_6 +} + +# creating secrets group for a single namespace to test trusted profile policy without any secret group in the TP policy +module "tp_secrets_manager_group_not_for_policy" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_name = "${var.prefix}-tp-secret-group-not-for-policy" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group for storing account credentials for tp authentication not to be added to the TP policy" #tfsec:ignore:general-secrets-no-plaintext-exposure + providers = { + ibm = ibm.ibm-sm + } +} + +# arbitrary secret to use with external secret with auth using TP and policy not restricted to secrets group +module "sm_arbitrary_secret_tp_nosecgroup" { + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.tp_secrets_manager_group_not_for_policy.secret_group_id + secret_type = "arbitrary" + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-eso-test-dummy-secret-tp-nosecgroup" #checkov:skip=CKV_SECRET_6 + secret_description = "eso_test_dummy_secret_tp_nosecgroup example secret in existing secret manager instance for tp auth for secrets group not in TP policy" #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_payload_password = "dummy_secret_value_eso_test_dummy_secret_tp" # pragma: allowlist secret + providers = { + ibm = ibm.ibm-sm + } +} + +# creating trusted profile with policy not restricted to secrets groups +module "external_secrets_trusted_profile_nosecgroup" { + source = "../../modules/eso-trusted-profile" + trusted_profile_name = "${var.prefix}-eso-tp-nosecgroup" + secrets_manager_guid = local.sm_guid + secret_groups_id = [] + tp_cluster_crn = module.ocp_base.cluster_crn + trusted_profile_claim_rule_type = "ROKS_SA" + tp_namespace = var.eso_namespace +} + +# eso externalsecret object with trusted profile authentication without secrects group in the policy +module "external_secret_tp_nosg" { + depends_on = [ + module.eso_tp_namespace_secretstore_nosecgroup + ] + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_namespace = kubernetes_namespace.tp_namespace_tpnosg.metadata[0].name + es_kubernetes_secret_name = "${var.prefix}-arbitrary-arb-tp-nosg" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "arbitrary" #checkov:skip=CKV_SECRET_6 + sm_secret_id = module.sm_arbitrary_secret_tp_nosecgroup.secret_id #checkov:skip=CKV_SECRET_6 + es_kubernetes_secret_type = "opaque" + es_kubernetes_secret_data_key = "apikey" + es_refresh_interval = "5m" + eso_store_name = "${var.es_namespace_tp_no_sg}-store" # each store created with the name of the namespace with "-store" as suffix + es_container_registry = "us.icr.io" + es_container_registry_email = "user@company.com" + es_helm_rls_name = "es-tp-nosg" +} diff --git a/examples/all-combined/variables.tf b/examples/all-combined/variables.tf new file mode 100644 index 00000000..f9449a8a --- /dev/null +++ b/examples/all-combined/variables.tf @@ -0,0 +1,357 @@ +####################################################################### +# Generic +####################################################################### + +variable "prefix" { + description = "Prefix for name of all resource created by this example" + type = string + default = "eso-example" +} + +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" + } + ] +} + + +## Image-pull module +variable "sm_iam_secret_name" { + type = string + description = "Name of SM IAM secret (dynamic ServiceID API Key) to be created" + default = "sm-iam-secret-puller" #tfsec:ignore:general-secrets-no-plaintext-exposure +} + +variable "sm_service_plan" { + type = string + description = "Secrets-Manager trial plan" + default = "trial" +} + +variable "cr_namespace_name" { + type = string + description = "Container registry namespace name to be configured in IAM policy." + default = "cr-namespace" +} + +## ESO Module + +variable "eso_namespace" { + type = string + description = "Namespace to deploy the External secrets Operator into" + default = "es-operator" +} + +variable "es_namespaces_apikey" { + type = list(string) + description = "Namespace(s) where secrets and secretstore will be created for apikey auth" + default = ["apikeynspace1", "apikeynspace2", "apikeynspace3", "apikeynspace4"] +} + +variable "es_namespaces_tp" { + type = list(string) + description = "Namespace(s) for the secrets synched through trusted profile authentication." + default = ["tpnspace1", "tpnspace2"] +} + +variable "es_namespace_tp_multi_sg" { + type = string + description = "Namespace for the secrets synched through trusted profile authentication and TP policy with multiple secrets groups policy." + default = "tpns-multisg" +} + +variable "es_namespace_tp_no_sg" { + type = string + description = "Namespace for the secret synched through trusted profile authentication and TP policy without secrets groups." + default = "tpns-nosg" +} + +variable "es_refresh_interval" { + description = "Specify interval for es secret synchronization" + default = "1h" + type = string +} + +variable "existing_sm_instance_guid" { + type = string + description = "Existing Secrets Manager GUID. If not provided a new instance will be provisioned" + default = null +} + +variable "existing_sm_instance_crn" { + type = string + description = "Existing Secrets Manager CRN. If existing_sm_instance_guid is provided, also this input must be provided." + default = null +} + +variable "existing_sm_instance_region" { + type = string + description = "Existing Secrets Manager Region. Required if value is passed into var.existing_instance_guid." + default = null +} + +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 = "public" +} + +## public certificate secret configuration +variable "skip_iam_authorization_policy" { + type = bool + default = false + description = "To skip the CIS IAM policy creation. To set to true if already exists (i.e. if using an existing SM instance)" +} + +variable "cert_common_name" { + description = "Public certificate common name" + type = string + default = "example.com" +} + +variable "ca_name" { + type = string + description = "Secret Managers certificate authority name. If null it will be set to [prefix value]-project-ca" + default = null +} + +variable "dns_provider_name" { + type = string + description = "Secret Managers DNS provider name. If null it will be set to [prefix value]-project-dns" + default = null +} + +variable "public_certificate_bundle" { + description = "Flag to enable the certificate bundle. If enabled the intermediate certificate is expected to be bundled with public, otherwise the template considers the intermediate field explicitly" + type = bool + default = true +} + +variable "acme_letsencrypt_private_key_sm_id" { + type = string + description = "Secrets Manager id where the Acme Lets Encrypt private key for certificate authority is stored" + default = null +} + +variable "acme_letsencrypt_private_key_secret_id" { + type = string + description = "Secret id for the Acme Lets Encrypt private key for certificate authority stored in Secrets Manager" + default = null +} + +variable "acme_letsencrypt_private_key_sm_region" { + type = string + description = "Region of the Secrets Manager id where the Acme Lets Encrypt private key for certificate authority is stored" + default = null +} + +variable "acme_letsencrypt_private_key" { + type = string + description = "Acme Lets Encrypt private key for certificate authority" + default = null +} + +# imported certificate +variable "imported_certificate_sm_id" { + type = string + default = null + description = "Secrets Manager instance id where the components for the intermediate certificate are stored" +} + +variable "imported_certificate_sm_region" { + type = string + default = null + description = "Region of the Secrets Manager instance where the components for the intermediate certificate are stored" +} + +variable "imported_certificate_intermediate_secret_id" { + type = string + default = null + description = "Secret id where the intermediate certificate for the imported certificate is stored" +} + +variable "imported_certificate_public_secret_id" { + type = string + default = null + description = "Secret id where the public certificate for the imported certificate is stored" +} + +variable "imported_certificate_private_secret_id" { + type = string + default = null + description = "Secret id where the private key for the imported certificate is stored" +} + +variable "existing_cis_instance_name" { + type = string + description = "Existing CIS instance name to create the dns configuration for the public certificate" + nullable = false +} + +variable "existing_cis_instance_resource_group_id" { + type = string + description = "Resource group ID of the existing CIS instance name to create the dns configuration for the public certificate" + nullable = false +} + +### private certificate secret configuration +variable "pvt_cert_common_name" { + description = "Private certificate common name" + type = string + default = "example.com" +} + +variable "pvt_ca_name" { + type = string + description = "Secret Managers certificate authority name. If null it will be set to pvt-[prefix]-project-root-ca" + default = null +} + +variable "pvt_root_ca_common_name" { + type = string + description = "Root CA common name for the private certificate" + default = "example.com" +} + +variable "pvt_ca_max_ttl" { + type = string + description = "Private certificate CA max TTL" + default = "8760h" +} + +variable "pvt_certificate_template_name" { + type = string + description = "Template name for the private certificate to create" + default = null +} diff --git a/examples/all-combined/version.tf b/examples/all-combined/version.tf new file mode 100644 index 00000000..5cc9cc6a --- /dev/null +++ b/examples/all-combined/version.tf @@ -0,0 +1,25 @@ +terraform { + required_version = ">= 1.1.0" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.16.1" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.11.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.9.1" + } + ibm = { + source = "IBM-Cloud/ibm" + version = ">= 1.62.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.2.1, < 4.0.0" + } + } +} diff --git a/examples/basic/README.md b/examples/basic/README.md index 86eab8eb..23cd7bec 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,11 +1,24 @@ -# Basic example +# Basic Example - +This module provides a basic example to deploy the External Secrets Operator along with a simple username-password type secret in an IBM Cloud environment. It showcases a comprehensive implementation for managing secrets within a Kubernetes cluster, leveraging IBM Cloud's capabilities for a secure and efficient secret management system. -An end-to-end basic example that will provision the following: -- A new resource group if one is not passed in. -- A new Cloud Object Storage instance. +## Actions Performed + +- **Resource Group Handling**: Loads an existing resource group or creates a new one based on the provided variables. + +- **VPC and Subnet Configuration**: Establishes a Virtual Private Cloud (VPC) with associated subnets, setting up network segmentation and ACL rules. + +- **OpenShift Cluster Provisioning**: Deploys an OpenShift (OCP) cluster, tailored for a cloud-native architecture with default worker pools . + +- **Secrets Manager Integration**: + - Either utilizes an existing Secrets Manager instance or creates a new one. + - Configures IAM engine, policies, and secret groups to manage access and operations on secrets. + +- **External Secrets Operator Configuration**: + - Deploys the External Secrets Operator in the Kubernetes cluster. + - Includes configurations for the External Secrets Operator to interact with the Secrets Manager and manage secrets at cluster and namespace levels. + +- **Secret Management**: + - Sets up a service ID (secret-puller) with IAM policies for accessing secrets from the Secrets Manager. + - Configures various types of secrets, including IAM service ID API keys and username-password combinations. + - Demonstrates the deployment of external secrets within Kubernetes, utilizing the configured `ClusterSecretStore` and `SecretStore` instances. diff --git a/examples/basic/main.tf b/examples/basic/main.tf index 95cc2a68..6f6e4cf7 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -1,6 +1,31 @@ -######################################################################################################################## -# Resource group -######################################################################################################################## +############################################################################## +# Locals +############################################################################## + +locals { + + # general + validate_sm_region_cnd = var.existing_sm_instance_guid != null && var.existing_sm_instance_region == null + validate_sm_region_msg = "existing_sm_instance_region must also be set when value given for existing_sm_instance_guid." + # tflint-ignore: terraform_unused_declarations + validate_sm_region_chk = regex( + "^${local.validate_sm_region_msg}$", + (!local.validate_sm_region_cnd + ? local.validate_sm_region_msg + : "")) + + sm_guid = var.existing_sm_instance_guid == null ? ibm_resource_instance.secrets_manager[0].guid : var.existing_sm_instance_guid + + + sm_region = var.existing_sm_instance_region == null ? var.region : var.existing_sm_instance_region + sm_acct_id = var.existing_sm_instance_guid == null ? module.iam_secrets_engine[0].acct_secret_group_id : module.secrets_manager_group_acct[0].secret_group_id + es_namespace_apikey = "es-operator" # pragma: allowlist secret + eso_namespace = "apikeynspace1" +} + +################################################################## +# Resource Group +################################################################## module "resource_group" { source = "terraform-ibm-modules/resource-group/ibm" @@ -10,15 +35,406 @@ module "resource_group" { existing_resource_group_name = var.resource_group } -######################################################################################################################## -# COS instance -######################################################################################################################## +################################################################## +# 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 = "REDHAT_8_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 -resource "ibm_resource_instance" "cos_instance" { - name = "${var.prefix}-cos" + base_cidr_block = each.value + + networks = null_resource.subnet_mappings[*].triggers +} + +module "vpc" { + source = "terraform-ibm-modules/vpc/ibm" + version = "1.5.0" + 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.0" + 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.0" + 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.0" + 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 - service = "cloud-object-storage" - plan = "standard" - location = "global" +} + +module "security_group" { + source = "terraform-ibm-modules/vpc/ibm//modules/security-group" + version = "1.5.0" + 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.0" + 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.35.10" + 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 + import_default_worker_pool_on_create = false +} + + +############################################################################## +# Init cluster config for helm and kubernetes providers +############################################################################## + +data "ibm_container_cluster_config" "cluster_config" { + cluster_name_id = module.ocp_base.cluster_id + resource_group_id = module.resource_group.resource_group_id +} + +# Wait time to allow cluster refreshes components after provisioning +resource "time_sleep" "wait_45_seconds" { + depends_on = [data.ibm_container_cluster_config.cluster_config] + create_duration = "45s" +} + +# Create namespace for apikey auth +resource "kubernetes_namespace" "apikey_namespace" { + + metadata { + name = local.es_namespace_apikey + } + lifecycle { + ignore_changes = [ + metadata[0].annotations, + metadata[0].labels + ] + } + depends_on = [ + time_sleep.wait_45_seconds + ] +} + +######################################## +# Secrets-Manager and IAM configuration +######################################## + +# IAM user policy, Secret Manager instance, Service ID for IAM engine, IAM service ID policies, associated Service ID API key stored in a secret object in account level secret-group and IAM engine configuration +resource "ibm_resource_instance" "secrets_manager" { + count = var.existing_sm_instance_guid == null ? 1 : 0 + name = "${var.prefix}-sm" + service = "secrets-manager" + plan = var.sm_service_plan + location = local.sm_region tags = var.resource_tags + resource_group_id = module.resource_group.resource_group_id + timeouts { + create = "30m" # Extending provisioning time to 30 minutes + } + provider = ibm.ibm-sm +} + +# Additional Secrets-Manager Secret-Group for SERVICE level secrets +module "secrets_manager_group_acct" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + count = var.existing_sm_instance_guid == null ? 0 : 1 + region = local.sm_region + secrets_manager_guid = local.sm_guid + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_group_name = "${var.prefix}-account-secret-group" #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + secret_group_description = "Secret-Group for storing account credentials" #tfsec:ignore:general-secrets-no-plaintext-exposure + depends_on = [module.iam_secrets_engine] + providers = { + ibm = ibm.ibm-sm + } +} + +# Configure instance with IAM engine +module "iam_secrets_engine" { + count = var.existing_sm_instance_guid == null ? 1 : 0 + source = "terraform-ibm-modules/secrets-manager-iam-engine/ibm" + version = "1.2.6" + region = local.sm_region + secrets_manager_guid = ibm_resource_instance.secrets_manager[0].guid + iam_secret_generator_service_id_name = "${var.prefix}-sid:0.0.1:${ibm_resource_instance.secrets_manager[0].name}-iam-secret-generator:automated:simple-service:secret-manager:" + iam_secret_generator_apikey_name = "${var.prefix}-iam-secret-generator-apikey" + new_secret_group_name = "${var.prefix}-account-secret-group" + iam_secret_generator_apikey_secret_name = "${var.prefix}-iam-secret-generator-apikey-secret" + iam_engine_name = "iam-engine" + providers = { + ibm = ibm.ibm-sm + } +} + +################################################################## +# Create service-id, policy to pull secrets from secret manager +################################################################## + +# Create service-id +resource "ibm_iam_service_id" "secret_puller" { + name = "sid:0.0.1:${var.prefix}-secret-puller:automated:simple-service:secret-manager:" + description = "ServiceID that can pull secrets from Secret Manager" +} + +# Create policy to allow new service id to pull secrets from secrets manager +resource "ibm_iam_service_policy" "secret_puller_policy" { + iam_service_id = ibm_iam_service_id.secret_puller.id + roles = ["Viewer", "SecretsReader"] + + resources { + service = "secrets-manager" + resource_instance_id = local.sm_guid + resource_type = "secret-group" + resource = local.sm_acct_id + } +} + +################################################################## +# ESO deployment +################################################################## + +module "external_secrets_operator" { + source = "../../" + eso_namespace = local.eso_namespace + depends_on = [ + kubernetes_namespace.apikey_namespace + ] +} +# +## Create dynamic Service ID API key and add to secret manager +module "dynamic_serviceid_apikey1" { + 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 = "${var.prefix}-${var.sm_iam_secret_name}" + sm_iam_secret_description = "Example of dynamic IAM secret / apikey" #tfsec:ignore:general-secrets-no-plaintext-exposure + serviceid_id = ibm_iam_service_id.secret_puller.id + secrets_manager_guid = local.sm_guid + secret_group_id = local.sm_acct_id + depends_on = [module.iam_secrets_engine, ibm_iam_service_policy.secret_puller_policy, ibm_iam_service_id.secret_puller] + providers = { + ibm = ibm.ibm-sm + } +} + +## Data source to get API Key from secret manager secret-puller-secret +data "ibm_sm_iam_credentials_secret" "secret_puller_secret" { + instance_id = local.sm_guid + #checkov:skip=CKV_SECRET_6: does not require high entropy string as is static type + secret_id = module.dynamic_serviceid_apikey1.secret_id + provider = ibm.ibm-sm +} + +################################################################## +# ESO ClusterStore creation with apikey authentication +################################################################## +module "eso_clusterstore" { + source = "../../modules/eso-clusterstore" + eso_authentication = "api_key" + clusterstore_secret_apikey = data.ibm_sm_iam_credentials_secret.secret_puller_secret.api_key + region = local.sm_region + clusterstore_helm_rls_name = "cluster-store" + clusterstore_secret_name = "generic-cluster-api-key" #checkov:skip=CKV_SECRET_6 + clusterstore_name = "cluster-store" + clusterstore_secrets_manager_guid = local.sm_guid + eso_namespace = local.eso_namespace + service_endpoints = "public" + depends_on = [ + module.external_secrets_operator, + ] +} + +################################################################## +# 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" { + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = local.sm_acct_id + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-usernamepassword-secret" # checkov:skip=CKV_SECRET_6 + secret_description = "example secret in existing secret manager instance" #tfsec:ignore:general-secrets-no-plaintext-exposure # checkov:skip=CKV_SECRET_6 + secret_payload_password = local.userpass_apikey + 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" { + depends_on = [module.external_secrets_operator] + 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 = module.sm_userpass_secret.secret_id + es_kubernetes_namespace = kubernetes_namespace.apikey_namespace.metadata[0].name + eso_store_name = "cluster-store" + 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" + reloader_watching = true } diff --git a/examples/basic/outputs.tf b/examples/basic/outputs.tf index 04b196e9..e124314b 100644 --- a/examples/basic/outputs.tf +++ b/examples/basic/outputs.tf @@ -1,18 +1,7 @@ -######################################################################################################################## +############################################################################## # Outputs -######################################################################################################################## - -output "cos_instance_id" { - description = "COS instance id" - value = ibm_resource_instance.cos_instance.id -} - -output "resource_group_name" { - description = "Resource group name" - value = module.resource_group.resource_group_name -} - -output "resource_group_id" { - description = "Resource group ID" - value = module.resource_group.resource_group_id +############################################################################## +output "cluster_id" { + description = "ID of the cluster deployed" + value = module.ocp_base.cluster_id } diff --git a/examples/basic/provider.tf b/examples/basic/provider.tf index 84b69850..8d6884a3 100644 --- a/examples/basic/provider.tf +++ b/examples/basic/provider.tf @@ -1,8 +1,25 @@ -######################################################################################################################## -# Provider config -######################################################################################################################## - provider "ibm" { ibmcloud_api_key = var.ibmcloud_api_key region = var.region } + +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key + region = var.existing_sm_instance_region == null ? var.region : var.existing_sm_instance_region + alias = "ibm-sm" +} + +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/examples/basic/variables.tf b/examples/basic/variables.tf index dd0d0af9..e2c036cd 100644 --- a/examples/basic/variables.tf +++ b/examples/basic/variables.tf @@ -1,33 +1,171 @@ -######################################################################################################################## -# Input variables -######################################################################################################################## +####################################################################### +# Generic +####################################################################### -variable "ibmcloud_api_key" { +variable "prefix" { + description = "Prefix for name of all resource created by this example" type = string - description = "The IBM Cloud API Key" - sensitive = true + default = "eso-example-basic" } variable "region" { type = string - description = "Region to provision all resources created by this example" + description = "Region where resources will be created." default = "us-south" } -variable "prefix" { +variable "ibmcloud_api_key" { type = string - description = "Prefix to append to all resources created by this example" - default = "basic" + 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 = "The name of an existing resource group to provision resources in to. If not set a new resource group will be created using the prefix variable" + 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 = [] } + +## Image-pull module +variable "sm_iam_secret_name" { + type = string + description = "Name of SM IAM secret (dynamic ServiceID API Key) to be created" + default = "sm-iam-secret-puller" #tfsec:ignore:general-secrets-no-plaintext-exposure +} + +variable "sm_service_plan" { + type = string + description = "Secrets-Manager trial plan" + default = "trial" +} + +## ESO Module +variable "existing_sm_instance_guid" { + type = string + description = "Existing Secrets Manager GUID. If not provided a new instance will be provisioned" + default = null +} + +variable "existing_sm_instance_region" { + type = string + description = "Existing Secrets Manager Region. Required if value is passed into var.existing_instance_guid." + default = null +} + +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/examples/basic/version.tf b/examples/basic/version.tf index 2b99b89d..a4b04903 100644 --- a/examples/basic/version.tf +++ b/examples/basic/version.tf @@ -1,12 +1,25 @@ terraform { - required_version = ">= 1.3.0" - - # Ensure that there is always 1 example locked into the lowest provider version of the range defined in the main - # module's version.tf (usually a basic example), and 1 example that will always use the latest provider version. + required_version = ">= 1.0.0" required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "= 2.16.1" + } + helm = { + source = "hashicorp/helm" + version = "= 2.11.0" + } + time = { + source = "hashicorp/time" + version = "= 0.9.1" + } ibm = { source = "IBM-Cloud/ibm" - version = "1.49.0" + version = "= 1.70.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.2.1, < 4.0.0" } } } diff --git a/examples/trusted-profiles-authentication/README.md b/examples/trusted-profiles-authentication/README.md new file mode 100644 index 00000000..4fdeb95f --- /dev/null +++ b/examples/trusted-profiles-authentication/README.md @@ -0,0 +1,18 @@ +# Example that uses trusted profiles (container authentication) + +This example shows how external secrets can be configured to use trusted profile with compute resource-based authentication. + +Resources with trusted profiles with compute resource can authenticate with IAM tokens that are generated by the compute resources. This type of authentication avoids key-related operations, such as rotation. + +The example shows also how to configure external secrets with trusted profiles in a multitenant configuration, by creating two different trusted profiles, the related external secrets resources, the stores and deploying the ESO into the cluster + +This example includes the following operations: + +- Creates a Secrets Manager instance and two different secrets groups for each tenant (represented by tenant namespace) +- Creates a trusted profile to access the created instance and secret group with secrets reader-only access, for each tenant +- Installs and configures external secrets operator +- Creates a secret store with trusted profile container authentication for each tenant + +In addiction to the mentioned details, this example includes guidance on: +- customising the deployment of external secrets operator on specific cluster's worker nodes: variable `eso_deployment_nodes_configuration` allows to configure nodeSelector and tolerations setting for the ESO deployment +- creating Virtual Private Endpoints (VPE) towards IAM and Secrets Manager private endpoints in the case of `service_endpoints` set to **private** diff --git a/examples/trusted-profiles-authentication/main.tf b/examples/trusted-profiles-authentication/main.tf new file mode 100644 index 00000000..347c4554 --- /dev/null +++ b/examples/trusted-profiles-authentication/main.tf @@ -0,0 +1,217 @@ +module "resource_group" { + source = "terraform-ibm-modules/resource-group/ibm" + version = "1.1.6" + # 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 +} + +locals { + sm_service_plan = "trial" + trusted_profile_name = "${var.prefix}-eso-tp" + secret_manager_instance_name = "${var.prefix}-sm-instance" #checkov:skip=CKV_SECRET_6 + secret_group_name = "${var.prefix}-sm-secret-group" #checkov:skip=CKV_SECRET_6 + es_kubernetes_namespaces = ["${var.prefix}-tp-test-1", "${var.prefix}-tp-test-2"] # namespace to create the externalsecrets resources for secrets sync + + validate_sm_region_cnd = var.existing_sm_instance_guid != null && var.existing_sm_instance_region == null + validate_sm_region_msg = "existing_sm_instance_region must also be set when value given for existing_sm_instance_guid." + # tflint-ignore: terraform_unused_declarations + validate_sm_region_chk = regex( + "^${local.validate_sm_region_msg}$", + (!local.validate_sm_region_cnd + ? local.validate_sm_region_msg + : "")) + + # validation for secrets manager crn to be set for existing secrets manager instance if using private service endpoints + validate_sm_crn_cnd = var.existing_sm_instance_guid != null && var.existing_sm_instance_crn == null && var.service_endpoints == "private" + validate_sm_crn_msg = "existing_sm_instance_crn must also be set when value given for existing_sm_instance_guid if service_endpoints is private." + # tflint-ignore: terraform_unused_declarations + validate_sm_crn_chk = regex( + "^${local.validate_sm_crn_msg}$", + (!local.validate_sm_crn_cnd + ? local.validate_sm_crn_msg + : "")) + + sm_guid = var.existing_sm_instance_guid == null ? ibm_resource_instance.secrets_manager[0].guid : var.existing_sm_instance_guid + # if service_endpoints is not private the crn for SM is not needed because of VPE creation is not needed + sm_crn = var.existing_sm_instance_crn == null ? (var.service_endpoints == "private" ? ibm_resource_instance.secrets_manager[0].crn : "") : var.existing_sm_instance_crn + + sm_region = var.existing_sm_instance_region == null ? var.region : var.existing_sm_instance_region + +} + +############################################################################## +## Create prerequisite. Secrets Manager, Secret Group and a Trusted Profile +############################################################################## + +resource "ibm_resource_instance" "secrets_manager" { + count = var.existing_sm_instance_guid == null ? 1 : 0 + name = local.secret_manager_instance_name + service = "secrets-manager" + plan = local.sm_service_plan + location = local.sm_region + resource_group_id = module.resource_group.resource_group_id + timeouts { + create = "20m" # Extending provisioning time to 20 minutes + } +} + +## Secret Group for organizing secrets + +module "secrets_manager_groups" { + source = "terraform-ibm-modules/secrets-manager-secret-group/ibm" + version = "1.2.2" + count = length(kubernetes_namespace.examples) + region = local.sm_region + secrets_manager_guid = local.sm_guid + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_group_name = "${local.secret_group_name}_${count.index}" + secret_group_description = "secret ${count.index} used for examples" #tfsec:ignore:general-secrets-no-plaintext-exposure +} + +# We retrieve metadata of the cluster to get the cluster's CRN. +# The CRN then will be used in the Trusted Profile Rule +data "ibm_container_vpc_cluster" "cluster" { + name = var.cluster_name_id +} + +################################################################## +## Example creating two arbitrary dummy secrets for test +################################################################## + +# creating the namespace to create the ES resources and the dummy secrets +resource "kubernetes_namespace" "examples" { + count = length(local.es_kubernetes_namespaces) + metadata { + name = local.es_kubernetes_namespaces[count.index] + } +} + +module "sm_arbitrary_secrets" { + count = length(kubernetes_namespace.examples) + source = "terraform-ibm-modules/secrets-manager-secret/ibm" + version = "1.4.0" + region = local.sm_region + secrets_manager_guid = local.sm_guid + secret_group_id = module.secrets_manager_groups[count.index].secret_group_id + secret_type = "arbitrary" + #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_name = "${var.prefix}-eso-test-dummy-secret-${count.index}" #checkov:skip=CKV_SECRET_6 + secret_description = "# ${count.index} example secret in existing secret manager instance" #tfsec:ignore:general-secrets-no-plaintext-exposure + secret_payload_password = "dummy_secret_value_${count.index}" # pragma: allowlist secret +} + +# creating trusted profiles +module "external_secrets_trusted_profiles" { + count = length(kubernetes_namespace.examples) + source = "../../modules/eso-trusted-profile" + trusted_profile_name = "${local.trusted_profile_name}_${count.index}" + secrets_manager_guid = local.sm_guid + secret_groups_id = [module.secrets_manager_groups[count.index].secret_group_id] + tp_cluster_crn = data.ibm_container_vpc_cluster.cluster.crn + trusted_profile_claim_rule_type = "ROKS_SA" + tp_namespace = var.eso_namespace +} + +######################################################################## +## Deploying ESO +######################################################################## + +module "external_secrets_operator" { + source = "../../" + eso_namespace = var.eso_namespace + eso_cluster_nodes_configuration = var.eso_deployment_nodes_configuration == null ? null : { + nodeSelector = { + label = "dedicated" + value = var.eso_deployment_nodes_configuration + } + tolerations = { + key = "dedicated" + operator = "Equal" + value = var.eso_deployment_nodes_configuration + effect = "NoExecute" + } + } +} + +######################################################################## +## Deploying ESO SecretStores +######################################################################## + +module "eso_namespace_secretstores" { + count = length(kubernetes_namespace.examples) + depends_on = [ + module.external_secrets_operator + ] + source = "../../modules/eso-secretstore" + eso_authentication = "trusted_profile" + region = local.sm_region + sstore_namespace = kubernetes_namespace.examples[count.index].metadata[0].name + sstore_secrets_manager_guid = local.sm_guid + sstore_store_name = "${kubernetes_namespace.examples[count.index].metadata[0].name}-store" # each store created with the name of the namespace with "-store" as suffix + sstore_trusted_profile_name = module.external_secrets_trusted_profiles[count.index].trusted_profile_name + service_endpoints = var.service_endpoints + sstore_helm_rls_name = "es-store-${count.index}" + sstore_secret_name = "secretstore-api-key" #checkov:skip=CKV_SECRET_6 +} + +###################################### +# creating eso externalsecret objects +###################################### + +module "external_secrets" { + depends_on = [ + module.eso_namespace_secretstores + ] + count = length(kubernetes_namespace.examples) + source = "../../modules/eso-external-secret" + eso_store_scope = "namespace" + es_kubernetes_namespace = kubernetes_namespace.examples[count.index].metadata[0].name + es_kubernetes_secret_name = "${var.prefix}-arbitrary-arb-${count.index}" #checkov:skip=CKV_SECRET_6 + sm_secret_type = "arbitrary" #checkov:skip=CKV_SECRET_6 + sm_secret_id = module.sm_arbitrary_secrets[count.index].secret_id #checkov:skip=CKV_SECRET_6 + es_kubernetes_secret_type = "opaque" + es_kubernetes_secret_data_key = "apikey" + es_refresh_interval = "5m" + eso_store_name = "${kubernetes_namespace.examples[count.index].metadata[0].name}-store" # each store created with the name of the namespace with "-store" as suffix + es_container_registry = "us.icr.io" + es_container_registry_email = "user@company.com" + es_helm_rls_name = "es-${count.index}" +} + +###################################### +# creating VPEs +###################################### + +module "vpes" { + source = "terraform-ibm-modules/vpe-gateway/ibm" + version = "4.3.0" + count = var.service_endpoints == "private" ? 1 : 0 + region = var.region + prefix = "vpe" + vpc_name = var.vpe_vpc_name + vpc_id = var.vpe_vpc_id + subnet_zone_list = [ + for index, subnet in var.vpe_vpc_subnets : { + name = "${var.region}-${index}" + zone = "${var.region}-${index}" + id = subnet + acl_name = "acl" + public_gateway = true + } + ] + resource_group_id = var.vpe_vpc_resource_group_id # pragma: allowlist secret + security_group_ids = [var.vpe_vpc_security_group_id] + cloud_services = [] + cloud_service_by_crn = [ + { + name = "iam-${var.region}" + crn = "crn:v1:bluemix:public:iam-svcs:global:::endpoint:private.iam.cloud.ibm.com" + }, + { + name = "sm-${var.region}" + crn = local.sm_crn + } + ] + service_endpoints = "private" +} diff --git a/examples/advanced/main.tf b/examples/trusted-profiles-authentication/outputs.tf similarity index 89% rename from examples/advanced/main.tf rename to examples/trusted-profiles-authentication/outputs.tf index 558c2107..b16a1a35 100644 --- a/examples/advanced/main.tf +++ b/examples/trusted-profiles-authentication/outputs.tf @@ -1,3 +1,3 @@ ############################################################################## -# Complete example +# Outputs ############################################################################## diff --git a/examples/trusted-profiles-authentication/providers.tf b/examples/trusted-profiles-authentication/providers.tf new file mode 100644 index 00000000..249afe5e --- /dev/null +++ b/examples/trusted-profiles-authentication/providers.tf @@ -0,0 +1,40 @@ +data "ibm_container_cluster_config" "cluster_config" { + cluster_name_id = var.cluster_name_id + resource_group_id = module.resource_group.resource_group_id +} + +provider "ibm" { + ibmcloud_api_key = var.ibmcloud_api_key +} + +data "ibm_iam_auth_token" "token_data" {} + +provider "restapi" { + uri = "https:" + write_returns_object = true + debug = false # set to true to show detailed logs, but use carefully as it might print API key values. + headers = { + Accept = "application/json" + Authorization = data.ibm_iam_auth_token.token_data.iam_access_token + Content-Type = "application/json" + } +} + +provider "kubernetes" { + client_certificate = data.ibm_container_cluster_config.cluster_config.admin_certificate + client_key = data.ibm_container_cluster_config.cluster_config.admin_key + cluster_ca_certificate = data.ibm_container_cluster_config.cluster_config.ca_certificate + host = data.ibm_container_cluster_config.cluster_config.host + token = data.ibm_container_cluster_config.cluster_config.token +} + + +provider "helm" { + kubernetes { + client_certificate = data.ibm_container_cluster_config.cluster_config.admin_certificate + client_key = data.ibm_container_cluster_config.cluster_config.admin_key + cluster_ca_certificate = data.ibm_container_cluster_config.cluster_config.ca_certificate + host = data.ibm_container_cluster_config.cluster_config.host + token = data.ibm_container_cluster_config.cluster_config.token + } +} diff --git a/examples/trusted-profiles-authentication/variables.tf b/examples/trusted-profiles-authentication/variables.tf new file mode 100644 index 00000000..00328482 --- /dev/null +++ b/examples/trusted-profiles-authentication/variables.tf @@ -0,0 +1,95 @@ +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" + sensitive = true +} + +variable "cluster_name_id" { + type = string + description = "Cluser Name or ID where resources will be created" +} + +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 +} + + +variable "prefix" { + description = "Prefix for name of all resource created by this example" + type = string + default = "eso-tp" +} + +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 = "public" +} + +variable "region" { + type = string + description = "Region where resources will be created" + default = "us-south" +} + +variable "existing_sm_instance_guid" { + type = string + description = "Existing Secrets Manager GUID. If not provided a new instance will be provisioned" + default = null +} + +variable "existing_sm_instance_crn" { + type = string + description = "Existing Secrets Manager CRN. If existing_sm_instance_guid is provided, also this input must be provided." + default = null +} + +variable "existing_sm_instance_region" { + type = string + description = "Existing Secrets Manager Region. Required if value is passed into var.existing_sm_instance_guid" + default = null +} + +variable "eso_namespace" { + type = string + description = "Namespace to deploy the External secrets Operator into" + default = "es-operator" +} + +variable "vpe_vpc_name" { + type = string + description = "Name of the VPC where to create the VPE endpoint in the case of private connections. Required only if service_endpoints is private" + default = null +} + +variable "vpe_vpc_id" { + type = string + description = "ID of the VPC where to create the VPE endpoint in the case of private connections. Required only if service_endpoints is private" + default = null +} + +variable "vpe_vpc_subnets" { + type = list(string) + description = "List of VPC subnets to use to create the VPE endpoints. Required only if service_endpoints is private" + default = [] +} + +variable "vpe_vpc_resource_group_id" { + type = string + description = "ID of resource group to use to create the VPE endpoint in the case of private connections (the same of the VPC usually). Required only if service_endpoints is private" + default = null +} + +variable "vpe_vpc_security_group_id" { + type = string + description = "ID of security group to use to create the VPE endpoint in the case of private connections (one of the VPC). Required only if service_endpoints is private" + default = null +} + +variable "eso_deployment_nodes_configuration" { + type = string + description = "Configuration to deploy ESO on specific cluster nodes. The value of this variable will be used for NodeSelector label value and tolerations configuration. If null standard ESO deployment is done." + default = null +} diff --git a/examples/trusted-profiles-authentication/version.tf b/examples/trusted-profiles-authentication/version.tf new file mode 100644 index 00000000..4ab524a2 --- /dev/null +++ b/examples/trusted-profiles-authentication/version.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.16.1" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.8.0" + } + ibm = { + source = "IBM-Cloud/ibm" + version = ">= 1.52.0" + } + restapi = { + source = "Mastercard/restapi" + version = ">= 1.18.0" + } + } +} diff --git a/main.tf b/main.tf index 0b919ea2..d2ae022a 100644 --- a/main.tf +++ b/main.tf @@ -1,3 +1,329 @@ -/******************************************************************** -This file is used to implement the ROOT module. -*********************************************************************/ +############################################################################## +# External Secrets Sync Module +# +# Module for deploying External Secret Operator (ESO) and use it to create and synchronize Kubernetes secrets into clusters based on Secrets-Manager secrets. +############################################################################## + + +# creating namespace to deploy ESO into RedHat ServiceMesh +module "eso_namespace" { + count = var.eso_namespace != null ? 1 : 0 + source = "terraform-ibm-modules/namespace/ibm" + version = "1.0.2" + namespaces = [ + { + name = var.eso_namespace + metadata = { + name = var.eso_namespace + labels = { + } + annotations = { + "istio-injection" = var.eso_enroll_in_servicemesh == true ? "enabled" : null + } + } + } + ] +} + +# loading existing eso namespace +data "kubernetes_namespace" "existing_eso_namespace" { + count = var.existing_eso_namespace != null ? 1 : 0 + metadata { + name = var.existing_eso_namespace + } +} + +locals { + # namespace to use for eso. If both eso_namespace and existing_eso_namespace are not null, eso_namespace takes the precedence + eso_namespace = var.eso_namespace != null ? var.eso_namespace : data.kubernetes_namespace.existing_eso_namespace[0].metadata[0].name +} + +locals { + eso_helm_release_values_cri = <<-EOF +installCRDs: true +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + enabled: true + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: "RuntimeDefault" +podAnnotations: +%{for key, value in var.eso_pod_configuration.annotations.external_secrets} "${key}": "${value}" %{endfor} +%{if var.eso_enroll_in_servicemesh == true} + sidecar.istio.io/inject: "true" + sidecar.istio.io/rewriteAppHTTPProbers: "true" +%{endif} +podLabels: +%{if var.eso_enroll_in_servicemesh == true} app: external-secrets-operator %{endif} +%{for key, value in var.eso_pod_configuration.labels.external_secrets} "${key}": "${value}" %{endfor} +%{if var.eso_enroll_in_servicemesh == true} +extraEnv: +- name: KUBERNETES_SERVICE_HOST + value: kubernetes.default.svc.cluster.local +%{endif} +extraVolumes: +- name: sa-token + projected: + defaultMode: 0644 + sources: + - serviceAccountToken: + path: sa-token + expirationSeconds: 3600 + audience: iam +extraVolumeMounts: +- mountPath: /var/run/secrets/tokens + name: sa-token +webhook: + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + enabled: true + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: "RuntimeDefault" + podAnnotations: + %{for key, value in var.eso_pod_configuration.annotations.external_secrets_webhook} "${key}": "${value}" %{endfor} + %{if var.eso_enroll_in_servicemesh == true} + sidecar.istio.io/inject: "true" + sidecar.istio.io/rewriteAppHTTPProbers: "true" + %{endif} + podLabels: + %{if var.eso_enroll_in_servicemesh == true} app: external-secrets-operator %{endif} + %{for key, value in var.eso_pod_configuration.labels.external_secrets_webhook} "${key}": "${value}" %{endfor} + %{if var.eso_enroll_in_servicemesh == true} + extraEnv: + - name: KUBERNETES_SERVICE_HOST + value: kubernetes.default.svc.cluster.local + %{endif} + extraVolumes: + - name: sa-token + projected: + defaultMode: 0644 + sources: + - serviceAccountToken: + path: sa-token + expirationSeconds: 3600 + audience: iam + extraVolumeMounts: + - mountPath: /var/run/secrets/tokens + name: sa-token +certController: + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + enabled: true + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: "RuntimeDefault" + podAnnotations: + %{for key, value in var.eso_pod_configuration.annotations.external_secrets_cert_controller} "${key}": "${value}" %{endfor} + %{if var.eso_enroll_in_servicemesh == true} + sidecar.istio.io/inject: "true" + sidecar.istio.io/rewriteAppHTTPProbers: "true" + %{endif} + podLabels: + %{if var.eso_enroll_in_servicemesh == true} app: external-secrets-operator %{endif} + %{for key, value in var.eso_pod_configuration.labels.external_secrets_cert_controller} "${key}": "${value}" %{endfor} + %{if var.eso_enroll_in_servicemesh == true} + extraEnv: + - name: KUBERNETES_SERVICE_HOST + value: kubernetes.default.svc.cluster.local + %{endif} +EOF + + eso_helm_release_values_workerselector = var.eso_cluster_nodes_configuration == null ? "" : <<-EOF +nodeSelector: { ${var.eso_cluster_nodes_configuration.nodeSelector.label}: ${var.eso_cluster_nodes_configuration.nodeSelector.value} } +tolerations: +- key: ${var.eso_cluster_nodes_configuration.tolerations.key} + operator: ${var.eso_cluster_nodes_configuration.tolerations.operator} + value: ${var.eso_cluster_nodes_configuration.tolerations.value} + effect: ${var.eso_cluster_nodes_configuration.tolerations.effect} +webhook: + nodeSelector: { ${var.eso_cluster_nodes_configuration.nodeSelector.label}: ${var.eso_cluster_nodes_configuration.nodeSelector.value} } + tolerations: + - key: ${var.eso_cluster_nodes_configuration.tolerations.key} + operator: ${var.eso_cluster_nodes_configuration.tolerations.operator} + value: ${var.eso_cluster_nodes_configuration.tolerations.value} + effect: ${var.eso_cluster_nodes_configuration.tolerations.effect} +certController: + nodeSelector: { ${var.eso_cluster_nodes_configuration.nodeSelector.label}: ${var.eso_cluster_nodes_configuration.nodeSelector.value} } + tolerations: + - key: ${var.eso_cluster_nodes_configuration.tolerations.key} + operator: ${var.eso_cluster_nodes_configuration.tolerations.operator} + value: ${var.eso_cluster_nodes_configuration.tolerations.value} + effect: ${var.eso_cluster_nodes_configuration.tolerations.effect} +EOF +} + +resource "helm_release" "external_secrets_operator" { + depends_on = [module.eso_namespace, data.kubernetes_namespace.existing_eso_namespace] + + name = "external-secrets" + namespace = local.eso_namespace + chart = "external-secrets" + version = var.eso_chart_version + wait = true + repository = var.eso_chart_location + + set { + name = "image.repository" + type = "string" + value = var.eso_image + } + + set { + name = "image.tag" + type = "string" + value = var.eso_image_version + } + + set { + name = "webhook.image.repository" + type = "string" + value = var.eso_image + } + + set { + name = "webhook.image.tag" + type = "string" + value = var.eso_image_version + } + + set { + name = "certController.image.repository" + type = "string" + value = var.eso_image + } + + set { + name = "certController.image.tag" + type = "string" + value = var.eso_image_version + } + + # The following mounts are needed for the CRI based authentication with Trusted Profiles + values = [local.eso_helm_release_values_cri, local.eso_helm_release_values_workerselector] +} + +resource "helm_release" "pod_reloader" { + depends_on = [module.eso_namespace, data.kubernetes_namespace.existing_eso_namespace] + count = var.reloader_deployed == true ? 1 : 0 + name = "reloader" + chart = "reloader" + namespace = local.eso_namespace + repository = var.reloader_chart_location + version = var.reloader_chart_version + wait = true + + # Set the deployment image name and tag + set { + name = "reloader.deployment.image.name" + type = "string" + value = var.reloader_image + } + + set { + name = "reloader.deployment.image.tag" + type = "string" + value = var.reloader_image_version + } + + # Set reload strategy + set { + name = "reloader.reloadStrategy" + type = "string" + value = var.reloader_reload_strategy + } + + # Set namespaces to ignore + dynamic "set" { + for_each = var.reloader_namespaces_to_ignore != null ? [1] : [] + content { + name = "reloader.namespacesToIgnore" + value = var.reloader_namespaces_to_ignore + } + } + + # Set resources to ignore + dynamic "set" { + for_each = var.reloader_resources_to_ignore != null ? [1] : [] + content { + name = "reloader.resourcesToIgnore" + value = var.reloader_resources_to_ignore + } + } + + # Set watchGlobally based on conditions + set { + name = "reloader.watchGlobally" + value = var.reloader_namespaces_selector == null && var.reloader_resource_label_selector == null ? true : false + } + + # Set ignoreSecrets and ignoreConfigMaps + set { + name = "reloader.ignoreSecrets" + value = var.reloader_ignore_secrets + } + + set { + name = "reloader.ignoreConfigMaps" + value = var.reloader_ignore_configmaps + } + + # Set OpenShift and Argo Rollouts options + set { + name = "reloader.isOpenshift" + value = var.reloader_is_openshift + } + # Set runAsUser to null if isOpenShift is true + dynamic "set" { + for_each = var.reloader_is_openshift ? [1] : [] + content { + name = "reloader.deployment.securityContext.runAsUser" + value = "null" + } + } + + set { + name = "reloader.podMonitor.enabled" + value = var.reloader_pod_monitor_metrics + } + dynamic "set" { + for_each = var.reloader_log_format == "json" ? [1] : [] + content { + name = "reloader.logFormat" + value = var.reloader_log_format + } + } + set { + name = "reloader.isArgoRollouts" + value = var.reloader_is_argo_rollouts + } + + # Set reloadOnCreate and syncAfterRestart options + set { + name = "reloader.reloadOnCreate" + value = var.reloader_reload_on_create + } + + set { + name = "reloader.syncAfterRestart" + value = var.reloader_sync_after_restart + } + + # Set the values attribute conditionally + values = var.reloader_custom_values != null ? yamldecode(var.reloader_custom_values) : [] +} diff --git a/modules/eso-clusterstore/README.md b/modules/eso-clusterstore/README.md new file mode 100644 index 00000000..615bb03d --- /dev/null +++ b/modules/eso-clusterstore/README.md @@ -0,0 +1,54 @@ +# ESO Cluster Store Module + +This module allows to configure an [ClusterSecretStore](https://external-secrets.io/latest/api/clustersecretstore/) resource for an ESO secret store with cluster scope, in the desired namespace (the same of the ESO deploymet is a requirement of ESO and it is up to the consumer) and with the desired configurations. + +For more information about ClusterSecretStore resource and about ESO please refer to the ESO documentation available [here](https://external-secrets.io/v0.8.3/guides/introduction/) + +This module supports ClusterSecretStore two authentication configurations to pull/push secrets with the configured Secrets Manager instance: +- apikey authentication +- trusted profile authentication + +For more information about Trusted Profiles refer to the IBM Cloud documentation available [here](https://cloud.ibm.com/docs/account?topic=account-create-trusted-profile&interface=ui) + + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [helm](#requirement\_helm) | >= 2.8.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.16.1, <3.0.0 | + +### Modules + +No modules. + +### Resources + +| Name | Type | +|------|------| +| [helm_release.cluster_secret_store_apikey](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.cluster_secret_store_tp](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [kubernetes_secret.eso_clusterstore_secret](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | + +### Inputs + +| 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 | +| [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 | +| [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 | + +### Outputs + +| Name | Description | +|------|-------------| +| [helm\_release\_cluster\_store](#output\_helm\_release\_cluster\_store) | ClusterSecretStore helm release. Returning the helm release for trusted profile or apikey authentication according to the authentication type | + diff --git a/modules/eso-clusterstore/main.tf b/modules/eso-clusterstore/main.tf new file mode 100644 index 00000000..987c4e19 --- /dev/null +++ b/modules/eso-clusterstore/main.tf @@ -0,0 +1,108 @@ +locals { + # preliminary authentication validation - one of clusterstore_secret_apikey and clusterstore_trusted_profile_name must be valid + auth_validate_condition = var.clusterstore_secret_apikey == null && var.clusterstore_trusted_profile_name == null + auth_clusterstore_msg = "One of the variables clusterstore_secret_apikey and clusterstore_trusted_profile_name must be provided, cannot be both set to null" + # tflint-ignore: terraform_unused_declarations + auth_validate_check = regex("^${local.auth_clusterstore_msg}$", (!local.auth_validate_condition ? local.auth_clusterstore_msg : "")) + + # auth is apikey so the variable clusterstore_secret_apikey cannot be null + api_key_clusterstore_validate_condition = var.eso_authentication == "api_key" && var.clusterstore_secret_apikey == null + api_key_clusterstore_msg = "API Key authentication is enabled and scope for store is cluster, therefore clusterstore_secret_apikey must be provided." + # tflint-ignore: terraform_unused_declarations + api_key_clusterstore_validate_check = regex("^${local.api_key_clusterstore_msg}$", (!local.api_key_clusterstore_validate_condition ? local.api_key_clusterstore_msg : "")) + + # auth is trustedprofile so the variable clusterstore_trusted_profile_name cannot be null + tp_clusterstore_validate_condition = var.eso_authentication == "trusted_profile" && var.clusterstore_trusted_profile_name == null + tp_clusterstore_msg = "Trusted profile authentication is enabled, therefore clusterstore_trusted_profile_name must be provided." + # tflint-ignore: terraform_unused_declarations + tp_clusterstore_validate_check = regex("^${local.tp_clusterstore_msg}$", (!local.tp_clusterstore_validate_condition ? local.tp_clusterstore_msg : "")) +} + +locals { + helm_raw_chart_name = "raw" + helm_raw_chart_version = "0.2.5" + + # endpoints definition according to endpoints to use are private or public (var.service_endpoints) + iam_endpoint = "${var.service_endpoints == "private" ? "private." : ""}iam.cloud.ibm.com" + regional_endpoint = var.service_endpoints == "private" ? "private.${var.region}" : var.region + cluster_store_secrets_manager_endpoint = "${var.clusterstore_secrets_manager_guid}.${local.regional_endpoint}.secrets-manager.appdomain.cloud" +} + +### creating secret to store apikey to authenticate on secretsmanager for apikey authentication +resource "kubernetes_secret" "eso_clusterstore_secret" { + count = var.eso_authentication == "api_key" ? 1 : 0 + metadata { + name = var.clusterstore_secret_name + namespace = var.eso_namespace #checkov:skip=CKV_K8S_21 + } + + data = { + apiKey = var.clusterstore_secret_apikey + } + type = "opaque" +} + + +### ClusterSecretStore used to connect with SM instance for clusterstore and authentication is through apikey + +# define cluster secret store for cluster scope and apikey auth +resource "helm_release" "cluster_secret_store_apikey" { + count = var.eso_authentication == "api_key" ? 1 : 0 + name = "${var.clusterstore_helm_rls_name}-apikey" + namespace = var.eso_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: ClusterSecretStore + metadata: + name: "${var.clusterstore_name}" + spec: + provider: + ibm: + serviceUrl: "https://${local.cluster_store_secrets_manager_endpoint}" + auth: + secretRef: + secretApiKeySecretRef: + name: "${var.clusterstore_secret_name}" + key: apiKey + namespace: "${var.eso_namespace}" + EOF + ] + + depends_on = [ + kubernetes_secret.eso_clusterstore_secret + ] +} + +# define cluster secret store for cluster scope and trusted store auth +# ContainerAuth with CRI based authentication +resource "helm_release" "cluster_secret_store_tp" { + count = var.eso_authentication == "trusted_profile" ? 1 : 0 + name = "${var.clusterstore_helm_rls_name}-tp" + namespace = var.eso_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: ClusterSecretStore + metadata: + name: "${var.clusterstore_name}" + spec: + provider: + ibm: + serviceUrl: "https://${local.cluster_store_secrets_manager_endpoint}" + auth: + containerAuth: + profile: "${var.clusterstore_trusted_profile_name}" + iamEndpoint: "https://${local.iam_endpoint}" + tokenLocation: /var/run/secrets/tokens/sa-token + EOF + ] +} diff --git a/modules/eso-clusterstore/outputs.tf b/modules/eso-clusterstore/outputs.tf new file mode 100644 index 00000000..8c91317c --- /dev/null +++ b/modules/eso-clusterstore/outputs.tf @@ -0,0 +1,8 @@ +############################################################################## +# Outputs +############################################################################## + +output "helm_release_cluster_store" { + value = var.eso_authentication == "trusted_profile" ? helm_release.cluster_secret_store_tp : helm_release.cluster_secret_store_apikey + description = "ClusterSecretStore helm release. Returning the helm release for trusted profile or apikey authentication according to the authentication type" +} diff --git a/modules/eso-clusterstore/variables.tf b/modules/eso-clusterstore/variables.tf new file mode 100644 index 00000000..3d821825 --- /dev/null +++ b/modules/eso-clusterstore/variables.tf @@ -0,0 +1,74 @@ +######## eso clusterstore configuration + +variable "eso_namespace" { + description = "Namespace where the ESO is deployed. It will be used to deploy the ClusterStore" + type = string +} + +variable "region" { + description = "Region where Secrets Manager is deployed. It will be used to build the regional URL to the service" + type = string +} + +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 = "public" + validation { + condition = contains(["public", "private"], var.service_endpoints) + error_message = "The specified service_endpoints is not a valid selection!" + } +} + +variable "clusterstore_name" { + description = "Name of the ESO secret store to be used/created for cluster scope." + default = "clustersecret-store" + type = string +} + +variable "clusterstore_helm_rls_name" { + description = "Name of helm release for clusterstore" + type = string + default = "cluster-secret-store" +} + +############################################################################## +# Authentication configuration for clusterstore that can be one of api_key or trusted_profile +############################################################################## +variable "eso_authentication" { + type = string + description = "Authentication method, Possible values are api_key or/and trusted_profile." + default = "trusted_profile" + validation { + condition = contains(["api_key", "trusted_profile"], var.eso_authentication) + error_message = "Authentication mode allowed are api_key or/and trusted_profile." + } +} + +variable "clusterstore_secret_name" { + description = "Secret name to be used/referenced in the ESO clusterstore 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" + sensitive = true + default = null +} + +####### trusted profile + +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" + default = null +} + +####### Secrets Manager instance + +variable "clusterstore_secrets_manager_guid" { + type = string + description = "Secrets manager instance GUID for clusterstore where secrets will be stored or fetched from" +} diff --git a/modules/eso-clusterstore/version.tf b/modules/eso-clusterstore/version.tf new file mode 100644 index 00000000..168b3dc2 --- /dev/null +++ b/modules/eso-clusterstore/version.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + # Use "greater than or equal to" range in modules + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.16.1, <3.0.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.8.0" + } + } +} diff --git a/modules/eso-external-secret/README.md b/modules/eso-external-secret/README.md new file mode 100644 index 00000000..19a9b5fa --- /dev/null +++ b/modules/eso-external-secret/README.md @@ -0,0 +1,62 @@ +# ESO External Secrets Module + +This module allows to configure an [ExternalSecrets](https://external-secrets.io/latest/api/externalsecret/) resource in the desired namespace and with the desired configurations. + +It if possible to create ExternalSecret resource referencing either: +- a `ClusterSecretStore` for store with cluster scope +- a `SecretStore` for 'namespace' for regular namespaced scope +by correctly setting the related input variable `eso_store_scope` + +For more information about ExternalSecrets on ESO please refer to the ESO documentation available [here](https://external-secrets.io/v0.8.3/guides/introduction/) + + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [helm](#requirement\_helm) | >= 2.8.0 | + +### Modules + +No modules. + +### Resources + +| Name | Type | +|------|------| +| [helm_release.kubernetes_secret](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.kubernetes_secret_certificate](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.kubernetes_secret_chain_list](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.kubernetes_secret_kv_all](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.kubernetes_secret_kv_key](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.kubernetes_secret_user_pw](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [es\_container\_registry](#input\_es\_container\_registry) | The registry URL to be used in dockerconfigjson | `string` | `"us.icr.io"` | no | +| [es\_container\_registry\_email](#input\_es\_container\_registry\_email) | Optional - Email to be used in dockerconfigjson | `string` | `null` | no | +| [es\_container\_registry\_secrets\_chain](#input\_es\_container\_registry\_secrets\_chain) | Structure to generate a chain of secrets into a single dockerjsonconfig secret for multiple registries authentication. |
list(object({
es_container_registry = string
sm_secret_id = string # id of the secret storing the apikey that will be used for the secrets chain
es_container_registry_email = optional(string, null)
}))
| `[]` | no | +| [es\_helm\_rls\_name](#input\_es\_helm\_rls\_name) | Name to use for the helm release for externalsecrets resource. Must be unique in the namespace | `string` | n/a | yes | +| [es\_helm\_rls\_namespace](#input\_es\_helm\_rls\_namespace) | Namespace to deploy the helm release for the externalsecret. Default if null is the externalsecret namespace | `string` | `null` | no | +| [es\_kubernetes\_namespace](#input\_es\_kubernetes\_namespace) | Namespace to use to generate the externalsecret | `string` | n/a | yes | +| [es\_kubernetes\_secret\_data\_key](#input\_es\_kubernetes\_secret\_data\_key) | Data key to be used in Kubernetes Opaque secret. Only needed when 'es\_kubernetes\_secret\_type' is configured as `opaque` and sm\_secret\_type is set to either 'arbitrary' or 'iam\_credentials' | `string` | `null` | no | +| [es\_kubernetes\_secret\_name](#input\_es\_kubernetes\_secret\_name) | Name of the secret to use for the kubernetes secret object | `string` | n/a | yes | +| [es\_kubernetes\_secret\_type](#input\_es\_kubernetes\_secret\_type) | Secret type/format to be installed in the Kubernetes/Openshift cluster by ESO. Valid inputs are `opaque` `dockerconfigjson` and `tls` | `string` | n/a | yes | +| [es\_refresh\_interval](#input\_es\_refresh\_interval) | Specify interval for es secret synchronization. See recommendations for specifying/customizing refresh interval in this IBM Cloud article > https://cloud.ibm.com/docs/secrets-manager?topic=secrets-manager-tutorial-kubernetes-secrets#kubernetes-secrets-best-practices | `string` | `"1h"` | no | +| [eso\_store\_name](#input\_eso\_store\_name) | ESO store name to use when creating the externalsecret. Cannot be null and it is mandatory | `string` | n/a | yes | +| [eso\_store\_scope](#input\_eso\_store\_scope) | Set to 'cluster' to configure ESO store as with cluster scope (ClusterSecretStore) or 'namespace' for regular namespaced scope (SecretStore). This value is used to configure the externalsecret reference | `string` | `"cluster"` | no | +| [reloader\_watching](#input\_reloader\_watching) | Flag to enable/disable the reloader watching. If enabled the reloader will watch for changes in the secret and reload the associated annotated pods if needed | `bool` | `false` | no | +| [sm\_certificate\_bundle](#input\_sm\_certificate\_bundle) | Flag to enable if the public/intermediate certificate is bundled. If enabled public key is managed as bundled with intermediate and private key, otherwise the template considers the public key not bundled with intermediate certificate and private key | `bool` | `true` | no | +| [sm\_certificate\_has\_intermediate](#input\_sm\_certificate\_has\_intermediate) | The secret manager certificate is provided with intermediate certificate. By enabling this flag the certificate body on kube will contain certificate and intermediate content, otherwise only certificate will be added. Valid only for public and imported certificate | `bool` | `true` | no | +| [sm\_kv\_keyid](#input\_sm\_kv\_keyid) | Secrets-Manager key value (kv) keyid | `string` | `null` | no | +| [sm\_kv\_keypath](#input\_sm\_kv\_keypath) | Secrets-Manager key value (kv) keypath | `string` | `null` | no | +| [sm\_secret\_id](#input\_sm\_secret\_id) | Secrets-Manager secret ID where source data will be synchronized with Kubernetes secret. It can be null only in the case of a dockerjsonconfig secrets chain | `string` | n/a | yes | +| [sm\_secret\_type](#input\_sm\_secret\_type) | Secrets-manager secret type to be used as source data by ESO. Valid input types are 'arbitrary', 'username\_password' and 'iam\_credentials' | `string` | n/a | yes | + +### Outputs + +No outputs. + diff --git a/modules/eso-external-secret/main.tf b/modules/eso-external-secret/main.tf new file mode 100644 index 00000000..46c8cabc --- /dev/null +++ b/modules/eso-external-secret/main.tf @@ -0,0 +1,368 @@ +locals { + # Validation approach based on https://stackoverflow.com/a/66682419 + validate_condition_secret = var.es_kubernetes_secret_data_key == null && (var.es_kubernetes_secret_type == "opaque" && (var.sm_secret_type == "arbitrary" || var.sm_secret_type == "iam_credentials")) # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + validate_msg_secret = "A value for 'es_kubernetes_secret_data_key' must be passed when 'es_kubernetes_secret_type = opaque' and 'sm_secret_type' is either 'arbitrary' or 'iam_credentials'" + # tflint-ignore: terraform_unused_declarations + validate_check_secret = regex("^${local.validate_msg_secret}$", (!local.validate_condition_secret ? local.validate_msg_secret : "")) + + # reloader annotation + reloader_annotation = var.reloader_watching ? "'reloader.stakater.com/auto': 'true'" : "{}" +} + +# secrets formatting +locals { + # certificate secret templates and management + is_certificate = can(regex("^imported_cert$|^public_cert$|^private_cert$", var.sm_secret_type)) + + # dockerjsonconfig secrets chain flag + is_dockerjsonconfig_chain = length(var.es_container_registry_secrets_chain) > 0 ? true : false + + # validation for dockerjsonconfig secrets chain -> if it is a chain the kube secret type must be dockerconfigjson and sm secret type iam_credentials + validate_condition_chain = local.is_dockerjsonconfig_chain == true && (var.es_kubernetes_secret_type != "dockerconfigjson" || var.sm_secret_type != "iam_credentials") # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + validate_msg_chain = "If the externalsecret is expected to generate a dockerjsonconfig secrets chain the only supported value for es_kubernetes_secret_type is dockerconfigjson and for sm_secret_type is iam_credentials" + # tflint-ignore: terraform_unused_declarations + validate_check_chain = regex("^${local.validate_msg_chain}$", (!local.validate_condition_chain ? local.validate_msg_chain : "")) + + # validation of sm_secret_id => it can be null only in the case of a dockerjsonconfig chain (secret_ids will be stored ) + validate_condition_sm_secret_id = var.sm_secret_id == null && local.is_dockerjsonconfig_chain == false + validate_msg_sm_secret_id = "The input variable sm_secret_id can be null only a dockerjsonconfig secrets chain is going to be created" + # tflint-ignore: terraform_unused_declarations + validate_check_sm_secret_id = regex("^${local.validate_msg_sm_secret_id}$", (!local.validate_condition_sm_secret_id ? local.validate_msg_sm_secret_id : "")) + + # for certificate secrets public_cert and private_cert the id is the last part of the sm_secret_sm + cert_remoteref_key = local.is_certificate ? "${var.sm_secret_type}/${var.sm_secret_id}" : "" + # defining the template data structure according to the type of certificate + # public and imported certificate template will contain intermediate field only if sm_certificate_has_intermediate flag is true and the certificate bundle flag is disabled + public_cert_tls_template_data = (var.sm_certificate_has_intermediate == true && var.sm_certificate_bundle == false) ? "tls.crt: \"{{ .certificate }}\\n{{ .intermediate }}\"\n tls.key: '{{ .private_key }}'" : "tls.crt: '{{ .certificate}}'\n tls.key: '{{ .private_key }}'" + imported_cert_tls_template_data = (var.sm_certificate_has_intermediate == true && var.sm_certificate_bundle == false) ? "tls.crt: \"{{ .certificate }}\\n{{ .intermediate }}\"\n tls.key: '{{ .private_key }}'" : "tls.crt: '{{ .certificate}}'\n tls.key: '{{ .private_key }}'" + private_cert_tls_template_data = "tls.crt: '{{ .certificate}}'\n tls.key: '{{ .private_key }}'" + # defining the spec data structure according to the type of certificate + # public and imported certificate template will contain intermediate field only if sm_certificate_has_intermediate flag is true + public_certificate_spec_data = (var.sm_certificate_has_intermediate == true && var.sm_certificate_bundle == false) ? "- secretKey: certificate\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: certificate\n - secretKey: intermediate\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: intermediate\n - secretKey: private_key\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: private_key" : "- secretKey: certificate\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: certificate\n - secretKey: private_key\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: private_key" + imported_certificate_spec_data = (var.sm_certificate_has_intermediate == true && var.sm_certificate_bundle == false) ? "- secretKey: certificate\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: certificate\n - secretKey: intermediate\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: intermediate\n - secretKey: private_key\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: private_key" : "- secretKey: certificate\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: certificate\n - secretKey: private_key\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: private_key" + private_certificate_spec_data = "- secretKey: certificate\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: certificate\n - secretKey: private_key\n remoteRef:\n key: ${local.cert_remoteref_key}\n property: private_key" + # definining the right structure to use according to the certificate type + certificate_template = local.is_certificate ? (var.sm_secret_type == "public_cert" ? local.public_cert_tls_template_data : (var.sm_secret_type == "imported_cert" ? local.imported_cert_tls_template_data : (var.sm_secret_type == "private_cert" ? local.private_cert_tls_template_data : ""))) : "" # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + certificate_spec_data = local.is_certificate ? (var.sm_secret_type == "public_cert" ? local.public_certificate_spec_data : (var.sm_secret_type == "imported_cert" ? local.imported_certificate_spec_data : (var.sm_secret_type == "private_cert" ? local.private_certificate_spec_data : ""))) : "" # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + + # dockerjson format + docker_user = var.sm_secret_type == "username_password" ? "{{ .username }}" : "iamapikey" # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + docker_password = var.sm_secret_type == "username_password" ? "{{ .password }}" : "{{ .secretid }}" + + # setting data_type according to the kube secret and the SM secret types + # if kube secret type is opaque && SM secret type arbitrary or iam_credentials -> var.es_kubernetes_secret_data_key + # if kube secret type is opaque && SM secret type username_password -> data_type = .dockerconfigjson + # if kube secret type is opaque && SM secret type != username_password arbitrary and iam_credentials (so kv or the certificate types) -> not setting anything here but handled in related sections + # if kube secret type is not opaque && kube secret type is dockerconfigjson -> data_type = .dockerconfigjson + # if kube secret type is not opaque && if kube secret type is not dockerconfigjson -> not setting here + data_type = var.es_kubernetes_secret_type == "opaque" ? ((var.sm_secret_type == "arbitrary" || var.sm_secret_type == "iam_credentials") ? var.es_kubernetes_secret_data_key : (var.sm_secret_type == "username_password") ? ".dockerconfigjson" : "") : (var.es_kubernetes_secret_type == "dockerconfigjson" ? ".dockerconfigjson" : "") + + # setting data_payload for dockerconfigjson according to the value of var.es_container_registry_email + # if es_kubernetes_secret_type = dockerconfigjson -> setting payload according to the fields available + # if es_kubernetes_secret_type != dockerconfigjson -> data_payload = {{ .secretid }} + data_payload = var.es_kubernetes_secret_type == "dockerconfigjson" && var.es_container_registry_email != null ? jsonencode({ "auths" : { (var.es_container_registry) : { "email" : (var.es_container_registry_email), "username" : (local.docker_user), "password" : (local.docker_password) } } }) : (var.es_kubernetes_secret_type == "dockerconfigjson" && var.es_container_registry_email == null ? jsonencode({ "auths" : { (var.es_container_registry) : { "username" : (local.docker_user), "password" : (local.docker_password) } } }) : "{{ .secretid }}") # checkov:skip=CKV_SECRET_6:does not require high entropy string as is static value + + # final data field format according to the secret type + # only in the case sm_secret_type is username_password and kube secret type is opaque -> data = username : '{{ .username }}'\n password : '{{ .password }} + # in all the other cases data is the resulting data_type : data_payload + username_password_opaque_data = "username : '{{ .username }}'\n password : '{{ .password }}'" + data = var.sm_secret_type == "username_password" && var.es_kubernetes_secret_type == "opaque" ? local.username_password_opaque_data : "${local.data_type} : '${local.data_payload}'" # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + + # setting value for template type field according to the var.es_kubernetes_secret_type value + es_kubernetes_secret_type = var.es_kubernetes_secret_type == "dockerconfigjson" ? "kubernetes.io/dockerconfigjson" : (var.es_kubernetes_secret_type == "tls" ? "kubernetes.io/tls" : "Opaque") + + # setting remote_ref field value according to the secret type + # for sm secrets types iam_credentials and kv the remoteref is sm_secret_type/sm_secret_id, for arbitrary is only sm_secret_id + # if is_dockerjsonconfig_chain is true it is set to empty as not used + es_remoteref_key = local.is_dockerjsonconfig_chain == false ? (var.sm_secret_type == "iam_credentials" || var.sm_secret_type == "kv" ? "${var.sm_secret_type}/${var.sm_secret_id}" : var.sm_secret_id) : "" # checkov:skip=CKV_SECRET_6: does not require high entropy string as is static value + + # dockerconfigjson config for chain of secrets - building a map for all the registries + data_payload_chain_map = local.is_dockerjsonconfig_chain == true ? { + "auths" : { + for index, element in var.es_container_registry_secrets_chain : + element.es_container_registry => (element.es_container_registry_email != null && element.es_container_registry_email != "") ? + { + "username" : "iamapikey", "password" : "{{ .secretid_${index} }}", "email" : (element.es_container_registry_email) + } + : + { + "username" : "iamapikey", "password" : "{{ .secretid_${index} }}" + } + } + } : {} + + # in order to have the content correctly mapped it needs to apply jsonencode twice + encodedchain = jsonencode(jsonencode(local.data_payload_chain_map)) + data_chain = ".dockerconfigjson : ${local.encodedchain}" + + # helm chart details + helm_raw_chart_name = "raw" + helm_raw_chart_version = "0.2.5" + + # if the scope is namespace the secret store kind is SecretStore while is ClusterSecretStore in all the other cases + secret_store_ref_kind = var.eso_store_scope != "namespace" ? "ClusterSecretStore" : "SecretStore" + + # if var.es_helm_rls_namespace is not set the namespace is set to es_kubernetes_namespace (default logic) + es_helm_rls_namespace = var.es_helm_rls_namespace != null ? var.es_helm_rls_namespace : var.es_kubernetes_namespace + + # key-value secret management + is_kv = can(regex("^kv$", var.sm_secret_type)) + + # validation for key-value secret type + validate_condition_kv_secret = local.is_kv && var.sm_kv_keyid != null && var.sm_kv_keypath != null + validate_msg_kv_secret = "For key-value secrets only one of input variables 'sm_kv_keyid' or 'sm_kv_keypath' can be set." + # tflint-ignore: terraform_unused_declarations + validate_check_kv_secret = regex("^${local.validate_msg_kv_secret}$", (!local.validate_condition_kv_secret ? local.validate_msg_kv_secret : "")) + + # second validation for key-value secret type - allowing only opaque for es_kubernetes_secret_type if sm_secret_type is kv + validate_condition_kv_kube_type = local.is_kv && var.es_kubernetes_secret_type != "opaque" + validate_msg_kv_kube_type = "For key-value secrets-manager secrets types es_kubernetes_secret_type cannot be different than opaque - found ${var.es_kubernetes_secret_type}" + # tflint-ignore: terraform_unused_declarations + validate_check_kv_kube_type = regex("^${local.validate_msg_kv_kube_type}$", (!local.validate_condition_kv_kube_type ? local.validate_msg_kv_kube_type : "")) + + # setting up the remoteref property for kv + kv_remoteref_property = var.sm_kv_keyid != null ? var.sm_kv_keyid : (var.sm_kv_keypath != null ? var.sm_kv_keypath : "") + + # kube secret name + helm_secret_name = substr(join("-", [var.es_kubernetes_namespace, var.es_helm_rls_name]), 0, 52) +} + +### Define kubernetes secret to be installed in cluster for sm_secret_type iam_credentials or arbitrary +resource "helm_release" "kubernetes_secret" { + count = (var.sm_secret_type == "iam_credentials" || var.sm_secret_type == "arbitrary") && local.is_dockerjsonconfig_chain == false ? 1 : 0 + name = local.helm_secret_name + namespace = local.es_helm_rls_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: ExternalSecret + metadata: + name: "${var.es_kubernetes_secret_name}" + namespace: "${var.es_kubernetes_namespace}" + spec: + refreshInterval: ${var.es_refresh_interval} + secretStoreRef: + name: "${var.eso_store_name}" + kind: "${local.secret_store_ref_kind}" + target: + name: "${var.es_kubernetes_secret_name}" + template: + engineVersion: v2 + type: "${local.es_kubernetes_secret_type}" + metadata: + annotations: + ${local.reloader_annotation} + data: + ${local.data} + data: + - secretKey: secretid + remoteRef: + key: "${local.es_remoteref_key}" + EOF + ] +} + +### Define kubernetes secret to be installed in cluster for sm_secret_type iam_credentials and kubernetes secret type dockerjsonconfig and configured with a chain of secrets +resource "helm_release" "kubernetes_secret_chain_list" { + count = local.is_dockerjsonconfig_chain == true ? 1 : 0 + name = local.helm_secret_name + namespace = local.es_helm_rls_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: ExternalSecret + metadata: + name: "${var.es_kubernetes_secret_name}" + namespace: "${var.es_kubernetes_namespace}" + spec: + refreshInterval: ${var.es_refresh_interval} + secretStoreRef: + name: "${var.eso_store_name}" + kind: "${local.secret_store_ref_kind}" + target: + name: "${var.es_kubernetes_secret_name}" + template: + engineVersion: v2 + type: "${local.es_kubernetes_secret_type}" + metadata: + annotations: + ${local.reloader_annotation} + data: + ${local.data_chain} + data: +%{for index, element in var.es_container_registry_secrets_chain~} + - secretKey: secretid_${index} + remoteRef: + key: "${var.sm_secret_type}/${element.sm_secret_id}" +%{endfor~} + EOF + ] +} + +### Define kubernetes secret to be installed in cluster for opaque secret type based on SM user credential secret type +resource "helm_release" "kubernetes_secret_user_pw" { + count = var.sm_secret_type == "username_password" ? 1 : 0 + name = local.helm_secret_name + namespace = var.es_kubernetes_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1alpha1 + kind: ExternalSecret + metadata: + name: "${var.es_kubernetes_secret_name}" + namespace: "${var.es_kubernetes_namespace}" + spec: + refreshInterval: ${var.es_refresh_interval} + secretStoreRef: + name: "${var.eso_store_name}" + kind: "${local.secret_store_ref_kind}" + target: + name: "${var.es_kubernetes_secret_name}" + template: + engineVersion: v2 + type: "${local.es_kubernetes_secret_type}" + metadata: + annotations: + ${local.reloader_annotation} + data: + ${local.data} + data: + - secretKey: username + remoteRef: + key: "username_password/${var.sm_secret_id}" + property: username + - secretKey: password + remoteRef: + key: "username_password/${var.sm_secret_id}" + property: password + EOF + ] +} + +### Define kubernetes secret to be installed in cluster for certificate secret based on SM certificate secret type +resource "helm_release" "kubernetes_secret_certificate" { + count = local.is_certificate ? 1 : 0 #checkov:skip=CKV_SECRET_6 + name = local.helm_secret_name + namespace = var.es_kubernetes_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: ExternalSecret + metadata: + name: "${var.es_kubernetes_secret_name}" + namespace: "${var.es_kubernetes_namespace}" + spec: + refreshInterval: ${var.es_refresh_interval} + secretStoreRef: + name: "${var.eso_store_name}" + kind: "${local.secret_store_ref_kind}" + target: + name: "${var.es_kubernetes_secret_name}" + template: + engineVersion: v2 + type: "${local.es_kubernetes_secret_type}" + metadata: + annotations: + ${local.reloader_annotation} + data: + ${local.certificate_template} + data: + ${local.certificate_spec_data} + EOF + ] +} + +### Define kubernetes secret to be installed in cluster for key-value secret based on SM kv secret type based on keyid or key path +resource "helm_release" "kubernetes_secret_kv_key" { + count = local.is_kv && local.kv_remoteref_property != "" ? 1 : 0 + name = local.helm_secret_name + namespace = var.es_kubernetes_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: ExternalSecret + metadata: + name: "${var.es_kubernetes_secret_name}" + namespace: "${var.es_kubernetes_namespace}" + spec: + refreshInterval: ${var.es_refresh_interval} + secretStoreRef: + name: "${var.eso_store_name}" + kind: "${local.secret_store_ref_kind}" + target: + name: "${var.es_kubernetes_secret_name}" + template: + engineVersion: v2 + type: "${local.es_kubernetes_secret_type}" + metadata: + annotations: + ${local.reloader_annotation} + data: + secret: "{{ .${local.kv_remoteref_property} }}" + data: + - secretKey: "${local.kv_remoteref_property}" + remoteRef: + key: "${local.es_remoteref_key}" + property: "${local.kv_remoteref_property}" + EOF + ] +} + +### Define kubernetes secret to be installed in cluster for key-value secret based on SM kv secret type pulling all the keys structure +resource "helm_release" "kubernetes_secret_kv_all" { + count = local.is_kv && local.kv_remoteref_property == "" ? 1 : 0 + name = local.helm_secret_name + namespace = var.es_kubernetes_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: ExternalSecret + metadata: + name: "${var.es_kubernetes_secret_name}" + namespace: "${var.es_kubernetes_namespace}" + spec: + refreshInterval: ${var.es_refresh_interval} + secretStoreRef: + name: "${var.eso_store_name}" + kind: "${local.secret_store_ref_kind}" + target: + name: "${var.es_kubernetes_secret_name}" + template: + engineVersion: v2 + type: "${local.es_kubernetes_secret_type}" + metadata: + annotations: + ${local.reloader_annotation} + data: + secret: '{{ .keys }}' + data: + - secretKey: keys + remoteRef: + key: "${local.es_remoteref_key}" + EOF + ] +} diff --git a/examples/advanced/provider.tf b/modules/eso-external-secret/outputs.tf similarity index 58% rename from examples/advanced/provider.tf rename to modules/eso-external-secret/outputs.tf index 2080946b..b16a1a35 100644 --- a/examples/advanced/provider.tf +++ b/modules/eso-external-secret/outputs.tf @@ -1,8 +1,3 @@ ############################################################################## -# Provider config +# Outputs ############################################################################## - -provider "ibm" { - ibmcloud_api_key = var.ibmcloud_api_key - region = var.region -} diff --git a/modules/eso-external-secret/variables.tf b/modules/eso-external-secret/variables.tf new file mode 100644 index 00000000..3dbb168f --- /dev/null +++ b/modules/eso-external-secret/variables.tf @@ -0,0 +1,139 @@ +variable "eso_store_scope" { + description = "Set to 'cluster' to configure ESO store as with cluster scope (ClusterSecretStore) or 'namespace' for regular namespaced scope (SecretStore). This value is used to configure the externalsecret reference" + type = string + default = "cluster" + + validation { + condition = var.eso_store_scope == "cluster" || var.eso_store_scope == "namespace" + error_message = "The eso_store_deployment value must be one of the following: cluster, namespace" + } +} + +variable "es_kubernetes_namespace" { + description = "Namespace to use to generate the externalsecret" + type = string +} + +variable "es_kubernetes_secret_name" { + description = "Name of the secret to use for the kubernetes secret object" + type = string +} + +variable "es_refresh_interval" { + description = "Specify interval for es secret synchronization. See recommendations for specifying/customizing refresh interval in this IBM Cloud article > https://cloud.ibm.com/docs/secrets-manager?topic=secrets-manager-tutorial-kubernetes-secrets#kubernetes-secrets-best-practices" + default = "1h" + type = string + validation { + condition = can(regex("^[1-9][0-9]?[smh]$", var.es_refresh_interval)) + error_message = "The refresh interval must be a value between 1 and 99s(seconds)/m(minutes)/h(hours)." + } +} +variable "eso_store_name" { + description = "ESO store name to use when creating the externalsecret. Cannot be null and it is mandatory" + type = string +} + +variable "es_kubernetes_secret_type" { + description = "Secret type/format to be installed in the Kubernetes/Openshift cluster by ESO. Valid inputs are `opaque` `dockerconfigjson` and `tls`" + type = string + validation { + condition = can(regex("^opaque$|^dockerconfigjson$|^tls$|^$", var.es_kubernetes_secret_type)) + # If it is empty, no secret will be created. + error_message = "The es_kubernetes_secret_type value must be one of the following: opaque, dockerconfigjson, tls or leave it empty." + } +} + +variable "es_kubernetes_secret_data_key" { + description = "Data key to be used in Kubernetes Opaque secret. Only needed when 'es_kubernetes_secret_type' is configured as `opaque` and sm_secret_type is set to either 'arbitrary' or 'iam_credentials'" + type = string + default = null +} + +variable "sm_secret_type" { + description = "Secrets-manager secret type to be used as source data by ESO. Valid input types are 'arbitrary', 'username_password' and 'iam_credentials'" + type = string + validation { + condition = can(regex("^iam_credentials$|^username_password$|^arbitrary$|^imported_cert$|^public_cert$|^private_cert|^kv$|$^$", var.sm_secret_type)) + # If it is empty, no secret will be created + error_message = "The sm_secret_type value must be one of the following: iam_credentials, username_password, arbitrary, imported_cert, public_cert, private_cert, kv or leave it empty." + } +} + +variable "sm_secret_id" { + description = "Secrets-Manager secret ID where source data will be synchronized with Kubernetes secret. It can be null only in the case of a dockerjsonconfig secrets chain" + type = string +} + +variable "es_container_registry" { + type = string + default = "us.icr.io" + description = "The registry URL to be used in dockerconfigjson" +} + +variable "es_container_registry_email" { + type = string + description = "Optional - Email to be used in dockerconfigjson" + default = null +} + +variable "es_container_registry_secrets_chain" { + description = "Structure to generate a chain of secrets into a single dockerjsonconfig secret for multiple registries authentication." + type = list(object({ + es_container_registry = string + sm_secret_id = string # id of the secret storing the apikey that will be used for the secrets chain + es_container_registry_email = optional(string, null) + })) + default = [] + nullable = false +} + +variable "es_helm_rls_name" { + description = "Name to use for the helm release for externalsecrets resource. Must be unique in the namespace" + type = string + validation { + condition = can(regex("^[0-9A-Za-z-]+$", var.es_helm_rls_name)) + error_message = "The value of the helm release for the es resource must match ^[0-9A-Za-z-]+$ regexp" + } +} + +variable "es_helm_rls_namespace" { + description = "Namespace to deploy the helm release for the externalsecret. Default if null is the externalsecret namespace" + type = string + validation { + condition = var.es_helm_rls_namespace == null || can(regex("^[0-9A-Za-z-]+$", var.es_helm_rls_namespace)) + error_message = "The value of the helm release for the es resource must match ^[0-9A-Za-z-]+$ regexp" + } + default = null +} + +variable "sm_kv_keyid" { + description = "Secrets-Manager key value (kv) keyid" + type = string + default = null +} + +variable "sm_kv_keypath" { + description = "Secrets-Manager key value (kv) keypath" + type = string + default = null +} + +variable "sm_certificate_has_intermediate" { + description = "The secret manager certificate is provided with intermediate certificate. By enabling this flag the certificate body on kube will contain certificate and intermediate content, otherwise only certificate will be added. Valid only for public and imported certificate" + type = bool + default = true +} + +variable "reloader_watching" { + description = "Flag to enable/disable the reloader watching. If enabled the reloader will watch for changes in the secret and reload the associated annotated pods if needed" + type = bool + default = false +} + +# provider is affected by https://github.com/IBM-Cloud/terraform-provider-ibm/issues/4803 +# check for its status before switching to false +variable "sm_certificate_bundle" { + description = "Flag to enable if the public/intermediate certificate is bundled. If enabled public key is managed as bundled with intermediate and private key, otherwise the template considers the public key not bundled with intermediate certificate and private key" + type = bool + default = true +} diff --git a/modules/eso-external-secret/version.tf b/modules/eso-external-secret/version.tf new file mode 100644 index 00000000..26ee8a3e --- /dev/null +++ b/modules/eso-external-secret/version.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + # Use "greater than or equal to" range in modules + helm = { + source = "hashicorp/helm" + version = ">= 2.8.0" + } + } +} diff --git a/modules/eso-secretstore/README.md b/modules/eso-secretstore/README.md new file mode 100644 index 00000000..49b93013 --- /dev/null +++ b/modules/eso-secretstore/README.md @@ -0,0 +1,54 @@ +# ESO (Namespaced) Secret Store Module + +This module allows to configure an [SecretStore](https://external-secrets.io/latest/api/secretstore/) resource for an ESO secret store with namespace scope, in the desired namespace and with the desired configurations. + +For more information about SecretStore resource and about ESO please refer to the ESO documentation available [here](https://external-secrets.io/v0.8.3/guides/introduction/) + +This module supports SecretStore two authentication configurations to pull/push secrets with the configured Secrets Manager instance: +- apikey authentication +- trusted profile authentication + +For more information about Trusted Profiles refer to the IBM Cloud documentation available [here](https://cloud.ibm.com/docs/account?topic=account-create-trusted-profile&interface=ui) + + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [helm](#requirement\_helm) | >= 2.8.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.16.1, <3.0.0 | + +### Modules + +No modules. + +### Resources + +| Name | Type | +|------|------| +| [helm_release.external_secret_store_apikey](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [helm_release.external_secret_store_tp](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [kubernetes_secret.eso_secretsstore_secret](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [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\_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 | + +### Outputs + +| Name | Description | +|------|-------------| +| [helm\_release\_secret\_store](#output\_helm\_release\_secret\_store) | SecretStore helm release. Returning the helm release for trusted profile or apikey authentication according to the authentication type | + diff --git a/modules/eso-secretstore/main.tf b/modules/eso-secretstore/main.tf new file mode 100644 index 00000000..0c14b62d --- /dev/null +++ b/modules/eso-secretstore/main.tf @@ -0,0 +1,99 @@ +locals { + helm_raw_chart_name = "raw" + helm_raw_chart_version = "0.2.5" + + + # preliminary authentication validation - one of sstore_secret_apikey and sstore_trusted_profile_name must be valid + auth_validate_condition = var.sstore_secret_apikey == null && var.sstore_trusted_profile_name == null + auth_esstore_msg = "One of the variables sstore_secret_apikey and sstore_trusted_profile_name must be provided, cannot be both set to null" + # tflint-ignore: terraform_unused_declarations + auth_validate_check = regex("^${local.auth_esstore_msg}$", (!local.auth_validate_condition ? local.auth_esstore_msg : "")) + + # auth is apikey so the variable sstore_secret_apikey cannot be null + api_key_esstore_validate_condition = var.eso_authentication == "api_key" && var.sstore_secret_apikey == null + api_key_esstore_msg = "API Key authentication is enabled and scope for store is cluster, therefore sstore_secret_apikey must be provided." + # tflint-ignore: terraform_unused_declarations + api_key_esstore_validate_check = regex("^${local.api_key_esstore_msg}$", (!local.api_key_esstore_validate_condition ? local.api_key_esstore_msg : "")) + + # auth is trustedprofile so the variable sstore_trusted_profile_name cannot be null + tp_esstore_validate_condition = var.eso_authentication == "trusted_profile" && var.sstore_trusted_profile_name == null + tp_esstore_msg = "Trusted profile authentication is enabled, therefore sstore_trusted_profile_name must be provided." + # tflint-ignore: terraform_unused_declarations + tp_esstore_validate_check = regex("^${local.tp_esstore_msg}$", (!local.tp_esstore_validate_condition ? local.tp_esstore_msg : "")) + + # endpoints definition according to endpoints to use are private or public (var.service_endpoints) + iam_endpoint = "${var.service_endpoints == "private" ? "private." : ""}iam.cloud.ibm.com" + regional_endpoint = var.service_endpoints == "private" ? "private.${var.region}" : var.region +} + +### creating secret to store apikey to authenticate on secretsmanager for apikey authentication +resource "kubernetes_secret" "eso_secretsstore_secret" { + count = var.eso_authentication == "api_key" ? 1 : 0 + metadata { + name = var.sstore_secret_name + namespace = var.sstore_namespace #checkov:skip=CKV_K8S_21 + } + + data = { + apiKey = var.sstore_secret_apikey + } + type = "opaque" +} + +### Define secret store used to connect with SM instance for apikey auth +resource "helm_release" "external_secret_store_apikey" { + count = var.eso_authentication == "api_key" ? 1 : 0 + name = substr(join("-", [var.sstore_namespace, var.sstore_helm_rls_name]), 0, 52) + namespace = var.sstore_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: SecretStore + metadata: + name: "${var.sstore_store_name}" + namespace: "${var.sstore_namespace}" + spec: + provider: + ibm: + serviceUrl: "https://${var.sstore_secrets_manager_guid}.${local.regional_endpoint}.secrets-manager.appdomain.cloud" + auth: + secretRef: + secretApiKeySecretRef: + name: "${var.sstore_secret_name}" + key: apiKey + EOF + ] +} + +# Trusted profile authentication Use ContainerAuth with CRI based authentication (trusted profile support) +resource "helm_release" "external_secret_store_tp" { + count = var.eso_authentication == "trusted_profile" ? 1 : 0 + name = substr(join("-", [var.sstore_namespace, var.sstore_helm_rls_name]), 0, 52) + namespace = var.sstore_namespace + chart = "${path.module}/../../chart/${local.helm_raw_chart_name}" + version = local.helm_raw_chart_version + timeout = 600 + values = [ + <<-EOF + resources: + - apiVersion: external-secrets.io/v1beta1 + kind: SecretStore + metadata: + name: "${var.sstore_store_name}" + namespace: "${var.sstore_namespace}" + spec: + provider: + ibm: + serviceUrl: "https://${var.sstore_secrets_manager_guid}.${local.regional_endpoint}.secrets-manager.appdomain.cloud" + auth: + containerAuth: + profile: "${var.sstore_trusted_profile_name}" + iamEndpoint: "https://${local.iam_endpoint}" + tokenLocation: /var/run/secrets/tokens/sa-token + EOF + ] +} diff --git a/modules/eso-secretstore/outputs.tf b/modules/eso-secretstore/outputs.tf new file mode 100644 index 00000000..dfd963f1 --- /dev/null +++ b/modules/eso-secretstore/outputs.tf @@ -0,0 +1,8 @@ +############################################################################## +# Outputs +############################################################################## + +output "helm_release_secret_store" { + value = var.eso_authentication == "trusted_profile" ? helm_release.external_secret_store_tp : helm_release.external_secret_store_apikey + description = "SecretStore helm release. Returning the helm release for trusted profile or apikey authentication according to the authentication type" +} diff --git a/modules/eso-secretstore/variables.tf b/modules/eso-secretstore/variables.tf new file mode 100644 index 00000000..345acaa3 --- /dev/null +++ b/modules/eso-secretstore/variables.tf @@ -0,0 +1,72 @@ +######## eso generic configurations + +variable "region" { + description = "Region where Secrets Manager is deployed. It will be used to build the regional URL to the service" + 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." + default = "public" + validation { + condition = contains(["public", "private"], var.service_endpoints) + error_message = "The specified service_endpoints is not a valid selection!" + } +} + +############################################################################## +# Authentication configuration for clusterstore that can be one of api_key or trusted_profile +############################################################################## +variable "eso_authentication" { + type = string + description = "Authentication method, Possible values are api_key or/and trusted_profile." + default = "trusted_profile" + validation { + condition = contains(["api_key", "trusted_profile"], var.eso_authentication) + error_message = "Authentication mode allowed are api_key or/and trusted_profile." + } +} + +####### apikey authentication + +variable "sstore_secret_name" { + description = "Secret name to be used/referenced in the ESO secretstore 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" + type = string + default = null +} + +####### trusted profile + +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" + default = null +} + +variable "sstore_secrets_manager_guid" { + type = string + description = "Secrets manager instance GUID for secretstore 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" +} + +variable "sstore_store_name" { + type = string + description = "Name of the SecretStore to create" +} diff --git a/modules/eso-secretstore/version.tf b/modules/eso-secretstore/version.tf new file mode 100644 index 00000000..168b3dc2 --- /dev/null +++ b/modules/eso-secretstore/version.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + # Use "greater than or equal to" range in modules + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.16.1, <3.0.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.8.0" + } + } +} diff --git a/modules/eso-trusted-profile/README.md b/modules/eso-trusted-profile/README.md new file mode 100644 index 00000000..6b8063b7 --- /dev/null +++ b/modules/eso-trusted-profile/README.md @@ -0,0 +1,45 @@ +# ESO Trusted Profile Module + +This module allows to create and configure an Trusted Profile to authenticate with ESO operator. + +For more information about Trusted Profiles refer to the IBM Cloud documentation available [here](https://cloud.ibm.com/docs/account?topic=account-create-trusted-profile&interface=ui) + + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [ibm](#requirement\_ibm) | >= 1.51.0 | + +### Modules + +No modules. + +### Resources + +| Name | Type | +|------|------| +| [ibm_iam_trusted_profile.trusted_profile](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_trusted_profile) | resource | +| [ibm_iam_trusted_profile_claim_rule.claim_rule](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_trusted_profile_claim_rule) | resource | +| [ibm_iam_trusted_profile_policy.policy](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_trusted_profile_policy) | resource | +| [ibm_iam_trusted_profile_policy.policy_multiple_secrets_groups](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_trusted_profile_policy) | resource | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [secret\_groups\_id](#input\_secret\_groups\_id) | The list of secret groups to limit access to for the trusted profile to create. | `list(string)` | `[]` | no | +| [secrets\_manager\_guid](#input\_secrets\_manager\_guid) | Secrets manager instance GUID where secrets will be stored or fetched from and the trusted profile will allow access to. | `string` | n/a | yes | +| [tp\_cluster\_crn](#input\_tp\_cluster\_crn) | Target cluster CRN for the trusted profile. Used when creating trusted profile | `string` | n/a | yes | +| [tp\_namespace](#input\_tp\_namespace) | Namespace to configure in the Trusted Profile on IAM. Its value must be the namespace where the operator is deployed and running. | `string` | n/a | yes | +| [trusted\_profile\_claim\_rule\_type](#input\_trusted\_profile\_claim\_rule\_type) | Trusted profile claim rule type, set the value to 'ROKS\_SA' for ROKS clusters, set to ROKS for IKS clusters | `string` | `"ROKS_SA"` | no | +| [trusted\_profile\_name](#input\_trusted\_profile\_name) | The name of the trusted profile to be used. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance | `string` | n/a | yes | + +### Outputs + +| Name | Description | +|------|-------------| +| [trusted\_profile\_id](#output\_trusted\_profile\_id) | ID of the trusted profile | +| [trusted\_profile\_name](#output\_trusted\_profile\_name) | Name of the trusted profile | + diff --git a/modules/eso-trusted-profile/main.tf b/modules/eso-trusted-profile/main.tf new file mode 100644 index 00000000..5602acd5 --- /dev/null +++ b/modules/eso-trusted-profile/main.tf @@ -0,0 +1,83 @@ +### Trusted Profiles resources + +# creates a trusted profile to use for container authentication in external secrets operator +resource "ibm_iam_trusted_profile" "trusted_profile" { + name = var.trusted_profile_name + description = "a trusted profile to access the secrets manager instance: ${var.secrets_manager_guid}." +} + +# The following Rule allows incoming requests from +# the external-secrets SA in external-secrets namespce in the +# target cluster with the retrieved cluster's CRN. +resource "ibm_iam_trusted_profile_claim_rule" "claim_rule" { + profile_id = ibm_iam_trusted_profile.trusted_profile.id + type = "Profile-CR" + name = "${var.trusted_profile_name}-rule" + cr_type = var.trusted_profile_claim_rule_type + + dynamic "conditions" { + for_each = [ + { + claim = "name" + operator = "EQUALS" + value = "\"external-secrets\"" + }, + { + claim = "namespace", + operator = "EQUALS", + value = "\"${var.tp_namespace}\"", + }, + { + claim = "crn", + operator = "EQUALS", + value = "\"${var.tp_cluster_crn}\"" + } + ] + + content { + claim = conditions.value["claim"] + operator = conditions.value["operator"] + value = conditions.value["value"] + } + } +} + +# This Trusted Profile policy grants access to the provided secrets +# manager instance, if one of more secret group ids are provided, it will then +# restrict access to these secret groups with SecretsReader role. + +# migration definition to avoid destruction of resources with support of multiple secrets group +moved { + from = module.your_trusted_profile_module_name.ibm_iam_trusted_profile_policy.policy + to = module.your_trusted_profile_module_name.ibm_iam_trusted_profile_policy.policy[0] +} + +# This Trusted Profile policy grants access to the provided secrets +# manager instance, if no secrets group id or one secrets group id is provided to restrict the access to the Secrets Manager instance +resource "ibm_iam_trusted_profile_policy" "policy" { + count = length(var.secret_groups_id) <= 1 ? 1 : 0 + profile_id = ibm_iam_trusted_profile.trusted_profile.id + description = length(var.secret_groups_id) == 0 ? "IAM Trusted Profile Policy to access the secrets in the target secret groups and secrets manager instance and not restricted to any secrets group" : "IAM Trusted Profile Policy to access the secrets in the target secret group and secrets manager instance" + roles = ["SecretsReader"] + resources { + service = "secrets-manager" + resource_type = length(var.secret_groups_id) == 1 ? "secret-group" : null + resource = length(var.secret_groups_id) == 1 ? var.secret_groups_id[0] : null + resource_instance_id = var.secrets_manager_guid + } +} + +# This Trusted Profile policy grants acccess to the provided secrets +# manager instance, if two or more secrets groups id are provided to restrict the access to the Secrets Manager instance +resource "ibm_iam_trusted_profile_policy" "policy_multiple_secrets_groups" { + count = length(var.secret_groups_id) > 1 ? length(var.secret_groups_id) : 0 + profile_id = ibm_iam_trusted_profile.trusted_profile.id + description = "IAM Trusted Profile Policy to access the secrets in the target secrets group ${var.secret_groups_id[count.index]} and secrets manager instance" + roles = ["SecretsReader"] + resources { + service = "secrets-manager" + resource_type = "secret-group" + resource = var.secret_groups_id[count.index] + resource_instance_id = var.secrets_manager_guid + } +} diff --git a/modules/eso-trusted-profile/outputs.tf b/modules/eso-trusted-profile/outputs.tf new file mode 100644 index 00000000..f5ad9114 --- /dev/null +++ b/modules/eso-trusted-profile/outputs.tf @@ -0,0 +1,13 @@ +############################################################################## +# Outputs +############################################################################## + +output "trusted_profile_id" { + value = ibm_iam_trusted_profile.trusted_profile.id + description = "ID of the trusted profile" +} + +output "trusted_profile_name" { + value = ibm_iam_trusted_profile.trusted_profile.name + description = "Name of the trusted profile" +} diff --git a/modules/eso-trusted-profile/variables.tf b/modules/eso-trusted-profile/variables.tf new file mode 100644 index 00000000..7832b810 --- /dev/null +++ b/modules/eso-trusted-profile/variables.tf @@ -0,0 +1,35 @@ +variable "trusted_profile_name" { + type = string + description = "The name of the trusted profile to be used. This allows ESO to use CRI based authentication to access secrets manager. The trusted profile must be created in advance" +} + +variable "secrets_manager_guid" { + type = string + description = "Secrets manager instance GUID where secrets will be stored or fetched from and the trusted profile will allow access to." +} + +variable "secret_groups_id" { + type = list(string) + description = "The list of secret groups to limit access to for the trusted profile to create." + default = [] +} + +variable "tp_cluster_crn" { + type = string + description = "Target cluster CRN for the trusted profile. Used when creating trusted profile" +} + +variable "trusted_profile_claim_rule_type" { + description = "Trusted profile claim rule type, set the value to 'ROKS_SA' for ROKS clusters, set to ROKS for IKS clusters" + type = string + default = "ROKS_SA" + validation { + condition = var.trusted_profile_claim_rule_type == "ROKS_SA" || var.trusted_profile_claim_rule_type == "ROKS" + error_message = "The trusted_profile_claim_rule_type value must be one of the following: ROKS_SA, ROKS" + } +} + +variable "tp_namespace" { + description = "Namespace to configure in the Trusted Profile on IAM. Its value must be the namespace where the operator is deployed and running." + type = string +} diff --git a/modules/eso-trusted-profile/version.tf b/modules/eso-trusted-profile/version.tf new file mode 100644 index 00000000..268432a0 --- /dev/null +++ b/modules/eso-trusted-profile/version.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + ibm = { + source = "IBM-Cloud/ibm" + version = ">= 1.51.0" + } + } +} diff --git a/outputs.tf b/outputs.tf index bb6ea662..b16a1a35 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,8 +1,3 @@ -######################################################################################################################## +############################################################################## # Outputs -######################################################################################################################## - -#output "myoutput" { -# description = "Description of my output" -# value = "value" -#} +############################################################################## diff --git a/renovate.json b/renovate.json index 8954b604..0da57df4 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,39 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>terraform-ibm-modules/common-dev-assets:commonRenovateConfig"] + "extends": ["github>terraform-ibm-modules/common-dev-assets:commonRenovateConfig"], + "customManagers": [ + { + "customType": "regex", + "description": "Update docker image digest to latest in variables.tf", + "fileMatch": ["variables.tf$"], + "datasourceTemplate": "docker", + "matchStrings": [ + "default\\s*=\\s*\"(?[\\w.-]+)@(?sha256:[a-f0-9]+)\"\\s*# datasource: (?[^\\s]+)" + ] + }, + { + "customType": "regex", + "description": "Update helm chart version to latest in variables.tf", + "fileMatch": ["variables.tf$"], + "matchStrings": [ + "datasource=(?.*?) depName=(?.*?)( versioning=(?.*?))?\\s.*?default = \"(?.*)\"\\s" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" + } + ], + "labels": ["renovate"], + "packageRules": [ + { + "matchPackageNames": ["external-secrets/external-secrets", "stakater/Reloader"], + "groupName": "Charts and Images", + "commitMessageExtra": "to latest", + "group": true + }, + { + "matchDatasources": ["docker"], + "groupName": "Charts and Images", + "commitMessageExtra": "to latest", + "group": true + } + ] } diff --git a/tests/README.md b/tests/README.md index dfd68426..581aa046 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,8 @@ + + # Tests For information about how to create and run tests, see [Validation tests](https://terraform-ibm-modules.github.io/documentation/#/tests) in the project documentation. + diff --git a/tests/go.mod b/tests/go.mod index 64bc6d26..82f452f9 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -5,12 +5,16 @@ go 1.22.4 toolchain go1.23.6 require ( + github.com/gruntwork-io/terratest v0.48.2 github.com/stretchr/testify v1.10.0 github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.45.2 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.28.4 ) require ( dario.cat/mergo v1.0.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/IBM-Cloud/bluemix-go v0.0.0-20240719075425-078fcb3a55be // indirect github.com/IBM-Cloud/power-go-client v1.9.0 // indirect github.com/IBM/cloud-databases-go-sdk v0.7.1 // indirect @@ -24,13 +28,53 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 // indirect + github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.38.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 // indirect + github.com/aws/aws-sdk-go-v2/service/lambda v1.69.0 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.33.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/smithy-go v1.22.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.13.2 // indirect @@ -49,10 +93,15 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gruntwork-io/terratest v0.48.2 // indirect + github.com/gruntwork-io/go-commons v0.8.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-getter/v2 v2.2.3 // indirect @@ -62,9 +111,16 @@ require ( github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl/v2 v2.22.0 // indirect github.com/hashicorp/terraform-json v0.24.0 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/copier v0.4.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -74,14 +130,22 @@ require ( github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pquerna/otp v1.4.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.3.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/tmccombs/hcl2json v0.6.4 // indirect github.com/ulikunitz/xz v0.5.11 // indirect + github.com/urfave/cli v1.22.16 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.15.1 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect @@ -91,11 +155,23 @@ require ( golang.org/x/crypto v0.33.0 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/net v0.34.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.22.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.28.4 // indirect + k8s.io/client-go v0.28.4 // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/tests/go.sum b/tests/go.sum index 25e6d152..4cdd5173 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -1,5 +1,9 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/IBM-Cloud/bluemix-go v0.0.0-20240719075425-078fcb3a55be h1:USOcBHkYQ4o/ccoEvoHinrba8NQthLJpFXnAoBY+MI4= github.com/IBM-Cloud/bluemix-go v0.0.0-20240719075425-078fcb3a55be/go.mod h1:/7hMjdZA6fEpd/dQAOEABxKEwN0t72P3PlpEDu0Y7bE= github.com/IBM-Cloud/power-go-client v1.9.0 h1:nnErpb/7TJQe8P7OfIlJPhSJVq5oyuCJlMje9Ry6XEY= @@ -35,13 +39,91 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= +github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= +github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= +github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 h1:hqcxMc2g/MwwnRMod9n6Bd+t+9Nf7d5qRg7RaXKPd6o= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41/go.mod h1:d1eH0VrttvPmrCraU68LOyNdu26zFxQFjrVSb5vdhog= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg= +github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 h1:fDg0RlN30Xf/yYzEUL/WXqhmgFsjVb/I3230oCfyI5w= +github.com/aws/aws-sdk-go-v2/service/acm v1.30.6/go.mod h1:zRR6jE3v/TcbfO8C2P+H0Z+kShiKKVaVyoIl8NQRjyg= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0 h1:1KzQVZi7OTixxaVJ8fWaJAUBjme+iQ3zBOCZhE4RgxQ= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0/go.mod h1:I1+/2m+IhnK5qEbhS3CrzjeiVloo9sItE/2K+so0fkU= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0 h1:OREVd94+oXW5a+3SSUAo4K0L5ci8cucCLu+PSiek8OU= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0/go.mod h1:Qbr4yfpNqVNl69l/GEDK+8wxLf/vHi0ChoiSDzD7thU= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 h1:vucMirlM6D+RDU8ncKaSZ/5dGrXNajozVwpmWNPn2gQ= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1/go.mod h1:fceORfs010mNxZbQhfqUjUeHlTwANmIT4mvHamuUaUg= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 h1:RhSoBFT5/8tTmIseJUXM6INTXTQDF8+0oyxWBnozIms= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 h1:zg+3FGHA0PBs0KM25qE/rOf2o5zsjNa1g/Qq83+SDI0= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6/go.mod h1:ZSq54Z9SIsOTf1Efwgw1msilSs4XVEfVQiP9nYVnKpM= +github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0 h1:7/vgFWplkusJN/m+3QOa+W9FNRqa8ujMPNmdufRaJpg= +github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0/go.mod h1:dPTOvmjJQ1T7Q+2+Xs2KSPrMvx+p0rpyV+HsQVnUK4o= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.1 h1:hfkzDZHBp9jAT4zcd5mtqckpU4E3Ax0LQaEWWk1VgN8= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.1/go.mod h1:u36ahDtZcQHGmVm/r+0L1sfKX4fzLEMdCqiKRKkUMVM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 h1:3Y457U2eGukmjYjeHG6kanZpDzJADa2m0ADqnuePYVQ= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5/go.mod h1:CfwEHGkTjYZpkQ/5PvcbEtT7AJlG68KkEvmtwU8z3/U= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 h1:CZImQdb1QbU9sGgJ9IswhVkxAcjkkD1eQTMA1KHWk+E= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.6/go.mod h1:YJDdlK0zsyxVBxGU48AR/Mi8DMrGdc1E3Yij4fNrONA= +github.com/aws/aws-sdk-go-v2/service/lambda v1.69.0 h1:BXt75frE/FYtAmEDBJRBa2HexOw+oAZWZl6QknZEFgg= +github.com/aws/aws-sdk-go-v2/service/lambda v1.69.0/go.mod h1:guz2K3x4FKSdDaoeB+TPVgJNU9oj2gftbp5cR8ela1A= +github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 h1:eqHz3Uih+gb0vLE5Cc4Xf733vOxsxDp6GFUUVQU4d7w= +github.com/aws/aws-sdk-go-v2/service/rds v1.91.0/go.mod h1:h2jc7IleH3xHY7y+h8FH7WAZcz3IVLOB6/jXotIQ/qU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 h1:wmt05tPp/CaRZpPV5B4SaJ5TwkHKom07/BzHoLdkY1o= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2/go.mod h1:d+K9HESMpGb1EU9/UmmpInbGIUcAkwmcY6ZO/A3zZsw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 h1:Q2ax8S21clKOnHhhr933xm3JxdJebql+R7aNo7p7GBQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 h1:1KDMKvOKNrpD667ORbZ/+4OgvUoaok1gg/MLzrHF9fw= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6/go.mod h1:DmtyfCfONhOyVAJ6ZMTrDSFIeyCBlEO93Qkfhxwbxu0= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.6 h1:lEUtRHICiXsd7VRwRjXaY7MApT2X4Ue0Mrwe6XbyBro= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.6/go.mod h1:SODr0Lu3lFdT0SGsGX1TzFTapwveBrT5wztVoYtppm8= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 h1:39WvSrVq9DD6UHkD+fx5x19P5KpRQfNdtgReDVNbelc= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1/go.mod h1:3gwPzC9LER/BTQdQZ3r6dUktb1rSjABF1D3Sr6nS7VU= +github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 h1:mADKqoZaodipGgiZfuAjtlcr4IVBtXPZKVjkzUZCCYM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0/go.mod h1:l9qF25TzH95FhcIak6e4vt79KE4I7M2Nf59eMUVjj6c= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= @@ -50,8 +132,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -64,6 +149,9 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU= +github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= @@ -72,6 +160,7 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -114,11 +203,16 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -131,7 +225,11 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -141,11 +239,19 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro= +github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78= github.com/gruntwork-io/terratest v0.48.2 h1:+VwfODchq8jxZZWD+s8gBlhD1z6/C4bFLNrhpm9ONrs= github.com/gruntwork-io/terratest v0.48.2/go.mod h1:Y5ETyD4ZQ2MZhasPno272fWuCpKwvTPYDi8Y0tIMqTE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -174,17 +280,36 @@ github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4 github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -197,10 +322,14 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM= github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -212,7 +341,16 @@ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTS github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -240,6 +378,7 @@ github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3Ro github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= @@ -271,14 +410,24 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -292,6 +441,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper v1.45.2 h1:QEYiEpKaprATt2DayLblT/srjKJLcujXzKaEZS8wHP4= @@ -301,6 +452,9 @@ github.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVL github.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -309,6 +463,7 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -342,6 +497,7 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -358,6 +514,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -383,6 +540,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -397,7 +556,9 @@ golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -467,10 +628,14 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= @@ -508,12 +673,15 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -521,3 +689,21 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/tests/other_test.go b/tests/other_test.go deleted file mode 100644 index d03784f3..00000000 --- a/tests/other_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// Tests in this file are NOT run in the PR pipeline. They are run in the continuous testing pipeline along with the ones in pr_test.go -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRunBasicExample(t *testing.T) { - t.Parallel() - - options := setupOptions(t, "mod-template-basic", "examples/basic") - - output, err := options.RunTestConsistency() - assert.Nil(t, err, "This should not have errored") - assert.NotNil(t, output, "Expected some output") -} diff --git a/tests/pr_test.go b/tests/pr_test.go index 370bfac3..c867f9af 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -1,45 +1,394 @@ -// Tests in this file are run in the PR pipeline and the continuous testing pipeline +// Tests in this file are run in the PR pipeline package test import ( + "log" + "os" "testing" + "time" + "github.com/gruntwork-io/terratest/modules/k8s" "github.com/stretchr/testify/assert" + "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" + "gopkg.in/yaml.v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// Use existing resource group -const resourceGroup = "geretain-test-resources" -const advancedExampleDir = "examples/advanced" +const resourceGroup = "geretain-test-ext-secrets-sync" +const defaultExampleTerraformDir = "examples/all-combined" +const basicExampleTerraformDir = "examples/basic" -func setupOptions(t *testing.T, prefix string, dir string) *testhelper.TestOptions { +// 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" + +type Config struct { + SmGuid string `yaml:"secretsManagerGuid"` + SmCRN string `yaml:"secretsManagerCRN"` + SmRegion string `yaml:"secretsManagerRegion"` + RgId string `yaml:"resourceGroupTestPermanentId"` + CisName string `yaml:"cisInstanceName"` + + // secret ids for the secrets composing the imported certificate to create + ImpCertIntermediateSecretId string `yaml:"imported_certificate_intermediate_secret_id"` + ImpCertPublicSecretId string `yaml:"imported_certificate_public_secret_id"` + ImpCertPrivateSecretId string `yaml:"imported_certificate_private_secret_id"` + ImpCertificateSmGuid string `yaml:"imported_certificate_sm_id"` + ImpCertificateSmRegion string `yaml:"imported_certificate_sm_region"` + + // acme private apikey references for CA + AcmeLEPrivateKeySmGuid string `yaml:"acme_letsencrypt_private_key_sm_id"` + AcmeLEPrivateKeySmRegion string `yaml:"acme_letsencrypt_private_key_sm_region"` + AcmeLEPrivateKeySecretId string `yaml:"acme_letsencrypt_private_key_secret_id"` +} + +var smGuid string +var smCRN string +var smRegion string +var rgId string +var cisName string +var impCertificateSmRegion string +var impCertificateSmGuid string +var impCertIntermediateSecretID string +var impCertPublicSecretID string +var impCertPrivateSecretID string +var acmeLEPrivateKeySmGuid string +var acmeLEPrivateKeySmRegion string +var acmeLEPrivateKeySecretId string + +// terraform vars for all-combined test (including Upgrade one) +var allCombinedTerraformVars map[string]interface{} + +// TestMain will be run before any parallel tests, used to read data from yaml for use with tests +func TestMain(m *testing.M) { + // Read the YAML file contents + data, err := os.ReadFile(yamlLocation) + if err != nil { + log.Fatal(err) + } + // Create a struct to hold the YAML data + var config Config + // Unmarshal the YAML data into the struct + err = yaml.Unmarshal(data, &config) + if err != nil { + log.Fatal(err) + } + + // Parse the SM guid and region from data and setting all-combined test input values used in TestRunDefaultExample and TestRunUpgradeExample + smGuid = config.SmGuid + smCRN = config.SmCRN + smRegion = config.SmRegion + cisName = config.CisName + rgId = config.RgId + impCertIntermediateSecretID = config.ImpCertIntermediateSecretId + impCertPrivateSecretID = config.ImpCertPrivateSecretId + impCertPublicSecretID = config.ImpCertPublicSecretId + acmeLEPrivateKeySmGuid = config.AcmeLEPrivateKeySmGuid + acmeLEPrivateKeySmRegion = config.AcmeLEPrivateKeySmRegion + acmeLEPrivateKeySecretId = config.AcmeLEPrivateKeySecretId + impCertificateSmGuid = config.ImpCertificateSmGuid + impCertificateSmRegion = config.ImpCertificateSmRegion + + allCombinedTerraformVars = map[string]interface{}{ + "existing_cis_instance_name": cisName, + "existing_cis_instance_resource_group_id": rgId, + // imported certificate and public certificate creation management + "existing_sm_instance_crn": smCRN, + "existing_sm_instance_guid": smGuid, + "existing_sm_instance_region": smRegion, + "imported_certificate_sm_region": impCertificateSmRegion, + "imported_certificate_sm_id": impCertificateSmGuid, + "imported_certificate_intermediate_secret_id": impCertIntermediateSecretID, + "imported_certificate_public_secret_id": impCertPublicSecretID, + "imported_certificate_private_secret_id": impCertPrivateSecretID, + "acme_letsencrypt_private_key_secret_id": acmeLEPrivateKeySecretId, + "acme_letsencrypt_private_key_sm_id": acmeLEPrivateKeySmGuid, + "acme_letsencrypt_private_key_sm_region": acmeLEPrivateKeySmRegion, + // setting skip_iam_authorization_policy to true because using the existing secrets manager instance and the policy already exists + "skip_iam_authorization_policy": true, + "service_endpoints": "public", + // setting CIS domain to be used in the test + "pvt_cert_common_name": "goldeneye.dev.cloud.ibm.com", + "pvt_root_ca_common_name": "goldeneye.dev.cloud.ibm.com", + "cert_common_name": "goldeneye.dev.cloud.ibm.com", + } + + os.Exit(m.Run()) +} + +var ignoreUpdates = []string{ + "module.es_kubernetes_secret_usr_pass.helm_release.external_secrets_operator[0]", + "module.es_kubernetes_secret_arbitrary_cloudant.helm_release.external_secrets_operator[0]", + "module.es_kubernetes_secret_arbitrary_cr_registry.helm_release.external_secrets_operator[0]", + "module.es_kubernetes_secret_image_pull.helm_release.external_secrets_operator[0]", + "module.external_secrets_operator.helm_release.external_secrets_operator", + "module.external_secrets_operator.helm_release.pod_reloader[0]", + "module.external_secret_arbitrary_cloudant.helm_release.kubernetes_secret[0]", + "module.external_secret_tp_multisg_2.helm_release.kubernetes_secret[0]", + "module.external_secret_imported_certificate[0].helm_release.kubernetes_secret_certificate[0]", + "module.external_secret_tp[0].helm_release.kubernetes_secret[0]", + "module.external_secret_private_certificate.helm_release.kubernetes_secret_certificate[0]", + "module.external_secret_kv_multiplekeys.helm_release.kubernetes_secret_kv_all[0]", + "module.external_secret_arbitrary_cr_registry.helm_release.kubernetes_secret[0]", + "module.external_secret_secret_image_pull.helm_release.kubernetes_secret[0]", + "module.external_secret_public_certificate[0].helm_release.kubernetes_secret_certificate[0]", + "module.external_secret_kv_singlekey.helm_release.kubernetes_secret_kv_key[0]", + "module.external_secret_tp[1].helm_release.kubernetes_secret[0]", + "module.external_secret_tp_multisg_1.helm_release.kubernetes_secret[0]", + "module.external_secret_usr_pass.helm_release.kubernetes_secret_user_pw[0]", + "module.external_secret_tp_nosg.helm_release.kubernetes_secret[0]", + "module.sdnlb_eso_secret.helm_release.sdnlb_external_secret", +} + +func setupOptions(t *testing.T, prefix string, terraformDir string, terraformVars map[string]interface{}) *testhelper.TestOptions { options := testhelper.TestOptionsDefaultWithVars(&testhelper.TestOptions{ Testing: t, - TerraformDir: dir, + TerraformDir: terraformDir, Prefix: prefix, ResourceGroup: resourceGroup, + TerraformVars: terraformVars, + + IgnoreUpdates: testhelper.Exemptions{ + List: ignoreUpdates, + }, + + IgnoreDestroys: testhelper.Exemptions{ // Ignore for consistency check + List: []string{ + // adding resources to ignore for modules version update - to be removed after the merge + "module.ocp_base.time_sleep.wait_operators", + }, + }, }) + return options } -func TestRunAdvancedExample(t *testing.T) { +func TestRunDefaultExample(t *testing.T) { t.Parallel() - options := setupOptions(t, "mod-template", advancedExampleDir) + options := setupOptions(t, "eso", defaultExampleTerraformDir, allCombinedTerraformVars) + options.SkipTestTearDown = true + defer func() { + options.TestTearDown() + }() output, err := options.RunTestConsistency() - assert.Nil(t, err, "This should not have errored") + + if assert.Nil(t, err, "Consistency test should not have errored") { + outputs := options.LastTestTerraformOutputs + _, tfOutputsErr := testhelper.ValidateTerraformOutputs(outputs, "cluster_id") + if assert.Nil(t, tfOutputsErr, tfOutputsErr) { + log.Println("Prefix used " + options.Prefix) + + clusterId := outputs["cluster_id"].(string) + + log.Println("clusterId " + clusterId) + + // building the list of secrets to test + namespaces_for_apikey_login := []string{"apikeynspace1", "apikeynspace2", "apikeynspace3", "apikeynspace4"} + namespaces_for_tp_login := []string{"tpnspace1", "tpnspace2"} + + secretsMap := map[string]string{ + "dockerconfigjson-uc": namespaces_for_apikey_login[0], + // temporary disabled cloudant resource key secret test + "dockerconfigjson-arb": namespaces_for_apikey_login[2], + "pvtcertificate-tls": namespaces_for_apikey_login[2], + "kv-single-key": namespaces_for_apikey_login[3], + "kv-multiple-keys": namespaces_for_apikey_login[3], + "dockerconfigjson-iam": namespaces_for_apikey_login[3], + "dockerconfigjson-chain": namespaces_for_apikey_login[3], + options.Prefix + "-arbitrary-arb-tp-0": namespaces_for_tp_login[0], + options.Prefix + "-arbitrary-arb-tp-1": namespaces_for_tp_login[1], + options.Prefix + "-arbitrary-arb-tp-multisg-1": "tpns-multisg", + options.Prefix + "-arbitrary-arb-tp-multisg-2": "tpns-multisg", + options.Prefix + "-arbitrary-arb-tp-nosg": "tpns-nosg", + options.Prefix + "-arbitrary-arb-cstore-tp": "eso-cstore-tp-namespace", + } + + log.Printf("secretsMap %s", secretsMap) + + // get cluster config + log.Println("Loading cluster configuration with id " + clusterId) + cloudinfosvc, err := cloudinfo.NewCloudInfoServiceFromEnv("TF_VAR_ibmcloud_api_key", cloudinfo.CloudInfoServiceOptions{}) + if assert.Nil(t, err, "Error creating cloud info service") { + clusterConfigPath, err := cloudinfosvc.GetClusterConfigConfigPath(clusterId) + defer func() { + // attempt to remove cluster config file after test + _ = os.Remove(clusterConfigPath) + }() + if assert.Nil(t, err, "Error getting cluster config path") { + // for each secret to test configure Terratest with cluster config + // the test checks if each secret is correctly created in the cluster + for secretName, secretNamespace := range secretsMap { + ocOptions := k8s.NewKubectlOptions("", clusterConfigPath, secretNamespace) + log.Printf("Testing secret name %s namespace %s\n", secretName, secretNamespace) + _, err := k8s.GetSecretE(t, ocOptions, secretName) + assert.Nil(t, err, "Error retrieving secret "+secretName+" in namespace "+secretNamespace) + } + } + } + } + } + assert.NotNil(t, output, "Expected some output") } func TestRunUpgradeExample(t *testing.T) { t.Parallel() - options := setupOptions(t, "mod-template-upg", advancedExampleDir) - + options := setupOptions(t, "eso-upg", defaultExampleTerraformDir, allCombinedTerraformVars) output, err := options.RunTestUpgrade() if !options.UpgradeTestSkipped { assert.Nil(t, err, "This should not have errored") assert.NotNil(t, output, "Expected some output") } } + +func TestReloaderOperational(t *testing.T) { + t.Parallel() + // terraform vars for reloader test + reloaderTerraformVars := map[string]interface{}{} + + reloaderTerraformVars["existing_sm_instance_guid"] = smGuid + reloaderTerraformVars["existing_sm_instance_region"] = smRegion + + options := setupOptions(t, "reloader", basicExampleTerraformDir, reloaderTerraformVars) + + options.SkipTestTearDown = true + defer func() { + options.TestTearDown() + }() + + _, err := options.RunTestConsistency() + if assert.Nil(t, err, "Consistency test should not have errored") { + outputs := options.LastTestTerraformOutputs + _, tfOutputsErr := testhelper.ValidateTerraformOutputs(outputs, "cluster_id") + if assert.Nil(t, tfOutputsErr, tfOutputsErr) { + + // get cluster config + cloudinfosvc, err := cloudinfo.NewCloudInfoServiceFromEnv("TF_VAR_ibmcloud_api_key", cloudinfo.CloudInfoServiceOptions{}) + if assert.Nil(t, err, "Error creating cloud info service") { + clusterConfigPath, err := cloudinfosvc.GetClusterConfigConfigPath(outputs["cluster_id"].(string)) + defer func() { + // attempt to remove cluster config file after test + _ = os.Remove(clusterConfigPath) + }() + if assert.Nil(t, err, "Error getting cluster config path") { + sampleApp := "./samples/sample.yaml" + deploymentName := "example-deployment" + namespace := "reloader-test-ns" + containerName := "busybox-container" + secretName := "example-secret" + secretValue := "top-secret" + updatedSecret := "./samples/updated_secret.yaml" // pragma: allowlist secret + updatedSecretValue := "updated-secret" + + sleepBetweenRetries := 20 * time.Second + // configure Terratest with cluster config + ocOptions := k8s.NewKubectlOptions("", clusterConfigPath, namespace) + // deploy sample app + applyError := k8s.KubectlApplyE(t, ocOptions, sampleApp) + if assert.Nil(t, applyError, "Error applying sample app") { + // confirm app is running + k8s.WaitUntilDeploymentAvailable(t, ocOptions, deploymentName, 20, sleepBetweenRetries) + k8s.WaitUntilSecretAvailable(t, ocOptions, secretName, 20, sleepBetweenRetries) + // Check that the secret value is correct + // Get pod name from deployment + pods, err := GetPodNamesFromDeployment(t, ocOptions, deploymentName) + if assert.Nil(t, err, "Error getting pod names") { + initialPod := k8s.GetPod(t, ocOptions, pods[0]) + k8s.WaitUntilPodAvailable(t, ocOptions, initialPod.Name, 20, sleepBetweenRetries) + logs := k8s.GetPodLogs(t, ocOptions, initialPod, containerName) + if assert.Contains(t, logs, secretValue, "Initial Secret value not found in logs") { + t.Log("Initial secret value found in logs") + t.Log(logs) + } + // update secret with updated secret + applyError = k8s.KubectlApplyE(t, ocOptions, updatedSecret) + if assert.Nil(t, applyError, "Error applying updated secret") { + // Set a timeout duration + timeout := 20 * time.Second + + // Create a channel to signal the end of the timeout + timeoutChan := time.After(timeout) + var newPodName string + failed := false + // Loop until a new initialPod is found or until timeout + // Using a label on the for loop allows us to break out of the loop, otherwise the break would only break out of the select statement + Loop: + for { + select { + case <-timeoutChan: + // Handle the timeout case + assert.Fail(t, "timeout reached while waiting for initialPod to change") + failed = true + break Loop + default: + // Sleep to avoid busy waiting + time.Sleep(time.Second) + + // Update initialPod names + currentPods, err := GetPodNamesFromDeployment(t, ocOptions, deploymentName) + if err != nil { + t.Log("Error getting initialPod names") + break + } + + for _, pod := range currentPods { + if !common.StrArrayContains(pods, pod) { + newPodName = pod + break Loop + } + } + + if newPodName != "" { + break + } + } + } + if !failed { + k8s.WaitUntilDeploymentAvailable(t, ocOptions, deploymentName, 20, sleepBetweenRetries) + newPod := k8s.GetPod(t, ocOptions, newPodName) + k8s.WaitUntilPodAvailable(t, ocOptions, newPod.Name, 20, sleepBetweenRetries) + // confirm app restarted and picked up new secret by checking logs + newLogs := k8s.GetPodLogs(t, ocOptions, newPod, containerName) + if assert.Contains(t, newLogs, updatedSecretValue, "Updated Secret value not found in logs") { + t.Log("Updated secret value found in logs") + t.Log(newLogs) + } + } + } + } + } + } + } + + } + } +} + +func GetPodNamesFromDeployment(t *testing.T, options *k8s.KubectlOptions, deploymentName string) ([]string, error) { + // Get the deployment object + deployment, err := k8s.GetDeploymentE(t, options, deploymentName) + if err != nil { + return nil, err + } + + // Construct the label selector from the deployment + labelSelector := metav1.FormatLabelSelector(deployment.Spec.Selector) + + // List Pods using label selector + pods, err := k8s.ListPodsE(t, options, metav1.ListOptions{LabelSelector: labelSelector}) + if err != nil { + return nil, err + } + + // Extract the pod names + var podNames []string + for _, pod := range pods { + podNames = append(podNames, pod.Name) + } + + return podNames, nil +} diff --git a/tests/samples/README.md b/tests/samples/README.md new file mode 100644 index 00000000..e467be55 --- /dev/null +++ b/tests/samples/README.md @@ -0,0 +1,4 @@ +# Sample App + +This is a sample app to test the reloader is in a working state. First the test deploys sample.yaml, +then it deploys updated_secret.yaml. The reloader should detect the change and redeploy the pod. diff --git a/tests/samples/sample.yaml b/tests/samples/sample.yaml new file mode 100644 index 00000000..dfcf1a92 --- /dev/null +++ b/tests/samples/sample.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: reloader-test-ns +--- +apiVersion: v1 +kind: Secret +metadata: + name: example-secret + namespace: reloader-test-ns + annotations: + reloader.stakater.com/auto: "true" +data: + # base64 encoded 'top-secret' + mysecret: dG9wLXNlY3JldAo= # pragma: allowlist secret +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: example-deployment + namespace: reloader-test-ns + annotations: + reloader.stakater.com/auto: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: example-busybox + template: + metadata: + labels: + app: example-busybox + spec: + nodeSelector: + dedicated: default + containers: + - name: busybox-container + image: busybox + command: ["/bin/sh"] + args: ["-c", "while true; do echo $(date);echo $MY_SECRET; sleep 5; done"] + env: + - name: MY_SECRET + valueFrom: + secretKeyRef: + name: example-secret + key: mysecret + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" diff --git a/tests/samples/updated_secret.yaml b/tests/samples/updated_secret.yaml new file mode 100644 index 00000000..1029a9c9 --- /dev/null +++ b/tests/samples/updated_secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: example-secret + namespace: reloader-test-ns + annotations: + reloader.stakater.com/auto: "true" +data: + # base64 encoded 'updated-secret' + mysecret: dXBkYXRlZC1zZWNyZXQK # pragma: allowlist secret diff --git a/variables.tf b/variables.tf index df604346..40b13534 100644 --- a/variables.tf +++ b/variables.tf @@ -1,9 +1,228 @@ -######################################################################################################################## -# Input Variables -######################################################################################################################## - -#variable "my_variable" { -# type = string -# description = "A description of my variable" -# default = "default_value" -#} +############################################################################################################ +# EXTERNAL SECRETS CONFIGURATIONS +############################################################################################################ + +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" + 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" + type = string + default = null +} + +# ESO deployment cluster nodes configuration +variable "eso_cluster_nodes_configuration" { + description = "Configuration to use to customise ESO deployment on specific cluster nodes. Setting appropriate values will result in customising ESO helm release. Default value is null to keep ESO standard deployment." + 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. Setting appropriate values will result in customising ESO helm release. Default value is {} to keep ESO standard deployment. Ignore the key if not required." + 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 = {} +} + +# ESO +variable "eso_enroll_in_servicemesh" { + description = "Flag to enroll ESO into istio servicemesh" + type = bool + default = false +} + +# external secrets 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.12.1-ubi@sha256:d38834043de0a4e4feeac8a08d0bc96b71ddd7fe1d4c8583ee3751badeaeb01d" # 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." + # renovate: datasource=github-tags depName=external-secrets/external-secrets versioning="regex:^helm-chart-(?\\d+)\\.(?\\d+)\\.(?\\d+)$" + default = "0.12.1" + nullable = false +} + +############################################################################################################ +# RELOADER CONFIGURATIONS +############################################################################################################ + +# Reloader variables full documentation https://github.com/stakater/Reloader/tree/master#helm-charts +variable "reloader_deployed" { + description = "Whether to deploy reloader or not https://github.com/stakater/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 = string + default = null +} +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 = string + default = null +} +variable "reloader_namespaces_selector" { + description = "List of comma separated label selectors, if multiple are provided they are combined with the AND operator" + type = string + default = null +} +variable "reloader_resource_label_selector" { + description = "List of comma separated label selectors, if multiple are provided they are combined with the AND operator" + type = string + default = null +} +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. See 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 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.2.1-ubi@sha256:80a557100c6835c7e3c9842194250c9c4ca78f43200bc3a93a32e5b105ad11bb" # 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." + # renovate: datasource=github-releases depName=stakater/Reloader + default = "1.2.1" + nullable = false +} diff --git a/version.tf b/version.tf index f15f6031..dfc5b80a 100644 --- a/version.tf +++ b/version.tf @@ -1,12 +1,14 @@ terraform { - required_version = ">= 1.3.0" - # If your module requires any terraform providers, uncomment the "required_providers" section below and add all required providers. - # Each required provider's version should be a flexible range to future proof the module's usage with upcoming minor and patch versions. - - # required_providers { - # ibm = { - # source = "IBM-Cloud/ibm" - # version = ">= 1.64.0, < 2.0.0" - # } - # } + required_version = ">= 1.0.0" + required_providers { + # Use "greater than or equal to" range in modules + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.16.1, < 3.0.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.11.0, < 3.0.0" + } + } }