diff --git a/examples/csharp/documentation/FunctionsOther.cs b/examples/csharp/documentation/FunctionsOther.cs new file mode 100644 index 0000000000..02c6039e07 --- /dev/null +++ b/examples/csharp/documentation/FunctionsOther.cs @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + + +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using Constructs; +using HashiCorp.Cdktf; +using aws.Provider; +using aws.DataAwsAvailabilityZones; + +namespace Examples +{ + class FunctionsOtherStack : TerraformStack + { + public FunctionsOtherStack(Construct scope, string name) : base(scope, name) + { + new AwsProvider(this, "aws", new AwsProviderConfig + { + Region = "eu-central-1" + }); + + + // DOCS_BLOCK_START:functions-raw + DataAwsAvailabilityZones zones = new DataAwsAvailabilityZones(this, "zones", new DataAwsAvailabilityZonesConfig + { + State = "available" + }); + + new TerraformOutput(this, "half-of-the-zone", new TerraformOutputConfig + { + Value = $"${{length({zones.Fqn}.names) / 2}}" + }); + // DOCS_BLOCK_END:functions-raw + + // DOCS_BLOCK_START:functions-lookup + TerraformVariable v = new TerraformVariable(this, "complex_object", new TerraformVariableConfig + { + Type = "object({users: list(object({name: string}))})", + }); + new TerraformOutput(this, "users", new TerraformOutputConfig + { + Value = Fn.Lookup(v.Value, "users") + }); + new TerraformOutput(this, "first-user-name", new TerraformOutputConfig + { + Value = Fn.LookupNested(v.Value, new[] { "users", "0", "name" }) + }); + // DOCS_BLOCK_END:functions-lookup + + // DOCS_BLOCK_START:functions-raw-string + new TerraformOutput(this, "quotes", new TerraformOutputConfig + { + Value = Fn.RawString("\"b\"") + }); + new TerraformOutput(this, "template", new TerraformOutputConfig + { + Value = Fn.RawString("${TEMPLATE}") + }); + // DOCS_BLOCK_END:functions-raw-string + } + } +} diff --git a/examples/csharp/documentation/FunctionsRawStack.cs b/examples/csharp/documentation/FunctionsRawStack.cs deleted file mode 100644 index 2fb8fdf4ef..0000000000 --- a/examples/csharp/documentation/FunctionsRawStack.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - - -using System; -using System.IO; -using System.Collections.Generic; -using System.Linq; -using Constructs; -using HashiCorp.Cdktf; -using aws.Provider; -using aws.DataAwsAvailabilityZones; - -namespace Examples -{ - class FunctionsRawStack : TerraformStack - { - public FunctionsRawStack(Construct scope, string name) : base(scope, name) - { - new AwsProvider(this, "aws", new AwsProviderConfig - { - Region = "eu-central-1" - }); - - - // DOCS_BLOCK_START:functions-raw - DataAwsAvailabilityZones zones = new DataAwsAvailabilityZones(this, "zones", new DataAwsAvailabilityZonesConfig - { - State = "available" - }); - - new TerraformOutput(this, "half-of-the-zone", new TerraformOutputConfig - { - Value = $"${{length({zones.Fqn}.names) / 2}}" - }); - // DOCS_BLOCK_END:functions-raw - } - } -} diff --git a/examples/csharp/documentation/Main.cs b/examples/csharp/documentation/Main.cs index f6e38e2c46..4dce35e4c6 100644 --- a/examples/csharp/documentation/Main.cs +++ b/examples/csharp/documentation/Main.cs @@ -68,7 +68,7 @@ public static void Main(string[] args) new Examples.Producer(app, "cdktf-producer"); new Examples.Consumer(app, "cdktf-consumer"); new Examples.OperatorsStack(app, "operators"); - new Examples.FunctionsRawStack(app, "functions-raw"); + new Examples.FunctionsOtherStack(app, "functions-other"); TerraformStack stack = new TerraformStack(app, "stack-escape-hatches"); // DOCS_BLOCK_START:stack-escape-hatches diff --git a/examples/go/documentation/functions-other.go b/examples/go/documentation/functions-other.go new file mode 100644 index 0000000000..0aac704a5e --- /dev/null +++ b/examples/go/documentation/functions-other.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "github.com/aws/constructs-go/constructs/v10" + "github.com/aws/jsii-runtime-go" + "github.com/hashicorp/terraform-cdk-go/cdktf" +) + +func NewFunctionsOtherStack(scope constructs.Construct, name string) cdktf.TerraformStack { + stack := cdktf.NewTerraformStack(scope, &name) + + // DOCS_BLOCK_START:functions-lookup + v := cdktf.NewTerraformVariable(stack, jsii.String("complex-object"), &cdktf.TerraformVariableConfig{ + Type: jsii.String("object({users: list(object({name: string}))})"), + }) + cdktf.NewTerraformOutput(stack, jsii.String("users"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_Lookup(v.Value(), jsii.String("users"), nil), + }) + cdktf.NewTerraformOutput(stack, jsii.String("first-user-name"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_LookupNested(v.Value(), &[]interface{}{"users", 0, "name"}), + }) + // DOCS_BLOCK_END:functions-lookup + + // DOCS_BLOCK_START:functions-raw-string + cdktf.NewTerraformOutput(stack, jsii.String("quotes"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_RawString(jsii.String("\"b\"")), + }) + cdktf.NewTerraformOutput(stack, jsii.String("template"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_RawString(jsii.String("${TEMPLATE}")), + }) + // DOCS_BLOCK_END:functions-raw-string + + return stack +} diff --git a/examples/go/documentation/main.go b/examples/go/documentation/main.go index 8bb1f8af00..c7a2436f19 100644 --- a/examples/go/documentation/main.go +++ b/examples/go/documentation/main.go @@ -15,6 +15,7 @@ func main() { NewAspectsStack(app, "aspects") NewPrefixAspectsStack(app, "aspects-validation") NewFunctionsStack(app, "functions") + NewFunctionsOtherStack(app, "functions-other") NewOperatorsAndFunctionsRawStack(app, "operators-functions-raw") NewHclInteropStack(app, "hcl-interop") NewProvidersStack(app, "providers") diff --git a/examples/java/documentation/src/main/java/com/mycompany/app/MainFunction.java b/examples/java/documentation/src/main/java/com/mycompany/app/MainFunction.java index 9af3f09735..9ba38138ef 100644 --- a/examples/java/documentation/src/main/java/com/mycompany/app/MainFunction.java +++ b/examples/java/documentation/src/main/java/com/mycompany/app/MainFunction.java @@ -11,8 +11,12 @@ import software.constructs.Construct; +import java.util.Arrays; + // DOCS_BLOCK_START:functions-usage-example import com.hashicorp.cdktf.Fn; +import com.hashicorp.cdktf.TerraformVariable; +import com.hashicorp.cdktf.TerraformVariableConfig; import com.hashicorp.cdktf.TerraformOutput; import com.hashicorp.cdktf.TerraformOutputConfig; import imports.aws.data_aws_availability_zones.DataAwsAvailabilityZones; @@ -41,5 +45,26 @@ public MainFunction(Construct scope, String id) { .build()); // DOCS_BLOCK_END:functions-usage-example + // DOCS_BLOCK_START:functions-lookup + TerraformVariable v = new TerraformVariable(this, "complex_object", TerraformVariableConfig.builder() + .type("object({users: list(object({name: string}))})") + .build()); + new TerraformOutput(this, "users", TerraformOutputConfig.builder() + .value(Fn.lookup(v.getValue(), "users")) + .build()); + new TerraformOutput(this, "first-user-name", TerraformOutputConfig.builder() + .value(Fn.lookupNested(v.getValue(), Arrays.asList("users", "0", "name"))) + .build()); + // DOCS_BLOCK_END:functions-lookup + + // DOCS_BLOCK_START:functions-raw-string + new TerraformOutput(this, "quotes", TerraformOutputConfig.builder() + .value(Fn.rawString("\"b\"")) + .build()); + new TerraformOutput(this, "template", TerraformOutputConfig.builder() + .value(Fn.rawString("${TEMPLATE}")) + .build()); + // DOCS_BLOCK_END:functions-raw-string + } } diff --git a/examples/python/documentation/functions.py b/examples/python/documentation/functions.py index 9fdd5024c9..fb6a067636 100644 --- a/examples/python/documentation/functions.py +++ b/examples/python/documentation/functions.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MPL-2.0 from constructs import Construct -from cdktf import TerraformStack, App +from cdktf import TerraformStack, App, TerraformVariable, Token # DOCS_BLOCK_START:functions-usage-example from cdktf import Fn, TerraformOutput from imports.aws.provider import AwsProvider @@ -28,3 +28,26 @@ def __init__(self, scope: Construct, id: str): # DOCS_BLOCK_END:functions-usage-example + # INTERNAL NOTE: Due to an JSII bug, we have to pass the variable as a string_value in Python + # We can remove it, once https://github.com/aws/jsii/pull/4209 is released + # DOCS_BLOCK_START:functions-lookup + v = TerraformVariable(self, "complex-object", + type = 'object({users: list(object({name: string}))})', + ) + TerraformOutput(self, 'users', + value=Fn.lookup(v.string_value, "users") + ) + TerraformOutput(self, 'first_user_name', + value=Fn.lookup_nested(v.string_value, ["users", 0, "name"]) + ) + # DOCS_BLOCK_END:functions-lookup + + # DOCS_BLOCK_START:functions-raw-string + TerraformOutput(self, 'quotes', + value=Fn.raw_string('"b"') + ) + TerraformOutput(self, 'template', + value=Fn.raw_string('${TEMPLATE}') + ) + # DOCS_BLOCK_END:functions-raw-string + diff --git a/examples/typescript/documentation/functions.ts b/examples/typescript/documentation/functions.ts index 2a85816cee..b339663077 100644 --- a/examples/typescript/documentation/functions.ts +++ b/examples/typescript/documentation/functions.ts @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc // SPDX-License-Identifier: MPL-2.0 // DOCS_BLOCK_START:functions -import { TerraformStack } from "cdktf"; +import { TerraformStack, TerraformVariable } from "cdktf"; import { Construct } from "constructs"; import { AwsProvider } from "@cdktf/provider-aws/lib/aws-provider"; // DOCS_BLOCK_END:functions @@ -37,6 +37,25 @@ export class FunctionsStack extends TerraformStack { }); // DOCS_BLOCK_END:functions + // DOCS_BLOCK_START:functions-lookup + const v = new TerraformVariable(this, "complex_object", { + type: "object({users: list(object({name: string}))})", + }); + new TerraformOutput(this, "users", { value: Fn.lookup(v.value, "users") }); + new TerraformOutput(this, "first_user_name", { + value: Fn.lookupNested(v.value, ["users", 0, "name"]), + }); + // DOCS_BLOCK_END:functions-lookup + + // DOCS_BLOCK_START:functions-raw-string + new TerraformOutput(this, "quotes", { + value: Fn.rawString(`"b"`), + }); + new TerraformOutput(this, "template", { + value: Fn.rawString("${TEMPLATE}"), + }); + // DOCS_BLOCK_END:functions-raw-string + // DOCS_BLOCK_START:operators // ... diff --git a/packages/cdktf/lib/terraform-functions.ts b/packages/cdktf/lib/terraform-functions.ts index 712866f2bd..e7eecaafb4 100644 --- a/packages/cdktf/lib/terraform-functions.ts +++ b/packages/cdktf/lib/terraform-functions.ts @@ -1,6 +1,7 @@ // Copyright (c) HashiCorp, Inc // SPDX-License-Identifier: MPL-2.0 -import { rawString, Token } from "."; +import { propertyAccess, rawString, Token } from "."; +import { asAny } from "./functions/helpers"; import { FnGenerated } from "./functions/terraform-functions.generated"; // eslint-disable-next-line jsdoc/require-jsdoc @@ -21,12 +22,22 @@ export class Fn extends FnGenerated { * {@link /terraform/docs/language/functions/lookup.html lookup} retrieves the value of a single element from a map, given its key. If the given key does not exist, the given default value is returned instead. * @param {any} inputMap * @param {string} key - * @param {Array} defaultValue + * @param {any} [defaultValue] */ - static lookup(inputMap: any, key: string, defaultValue: any) { + static lookup(inputMap: any, key: string, defaultValue?: any) { // overwritten because lookup() uses a variadic argument for its optional defaultValue - // we don't model it as optional since not passing it is deprecated in favor of the native Terraform expression "inputMap[key]" - return Fn._lookup(inputMap, key, [defaultValue]); + if (defaultValue) return Fn._lookup(inputMap, key, [defaultValue]); + return asAny(propertyAccess(inputMap, [key])); // -> renders inputMap[key] (which is recommended if no default value is given) + } + + /** + * returns a property access expression that accesses the property at the given path in the given inputMap. + * For example lookupNested(x, ["a", "b", "c"]) will return a Terraform expression like x["a"]["b"]["c"] + * @param {any} inputMap + * @param {Array} path + */ + static lookupNested(inputMap: any, path: any[]) { + return asAny(propertyAccess(inputMap, path)); } /** diff --git a/packages/cdktf/test/fqn.test.ts b/packages/cdktf/test/fqn.test.ts index ce93235c2b..945d6bd6d3 100644 --- a/packages/cdktf/test/fqn.test.ts +++ b/packages/cdktf/test/fqn.test.ts @@ -106,7 +106,7 @@ test("works when escape is mid-way", () => { name: "${test_resource.first-resource}-second", tags: { firstResourceName: - 'simple-test-${lookup(test_resource.other-resource, "name", "")}-${test_resource.first-resource.name}', + "simple-test-${test_resource.other-resource.name}-${test_resource.first-resource.name}", }, }; @@ -147,7 +147,7 @@ test("after escape, reverts to normal", () => { name: "${test_resource.first-resource}-second", tags: { firstResourceName: - 'simple-test-${test_resource.first-resource.name}-${lookup(test_resource.other-resource, "name", "")}', + "simple-test-${test_resource.first-resource.name}-${test_resource.other-resource.name}", }, }; @@ -186,7 +186,7 @@ test("can have multiple escapes", () => { name: "${test_resource.first-resource}-second", tags: { firstResourceName: - 'simple-test-${test_resource.first-resource.name}-${lookup(test_resource.other-resource, "name", "")}', + "simple-test-${test_resource.first-resource.name}-${test_resource.other-resource.name}", }, }; @@ -291,7 +291,7 @@ test("works with functions", () => { name: "${test_resource.first-resource}-second", tags: { firstResourceName: - 'simple-test-${test_resource.first-resource.name}-${lookup(test_resource.other-resource, "name", "")}', + "simple-test-${test_resource.first-resource.name}-${test_resource.other-resource.name}", }, }; @@ -416,7 +416,7 @@ test("allows functions within functions", () => { name: "bar", tags: { firstResourceName: - '${lookup(test_resource.other-resource, "name", upper(lookup(test_resource.other-resource, "name", "")))}', + '${lookup(test_resource.other-resource, "name", upper(test_resource.other-resource.name))}', }, }; diff --git a/packages/cdktf/test/functions.test.ts b/packages/cdktf/test/functions.test.ts index 3b07f57a78..b62aeb55cf 100644 --- a/packages/cdktf/test/functions.test.ts +++ b/packages/cdktf/test/functions.test.ts @@ -553,3 +553,43 @@ it("errors mentioning function name and argument", () => { `"Argument 1 of replace failed the validation: Error: 'this one " not' can not be used as value directly since it has unescaped double quotes in it. To safely use the value please use Fn.rawString on your string."` ); }); + +test("Property access using lookup and lookupNested functions", () => { + const app = Testing.app(); + const stack = new TerraformStack(app, "test"); + + const variable = new TerraformVariable(stack, "test-var", { + type: `object({a = object({b = string}), z = string})`, + }); + + new TerraformOutput(stack, "lookup", { + value: Fn.lookup(variable.value, "z", "defaultzzzz"), + }); + new TerraformOutput(stack, "native-access", { + value: Fn.lookup(variable.value, "z"), + }); + new TerraformOutput(stack, "native-access-nested", { + value: Fn.lookupNested(variable.value, ["a", "b"]), + }); + + expect(Testing.synth(stack)).toMatchInlineSnapshot(` + "{ + "output": { + "lookup": { + "value": "\${lookup(var.test-var, \\"z\\", \\"defaultzzzz\\")}" + }, + "native-access": { + "value": "\${var.test-var.z}" + }, + "native-access-nested": { + "value": "\${var.test-var.a.b}" + } + }, + "variable": { + "test-var": { + "type": "object({a = object({b = string}), z = string})" + } + } + }" + `); +}); diff --git a/packages/cdktf/test/iterator.test.ts b/packages/cdktf/test/iterator.test.ts index 28e25cf671..5107900492 100644 --- a/packages/cdktf/test/iterator.test.ts +++ b/packages/cdktf/test/iterator.test.ts @@ -151,7 +151,7 @@ test("iterator access nested types", () => { ); expect(synth.resource.test_resource.test.tags).toMatchInlineSnapshot(` { - "boolean_map": "\${lookup(each.value.map, "a", false)}", + "boolean_map": "\${each.value.map.a}", "list": "\${join(",", each.value.list_attribute)}", "map": "\${lookup(each.value.map, "a", "default")}", "number": "\${tostring(each.value.number_attribute)}", diff --git a/website/docs/cdktf/concepts/functions.mdx b/website/docs/cdktf/concepts/functions.mdx index 6716f047d0..b8ca474c4a 100644 --- a/website/docs/cdktf/concepts/functions.mdx +++ b/website/docs/cdktf/concepts/functions.mdx @@ -30,7 +30,7 @@ The `element` function gets the first element from the list of Availability Zone ```ts -import { TerraformStack } from "cdktf"; +import { TerraformStack, TerraformVariable } from "cdktf"; import { Construct } from "constructs"; import { AwsProvider } from "@cdktf/provider-aws/lib/aws-provider"; import { Fn, TerraformOutput } from "cdktf"; @@ -55,6 +55,8 @@ export class FunctionsStack extends TerraformStack { ```java import com.hashicorp.cdktf.Fn; +import com.hashicorp.cdktf.TerraformVariable; +import com.hashicorp.cdktf.TerraformVariableConfig; import com.hashicorp.cdktf.TerraformOutput; import com.hashicorp.cdktf.TerraformOutputConfig; import imports.aws.data_aws_availability_zones.DataAwsAvailabilityZones; @@ -152,6 +154,136 @@ func NewFunctionsStack(scope constructs.Construct, name string) cdktf.TerraformS +## Special functions + +### Property Access Helpers + +To access nested properties from untyped objects or other datasources that return a dynamic datatype, use the Terraform function `lookup` or, for nested access, the function "Fn.lookupNested()" which is a function offered by CDKTF that allows to avoid nesting `Fn.lookup` calls. + + + + + + + +```ts +const v = new TerraformVariable(this, "complex_object", { + type: "object({users: list(object({name: string}))})", +}); +new TerraformOutput(this, "users", { value: Fn.lookup(v.value, "users") }); +new TerraformOutput(this, "first_user_name", { + value: Fn.lookupNested(v.value, ["users", 0, "name"]), +}); +``` + +```python +v = TerraformVariable(self, "complex-object", + type = 'object({users: list(object({name: string}))})', +) +TerraformOutput(self, 'users', + value=Fn.lookup(v.string_value, "users") +) +TerraformOutput(self, 'first_user_name', + value=Fn.lookup_nested(v.string_value, ["users", 0, "name"]) +) +``` + +```java +TerraformVariable v = new TerraformVariable(this, "complex_object", TerraformVariableConfig.builder() + .type("object({users: list(object({name: string}))})") + .build()); +new TerraformOutput(this, "users", TerraformOutputConfig.builder() + .value(Fn.lookup(v.getValue(), "users")) + .build()); +new TerraformOutput(this, "first-user-name", TerraformOutputConfig.builder() + .value(Fn.lookupNested(v.getValue(), Arrays.asList("users", "0", "name"))) + .build()); +``` + +```csharp +TerraformVariable v = new TerraformVariable(this, "complex_object", new TerraformVariableConfig +{ + Type = "object({users: list(object({name: string}))})", +}); +new TerraformOutput(this, "users", new TerraformOutputConfig +{ + Value = Fn.Lookup(v.Value, "users") +}); +new TerraformOutput(this, "first-user-name", new TerraformOutputConfig +{ + Value = Fn.LookupNested(v.Value, new[] { "users", "0", "name" }) +}); +``` + +```go +v := cdktf.NewTerraformVariable(stack, jsii.String("complex-object"), &cdktf.TerraformVariableConfig{ + Type: jsii.String("object({users: list(object({name: string}))})"), +}) +cdktf.NewTerraformOutput(stack, jsii.String("users"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_Lookup(v.Value(), jsii.String("users"), nil), +}) +cdktf.NewTerraformOutput(stack, jsii.String("first-user-name"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_LookupNested(v.Value(), &[]interface{}{"users", 0, "name"}), +}) +``` + +### Raw string helper + +Another helper function offered by CDKTF is `Fn.rawString` which can be used to escape raw strings that contain characters that CDKTF or Terraform would try to interpret otherwise. + + + + + + + +```ts +new TerraformOutput(this, "quotes", { + value: Fn.rawString(`"b"`), +}); +new TerraformOutput(this, "template", { + value: Fn.rawString("${TEMPLATE}"), +}); +``` + +```python +TerraformOutput(self, 'quotes', + value=Fn.raw_string('"b"') +) +TerraformOutput(self, 'template', + value=Fn.raw_string('${TEMPLATE}') +) +``` + +```java +new TerraformOutput(this, "quotes", TerraformOutputConfig.builder() + .value(Fn.rawString("\"b\"")) + .build()); +new TerraformOutput(this, "template", TerraformOutputConfig.builder() + .value(Fn.rawString("${TEMPLATE}")) + .build()); +``` + +```csharp +new TerraformOutput(this, "quotes", new TerraformOutputConfig +{ + Value = Fn.RawString("\"b\"") +}); +new TerraformOutput(this, "template", new TerraformOutputConfig +{ + Value = Fn.RawString("${TEMPLATE}") +}); +``` + +```go +cdktf.NewTerraformOutput(stack, jsii.String("quotes"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_RawString(jsii.String("\"b\"")), +}) +cdktf.NewTerraformOutput(stack, jsii.String("template"), &cdktf.TerraformOutputConfig{ + Value: cdktf.Fn_RawString(jsii.String("${TEMPLATE}")), +}) +``` + ## Operators Use the `Op` object to include operators like `!`, `+`, and `-`.