From 3ddb61eb84c4545f06fa6035009dec5c977f59e4 Mon Sep 17 00:00:00 2001 From: Nick Beenham <1985327+superbeeny@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:33:13 -0400 Subject: [PATCH] Adding Kubernetes native secrets to secret stores (#7744) # Description Adding secret store capabilities to reference kubernetes secrets natively ## Type of change - This pull request adds or changes features of Radius and has an approved issue (#5520) . Fixes: #5520 --------- Signed-off-by: Nick Beenham <1985327+superbeeny@users.noreply.github.com> --- .../2023-10-01-preview/types.json | 471 ++++++++++-------- hack/bicep-types-radius/generated/index.json | 12 +- .../v20231001preview/container_conversion.go | 73 ++- .../container_conversion_test.go | 37 ++ .../containerresource-nil-env-variables.json | 80 +++ .../testdata/containerresource.json | 11 + .../testdata/containerresourcedatamodel.json | 11 + .../v20231001preview/zz_generated_models.go | 44 +- .../zz_generated_models_serde.go | 147 ++++++ pkg/corerp/datamodel/container.go | 41 +- pkg/corerp/renderers/container/render.go | 85 +++- pkg/corerp/renderers/container/render_test.go | 247 +++++++-- pkg/kubeutil/client.go | 3 + .../preview/2023-10-01-preview/openapi.json | 69 ++- .../cli/noncloud/cli_test.go | 3 +- .../corerp-resources-container-workload.bicep | 4 +- ...s-redeploy-withupdatedresource.step2.bicep | 4 +- .../noncloud/resources/container_test.go | 26 + ...sources-friendly-container-version-1.bicep | 4 +- .../corerp-resources-container-secrets.bicep | 59 +++ .../testdata/corerp-resources-extender.bicep | 12 +- .../corerp-resources-gateway-dns.bicep | 4 +- ...resources-gateway-kubernetesmetadata.bicep | 4 +- ...erp-resources-gateway-sslpassthrough.bicep | 8 +- .../corerp-resources-simulatedenv.bicep | 4 +- .../daprrp-resources-serviceinvocation.bicep | 4 +- ...datastoresrp-resources-microsoft-sql.bicep | 4 +- ...atastoresrp-resources-mongodb-recipe.bicep | 4 +- .../datastoresrp-resources-redis-manual.bicep | 5 +- ...oresrp-resources-simulatedenv-recipe.bicep | 4 +- .../datastoresrp-resources-sqldb-manual.bicep | 16 +- .../datastoresrp-resources-sqldb-recipe.bicep | 4 +- .../datastoresrp-rs-mongodb-manual.bicep | 12 +- typespec/Applications.Core/common.tsp | 9 + typespec/Applications.Core/containers.tsp | 17 +- 35 files changed, 1232 insertions(+), 310 deletions(-) create mode 100644 pkg/corerp/api/v20231001preview/testdata/containerresource-nil-env-variables.json create mode 100644 test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-container-secrets.bicep diff --git a/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json b/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json index 10edcb4a28..7accdb2471 100644 --- a/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json +++ b/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json @@ -715,7 +715,7 @@ }, "tags": { "type": { - "$ref": "#/118" + "$ref": "#/121" }, "flags": 0, "description": "Resource tags." @@ -777,7 +777,7 @@ }, "connections": { "type": { - "$ref": "#/103" + "$ref": "#/106" }, "flags": 0, "description": "Specifies a connection to another resource." @@ -791,35 +791,35 @@ }, "extensions": { "type": { - "$ref": "#/104" + "$ref": "#/107" }, "flags": 0, "description": "Extensions spec of the resource" }, "resourceProvisioning": { "type": { - "$ref": "#/107" + "$ref": "#/110" }, "flags": 0, "description": "Specifies how the underlying service/resource is provisioned and managed. Available values are 'internal', where Radius manages the lifecycle of the resource internally, and 'manual', where a user manages the resource." }, "resources": { "type": { - "$ref": "#/109" + "$ref": "#/112" }, "flags": 0, "description": "A collection of references to resources associated with the container" }, "restartPolicy": { "type": { - "$ref": "#/113" + "$ref": "#/116" }, "flags": 0, "description": "Restart policy for the container" }, "runtimes": { "type": { - "$ref": "#/114" + "$ref": "#/117" }, "flags": 0, "description": "The properties for runtime configuration" @@ -900,49 +900,49 @@ }, "env": { "type": { - "$ref": "#/71" + "$ref": "#/74" }, "flags": 0, "description": "environment" }, "ports": { "type": { - "$ref": "#/76" + "$ref": "#/79" }, "flags": 0, "description": "container ports" }, "readinessProbe": { "type": { - "$ref": "#/77" + "$ref": "#/80" }, "flags": 0, "description": "Properties for readiness/liveness probe" }, "livenessProbe": { "type": { - "$ref": "#/77" + "$ref": "#/80" }, "flags": 0, "description": "Properties for readiness/liveness probe" }, "volumes": { "type": { - "$ref": "#/96" + "$ref": "#/99" }, "flags": 0, "description": "container volumes" }, "command": { "type": { - "$ref": "#/97" + "$ref": "#/100" }, "flags": 0, "description": "Entrypoint array. Overrides the container image's ENTRYPOINT" }, "args": { "type": { - "$ref": "#/98" + "$ref": "#/101" }, "flags": 0, "description": "Arguments to the entrypoint. Overrides the container image's CMD" @@ -982,12 +982,65 @@ } ] }, + { + "$type": "ObjectType", + "name": "EnvironmentVariable", + "properties": { + "value": { + "type": { + "$ref": "#/0" + }, + "flags": 0, + "description": "The value of the environment variable" + }, + "valueFrom": { + "type": { + "$ref": "#/72" + }, + "flags": 0, + "description": "The reference to the variable" + } + } + }, + { + "$type": "ObjectType", + "name": "EnvironmentVariableReference", + "properties": { + "secretRef": { + "type": { + "$ref": "#/73" + }, + "flags": 1, + "description": "This secret is used within a recipe. Secrets are encrypted, often have fine-grained access control, auditing and are recommended to be used to hold sensitive data." + } + } + }, + { + "$type": "ObjectType", + "name": "SecretReference", + "properties": { + "source": { + "type": { + "$ref": "#/0" + }, + "flags": 1, + "description": "The ID of an Applications.Core/SecretStore resource containing sensitive data required for recipe execution." + }, + "key": { + "type": { + "$ref": "#/0" + }, + "flags": 1, + "description": "The key for the secret in the secret store." + } + } + }, { "$type": "ObjectType", "name": "ContainerEnv", "properties": {}, "additionalProperties": { - "$ref": "#/0" + "$ref": "#/71" } }, { @@ -1003,7 +1056,7 @@ }, "protocol": { "type": { - "$ref": "#/75" + "$ref": "#/78" }, "flags": 0, "description": "The protocol in use by the port" @@ -1036,10 +1089,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/73" + "$ref": "#/76" }, { - "$ref": "#/74" + "$ref": "#/77" } ] }, @@ -1048,7 +1101,7 @@ "name": "ContainerPorts", "properties": {}, "additionalProperties": { - "$ref": "#/72" + "$ref": "#/75" } }, { @@ -1087,13 +1140,13 @@ }, "elements": { "exec": { - "$ref": "#/78" + "$ref": "#/81" }, "httpGet": { - "$ref": "#/80" + "$ref": "#/83" }, "tcp": { - "$ref": "#/83" + "$ref": "#/86" } } }, @@ -1110,7 +1163,7 @@ }, "kind": { "type": { - "$ref": "#/79" + "$ref": "#/82" }, "flags": 1, "description": "Discriminator property for HealthProbeProperties." @@ -1141,14 +1194,14 @@ }, "headers": { "type": { - "$ref": "#/81" + "$ref": "#/84" }, "flags": 0, "description": "Custom HTTP headers to add to the get request" }, "kind": { "type": { - "$ref": "#/82" + "$ref": "#/85" }, "flags": 1, "description": "Discriminator property for HealthProbeProperties." @@ -1180,7 +1233,7 @@ }, "kind": { "type": { - "$ref": "#/84" + "$ref": "#/87" }, "flags": 1, "description": "Discriminator property for HealthProbeProperties." @@ -1206,10 +1259,10 @@ }, "elements": { "ephemeral": { - "$ref": "#/86" + "$ref": "#/89" }, "persistent": { - "$ref": "#/91" + "$ref": "#/94" } } }, @@ -1219,14 +1272,14 @@ "properties": { "managedStore": { "type": { - "$ref": "#/89" + "$ref": "#/92" }, "flags": 1, "description": "The managed store for the ephemeral volume" }, "kind": { "type": { - "$ref": "#/90" + "$ref": "#/93" }, "flags": 1, "description": "Discriminator property for Volume." @@ -1245,10 +1298,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/87" + "$ref": "#/90" }, { - "$ref": "#/88" + "$ref": "#/91" } ] }, @@ -1262,7 +1315,7 @@ "properties": { "permission": { "type": { - "$ref": "#/94" + "$ref": "#/97" }, "flags": 0, "description": "The persistent volume permission" @@ -1276,7 +1329,7 @@ }, "kind": { "type": { - "$ref": "#/95" + "$ref": "#/98" }, "flags": 1, "description": "Discriminator property for Volume." @@ -1295,10 +1348,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/92" + "$ref": "#/95" }, { - "$ref": "#/93" + "$ref": "#/96" } ] }, @@ -1311,7 +1364,7 @@ "name": "ContainerVolumes", "properties": {}, "additionalProperties": { - "$ref": "#/85" + "$ref": "#/88" } }, { @@ -1346,7 +1399,7 @@ }, "iam": { "type": { - "$ref": "#/100" + "$ref": "#/103" }, "flags": 0, "description": "IAM properties" @@ -1359,14 +1412,14 @@ "properties": { "kind": { "type": { - "$ref": "#/101" + "$ref": "#/104" }, "flags": 1, "description": "The kind of IAM provider to configure" }, "roles": { "type": { - "$ref": "#/102" + "$ref": "#/105" }, "flags": 0, "description": "RBAC permissions to be assigned on the source resource" @@ -1388,7 +1441,7 @@ "name": "ContainerPropertiesConnections", "properties": {}, "additionalProperties": { - "$ref": "#/99" + "$ref": "#/102" } }, { @@ -1409,10 +1462,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/105" + "$ref": "#/108" }, { - "$ref": "#/106" + "$ref": "#/109" } ] }, @@ -1432,7 +1485,7 @@ { "$type": "ArrayType", "itemType": { - "$ref": "#/108" + "$ref": "#/111" } }, { @@ -1451,13 +1504,13 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/110" + "$ref": "#/113" }, { - "$ref": "#/111" + "$ref": "#/114" }, { - "$ref": "#/112" + "$ref": "#/115" } ] }, @@ -1467,7 +1520,7 @@ "properties": { "kubernetes": { "type": { - "$ref": "#/115" + "$ref": "#/118" }, "flags": 0, "description": "The runtime configuration properties for Kubernetes" @@ -1487,7 +1540,7 @@ }, "pod": { "type": { - "$ref": "#/117" + "$ref": "#/120" }, "flags": 0, "description": "A strategic merge patch that will be applied to the PodSpec object when this container is being deployed." @@ -1502,7 +1555,7 @@ "name": "KubernetesPodSpec", "properties": {}, "additionalProperties": { - "$ref": "#/116" + "$ref": "#/119" } }, { @@ -1551,28 +1604,28 @@ }, "type": { "type": { - "$ref": "#/120" + "$ref": "#/123" }, "flags": 10, "description": "The resource type" }, "apiVersion": { "type": { - "$ref": "#/121" + "$ref": "#/124" }, "flags": 10, "description": "The resource api version" }, "properties": { "type": { - "$ref": "#/123" + "$ref": "#/126" }, "flags": 1, "description": "Environment properties" }, "tags": { "type": { - "$ref": "#/156" + "$ref": "#/158" }, "flags": 0, "description": "Resource tags." @@ -1599,7 +1652,7 @@ "properties": { "provisioningState": { "type": { - "$ref": "#/131" + "$ref": "#/134" }, "flags": 2, "description": "Provisioning state of the resource at the time the operation was called" @@ -1613,7 +1666,7 @@ }, "providers": { "type": { - "$ref": "#/132" + "$ref": "#/135" }, "flags": 0, "description": "The Cloud providers configuration." @@ -1627,21 +1680,21 @@ }, "recipes": { "type": { - "$ref": "#/141" + "$ref": "#/144" }, "flags": 0, "description": "Specifies Recipes linked to the Environment." }, "recipeConfig": { "type": { - "$ref": "#/142" + "$ref": "#/145" }, "flags": 0, "description": "Configuration for Recipes. Defines how each type of Recipe should be configured and run." }, "extensions": { "type": { - "$ref": "#/155" + "$ref": "#/157" }, "flags": 0, "description": "The environment extension." @@ -1680,25 +1733,25 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/124" + "$ref": "#/127" }, { - "$ref": "#/125" + "$ref": "#/128" }, { - "$ref": "#/126" + "$ref": "#/129" }, { - "$ref": "#/127" + "$ref": "#/130" }, { - "$ref": "#/128" + "$ref": "#/131" }, { - "$ref": "#/129" + "$ref": "#/132" }, { - "$ref": "#/130" + "$ref": "#/133" } ] }, @@ -1708,14 +1761,14 @@ "properties": { "azure": { "type": { - "$ref": "#/133" + "$ref": "#/136" }, "flags": 0, "description": "The Azure cloud provider definition." }, "aws": { "type": { - "$ref": "#/134" + "$ref": "#/137" }, "flags": 0, "description": "The AWS cloud provider definition." @@ -1762,7 +1815,7 @@ }, "parameters": { "type": { - "$ref": "#/116" + "$ref": "#/119" }, "flags": 0, "description": "Any object" @@ -1770,10 +1823,10 @@ }, "elements": { "bicep": { - "$ref": "#/136" + "$ref": "#/139" }, "terraform": { - "$ref": "#/138" + "$ref": "#/141" } } }, @@ -1790,7 +1843,7 @@ }, "templateKind": { "type": { - "$ref": "#/137" + "$ref": "#/140" }, "flags": 1, "description": "Discriminator property for RecipeProperties." @@ -1814,7 +1867,7 @@ }, "templateKind": { "type": { - "$ref": "#/139" + "$ref": "#/142" }, "flags": 1, "description": "Discriminator property for RecipeProperties." @@ -1830,7 +1883,7 @@ "name": "DictionaryOfRecipeProperties", "properties": {}, "additionalProperties": { - "$ref": "#/135" + "$ref": "#/138" } }, { @@ -1838,7 +1891,7 @@ "name": "EnvironmentPropertiesRecipes", "properties": {}, "additionalProperties": { - "$ref": "#/140" + "$ref": "#/143" } }, { @@ -1847,21 +1900,21 @@ "properties": { "terraform": { "type": { - "$ref": "#/143" + "$ref": "#/146" }, "flags": 0, "description": "Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment." }, "env": { "type": { - "$ref": "#/153" + "$ref": "#/155" }, "flags": 0, "description": "The environment variables injected during Terraform Recipe execution for the recipes in the environment." }, "envSecrets": { "type": { - "$ref": "#/154" + "$ref": "#/156" }, "flags": 0, "description": "Environment variables containing sensitive information can be stored as secrets. The secrets are stored in Applications.Core/SecretStores resource." @@ -1874,14 +1927,14 @@ "properties": { "authentication": { "type": { - "$ref": "#/144" + "$ref": "#/147" }, "flags": 0, "description": "Authentication information used to access private Terraform module sources. Supported module sources: Git." }, "providers": { "type": { - "$ref": "#/152" + "$ref": "#/154" }, "flags": 0, "description": "Configuration for Terraform Recipe Providers. Controls how Terraform interacts with cloud providers, SaaS providers, and other APIs. For more information, please see: https://developer.hashicorp.com/terraform/language/providers/configuration." @@ -1894,7 +1947,7 @@ "properties": { "git": { "type": { - "$ref": "#/145" + "$ref": "#/148" }, "flags": 0, "description": "Authentication information used to access private Terraform modules from Git repository sources." @@ -1907,7 +1960,7 @@ "properties": { "pat": { "type": { - "$ref": "#/147" + "$ref": "#/150" }, "flags": 0, "description": "Personal Access Token (PAT) configuration used to authenticate to Git platforms." @@ -1932,7 +1985,7 @@ "name": "GitAuthConfigPat", "properties": {}, "additionalProperties": { - "$ref": "#/146" + "$ref": "#/149" } }, { @@ -1941,34 +1994,14 @@ "properties": { "secrets": { "type": { - "$ref": "#/150" + "$ref": "#/152" }, "flags": 0, "description": "Sensitive data in provider configuration can be stored as secrets. The secrets are stored in Applications.Core/SecretStores resource." } }, "additionalProperties": { - "$ref": "#/116" - } - }, - { - "$type": "ObjectType", - "name": "SecretReference", - "properties": { - "source": { - "type": { - "$ref": "#/0" - }, - "flags": 1, - "description": "The ID of an Applications.Core/SecretStore resource containing sensitive data required for recipe execution." - }, - "key": { - "type": { - "$ref": "#/0" - }, - "flags": 1, - "description": "The key for the secret in the secret store." - } + "$ref": "#/119" } }, { @@ -1976,13 +2009,13 @@ "name": "ProviderConfigPropertiesSecrets", "properties": {}, "additionalProperties": { - "$ref": "#/149" + "$ref": "#/73" } }, { "$type": "ArrayType", "itemType": { - "$ref": "#/148" + "$ref": "#/151" } }, { @@ -1990,7 +2023,7 @@ "name": "TerraformConfigPropertiesProviders", "properties": {}, "additionalProperties": { - "$ref": "#/151" + "$ref": "#/153" } }, { @@ -2006,7 +2039,7 @@ "name": "RecipeConfigPropertiesEnvSecrets", "properties": {}, "additionalProperties": { - "$ref": "#/149" + "$ref": "#/73" } }, { @@ -2028,7 +2061,7 @@ "name": "Applications.Core/environments@2023-10-01-preview", "scopeType": 0, "body": { - "$ref": "#/122" + "$ref": "#/125" }, "flags": 0, "functions": {} @@ -2061,28 +2094,28 @@ }, "type": { "type": { - "$ref": "#/158" + "$ref": "#/160" }, "flags": 10, "description": "The resource type" }, "apiVersion": { "type": { - "$ref": "#/159" + "$ref": "#/161" }, "flags": 10, "description": "The resource api version" }, "properties": { "type": { - "$ref": "#/161" + "$ref": "#/163" }, "flags": 1, "description": "ExtenderResource portable resource properties" }, "tags": { "type": { - "$ref": "#/174" + "$ref": "#/176" }, "flags": 0, "description": "Resource tags." @@ -2123,7 +2156,7 @@ }, "provisioningState": { "type": { - "$ref": "#/169" + "$ref": "#/171" }, "flags": 2, "description": "Provisioning state of the resource at the time the operation was called" @@ -2137,28 +2170,28 @@ }, "secrets": { "type": { - "$ref": "#/116" + "$ref": "#/119" }, "flags": 0, "description": "Any object" }, "recipe": { "type": { - "$ref": "#/170" + "$ref": "#/172" }, "flags": 0, "description": "The recipe used to automatically deploy underlying infrastructure for a portable resource" }, "resourceProvisioning": { "type": { - "$ref": "#/173" + "$ref": "#/175" }, "flags": 0, "description": "Specifies how the underlying service/resource is provisioned and managed. Available values are 'recipe', where Radius manages the lifecycle of the resource through a Recipe, and 'manual', where a user manages the resource and provides the values." } }, "additionalProperties": { - "$ref": "#/116" + "$ref": "#/119" } }, { @@ -2192,12 +2225,6 @@ { "$type": "UnionType", "elements": [ - { - "$ref": "#/162" - }, - { - "$ref": "#/163" - }, { "$ref": "#/164" }, @@ -2212,6 +2239,12 @@ }, { "$ref": "#/168" + }, + { + "$ref": "#/169" + }, + { + "$ref": "#/170" } ] }, @@ -2228,7 +2261,7 @@ }, "parameters": { "type": { - "$ref": "#/116" + "$ref": "#/119" }, "flags": 0, "description": "Any object" @@ -2247,10 +2280,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/171" + "$ref": "#/173" }, { - "$ref": "#/172" + "$ref": "#/174" } ] }, @@ -2266,7 +2299,7 @@ "$type": "FunctionType", "parameters": [], "output": { - "$ref": "#/116" + "$ref": "#/119" } }, { @@ -2274,13 +2307,13 @@ "name": "Applications.Core/extenders@2023-10-01-preview", "scopeType": 0, "body": { - "$ref": "#/160" + "$ref": "#/162" }, "flags": 0, "functions": { "listSecrets": { "type": { - "$ref": "#/175" + "$ref": "#/177" }, "description": "listSecrets" } @@ -2314,28 +2347,28 @@ }, "type": { "type": { - "$ref": "#/177" + "$ref": "#/179" }, "flags": 10, "description": "The resource type" }, "apiVersion": { "type": { - "$ref": "#/178" + "$ref": "#/180" }, "flags": 10, "description": "The resource api version" }, "properties": { "type": { - "$ref": "#/180" + "$ref": "#/182" }, "flags": 1, "description": "Gateway properties" }, "tags": { "type": { - "$ref": "#/196" + "$ref": "#/198" }, "flags": 0, "description": "Resource tags." @@ -2376,7 +2409,7 @@ }, "provisioningState": { "type": { - "$ref": "#/188" + "$ref": "#/190" }, "flags": 2, "description": "Provisioning state of the resource at the time the operation was called" @@ -2397,21 +2430,21 @@ }, "hostname": { "type": { - "$ref": "#/189" + "$ref": "#/191" }, "flags": 0, "description": "Declare hostname information for the Gateway. Leaving the hostname empty auto-assigns one: mygateway.myapp.PUBLICHOSTNAMEORIP.nip.io." }, "routes": { "type": { - "$ref": "#/191" + "$ref": "#/193" }, "flags": 1, "description": "Routes attached to this Gateway" }, "tls": { "type": { - "$ref": "#/192" + "$ref": "#/194" }, "flags": 0, "description": "TLS configuration definition for Gateway resource." @@ -2456,12 +2489,6 @@ { "$type": "UnionType", "elements": [ - { - "$ref": "#/181" - }, - { - "$ref": "#/182" - }, { "$ref": "#/183" }, @@ -2476,6 +2503,12 @@ }, { "$ref": "#/187" + }, + { + "$ref": "#/188" + }, + { + "$ref": "#/189" } ] }, @@ -2536,7 +2569,7 @@ { "$type": "ArrayType", "itemType": { - "$ref": "#/190" + "$ref": "#/192" } }, { @@ -2552,7 +2585,7 @@ }, "minimumProtocolVersion": { "type": { - "$ref": "#/195" + "$ref": "#/197" }, "flags": 0, "description": "Tls Minimum versions for Gateway resource." @@ -2578,10 +2611,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/193" + "$ref": "#/195" }, { - "$ref": "#/194" + "$ref": "#/196" } ] }, @@ -2598,7 +2631,7 @@ "name": "Applications.Core/gateways@2023-10-01-preview", "scopeType": 0, "body": { - "$ref": "#/179" + "$ref": "#/181" }, "flags": 0, "functions": {} @@ -2631,28 +2664,28 @@ }, "type": { "type": { - "$ref": "#/198" + "$ref": "#/200" }, "flags": 10, "description": "The resource type" }, "apiVersion": { "type": { - "$ref": "#/199" + "$ref": "#/201" }, "flags": 10, "description": "The resource api version" }, "properties": { "type": { - "$ref": "#/201" + "$ref": "#/203" }, "flags": 1, "description": "The properties of SecretStore" }, "tags": { "type": { - "$ref": "#/222" + "$ref": "#/224" }, "flags": 0, "description": "Resource tags." @@ -2693,7 +2726,7 @@ }, "provisioningState": { "type": { - "$ref": "#/209" + "$ref": "#/211" }, "flags": 2, "description": "Provisioning state of the resource at the time the operation was called" @@ -2707,14 +2740,14 @@ }, "type": { "type": { - "$ref": "#/215" + "$ref": "#/217" }, "flags": 0, "description": "The type of SecretStore data" }, "data": { "type": { - "$ref": "#/221" + "$ref": "#/223" }, "flags": 1, "description": "An object to represent key-value type secrets" @@ -2759,12 +2792,6 @@ { "$type": "UnionType", "elements": [ - { - "$ref": "#/202" - }, - { - "$ref": "#/203" - }, { "$ref": "#/204" }, @@ -2779,6 +2806,12 @@ }, { "$ref": "#/208" + }, + { + "$ref": "#/209" + }, + { + "$ref": "#/210" } ] }, @@ -2806,19 +2839,19 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/210" + "$ref": "#/212" }, { - "$ref": "#/211" + "$ref": "#/213" }, { - "$ref": "#/212" + "$ref": "#/214" }, { - "$ref": "#/213" + "$ref": "#/215" }, { - "$ref": "#/214" + "$ref": "#/216" } ] }, @@ -2828,7 +2861,7 @@ "properties": { "encoding": { "type": { - "$ref": "#/219" + "$ref": "#/221" }, "flags": 0, "description": "The type of SecretValue Encoding" @@ -2842,7 +2875,7 @@ }, "valueFrom": { "type": { - "$ref": "#/220" + "$ref": "#/222" }, "flags": 0, "description": "The Secret value source properties" @@ -2861,10 +2894,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/217" + "$ref": "#/219" }, { - "$ref": "#/218" + "$ref": "#/220" } ] }, @@ -2893,7 +2926,7 @@ "name": "SecretStorePropertiesData", "properties": {}, "additionalProperties": { - "$ref": "#/216" + "$ref": "#/218" } }, { @@ -2910,14 +2943,14 @@ "properties": { "type": { "type": { - "$ref": "#/229" + "$ref": "#/231" }, "flags": 2, "description": "The type of SecretStore data" }, "data": { "type": { - "$ref": "#/230" + "$ref": "#/232" }, "flags": 2, "description": "An object to represent key-value type secrets" @@ -2948,19 +2981,19 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/224" + "$ref": "#/226" }, { - "$ref": "#/225" + "$ref": "#/227" }, { - "$ref": "#/226" + "$ref": "#/228" }, { - "$ref": "#/227" + "$ref": "#/229" }, { - "$ref": "#/228" + "$ref": "#/230" } ] }, @@ -2969,14 +3002,14 @@ "name": "SecretStoreListSecretsResultData", "properties": {}, "additionalProperties": { - "$ref": "#/216" + "$ref": "#/218" } }, { "$type": "FunctionType", "parameters": [], "output": { - "$ref": "#/223" + "$ref": "#/225" } }, { @@ -2984,13 +3017,13 @@ "name": "Applications.Core/secretStores@2023-10-01-preview", "scopeType": 0, "body": { - "$ref": "#/200" + "$ref": "#/202" }, "flags": 0, "functions": { "listSecrets": { "type": { - "$ref": "#/231" + "$ref": "#/233" }, "description": "listSecrets" } @@ -3024,28 +3057,28 @@ }, "type": { "type": { - "$ref": "#/233" + "$ref": "#/235" }, "flags": 10, "description": "The resource type" }, "apiVersion": { "type": { - "$ref": "#/234" + "$ref": "#/236" }, "flags": 10, "description": "The resource api version" }, "properties": { "type": { - "$ref": "#/236" + "$ref": "#/238" }, "flags": 1, "description": "Volume properties" }, "tags": { "type": { - "$ref": "#/268" + "$ref": "#/270" }, "flags": 0, "description": "Resource tags." @@ -3087,7 +3120,7 @@ }, "provisioningState": { "type": { - "$ref": "#/244" + "$ref": "#/246" }, "flags": 2, "description": "Provisioning state of the resource at the time the operation was called" @@ -3102,7 +3135,7 @@ }, "elements": { "azure.com.keyvault": { - "$ref": "#/245" + "$ref": "#/247" } } }, @@ -3137,12 +3170,6 @@ { "$type": "UnionType", "elements": [ - { - "$ref": "#/237" - }, - { - "$ref": "#/238" - }, { "$ref": "#/239" }, @@ -3157,6 +3184,12 @@ }, { "$ref": "#/243" + }, + { + "$ref": "#/244" + }, + { + "$ref": "#/245" } ] }, @@ -3166,14 +3199,14 @@ "properties": { "certificates": { "type": { - "$ref": "#/258" + "$ref": "#/260" }, "flags": 0, "description": "The KeyVault certificates that this volume exposes" }, "keys": { "type": { - "$ref": "#/260" + "$ref": "#/262" }, "flags": 0, "description": "The KeyVault keys that this volume exposes" @@ -3187,14 +3220,14 @@ }, "secrets": { "type": { - "$ref": "#/266" + "$ref": "#/268" }, "flags": 0, "description": "The KeyVault secrets that this volume exposes" }, "kind": { "type": { - "$ref": "#/267" + "$ref": "#/269" }, "flags": 1, "description": "Discriminator property for VolumeProperties." @@ -3214,14 +3247,14 @@ }, "encoding": { "type": { - "$ref": "#/250" + "$ref": "#/252" }, "flags": 0, "description": "Represents secret encodings" }, "format": { "type": { - "$ref": "#/253" + "$ref": "#/255" }, "flags": 0, "description": "Represents certificate formats" @@ -3235,7 +3268,7 @@ }, "certType": { "type": { - "$ref": "#/257" + "$ref": "#/259" }, "flags": 0, "description": "Represents certificate types" @@ -3265,13 +3298,13 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/247" + "$ref": "#/249" }, { - "$ref": "#/248" + "$ref": "#/250" }, { - "$ref": "#/249" + "$ref": "#/251" } ] }, @@ -3287,10 +3320,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/251" + "$ref": "#/253" }, { - "$ref": "#/252" + "$ref": "#/254" } ] }, @@ -3310,13 +3343,13 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/254" + "$ref": "#/256" }, { - "$ref": "#/255" + "$ref": "#/257" }, { - "$ref": "#/256" + "$ref": "#/258" } ] }, @@ -3325,7 +3358,7 @@ "name": "AzureKeyVaultVolumePropertiesCertificates", "properties": {}, "additionalProperties": { - "$ref": "#/246" + "$ref": "#/248" } }, { @@ -3360,7 +3393,7 @@ "name": "AzureKeyVaultVolumePropertiesKeys", "properties": {}, "additionalProperties": { - "$ref": "#/259" + "$ref": "#/261" } }, { @@ -3376,7 +3409,7 @@ }, "encoding": { "type": { - "$ref": "#/265" + "$ref": "#/267" }, "flags": 0, "description": "Represents secret encodings" @@ -3413,13 +3446,13 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/262" + "$ref": "#/264" }, { - "$ref": "#/263" + "$ref": "#/265" }, { - "$ref": "#/264" + "$ref": "#/266" } ] }, @@ -3428,7 +3461,7 @@ "name": "AzureKeyVaultVolumePropertiesSecrets", "properties": {}, "additionalProperties": { - "$ref": "#/261" + "$ref": "#/263" } }, { @@ -3448,7 +3481,7 @@ "name": "Applications.Core/volumes@2023-10-01-preview", "scopeType": 0, "body": { - "$ref": "#/235" + "$ref": "#/237" }, "flags": 0, "functions": {} diff --git a/hack/bicep-types-radius/generated/index.json b/hack/bicep-types-radius/generated/index.json index aa50617f14..6657645e5d 100644 --- a/hack/bicep-types-radius/generated/index.json +++ b/hack/bicep-types-radius/generated/index.json @@ -4,22 +4,22 @@ "$ref": "applications/applications.core/2023-10-01-preview/types.json#/53" }, "Applications.Core/containers@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/119" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/122" }, "Applications.Core/environments@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/157" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/159" }, "Applications.Core/extenders@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/176" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/178" }, "Applications.Core/gateways@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/197" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/199" }, "Applications.Core/secretStores@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/232" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/234" }, "Applications.Core/volumes@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/269" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/271" }, "Applications.Dapr/pubSubBrokers@2023-10-01-preview": { "$ref": "applications/applications.dapr/2023-10-01-preview/types.json#/44" diff --git a/pkg/corerp/api/v20231001preview/container_conversion.go b/pkg/corerp/api/v20231001preview/container_conversion.go index e92a0c2eff..be3c48c97c 100644 --- a/pkg/corerp/api/v20231001preview/container_conversion.go +++ b/pkg/corerp/api/v20231001preview/container_conversion.go @@ -18,6 +18,7 @@ package v20231001preview import ( "encoding/json" + "fmt" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/corerp/datamodel" @@ -101,6 +102,11 @@ func (src *ContainerResource) ConvertTo() (v1.DataModelInterface, error) { } } + convertedEnvironmentVariables, err := toEnvironmentVariableDataModel(src.Properties.Container.Env) + if err != nil { + return nil, err + } + converted := &datamodel.ContainerResource{ BaseResource: v1.BaseResource{ TrackedResource: v1.TrackedResource{ @@ -123,7 +129,7 @@ func (src *ContainerResource) ConvertTo() (v1.DataModelInterface, error) { Container: datamodel.Container{ Image: to.String(src.Properties.Container.Image), ImagePullPolicy: toImagePullPolicyDataModel(src.Properties.Container.ImagePullPolicy), - Env: to.StringMap(src.Properties.Container.Env), + Env: convertedEnvironmentVariables, LivenessProbe: livenessProbe, Ports: ports, ReadinessProbe: readinessProbe, @@ -147,10 +153,71 @@ func (src *ContainerResource) ConvertTo() (v1.DataModelInterface, error) { Resource: to.String(src.Properties.Identity.Resource), } } - return converted, nil } +// toEnvironmentVariableDataModel: Converts from versioned datamodel to base datamodel +func toEnvironmentVariableDataModel(e map[string]*EnvironmentVariable) (map[string]datamodel.EnvironmentVariable, error) { + environmentVariableMap := map[string]datamodel.EnvironmentVariable{} + + for key, val := range e { + if val == nil { + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("Environment variable %s is nil", key)) + } + // An environment variable can have either value(Value) or secret value(ValueFrom), but not both + if val.Value != nil && val.ValueFrom != nil { + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("Environment variable %s has both value and secret value", key)) + } + + // An environment variable must have either value(Value) or secret value(ValueFrom) + if val.Value == nil && val.ValueFrom == nil { + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("Environment variable %s has neither value nor secret value", key)) + } + + if val.Value != nil { + environmentVariableMap[key] = datamodel.EnvironmentVariable{ + Value: val.Value, + } + } else { + environmentVariableMap[key] = datamodel.EnvironmentVariable{ + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: to.String(val.ValueFrom.SecretRef.Source), + Key: to.String(val.ValueFrom.SecretRef.Key), + }, + }, + } + + } + + } + return environmentVariableMap, nil +} + +// fromEnvironmentVariableDataModel: Converts from base datamodel to versioned datamodel +func fromEnvironmentVariableDataModel(e map[string]datamodel.EnvironmentVariable) map[string]*EnvironmentVariable { + environmentVariableMap := map[string]*EnvironmentVariable{} + + for key, val := range e { + if val.Value != nil { + environmentVariableMap[key] = &EnvironmentVariable{ + Value: val.Value, + } + } else if val.ValueFrom != nil { + environmentVariableMap[key] = &EnvironmentVariable{ + ValueFrom: &EnvironmentVariableReference{ + SecretRef: &SecretReference{ + Source: to.Ptr(val.ValueFrom.SecretRef.Source), + Key: to.Ptr(val.ValueFrom.SecretRef.Key), + }, + }, + } + } + } + + return environmentVariableMap +} + // ConvertFrom converts from version-agnostic datamodel to the versioned Container resource. func (dst *ContainerResource) ConvertFrom(src v1.DataModelInterface) error { c, ok := src.(*datamodel.ContainerResource) @@ -250,7 +317,7 @@ func (dst *ContainerResource) ConvertFrom(src v1.DataModelInterface) error { Container: &Container{ Image: to.Ptr(c.Properties.Container.Image), ImagePullPolicy: fromImagePullPolicyDataModel(c.Properties.Container.ImagePullPolicy), - Env: *to.StringMapPtr(c.Properties.Container.Env), + Env: fromEnvironmentVariableDataModel(c.Properties.Container.Env), LivenessProbe: livenessProbe, Ports: ports, ReadinessProbe: readinessProbe, diff --git a/pkg/corerp/api/v20231001preview/container_conversion_test.go b/pkg/corerp/api/v20231001preview/container_conversion_test.go index 8760ced3b6..b798adfd60 100644 --- a/pkg/corerp/api/v20231001preview/container_conversion_test.go +++ b/pkg/corerp/api/v20231001preview/container_conversion_test.go @@ -61,6 +61,11 @@ func TestContainerConvertVersionedToDataModel(t *testing.T) { err: nil, emptyExt: true, }, + { + filename: "containerresource-nil-env-variables.json", + err: v1.NewClientErrInvalidRequest("Environment variable DB_USER has neither value nor secret value"), + emptyExt: false, + }, } for _, tt := range conversionTests { @@ -91,6 +96,22 @@ func TestContainerConvertVersionedToDataModel(t *testing.T) { return } + if tt.filename == "containerresource.json" { + require.Equal(t, map[string]datamodel.EnvironmentVariable{ + "DB_USER": { + Value: to.Ptr("DB_USER"), + }, + "DB_PASSWORD": { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: "secret.id", + Key: "DB_PASSWORD", + }, + }, + }, + }, ct.Properties.Container.Env) + } + val, ok := ct.Properties.Connections["inventory"] require.True(t, ok) require.Equal(t, "inventory_route_id", val.Source) @@ -176,6 +197,22 @@ func TestContainerConvertDataModelToVersioned(t *testing.T) { return } + if tt.filename == "containerresourcedatamodel.json" { + require.Equal(t, map[string]datamodel.EnvironmentVariable{ + "DB_USER": { + Value: to.Ptr("DB_USER"), + }, + "DB_PASSWORD": { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: "secret.id", + Key: "DB_PASSWORD", + }, + }, + }, + }, r.Properties.Container.Env) + } + val, ok := r.Properties.Connections["inventory"] require.True(t, ok) require.Equal(t, "inventory_route_id", val.Source) diff --git a/pkg/corerp/api/v20231001preview/testdata/containerresource-nil-env-variables.json b/pkg/corerp/api/v20231001preview/testdata/containerresource-nil-env-variables.json new file mode 100644 index 0000000000..1716f56f5c --- /dev/null +++ b/pkg/corerp/api/v20231001preview/testdata/containerresource-nil-env-variables.json @@ -0,0 +1,80 @@ +{ + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/containers/container0", + "name": "container0", + "type": "Applications.Core/containers", + "properties": { + "status": { + "outputResources": [ + { + "id": "/planes/test/local/providers/Test.Namespace/testResources/test-resource" + } + ] + }, + "provisioningState": "Succeeded", + "application": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", + "connections": { + "inventory": { + "source": "inventory_route_id", + "disableDefaultEnvVars": true, + "iam": { + "kind": "azure", + "roles": [ + "read" + ] + } + } + }, + "restartPolicy": "Always", + "container": { + "image": "ghcr.io/radius-project/webapptutorial-todoapp", + "livenessProbe": { + "kind": "tcp", + "failureThreshold": 5, + "initialDelaySeconds": 5, + "periodSeconds": 5, + "timeoutSeconds": 5, + "containerPort": 8080 + }, + "env": { + "DB_USER": { } + }, + "command": [ + "/bin/sh" + ], + "args": [ + "-c", + "while true; do echo hello; sleep 10;done" + ], + "workingDir": "/app" + }, + "identity": { + "kind": "azure.com.workload", + "oidcIssuer": "https://oidcuri/id", + "resource": "resourceid" + }, + "extensions": [ + { + "kind": "manualScaling", + "replicas": 2 + }, + { + "kind": "daprSidecar", + "appId": "app-id", + "appPort": 80, + "config": "config", + "protocol": "http" + }, + { + "kind": "kubernetesMetadata", + "annotations": { + "prometheus.io/scrape": "true", + "prometheus.io/port": "80" + }, + "labels": { + "foo/bar/team": "credit", + "foo/bar/contact": "radiususer" + } + } + ] + } +} \ No newline at end of file diff --git a/pkg/corerp/api/v20231001preview/testdata/containerresource.json b/pkg/corerp/api/v20231001preview/testdata/containerresource.json index 9e244bfebb..0775bb9705 100644 --- a/pkg/corerp/api/v20231001preview/testdata/containerresource.json +++ b/pkg/corerp/api/v20231001preview/testdata/containerresource.json @@ -35,6 +35,17 @@ "timeoutSeconds": 5, "containerPort": 8080 }, + "env": { + "DB_USER": { "value": "DB_USER" }, + "DB_PASSWORD": { + "valueFrom": { + "secretRef": { + "source": "secret.id", + "key": "DB_PASSWORD" + } + } + } + }, "command": [ "/bin/sh" ], diff --git a/pkg/corerp/api/v20231001preview/testdata/containerresourcedatamodel.json b/pkg/corerp/api/v20231001preview/testdata/containerresourcedatamodel.json index 815b126465..9540ae59f0 100644 --- a/pkg/corerp/api/v20231001preview/testdata/containerresourcedatamodel.json +++ b/pkg/corerp/api/v20231001preview/testdata/containerresourcedatamodel.json @@ -52,6 +52,17 @@ "containerPort": 8080 } }, + "env": { + "DB_USER": { "value": "DB_USER" }, + "DB_PASSWORD": { + "valueFrom": { + "secretRef": { + "source": "secret.id", + "key": "DB_PASSWORD" + } + } + } + }, "command": [ "/bin/sh" ], diff --git a/pkg/corerp/api/v20231001preview/zz_generated_models.go b/pkg/corerp/api/v20231001preview/zz_generated_models.go index 2c4257e39e..40dfe5d89d 100644 --- a/pkg/corerp/api/v20231001preview/zz_generated_models.go +++ b/pkg/corerp/api/v20231001preview/zz_generated_models.go @@ -278,7 +278,7 @@ type Container struct { Command []*string // environment - Env map[string]*string + Env map[string]*EnvironmentVariable // The pull policy for the container image ImagePullPolicy *ImagePullPolicy @@ -454,7 +454,7 @@ type ContainerUpdate struct { Command []*string // environment - Env map[string]*string + Env map[string]*EnvironmentVariableUpdate // The registry and image to download and run in your container Image *string @@ -620,6 +620,36 @@ type EnvironmentResourceUpdateProperties struct { Simulated *bool } +// EnvironmentVariable - Environment variables type +type EnvironmentVariable struct { + // The value of the environment variable + Value *string + + // The reference to the variable + ValueFrom *EnvironmentVariableReference +} + +// EnvironmentVariableReference - The reference to the variable +type EnvironmentVariableReference struct { + // REQUIRED; The secret reference + SecretRef *SecretReference +} + +// EnvironmentVariableReferenceUpdate - The reference to the variable +type EnvironmentVariableReferenceUpdate struct { + // The secret reference + SecretRef *SecretReferenceUpdate +} + +// EnvironmentVariableUpdate - Environment variables type +type EnvironmentVariableUpdate struct { + // The value of the environment variable + Value *string + + // The reference to the variable + ValueFrom *EnvironmentVariableReferenceUpdate +} + // EphemeralVolume - Specifies an ephemeral volume for a container type EphemeralVolume struct { // REQUIRED; Discriminator property for Volume. @@ -1476,6 +1506,16 @@ type SecretReference struct { Source *string } +// SecretReferenceUpdate - This secret is used within a recipe. Secrets are encrypted, often have fine-grained access control, +// auditing and are recommended to be used to hold sensitive data. +type SecretReferenceUpdate struct { + // The key for the secret in the secret store. + Key *string + + // The ID of an Applications.Core/SecretStore resource containing sensitive data required for recipe execution. + Source *string +} + // SecretStoreListSecretsResult - The list of secrets type SecretStoreListSecretsResult struct { // REQUIRED; An object to represent key-value type secrets diff --git a/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go b/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go index 4b9af82111..18f150d197 100644 --- a/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go +++ b/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go @@ -1415,6 +1415,122 @@ func (e *EnvironmentResourceUpdateProperties) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type EnvironmentVariable. +func (e EnvironmentVariable) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "value", e.Value) + populate(objectMap, "valueFrom", e.ValueFrom) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type EnvironmentVariable. +func (e *EnvironmentVariable) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "value": + err = unpopulate(val, "Value", &e.Value) + delete(rawMsg, key) + case "valueFrom": + err = unpopulate(val, "ValueFrom", &e.ValueFrom) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type EnvironmentVariableReference. +func (e EnvironmentVariableReference) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "secretRef", e.SecretRef) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type EnvironmentVariableReference. +func (e *EnvironmentVariableReference) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "secretRef": + err = unpopulate(val, "SecretRef", &e.SecretRef) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type EnvironmentVariableReferenceUpdate. +func (e EnvironmentVariableReferenceUpdate) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "secretRef", e.SecretRef) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type EnvironmentVariableReferenceUpdate. +func (e *EnvironmentVariableReferenceUpdate) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "secretRef": + err = unpopulate(val, "SecretRef", &e.SecretRef) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + } + return nil +} + +// MarshalJSON implements the json.Marshaller interface for type EnvironmentVariableUpdate. +func (e EnvironmentVariableUpdate) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "value", e.Value) + populate(objectMap, "valueFrom", e.ValueFrom) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type EnvironmentVariableUpdate. +func (e *EnvironmentVariableUpdate) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "value": + err = unpopulate(val, "Value", &e.Value) + delete(rawMsg, key) + case "valueFrom": + err = unpopulate(val, "ValueFrom", &e.ValueFrom) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", e, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type EphemeralVolume. func (e EphemeralVolume) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) @@ -3569,6 +3685,37 @@ func (s *SecretReference) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type SecretReferenceUpdate. +func (s SecretReferenceUpdate) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "key", s.Key) + populate(objectMap, "source", s.Source) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type SecretReferenceUpdate. +func (s *SecretReferenceUpdate) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", s, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "key": + err = unpopulate(val, "Key", &s.Key) + delete(rawMsg, key) + case "source": + err = unpopulate(val, "Source", &s.Source) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", s, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type SecretStoreListSecretsResult. func (s SecretStoreListSecretsResult) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) diff --git a/pkg/corerp/datamodel/container.go b/pkg/corerp/datamodel/container.go index c1b6f0eeb9..dd67ff83f4 100644 --- a/pkg/corerp/datamodel/container.go +++ b/pkg/corerp/datamodel/container.go @@ -116,16 +116,37 @@ type ConnectionProperties struct { // Container - Definition of a container. type Container struct { - Image string `json:"image,omitempty"` - ImagePullPolicy string `json:"imagePullPolicy,omitempty"` - Env map[string]string `json:"env,omitempty"` - LivenessProbe HealthProbeProperties `json:"livenessProbe,omitempty"` - Ports map[string]ContainerPort `json:"ports,omitempty"` - ReadinessProbe HealthProbeProperties `json:"readinessProbe,omitempty"` - Volumes map[string]VolumeProperties `json:"volumes,omitempty"` - Command []string `json:"command,omitempty"` - Args []string `json:"args,omitempty"` - WorkingDir string `json:"workingDir,omitempty"` + Image string `json:"image,omitempty"` + ImagePullPolicy string `json:"imagePullPolicy,omitempty"` + Env map[string]EnvironmentVariable `json:"env,omitempty"` + LivenessProbe HealthProbeProperties `json:"livenessProbe,omitempty"` + Ports map[string]ContainerPort `json:"ports,omitempty"` + ReadinessProbe HealthProbeProperties `json:"readinessProbe,omitempty"` + Volumes map[string]VolumeProperties `json:"volumes,omitempty"` + Command []string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + WorkingDir string `json:"workingDir,omitempty"` +} + +// EnvironmentVariable - Environment variable for the container +type EnvironmentVariable struct { + // Value is the property for the environment variable specified by the user. Such as "key": "value" + Value *string `json:"value,omitempty"` + // ValueFrom is the property for the environment variable specified by a reference to a secret. + ValueFrom *EnvironmentVariableReference `json:"valueFrom,omitempty"` +} + +// EnvironmentVariableReference - Environment variable reference for the container +type EnvironmentVariableReference struct { + // SecretRef is the property for the environment variable specified by a reference to a secret. + SecretRef *EnvironmentVariableSecretReference `json:"secretRef"` +} + +// EnvironmentVariableSecretReference - Environment variable secret reference for the container +type EnvironmentVariableSecretReference struct { + // Source is either the resource id of a radius Applications.Core/secretStore resource or a kubernetes secret reference. + Source string `json:"source"` + Key string `json:"key"` } // ContainerPort - Specifies a listening port for the container diff --git a/pkg/corerp/renderers/container/render.go b/pkg/corerp/renderers/container/render.go index 58670f15a3..e041d1454b 100644 --- a/pkg/corerp/renderers/container/render.go +++ b/pkg/corerp/renderers/container/render.go @@ -73,7 +73,7 @@ type Renderer struct { RoleAssignmentMap map[datamodel.IAMKind]RoleAssignmentData } -// GetDependencyIDs parses the connections, ports and volumes of a container resource to return the Radius and Azure +// GetDependencyIDs parses the connections, ports, environment variables, and volumes of a container resource to return the Radius and Azure // resource IDs. func (r Renderer) GetDependencyIDs(ctx context.Context, dm v1.DataModelInterface) (radiusResourceIDs []resources.ID, azureResourceIDs []resources.ID, err error) { resource, ok := dm.(*datamodel.ContainerResource) @@ -109,6 +109,23 @@ func (r Renderer) GetDependencyIDs(ctx context.Context, dm v1.DataModelInterface } } + // Environment variables can be sourced from secrets, which are resources. We need to iterate over the environment variables to handle any possible instances. + for _, envVars := range properties.Container.Env { + if envVars.ValueFrom != nil && envVars.ValueFrom.SecretRef != nil { + // If the string begins with a '/', it is a radius resourceID. + if strings.HasPrefix(envVars.ValueFrom.SecretRef.Source, "/") { + resourceID, err := resources.ParseResource(envVars.ValueFrom.SecretRef.Source) + if err != nil { + return nil, nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid source: %s. Must be either a kubernetes secret name or a valid resourceID", envVars.ValueFrom.SecretRef.Source)) + } + + if resources_radius.IsRadiusResource(resourceID) { + radiusResourceIDs = append(radiusResourceIDs, resourceID) + } + } + } + } + for _, volume := range properties.Container.Volumes { switch volume.Kind { case datamodel.Persistent: @@ -362,7 +379,10 @@ func (r Renderer) makeDeployment( } for k, v := range properties.Container.Env { - env[k] = corev1.EnvVar{Name: k, Value: v} + env[k], err = convertEnvVar(k, v, options) + if err != nil { + return []rpv1.OutputResource{}, nil, fmt.Errorf("failed to convert environment variable: %w", err) + } } // Append in sorted order @@ -616,6 +636,67 @@ func (r Renderer) makeDeployment( return outputResources, secretData, nil } +// convertEnvVar function to convert from map[string]EnvironmentVariable to map[string]corev1.EnvVar +func convertEnvVar(key string, env datamodel.EnvironmentVariable, options renderers.RenderOptions) (corev1.EnvVar, error) { + if env.Value != nil { + return corev1.EnvVar{Name: key, Value: *env.Value}, nil + } else if env.ValueFrom != nil { + // There are two cases to handle here: + // 1. The value comes from a kubernetes secret + // 2. The value comes from a Applications.Core/SecretStore resource id. + + // If the value comes from a kubernetes secret, we'll reference it. + if strings.HasPrefix(env.ValueFrom.SecretRef.Source, "/") { + secretStore, ok := options.Dependencies[env.ValueFrom.SecretRef.Source].Resource.(*datamodel.SecretStore) + if !ok { + return corev1.EnvVar{}, fmt.Errorf("failed to find source in dependencies: %s", env.ValueFrom.SecretRef.Source) + } + + // The format may be / or , as an example "default/my-secret" or "my-secret". We split the string on '/' + // and take the second part if the secret is namespace qualified. + var name string + if strings.Contains(secretStore.Properties.Resource, "/") { + parts := strings.Split(secretStore.Properties.Resource, "/") + if len(parts) == 2 { + name = parts[1] + } else { + name = secretStore.Properties.Resource + } + } else { + name = env.ValueFrom.SecretRef.Source + } + + return corev1.EnvVar{ + Name: key, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: env.ValueFrom.SecretRef.Key, + }, + }, + }, nil + + } else { + return corev1.EnvVar{ + Name: key, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: env.ValueFrom.SecretRef.Source, + }, + Key: env.ValueFrom.SecretRef.Key, + }, + }, + }, nil + } + + } else { + return corev1.EnvVar{}, fmt.Errorf("failed to convert environment variable: %s, both value and valueFrom cannot be nil", key) + } +} + func getEnvVarsAndSecretData(resource *datamodel.ContainerResource, dependencies map[string]renderers.RendererDependency) (map[string]corev1.EnvVar, map[string][]byte, error) { env := map[string]corev1.EnvVar{} secretData := map[string][]byte{} diff --git a/pkg/corerp/renderers/container/render_test.go b/pkg/corerp/renderers/container/render_test.go index 3c1743fa8f..bdb9db0860 100644 --- a/pkg/corerp/renderers/container/render_test.go +++ b/pkg/corerp/renderers/container/render_test.go @@ -53,6 +53,13 @@ const ( envVarValue1 = "TEST_VALUE_1" envVarName2 = "TEST_VAR_2" envVarValue2 = "81" + envVarSource2 = "TEST_SOURCE_2" + envVarName3 = "TEST_VAR_3" + envVarSource3 = "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/secretStores/test-secret" + envVarValue3 = "/planes/are/cool" + envVarName4 = "TEST_VAR_4" + envVarSource4 = "test_Namespace/TEST_SOURCE_4" + envVarValue4 = "TEST_VALUE_4" secretName = "test-container" tempVolName = "TempVolume" @@ -173,6 +180,35 @@ func Test_GetDependencyIDs_Success(t *testing.T) { ContainerPort: 5000, }, }, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: envVarSource2, + Key: envVarValue2, + }, + }, + }, + envVarName3: { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: envVarSource3, + Key: envVarValue3, + }, + }, + }, + envVarName4: { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: envVarSource4, + Key: envVarValue4, + }, + }, + }, + }, Volumes: map[string]datamodel.VolumeProperties{ "vol1": { Kind: datamodel.Persistent, @@ -193,12 +229,13 @@ func Test_GetDependencyIDs_Success(t *testing.T) { renderer := Renderer{} radiusResourceIDs, azureResourceIDs, err := renderer.GetDependencyIDs(ctx, resource) require.NoError(t, err) - require.Len(t, radiusResourceIDs, 2) + require.Len(t, radiusResourceIDs, 3) require.Len(t, azureResourceIDs, 1) expectedRadiusResourceIDs := []resources.ID{ makeRadiusResourceID(t, "Applications.Datastores/redisCaches", "A"), makeRadiusResourceID(t, "Applications.Datastores/redisCaches", "B"), + resources.MustParse(envVarSource3), } require.ElementsMatch(t, expectedRadiusResourceIDs, radiusResourceIDs) @@ -272,14 +309,57 @@ func Test_Render_Basic(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: envVarSource2, + Key: envVarValue2, + }, + }, + }, + envVarName3: { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: envVarSource3, + Key: envVarValue3, + }, + }, + }, + envVarName4: { + ValueFrom: &datamodel.EnvironmentVariableReference{ + SecretRef: &datamodel.EnvironmentVariableSecretReference{ + Source: envVarSource4, + Key: envVarValue4, + }, + }, + }, }, }, } + resource := makeResource(properties) - dependencies := map[string]renderers.RendererDependency{} + dependencies := map[string]renderers.RendererDependency{ + + envVarSource3: { + ResourceID: resources.MustParse(envVarSource3), + Resource: &datamodel.SecretStore{ + BaseResource: apiv1.BaseResource{ + TrackedResource: apiv1.TrackedResource{ + ID: envVarSource3, + }, + }, + Properties: &datamodel.SecretStoreProperties{ + BasicResourceProperties: rpv1.BasicResourceProperties{ + Application: applicationResourceID, + }, + }, + }, + }, + } ctx := testcontext.New(t) renderer := Renderer{} @@ -325,7 +405,30 @@ func Test_Render_Basic(t *testing.T) { expectedEnv := []corev1.EnvVar{ {Name: envVarName1, Value: envVarValue1}, - {Name: envVarName2, Value: envVarValue2}, + {Name: envVarName2, ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: envVarSource2, + }, + Key: envVarValue2, + }, + }}, + {Name: envVarName3, ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: envVarSource3, + }, + Key: envVarValue3, + }, + }}, + {Name: envVarName4, ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: envVarSource4, + }, + Key: envVarValue4, + }, + }}, } require.Equal(t, expectedEnv, container.Env) @@ -333,6 +436,28 @@ func Test_Render_Basic(t *testing.T) { require.Len(t, output.Resources, 4) } +func Test_Render_WithInvalidEnvironmentVariables(t *testing.T) { + properties := datamodel.ContainerProperties{ + BasicResourceProperties: rpv1.BasicResourceProperties{ + Application: applicationResourceID, + }, + Container: datamodel.Container{ + Image: "someimage:latest", + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: {}, + }, + }, + } + resource := makeResource(properties) + dependencies := map[string]renderers.RendererDependency{} + + ctx := testcontext.New(t) + renderer := Renderer{} + _, err := renderer.Render(ctx, resource, renderers.RenderOptions{Dependencies: dependencies}) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to convert environment variable: failed to convert environment variable: TEST_VAR_1, both value and valueFrom cannot be nil") +} + func Test_Render_WithCommandArgsWorkingDir(t *testing.T) { properties := datamodel.ContainerProperties{ BasicResourceProperties: rpv1.BasicResourceProperties{ @@ -340,9 +465,13 @@ func Test_Render_WithCommandArgsWorkingDir(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, Command: []string{"command1", "command2"}, Args: []string{"arg1", "arg2"}, @@ -497,9 +626,13 @@ func Test_Render_Connections(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, }, } @@ -659,9 +792,13 @@ func Test_Render_Connections_SecretsGetHashed(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, }, } @@ -954,9 +1091,13 @@ func Test_Render_EphemeralVolumes(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, Volumes: map[string]datamodel.VolumeProperties{ tempVolName: { @@ -1244,9 +1385,13 @@ func Test_Render_ReadinessProbeHttpGet(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, ReadinessProbe: datamodel.HealthProbeProperties{ Kind: datamodel.HTTPGetHealthProbe, @@ -1323,9 +1468,13 @@ func Test_Render_ReadinessProbeTcp(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, ReadinessProbe: datamodel.HealthProbeProperties{ Kind: datamodel.TCPHealthProbe, @@ -1393,9 +1542,13 @@ func Test_Render_LivenessProbeExec(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, LivenessProbe: datamodel.HealthProbeProperties{ Kind: datamodel.ExecHealthProbe, @@ -1608,9 +1761,13 @@ func Test_Render_ImagePullPolicySpecified(t *testing.T) { Container: datamodel.Container{ Image: "someimage:latest", ImagePullPolicy: "Never", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, }, } @@ -1665,9 +1822,13 @@ func Test_Render_StrategicPatchMerge(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, }, Runtimes: &datamodel.RuntimeProperties{ @@ -1727,9 +1888,13 @@ func Test_Render_BaseManifest(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, Volumes: map[string]datamodel.VolumeProperties{ "ephemeralVolume": { @@ -1755,9 +1920,13 @@ func Test_Render_BaseManifest(t *testing.T) { }, Container: datamodel.Container{ Image: "someimage:latest", - Env: map[string]string{ - envVarName1: envVarValue1, - envVarName2: envVarValue2, + Env: map[string]datamodel.EnvironmentVariable{ + envVarName1: { + Value: to.Ptr(envVarValue1), + }, + envVarName2: { + Value: to.Ptr(envVarValue2), + }, }, }, }, diff --git a/pkg/kubeutil/client.go b/pkg/kubeutil/client.go index 593a9269c5..63cf3aad79 100644 --- a/pkg/kubeutil/client.go +++ b/pkg/kubeutil/client.go @@ -28,6 +28,9 @@ import ( "k8s.io/client-go/rest" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" csidriver "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" + + // Import kubernetes auth plugins + _ "k8s.io/client-go/plugin/pkg/client/auth" ) // Clients is a collection of Kubernetes clients. diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json index 67e43e73b0..28bfa030d6 100644 --- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json +++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json @@ -2676,7 +2676,7 @@ "type": "object", "description": "environment", "additionalProperties": { - "type": "string" + "$ref": "#/definitions/EnvironmentVariable" } }, "ports": { @@ -3002,7 +3002,7 @@ "type": "object", "description": "environment", "additionalProperties": { - "type": "string" + "$ref": "#/definitions/EnvironmentVariableUpdate" } }, "ports": { @@ -3320,6 +3320,57 @@ } } }, + "EnvironmentVariable": { + "type": "object", + "description": "Environment variables type", + "properties": { + "value": { + "type": "string", + "description": "The value of the environment variable" + }, + "valueFrom": { + "$ref": "#/definitions/EnvironmentVariableReference", + "description": "The reference to the variable" + } + } + }, + "EnvironmentVariableReference": { + "type": "object", + "description": "The reference to the variable", + "properties": { + "secretRef": { + "$ref": "#/definitions/SecretReference", + "description": "The secret reference" + } + }, + "required": [ + "secretRef" + ] + }, + "EnvironmentVariableReferenceUpdate": { + "type": "object", + "description": "The reference to the variable", + "properties": { + "secretRef": { + "$ref": "#/definitions/SecretReferenceUpdate", + "description": "The secret reference" + } + } + }, + "EnvironmentVariableUpdate": { + "type": "object", + "description": "Environment variables type", + "properties": { + "value": { + "type": "string", + "description": "The value of the environment variable" + }, + "valueFrom": { + "$ref": "#/definitions/EnvironmentVariableReferenceUpdate", + "description": "The reference to the variable" + } + } + }, "EnvironmentVariables": { "type": "object", "description": "The environment variables injected during Terraform Recipe execution for the recipes in the environment.", @@ -4655,6 +4706,20 @@ "key" ] }, + "SecretReferenceUpdate": { + "type": "object", + "description": "This secret is used within a recipe. Secrets are encrypted, often have fine-grained access control, auditing and are recommended to be used to hold sensitive data.", + "properties": { + "source": { + "type": "string", + "description": "The ID of an Applications.Core/SecretStore resource containing sensitive data required for recipe execution." + }, + "key": { + "type": "string", + "description": "The key for the secret in the secret store." + } + } + }, "SecretStoreDataType": { "type": "string", "description": "The type of SecretStore data", diff --git a/test/functional-portable/cli/noncloud/cli_test.go b/test/functional-portable/cli/noncloud/cli_test.go index 4a79c3dc0d..48b67e09bf 100644 --- a/test/functional-portable/cli/noncloud/cli_test.go +++ b/test/functional-portable/cli/noncloud/cli_test.go @@ -69,7 +69,8 @@ func verifyRecipeCLI(ctx context.Context, t *testing.T, test rp.RPTest) { resourceType := "Applications.Datastores/redisCaches" file := "../../../testrecipes/test-bicep-recipes/corerp-redis-recipe.bicep" - target := fmt.Sprintf("br:%s/dev/test-bicep-recipes/redis-recipe:%s", registry, generateUniqueTag()) + target := fmt.Sprintf("br:%s/dev/test-bicep-recipes/redis-recipe:%s", + strings.TrimPrefix(registry, "registry="), generateUniqueTag()) recipeName := "recipeName" recipeTemplate := fmt.Sprintf("%s/recipes/local-dev/rediscaches:%s", registry, version) diff --git a/test/functional-portable/corerp/cloud/resources/testdata/corerp-resources-container-workload.bicep b/test/functional-portable/corerp/cloud/resources/testdata/corerp-resources-container-workload.bicep index 13ade6ed20..2c0e66fdb8 100644 --- a/test/functional-portable/corerp/cloud/resources/testdata/corerp-resources-container-workload.bicep +++ b/test/functional-portable/corerp/cloud/resources/testdata/corerp-resources-container-workload.bicep @@ -53,7 +53,9 @@ resource container 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - CONNECTION_STORAGE_ACCOUNTNAME: storageAccount.name + CONNECTION_STORAGE_ACCOUNTNAME: { + value: storageAccount.name + } } readinessProbe:{ kind:'httpGet' diff --git a/test/functional-portable/corerp/noncloud/mechanics/testdata/corerp-mechanics-redeploy-withupdatedresource.step2.bicep b/test/functional-portable/corerp/noncloud/mechanics/testdata/corerp-mechanics-redeploy-withupdatedresource.step2.bicep index d882dd9381..556cca4782 100644 --- a/test/functional-portable/corerp/noncloud/mechanics/testdata/corerp-mechanics-redeploy-withupdatedresource.step2.bicep +++ b/test/functional-portable/corerp/noncloud/mechanics/testdata/corerp-mechanics-redeploy-withupdatedresource.step2.bicep @@ -25,7 +25,9 @@ resource mechanicsd 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - TEST: 'updated' + TEST: { + value: 'updated' + } } } } diff --git a/test/functional-portable/corerp/noncloud/resources/container_test.go b/test/functional-portable/corerp/noncloud/resources/container_test.go index 4dc8e47e53..572d20bc00 100644 --- a/test/functional-portable/corerp/noncloud/resources/container_test.go +++ b/test/functional-portable/corerp/noncloud/resources/container_test.go @@ -354,3 +354,29 @@ func Test_Container_FailDueToBadHealthProbe(t *testing.T) { test.Test(t) } + +func Test_Container_Secrets(t *testing.T) { + template := "testdata/corerp-resources-container-secrets.bicep" + name := "corerp-resources-container-secrets" + appNamespace := "corerp-resources-container-secrets" + + test := rp.NewRPTest(t, name, []rp.TestStep{ + { + Executor: step.NewDeployExecutor(template, testutil.GetMagpieImage()), + SkipKubernetesOutputResourceValidation: true, + SkipObjectValidation: true, + RPResources: &validation.RPResourceSet{ + Resources: []validation.RPResource{}, + }, + K8sObjects: &validation.K8sObjectSet{ + Namespaces: map[string][]validation.K8sObject{ + appNamespace: { + validation.NewK8sPodForResource(name, "cntr-cntr-secrets"), + }, + }, + }, + }, + }) + + test.Test(t) +} diff --git a/test/functional-portable/corerp/noncloud/resources/testdata/containers/corerp-resources-friendly-container-version-1.bicep b/test/functional-portable/corerp/noncloud/resources/testdata/containers/corerp-resources-friendly-container-version-1.bicep index 865cbcb717..458f29c5e3 100644 --- a/test/functional-portable/corerp/noncloud/resources/testdata/containers/corerp-resources-friendly-container-version-1.bicep +++ b/test/functional-portable/corerp/noncloud/resources/testdata/containers/corerp-resources-friendly-container-version-1.bicep @@ -19,7 +19,9 @@ resource webapp 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - DBCONNECTION: redis.listSecrets().connectionString + DBCONNECTION: { + value: redis.listSecrets().connectionString + } } readinessProbe: { kind: 'httpGet' diff --git a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-container-secrets.bicep b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-container-secrets.bicep new file mode 100644 index 0000000000..ab32b5c1d3 --- /dev/null +++ b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-container-secrets.bicep @@ -0,0 +1,59 @@ + +extension radius + +@description('Specifies the image of the container resource.') +param magpieimage string + +@description('Specifies the environment for resources.') +param environment string + +resource app 'Applications.Core/applications@2023-10-01-preview' = { + name: 'corerp-resources-container-secrets' + properties: { + environment: environment + extensions: [ + { + kind: 'kubernetesNamespace' + namespace: 'corerp-resources-container-secrets' + } + ] + } +} + +resource container 'Applications.Core/containers@2023-10-01-preview' = { + name: 'cntr-cntr-secrets' + properties: { + application: app.id + container: { + image: magpieimage + env: { + DB_USER: { value: 'DB_USER' } + DB_PASSWORD: { + valueFrom: { + secretRef: { + source: saltysecret.id + key: 'DB_PASSWORD' + } + } + } + } + ports: { + web: { + containerPort: 5000 + } + } + } + } + } + + resource saltysecret 'Applications.Core/secretStores@2023-10-01-preview' = { + name: 'saltysecret' + properties: { + application: app.id + data: { + DB_PASSWORD: { + value: 'password' + } + } + } + } diff --git a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-extender.bicep b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-extender.bicep index 70143fdcc5..1f6b23f3c2 100644 --- a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-extender.bicep +++ b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-extender.bicep @@ -33,9 +33,15 @@ resource container 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - TWILIO_NUMBER: twilio.properties.fromNumber - TWILIO_SID: twilio.listSecrets().accountSid - TWILIO_ACCOUNT: twilio.listSecrets().authToken + TWILIO_NUMBER: { + value: twilio.properties.fromNumber + } + TWILIO_SID: { + value: twilio.listSecrets().accountSid + } + TWILIO_ACCOUNT: { + value: twilio.listSecrets().authToken + } } } connections: {} diff --git a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-dns.bicep b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-dns.bicep index 298aad6acf..650ad24f02 100644 --- a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-dns.bicep +++ b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-dns.bicep @@ -79,7 +79,9 @@ resource backendcontainerdns 'Applications.Core/containers@2023-10-01-preview' = container: { image: magpieimage env: { - gatewayUrl: gateway.properties.url + gatewayUrl: { + value: gateway.properties.url + } } ports: { web: { diff --git a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-kubernetesmetadata.bicep b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-kubernetesmetadata.bicep index 4417712d88..f9f9975718 100644 --- a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-kubernetesmetadata.bicep +++ b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-kubernetesmetadata.bicep @@ -93,7 +93,9 @@ resource backendContainer 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - gatewayUrl: gateway.properties.url + gatewayUrl: { + value: gateway.properties.url + } } ports: { web: { diff --git a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-sslpassthrough.bicep b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-sslpassthrough.bicep index 544e594f48..85467f5b03 100644 --- a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-sslpassthrough.bicep +++ b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-gateway-sslpassthrough.bicep @@ -54,8 +54,12 @@ resource frontendContainer 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - TLS_KEY: tlskey - TLS_CERT: tlscrt + TLS_KEY: { + value: tlskey + } + TLS_CERT: { + value: tlscrt + } } ports: { web: { diff --git a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-simulatedenv.bicep b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-simulatedenv.bicep index 6c923bee3d..d200878ee8 100644 --- a/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-simulatedenv.bicep +++ b/test/functional-portable/corerp/noncloud/resources/testdata/corerp-resources-simulatedenv.bicep @@ -91,7 +91,9 @@ resource backendContainer 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - gatewayUrl: gateway.properties.url + gatewayUrl: { + value: gateway.properties.url + } } ports: { web: { diff --git a/test/functional-portable/daprrp/noncloud/resources/testdata/daprrp-resources-serviceinvocation.bicep b/test/functional-portable/daprrp/noncloud/resources/testdata/daprrp-resources-serviceinvocation.bicep index 316b57d407..869a7a5251 100644 --- a/test/functional-portable/daprrp/noncloud/resources/testdata/daprrp-resources-serviceinvocation.bicep +++ b/test/functional-portable/daprrp/noncloud/resources/testdata/daprrp-resources-serviceinvocation.bicep @@ -21,7 +21,9 @@ resource frontend 'Applications.Core/containers@2023-10-01-preview' = { image: magpieimage env: { // Used by magpie to communicate with the backend. - CONNECTION_DAPRHTTP_APPID: 'backend' + CONNECTION_DAPRHTTP_APPID: { + value: 'backend' + } } readinessProbe:{ kind:'httpGet' diff --git a/test/functional-portable/datastoresrp/cloud/resources/testdata/datastoresrp-resources-microsoft-sql.bicep b/test/functional-portable/datastoresrp/cloud/resources/testdata/datastoresrp-resources-microsoft-sql.bicep index 66d13ab024..7cc28e8233 100644 --- a/test/functional-portable/datastoresrp/cloud/resources/testdata/datastoresrp-resources-microsoft-sql.bicep +++ b/test/functional-portable/datastoresrp/cloud/resources/testdata/datastoresrp-resources-microsoft-sql.bicep @@ -46,7 +46,9 @@ resource sqlapp 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieImage env: { - CONNECTION_SQL_CONNECTIONSTRING: db.listSecrets().connectionString + CONNECTION_SQL_CONNECTIONSTRING: { + value: db.listSecrets().connectionString + } } readinessProbe: { kind: 'httpGet' diff --git a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-mongodb-recipe.bicep b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-mongodb-recipe.bicep index 1f80e4083c..7286d82f2b 100644 --- a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-mongodb-recipe.bicep +++ b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-mongodb-recipe.bicep @@ -53,7 +53,9 @@ resource webapp 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - DBCONNECTION: recipedb.listSecrets().connectionString + DBCONNECTION: { + value: recipedb.listSecrets().connectionString + } } readinessProbe: { kind: 'httpGet' diff --git a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-redis-manual.bicep b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-redis-manual.bicep index f453e2550c..31197271b4 100644 --- a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-redis-manual.bicep +++ b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-redis-manual.bicep @@ -1,5 +1,4 @@ extension radius - param magpieimage string param environment string @@ -19,7 +18,9 @@ resource webapp 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - DBCONNECTION: redis.listSecrets().connectionString + DBCONNECTION: { + value: redis.listSecrets().connectionString + } } readinessProbe: { kind: 'httpGet' diff --git a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-simulatedenv-recipe.bicep b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-simulatedenv-recipe.bicep index f1585f6364..61c2989922 100644 --- a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-simulatedenv-recipe.bicep +++ b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-simulatedenv-recipe.bicep @@ -54,7 +54,9 @@ resource webapp 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieimage env: { - DBCONNECTION: recipedb.listSecrets().connectionString + DBCONNECTION: { + value: recipedb.listSecrets().connectionString + } } readinessProbe: { kind: 'httpGet' diff --git a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-manual.bicep b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-manual.bicep index c30dd40801..1439c50243 100644 --- a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-manual.bicep +++ b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-manual.bicep @@ -46,7 +46,9 @@ resource webapp 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieImage env: { - CONNECTION_SQL_CONNECTIONSTRING: db.listSecrets().connectionString + CONNECTION_SQL_CONNECTIONSTRING: { + value: db.listSecrets().connectionString + } } readinessProbe: { kind: 'httpGet' @@ -82,9 +84,15 @@ resource sqlContainer 'Applications.Core/containers@2023-10-01-preview' = { container: { image: sqlImage env: { - ACCEPT_EULA: 'Y' - MSSQL_PID: 'Developer' - MSSQL_SA_PASSWORD: password + ACCEPT_EULA: { + value: 'Y' + } + MSSQL_PID: { + value: 'Developer' + } + MSSQL_SA_PASSWORD: { + value: password + } } ports: { sql: { diff --git a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-recipe.bicep b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-recipe.bicep index 9f980e0186..dbd75d2e1b 100644 --- a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-recipe.bicep +++ b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-resources-sqldb-recipe.bicep @@ -71,7 +71,9 @@ resource webapp 'Applications.Core/containers@2023-10-01-preview' = { container: { image: magpieImage env: { - CONNECTION_SQL_CONNECTIONSTRING: db.listSecrets().connectionString + CONNECTION_SQL_CONNECTIONSTRING: { + value: db.listSecrets().connectionString + } } readinessProbe: { kind: 'httpGet' diff --git a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-rs-mongodb-manual.bicep b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-rs-mongodb-manual.bicep index 930382836b..e7f514e072 100644 --- a/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-rs-mongodb-manual.bicep +++ b/test/functional-portable/datastoresrp/noncloud/resources/testdata/datastoresrp-rs-mongodb-manual.bicep @@ -43,9 +43,15 @@ resource mongoContainer 'Applications.Core/containers@2023-10-01-preview' = { container: { image: 'ghcr.io/radius-project/mirror/mongo:4.2' env: { - DBCONNECTION: mongo.listSecrets().connectionString - MONGO_INITDB_ROOT_USERNAME: username - MONGO_INITDB_ROOT_PASSWORD: password + DBCONNECTION: { + value: mongo.listSecrets().connectionString + } + MONGO_INITDB_ROOT_USERNAME: { + value: username + } + MONGO_INITDB_ROOT_PASSWORD: { + value: password + } } ports: { mongo: { diff --git a/typespec/Applications.Core/common.tsp b/typespec/Applications.Core/common.tsp index 13aac0cadd..df8c7ae04a 100644 --- a/typespec/Applications.Core/common.tsp +++ b/typespec/Applications.Core/common.tsp @@ -17,3 +17,12 @@ limitations under the License. import "@typespec/openapi"; using OpenAPI; + +@doc("This specifies a reference to a secret. Secrets are encrypted, often have fine-grained access control, auditing and are recommended to be used to hold sensitive data.") +model SecretReference { + @doc("The ID of an Applications.Core/SecretStore resource containing sensitive data required for recipe execution.") + source: string; + + @doc("The key for the secret in the secret store.") + key: string; +} diff --git a/typespec/Applications.Core/containers.tsp b/typespec/Applications.Core/containers.tsp index aa5963bf80..c9b02cf90d 100644 --- a/typespec/Applications.Core/containers.tsp +++ b/typespec/Applications.Core/containers.tsp @@ -240,7 +240,7 @@ model Container { imagePullPolicy?: ImagePullPolicy; @doc("environment") - env?: Record; + env?: Record; @doc("container ports") ports?: Record; @@ -264,6 +264,21 @@ model Container { workingDir?: string; } +@doc("Environment variables type") +model EnvironmentVariable { + @doc("The value of the environment variable") + value?: string; + + @doc("The reference to the variable") + valueFrom?: EnvironmentVariableReference; +} + +@doc("The reference to the variable") +model EnvironmentVariableReference { + @doc("The secret reference") + secretRef: SecretReference; +} + @doc("The image pull policy for the container") enum ImagePullPolicy { @doc("Always")