diff --git a/README.md b/README.md index 3a0f037..bcd791b 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ No modules. | [container\_app\_environment\_name](#input\_container\_app\_environment\_name) | (Required) The name of the container apps managed environment. Changing this forces a new resource to be created. | `string` | n/a | yes | | [container\_app\_environment\_tags](#input\_container\_app\_environment\_tags) | A map of the tags to use on the resources that are deployed with this module. | `map(string)` | `{}` | no | | [container\_app\_secrets](#input\_container\_app\_secrets) | (Optional) The secrets of the container apps. The key of the map should be aligned with the corresponding container app. |
map(list(object({| `{}` | no | -| [container\_apps](#input\_container\_apps) | The container apps to deploy. |
name = string
value = string
})))
map(object({| n/a | yes | +| [container\_apps](#input\_container\_apps) | The container apps to deploy. |
name = string
tags = optional(map(string))
revision_mode = string
workload_profile_name = optional(string)
template = object({
containers = set(object({
name = string
image = string
args = optional(list(string))
command = optional(list(string))
cpu = string
memory = string
env = optional(set(object({
name = string
secret_name = optional(string)
value = optional(string)
})))
liveness_probe = optional(object({
failure_count_threshold = optional(number)
header = optional(object({
name = string
value = string
}))
host = optional(string)
initial_delay = optional(number, 1)
interval_seconds = optional(number, 10)
path = optional(string)
port = number
timeout = optional(number, 1)
transport = string
}))
readiness_probe = optional(object({
failure_count_threshold = optional(number)
header = optional(object({
name = string
value = string
}))
host = optional(string)
interval_seconds = optional(number, 10)
path = optional(string)
port = number
success_count_threshold = optional(number, 3)
timeout = optional(number)
transport = string
}))
startup_probe = optional(object({
failure_count_threshold = optional(number)
header = optional(object({
name = string
value = string
}))
host = optional(string)
interval_seconds = optional(number, 10)
path = optional(string)
port = number
timeout = optional(number)
transport = string
}))
volume_mounts = optional(object({
name = string
path = string
}))
}))
max_replicas = optional(number)
min_replicas = optional(number)
revision_suffix = optional(string)
volume = optional(set(object({
name = string
storage_name = optional(string)
storage_type = optional(string)
})))
})
ingress = optional(object({
allow_insecure_connections = optional(bool, false)
external_enabled = optional(bool, false)
target_port = number
transport = optional(string)
traffic_weight = object({
label = optional(string)
latest_revision = optional(string)
revision_suffix = optional(string)
percentage = number
})
}))
identity = optional(object({
type = string
identity_ids = optional(list(string))
}))
dapr = optional(object({
app_id = string
app_port = number
app_protocol = optional(string)
}))
registry = optional(list(object({
server = string
username = optional(string)
password_secret_name = optional(string)
identity = optional(string)
})))
}))
map(object({| n/a | yes | | [dapr\_component](#input\_dapr\_component) | (Optional) The Dapr component to deploy. |
name = string
tags = optional(map(string))
revision_mode = string
workload_profile_name = optional(string)
template = object({
init_containers = optional(set(object({
args = optional(list(string))
command = optional(list(string))
cpu = optional(number)
image = string
name = string
memory = optional(string)
env = optional(list(object({
name = string
secret_name = optional(string)
value = optional(string)
})))
volume_mounts = optional(list(object({
name = string
path = string
})))
})), [])
containers = set(object({
name = string
image = string
args = optional(list(string))
command = optional(list(string))
cpu = string
memory = string
env = optional(set(object({
name = string
secret_name = optional(string)
value = optional(string)
})))
liveness_probe = optional(object({
failure_count_threshold = optional(number)
header = optional(object({
name = string
value = string
}))
host = optional(string)
initial_delay = optional(number, 1)
interval_seconds = optional(number, 10)
path = optional(string)
port = number
timeout = optional(number, 1)
transport = string
}))
readiness_probe = optional(object({
failure_count_threshold = optional(number)
header = optional(object({
name = string
value = string
}))
host = optional(string)
interval_seconds = optional(number, 10)
path = optional(string)
port = number
success_count_threshold = optional(number, 3)
timeout = optional(number)
transport = string
}))
startup_probe = optional(object({
failure_count_threshold = optional(number)
header = optional(object({
name = string
value = string
}))
host = optional(string)
interval_seconds = optional(number, 10)
path = optional(string)
port = number
timeout = optional(number)
transport = string
}))
volume_mounts = optional(object({
name = string
path = string
}))
}))
max_replicas = optional(number)
min_replicas = optional(number)
revision_suffix = optional(string)
volume = optional(set(object({
name = string
storage_name = optional(string)
storage_type = optional(string)
})))
})
ingress = optional(object({
allow_insecure_connections = optional(bool, false)
external_enabled = optional(bool, false)
target_port = number
transport = optional(string)
traffic_weight = object({
label = optional(string)
latest_revision = optional(string)
revision_suffix = optional(string)
percentage = number
})
}))
identity = optional(object({
type = string
identity_ids = optional(list(string))
}))
dapr = optional(object({
app_id = string
app_port = number
app_protocol = optional(string)
}))
registry = optional(list(object({
server = string
username = optional(string)
password_secret_name = optional(string)
identity = optional(string)
})))
}))
map(object({| `{}` | no | | [dapr\_component\_secrets](#input\_dapr\_component\_secrets) | (Optional) The secrets of the Dapr components. The key of the map should be aligned with the corresponding Dapr component. |
name = string
component_type = string
version = string
ignore_errors = optional(bool, false)
init_timeout = optional(string, "5s")
scopes = optional(list(string))
metadata = optional(set(object({
name = string
secret_name = optional(string)
value = string
})))
}))
map(list(object({| `{}` | no | | [env\_storage](#input\_env\_storage) | (Optional) Manages a Container App Environment Storage, writing files to this file share to make data accessible by other systems. |
name = string
value = string
})))
map(object({| `{}` | no | diff --git a/examples/init-container/main.tf b/examples/init-container/main.tf new file mode 100644 index 0000000..4d0594f --- /dev/null +++ b/examples/init-container/main.tf @@ -0,0 +1,84 @@ +resource "random_id" "rg_name" { + byte_length = 8 +} + +resource "random_id" "env_name" { + byte_length = 8 +} + +resource "random_id" "container_name" { + byte_length = 4 +} + +resource "azurerm_resource_group" "test" { + location = var.location + name = "example-container-app-${random_id.rg_name.hex}-init-container" +} + +module "container_apps" { + source = "../.." + resource_group_name = azurerm_resource_group.test.name + location = var.location + container_app_environment_name = "example-env-${random_id.env_name.hex}" + + container_apps = { + example = { + name = "example" + revision_mode = "Single" + + template = { + init_containers = [ + { + name = "debian" + image = "debian:latest" + memory = "0.5Gi" + cpu = 0.25 + command = [ + "/bin/sh", + ] + args = [ + "-c", "echo Hello from the debian container > /shared/index.html" + ] + volume_mounts = [ + { + name = "shared" + path = "/shared" + } + ] + } + ], + containers = [ + { + name = "nginx" + image = "nginx:latest" + memory = "1Gi" + cpu = 0.5 + volume_mounts = { + name = "shared" + path = "/usr/share/nginx/html" + } + } + ], + volume = [ + { + name = "shared" + storage_type = "EmptyDir" + } + ] + } + + + ingress = { + allow_insecure_connections = false + target_port = 80 + external_enabled = true + + traffic_weight = { + latest_revision = true + percentage = 100 + } + } + }, + } + log_analytics_workspace_name = "container-app-module-lawn-${random_id.container_name.hex}" +} \ No newline at end of file diff --git a/examples/init-container/outputs.tf b/examples/init-container/outputs.tf new file mode 100644 index 0000000..85facbf --- /dev/null +++ b/examples/init-container/outputs.tf @@ -0,0 +1,3 @@ +output "url" { + value = module.container_apps.container_app_fqdn["example"] +} diff --git a/examples/init-container/variables.tf b/examples/init-container/variables.tf new file mode 100644 index 0000000..817025c --- /dev/null +++ b/examples/init-container/variables.tf @@ -0,0 +1,3 @@ +variable "location" { + default = "eastus" +} diff --git a/examples/init-container/versions.tf b/examples/init-container/versions.tf new file mode 100644 index 0000000..d52a40c --- /dev/null +++ b/examples/init-container/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.2" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.85, < 4.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.0.0" + } + } +} + +provider "azurerm" { + features {} +} \ No newline at end of file diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..acb1dfc --- /dev/null +++ b/go.sum @@ -0,0 +1 @@ +github.com/Azure/terraform-module-test-helper v0.7.1/go.mod h1:7DojWQTd4S94vojNvnidodLskyZDHaDuOwnjKl27sH4= diff --git a/main.tf b/main.tf index 2b37ca2..237b12f 100644 --- a/main.tf +++ b/main.tf @@ -200,6 +200,34 @@ resource "azurerm_container_app" "container_app" { } } } + dynamic "init_container" { + for_each = each.value.template.init_containers == null ? [] : each.value.template.init_containers + + content { + image = init_container.value.image + name = init_container.value.name + args = init_container.value.args + command = init_container.value.command + cpu = init_container.value.cpu + memory = init_container.value.memory + + dynamic "env" { + for_each = init_container.value.env == null ? [] : init_container.value.env + content { + name = env.value.name + secret_name = env.value.secret_name + value = env.value.value + } + } + dynamic "volume_mounts" { + for_each = init_container.value.volume_mounts == null ? [] : init_container.value.volume_mounts + content { + name = volume_mounts.value.name + path = volume_mounts.value.path + } + } + } + } dynamic "volume" { for_each = each.value.template.volume == null ? [] : each.value.template.volume diff --git a/test/e2e/terraform_e2e_test.go b/test/e2e/terraform_e2e_test.go index 07f0ac9..7d70ed4 100644 --- a/test/e2e/terraform_e2e_test.go +++ b/test/e2e/terraform_e2e_test.go @@ -13,6 +13,7 @@ import ( ) func TestExamplesDapr(t *testing.T) { + t.Parallel() // For dapr example var vars map[string]any managedIdentityId := os.Getenv("MSI_ID") @@ -29,6 +30,7 @@ func TestExamplesDapr(t *testing.T) { } func TestExamplesStartup(t *testing.T) { + t.Parallel() vars := make(map[string]interface{}) test_helper.RunE2ETest(t, "../../", "examples/startup", terraform.Options{ @@ -38,6 +40,7 @@ func TestExamplesStartup(t *testing.T) { } func TestExampleAcr(t *testing.T) { + t.Parallel() test_helper.RunE2ETest(t, "../..", "examples/acr", terraform.Options{ Upgrade: true, }, func(t *testing.T, output test_helper.TerraformOutput) { @@ -50,6 +53,19 @@ func TestExampleAcr(t *testing.T) { }) } +func TestInitContainer(t *testing.T) { + t.Parallel() + test_helper.RunE2ETest(t, "../..", "examples/init-container", terraform.Options{ + Upgrade: true, + }, func(t *testing.T, output test_helper.TerraformOutput) { + url, ok := output["url"].(string) + require.True(t, ok) + html, err := getHTML(url) + require.NoError(t, err) + assert.Contains(t, html, "Hello from the debian container") + }) +} + func getHTML(url string) (string, error) { resp, err := http.Get(url) // #nosec G107 if err != nil { diff --git a/variables.tf b/variables.tf index 87a6a98..c449ae2 100644 --- a/variables.tf +++ b/variables.tf @@ -12,6 +12,23 @@ variable "container_apps" { workload_profile_name = optional(string) template = object({ + init_containers = optional(set(object({ + args = optional(list(string)) + command = optional(list(string)) + cpu = optional(number) + image = string + name = string + memory = optional(string) + env = optional(list(object({ + name = string + secret_name = optional(string) + value = optional(string) + }))) + volume_mounts = optional(list(object({ + name = string + path = string + }))) + })), []) containers = set(object({ name = string image = string
name = string
account_name = string
share_name = string
access_mode = string
}))