Convert API Gateway Cloudformation template to Swagger file

Viewed 623

There is an existing API described in a Coludformation template. Now I want to document the API using Swagger. Is there a way to parse the Cloudformation template to create the swagger.yaml specification file? I would like to avoid writing the API a second time, if possible.

Note: I am aware that you can define your API using Swagger, then import the API configuration in your Cloudformation template. This is not what I need. The Cloudformation already exists and will not be changed. Hence, I need the opposite: a Swagger configuration file based on an existing Cloudformation template.

2 Answers

There is no way to convert the template to a swagger file that I know about. But if you are looking for a way to keep service-spec in one place only (template) and you have it deployed, you can take swagger or OAS file from the stage (so to do it you must have a stage as well) in two ways at least:

  1. By Web console. Use Amazon API Gateway-> APIs->Your API->Stages>Your Stage -> Export tab. See the picture: exporting Swagger or OAS as a file by Web console

  2. aws apigateway get-export ... Here is an example:

aws apigateway get-export --rest-api-id ${API_ID} --stage-name ${STAGE_NAME} --export-type swagger swagger.json

I just made this, it is not setup for perfect plug/play, but will give you an idea what you need to adjust to get it working (also need to make sure you CF template is setup so it has the needed info, on mine I had to add some missing requestParams I was missing, also use this site to test your results from this code to see it works with swagger):

const yaml = require('js-yaml');
const fs = require('fs');

// Get document, or throw exception on error
try {
  // loads file from local
  const inputStr = fs.readFileSync('../template.yaml', { encoding: 'UTF-8' });
  // creating a schema to handle custom tags (cloud formation) which then js-yaml can handle when parsing
  const CF_SCHEMA = yaml.DEFAULT_SCHEMA.extend([
    new yaml.Type('!ImportValue', {
      kind: 'scalar',
      construct: function (data) {
        return { 'Fn::ImportValue': data };
      },
    }),
    new yaml.Type('!Ref', {
      kind: 'scalar',
      construct: function (data) {
        return { Ref: data };
      },
    }),
    new yaml.Type('!Equals', {
      kind: 'sequence',
      construct: function (data) {
        return { 'Fn::Equals': data };
      },
    }),
    new yaml.Type('!Not', {
      kind: 'sequence',
      construct: function (data) {
        return { 'Fn::Not': data };
      },
    }),
    new yaml.Type('!Sub', {
      kind: 'scalar',
      construct: function (data) {
        return { 'Fn::Sub': data };
      },
    }),
    new yaml.Type('!If', {
      kind: 'sequence',
      construct: function (data) {
        return { 'Fn::If': data };
      },
    }),
    new yaml.Type('!Join', {
      kind: 'sequence',
      construct: function (data) {
        return { 'Fn::Join': data };
      },
    }),
    new yaml.Type('!Select', {
      kind: 'sequence',
      construct: function (data) {
        return { 'Fn::Select': data };
      },
    }),
    new yaml.Type('!FindInMap', {
      kind: 'sequence',
      construct: function (data) {
        return { 'Fn::FindInMap': data };
      },
    }),
    new yaml.Type('!GetAtt', {
      kind: 'scalar',
      construct: function (data) {
        return { 'Fn::GetAtt': data };
      },
    }),
    new yaml.Type('!GetAZs', {
      kind: 'scalar',
      construct: function (data) {
        return { 'Fn::GetAZs': data };
      },
    }),
    new yaml.Type('!Base64', {
      kind: 'mapping',
      construct: function (data) {
        return { 'Fn::Base64': data };
      },
    }),
  ]);
  const input = yaml.load(inputStr, { schema: CF_SCHEMA });
  // now that we have our AWS yaml copied and formatted into an object, lets pluck what we need to match up with the swagger.yaml format
  const rawResources = input.Resources;
  let guts = [];
  // if an object does not contain a properties.path object then we need to remove it as a possible api to map for swagger
  for (let i in rawResources) {
    if (rawResources[i].Properties.Events) {
      for (let key in rawResources[i].Properties.Events) {
        // console.log(i, rawResources[i]);
        if (rawResources[i].Properties.Events[key].Properties.Path) {
          let tempResource = rawResources[i].Properties.Events[key].Properties;
          tempResource.Name = key;
          guts.push(tempResource);
        }
      }
    }
  } // console.log(guts);
  const defaultResponses = {
    '200': {
      description: 'successful operation',
    },
    '400': {
      description: 'Invalid ID supplied',
    },
  };
  const formattedGuts = guts.map(function (x) {
    if (x.RequestParameters) {
      if (
        Object.keys(x.RequestParameters[0])[0].includes('path') &&
        x.RequestParameters.length > 1
      ) {
        return {
          [x.Path]: {
            [x.Method]: {
              tags: [x.RestApiId.Ref],
              summary: x.Name,
              parameters: [
                {
                  name: Object.keys(x.RequestParameters[0])[0].split('method.request.path.')[1],
                  in: 'path',
                  type: 'string',
                  required: Object.values(x.RequestParameters[0])[0].Required,
                },
                {
                  name: Object.keys(x.RequestParameters[1])[0].split('method.request.path.')[1],
                  in: 'path',
                  type: 'string',
                  required: Object.values(x.RequestParameters[1])[0].Required,
                },
              ],
              responses: defaultResponses,
            },
          },
        };
      } else if (Object.keys(x.RequestParameters[0])[0].includes('path')) {
        return {
          [x.Path]: {
            [x.Method]: {
              tags: [x.RestApiId.Ref],
              summary: x.Name,
              parameters: [
                {
                  name: Object.keys(x.RequestParameters[0])[0].split('method.request.path.')[1],
                  in: 'path',
                  type: 'string',
                  required: Object.values(x.RequestParameters[0])[0].Required,
                },
              ],
              responses: defaultResponses,
            },
          },
        };
      } else if (Object.keys(x.RequestParameters[0])[0].includes('querystring')) {
        return {
          [x.Path]: {
            [x.Method]: {
              tags: [x.RestApiId.Ref],
              summary: x.Name,
              parameters: [
                {
                  name: Object.keys(x.RequestParameters[0])[0].split(
                    'method.request.querystring.'
                  )[1],
                  in: 'query',
                  type: 'string',
                  required: Object.values(x.RequestParameters[0])[0].Required,
                },
              ],
              responses: defaultResponses,
            },
          },
        };
      }
    }
    return {
      [x.Path]: {
        [x.Method]: {
          tags: [x.RestApiId.Ref],
          summary: x.Name,
          responses: defaultResponses,
        },
      },
    };
  });
  const swaggerYaml = yaml.dump(
    {
      swagger: '2.0',
      info: {
        description: '',
        version: '1.0.0',
        title: '',
      },
      paths: Object.assign({}, ...formattedGuts),
    },
    { noRefs: true }
  ); // need to keep noRefs as true, otherwise you will see "*ref_0" instead of the response obj
  //   console.log(swaggerYaml);
  fs.writeFile('../swagger.yaml', swaggerYaml, 'utf8', function (err) {
    if (err) return console.log(err);
  });
} catch (e) {
  console.log(e);
  console.log('error above');
}
Related