Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(core): Vpc.fromVpcAttributes using list tokens now throws errors #11945

Closed
bf-sodle opened this issue Dec 8, 2020 · 17 comments · Fixed by #12040
Closed

(core): Vpc.fromVpcAttributes using list tokens now throws errors #11945

bf-sodle opened this issue Dec 8, 2020 · 17 comments · Fixed by #12040
Assignees
Labels
@aws-cdk/core Related to core CDK functionality bug This issue is a bug. needs-triage This issue or PR still needs to be triaged.

Comments

@bf-sodle
Copy link

bf-sodle commented Dec 8, 2020

When importing a VPC using outputs from another stack, if the route table IDs are not specified for each imported subnet, a warning is printed to the console, as expected:

No routeTableId was provided to the subnet '#{Token[TOKEN.58]}'. Attempting to read its .routeTable.routeTableId will return null/undefined. (More info: https://github.com/aws/aws-cdk/pull/3171)

This warning line is wrongly evaluated by the new "floating token" check introduced by #11899, and throws an error when synthesizing the stack:

Found an encoded list token string in a scalar string context. Use 'Fn.select(0, list)' (not 'list[0]') to extract elements from token lists.

Additionally, this line is "consumed" by the floating-token check and does not reach the console.

Reproduction Steps

import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";

class TestStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    const vpcId = cdk.Fn.importValue("myVpcId");
    const availabilityZones = cdk.Fn.split(",", cdk.Fn.importValue("myAvailabilityZones"));
    const publicSubnetIds = cdk.Fn.split(",", cdk.Fn.importValue("myPublicSubnetIds"));

    ec2.Vpc.fromVpcAttributes(this, "importedVpc", {
      vpcId,
      availabilityZones,
      publicSubnetIds,
    });
  }
}

const app = new cdk.App();
new TestStack(app, "TestStack", {});

What did you expect to happen?

VPC is imported without error, and the following warning line is printed to the console:

No routeTableId was provided to the subnet '#{Token[TOKEN.58]}'. Attempting to read its .routeTable.routeTableId will return null/undefined. (More info: https://github.com/aws/aws-cdk/pull/3171)

What actually happened?

No warning is printed about the route table, and the stack fails to synthesize with the following error:

Found an encoded list token string in a scalar string context. Use 'Fn.select(0, list)' (not 'list[0]') to extract elements from token lists.

Environment

  • CDK CLI Version : 1.77.0
  • Framework Version: 1.77.0
  • Node.js Version: 12.20.0
  • OS : Linux (Pop!_OS 20.04 LTS x86_64)
  • Language (Version): TypeScript 3.9.7

Other

The floating token check should ignore CDK "logging" lines if possible.


This is 🐛 Bug Report

@bf-sodle bf-sodle added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Dec 8, 2020
@github-actions github-actions bot added the @aws-cdk/core Related to core CDK functionality label Dec 8, 2020
@markussiebert
Copy link
Contributor

Wanted to open up this issue right now :-) None of my projects work with cdk 1.77

got exactly the same behaviour ...

i added some debug output tu the core function function resolve(obj, options) {

// If this is a "list element" Token, it should never occur by itself in string context
        if (encoding_1.TokenString.forListToken(obj).test()) {
            throw new Error(`Found an encoded list token string in a scalar string context. Use \'Fn.select(0, list)\' (not \'list[0]\') to extract elements from token lists. ${obj}`);
        }

and ended up with an error message like:

Error: Found an encoded list token string in a scalar string context. Use 'Fn.select(0, list)' (not 'list[0]') to extract elements from token lists. No routeTableId was provided to the subnet '#{Token[TOKEN.198]}'. Attempting to read its .routeTable.routeTableId will return null/undefined. (More info: https://github.com/aws/aws-cdk/pull/3171)

please fix this, want to use cdk 1.77 because auf WEB_IDENTITY_TOKEN_FILE Credentials, but right now its unusable :-/

@bf-sodle
Copy link
Author

bf-sodle commented Dec 8, 2020

@Markus7811 I was able to work around this by importing the route tables IDs as well. This silences the warning, and prevents it from triggering the check. That could be an option for you as well if that data is available to you.

@markussiebert
Copy link
Contributor

markussiebert commented Dec 8, 2020

good point ... testing it out :-)
... no chance in my case.

Main Problem ... the errormessage tells me that it's token 202 - but i don't have any idea what resource or import is causing this.

@rix0rrr
Copy link
Contributor

rix0rrr commented Dec 9, 2020

Thanks for the reproduction steps @bf-sodle , but the code you pasted doesn't do it for me.

I'm also going to need details on how you're USING the vpc.

@markussiebert
Copy link
Contributor

markussiebert commented Dec 9, 2020

I figured it out, that this line is causing (my) problems (importing subnets with id only):

this.kubectlPrivateSubnets = props.kubectlPrivateSubnetIds ? props.kubectlPrivateSubnetIds.map((subnetid, index) => ec2.Subnet.fromSubnetId(this, `KubectlSubnet${index}`, subnetid)) : undefined;

But the code causing this issue is:

Annotations.of(this).addWarning(`No routeTableId was provided to the subnet ${ref}. Attempting to read its .routeTable.routeTableId will return null/undefined. (More info: https://github.com/aws/aws-cdk/pull/3171)`);

@rix0rrr
Copy link
Contributor

rix0rrr commented Dec 9, 2020

Honestly, because of the way the CDK works, ec2.Vpc.fromVpcAttributes with an ImportValue (or a CfnParameter for that matter), can and will never work.

You must have been getting infrastructure that was only using the first AZ/SubnetId of your list, right?

I mean, it could be done if you tell us how many elements there will be in the list, but not automatically.

@rix0rrr rix0rrr changed the title (core): new "floating token" check wrongly evaluates CDK log lines and throws false positive. (core): Vpc.fromVpcAttributes using list tokens now throws errors Dec 9, 2020
@rix0rrr
Copy link
Contributor

rix0rrr commented Dec 9, 2020

Since the #{Token[TOKEN.58]} token wouldn't be resolved even in previous versions, I'm also struggling to see how any template synthesized before (which would still have that literal value in it), would have successfully deployed.

@markussiebert
Copy link
Contributor

It was working ... i was using it with ssm parameter and fn.split to get an array ... (the parameter was written joining the subnetids with ",")

@rix0rrr
Copy link
Contributor

rix0rrr commented Dec 9, 2020

Can you show me some code of how you were using that Vpc imported in that way though? With what kind of resource(s) were you using it?

@markussiebert
Copy link
Contributor

markussiebert commented Dec 9, 2020

of course :-) As written above, I import eks clusters from ssm ...

 this.clusterRef = eks.Cluster.fromClusterAttributes(this, id, {
      clusterName: props.clusterName,
      vpc: ec2.Vpc.fromVpcAttributes(this, 'Vpc', {
        availabilityZones: ssm.StringListParameter.fromStringListParameterName(this, 'VpcAvailabilityZonesSsm', `/eks/${props.clusterName}/EksClusterRef/VpcAvailabilityZones`).stringListValue,
        vpcId: vpcId,
      }),
      kubectlRoleArn: ssm.StringParameter.fromStringParameterName(
        this,
        'KubectlRoleArnSsm',
        `/eks/${props.clusterName}/EksClusterRef/KubectlRoleArn`,
      ).stringValue,
      kubectlSecurityGroupId: ssm.StringParameter.fromStringParameterName(
        this,
        'KubectlSecurityGroupIdSsm',
        `/eks/${props.clusterName}/EksClusterRef/KubectlSecurityGroupId`,
      ).stringValue,
      kubectlLayer: lambda.LayerVersion.fromLayerVersionArn(
        this,
        'KubectlLayer',
        ssm.StringParameter.fromStringParameterName(
          this,
          'KubectlLayerSsm',
          `/eks/${props.clusterName}/EksClusterRef/KubectlLayer`,
        ).stringValue,
      ),
      clusterCertificateAuthorityData: ssm.StringParameter.fromStringParameterName(
        this,
        'ClusterCertificateAuthorityDataSsm',
        `/eks/${props.clusterName}/EksClusterRef/ClusterCertificateAuthorityData`,
      ).stringValue,
      clusterEncryptionConfigKeyArn: ssm.StringParameter.fromStringParameterName(
        this,
        'ClusterEncryptionConfigKeyArnSsm',
        `/eks/${props.clusterName}/EksClusterRef/ClusterEncryptionConfigKeyArn`,
      ).stringValue,
      clusterSecurityGroupId: ssm.StringParameter.fromStringParameterName(
        this,
        'ClusterSecurityGroupIdSsm',
        `/eks/${props.clusterName}/EksClusterRef/ClusterSecurityGroupId`,
      ).stringValue,
      openIdConnectProvider: openIdConnectProvider,
      kubectlPrivateSubnetIds: cdk.Fn.split(',', ssm.StringParameter.fromStringParameterName(this, 'KubectlPrivateSubnetsStringSsm', `/eks/${props.clusterName}/EksClusterRef/KubectlPrivateSubnetsString`).stringValue),
      kubectlEnvironment: {
        http_proxy: ssm.StringParameter.fromStringParameterName(
          this,
          'KubectlEnvironmentHttpProxySsm',
          `/eks/${props.clusterName}/EksClusterRef/KubectlEnvironmentHttpProxy`,
        ).stringValue,
        https_proxy: ssm.StringParameter.fromStringParameterName(
          this,
          'KubectlEnvironmentHttpsProxySsm',
          `/eks/${props.clusterName}/EksClusterRef/KubectlEnvironmentHttpsProxy`,
        ).stringValue,
        no_proxy: ssm.StringParameter.fromStringParameterName(
          this,
          'KubectlEnvironmentNoProxySsm',
          `/eks/${props.clusterName}/EksClusterRef/KubectlEnvironmentNoProxy`,
        ).stringValue,
      },
    });

This was working till 1.76, but not with 1.77 ...
Tried to workaround by using vpc.fromLooukup (worked) and subnetSelection (didn't work - lacking fine grained selection). So at the moment, i think this is a bug?

@bf-sodle
Copy link
Author

bf-sodle commented Dec 9, 2020

On our end, we're importing the entire VPC as described above. We're then able to create an auto-scaling group like so, and it seems to span both all private subnets as it should.

const vpcId = cdk.Fn.importValue("myVpcId");
const availabilityZones = cdk.Fn.split(",", cdk.Fn.importValue("myAvailabilityZones"));
const publicSubnetIds = cdk.Fn.split(",", cdk.Fn.importValue("myPublicSubnetIds"));
const privateSubnetIds = cdk.Fn.split(",", cdk.Fn.importValue("myPrivateSubnetIds"));
const isolatedSubnetIds = cdk.Fn.split(",", cdk.Fn.importValue("myIsolatedSubnetIds"));

const vpc = ec2.Vpc.fromVpcAttributes(this, "importedVpc", {
  vpcId,
  availabilityZones,
  publicSubnetIds,
  privateSubnetIds,
  isolatedSubnetIds,
});

const clusterAsg = new autoscaling.AutoScalingGroup(this, "ecs-ec2-asg", {
  instanceType: ec2.InstanceType.of(instanceClass, instanceSize),
  machineImage,
  minCapacity,
  maxCapacity,
  desiredCapacity,
  vpc,
  allowAllOutbound: false,
  associatePublicIpAddress: false,
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE },
  role,
});

@rix0rrr
Copy link
Contributor

rix0rrr commented Dec 9, 2020

it seems to span both all private subnets as it should.

Blows my mind, because I feel like that wouldn't work. I will look into that.

EDIT: Oh wait for ASG it might...

@bf-sodle
Copy link
Author

bf-sodle commented Dec 9, 2020

I believe the magic is in using the SubnetSelection type (the value of vpcSubnets when creating the ASG). It produces the following CloudFormation, which resolves our imported value into an array of subnets just fine.

  ecsec2asgASGBC05D157:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      MaxSize: "4"
      MinSize: "2"
      DesiredCapacity: "2"
      LaunchConfigurationName:
        Ref: ecsec2asgLaunchConfig487A86FD
      VPCZoneIdentifier:
        Fn::Split:
          - ","
          - Fn::ImportValue: myPrivateSubnetIds

@markussiebert
Copy link
Contributor

I solved it for my use case by switching to VPC fromLookup instead of fromAttributes and switching from ssm.StringParameter.fromStringParameterName to ssm.StringParameter.valueFromLookup for the subnet part. Now the warnings are displayed and it works like it should ...

rix0rrr added a commit that referenced this issue Dec 12, 2020
…ime lists

Even though using `Vpc.fromVpcAttributes()` using deploy-time lists like
from `Fn.importValue()`s and `CfnParameters` was never really supposed
to work, it accidentally did.

The reason for that is:

```ts

// Encoded list token
const subnetIds = Token.asList(Fn.importValue('someValue'))
// [ '#{Token[1234]}' ]

// Gets parsed to a singleton `Subnet` list:
const subnets = subnetIds.map(s => Subnet.fromSubnetAttributes({ subnetId: s }));
// [ Subnet({ subnetId: '#{Token[1234]}' }) ]

// This 'subnetId' is illegal by itself, and if yould try to use it for,
// say, an ec2.Instance it would fail. However, if you treat this single
// subnet as a GROUP of subnets:
new CfnAutoScalingGroup({ subnetIds: subnets.map(s => s.subnetId) })
// [ '#{Token[1234]}' ]

// And this resolves back to:
resolve(cfnSubnetIds)
// SubnetIds: { Fn::ImportValue: 'someValue' }
```

--------

We introduced an additional check in #11899 to make sure that the
list-element token that represents an encoded list (`'#{Token[1234]}'`)
never occurs in a non-list context, because it's illegal there.

However, because:

* `Subnet.fromSubnetAttributes()` logs the subnetId as a *warning*
  to its own metadata (which will log a string like `"there's something
  wrong with '#{Token[1234]}' ..."`).
* All metadata is resolved just the same as the template expressions
  are.

The `resolve()` function encounters that orphaned list token in the
metadata and throws.

--------

The *proper* solution would be to handle unparseable list tokens
specially to never get into this situation, but doing that requires
introducing classes and new concepts that will be a large effort and
not be backwards compatible. Tracking in #4118.

Another possible solution is to stop resolving metadata. I don't
know if we usefully use this feature; I think we don't. However,
we have tests enforcing that it is being done, and I'm scared
to break something there.

The quick solution around this for now is to have
`Subnet.fromSubnetAttributes()` recognize when it's about to log
a problematic identifier to metadata, and don't do it.

Fixes #11945.
@mergify mergify bot closed this as completed in #12040 Dec 14, 2020
mergify bot pushed a commit that referenced this issue Dec 14, 2020
…ime lists (#12040)

Even though using `Vpc.fromVpcAttributes()` using deploy-time lists like
from `Fn.importValue()`s and `CfnParameters` was never really supposed
to work, it accidentally did.

The reason for that is:

```ts

// Encoded list token
const subnetIds = Token.asList(Fn.importValue('someValue'))
// [ '#{Token[1234]}' ]

// Gets parsed to a singleton `Subnet` list:
const subnets = subnetIds.map(s => Subnet.fromSubnetAttributes({ subnetId: s }));
// [ Subnet({ subnetId: '#{Token[1234]}' }) ]

// This 'subnetId' is illegal by itself, and if yould try to use it for,
// say, an ec2.Instance it would fail. However, if you treat this single
// subnet as a GROUP of subnets:
new CfnAutoScalingGroup({ subnetIds: subnets.map(s => s.subnetId) })
// [ '#{Token[1234]}' ]

// And this resolves back to:
resolve(cfnSubnetIds)
// SubnetIds: { Fn::ImportValue: 'someValue' }
```

--------

We introduced an additional check in #11899 to make sure that the
list-element token that represents an encoded list (`'#{Token[1234]}'`)
never occurs in a non-list context, because it's illegal there.

However, because:

* `Subnet.fromSubnetAttributes()` logs the subnetId as a *warning*
  to its own metadata (which will log a string like `"there's something
  wrong with '#{Token[1234]}' ..."`).
* All metadata is resolved just the same as the template expressions
  are.

The `resolve()` function encounters that orphaned list token in the
metadata and throws.

--------

The *proper* solution would be to handle unparseable list tokens
specially to never get into this situation, but doing that requires
introducing classes and new concepts that will be a large effort and
not be backwards compatible. Tracking in #4118.

Another possible solution is to stop resolving metadata. I don't
know if we usefully use this feature; I think we don't. However,
we have tests enforcing that it is being done, and I'm scared
to break something there.

The quick solution around this for now is to have
`Subnet.fromSubnetAttributes()` recognize when it's about to log
a problematic identifier to metadata, and don't do it.

Fixes #11945.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@github-actions
Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

@adriantaut
Copy link
Contributor

adriantaut commented Jan 4, 2021

@rix0rrr not sure if I'm missing something, but I'm still facing the same issue with [email protected] when attempting to spin up an EKS cluster. Maybe it's worth mentioning, the VPC has both public/private subnets, thus public/private Routing Tables. Please find my code below:

const vpc = Vpc.fromVpcAttributes(scope, 'DefaultVpc', {
    vpcId: ssm.StringParameter.valueForStringParameter(scope, `${basePath}/VpcId`),
    availabilityZones: ssm.StringParameter.valueForStringParameter(scope, `${basePath}/AvailabilityZones`),
    privateSubnetIds: ssm.StringParameter.valueForStringParameter(scope, `${basePath}/PrivateSubnetIds`),
    privateSubnetRouteTableIds: ssm.StringParameter.valueForStringParameter(scope, `${basePath}/PrivateSubnetRouteTableIds`),
    publicSubnetIds: ssm.StringParameter.valueForStringParameter(scope, `${basePath}/PublicSubnetIds`),
    publicSubnetRouteTableIds: ssm.StringParameter.valueForStringParameter(scope, `${basePath}/PublicSubnetRouteTableIds`),
  });

const cluster = new eks.Cluster(this, 'DetestadrCluster', {
     vpc: vpc,
     defaultCapacity: 0,
     version: eks.KubernetesVersion.V1_18
});

Throwing the same error when resolving the template:

/Users/tauta/gitlab/aws/2.0aws/cdk/detestadr-configrepo-aws-ppb/cdk/node_modules/@aws-cdk/core/lib/private/resolve.ts:94
      throw new Error('Found an encoded list token string in a scalar string context. Use \'Fn.select(0, list)\' (not \'list[0]\') to extract elements from token lists.');

In addition, spinning an ECS cluster has no issues:

        const ecsCluster = new ecs.Cluster(this, 'EcsCluster', {
            clusterName: `blablablaCluster`,
            vpc: vpc,
        });

flochaz pushed a commit to flochaz/aws-cdk that referenced this issue Jan 5, 2021
…ime lists (aws#12040)

Even though using `Vpc.fromVpcAttributes()` using deploy-time lists like
from `Fn.importValue()`s and `CfnParameters` was never really supposed
to work, it accidentally did.

The reason for that is:

```ts

// Encoded list token
const subnetIds = Token.asList(Fn.importValue('someValue'))
// [ '#{Token[1234]}' ]

// Gets parsed to a singleton `Subnet` list:
const subnets = subnetIds.map(s => Subnet.fromSubnetAttributes({ subnetId: s }));
// [ Subnet({ subnetId: '#{Token[1234]}' }) ]

// This 'subnetId' is illegal by itself, and if yould try to use it for,
// say, an ec2.Instance it would fail. However, if you treat this single
// subnet as a GROUP of subnets:
new CfnAutoScalingGroup({ subnetIds: subnets.map(s => s.subnetId) })
// [ '#{Token[1234]}' ]

// And this resolves back to:
resolve(cfnSubnetIds)
// SubnetIds: { Fn::ImportValue: 'someValue' }
```

--------

We introduced an additional check in aws#11899 to make sure that the
list-element token that represents an encoded list (`'#{Token[1234]}'`)
never occurs in a non-list context, because it's illegal there.

However, because:

* `Subnet.fromSubnetAttributes()` logs the subnetId as a *warning*
  to its own metadata (which will log a string like `"there's something
  wrong with '#{Token[1234]}' ..."`).
* All metadata is resolved just the same as the template expressions
  are.

The `resolve()` function encounters that orphaned list token in the
metadata and throws.

--------

The *proper* solution would be to handle unparseable list tokens
specially to never get into this situation, but doing that requires
introducing classes and new concepts that will be a large effort and
not be backwards compatible. Tracking in aws#4118.

Another possible solution is to stop resolving metadata. I don't
know if we usefully use this feature; I think we don't. However,
we have tests enforcing that it is being done, and I'm scared
to break something there.

The quick solution around this for now is to have
`Subnet.fromSubnetAttributes()` recognize when it's about to log
a problematic identifier to metadata, and don't do it.

Fixes aws#11945.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@jahtoe
Copy link

jahtoe commented Feb 26, 2021

Greetings,

Is the LAMP stack scalable and durable example on on the example templates to provision application frameworks AWS page as good example of a template that can lead to the error when trying to resolve the subnets from the parameter?

Such that if you set a default for the subnets as follows

"Subnets" : { "Type" : "List<AWS::EC2::Subnet::Id>", "Description" : "The list of SubnetIds in your Virtual Private Cloud (VPC)", "ConstraintDescription" : "must be a list of at least two existing subnets associated with at least two different availability zones. They should be residing in the selected Virtual Private Cloud.", "Default": [ "subnet-123a351e", "subnet-456b351e" ] },

Which is referenced in the AutoScaling resource by

"VPCZoneIdentifier" : { "Ref" : "Subnets" }

When trying to resolve an item in the CfnAutoScalingGroup C.getVpcZoneIdentifier() list, using the template this.resolve(item) method, the error

Found an encoded list token string in a scalar string context. Use 'Fn.select(0, list)' (not 'list[0]') to extract elements from token lists.
is Expected?

And a solution is to replace the reference to the subnet parameter as

"VPCZoneIdentifier" : [ "subnet-123a351e", "subnet-456b351e" ]

Or is there a coding solution for this and a way to recognize this problem?

Regards
Alex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/core Related to core CDK functionality bug This issue is a bug. needs-triage This issue or PR still needs to be triaged.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants