Skip to content

Commit

Permalink
Fix issue #20 "unexpected end of JSON input" error (#64)
Browse files Browse the repository at this point in the history
* correct input reading logic

This addresses #20 and
ensures input is read from the plan file -- and not via STDIN -- if a
plan file argument is provided.

This also seeks to improve some of the error messaging to be a bit more
clear.

* demo issue 20 fix via GH Actions

Ideally, tf-summarize would feature a suite of automated tests verifying
its functionality. In absenece of that, this demos the issue #20 fix via
GH Actions.

* use consistent TF version when invoking TF

- ensure the generation of the `example` directory data is done in a
  consistent, reproducible fashion
- ensure GH Actions uses the same version of TF expected by the `example` directory
- add plan and plan JSON files to source control, for testing purposes

* add automated test verifying STDIN vs file-provided input

This adds a basic automated test verifying the validity of the issue #20
fix.

* remove `demo` job from Build GH Actions workflow

Per code review request, @dineshba would prefer this be kept in a
separate file.
  • Loading branch information
mdb committed Jan 27, 2024
1 parent 908b8f0 commit 29d45b2
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 78 deletions.
39 changes: 19 additions & 20 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,33 @@ name: Build

on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4

- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: -exclude=G204 ./...
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: -exclude=G204 ./...

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.21"

- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

- name: Test
run: go test -v ./...
- name: Test
run: go test -v ./...

- name: Build
run: go build
- name: Build
run: go build
97 changes: 51 additions & 46 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,54 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_wrapper: false

- name: Install terraform-plan-summary
run: |
REPO="dineshba/tf-summarize"
curl -LO https://github.com/$REPO/releases/latest/download/tf-summarize_linux_amd64.zip
tmpDir=$(mktemp -d -t tmp.XXXXXXXXXX)
mv tf-summarize_linux_amd64.zip $tmpDir
cd $tmpDir
unzip tf-summarize_linux_amd64.zip
chmod +x tf-summarize
echo $PWD >> $GITHUB_PATH
- name: Print tf-summarize version and help
run: |
tf-summarize -v
tf-summarize -h
- name: Terraform Init
run: terraform init
working-directory: ./example

- name: Terraform Plan
run: terraform plan -out=tfplan -refresh=false # -refresh=false is only for demo workflow
working-directory: ./example

- name: summary in table format
run: terraform show -json tfplan | tf-summarize
working-directory: ./example

- name: summary in tree format
run: terraform show -json tfplan | tf-summarize -tree
working-directory: ./example

- name: summary in separate tree format
run: terraform show -json tfplan | tf-summarize -separate-tree
working-directory: ./example

- name: summary in draw visual tree format
run: terraform show -json tfplan | tf-summarize -tree -draw
working-directory: ./example
- uses: actions/checkout@v3

- name: Set Terraform version
id: set-terraform-version
run: echo "terraform-version=$(cat example/.terraform-version)" >> $GITHUB_OUTPUT

- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_wrapper: false
terraform_version: ${{ steps.set-terraform-version.outputs.terraform-version }}

- name: Install terraform-plan-summary
run: |
REPO="dineshba/tf-summarize"
curl -LO https://github.com/$REPO/releases/latest/download/tf-summarize_linux_amd64.zip
tmpDir=$(mktemp -d -t tmp.XXXXXXXXXX)
mv tf-summarize_linux_amd64.zip $tmpDir
cd $tmpDir
unzip tf-summarize_linux_amd64.zip
chmod +x tf-summarize
echo $PWD >> $GITHUB_PATH
- name: Print tf-summarize version and help
run: |
tf-summarize -v
tf-summarize -h
- name: Terraform Init
run: terraform init
working-directory: ./example

- name: Terraform Plan
run: terraform plan -out=tfplan -refresh=false # -refresh=false is only for demo workflow
working-directory: ./example

- name: summary in table format
run: terraform show -json tfplan | tf-summarize
working-directory: ./example

- name: summary in tree format
run: terraform show -json tfplan | tf-summarize -tree
working-directory: ./example

- name: summary in separate tree format
run: terraform show -json tfplan | tf-summarize -separate-tree
working-directory: ./example

- name: summary in draw visual tree format
run: terraform show -json tfplan | tf-summarize -tree -draw
working-directory: ./example
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ tf-summarize
**/.envrc
**/.terraform
*.swp

example/tfplan
22 changes: 21 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
TERRAFORM_VERSION:=$(shell cat example/.terraform-version)

.PHONY: help
help: ## prints help (only for tasks with comment)
@grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand All @@ -17,4 +19,22 @@ test: lint
i: install ## build and install to /usr/local/bin/

lint:
golangci-lint run --timeout 10m -v
golangci-lint run --timeout 10m -v

define generate-example
docker run \
--interactive \
--tty \
--volume $(shell pwd):/src \
--workdir /src/example \
--entrypoint /bin/sh \
hashicorp/terraform:$(1) \
-c \
"terraform init && \
terraform plan -out tfplan && \
terraform show -json tfplan > tfplan.json"
endef

example:
$(call generate-example,$(TERRAFORM_VERSION))
.PHONY: example
1 change: 1 addition & 0 deletions example/.terraform-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.1.4
1 change: 1 addition & 0 deletions example/tfplan.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func main() {
err := validateFlags(*tree, *separateTree, *drawable, *md, args)
logIfErrorAndExit("invalid input flags: %s\n", err, flag.Usage)

newReader, err := reader.CreateReader(os.Stdin, args)
newReader, err := reader.CreateReader(args)
logIfErrorAndExit("error creating input reader: %s\n", err, flag.Usage)

input, err := newReader.Read()
Expand Down
95 changes: 95 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package main

import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"testing"
)

const (
testVersion string = "test"
testExecutable string = "tf-summarize-test"
)

func TestMain(m *testing.M) {
// compile a 'tf-summarize' for use in running tests
exe := exec.Command("go", "build", "-ldflags", fmt.Sprintf("-X main.version=%s", testVersion), "-o", testExecutable)
err := exe.Run()
if err != nil {
os.Exit(1)
}

m.Run()

// delete the compiled tf-summarize
err = os.Remove(testExecutable)
if err != nil {
log.Fatal(err)
}
}

func TestVersionArg(t *testing.T) {
args := []string{
"-v",
}

for _, arg := range args {
t.Run(fmt.Sprintf("when tf-summarize is passed '%s'", arg), func(t *testing.T) {
output, err := exec.Command(fmt.Sprintf("./%s", testExecutable), arg).CombinedOutput()
if err != nil {
t.Errorf("expected '%s' not to cause error; got '%v'", arg, err)
}

if !strings.Contains(string(output), testVersion) {
t.Errorf("expected '%s' to output version '%s'; got '%s'", arg, testVersion, output)
}
})
}
}

func TestTFSummarize(t *testing.T) {
tests := []struct {
command string
expectedError error
expectedOutput string
}{{
command: fmt.Sprintf("./%s -md example/tfplan.json", testExecutable),
expectedOutput: "basic.txt",
}, {
command: fmt.Sprintf("cat example/tfplan.json | ./%s -md", testExecutable),
expectedOutput: "basic.txt",
}}

for _, test := range tests {
t.Run(fmt.Sprintf("when tf-summarize is passed '%q'", test.command), func(t *testing.T) {
output, err := exec.Command("/bin/sh", "-c", test.command).CombinedOutput()
if err != nil && test.expectedError == nil {
t.Errorf("expected '%s' not to error; got '%v'", test.command, err)
}

b, err := os.ReadFile(fmt.Sprintf("testdata/%s", test.expectedOutput))
if err != nil {
t.Errorf("error reading file '%s': '%v'", test.expectedOutput, err)
}

expected := string(b)

if test.expectedError != nil && err == nil {
t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), err)
}

if test.expectedError != nil && err != nil && test.expectedError.Error() != err.Error() {
t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), err.Error())
}

if string(output) != expected {
t.Logf("expected output: \n%s", expected)
t.Logf("got output: \n%s", output)
t.Errorf("received unexpected output from '%s'", test.command)
}
})
}
}
24 changes: 14 additions & 10 deletions reader/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package reader

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"
)

type Reader interface {
Expand All @@ -22,19 +23,22 @@ func readFile(f io.Reader) ([]byte, error) {
input = append(input, line...)
}
if err != io.EOF {
return nil, fmt.Errorf("error reading file: %s", err.Error())
return nil, fmt.Errorf("error reading input: %s", err.Error())
}
if len(input) == 0 {
return nil, errors.New("no input data; expected input via a non-empty file or via STDIN")
}
return input, nil
}

func CreateReader(stdin *os.File, args []string) (Reader, error) {
stat, _ := stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
return NewStdinReader(), nil
func CreateReader(args []string) (Reader, error) {
if len(args) > 1 {
return nil, fmt.Errorf("expected input via a single filename argument or via STDIN; received multiple arguments: %s", strings.Join(args, ", "))
}
if len(args) < 1 {
return nil, fmt.Errorf("should have either stdin input through pipe or first argument should be file")

if len(args) == 1 {
return NewFileReader(args[0]), nil
}
fileName := args[0]
return NewFileReader(fileName), nil

return NewStdinReader(), nil
}
9 changes: 9 additions & 0 deletions testdata/basic.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
| CHANGE | RESOURCE |
|--------|------------------------------------------------------------------------|
| add | `github_repository.terraform_plan_summary` |
| | `module.github["demo-repository"].github_branch.development` |
| | `module.github["demo-repository"].github_branch.main` |
| | `module.github["demo-repository"].github_repository.repository` |
| | `module.github["terraform-plan-summary"].github_branch.development` |
| | `module.github["terraform-plan-summary"].github_branch.main` |
| | `module.github["terraform-plan-summary"].github_repository.repository` |

0 comments on commit 29d45b2

Please sign in to comment.