YAML Error: could not determine a constructor for the tag

Matthew Patton picture Matthew Patton · Aug 30, 2017 · Viewed 11.9k times · Source

This is very similar to questions/44786412 but mine appears to be triggered by YAML safe_load(). I'm using Ruamel's library and YamlReader to glue a bunch of CloudFormation pieces together into a single, merged template. Is bang-notation just not proper YAML?

Outputs:
  Vpc:
    Value: !Ref vpc
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"

No problem with these

Outputs:
  Vpc:
    Value:
      Ref: vpc
    Export:
      Name:
        Fn::Sub: "${AWS::StackName}-Vpc"

Resources:
  vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock:
        Fn::FindInMap: [ CidrBlock, !Ref "AWS::Region", Vpc ]

Part 2; how to get load() to leave what's right of the 'Fn::Select:' alone.

  FromPort: 
    Fn::Select: [ 0, Fn::FindInMap: [ Service, https, Ports ] ]

gets converted to this, that now CF doesn't like.

  FromPort:
    Fn::Select: [0, {Fn::FindInMap: [Service, https, Ports]}]

If I unroll the statement fully then no problems. I guess the shorthand is just problematic.

  FromPort:
    Fn::Select:
    - 0
    - Fn::FindInMap: [Service, ssh, Ports]

Answer

Anthon picture Anthon · Aug 30, 2017

Your "bang notation" is proper YAML, normally this is called a tag. If you want to use the safe_load() with those you'll have to provide constructors for the !Ref and !Sub tags, e.g. using:

ruamel.yaml.add_constructor(u'!Ref', your_ref_constructor, constructor=ruamel.yaml.SafeConstructor)

where for both tags you should expect to handle scalars a value. and not the more common mapping.

I recommend you use the RoundTripLoader instead of the SafeLoader, that will preserve order, comments, etc. as well. The RoundTripLoader is a subclass of the SafeLoader.

If you are using ruamel.yaml>=0.15.33, which supports round-tripping scalars, you can do (using the new ruamel.yaml API):

import sys
from ruamel.yaml import YAML

yaml = YAML()
yaml.preserve_quotes = True

data = yaml.load("""\
Outputs:
  Vpc:
    Value: !Ref: vpc    # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag
""")

yaml.dump(data, sys.stdout)

to get:

Outputs:
  Vpc:
    Value: !Ref: vpc    # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag

In older 0.15.X versions, you'll have to specify the classes for the scalar objects yourself. This is cumbersome, if you have many objects, but allows for additional functionality:

import sys
from ruamel.yaml import YAML


class Ref:
    yaml_tag = u'!Ref:'

    def __init__(self, value, style=None):
        self.value = value
        self.style = style

    @classmethod
    def to_yaml(cls, representer, node):
        return representer.represent_scalar(cls.yaml_tag,
                                            u'{.value}'.format(node), node.style)

    @classmethod
    def from_yaml(cls, constructor, node):
        return cls(node.value, node.style)

    def __iadd__(self, v):
        self.value += str(v)
        return self

class Sub:
    yaml_tag = u'!Sub'
    def __init__(self, value, style=None):
        self.value = value
        self.style = style

    @classmethod
    def to_yaml(cls, representer, node):
        return representer.represent_scalar(cls.yaml_tag,
                                            u'{.value}'.format(node), node.style)

    @classmethod
    def from_yaml(cls, constructor, node):
        return cls(node.value, node.style)


yaml = YAML(typ='rt')
yaml.register_class(Ref)
yaml.register_class(Sub)

data = yaml.load("""\
Outputs:
  Vpc:
    Value: !Ref: vpc    # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag
""")

data['Outputs']['Vpc']['Value'] += '123'

yaml.dump(data, sys.stdout)

which gives:

Outputs:
  Vpc:
    Value: !Ref: vpc123 # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag