Repository: lifeomic/terraform-plan-parser
Branch: master
Commit: c4e284355312
Files: 51
Total size: 82.6 KB
Directory structure:
gitextract_m0mzu67e/
├── .babelrc
├── .github/
│ └── workflows/
│ └── code-scanning-2022-06-29.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── index.d.ts
├── package.json
├── src/
│ ├── cli.ts
│ ├── index.ts
│ └── util/
│ └── endsWith.ts
├── test/
│ └── unit/
│ ├── data/
│ │ ├── 00-terraform-plan.expected.json
│ │ ├── 00-terraform-plan.stdout.txt
│ │ ├── 01-terraform-plan.expected.json
│ │ ├── 01-terraform-plan.stdout.txt
│ │ ├── 02-terraform-plan.expected.json
│ │ ├── 02-terraform-plan.stdout.txt
│ │ ├── 03-terraform-plan.expected.json
│ │ ├── 03-terraform-plan.stdout.txt
│ │ ├── 04-no-magic-start.expected.json
│ │ ├── 04-no-magic-start.stdout.txt
│ │ ├── 05-no-magic-end.expected.json
│ │ ├── 05-no-magic-end.stdout.txt
│ │ ├── 06-attribute-value-unexpected-delimiter.expected.json
│ │ ├── 06-attribute-value-unexpected-delimiter.stdout.txt
│ │ ├── 07-invalid-action-line.expected.json
│ │ ├── 07-invalid-action-line.stdout.txt
│ │ ├── 08-no-attribute-name.expected.json
│ │ ├── 08-no-attribute-name.stdout.txt
│ │ ├── 09-terraform-plan-windows-line-end.expected.json
│ │ ├── 09-terraform-plan-windows-line-end.stdout.txt
│ │ ├── 10-issue-4.expected.json
│ │ ├── 10-issue-4.stdout.txt
│ │ ├── 11-tainted-resource.expected.json
│ │ ├── 11-tainted-resource.stdout.txt
│ │ ├── 12-modules.expected.json
│ │ ├── 12-modules.stdout.txt
│ │ ├── 13-no-changes.expected.json
│ │ ├── 13-no-changes.stdout.txt
│ │ ├── 14-ignore-unchanged-attributes.expected.json
│ │ └── 14-ignore-unchanged-attributes.stdout.txt
│ └── terraform-plan-parser.test.ts
├── tools/
│ ├── bin/
│ │ ├── postbuild
│ │ └── publish
│ └── build-util.js
├── tsconfig-src-cjs.json
├── tsconfig-src-es6.json
├── tsconfig-src-esnext.json
├── tsconfig-test.json
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"env": {
"test": {
"plugins": [ "istanbul" ],
"sourceMaps": "inline"
}
}
}
================================================
FILE: .github/workflows/code-scanning-2022-06-29.yml
================================================
# This workflow is inherited from our internal .github repo at https://github.com/lifeomic/.github/blob/master/workflow-templates/code-scanning-2022-06-29.yml
# Setting up this workflow on the repository will perform a static scan for security issues using GitHub Code Scanning.
# Any findings for a repository can be found under the `Security` tab -> `Code Scanning Alerts`
name: "CodeQL"
on:
push:
branches:
- main
- master
paths-ignore:
- test
- tests
- '**/test'
- '**/tests'
- '**/*.test.js'
- '**/*.test.ts'
pull_request:
branches:
- main
- master
paths-ignore:
- test
- tests
- '**/test'
- '**/tests'
- '**/*.test.js'
- '**/*.test.ts'
jobs:
analyze:
if: ${{ !contains(github.head_ref, 'dependabot') }}
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
config-file: lifeomic/.github/config-files/codeql-config.yml@master # uses our config file from the lifeomic/.github repo
queries: +security-extended # This will run all queries at https://github.com/github/codeql/:language/ql/src/codeql-suites/:language-security-extended.qls
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, it should be removed and replaced with custom build steps.
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
================================================
FILE: .gitignore
================================================
/.git/
node_modules/
temp/
work/
dist/
*.log
*.bak
*.bak.*
.npmrc
.DS_STORE
dump.rdb
.nyc_output/
.vscode/
.nyc_output/
/package-lock.json
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "8"
- "10"
script:
- yarn build
after_success: yarn coverage
notifications:
email:
on_success: never
on_failure: always
before_deploy: cd work/dist
deploy:
skip_cleanup: true
provider: npm
email: $NPM_EMAIL
api_key: $NPM_KEY
on:
tags: true
repo: lifeomic/terraform-plan-parser
node: "8"
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright 2018 LifeOmic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Terraform Plan Parser
[](https://greenkeeper.io/) [](https://travis-ci.org/lifeomic/terraform-plan-parser) [](https://coveralls.io/github/lifeomic/terraform-plan-parser?branch=master) [](https://badge.fury.io/js/terraform-plan-parser)
This project provides a CLI and JavaScript API for parsing terraform
plan output.
**IMPORTANT:** This tool does not parse the file produced by the `-out=path`
argument to `terraform plan` which is a binary file. There is not a stable
specification for this binary file format so, at this time, it is safer
to parse the somewhat structured textual output that gets written to `stdout`.
## Why should I use this?
This parser allows the textual log output from `terraform plan` to be converted
to JSON which is more machine readable.
Here are some suggested use cases:
- Send notification when certain types of changes are detected.
For example, email security team if an IAM policy is modified.
- Validate that certain changes are allowed for a given _change management_
request before invoking `terraform apply`.
- Kick-off a special workflow for certain types of changes to the
infrastructure (possibly, before calling `terraform apply`).
If you wish to perform linting or enforcement of best practices then your
better option might be to analyze the source terraform code instead of
only looking at the changes that are described by the `terraform plan`
output.
## Usage
### JavaScript API
**NPM:**
```bash
npm install terraform-plan-parser
```
**Yarn Package Manager:**
```bash
yarn add terraform-plan-parser
```
**IMPORTANT:**
This project requires [Node v8.9.0 (LTS)](https://nodejs.org/en/blog/release/v8.9.0/)
or newer because the source code utilizes language features such as
`async` / `await`. If you are using an unsupported version of Node then you
will see `SyntaxError: Unexpected token function`. It's possible to use
`babel` to transpile the code for older versions of the Node runtime.
The [babel-preset-env](https://github.com/babel/babel/tree/master/packages/babel-preset-env)
is a good package for supporting this.
### Parse string that contains stdout logs from terraform plan
```javascript
const fs = require('fs');
const parser = require('terraform-plan-parser');
const stdout = fs.readFileSync('terraform-plan.stdout', {encoding: 'utf8'});
const result = parser.parseStdout(stdout);
```
### Command Line
**NPM:**
```bash
npm install -g terraform-plan-parser
```
**Yarn Package Manager:**
```bash
yarn add global terraform-plan-parser
```
**Command help:**
```bash
# Get help on using command
parse-terraform-plan --help
```
```
Options:
--help Show help [boolean]
--version Show version number [boolean]
-i, --input Input file (stdin is used if not provided) [string]
-o, --output Output file (stdout is used if not provided) [string]
--pretty Output JSON in pretty format [boolean] [default: false]
```
**Read from stdin and write to stdout**:
```bash
# Pipe output from "terraform plan" to parser which will convert it to JSON
terraform plan | parse-terraform-plan --pretty
```
**Read from file and write to file**:
```bash
# Store "terraform plan" output in file
terraform plan > terraform-plan.stdout
# Read from "terraform plan" output file and write to JSON file
parse-terraform-plan --pretty -i terraform-plan.stdout -o terraform-plan.json
```
## Output Schema
The output is an object with these top-level properties:
- **`errors`:** An array of parsing errors
- **`changedResources`:** An array of changed resources
- **`changedDataSources`:** An array of changed data sources
Each _changed resource_ has the following properties:
- **`action`:** One of `"create"`, `"destroy"`, `"replace"`, `"update"`
- **`type`:** Type of resource (e.g. `"aws_ecs_service"`)
- **`name`:** Resource name (e.g. `"my_service"`)
- **`path`:** Full path to resource as printed in plan output (e.g. `"module.module1.module.module2.aws_ecs_service.my_service"`)
- **`module`:** Fully qualified module name (e.g. `"module1.module2"`) or `undefined` if resource not within module.
- **`changedAttributes`:** An object whose keys are an attribute name and value is an object
- **`newResourceRequired`:** A flag to indicate if a new resource is required (only present if `true`)
- **`tainted`:** A flag to indicate if resource is tainted (only present if `true`)
A _changed attribute_ object has the following properties:
- **`old`:** An object with `type` property and `value` property which
describes the old state of the attribute.
The `type` will be `"computed"` or `"string"`. The `value` will be a string.
- **`new`:** An object with `type` property and `value` property which
describes the new state of the attribute.
The `type` will be `"computed"` or `"string"`. The `value` will be a string.
Each _data source_ has the following properties:
- **`action`:** The action will always be `"read"`
- **`type`:** Type of resource (e.g. `"external"`)
- **`name`:** Data source name (e.g. `"ecr_image_digests"`)
- **`path`:** Full path to data source as printed in plan output (e.g. `"module.module1.module.module2.data.external.ecr_image_digests"`)
- **`module`:** Fully qualified module name (e.g. `"module1.module2"`) or `undefined` if data source not within module.
- **`changedAttributes`:** An object whose keys are an attribute name and value is an object
## Example Output
```json
{
"errors": [],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "sample_app",
"path": "aws_ecs_service.sample_app",
"changedAttributes": {
"task_definition": {
"old": {
"type": "string",
"value": "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186"
},
"new": {
"type": "string",
"value": "${ aws_ecs_task_definition.sample_app.arn }"
}
}
}
}
],
"changedDataSources": [
{
"action": "read",
"type": "external",
"name": "ecr_image_digests",
"path": "data.external.ecr_image_digests",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"program.#": {
"new": {
"type": "string",
"value": "1"
}
},
"program.0": {
"new": {
"type": "string",
"value": "extract-image-digests"
}
},
"result.%": {
"new": {
"type": "computed"
}
}
}
}
]
}
```
================================================
FILE: index.d.ts
================================================
export * from './src/';
================================================
FILE: package.json
================================================
{
"name": "terraform-plan-parser",
"version": "1.6.0",
"description": "This module provides functionality for parsing stdout from \"terraform plan\" and converting it to JSON that can be more easily analyzed.",
"repository": {
"type": "git",
"url": "https://github.com/lifeomic/terraform-plan-parser.git"
},
"author": "Phil Gates-Idem <phil.gates-idem@lifeomic.com>",
"license": "MIT",
"main": "./work/dist/src/",
"module": "./work/dist/es6/src/",
"jsnext:main": "./work/dist/esnext/src/",
"types": "./work/dist/src/index.d.ts",
"bin": {
"parse-terraform-plan": "./work/dist/src/cli.js"
},
"scripts": {
"compile-src-cjs": "tsc --declaration --declarationDir ./work/dist -p tsconfig-src-cjs.json",
"compile-src-es6": "tsc -p tsconfig-src-es6.json",
"compile-src-esnext": "tsc -p tsconfig-src-esnext.json",
"precompile-src": "rm -rf ./work/dist",
"compile-src": "yarn compile-src-cjs && yarn compile-src-es6 && yarn compile-src-esnext",
"transpile-test-js": "BABEL_ENV=test babel work/dist-test --out-dir work/dist-test --source-maps",
"precompile-test": "rm -rf ./work/dist-test",
"compile-test": "tsc -p tsconfig-test.json && yarn transpile-test-js",
"postcompile-test": "cp -r test/unit/data ./work/dist-test/test/unit/",
"lint": "tslint --format codeFrame --project tsconfig.json 'src/**/*.ts' 'test/**/*.ts'",
"pretest": "yarn lint && yarn compile-test",
"test": "BABEL_ENV=test nyc ava 'work/dist-test/test/**/*.test.js'",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"prebuild": "yarn test",
"build": "yarn compile-src && ./tools/bin/postbuild",
"publish-patch": "yarn build && ./tools/bin/publish patch",
"publish-minor": "yarn build && ./tools/bin/publish minor",
"publish-major": "yarn build && ./tools/bin/publish major"
},
"dependencies": {
"strip-ansi": "^5.0.0",
"yargs": "^12.0.1"
},
"devDependencies": {
"@types/node": "^10.0.7",
"ava": "^0.25.0",
"babel-cli": "^6.26.0",
"babel-plugin-istanbul": "^4.1.5",
"chalk": "^2.3.1",
"coveralls": "^3.0.0",
"inquirer": "^6.0.0",
"nyc": "^12.0.1",
"proxyquire": "^2.0.0",
"tslint": "^5.9.1",
"tslint-config-semistandard": "^7.0.0",
"typescript": "^2.7.2"
},
"ava": {
"require": [
"source-map-support/register"
]
},
"nyc": {
"sourceMap": false,
"instrument": false
}
}
================================================
FILE: src/cli.ts
================================================
#!/usr/bin/env node
import * as fs from 'fs';
import { promisify } from 'util';
import * as parser from '.';
const readFileAsync = promisify(fs.readFile);
const writeFileAsync = promisify(fs.writeFile);
const yargs = require('yargs');
async function readFromStdin (): Promise<string> {
const chunks: Array<Buffer> = [];
await new Promise((resolve, reject) => {
const stdin = process.stdin;
stdin.on('data', (chunk) => {
chunks.push(chunk);
});
stdin.on('error', reject);
stdin.on('end', resolve);
});
return Buffer.concat(chunks).toString('utf8');
}
async function readFromFile (file: string): Promise<string> {
return readFileAsync(file, { encoding: 'utf8' });
}
async function run () {
const input = yargs
.option('i', {
alias: 'input',
describe: 'Input file (stdin is used if not provided)',
type: 'string'
})
.option('o', {
alias: 'output',
describe: 'Output file (stdout is used if not provided)',
type: 'string'
})
.option('pretty', {
describe: 'Output JSON in pretty format',
type: 'boolean',
default: false
})
.argv;
if (input.help) {
return yargs.showHelp();
}
const inputData = (input.input)
? await readFromFile(input.input)
: await readFromStdin();
const json = input.pretty
? JSON.stringify(parser.parseStdout(inputData), null, ' ')
: JSON.stringify(parser.parseStdout(inputData));
if (input.output) {
await writeFileAsync(input.output, json, { encoding: 'utf8' });
} else {
process.stdout.write(json);
}
}
run().catch(function (err) {
console.error('Error parsing terraform plan. ' + (err.stack || err.toString()));
process.exitCode = -1;
});
================================================
FILE: src/index.ts
================================================
const stripAnsi = require('strip-ansi');
import endsWith from './util/endsWith';
export enum Action {
CREATE = 'create',
DESTROY = 'destroy',
REPLACE = 'replace',
UPDATE = 'update',
READ = 'read'
}
export interface ChangedAttributesMap {
[key: string]: ChangedAttribute;
}
export interface Changed {
module: string;
action: Action;
type: string;
name: string;
path: string;
changedAttributes: ChangedAttributesMap;
newResourceRequired: boolean;
tainted: boolean;
}
export enum AttributeValueType {
UNKNOWN = 'unknown',
STRING = 'string',
COMPUTED = 'computed'
}
export interface AttributeValue {
type: AttributeValueType;
value: string;
}
export interface ChangedAttribute {
new: AttributeValue;
old: AttributeValue;
forcesNewResource: boolean;
}
export class ParseError {
code: String;
message: String;
}
export interface ParseResult {
errors: Array<ParseError>;
changedResources: Array<Changed>;
changedDataSources: Array<Changed>;
}
const NO_CHANGES_STRING = '\nNo changes. Infrastructure is up-to-date.\n';
const CONTENT_START_STRING = '\nTerraform will perform the following actions:\n';
const CONTENT_END_STRING = '\nPlan:';
const OLD_NEW_SEPARATOR = ' => ';
const ATTRIBUTE_FORCES_NEW_RESOURCE_SUFFIX = ' (forces new resource)';
/**
* Find the starting position within terraform stdout that we should try
* to parse.
*
* @param logOutput terraform log output
* @return the position at which parsing should begin or -1 if not found
*/
function findParseableContentStartPos (logOutput: string): number {
const pos = logOutput.indexOf(CONTENT_START_STRING);
return (pos === -1) ? pos : pos + CONTENT_START_STRING.length;
}
/**
* Find the ending position within terraform stdout that we should try
* to parse.
*
* @param logOutput terraform log output
* @return the position at which parsing should end or -1 if not found
*/
function findParseableContentEndPos (logOutput: string, startPos: number): number {
return logOutput.indexOf(CONTENT_END_STRING, startPos);
}
interface ActionMapping {
[key: string]: Action;
}
const ACTION_MAPPING: ActionMapping = {};
ACTION_MAPPING['+'] = Action.CREATE;
ACTION_MAPPING['-'] = Action.DESTROY;
ACTION_MAPPING['-/+'] = Action.REPLACE;
ACTION_MAPPING['~'] = Action.UPDATE;
ACTION_MAPPING['<='] = Action.READ;
const ACTION_LINE_REGEX = /^(?:((?:.*\.)?module\.[^.]*)\.)?(?:(data)\.)?([^.]+)\.([^ ]+)( \(tainted\))?( \(new resource required\))?$/;
const ATTRIBUTE_LINE_REGEX = /^ {6}[^ ]/;
// Convert something like "module.test1.module.test2" to "test1.test2"
function parseModulePath (rawModuleStr: string) {
return rawModuleStr.split(/\.?module./).slice(1).join('.');
}
/**
* Parse a line that looks similar to a resource or data source.
*
* Example line for resource:
* ```
* -/+ aws_ecs_task_definition.sample_app (new resource required)
* ```
*
* Example line for data source:
* ```
* <= data.external.ecr_image_digests
* ```
* @param line current line within stdout text
* @param action the pre-determined action which was found by looking at start of line
* @param result an object that collects changed data sources, changed resources, and errors
* @return an object that identifies a changed resource or data sources
*/
function parseActionLine (offset: number, line: string, action: Action, result: ParseResult): Changed | null {
// start position is after the action symbol
// For example, we move past "-/+ " (4 characters)
const match = ACTION_LINE_REGEX.exec(line.substring(offset));
if (!match) {
result.errors.push({
code: 'UNABLE_TO_PARSE_CHANGE_LINE',
message: `Unable to parse "${line}" (ignoring)`
});
return null;
}
const [, module, dataSourceStr, type, name, taintedStr, newResourceRequiredStr] = match;
const fullyQualifiedPath = [module, dataSourceStr, type, name].filter(str =>
str && str.length > 0).join('.');
let change;
change = {
action: action,
type: type,
name: name,
path: fullyQualifiedPath,
changedAttributes: {}
} as Changed;
if (module) {
change.module = parseModulePath(module);
}
if (taintedStr === ' (tainted)') {
change.tainted = true;
}
if (dataSourceStr) {
result.changedDataSources.push(change);
} else {
if (newResourceRequiredStr) {
change.newResourceRequired = true;
}
result.changedResources.push(change);
}
return change;
}
/**
* Find the position of next non-space character or -1 if we didn't
* find a non-space character
* @param str a string
* @param fromIndex the starting position
* @return the next position within string that is non-space character or -1 if not found
*/
function findPosOfNextNonSpaceChar (str: string, fromIndex: number): number {
let pos = fromIndex;
const end = str.length;
while (pos < end) {
if (str.charAt(pos) !== ' ') {
return pos;
}
pos++;
}
return -1;
}
/**
* Read ahead in a string until we ecounter the given terminator character or end of string
* @param str a string
* @param fromIndex the starting position
* @param terminatorChar a terminator string of length 1
* @return the substring from `fromIndex` up to (but not including) the terminator character
*/
function readUpToChar (str: string, fromIndex: number, terminatorChar: string): string | null {
let pos = fromIndex;
const end = str.length;
while (pos < end) {
if (str.charAt(pos) === terminatorChar) {
return str.substring(fromIndex, pos);
}
pos++;
}
return null;
}
/**
* [findStringEndDelimiterPos description]
* @param str the string that contains a quoted string that needs to be parsed
* @param fromIndex the position of the first character after the `"` character
* @return the position of the ending `"` or -1 if the string is unterminated
*/
function findStringEndDelimiterPos (str: string, fromIndex: number): number {
let pos = fromIndex;
let escaped = false;
const end = str.length;
while (pos < end) {
if (escaped) {
escaped = false;
} else {
if (str.charAt(pos) === '"') {
return pos;
} else if (str.charAt(pos) === '\\') {
escaped = true;
}
}
pos++;
}
return -1;
}
/**
* This function is used to parse values such as:
* `<computed>`
* `"arn:aws:iam::123123123123:role/SampleApp"`
*
* @param line the line read from terraform stdout content
* @param fromIndex the starting position of a _value_
* @param errors an array that collects errors
* @return an array with two items (first item is result value object and second item is the end position of the value)
*/
function parseValue (line: string, fromIndex: number, errors: Array<ParseError>): [AttributeValue, number] {
const foundDelimiter = line.charAt(fromIndex);
let endPos: number;
let type: AttributeValueType | undefined;
let value;
if (foundDelimiter === '"') {
endPos = findStringEndDelimiterPos(line, fromIndex + 1);
if (endPos === -1) {
endPos = line.length;
value = line.substring(fromIndex, endPos);
errors.push({
code: 'UNTERMINATED_STRING',
message: `Unterminated string on line "${line}"`
});
} else {
type = AttributeValueType.STRING;
value = JSON.parse(line.substring(fromIndex, endPos + 1));
}
} else if (foundDelimiter === '<') {
const contents = readUpToChar(line, fromIndex + 1, '>');
if (contents === null) {
// we did not find the terminator character
value = line.substring(fromIndex);
endPos = line.length;
} else {
if (contents === 'computed') {
type = AttributeValueType.COMPUTED;
} else {
value = line.substring(fromIndex, fromIndex + contents.length + 1);
}
endPos = fromIndex + contents.length + 1;
}
} else {
value = line.substring(fromIndex);
endPos = fromIndex + value.length;
}
const result = {} as AttributeValue;
result.type = type || AttributeValueType.UNKNOWN;
if (value !== undefined) {
result.value = value;
}
return [result, endPos];
}
/**
* Parses a line that we think looks like an attribute because it starts
* with six spaces and then a non-space character.
*
* @param line the line that looks like an attribute change
* @param lastChange the change object for resource or data source that will hold attribute
* @param errors an array that will collect errors
*/
function parseAttributeLine (line: string, lastChange: Changed, errors: Array<ParseError>) {
let startPos = 6;
const nameEndPos = line.indexOf(':', startPos + 1);
if (nameEndPos === -1) {
errors.push({
code: 'UNABLE_TO_PARSE_ATTRIBUTE_NAME',
message: `Attribute name not found on line "${line}" (ignored)`
});
return;
}
let oldObj: AttributeValue | undefined;
let newObj: AttributeValue | undefined;
let forcesNewResource;
const name = line.substring(startPos, nameEndPos);
startPos = findPosOfNextNonSpaceChar(line, nameEndPos + 1);
if (startPos !== -1) {
const [firstObj, firstValueEndPos] = parseValue(line, startPos, errors);
startPos = firstValueEndPos + 1;
if (line.substring(startPos, startPos + OLD_NEW_SEPARATOR.length) === OLD_NEW_SEPARATOR) {
// there is a " => " so we have an old and new value
[newObj] = parseValue(line, startPos + OLD_NEW_SEPARATOR.length, errors);
oldObj = firstObj;
} else {
// there is no " => " so we only have a new value
newObj = firstObj;
}
if (endsWith(line, ATTRIBUTE_FORCES_NEW_RESOURCE_SUFFIX)) {
forcesNewResource = true;
}
}
const result = {} as ChangedAttribute;
if (oldObj) {
result.old = oldObj;
}
if (newObj) {
result.new = newObj;
}
if (result.old && result.old.value === result.new.value) {
return;
}
if (forcesNewResource) {
result.forcesNewResource = true;
}
lastChange.changedAttributes[name] = result;
}
export function parseStdout (logOutput: string): ParseResult {
logOutput = stripAnsi(logOutput).replace(/\r\n/g, '\n');
const result = {} as ParseResult;
result.errors = [];
result.changedResources = [];
result.changedDataSources = [];
let lastChange = null;
if (logOutput.includes(NO_CHANGES_STRING)) {
// no changes to parse...
return result;
}
const startPos = findParseableContentStartPos(logOutput);
if (startPos === -1) {
result.errors.push({
code: 'UNABLE_TO_FIND_STARTING_POSITION_WITHIN_STDOUT',
message: `Did not find magic starting string: ${CONTENT_START_STRING}`
});
return result;
}
const endPos = findParseableContentEndPos(logOutput, startPos);
if (endPos === -1) {
result.errors.push({
code: 'UNABLE_TO_FIND_ENDING_POSITION_WITHIN_STDOUT',
message: `Did not find magic ending string: ${CONTENT_END_STRING}`
});
return result;
}
const changesText = logOutput.substring(startPos, endPos);
const lines = changesText.split('\n');
for (const line of lines) {
if (line.length === 0) {
// blank lines separate each resource / data source.
lastChange = null;
continue;
}
let offset;
let possibleActionSymbol = line.substring(0, 3).trim();
const spacePos = possibleActionSymbol.lastIndexOf(' ');
if (spacePos === -1) {
// action line is something like:
// "-/+ aws_ecs_task_definition.sample_app (new resource required)"
offset = 4;
} else {
// action line is something like:
// "+ aws_iam_role.terraform_demo"
offset = spacePos + 1;
possibleActionSymbol = possibleActionSymbol.substring(0, spacePos);
}
const action = ACTION_MAPPING[possibleActionSymbol];
if (action) {
// line starts with an action symbol so it will be followed by
// something like "data.external.ecr_image_digests"
// or "aws_ecs_task_definition.sample_app (new resource required)"
lastChange = parseActionLine(offset, line, action, result);
} else if (ATTRIBUTE_LINE_REGEX.test(line)) {
if (lastChange) {
parseAttributeLine(line, lastChange, result.errors);
} else {
// This line looks like an attribute but there is no resource
// or data source that will hold it.
result.errors.push({
code: 'ORPHAN_ATTRIBUTE_LINE',
message: `Attribute line "${line}" is not associated with a data source or resource (ignoring)`
});
}
} else {
// We don't recognize what this line is....
result.errors.push({
code: 'UNABLE_TO_PARSE_LINE',
message: `Unable to parse "${line}" (ignoring)`
});
}
}
return result;
}
================================================
FILE: src/util/endsWith.ts
================================================
export default function (str: string, search: string): boolean {
const strLen = str.length;
return str.substring(strLen - search.length, strLen) === search;
}
================================================
FILE: test/unit/data/00-terraform-plan.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "sample_app",
"path": "aws_ecs_service.sample_app",
"changedAttributes": {
"task_definition": {
"old": {
"type": "string",
"value": "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186"
},
"new": {
"type": "string",
"value": "${ aws_ecs_task_definition.sample_app.arn }"
}
}
}
},
{
"action": "replace",
"type": "aws_ecs_task_definition",
"name": "sample_app",
"path": "aws_ecs_task_definition.sample_app",
"changedAttributes": {
"id": {
"old": {
"type": "string",
"value": "sample-app"
},
"new": {
"type": "computed"
},
"forcesNewResource": true
},
"arn": {
"old": {
"type": "string",
"value": "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186"
},
"new": {
"type": "computed"
}
},
"container_definitions": {
"old": {
"type": "string",
"value": "[{\"cpu\":1,\"environment\":[],\"essential\":true,\"image\":\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"sample-app\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"sample-app\"}},\"memory\":256,\"mountPoints\":[],\"name\":\"sample-app\",\"portMappings\":[{\"containerPort\":8443,\"hostPort\":0,\"protocol\":\"tcp\"}],\"volumesFrom\":[]}]"
},
"new": {
"type": "string",
"value": "[\n {\n \"name\": \"sample-app\",\n \"image\": \"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\"sample-app\"] }\",\n \"cpu\": 1,\n \"memory\": 256,\n \"essential\": true,\n \"logConfiguration\": {\n \"logDriver\": \"awslogs\",\n \"options\": {\n \"awslogs-group\": \"${ aws_cloudwatch_log_group.sample_app.name }\",\n \"awslogs-region\": \"${ var.target_aws_region }\",\n \"awslogs-stream-prefix\": \"sample-app\"\n }\n },\n \"portMappings\": [\n {\n \"containerPort\": 8443,\n \"hostPort\": 0\n }\n ]\n }\n]\n"
},
"forcesNewResource": true
},
"network_mode": {
"old": {
"type": "string",
"value": ""
},
"new": {
"type": "computed"
}
},
"revision": {
"old": {
"type": "string",
"value": "186"
},
"new": {
"type": "computed"
}
}
},
"newResourceRequired": true
},
{
"action": "replace",
"type": "null_resource",
"name": "promote_images",
"path": "null_resource.promote_images",
"changedAttributes": {
"id": {
"old": {
"type": "string",
"value": "1236159896537553123"
},
"new": {
"type": "computed"
},
"forcesNewResource": true
},
"triggers.deploy_job_hash": {
"old": {
"type": "string",
"value": "6c37ac7175bdf35e24a2f2755addd238"
},
"new": {
"type": "string",
"value": "1a0bd86fc5831ee66858f2e159efa547"
},
"forcesNewResource": true
}
},
"newResourceRequired": true
}
],
"changedDataSources": [
{
"action": "read",
"type": "external",
"name": "ecr_image_digests",
"path": "data.external.ecr_image_digests",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"program.#": {
"new": {
"type": "string",
"value": "1"
}
},
"program.0": {
"new": {
"type": "string",
"value": "extract-image-digests"
}
},
"result.%": {
"new": {
"type": "computed"
}
}
}
}
]
}
================================================
FILE: test/unit/data/00-terraform-plan.stdout.txt
================================================
[0m[1mRefreshing Terraform state in-memory prior to plan...[0m
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
[0m
[0m[1maws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)[0m
[0m[1maws_iam_role.service_role: Refreshing state... (ID: SampleApp)[0m
[0m[1maws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)[0m
[0m[1maws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)[0m
[0m[1maws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)[0m
[0m[1mnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)[0m
[0m[1maws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)[0m
[0m[1maws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)[0m
[0m[1maws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)[0m
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
[33m~[0m update in-place
[31m-[0m/[32m+[0m destroy and then create replacement
[36m<=[0m read (data resources)
[0m
Terraform will perform the following actions:
[36m [36m<=[0m [36mdata.external.ecr_image_digests
[0m id: <computed>
program.#: "1"
program.0: "extract-image-digests"
result.%: <computed>
[0m
[0m[33m [33m~[0m [33maws_ecs_service.sample_app
[0m task_definition: "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" => "${ aws_ecs_task_definition.sample_app.arn }"
[0m
[0m[33m[31m-[0m/[32m+[0m [33maws_ecs_task_definition.sample_app [31m[1m(new resource required)[0m
[0m id: "sample-app" => <computed> [31m(forces new resource)[0m
arn: "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" => <computed>
container_definitions: "[{\"cpu\":1,\"environment\":[],\"essential\":true,\"image\":\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"sample-app\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"sample-app\"}},\"memory\":256,\"mountPoints\":[],\"name\":\"sample-app\",\"portMappings\":[{\"containerPort\":8443,\"hostPort\":0,\"protocol\":\"tcp\"}],\"volumesFrom\":[]}]" => "[\n {\n \"name\": \"sample-app\",\n \"image\": \"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\"sample-app\"] }\",\n \"cpu\": 1,\n \"memory\": 256,\n \"essential\": true,\n \"logConfiguration\": {\n \"logDriver\": \"awslogs\",\n \"options\": {\n \"awslogs-group\": \"${ aws_cloudwatch_log_group.sample_app.name }\",\n \"awslogs-region\": \"${ var.target_aws_region }\",\n \"awslogs-stream-prefix\": \"sample-app\"\n }\n },\n \"portMappings\": [\n {\n \"containerPort\": 8443,\n \"hostPort\": 0\n }\n ]\n }\n]\n" [31m(forces new resource)[0m
family: "sample-app" => "sample-app"
network_mode: "" => <computed>
revision: "186" => <computed>
task_role_arn: "arn:aws:iam::123123123123:role/SampleApp" => "arn:aws:iam::123123123123:role/SampleApp"
[0m
[0m[33m[31m-[0m/[32m+[0m [33mnull_resource.promote_images [31m[1m(new resource required)[0m
[0m id: "1236159896537553123" => <computed> [31m(forces new resource)[0m
triggers.%: "1" => "1"
triggers.deploy_job_hash: "6c37ac7175bdf35e24a2f2755addd238" => "1a0bd86fc5831ee66858f2e159efa547" [31m(forces new resource)[0m
[0m
[0m
[0m[1mPlan:[0m 2 to add, 1 to change, 2 to destroy.[0m
------------------------------------------------------------------------
This plan was saved to: terraform.plan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.plan"
================================================
FILE: test/unit/data/01-terraform-plan.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "sample_app",
"path": "aws_ecs_service.sample_app",
"changedAttributes": {
"task_definition": {
"old": {
"type": "string",
"value": "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186"
},
"new": {
"type": "string",
"value": "${ aws_ecs_task_definition.sample_app.arn }"
}
}
}
},
{
"action": "replace",
"type": "aws_ecs_task_definition",
"name": "sample_app",
"path": "aws_ecs_task_definition.sample_app",
"changedAttributes": {
"id": {
"old": {
"type": "string",
"value": "sample-app"
},
"new": {
"type": "computed"
},
"forcesNewResource": true
},
"arn": {
"old": {
"type": "string",
"value": "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186"
},
"new": {
"type": "computed"
}
},
"container_definitions": {
"old": {
"type": "string",
"value": "[{\"cpu\":1,\"environment\":[],\"essential\":true,\"image\":\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"sample-app\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"sample-app\"}},\"memory\":256,\"mountPoints\":[],\"name\":\"sample-app\",\"portMappings\":[{\"containerPort\":8443,\"hostPort\":0,\"protocol\":\"tcp\"}],\"volumesFrom\":[]}]"
},
"new": {
"type": "string",
"value": "[\n {\n \"name\": \"sample-app\",\n \"image\": \"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\"sample-app\"] }\",\n \"cpu\": 1,\n \"memory\": 256,\n \"essential\": true,\n \"logConfiguration\": {\n \"logDriver\": \"awslogs\",\n \"options\": {\n \"awslogs-group\": \"${ aws_cloudwatch_log_group.sample_app.name }\",\n \"awslogs-region\": \"${ var.target_aws_region }\",\n \"awslogs-stream-prefix\": \"sample-app\"\n }\n },\n \"portMappings\": [\n {\n \"containerPort\": 8443,\n \"hostPort\": 0\n }\n ]\n }\n]\n"
},
"forcesNewResource": true
},
"network_mode": {
"old": {
"type": "string",
"value": ""
},
"new": {
"type": "computed"
}
},
"revision": {
"old": {
"type": "string",
"value": "186"
},
"new": {
"type": "computed"
}
}
},
"newResourceRequired": true
},
{
"action": "replace",
"type": "null_resource",
"name": "promote_images",
"path": "null_resource.promote_images",
"changedAttributes": {
"id": {
"old": {
"type": "string",
"value": "1236159896537553123"
},
"new": {
"type": "computed"
},
"forcesNewResource": true
},
"triggers.deploy_job_hash": {
"old": {
"type": "string",
"value": "6c37ac7175bdf35e24a2f2755addd238"
},
"new": {
"type": "string",
"value": "1a0bd86fc5831ee66858f2e159efa547"
},
"forcesNewResource": true
}
},
"newResourceRequired": true
}
],
"changedDataSources": [
{
"action": "read",
"type": "external",
"name": "ecr_image_digests",
"path": "data.external.ecr_image_digests",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"program.#": {
"new": {
"type": "string",
"value": "1"
}
},
"program.0": {
"new": {
"type": "string",
"value": "extract-image-digests"
}
},
"result.%": {
"new": {
"type": "computed"
}
}
}
}
]
}
================================================
FILE: test/unit/data/01-terraform-plan.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)
aws_iam_role.service_role: Refreshing state... (ID: SampleApp)
aws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)
aws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)
aws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)
null_resource.promote_images: Refreshing state... (ID: 1236159896537553123)
aws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)
aws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)
aws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
<= read (data resources)
Terraform will perform the following actions:
<= data.external.ecr_image_digests
id: <computed>
program.#: "1"
program.0: "extract-image-digests"
result.%: <computed>
~ aws_ecs_service.sample_app
task_definition: "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" => "${ aws_ecs_task_definition.sample_app.arn }"
-/+ aws_ecs_task_definition.sample_app (new resource required)
id: "sample-app" => <computed> (forces new resource)
arn: "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" => <computed>
container_definitions: "[{\"cpu\":1,\"environment\":[],\"essential\":true,\"image\":\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"sample-app\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"sample-app\"}},\"memory\":256,\"mountPoints\":[],\"name\":\"sample-app\",\"portMappings\":[{\"containerPort\":8443,\"hostPort\":0,\"protocol\":\"tcp\"}],\"volumesFrom\":[]}]" => "[\n {\n \"name\": \"sample-app\",\n \"image\": \"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\"sample-app\"] }\",\n \"cpu\": 1,\n \"memory\": 256,\n \"essential\": true,\n \"logConfiguration\": {\n \"logDriver\": \"awslogs\",\n \"options\": {\n \"awslogs-group\": \"${ aws_cloudwatch_log_group.sample_app.name }\",\n \"awslogs-region\": \"${ var.target_aws_region }\",\n \"awslogs-stream-prefix\": \"sample-app\"\n }\n },\n \"portMappings\": [\n {\n \"containerPort\": 8443,\n \"hostPort\": 0\n }\n ]\n }\n]\n" (forces new resource)
family: "sample-app" => "sample-app"
network_mode: "" => <computed>
revision: "186" => <computed>
task_role_arn: "arn:aws:iam::123123123123:role/SampleApp" => "arn:aws:iam::123123123123:role/SampleApp"
-/+ null_resource.promote_images (new resource required)
id: "1236159896537553123" => <computed> (forces new resource)
triggers.%: "1" => "1"
triggers.deploy_job_hash: "6c37ac7175bdf35e24a2f2755addd238" => "1a0bd86fc5831ee66858f2e159efa547" (forces new resource)
Plan: 2 to add, 1 to change, 2 to destroy.
------------------------------------------------------------------------
This plan was saved to: terraform.plan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.plan"
================================================
FILE: test/unit/data/02-terraform-plan.expected.json
================================================
{
"errors": [
{
"code": "UNABLE_TO_PARSE_LINE",
"message": "Unable to parse \"XYZ strange line will be ignored\" (ignoring)"
},
{
"code": "ORPHAN_ATTRIBUTE_LINE",
"message": "Attribute line \" floating_attribute_at_start: \"myservice\" (forces new resource)\" is not associated with a data source or resource (ignoring)"
},
{
"code": "UNTERMINATED_STRING",
"message": "Unterminated string on line \" unterminated_string: \"arn:aws:ecs:us-east-1:123123123123\""
},
{
"code": "ORPHAN_ATTRIBUTE_LINE",
"message": "Attribute line \" floating_attribute_at_end: \"myservice\" (forces new resource)\" is not associated with a data source or resource (ignoring)"
}
],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "sample_app",
"path": "aws_ecs_service.sample_app",
"changedAttributes": {
"unterminated_string": {
"new": {
"type": "unknown",
"value": "\"arn:aws:ecs:us-east-1:123123123123"
}
},
"unterminated_computed": {
"new": {
"type": "unknown",
"value": "<computed"
}
},
"unrecognized_attribute_value": {
"new": {
"type": "unknown",
"value": "<blah"
}
},
"some_value_that_is_not_a_string": {
"new": {
"type": "unknown",
"value": "1234"
}
}
}
},
{
"action": "replace",
"type": "aws_ecs_task_definition",
"name": "sample_app",
"path": "aws_ecs_task_definition.sample_app",
"changedAttributes": {
"id": {
"new": {
"type": "string",
"value": "myservice"
},
"forcesNewResource": true
}
},
"newResourceRequired": true
}
],
"changedDataSources": [
{
"action": "read",
"type": "external",
"name": "ecr_image_digests",
"path": "data.external.ecr_image_digests",
"changedAttributes": {
"attribute-with-no-value": {}
}
}
]
}
================================================
FILE: test/unit/data/02-terraform-plan.stdout.txt
================================================
The line that starts with "Terraform will perform the following actions:" is
the magic delimiter when parsing content.
Changing anything above the magic "start string" should not cause a problem
Terraform will perform the following actions:
XYZ strange line will be ignored
floating_attribute_at_start: "myservice" (forces new resource)
<= data.external.ecr_image_digests
attribute-with-no-value:
~ aws_ecs_service.sample_app
unterminated_string: "arn:aws:ecs:us-east-1:123123123123
unterminated_computed: <computed
unrecognized_attribute_value: <blah>
some_value_that_is_not_a_string: 1234
-/+ aws_ecs_task_definition.sample_app (new resource required)
id: "myservice" (forces new resource)
floating_attribute_at_end: "myservice" (forces new resource)
Plan: 2 to add, 1 to change, 2 to destroy. <-- The line that starts with "Plan: " is the magic end string
Changing anything below the magic "end string" should not cause a problem
================================================
FILE: test/unit/data/03-terraform-plan.expected.json
================================================
{
"errors": [
{
"code": "UNABLE_TO_PARSE_LINE",
"message": "Unable to parse \"--- aws_ecs_service.this_is_unrecognized\" (ignoring)"
}
],
"changedResources": [
{
"action": "create",
"type": "aws_ecs_service",
"name": "this_is_created",
"path": "aws_ecs_service.this_is_created",
"changedAttributes": {}
},
{
"action": "destroy",
"type": "aws_ecs_service",
"name": "this_is_destroyed",
"path": "aws_ecs_service.this_is_destroyed",
"changedAttributes": {}
},
{
"action": "replace",
"type": "aws_ecs_service",
"name": "this_is_replaced",
"path": "aws_ecs_service.this_is_replaced",
"changedAttributes": {}
},
{
"action": "update",
"type": "aws_ecs_service",
"name": "this_is_updated",
"path": "aws_ecs_service.this_is_updated",
"changedAttributes": {}
}
],
"changedDataSources": [
{
"action": "read",
"type": "external",
"name": "this_is_a_read",
"path": "data.external.this_is_a_read",
"changedAttributes": {}
}
]
}
================================================
FILE: test/unit/data/03-terraform-plan.stdout.txt
================================================
Terraform will perform the following actions:
<= data.external.this_is_a_read
+ aws_ecs_service.this_is_created
- aws_ecs_service.this_is_destroyed
-/+ aws_ecs_service.this_is_replaced
~ aws_ecs_service.this_is_updated
--- aws_ecs_service.this_is_unrecognized
Plan: 2 to add, 1 to change, 2 to destroy.
================================================
FILE: test/unit/data/04-no-magic-start.expected.json
================================================
{
"changedDataSources": [],
"changedResources": [],
"errors": [
{
"code": "UNABLE_TO_FIND_STARTING_POSITION_WITHIN_STDOUT",
"message": "Did not find magic starting string: \nTerraform will perform the following actions:\n"
}
]
}
================================================
FILE: test/unit/data/04-no-magic-start.stdout.txt
================================================
The line that starts with "Terraform will perform the following actions:" is
the magic delimiter when parsing content.
Changing anything above the magic "start string" should not cause a problem
BLAH BLAH BLAH Terraform will perform the following actions:
XYZ strange line will be ignored
floating_attribute_at_start: "myservice" (forces new resource)
<= data.external.ecr_image_digests
attribute-with-no-value:
~ aws_ecs_service.sample_app
unterminated_string: "arn:aws:ecs:us-east-1:123123123123
unterminated_computed: <computed
unrecognized_attribute_value: <blah>
some_value_that_is_not_a_string: 1234
-/+ aws_ecs_task_definition.sample_app (new resource required)
id: "myservice" (forces new resource)
floating_attribute_at_end: "myservice" (forces new resource)
Plan: 2 to add, 1 to change, 2 to destroy. <-- The line that starts with "Plan: " is the magic end string
Changing anything below the magic "end string" should not cause a problem
================================================
FILE: test/unit/data/05-no-magic-end.expected.json
================================================
{
"changedDataSources": [],
"changedResources": [],
"errors": [
{
"code": "UNABLE_TO_FIND_ENDING_POSITION_WITHIN_STDOUT",
"message": "Did not find magic ending string: \nPlan:"
}
]
}
================================================
FILE: test/unit/data/05-no-magic-end.stdout.txt
================================================
The line that starts with "Terraform will perform the following actions:" is
the magic delimiter when parsing content.
Changing anything above the magic "start string" should not cause a problem
Terraform will perform the following actions:
XYZ strange line will be ignored
floating_attribute_at_start: "myservice" (forces new resource)
<= data.external.ecr_image_digests
attribute-with-no-value:
~ aws_ecs_service.sample_app
unterminated_string: "arn:aws:ecs:us-east-1:123123123123
unterminated_computed: <computed
unrecognized_attribute_value: <blah>
some_value_that_is_not_a_string: 1234
-/+ aws_ecs_task_definition.sample_app (new resource required)
id: "myservice" (forces new resource)
floating_attribute_at_end: "myservice" (forces new resource)
BLAH BLAH BLAH Plan: 2 to add, 1 to change, 2 to destroy. <-- The line that starts with "Plan: " is the magic end string
Changing anything below the magic "end string" should not cause a problem
================================================
FILE: test/unit/data/06-attribute-value-unexpected-delimiter.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "sample_app",
"path": "aws_ecs_service.sample_app",
"changedAttributes": {
"task_definition": {
"new": {
"type": "unknown",
"value": "this is a test"
}
}
}
}
],
"changedDataSources": []
}
================================================
FILE: test/unit/data/06-attribute-value-unexpected-delimiter.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)
aws_iam_role.service_role: Refreshing state... (ID: SampleApp)
aws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)
aws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)
aws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)
null_resource.promote_images: Refreshing state... (ID: 1236159896537553123)
aws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)
aws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)
aws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
<= read (data resources)
Terraform will perform the following actions:
~ aws_ecs_service.sample_app
task_definition: this is a test
Plan: 2 to add, 1 to change, 2 to destroy.
------------------------------------------------------------------------
This plan was saved to: terraform.plan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.plan"
================================================
FILE: test/unit/data/07-invalid-action-line.expected.json
================================================
{
"errors": [
{
"code": "UNABLE_TO_PARSE_CHANGE_LINE",
"message": "Unable to parse \" ~ aws_ecs_service\" (ignoring)"
},
{
"code": "ORPHAN_ATTRIBUTE_LINE",
"message": "Attribute line \" task_definition: \"this is a test\"\" is not associated with a data source or resource (ignoring)"
}
],
"changedResources": [],
"changedDataSources": []
}
================================================
FILE: test/unit/data/07-invalid-action-line.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)
aws_iam_role.service_role: Refreshing state... (ID: SampleApp)
aws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)
aws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)
aws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)
null_resource.promote_images: Refreshing state... (ID: 1236159896537553123)
aws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)
aws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)
aws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
<= read (data resources)
Terraform will perform the following actions:
~ aws_ecs_service
task_definition: "this is a test"
Plan: 2 to add, 1 to change, 2 to destroy.
------------------------------------------------------------------------
This plan was saved to: terraform.plan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.plan"
================================================
FILE: test/unit/data/08-no-attribute-name.expected.json
================================================
{
"errors": [
{
"code": "UNABLE_TO_PARSE_ATTRIBUTE_NAME",
"message": "Attribute name not found on line \" task_definition\" (ignored)"
}
],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "blah",
"path": "aws_ecs_service.blah",
"changedAttributes": {}
}
],
"changedDataSources": []
}
================================================
FILE: test/unit/data/08-no-attribute-name.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)
aws_iam_role.service_role: Refreshing state... (ID: SampleApp)
aws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)
aws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)
aws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)
null_resource.promote_images: Refreshing state... (ID: 1236159896537553123)
aws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)
aws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)
aws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
<= read (data resources)
Terraform will perform the following actions:
~ aws_ecs_service.blah
task_definition
Plan: 2 to add, 1 to change, 2 to destroy.
------------------------------------------------------------------------
This plan was saved to: terraform.plan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.plan"
================================================
FILE: test/unit/data/09-terraform-plan-windows-line-end.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "sample_app",
"path": "aws_ecs_service.sample_app",
"changedAttributes": {
"task_definition": {
"old": {
"type": "string",
"value": "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186"
},
"new": {
"type": "string",
"value": "${ aws_ecs_task_definition.sample_app.arn }"
}
}
}
},
{
"action": "replace",
"type": "aws_ecs_task_definition",
"name": "sample_app",
"path": "aws_ecs_task_definition.sample_app",
"changedAttributes": {
"id": {
"old": {
"type": "string",
"value": "sample-app"
},
"new": {
"type": "computed"
},
"forcesNewResource": true
},
"arn": {
"old": {
"type": "string",
"value": "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186"
},
"new": {
"type": "computed"
}
},
"container_definitions": {
"old": {
"type": "string",
"value": "[{\"cpu\":1,\"environment\":[],\"essential\":true,\"image\":\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"sample-app\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"sample-app\"}},\"memory\":256,\"mountPoints\":[],\"name\":\"sample-app\",\"portMappings\":[{\"containerPort\":8443,\"hostPort\":0,\"protocol\":\"tcp\"}],\"volumesFrom\":[]}]"
},
"new": {
"type": "string",
"value": "[\n {\n \"name\": \"sample-app\",\n \"image\": \"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\"sample-app\"] }\",\n \"cpu\": 1,\n \"memory\": 256,\n \"essential\": true,\n \"logConfiguration\": {\n \"logDriver\": \"awslogs\",\n \"options\": {\n \"awslogs-group\": \"${ aws_cloudwatch_log_group.sample_app.name }\",\n \"awslogs-region\": \"${ var.target_aws_region }\",\n \"awslogs-stream-prefix\": \"sample-app\"\n }\n },\n \"portMappings\": [\n {\n \"containerPort\": 8443,\n \"hostPort\": 0\n }\n ]\n }\n]\n"
},
"forcesNewResource": true
},
"network_mode": {
"old": {
"type": "string",
"value": ""
},
"new": {
"type": "computed"
}
},
"revision": {
"old": {
"type": "string",
"value": "186"
},
"new": {
"type": "computed"
}
}
},
"newResourceRequired": true
},
{
"action": "replace",
"type": "null_resource",
"name": "promote_images",
"path": "null_resource.promote_images",
"changedAttributes": {
"id": {
"old": {
"type": "string",
"value": "1236159896537553123"
},
"new": {
"type": "computed"
},
"forcesNewResource": true
},
"triggers.deploy_job_hash": {
"old": {
"type": "string",
"value": "6c37ac7175bdf35e24a2f2755addd238"
},
"new": {
"type": "string",
"value": "1a0bd86fc5831ee66858f2e159efa547"
},
"forcesNewResource": true
}
},
"newResourceRequired": true
}
],
"changedDataSources": [
{
"action": "read",
"type": "external",
"name": "ecr_image_digests",
"path": "data.external.ecr_image_digests",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"program.#": {
"new": {
"type": "string",
"value": "1"
}
},
"program.0": {
"new": {
"type": "string",
"value": "extract-image-digests"
}
},
"result.%": {
"new": {
"type": "computed"
}
}
}
}
]
}
================================================
FILE: test/unit/data/09-terraform-plan-windows-line-end.stdout.txt
================================================
[0m[1mRefreshing Terraform state in-memory prior to plan...[0m
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
[0m
[0m[1maws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)[0m
[0m[1maws_iam_role.service_role: Refreshing state... (ID: SampleApp)[0m
[0m[1maws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)[0m
[0m[1maws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)[0m
[0m[1maws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)[0m
[0m[1mnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)[0m
[0m[1maws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)[0m
[0m[1maws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)[0m
[0m[1maws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)[0m
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
[33m~[0m update in-place
[31m-[0m/[32m+[0m destroy and then create replacement
[36m<=[0m read (data resources)
[0m
Terraform will perform the following actions:
[36m [36m<=[0m [36mdata.external.ecr_image_digests
[0m id: <computed>
program.#: "1"
program.0: "extract-image-digests"
result.%: <computed>
[0m
[0m[33m [33m~[0m [33maws_ecs_service.sample_app
[0m task_definition: "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" => "${ aws_ecs_task_definition.sample_app.arn }"
[0m
[0m[33m[31m-[0m/[32m+[0m [33maws_ecs_task_definition.sample_app [31m[1m(new resource required)[0m
[0m id: "sample-app" => <computed> [31m(forces new resource)[0m
arn: "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" => <computed>
container_definitions: "[{\"cpu\":1,\"environment\":[],\"essential\":true,\"image\":\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"sample-app\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"sample-app\"}},\"memory\":256,\"mountPoints\":[],\"name\":\"sample-app\",\"portMappings\":[{\"containerPort\":8443,\"hostPort\":0,\"protocol\":\"tcp\"}],\"volumesFrom\":[]}]" => "[\n {\n \"name\": \"sample-app\",\n \"image\": \"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\"sample-app\"] }\",\n \"cpu\": 1,\n \"memory\": 256,\n \"essential\": true,\n \"logConfiguration\": {\n \"logDriver\": \"awslogs\",\n \"options\": {\n \"awslogs-group\": \"${ aws_cloudwatch_log_group.sample_app.name }\",\n \"awslogs-region\": \"${ var.target_aws_region }\",\n \"awslogs-stream-prefix\": \"sample-app\"\n }\n },\n \"portMappings\": [\n {\n \"containerPort\": 8443,\n \"hostPort\": 0\n }\n ]\n }\n]\n" [31m(forces new resource)[0m
family: "sample-app" => "sample-app"
network_mode: "" => <computed>
revision: "186" => <computed>
task_role_arn: "arn:aws:iam::123123123123:role/SampleApp" => "arn:aws:iam::123123123123:role/SampleApp"
[0m
[0m[33m[31m-[0m/[32m+[0m [33mnull_resource.promote_images [31m[1m(new resource required)[0m
[0m id: "1236159896537553123" => <computed> [31m(forces new resource)[0m
triggers.%: "1" => "1"
triggers.deploy_job_hash: "6c37ac7175bdf35e24a2f2755addd238" => "1a0bd86fc5831ee66858f2e159efa547" [31m(forces new resource)[0m
[0m
[0m
[0m[1mPlan:[0m 2 to add, 1 to change, 2 to destroy.[0m
------------------------------------------------------------------------
This plan was saved to: terraform.plan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.plan"
================================================
FILE: test/unit/data/10-issue-4.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "create",
"type": "aws_iam_role",
"name": "terraform_demo",
"path": "aws_iam_role.terraform_demo",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"arn": {
"new": {
"type": "computed"
}
},
"assume_role_policy": {
"new": {
"type": "string",
"value": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Action\": \"sts:AssumeRole\",\n \"Principal\": {\n \"Service\": \"ec2.amazonaws.com\"\n },\n \"Effect\": \"Allow\",\n \"Sid\": \"\"\n }\n ]\n}\n"
}
},
"create_date": {
"new": {
"type": "computed"
}
},
"description": {
"new": {
"type": "string",
"value": "test IAM role"
}
},
"force_detach_policies": {
"new": {
"type": "string",
"value": "false"
}
},
"name": {
"new": {
"type": "string",
"value": "terraform_demo"
}
},
"path": {
"new": {
"type": "string",
"value": "/"
}
},
"unique_id": {
"new": {
"type": "computed"
}
}
}
}
],
"changedDataSources": []
}
================================================
FILE: test/unit/data/10-issue-4.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ aws_iam_role.terraform_demo
id: <computed>
arn: <computed>
assume_role_policy: "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Action\": \"sts:AssumeRole\",\n \"Principal\": {\n \"Service\": \"ec2.amazonaws.com\"\n },\n \"Effect\": \"Allow\",\n \"Sid\": \"\"\n }\n ]\n}\n"
create_date: <computed>
description: "test IAM role"
force_detach_policies: "false"
name: "terraform_demo"
path: "/"
unique_id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
This plan was saved to: xxx/iam/terraform.tfstate.d/default/terraform.tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "xxx/iam/terraform.tfstate.d/default/terraform.tfplan"
================================================
FILE: test/unit/data/11-tainted-resource.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "replace",
"type": "aws_ecs_task_definition",
"name": "sample_app",
"path": "aws_ecs_task_definition.sample_app",
"tainted": true,
"changedAttributes": {
"id": {
"new": {
"type": "string",
"value": "myservice"
},
"forcesNewResource": true
}
},
"newResourceRequired": true
}
],
"changedDataSources": []
}
================================================
FILE: test/unit/data/11-tainted-resource.stdout.txt
================================================
Terraform will perform the following actions:
-/+ aws_ecs_task_definition.sample_app (tainted) (new resource required)
id: "myservice" (forces new resource)
Plan: 1 to add, 0 to change, 1 to destroy.
================================================
FILE: test/unit/data/12-modules.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "create",
"type": "null_resource",
"name": "test0",
"path": "null_resource.test0",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"triggers.%": {
"new": {
"type": "string",
"value": "1"
}
},
"triggers.trigger": {
"new": {
"type": "string",
"value": "test0"
}
}
}
},
{
"action": "create",
"module": "test1",
"type": "null_resource",
"name": "test1",
"path": "module.test1.null_resource.test1",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"triggers.%": {
"new": {
"type": "string",
"value": "1"
}
},
"triggers.trigger": {
"new": {
"type": "string",
"value": "test1"
}
}
}
},
{
"action": "create",
"module": "test1.test2",
"type": "null_resource",
"name": "test2",
"path": "module.test1.module.test2.null_resource.test2",
"changedAttributes": {
"id": {
"new": {
"type": "computed"
}
},
"triggers.%": {
"new": {
"type": "string",
"value": "1"
}
},
"triggers.trigger": {
"new": {
"type": "string",
"value": "test2"
}
}
}
}
],
"changedDataSources": []
}
================================================
FILE: test/unit/data/12-modules.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ null_resource.test0
id: <computed>
triggers.%: "1"
triggers.trigger: "test0"
+ module.test1.null_resource.test1
id: <computed>
triggers.%: "1"
triggers.trigger: "test1"
+ module.test1.module.test2.null_resource.test2
id: <computed>
triggers.%: "1"
triggers.trigger: "test2"
Plan: 3 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
================================================
FILE: test/unit/data/13-no-changes.expected.json
================================================
{
"errors": [],
"changedResources": [],
"changedDataSources": []
}
================================================
FILE: test/unit/data/13-no-changes.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
================================================
FILE: test/unit/data/14-ignore-unchanged-attributes.expected.json
================================================
{
"errors": [],
"changedResources": [
{
"action": "update",
"type": "aws_ecs_service",
"name": "sample_app",
"path": "aws_ecs_service.sample_app",
"changedAttributes": {
"setting.changed": {
"old": {
"type": "string",
"value": "A"
},
"new": {
"type": "string",
"value": "B"
}
}
}
}
],
"changedDataSources": []
}
================================================
FILE: test/unit/data/14-ignore-unchanged-attributes.stdout.txt
================================================
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
<= read (data resources)
Terraform will perform the following actions:
~ aws_ecs_service.sample_app
setting.unchanged: "A" => "A"
setting.changed: "A" => "B"
Plan: 0 to add, 1 to change, 0 to destroy.
------------------------------------------------------------------------
This plan was saved to: terraform.plan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.plan"
================================================
FILE: test/unit/terraform-plan-parser.test.ts
================================================
import { promisify } from 'util';
import test from 'ava';
import * as path from 'path';
import * as fs from 'fs';
import { parseStdout, ParseResult } from '../../src';
const readFileAsync = promisify(fs.readFile);
function readExpected (dataFile: string): ParseResult {
const dataObj: any = require(path.join(__dirname,'data', dataFile + '.expected.json'));
return dataObj as ParseResult;
}
async function readActual (dataFile: string): Promise<ParseResult> {
const stdout = await readFileAsync(path.join(__dirname, 'data', dataFile + '.stdout.txt'),
{ encoding: 'utf8' });
return parseStdout(stdout);
}
async function runTest (dataName: string, t: any) {
const actual = await readActual(dataName);
const expected = readExpected(dataName);
t.deepEqual(actual, expected);
}
test('should strip ansi color codes', async (t) => {
return runTest('00-terraform-plan', t);
});
test('should parse terraform output - 01', async (t) => {
return runTest('01-terraform-plan', t);
});
test('should parse terraform output and be fairly lenient - 02', async (t) => {
return runTest('02-terraform-plan', t);
});
test('should parse terraform output and support all types of changes - 03', async (t) => {
return runTest('03-terraform-plan', t);
});
test('should fail gracefully if no magic start string is found', async (t) => {
return runTest('04-no-magic-start', t);
});
test('should fail gracefully if no magic end string is found', async (t) => {
return runTest('05-no-magic-end', t);
});
test('should handle unexpected attribute value that is not delimited', async (t) => {
return runTest('06-attribute-value-unexpected-delimiter', t);
});
test('should ignore invalid resource action line', async (t) => {
return runTest('07-invalid-action-line', t);
});
test('should ignore attribute with missing name', async (t) => {
return runTest('08-no-attribute-name', t);
});
test('should handle plan output with Windows line terminator', async (t) => {
return runTest('09-terraform-plan-windows-line-end', t);
});
test('should handle sample provided in issue #4', async (t) => {
return runTest('10-issue-4', t);
});
test('should handle tainted resources', async (t) => {
return runTest('11-tainted-resource', t);
});
test('should handle modules', async (t) => {
return runTest('12-modules', t);
});
test('should handle no changes', async (t) => {
return runTest('13-no-changes', t);
});
test('should ignore unchanged attributes', async (t) => {
return runTest('14-ignore-unchanged-attributes', t);
});
================================================
FILE: tools/bin/postbuild
================================================
#!/usr/bin/env node
/* eslint-disable security/detect-object-injection */
const {
spawn,
chalk,
writeJsonFile,
readJsonFile,
ROOT_DIR,
DIST_DIR,
ROOT_PACKAGE_JSON_FILE,
DIST_PACKAGE_JSON_FILE
} = require('../build-util');
function rewritePackageJsonPaths (obj, keys) {
if (!obj) {
return;
}
if (!keys) {
keys = Object.keys(obj);
}
for (const key of keys) {
const value = obj[key];
if (typeof value === 'string') {
obj[key] = value.replace('./work/dist/', './');
}
}
}
async function run () {
await spawn('cp', ['-r', 'LICENSE', 'README.md', 'src', `${DIST_DIR}/`], { cwd: ROOT_DIR });
const packageObj = await readJsonFile(ROOT_PACKAGE_JSON_FILE);
// Fix some of the paths
rewritePackageJsonPaths(packageObj.bin);
rewritePackageJsonPaths(packageObj, [
'main',
'module',
'jsnext:main',
'types'
]);
await writeJsonFile(DIST_PACKAGE_JSON_FILE, packageObj);
return packageObj;
}
run().then((builtPackage) => {
console.log(chalk.bold.green(`Built ${builtPackage.name}@${builtPackage.version}`));
}).catch((err) => {
console.error(chalk.bold.red(`Error building. ${err.stack || err.toString()}`));
});
================================================
FILE: tools/bin/publish
================================================
#!/usr/bin/env node
const VERSION_TYPE = process.argv[2] || 'patch';
const inquirer = require('inquirer');
const {
exec,
chalk,
writeJsonFile,
readJsonFile,
ROOT_DIR,
DIST_DIR,
ROOT_PACKAGE_JSON_FILE,
DIST_PACKAGE_JSON_FILE
} = require('../build-util');
// The possible version values are
// [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | from-git]
// Since <newversion> can be almost anything, rather than checking for each
// possibility, just verify that only a restricted number of characters are
// provided
const ALLOWED_VERSION_PATTERN = /^[.0-9A-Za-z-]+$/
async function run() {
const inputs = await inquirer.prompt([
{
type: 'string',
name: 'mfa_token',
message: 'NPM MFA Token?',
}
]);
// Sanity check version strings
if (!ALLOWED_VERSION_PATTERN.test(VERSION_TYPE)) {
throw new Error('Invalid version');
}
await exec(`npm version ${VERSION_TYPE} --no-git-tag-version`, { cwd: DIST_DIR });
await exec(`npm publish --access public --otp="${inputs.mfa_token}"`, { cwd: DIST_DIR });
const rootPackageObj = await readJsonFile(ROOT_PACKAGE_JSON_FILE);
const distPackageObj = await readJsonFile(DIST_PACKAGE_JSON_FILE);
rootPackageObj.version = distPackageObj.version;
const tagName = `v${rootPackageObj.version}`;
await writeJsonFile(ROOT_PACKAGE_JSON_FILE, rootPackageObj);
await exec('git add .', { cwd: ROOT_DIR });
await exec(`git commit -m "Published ${rootPackageObj.version}"`, { cwd: ROOT_DIR });
await exec('git push', { cwd: ROOT_DIR });
await exec(`git tag ${tagName}`, { cwd: ROOT_DIR });
await exec(`git push origin ${tagName}`, { cwd: ROOT_DIR });
return rootPackageObj;
}
run().then((publishedPackage) => {
console.log(chalk.bold.green(`Published ${publishedPackage.name}@${publishedPackage.version}`));
}).catch((err) => {
console.error(chalk.bold.red(`Error publishing. ${err.stack || err.toString()}`));
});
================================================
FILE: tools/build-util.js
================================================
/* eslint-disable security/detect-child-process */
/* eslint-disable security/detect-non-literal-fs-filename */
const path = require('path');
const fs = require('fs');
const util = require('util');
const writeFile = util.promisify(fs.writeFile);
const readFile = util.promisify(fs.readFile);
const spawn = require('child_process').spawn;
exports.ROOT_DIR = path.normalize(path.join(__dirname, '..'));
exports.DIST_DIR = path.join(exports.ROOT_DIR, 'work/dist');
exports.ROOT_PACKAGE_JSON_FILE = path.join(exports.ROOT_DIR, 'package.json');
exports.DIST_PACKAGE_JSON_FILE = path.join(exports.DIST_DIR, 'package.json');
exports.exec = util.promisify(require('child_process').exec);
exports.spawn = async function (command, args, options) {
const child = spawn.apply(this, arguments);
if (!options.stdio) {
options.stdio = 'inherit';
}
return new Promise((resolve, reject) => {
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${command} return non-zero exit code`));
}
});
});
};
exports.chalk = require('chalk');
exports.writeJsonFile = async (file, obj) => {
return writeFile(file, JSON.stringify(obj, null, ' ') + '\n', { encoding: 'utf8' });
};
exports.readJsonFile = async (file) => {
return JSON.parse(await readFile(file, {encoding: 'utf8'}));
};
================================================
FILE: tsconfig-src-cjs.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./work/dist",
"module": "CommonJS"
},
"include": [
"src/**/*"
]
}
================================================
FILE: tsconfig-src-es6.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./work/dist/es6",
"module": "ES6"
},
"include": [
"src/**/*"
]
}
================================================
FILE: tsconfig-src-esnext.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./work/dist/esnext",
"module": "ESNext"
},
"include": [
"src/**/*"
]
}
================================================
FILE: tsconfig-test.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./work/dist-test",
"module": "CommonJS",
"inlineSourceMap": true,
"inlineSources": true
},
"include": [
"test/**/*"
]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"rootDir": ".",
"baseUrl": ".",
"outDir": "./work/dist",
"noUnusedLocals": true,
"allowSyntheticDefaultImports": false,
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"allowJs": false,
"moduleResolution": "node",
"module": "ES6",
"target": "ESNext",
"pretty": true,
"inlineSourceMap": false,
"inlineSources": false,
"paths": {
"~/*": ["./*"]
}
},
"include": [
"src/**/*",
"test/**/*"
]
}
================================================
FILE: tslint.json
================================================
{
"extends": "tslint-config-semistandard"
}
gitextract_m0mzu67e/ ├── .babelrc ├── .github/ │ └── workflows/ │ └── code-scanning-2022-06-29.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.d.ts ├── package.json ├── src/ │ ├── cli.ts │ ├── index.ts │ └── util/ │ └── endsWith.ts ├── test/ │ └── unit/ │ ├── data/ │ │ ├── 00-terraform-plan.expected.json │ │ ├── 00-terraform-plan.stdout.txt │ │ ├── 01-terraform-plan.expected.json │ │ ├── 01-terraform-plan.stdout.txt │ │ ├── 02-terraform-plan.expected.json │ │ ├── 02-terraform-plan.stdout.txt │ │ ├── 03-terraform-plan.expected.json │ │ ├── 03-terraform-plan.stdout.txt │ │ ├── 04-no-magic-start.expected.json │ │ ├── 04-no-magic-start.stdout.txt │ │ ├── 05-no-magic-end.expected.json │ │ ├── 05-no-magic-end.stdout.txt │ │ ├── 06-attribute-value-unexpected-delimiter.expected.json │ │ ├── 06-attribute-value-unexpected-delimiter.stdout.txt │ │ ├── 07-invalid-action-line.expected.json │ │ ├── 07-invalid-action-line.stdout.txt │ │ ├── 08-no-attribute-name.expected.json │ │ ├── 08-no-attribute-name.stdout.txt │ │ ├── 09-terraform-plan-windows-line-end.expected.json │ │ ├── 09-terraform-plan-windows-line-end.stdout.txt │ │ ├── 10-issue-4.expected.json │ │ ├── 10-issue-4.stdout.txt │ │ ├── 11-tainted-resource.expected.json │ │ ├── 11-tainted-resource.stdout.txt │ │ ├── 12-modules.expected.json │ │ ├── 12-modules.stdout.txt │ │ ├── 13-no-changes.expected.json │ │ ├── 13-no-changes.stdout.txt │ │ ├── 14-ignore-unchanged-attributes.expected.json │ │ └── 14-ignore-unchanged-attributes.stdout.txt │ └── terraform-plan-parser.test.ts ├── tools/ │ ├── bin/ │ │ ├── postbuild │ │ └── publish │ └── build-util.js ├── tsconfig-src-cjs.json ├── tsconfig-src-es6.json ├── tsconfig-src-esnext.json ├── tsconfig-test.json ├── tsconfig.json └── tslint.json
SYMBOL INDEX (33 symbols across 3 files)
FILE: src/cli.ts
function readFromStdin (line 12) | async function readFromStdin (): Promise<string> {
function readFromFile (line 27) | async function readFromFile (file: string): Promise<string> {
function run (line 31) | async function run () {
FILE: src/index.ts
type Action (line 4) | enum Action {
type ChangedAttributesMap (line 12) | interface ChangedAttributesMap {
type Changed (line 16) | interface Changed {
type AttributeValueType (line 27) | enum AttributeValueType {
type AttributeValue (line 33) | interface AttributeValue {
type ChangedAttribute (line 38) | interface ChangedAttribute {
class ParseError (line 44) | class ParseError {
type ParseResult (line 49) | interface ParseResult {
constant NO_CHANGES_STRING (line 55) | const NO_CHANGES_STRING = '\nNo changes. Infrastructure is up-to-date.\n';
constant CONTENT_START_STRING (line 56) | const CONTENT_START_STRING = '\nTerraform will perform the following act...
constant CONTENT_END_STRING (line 57) | const CONTENT_END_STRING = '\nPlan:';
constant OLD_NEW_SEPARATOR (line 58) | const OLD_NEW_SEPARATOR = ' => ';
constant ATTRIBUTE_FORCES_NEW_RESOURCE_SUFFIX (line 59) | const ATTRIBUTE_FORCES_NEW_RESOURCE_SUFFIX = ' (forces new resource)';
function findParseableContentStartPos (line 68) | function findParseableContentStartPos (logOutput: string): number {
function findParseableContentEndPos (line 80) | function findParseableContentEndPos (logOutput: string, startPos: number...
type ActionMapping (line 84) | interface ActionMapping {
constant ACTION_MAPPING (line 88) | const ACTION_MAPPING: ActionMapping = {};
constant ACTION_LINE_REGEX (line 96) | const ACTION_LINE_REGEX = /^(?:((?:.*\.)?module\.[^.]*)\.)?(?:(data)\.)?...
constant ATTRIBUTE_LINE_REGEX (line 97) | const ATTRIBUTE_LINE_REGEX = /^ {6}[^ ]/;
function parseModulePath (line 100) | function parseModulePath (rawModuleStr: string) {
function parseActionLine (line 121) | function parseActionLine (offset: number, line: string, action: Action, ...
function findPosOfNextNonSpaceChar (line 174) | function findPosOfNextNonSpaceChar (str: string, fromIndex: number): num...
function readUpToChar (line 193) | function readUpToChar (str: string, fromIndex: number, terminatorChar: s...
function findStringEndDelimiterPos (line 212) | function findStringEndDelimiterPos (str: string, fromIndex: number): num...
function parseValue (line 242) | function parseValue (line: string, fromIndex: number, errors: Array<Pars...
function parseAttributeLine (line 298) | function parseAttributeLine (line: string, lastChange: Changed, errors: ...
function parseStdout (line 354) | function parseStdout (logOutput: string): ParseResult {
FILE: test/unit/terraform-plan-parser.test.ts
function readExpected (line 9) | function readExpected (dataFile: string): ParseResult {
function readActual (line 14) | async function readActual (dataFile: string): Promise<ParseResult> {
function runTest (line 20) | async function runTest (dataName: string, t: any) {
Condensed preview — 51 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (95K chars).
[
{
"path": ".babelrc",
"chars": 101,
"preview": "{\n \"env\": {\n \"test\": {\n \"plugins\": [ \"istanbul\" ],\n \"sourceMaps\": \"inline\"\n }\n }\n}\n"
},
{
"path": ".github/workflows/code-scanning-2022-06-29.yml",
"chars": 1902,
"preview": "# This workflow is inherited from our internal .github repo at https://github.com/lifeomic/.github/blob/master/workflow-"
},
{
"path": ".gitignore",
"chars": 139,
"preview": "/.git/\nnode_modules/\ntemp/\nwork/\ndist/\n*.log\n*.bak\n*.bak.*\n.npmrc\n.DS_STORE\ndump.rdb\n.nyc_output/\n.vscode/\n.nyc_output/\n"
},
{
"path": ".travis.yml",
"chars": 356,
"preview": "language: node_js\nnode_js:\n - \"8\"\n - \"10\"\nscript:\n - yarn build\nafter_success: yarn coverage\n\nnotifications:\n email:"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "The MIT License (MIT)\n\nCopyright 2018 LifeOmic\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 7086,
"preview": "# Terraform Plan Parser\n\n[](https:"
},
{
"path": "index.d.ts",
"chars": 24,
"preview": "export * from './src/';\n"
},
{
"path": "package.json",
"chars": 2445,
"preview": "{\n \"name\": \"terraform-plan-parser\",\n \"version\": \"1.6.0\",\n \"description\": \"This module provides functionality for pars"
},
{
"path": "src/cli.ts",
"chars": 1753,
"preview": "#!/usr/bin/env node\n\nimport * as fs from 'fs';\nimport { promisify } from 'util';\nimport * as parser from '.';\n\nconst rea"
},
{
"path": "src/index.ts",
"chars": 12686,
"preview": "const stripAnsi = require('strip-ansi');\nimport endsWith from './util/endsWith';\n\nexport enum Action {\n CREATE = 'creat"
},
{
"path": "src/util/endsWith.ts",
"chars": 163,
"preview": "export default function (str: string, search: string): boolean {\n const strLen = str.length;\n return str.substring(str"
},
{
"path": "test/unit/data/00-terraform-plan.expected.json",
"chars": 4459,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"update\",\n \"type\": \"aws_ecs_service\",\n \"name\":"
},
{
"path": "test/unit/data/00-terraform-plan.stdout.txt",
"chars": 4451,
"preview": "\u001b[0m\u001b[1mRefreshing Terraform state in-memory prior to plan...\u001b[0m\nThe refreshed state will be used to calculate this pla"
},
{
"path": "test/unit/data/01-terraform-plan.expected.json",
"chars": 4459,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"update\",\n \"type\": \"aws_ecs_service\",\n \"name\":"
},
{
"path": "test/unit/data/01-terraform-plan.stdout.txt",
"chars": 4067,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/data/02-terraform-plan.expected.json",
"chars": 2260,
"preview": "{\n \"errors\": [\n {\n \"code\": \"UNABLE_TO_PARSE_LINE\",\n \"message\": \"Unable to parse \\\"XYZ strange line will be"
},
{
"path": "test/unit/data/02-terraform-plan.stdout.txt",
"chars": 1076,
"preview": "The line that starts with \"Terraform will perform the following actions:\" is\nthe magic delimiter when parsing content.\n\n"
},
{
"path": "test/unit/data/03-terraform-plan.expected.json",
"chars": 1136,
"preview": "{\n \"errors\": [\n {\n \"code\": \"UNABLE_TO_PARSE_LINE\",\n \"message\": \"Unable to parse \\\"--- aws_ecs_service.this"
},
{
"path": "test/unit/data/03-terraform-plan.stdout.txt",
"chars": 318,
"preview": "\nTerraform will perform the following actions:\n\n <= data.external.this_is_a_read\n\n + aws_ecs_service.this_is_created\n\n "
},
{
"path": "test/unit/data/04-no-magic-start.expected.json",
"chars": 257,
"preview": "{\n \"changedDataSources\": [],\n \"changedResources\": [],\n \"errors\": [\n {\n \"code\": \"UNABLE_TO_FIND_STARTING_POSIT"
},
{
"path": "test/unit/data/04-no-magic-start.stdout.txt",
"chars": 1091,
"preview": "The line that starts with \"Terraform will perform the following actions:\" is\nthe magic delimiter when parsing content.\n\n"
},
{
"path": "test/unit/data/05-no-magic-end.expected.json",
"chars": 211,
"preview": "{\n \"changedDataSources\": [],\n \"changedResources\": [],\n \"errors\": [\n {\n \"code\": \"UNABLE_TO_FIND_ENDING_POSITIO"
},
{
"path": "test/unit/data/05-no-magic-end.stdout.txt",
"chars": 1091,
"preview": "The line that starts with \"Terraform will perform the following actions:\" is\nthe magic delimiter when parsing content.\n\n"
},
{
"path": "test/unit/data/06-attribute-value-unexpected-delimiter.expected.json",
"chars": 395,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"update\",\n \"type\": \"aws_ecs_service\",\n \"name\":"
},
{
"path": "test/unit/data/06-attribute-value-unexpected-delimiter.stdout.txt",
"chars": 1661,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/data/07-invalid-action-line.expected.json",
"chars": 403,
"preview": "{\n \"errors\": [\n {\n \"code\": \"UNABLE_TO_PARSE_CHANGE_LINE\",\n \"message\": \"Unable to parse \\\" ~ aws_ecs_servi"
},
{
"path": "test/unit/data/07-invalid-action-line.stdout.txt",
"chars": 1652,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/data/08-no-attribute-name.expected.json",
"chars": 388,
"preview": "{\n \"errors\": [\n {\n \"code\": \"UNABLE_TO_PARSE_ATTRIBUTE_NAME\",\n \"message\": \"Attribute name not found on line"
},
{
"path": "test/unit/data/08-no-attribute-name.stdout.txt",
"chars": 1630,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/data/09-terraform-plan-windows-line-end.expected.json",
"chars": 4459,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"update\",\n \"type\": \"aws_ecs_service\",\n \"name\":"
},
{
"path": "test/unit/data/09-terraform-plan-windows-line-end.stdout.txt",
"chars": 4507,
"preview": "\u001b[0m\u001b[1mRefreshing Terraform state in-memory prior to plan...\u001b[0m\r\nThe refreshed state will be used to calculate this pl"
},
{
"path": "test/unit/data/10-issue-4.expected.json",
"chars": 1505,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"create\",\n \"type\": \"aws_iam_role\",\n \"name\": \"t"
},
{
"path": "test/unit/data/10-issue-4.stdout.txt",
"chars": 1379,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/data/11-tainted-resource.expected.json",
"chars": 488,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"replace\",\n \"type\": \"aws_ecs_task_definition\",\n "
},
{
"path": "test/unit/data/11-tainted-resource.stdout.txt",
"chars": 231,
"preview": "\nTerraform will perform the following actions:\n\n-/+ aws_ecs_task_definition.sample_app (tainted) (new resource required)"
},
{
"path": "test/unit/data/12-modules.expected.json",
"chars": 1691,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"create\",\n \"type\": \"null_resource\",\n \"name\": \""
},
{
"path": "test/unit/data/12-modules.stdout.txt",
"chars": 1120,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/data/13-no-changes.expected.json",
"chars": 73,
"preview": "{\n \"errors\": [],\n \"changedResources\": [],\n \"changedDataSources\": []\n}\n"
},
{
"path": "test/unit/data/13-no-changes.stdout.txt",
"chars": 460,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/data/14-ignore-unchanged-attributes.expected.json",
"chars": 516,
"preview": "{\n \"errors\": [],\n \"changedResources\": [\n {\n \"action\": \"update\",\n \"type\": \"aws_ecs_service\",\n "
},
{
"path": "test/unit/data/14-ignore-unchanged-attributes.stdout.txt",
"chars": 976,
"preview": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will "
},
{
"path": "test/unit/terraform-plan-parser.test.ts",
"chars": 2548,
"preview": "import { promisify } from 'util';\nimport test from 'ava';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport"
},
{
"path": "tools/bin/postbuild",
"chars": 1196,
"preview": "#!/usr/bin/env node\n\n/* eslint-disable security/detect-object-injection */\n\nconst {\n spawn,\n chalk,\n writeJsonFile,\n "
},
{
"path": "tools/bin/publish",
"chars": 1995,
"preview": "#!/usr/bin/env node\n\nconst VERSION_TYPE = process.argv[2] || 'patch';\nconst inquirer = require('inquirer');\n\nconst {\n e"
},
{
"path": "tools/build-util.js",
"chars": 1357,
"preview": "/* eslint-disable security/detect-child-process */\n/* eslint-disable security/detect-non-literal-fs-filename */\n\nconst p"
},
{
"path": "tsconfig-src-cjs.json",
"chars": 152,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"./work/dist\",\n \"module\": \"CommonJS\"\n },\n \"i"
},
{
"path": "tsconfig-src-es6.json",
"chars": 151,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"./work/dist/es6\",\n \"module\": \"ES6\"\n },\n \"in"
},
{
"path": "tsconfig-src-esnext.json",
"chars": 157,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"./work/dist/esnext\",\n \"module\": \"ESNext\"\n },"
},
{
"path": "tsconfig-test.json",
"chars": 214,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"./work/dist-test\",\n \"module\": \"CommonJS\",\n "
},
{
"path": "tsconfig.json",
"chars": 586,
"preview": "{\n \"compilerOptions\": {\n \"rootDir\": \".\",\n \"baseUrl\": \".\",\n \"outDir\": \"./work/dist\",\n \"noUnusedLocals\": true"
},
{
"path": "tslint.json",
"chars": 46,
"preview": "{\n \"extends\": \"tslint-config-semistandard\"\n}\n"
}
]
About this extraction
This page contains the full source code of the lifeomic/terraform-plan-parser GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 51 files (82.6 KB), approximately 23.5k tokens, and a symbol index with 33 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.