From afd97f62b77cadacbbf76a777bad251bc5c092a5 Mon Sep 17 00:00:00 2001 From: akocbek Date: Tue, 3 Jun 2025 17:23:17 +0100 Subject: [PATCH 1/7] feat: add a new feature to run builds --- ibm_catalog.json | 3 + modules/build/README.md | 2 + modules/build/outputs.tf | 10 +++ solutions/project/main.tf | 77 +++++++++++++++++++++- solutions/project/scripts/build-run.sh | 38 +++++++++++ solutions/project/scripts/get-cr-region.sh | 33 ++++++++++ solutions/project/variables.tf | 16 ++++- solutions/project/version.tf | 8 +++ tests/pr_test.go | 7 ++ 9 files changed, 191 insertions(+), 3 deletions(-) create mode 100755 solutions/project/scripts/build-run.sh create mode 100755 solutions/project/scripts/get-cr-region.sh diff --git a/ibm_catalog.json b/ibm_catalog.json index 4aa430b..a095123 100644 --- a/ibm_catalog.json +++ b/ibm_catalog.json @@ -352,6 +352,9 @@ { "key": "builds" }, + { + "key": "container_registry_namespace" + }, { "key": "domain_mappings" }, diff --git a/modules/build/README.md b/modules/build/README.md index c19dd65..1065be1 100644 --- a/modules/build/README.md +++ b/modules/build/README.md @@ -73,4 +73,6 @@ No modules. | [build\_id](#output\_build\_id) | The ID of the created code engine build. | | [id](#output\_id) | The unique identifier of the created code engine build. | | [name](#output\_name) | The name of the created code engine build. | +| [output\_image](#output\_output\_image) | The container image reference of the created code engine build. | +| [output\_secret](#output\_output\_secret) | The registry secret of the created code engine build. | diff --git a/modules/build/outputs.tf b/modules/build/outputs.tf index 5f01b37..240f76b 100644 --- a/modules/build/outputs.tf +++ b/modules/build/outputs.tf @@ -16,3 +16,13 @@ output "name" { description = "The name of the created code engine build." value = resource.ibm_code_engine_build.ce_build.name } + +output "output_image" { + description = "The container image reference of the created code engine build." + value = resource.ibm_code_engine_build.ce_build.output_image +} + +output "output_secret" { + description = "The registry secret of the created code engine build." + value = resource.ibm_code_engine_build.ce_build.output_secret +} diff --git a/solutions/project/main.tf b/solutions/project/main.tf index c251c8d..2a83294 100644 --- a/solutions/project/main.tf +++ b/solutions/project/main.tf @@ -26,10 +26,48 @@ module "project" { ############################################################################## # Code Engine Build ############################################################################## +locals { + + container_registry = "private.${data.external.container_registry_region.result["registry"]}" + + # if no build defines a container image reference (output_image), a new container registry namespace must be created using container_registry_namespace. + any_missing_output_image = anytrue([ + for build in values(var.builds) : + !contains(keys(build), "output_image") || build.output_image == null + ]) + image_container = local.any_missing_output_image ? "${local.container_registry}/${resource.ibm_cr_namespace.my_namespace[0].name}" : "" + + # if output_image not exists then a new created container image reference + updated_builds = { + for name, build in var.builds : + name => merge( + build, + { + output_image = coalesce(build.output_image, "${local.image_container}/${name}") + } + ) + } +} + +resource "ibm_cr_namespace" "my_namespace" { + count = local.any_missing_output_image && var.container_registry_namespace != null ? 1 : 0 + name = var.container_registry_namespace +} + +data "external" "container_registry_region" { + program = ["bash", "${path.module}/scripts/get-cr-region.sh"] + + query = { + RESOURCE_GROUP_ID = module.resource_group.resource_group_id + REGION = var.region + IBMCLOUD_API_KEY = var.ibmcloud_api_key + } +} + module "build" { depends_on = [module.secret] source = "../../modules/build" - for_each = var.builds + for_each = local.updated_builds project_id = module.project.project_id name = each.key output_image = each.value.output_image @@ -43,7 +81,21 @@ module "build" { strategy_size = each.value.strategy_size strategy_spec_file = each.value.strategy_spec_file timeout = each.value.timeout +} +resource "null_resource" "run_build" { + depends_on = [module.build] + provisioner "local-exec" { + interpreter = ["/bin/bash", "-c"] + command = "${path.module}/scripts/build-run.sh" + environment = { + IBMCLOUD_API_KEY = var.ibmcloud_api_key + RESOURCE_GROUP_ID = module.resource_group.resource_group_id + CE_PROJECT_NAME = module.project.name + REGION = var.region + BUILDS = join(" ", keys(local.updated_builds)) + } + } } ############################################################################## @@ -72,9 +124,30 @@ module "config_map" { ############################################################################## # Code Engine Secret ############################################################################## +locals { + # if the secret is a registry type, inject generated credentials (username, password, server) if they're not already provided. + secrets = { + for name, secret in var.secrets : + name => merge( + secret, + { + data = ( + secret.format == "registry" + ? merge(secret.data, { + password = coalesce(secret.data.password, var.ibmcloud_api_key), + username = coalesce(secret.data.username, "iamapikey"), + server = coalesce(secret.data.server, local.container_registry) + }) + : secret.data + ) + } + ) + } +} + module "secret" { source = "../../modules/secret" - for_each = var.secrets + for_each = local.secrets project_id = module.project.project_id name = each.key data = sensitive(each.value.data) diff --git a/solutions/project/scripts/build-run.sh b/solutions/project/scripts/build-run.sh new file mode 100755 index 0000000..210d338 --- /dev/null +++ b/solutions/project/scripts/build-run.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +if [[ -z "${IBMCLOUD_API_KEY}" ]]; then + echo "IBMCLOUD_API_KEY is required" >&2 + exit 1 +fi + +if [[ -z "${RESOURCE_GROUP_ID}" ]]; then + echo "RESOURCE_GROUP_ID is required" >&2 + exit 1 +fi + +if [[ -z "${CE_PROJECT_NAME}" ]]; then + echo "CE_PROJECT_NAME is required" >&2 + exit 1 +fi + +if [[ -z "${REGION}" ]]; then + echo "REGION is required" >&2 + exit 1 +fi + +if [[ -z "${BUILDS}" ]]; then + echo "BUILDS is required" >&2 + exit 1 +fi + +ibmcloud login -r "${REGION}" -g "${RESOURCE_GROUP_ID}" --apikey "${IBMCLOUD_API_KEY}" + +# selecet the right code engine project +ibmcloud ce project select -n "${CE_PROJECT_NAME}" + +# run a build for all builds +for build in $BUILDS; do + echo "$build" + ibmcloud ce buildrun submit --build "$build" +done diff --git a/solutions/project/scripts/get-cr-region.sh b/solutions/project/scripts/get-cr-region.sh new file mode 100755 index 0000000..1724cad --- /dev/null +++ b/solutions/project/scripts/get-cr-region.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +INPUT=$(cat) +REGION=$(echo "$INPUT" | jq -r '.REGION') +RESOURCE_GROUP_ID=$(echo "$INPUT" | jq -r '.RESOURCE_GROUP_ID') +IBMCLOUD_API_KEY=$(echo "$INPUT" | jq -r '.IBMCLOUD_API_KEY') + +if [[ -z "${IBMCLOUD_API_KEY}" || "${IBMCLOUD_API_KEY}" == "null" ]]; then + echo "IBMCLOUD_API_KEY is required" >&2 + exit 1 +fi + +if [[ -z "${RESOURCE_GROUP_ID}" || "${RESOURCE_GROUP_ID}" == "null" ]]; then + echo "RESOURCE_GROUP_ID is required" >&2 + exit 1 +fi + +if [[ -z "${REGION}" || "${REGION}" == "null" ]]; then + echo "REGION is required" >&2 + exit 1 +fi + +# Login to IBM Cloud quietly +if ! ibmcloud login -r "${REGION}" -g "${RESOURCE_GROUP_ID}" --apikey "${IBMCLOUD_API_KEY}" --quiet > /dev/null 2>&1; then + exit 1 +fi + +# extract registry value from text "You are targeting region 'us-south', the registry is 'us.icr.io'." +registry=$(ibmcloud cr region 2>/dev/null | grep registry | sed -E "s/.*registry is '([^']+)'.*/\1/") || exit 1 + +# Output valid JSON for Terraform external data source +echo "{\"registry\": \"${registry}\"}" diff --git a/solutions/project/variables.tf b/solutions/project/variables.tf index da30505..3c980ff 100644 --- a/solutions/project/variables.tf +++ b/solutions/project/variables.tf @@ -46,7 +46,7 @@ variable "project_name" { variable "builds" { description = "A map of code engine builds to be created.[Learn more](https://github.com/terraform-ibm-modules/terraform-ibm-code-engine/blob/main/solutions/project/DA-inputs.md#builds)" type = map(object({ - output_image = string + output_image = optional(string) output_secret = string # pragma: allowlist secret source_url = string strategy_type = string @@ -61,6 +61,20 @@ variable "builds" { default = {} } +variable "container_registry_namespace" { + description = "The name of the namespace to create in IBM Cloud Container Registry for organizing container images. Used only for builds that do not have output_image set." + type = string + default = null + + validation { + condition = alltrue([ + for build in values(var.builds) : + contains(keys(build), "output_image") && build.output_image != null + ]) || var.container_registry_namespace != null + error_message = "container_registry_namespace is required because at least one build is missing an output_image" + } +} + ############################################################################## # Code Engine Domain Mapping ############################################################################## diff --git a/solutions/project/version.tf b/solutions/project/version.tf index 6d7d1d9..365303a 100644 --- a/solutions/project/version.tf +++ b/solutions/project/version.tf @@ -6,5 +6,13 @@ terraform { source = "IBM-Cloud/ibm" version = "1.79.2" } + external = { + source = "hashicorp/external" + version = "2.3.5" + } + null = { + source = "hashicorp/null" + version = "3.2.4" + } } } diff --git a/tests/pr_test.go b/tests/pr_test.go index 11ba61f..265fe24 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -262,6 +262,7 @@ func TestDeployCEProjectDA(t *testing.T) { "provider_visibility": "public", "project_name": prefix, "existing_resource_group_name": resourceGroup, + "container_registry_namespace": prefix, "builds": map[string]interface{}{ fmt.Sprintf("%s-build", prefix): map[string]interface{}{ "output_image": fmt.Sprintf("us.icr.io/%s/%s", terraform.Output(t, existingTerraformOptions, "cr_name"), prefix), @@ -269,6 +270,12 @@ func TestDeployCEProjectDA(t *testing.T) { "source_url": "https://github.com/IBM/CodeEngine", "strategy_type": "dockerfile", }, + fmt.Sprintf("%s-build-2", prefix): map[string]interface{}{ + "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret + "source_url": "https://github.com/IBM/CodeEngine", + "strategy_type": "dockerfile", + "source_context_dir": "hello", + }, }, "config_maps": map[string]interface{}{ fmt.Sprintf("%s-cm", prefix): map[string]interface{}{ From b3531c9fb6153d5fa4a3ce3ce148eaba2d057930 Mon Sep 17 00:00:00 2001 From: akocbek Date: Thu, 5 Jun 2025 08:04:44 +0100 Subject: [PATCH 2/7] update test --- tests/pr_test.go | 233 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 177 insertions(+), 56 deletions(-) diff --git a/tests/pr_test.go b/tests/pr_test.go index 265fe24..a3d1804 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -2,9 +2,15 @@ package test import ( + "bytes" "fmt" "log" "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" "strings" "testing" @@ -247,67 +253,77 @@ func TestDeployCEProjectDA(t *testing.T) { if existErr != nil { assert.True(t, existErr == nil, "Init and Apply of temp existing resource failed") } else { - outputs, err := terraform.OutputAllE(t, existingTerraformOptions) - require.NoError(t, err, "Failed to retrieve Terraform outputs") - expectedOutputs := []string{"tls_cert", "tls_key", "cr_name"} - _, tfOutputsErr := testhelper.ValidateTerraformOutputs(outputs, expectedOutputs...) - if assert.Nil(t, tfOutputsErr, tfOutputsErr) { - options := testhelper.TestOptionsDefault(&testhelper.TestOptions{ - Testing: t, - TerraformDir: "solutions/project", - // Do not hard fail the test if the implicit destroy steps fail to allow a full destroy of resource to occur - ImplicitRequired: false, - TerraformVars: map[string]interface{}{ - "prefix": prefix, - "provider_visibility": "public", - "project_name": prefix, - "existing_resource_group_name": resourceGroup, - "container_registry_namespace": prefix, - "builds": map[string]interface{}{ - fmt.Sprintf("%s-build", prefix): map[string]interface{}{ - "output_image": fmt.Sprintf("us.icr.io/%s/%s", terraform.Output(t, existingTerraformOptions, "cr_name"), prefix), - "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret - "source_url": "https://github.com/IBM/CodeEngine", - "strategy_type": "dockerfile", - }, - fmt.Sprintf("%s-build-2", prefix): map[string]interface{}{ - "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret - "source_url": "https://github.com/IBM/CodeEngine", - "strategy_type": "dockerfile", - "source_context_dir": "hello", - }, + + cr_name, _ := getTerraformOutput(tempTerraformDir+"/tests/resources", "cr_name") + tls_cert, _ := getTerraformOutput(tempTerraformDir+"/tests/resources", "tls_cert") + tls_key, _ := getTerraformOutput(tempTerraformDir+"/tests/resources", "tls_key") + + tfVars := map[string]interface{}{ + "prefix": prefix, + "provider_visibility": "public", + "project_name": prefix, + "existing_resource_group_name": resourceGroup, + "container_registry_namespace": cr_name, + "builds": map[string]interface{}{ + fmt.Sprintf("%s-build", prefix): map[string]interface{}{ + "output_image": fmt.Sprintf("private.us.icr.io/%s/%s", cr_name, prefix), + "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret + "source_url": "https://github.com/IBM/CodeEngine", + "strategy_type": "dockerfile", + }, + fmt.Sprintf("%s-build-2", prefix): map[string]interface{}{ + "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret + "source_url": "https://github.com/IBM/CodeEngine", + "strategy_type": "dockerfile", + "source_context_dir": "hello", + }, + }, + "config_maps": map[string]interface{}{ + fmt.Sprintf("%s-cm", prefix): map[string]interface{}{ + "data": map[string]interface{}{ + "key_1": "value_1", + "key_2": "value_2", }, - "config_maps": map[string]interface{}{ - fmt.Sprintf("%s-cm", prefix): map[string]interface{}{ - "data": map[string]interface{}{ - "key_1": "value_1", - "key_2": "value_2", - }, - }, + }, + }, + "secrets": map[string]interface{}{ + fmt.Sprintf("%s-tls", prefix): map[string]interface{}{ + "format": "tls", + "data": map[string]string{ + "tls_cert": strings.ReplaceAll(tls_cert, "\n", `\n`), + "tls_key": strings.ReplaceAll(tls_key, "\n", `\n`), }, - "secrets": map[string]interface{}{ - fmt.Sprintf("%s-tls", prefix): map[string]interface{}{ - "format": "tls", - "data": map[string]string{ - "tls_cert": strings.ReplaceAll(terraform.Output(t, existingTerraformOptions, "tls_cert"), "\n", `\n`), - "tls_key": strings.ReplaceAll(terraform.Output(t, existingTerraformOptions, "tls_key"), "\n", `\n`), - }, - }, - fmt.Sprintf("%s-registry", prefix): map[string]interface{}{ - "format": "registry", - "data": map[string]string{ - "server": "us.icr.io", - "username": "iamapikey", - "password": val, // pragma: allowlist secret - }, - }, + }, + fmt.Sprintf("%s-registry", prefix): map[string]interface{}{ + "format": "registry", + "data": map[string]string{ + "server": "private.us.icr.io", + "username": "iamapikey", + "password": val, // pragma: allowlist secret }, }, - }) - output, err := options.RunTestConsistency() - assert.Nil(t, err, "This should not have errored") - assert.NotNil(t, output, "Expected some output") + }, } + + tfvarsPath := tempTerraformDir + "/solutions/project/terraform.auto.tfvars" + writeTfvarsFile(t, tfvarsPath, tfVars) + if err := os.Remove(tfvarsPath); err != nil { + log.Printf("Warning: failed to remove %s: %v\n", tfvarsPath, err) + } + + options := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: tempTerraformDir + "/solutions/project", + }) + + cleanTerraformCache(tempTerraformDir + "/solutions/project") + terraform.WorkspaceSelectOrNew(t, options, prefix) + output, existErr := terraform.InitAndApplyE(t, options) + if existErr != nil { + assert.True(t, existErr == nil, "Init and Apply of temp existing resource failed") + } + + assert.Nil(t, existErr, "This should not have errored") + assert.NotNil(t, output, "Expected some output") } // Check if "DO_NOT_DESTROY_ON_FAILURE" is set @@ -322,3 +338,108 @@ func TestDeployCEProjectDA(t *testing.T) { logger.Log(t, "END: Destroy (existing resources)") } } + +func getTerraformOutput(dir, name string) (string, error) { + cmd := exec.Command("terraform", "output", "-no-color", name) + cmd.Dir = dir + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err := cmd.Run() + if err != nil { + return "", err + } + + // Trim and unquote if needed + raw := strings.TrimSpace(out.String()) + + // If heredoc-style, strip the surrounding < Date: Thu, 5 Jun 2025 10:38:06 +0100 Subject: [PATCH 3/7] update test --- tests/pr_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/pr_test.go b/tests/pr_test.go index a3d1804..98f1414 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -307,9 +307,11 @@ func TestDeployCEProjectDA(t *testing.T) { tfvarsPath := tempTerraformDir + "/solutions/project/terraform.auto.tfvars" writeTfvarsFile(t, tfvarsPath, tfVars) - if err := os.Remove(tfvarsPath); err != nil { - log.Printf("Warning: failed to remove %s: %v\n", tfvarsPath, err) - } + defer func() { + if err := os.Remove(tfvarsPath); err != nil { + fmt.Printf("Warning: failed to remove %s: %v\n", tfvarsPath, err) + } + }() options := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ TerraformDir: tempTerraformDir + "/solutions/project", From 8b56512c8d94504c7e340600d04778f9119aba4d Mon Sep 17 00:00:00 2001 From: akocbek Date: Thu, 5 Jun 2025 18:27:52 +0100 Subject: [PATCH 4/7] fix test --- tests/pr_test.go | 236 ++++++++++++++++------------------------------- 1 file changed, 78 insertions(+), 158 deletions(-) diff --git a/tests/pr_test.go b/tests/pr_test.go index 98f1414..d4403a0 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -2,22 +2,14 @@ package test import ( - "bytes" "fmt" "log" "os" - "os/exec" - "path/filepath" - "regexp" "sort" - "strconv" "strings" "testing" - "github.com/gruntwork-io/terratest/modules/files" - "github.com/gruntwork-io/terratest/modules/logger" "github.com/gruntwork-io/terratest/modules/random" - "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/common" @@ -221,178 +213,105 @@ func TestUpgradeCEProjectDA(t *testing.T) { func TestDeployCEProjectDA(t *testing.T) { t.Parallel() - // Provision watsonx Assistant instance - prefix := fmt.Sprintf("ce-data-%s", strings.ToLower(random.UniqueId())) - realTerraformDir := ".." - tempTerraformDir, _ := files.CopyTerraformFolderToTemp(realTerraformDir, fmt.Sprintf(prefix+"-%s", strings.ToLower(random.UniqueId()))) - // tags := common.GetTagsFromTravis() - // Verify ibmcloud_api_key variable is set checkVariable := "TF_VAR_ibmcloud_api_key" val, present := os.LookupEnv(checkVariable) require.True(t, present, checkVariable+" environment variable not set") require.NotEqual(t, "", val, checkVariable+" environment variable is empty") - logger.Log(t, "Tempdir: ", tempTerraformDir) - existingTerraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ - TerraformDir: tempTerraformDir + "/tests/resources", - Vars: map[string]interface{}{ - "resource_group": resourceGroup, - "prefix": prefix, + prefix := fmt.Sprintf("ce-data-%s", strings.ToLower(random.UniqueId())) + // set up the options for existing resource deployment + // needed by solution + existingResourceOptions := testhelper.TestOptionsDefault(&testhelper.TestOptions{ + Testing: t, + TerraformDir: "tests/resources", + Prefix: prefix, + ResourceGroup: resourceGroup, + TerraformVars: map[string]interface{}{ "existing_sm_instance_guid": permanentResources["secretsManagerGuid"], "existing_sm_instance_region": permanentResources["secretsManagerRegion"], "existing_cert_secret_id": permanentResources["cePublicCertId"], + "resource_group": resourceGroup, + "prefix": prefix, }, - // Set Upgrade to true to ensure latest version of providers and modules are used by terratest. - // This is the same as setting the -upgrade=true flag with terraform. - Upgrade: true, }) + // Creates temp dirs and runs InitAndApply for existing resources + // outputs will be in options after apply + existingResourceOptions.SkipTestTearDown = true + _, existDeployErr := existingResourceOptions.RunTest() - terraform.WorkspaceSelectOrNew(t, existingTerraformOptions, prefix) - _, existErr := terraform.InitAndApplyE(t, existingTerraformOptions) - if existErr != nil { - assert.True(t, existErr == nil, "Init and Apply of temp existing resource failed") - } else { - - cr_name, _ := getTerraformOutput(tempTerraformDir+"/tests/resources", "cr_name") - tls_cert, _ := getTerraformOutput(tempTerraformDir+"/tests/resources", "tls_cert") - tls_key, _ := getTerraformOutput(tempTerraformDir+"/tests/resources", "tls_key") - - tfVars := map[string]interface{}{ - "prefix": prefix, - "provider_visibility": "public", - "project_name": prefix, - "existing_resource_group_name": resourceGroup, - "container_registry_namespace": cr_name, - "builds": map[string]interface{}{ - fmt.Sprintf("%s-build", prefix): map[string]interface{}{ - "output_image": fmt.Sprintf("private.us.icr.io/%s/%s", cr_name, prefix), - "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret - "source_url": "https://github.com/IBM/CodeEngine", - "strategy_type": "dockerfile", - }, - fmt.Sprintf("%s-build-2", prefix): map[string]interface{}{ - "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret - "source_url": "https://github.com/IBM/CodeEngine", - "strategy_type": "dockerfile", - "source_context_dir": "hello", - }, + defer existingResourceOptions.TestTearDown() // public function ignores skip above + + require.NoError(t, existDeployErr, "error creating needed existing VPC resources") + + tfVars := map[string]interface{}{ + "prefix": prefix, + "provider_visibility": "public", + "project_name": prefix, + "existing_resource_group_name": resourceGroup, + "container_registry_namespace": fmt.Sprintf("test_%s_ns", prefix), + "builds": map[string]interface{}{ + fmt.Sprintf("%s-build", prefix): map[string]interface{}{ + "output_image": fmt.Sprintf("private.us.icr.io/%s/%s", existingResourceOptions.LastTestTerraformOutputs["cr_name"].(string), prefix), + "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret + "source_url": "https://github.com/IBM/CodeEngine", + "strategy_type": "dockerfile", }, - "config_maps": map[string]interface{}{ - fmt.Sprintf("%s-cm", prefix): map[string]interface{}{ - "data": map[string]interface{}{ - "key_1": "value_1", - "key_2": "value_2", - }, + fmt.Sprintf("%s-build-2", prefix): map[string]interface{}{ + "output_secret": fmt.Sprintf("%s-registry", prefix), // pragma: allowlist secret + "source_url": "https://github.com/IBM/CodeEngine", + "strategy_type": "dockerfile", + "source_context_dir": "hello", + }, + }, + "config_maps": map[string]interface{}{ + fmt.Sprintf("%s-cm", prefix): map[string]interface{}{ + "data": map[string]interface{}{ + "key_1": "value_1", + "key_2": "value_2", }, }, - "secrets": map[string]interface{}{ - fmt.Sprintf("%s-tls", prefix): map[string]interface{}{ - "format": "tls", - "data": map[string]string{ - "tls_cert": strings.ReplaceAll(tls_cert, "\n", `\n`), - "tls_key": strings.ReplaceAll(tls_key, "\n", `\n`), - }, + }, + "secrets": map[string]interface{}{ + fmt.Sprintf("%s-tls", prefix): map[string]interface{}{ + "format": "tls", + "data": map[string]string{ + "tls_cert": strings.ReplaceAll(existingResourceOptions.LastTestTerraformOutputs["tls_cert"].(string), "\n", `\n`), + "tls_key": strings.ReplaceAll(existingResourceOptions.LastTestTerraformOutputs["tls_key"].(string), "\n", `\n`), }, - fmt.Sprintf("%s-registry", prefix): map[string]interface{}{ - "format": "registry", - "data": map[string]string{ - "server": "private.us.icr.io", - "username": "iamapikey", - "password": val, // pragma: allowlist secret - }, + }, + fmt.Sprintf("%s-registry", prefix): map[string]interface{}{ + "format": "registry", + "data": map[string]string{ + "server": "private.us.icr.io", + "username": "iamapikey", + "password": val, // pragma: allowlist secret }, }, - } - - tfvarsPath := tempTerraformDir + "/solutions/project/terraform.auto.tfvars" - writeTfvarsFile(t, tfvarsPath, tfVars) - defer func() { - if err := os.Remove(tfvarsPath); err != nil { - fmt.Printf("Warning: failed to remove %s: %v\n", tfvarsPath, err) - } - }() - - options := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ - TerraformDir: tempTerraformDir + "/solutions/project", - }) - - cleanTerraformCache(tempTerraformDir + "/solutions/project") - terraform.WorkspaceSelectOrNew(t, options, prefix) - output, existErr := terraform.InitAndApplyE(t, options) - if existErr != nil { - assert.True(t, existErr == nil, "Init and Apply of temp existing resource failed") - } - - assert.Nil(t, existErr, "This should not have errored") - assert.NotNil(t, output, "Expected some output") - } - - // Check if "DO_NOT_DESTROY_ON_FAILURE" is set - envVal, _ := os.LookupEnv("DO_NOT_DESTROY_ON_FAILURE") - // Destroy the temporary existing resources if required - if t.Failed() && strings.ToLower(envVal) == "true" { - fmt.Println("Terratest failed. Debug the test and delete resources manually.") - } else { - logger.Log(t, "START: Destroy (existing resources)") - terraform.Destroy(t, existingTerraformOptions) - terraform.WorkspaceDelete(t, existingTerraformOptions, prefix) - logger.Log(t, "END: Destroy (existing resources)") - } -} - -func getTerraformOutput(dir, name string) (string, error) { - cmd := exec.Command("terraform", "output", "-no-color", name) - cmd.Dir = dir - - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &out - - err := cmd.Run() - if err != nil { - return "", err - } - - // Trim and unquote if needed - raw := strings.TrimSpace(out.String()) - - // If heredoc-style, strip the surrounding < Date: Tue, 17 Jun 2025 11:17:29 +0100 Subject: [PATCH 5/7] address PR comments --- solutions/project/main.tf | 10 ++++++-- solutions/project/scripts/build-run.sh | 2 +- solutions/project/scripts/get-cr-region.sh | 30 +++++++++++++--------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/solutions/project/main.tf b/solutions/project/main.tf index 2a83294..543d9bf 100644 --- a/solutions/project/main.tf +++ b/solutions/project/main.tf @@ -27,15 +27,21 @@ module "project" { # Code Engine Build ############################################################################## locals { + registry_region_result = data.external.container_registry_region.result + registry = lookup(local.registry_region_result, "registry", null) + container_registry = local.registry != null ? "private.${local.registry}" : null + registry_region_error = lookup(local.registry_region_result, "error", null) - container_registry = "private.${data.external.container_registry_region.result["registry"]}" + # This will cause Terraform to fail if "error" is present in the external script output executed as a part of container_registry_region + # tflint-ignore: terraform_unused_declarations + fail_if_registry_region_error = local.registry_region_error != null ? tobool("Registry region script failed: ${local.registry_region_error}") : null # if no build defines a container image reference (output_image), a new container registry namespace must be created using container_registry_namespace. any_missing_output_image = anytrue([ for build in values(var.builds) : !contains(keys(build), "output_image") || build.output_image == null ]) - image_container = local.any_missing_output_image ? "${local.container_registry}/${resource.ibm_cr_namespace.my_namespace[0].name}" : "" + image_container = local.any_missing_output_image && local.container_registry != null ? "${local.container_registry}/${resource.ibm_cr_namespace.my_namespace[0].name}" : "" # if output_image not exists then a new created container image reference updated_builds = { diff --git a/solutions/project/scripts/build-run.sh b/solutions/project/scripts/build-run.sh index 210d338..a9bc947 100755 --- a/solutions/project/scripts/build-run.sh +++ b/solutions/project/scripts/build-run.sh @@ -26,7 +26,7 @@ if [[ -z "${BUILDS}" ]]; then exit 1 fi -ibmcloud login -r "${REGION}" -g "${RESOURCE_GROUP_ID}" --apikey "${IBMCLOUD_API_KEY}" +ibmcloud login -r "${REGION}" -g "${RESOURCE_GROUP_ID}" --quiet # selecet the right code engine project ibmcloud ce project select -n "${CE_PROJECT_NAME}" diff --git a/solutions/project/scripts/get-cr-region.sh b/solutions/project/scripts/get-cr-region.sh index 1724cad..b7230ee 100755 --- a/solutions/project/scripts/get-cr-region.sh +++ b/solutions/project/scripts/get-cr-region.sh @@ -1,33 +1,39 @@ #!/bin/bash -set -e +set -euo pipefail INPUT=$(cat) REGION=$(echo "$INPUT" | jq -r '.REGION') RESOURCE_GROUP_ID=$(echo "$INPUT" | jq -r '.RESOURCE_GROUP_ID') IBMCLOUD_API_KEY=$(echo "$INPUT" | jq -r '.IBMCLOUD_API_KEY') +export IBMCLOUD_API_KEY if [[ -z "${IBMCLOUD_API_KEY}" || "${IBMCLOUD_API_KEY}" == "null" ]]; then - echo "IBMCLOUD_API_KEY is required" >&2 - exit 1 + echo '{"error": "IBMCLOUD_API_KEY is required"}' + exit 0 fi if [[ -z "${RESOURCE_GROUP_ID}" || "${RESOURCE_GROUP_ID}" == "null" ]]; then - echo "RESOURCE_GROUP_ID is required" >&2 - exit 1 + echo '{"error": "RESOURCE_GROUP_ID is required"}' + exit 0 fi if [[ -z "${REGION}" || "${REGION}" == "null" ]]; then - echo "REGION is required" >&2 - exit 1 + echo '{"error": "REGION is required"}' + exit 0 fi -# Login to IBM Cloud quietly -if ! ibmcloud login -r "${REGION}" -g "${RESOURCE_GROUP_ID}" --apikey "${IBMCLOUD_API_KEY}" --quiet > /dev/null 2>&1; then - exit 1 +if ! ibmcloud login -r "${REGION}" -g "${RESOURCE_GROUP_ID}" --quiet > /dev/null 2>&1; then + printf '{"error": "Failed to login using: ibmcloud login -r %s -g %s"}' "$REGION" "$RESOURCE_GROUP_ID" + exit 0 fi # extract registry value from text "You are targeting region 'us-south', the registry is 'us.icr.io'." -registry=$(ibmcloud cr region 2>/dev/null | grep registry | sed -E "s/.*registry is '([^']+)'.*/\1/") || exit 1 +registry=$(ibmcloud cr region 2>/dev/null | grep registry | sed -E "s/.*registry is '([^']+)'.*/\1/") + +# Validate registry value +if [[ -z "$registry" ]]; then + echo '{"error": "Failed to parse registry region from ibmcloud cr region"}' + exit 0 +fi -# Output valid JSON for Terraform external data source echo "{\"registry\": \"${registry}\"}" From 64b21b0c104e99b7004489dbdc24b5389a83f2e7 Mon Sep 17 00:00:00 2001 From: akocbek Date: Tue, 17 Jun 2025 11:59:15 +0100 Subject: [PATCH 6/7] address PR comments --- solutions/project/DA-inputs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/project/DA-inputs.md b/solutions/project/DA-inputs.md index 1cc3492..42a2592 100644 --- a/solutions/project/DA-inputs.md +++ b/solutions/project/DA-inputs.md @@ -17,7 +17,7 @@ The `builds` input variable allows you to provide details of the of builds which ### Options for Builds - - `output_image` (required): The name of the image. + - `output_image` (optional): The name of the image. A container image can be identified by a container image reference with the following structure: registry / namespace / repository : tag. If not provided, the name is automatically build using region registry / container_registry_namespace input / build name. - `output_secret` (required): The secret that is required to access the image registry. - `source_url` (required): The URL of the code repository. - `strategy_type` (required): Specifies the type of source to determine if your build source is in a repository or based on local source code. From 7eeef3b1827ae518ff8f21aca1028772c58a6016 Mon Sep 17 00:00:00 2001 From: akocbek Date: Tue, 17 Jun 2025 12:11:46 +0100 Subject: [PATCH 7/7] address PR comments --- solutions/project/DA-inputs.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/solutions/project/DA-inputs.md b/solutions/project/DA-inputs.md index 42a2592..add0c93 100644 --- a/solutions/project/DA-inputs.md +++ b/solutions/project/DA-inputs.md @@ -118,10 +118,30 @@ The `secrets` input variable allows you to provide a method to include sensitive ### Example for Secrets ```hcl +# generic secret { "your-secret-name" = { format = "generic" data = { "key_1" : "value_1", "key_2" : "value_2" } } } + +# registry secret +"registry_secret_name" = { + format = "registry" + optional("data") = { + "server" = "private.us.icr.io", + "username" = "iamapikey", + "password" = iam_api_key, # pragma: allowlist secret + } +} + +# private repository +"private_repo" = { + format = "generic" + "data" = { + "password" = github_token, # pragma: allowlist secret + "username" = github_user + } +} ```