terraform-cdk icon indicating copy to clipboard operation
terraform-cdk copied to clipboard

Resource FQN tokens aren't resolved in a user friendly way

Open jsteinich opened this issue 3 years ago • 6 comments

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

cdktf & Language Versions

0.9.0. Any language.

Affected Resource(s)

Attempting to use .fqn on any resource when appending to it or using in an array (probably other cases as well). This mainly comes up when needing to use escape hatches.

Expected Behavior

Synthesized output is valid.

Actual Behavior

  • Expected the start of an expression, but found an invalid expression token
  • jsii.errors.JSIIError: Expected array type, got ""
  • X is object with Y attributes

Important Factoids

Support for cross stack references caused a change in behavior. Previously .fqn actually return just a raw string (string token of string is the input string).

References

  • https://discuss.hashicorp.com/t/python-escape-hatching-in-0-9-x/37202

jsteinich avatar Mar 22 '22 21:03 jsteinich

I just reached an scenario where .fqn is of type string, but it actually resolves into an array of objects, making it impossible to use it as value in any other object.

Just for the case, this is the scenario:

const webappService = new Service(this, 'k8s-service-webapp', {
  ...
  waitForLoadBalancer: true,
});

new DnsRecordSet(this, `webapp-dns-zone-record`, {
  ...
  rrdatas: [(webappService?.status.fqn as any)[0]?.load_balancer?.ingress?.ip], // won't be resolved properly
});

armandosorianopfs avatar Jul 07 '22 13:07 armandosorianopfs

You would need to take terraform functions in this case. It should look sth like this: Fn.lookup(Fn.lookup(Fn.lookup(Fn.element(webappService.status.fqn, 0), "load_balancer"), "ingress"), "ip")

DanielMSchmidt avatar Jul 07 '22 15:07 DanielMSchmidt

@DanielMSchmidt you made my day!! thanks!!

If anyone reaches this issue with similar issue note that you need to use lookup to resolve object properties and element to resolve arrays. For my case, I've reached to this:

private getIpFromStatusFqn(fqn: string): string {
  const resolvedFqn = Fn.element(fqn as unknown as any[], 0);
  const loadBalancer = Fn.lookup(resolvedFqn, 'load_balancer', []);
  const loadBalancerFirstElement = Fn.element(loadBalancer, 0);
  const ingress = Fn.lookup(loadBalancerFirstElement, 'ingress', []);
  const ingressFirstElement = Fn.element(ingress, 0);

  return Fn.lookup(ingressFirstElement, 'ip', 'no-ip');
}

armandosorianopfs avatar Jul 08 '22 08:07 armandosorianopfs

I'm running into a similar issue though using Fn.lookup and Fn.element isn't working well in all cases. Here's code that works well on version 0.8.6 (note the use of .fqn in the addOverride at the bottom)

    const dataAwsAvailabilityZonesAll =
      new aws.datasources.DataAwsAvailabilityZones(
        this,
        "allAvailableZones",
        {}
      );

    const efs = new aws.efs.EfsFileSystem(this, "efs-volume", {
      tags: {
        name: name,
        deployment: "ipfs",
      },
    });

    const efsMountTarget = new aws.efs.EfsMountTarget(
      this,
      "efs-mount-target",
      {
        dependsOn: [efs],
        count: 2,
        fileSystemId: efs.id,
        subnetId: "${aws_subnet.priv_subnet[count.index].id}",
        securityGroups: config.vpcSecurityGroups.ids,
      }
    );
    efsMountTarget.addOverride(
      "count",
      `\${length(${dataAwsAvailabilityZonesAll.fqn}.names)}`
    );

and here's my attempt at using Fn.element and Fn.lookup on version 0.12

    const zoneNames = Fn.lookup(dataAwsAvailabilityZonesAll.fqn, "names", undefined)
    efsMountTarget.addOverride(
      "count",
      Fn.lengthOf(zoneNames)
    );

but I still get issues like @armandosorianopfs gets:

[2022-08-02T09:47:39.574] [ERROR] default - ╷
│ Error: Invalid character
│
│   on cdk.tf.json line 271, in resource.aws_efs_mount_target.mercury-ipfs_efs-mount-target_B5A83491:
│  271:         "count": "${length(${data.aws_availability_zones.mercury-ipfs_allAvailableZones_45C25C25}.names)}",
│
│ This character is not used within the language.
goldsky-infra-dev  ╷
                   │ Error: Invalid character
                   │
                   │   on cdk.tf.json line 271, in resource.aws_efs_mount_target.mercury-ipfs_efs-mount-target_B5A83491 (mercury-ipfs/efs-mount-target):
                   │  271:         "count": "${length(${data.aws_availability_zones.mercury-ipfs_allAvailableZones_45C25C25 (mercury-ipfs/allAvailableZones)}.names)}",
                   │
                   │ This character is not used within the language.
                   ╵

⠇  Processing
[2022-08-02T09:47:39.592] [ERROR] default - ╷
│ Error: Invalid expression
│
│   on cdk.tf.json line 271, in resource.aws_efs_mount_target.mercury-ipfs_efs-mount-target_B5A83491:
│  271:         "count": "${length(${data.aws_availability_zones.mercury-ipfs_allAvailableZones_45C25C25}.names)}",
│
│ Expected the start of an expression, but found an invalid expression token.
goldsky-infra-dev  ╷
                   │ Error: Invalid expression
                   │
                   │   on cdk.tf.json line 271, in resource.aws_efs_mount_target.mercury-ipfs_efs-mount-target_B5A83491 (mercury-ipfs/efs-mount-target):
                   │  271:         "count": "${length(${data.aws_availability_zones.mercury-ipfs_allAvailableZones_45C25C25 (mercury-ipfs/allAvailableZones)}.names)}",
                   │
                   │ Expected the start of an expression, but found an invalid expression token.

paymog avatar Aug 02 '22 15:08 paymog

@paymog I've been looking into this issue. I was hoping to get some more information, as I couldn't reproduce it exactly.

Why is the syntax error happening

From what I understand, the root of the problem is this pattern:

${length(${data.foo}.names)}

Our token system assumes that we're going to be used within a string and adds interpolation syntax ${} around the token. However, since you're already wrapping it within a ${}, that becomes a syntax error in Terraform.

The 0.12 example doesn't work

and here's my attempt at using Fn.element and Fn.lookup on version 0.12

   const zoneNames = Fn.lookup(dataAwsAvailabilityZonesAll.fqn, "names", undefined)
   efsMountTarget.addOverride(
     "count",
     Fn.lengthOf(zoneNames)
   );

This shouldn't emit the error you mentioned. I tried it out on my end, and it seems to be working fine on my end.

It outputs:

"count": "${length(lookup(data.aws_availability_zones.allAvailableZones, \"names\", []))}",

mutahhir avatar Sep 20 '22 10:09 mutahhir

Thanks for looking into this @mutahir! I've long since resolved this issue and I don't quite remember how I did it.

Though, I suspect that the reason I was seeing that error on 0.12 was because 0.12 requires running tsc prior to diff/deploy while 0.8 didn't. I didn't realize that until quite a few hours into debugging the upgrade.

paymog avatar Sep 20 '22 20:09 paymog

While more functionality has been added to reduce the need for using fqn, it still doesn't change that fqn is less useable to users in the cases that it would still be ideal to use it.

jsteinich avatar Sep 27 '22 02:09 jsteinich

Ah, I didn't fully understand the core of the issue but went on from the last error message. My bad. Thanks for reopening @jsteinich, I'll take another look soon.

To clarify: The main issue is the inability to use .fqn within the interpolated section of a string. The token system's expectation that the resource fqn is being inserted into a non-interpolated terraform string is requiring users to jump through hoops to reference something that previously just required them to just do resource.fqn.

mutahhir avatar Sep 27 '22 14:09 mutahhir

Adding another example I'm currently running into while trying to setup a conditional expression, with variables in this case: If there's a better way I should be doing this please let me know.

Goal:

variable "a_var" {
    type = bool
}

variable "b_var" {}

output "test" {
    value = "${var.a_var ? var.b_var : null}"
}

Attempt:

class MyStack(TerraformStack):
    def __init__(self, scope: Construct, ns: str):
        super().__init__(scope, ns)

        a_var = TerraformVariable(self, "a_var")
        b_var = TerraformVariable(self, "b_var")

        TerraformOutput(self,
                        "test",
                        value="${" + f"{a_var.fqn} ? {b_var.fqn} : null" + "}"
        )

Generates:

...
  "output": {
    "test": {
      "value": "${${var.a_var} ? ${var.b_var} : null"
    }
  },
...

Workaround I'm currently using:

        TerraformOutput(self,
                        "test",
                        value="${" + f"var.{a_var.node.id} ? var.{b_var.node.id} : null" + "}"
        )

M1kep avatar Oct 17 '22 20:10 M1kep