aws-cloudformation / cfn-language-discussion

Language discussions for CloudFormation template language
https://aws.amazon.com/cloudformation/
Apache License 2.0
143 stars 13 forks source link

Fn::ToJsonString: Template error: every value of the context object of every Fn::Sub object must be a string or a function that returns a string #103

Open rix0rrr opened 1 year ago

rix0rrr commented 1 year ago

Community Note

Tell us about the bug

When Fn::ToJsonString is given an array retrieved from a resource using { Fn::GetAtt }, the deployment fails with the following error:

Template error: every value of the context object of every Fn::Sub object must be a string or a function that returns a string

Expected behavior

I want to see a JSON-ified representation of the array.

Observed behavior

Deployment fails with an error.

Test cases

Transform: AWS::LanguageExtensions
Resources:
  ZoneA5DE4B68:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: banana.com.
Outputs:
  Output:
    Value:
      Fn::ToJsonString:
        someList:
          Fn::GetAtt:
            - ZoneA5DE4B68
            - NameServers

Additional context

I'm from the CDK team. It would make our life A LOT simpler if we could rely on CloudFormation built-ins to produce JSON strings from object literals (given that 99.9% of AWS APIs run on JSON 😅), but limitations of the Fn::ToJsonString functions make it unreliable, and therefore impossible for us to use (since the CDK middleware will not have the context that a human author would have on whether it's okay to use the function in this particular use case or not).

benbridts commented 1 year ago

This isn't called out in the documentation or RFC, but since the language extension is a transform / macro, it can't access Property Values from resources (or rather, only handles them opaquely). This happens because it has to run at change-set generation).

As an example, this does not output what you want, but is a valid way to use ToJsonString with GetAtt (!Join will convert the List to a String):

Transform: AWS::LanguageExtensions
Resources:
  ZoneA5DE4B68:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: banana.com.
Outputs:
  Output:
    Value:
      Fn::ToJsonString:
        someString: !Join
          - ','
          - Fn::GetAtt:
              - ZoneA5DE4B68
              - NameServers

The output would be something like {"someString":"ns-202.awsdns-25.com,ns-840.awsdns-41.net,ns-1768.awsdns-29.co.uk,ns-1281.awsdns-32.org"}

rix0rrr commented 1 year ago

I know why this happening. I am reporting this as an uncovered need so CloudFormation can have an internal discussion on whether {Fn::ToJsonString} should be a proper intrinsic function or not.

MalikAtalla-AWS commented 1 year ago

Hey @rix0rrr, before I dive too deep into this issue, just want to double-check if the indentation issue in your example is accidental or intentional. I assume you mean this? (Output a sibling of Resources, not a child)

Transform: AWS::LanguageExtensions
Resources:
  ZoneA5DE4B68:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: banana.com.
Outputs:
  Output:
    Value:
      Fn::ToJsonString:
        someList:
          Fn::GetAtt:
            - ZoneA5DE4B68
            - NameServers
MalikAtalla-AWS commented 1 year ago

Hey @rix0rrr, (I'm putting aside the question if Fn::ToJsonString should be natively supported (without Transform). That's a legitimate ask, but let's discuss it in the other issue (#105) you have opened.)

When I use the template above where I adjusted the indentation, then the Transform succeeds and yields the output below. Since it can't resolve the Fn::GetAtt expression, it converts it into an Fn::Sub so that it can be resolved later. That's the intended behaviour. Let me know if it doesn't behave as you expected.

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Outputs": {
    "Output": {
      "Value": {
        "Fn::Sub": [
          "{\"someList\":\"${44b47ae35a7d41dc95bd5991efc31fab}\"}",
          {
            "44b47ae35a7d41dc95bd5991efc31fab": {
              "Fn::GetAtt": [
                "Zone",
                "NameServers"
              ]
            }
          }
        ]
      }
    }
  },
  "Resources": {
    "Zone": {
      "Type": "AWS::Route53::HostedZone",
      "Properties": {
        "Name": "banana.com"
      }
    }
  }
}
MalikAtalla-AWS commented 1 year ago

I'll close this issue. Feel free to reopen, if you think we're missing something.

rix0rrr commented 1 year ago

Hi @MalikAtalla-AWS, you are right, I messed up my indentation. Sorry for the confusion, but you figured out what I meant to type.


I know that the transform works, but a deployment of that transformed template does not work. The reason is that ${Zone.Nameservers} returns an array, and { Fn::Sub } does not like that.

I tried to deploy your transformed template:

$ aws cloudformation update-stack --stack-name TestarraysStack --template-body file://./transformed.json

And it does not deploy:

image


What I wanted as output is

"{ \"someList\": [\"ns1.amazonaws.com\", \"ns2.amazonaws.com\", \"ns3.amazonaws.com\"] }"
               ^^^^^                                                                ^^^^

And there is no way that:

Fn::Sub: 
  - "{ \"someList\": \" <.....> \" }"
    #              ^^^^^      ^^^^^
  - ....

Is going to give me that, because the quotes it adds around the value are already incorrect. I know the transform assumes that all values it can't see yet are going to be strings, but that is exactly the problem!

There is no way this issue can be solved by a transform. { Fn::ToJsonString } needs access to the actual value to do the right jsonification operation.

rix0rrr commented 1 year ago

You might be tempted to say you could trace the types of all attributes and try to do something clever in the transform such as omitting the quotes...

...but in CloudFormation Registry resources it's possible to {Fn::GetAtt} complex objects, and arrays of complex objects, and there's definitely no way that a transform is going to know the shape of format string to produce to accurately stringify any data structure of variable size.

(We struggle with this in CDK as well, which is why I'm hoping to offload the work onto a CloudFormation intrinsic 🥹)

rsorelli-hedgepoint commented 6 months ago

Having the same issue to retrieve the AmqpEndpoints attribute from AWS::AmazonMQ::Broker which returns and object and can't stringfy it to save on a parameter store.

  RabbitMQEndpointParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Description: "AWS RabbitMQ Endpoint"
      Name:  !Sub "/myPath/rabbitmq/endpoint"
      Type: StringList
      Value: 
        Fn::ToJsonString: 
          !GetAtt AWSRabbitMQ.AmqpEndpoints