Skip to content

Commit

Permalink
moved output related code
Browse files Browse the repository at this point in the history
  • Loading branch information
mkyc committed Nov 25, 2020
1 parent 27ad702 commit 9acd991
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 109 deletions.
20 changes: 19 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package main

import terra "github.com/mkyc/go-terraform"
import (
"fmt"
terra "github.com/mkyc/go-terraform"
)

func main() {
opts, err := terra.WithDefaultRetryableErrors(&terra.Options{
Expand Down Expand Up @@ -47,6 +50,21 @@ func main() {
}
println(s)

println("===============")
println("===============")
println("=== output ====")
println("===============")
println("===============")

m, err := terra.OutputAll(opts)
if err != nil {
panic(err)
}
println(len(m))
for k, v := range m {
fmt.Printf("%s : %v\n", k, v)
}

println("===============")
println("===============")
println("=== destroy ===")
Expand Down
39 changes: 39 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package terra

import "fmt"

type ErrWithCmdOutput struct {
Underlying error
Output *output
}

func (e *ErrWithCmdOutput) Error() string {
return fmt.Sprintf("error while running command: %v; %s", e.Underlying, e.Output.Stderr())
}

// MaxRetriesExceeded is an error that occurs when the maximum amount of retries is exceeded.
type MaxRetriesExceeded struct {
Description string
MaxRetries int
}

func (err MaxRetriesExceeded) Error() string {
return fmt.Sprintf("'%s' unsuccessful after %d retries", err.Description, err.MaxRetries)
}

// FatalError is a marker interface for errors that should not be retried.
type FatalError struct {
Underlying error
}

func (err FatalError) Error() string {
return fmt.Sprintf("FatalError{Underlying: %v}", err.Underlying)
}

// OutputKeyNotFound occurs when terraform output does not contain a value for the key
// specified in the function call
type OutputKeyNotFound string

func (err OutputKeyNotFound) Error() string {
return fmt.Sprintf("output doesn't contain a value for the key %q", string(err))
}
1 change: 1 addition & 0 deletions format.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var terraformCommandWithStateFileSupport = []string{
"taint",
"untaint",
"import",
"output",
}

// FormatArgs converts the inputs to a format palatable to terraform. This includes converting the given vars to the
Expand Down
92 changes: 92 additions & 0 deletions out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package terra

import (
"strings"
"sync"
)

// output contains the output after running a command.
type output struct {
stdout *outputStream
stderr *outputStream
// merged contains stdout and stderr merged into one stream.
merged *merged
}

func newOutput() *output {
m := new(merged)
return &output{
merged: m,
stdout: &outputStream{
merged: m,
},
stderr: &outputStream{
merged: m,
},
}
}

func (o *output) Stdout() string {
if o == nil {
return ""
}

return o.stdout.String()
}

func (o *output) Stderr() string {
if o == nil {
return ""
}

return o.stderr.String()
}

func (o *output) Combined() string {
if o == nil {
return ""
}

return o.merged.String()
}

type outputStream struct {
Lines []string
*merged
}

func (st *outputStream) WriteString(s string) (n int, err error) {
st.Lines = append(st.Lines, string(s))
return st.merged.WriteString(s)
}

func (st *outputStream) String() string {
if st == nil {
return ""
}

return strings.Join(st.Lines, "\n")
}

type merged struct {
// ensure that there are no parallel writes
sync.Mutex
Lines []string
}

func (m *merged) String() string {
if m == nil {
return ""
}

return strings.Join(m.Lines, "\n")
}

func (m *merged) WriteString(s string) (n int, err error) {
m.Lock()
defer m.Unlock()

m.Lines = append(m.Lines, string(s))

return len(s), nil
}
107 changes: 28 additions & 79 deletions output.go
Original file line number Diff line number Diff line change
@@ -1,92 +1,41 @@
package terra

import (
"strings"
"sync"
)
import "encoding/json"

// output contains the output after runnig a command.
type output struct {
stdout *outputStream
stderr *outputStream
// merged contains stdout and stderr merged into one stream.
merged *merged
// OutputAll calls terraform output returns all values as a map.
// If there is error fetching the output, fails the test
func OutputAll(options *Options) (map[string]interface{}, error) {
return OutputForKeysE(options, nil)
}

func newOutput() *output {
m := new(merged)
return &output{
merged: m,
stdout: &outputStream{
merged: m,
},
stderr: &outputStream{
merged: m,
},
// OutputForKeysE calls terraform output for the given key list and returns values as a map.
// The returned values are of type interface{} and need to be type casted as necessary. Refer to output_test.go
func OutputForKeysE(options *Options, keys []string) (map[string]interface{}, error) {
out, err := RunTerraformCommandAndGetStdoutE(options, FormatArgs(options, "output", "-no-color", "-json")...)
if err != nil {
return nil, err
}
}

func (o *output) Stdout() string {
if o == nil {
return ""
}

return o.stdout.String()
}

func (o *output) Stderr() string {
if o == nil {
return ""
}

return o.stderr.String()
}

func (o *output) Combined() string {
if o == nil {
return ""
outputMap := map[string]map[string]interface{}{}
if err := json.Unmarshal([]byte(out), &outputMap); err != nil {
return nil, err
}

return o.merged.String()
}

type outputStream struct {
Lines []string
*merged
}

func (st *outputStream) WriteString(s string) (n int, err error) {
st.Lines = append(st.Lines, string(s))
return st.merged.WriteString(s)
}

func (st *outputStream) String() string {
if st == nil {
return ""
if keys == nil {
outputKeys := make([]string, 0, len(outputMap))
for k := range outputMap {
outputKeys = append(outputKeys, k)
}
keys = outputKeys
}

return strings.Join(st.Lines, "\n")
}

type merged struct {
// ensure that there are no parallel writes
sync.Mutex
Lines []string
}

func (m *merged) String() string {
if m == nil {
return ""
resultMap := make(map[string]interface{})
for _, key := range keys {
value, containsValue := outputMap[key]["value"]
if !containsValue {
return nil, OutputKeyNotFound(key)
}
resultMap[key] = value
}

return strings.Join(m.Lines, "\n")
}

func (m *merged) WriteString(s string) (n int, err error) {
m.Lock()
defer m.Unlock()

m.Lines = append(m.Lines, string(s))

return len(s), nil
return resultMap, nil
}
49 changes: 24 additions & 25 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ func RunTerraformCommandE(additionalOptions *Options, additionalArgs ...string)
})
}

// RunTerraformCommandAndGetStdoutE runs terraform with the given arguments and options and returns solely its stdout
// (but not stderr).
func RunTerraformCommandAndGetStdoutE(additionalOptions *Options, additionalArgs ...string) (string, error) {
options, args := GetCommonOptions(additionalOptions, additionalArgs...)

cmd := generateCommand(options, args...)
description := fmt.Sprintf("%s %v", options.TerraformBinary, args)
return DoWithRetryableErrorsE(description, options.RetryableTerraformErrors, options.MaxRetries, options.TimeBetweenRetries, func() (string, error) {
return RunCommandAndGetStdOutE(cmd)
})
}

func generateCommand(options *Options, args ...string) Command {
cmd := Command{
Command: options.TerraformBinary,
Expand All @@ -59,13 +71,19 @@ func RunCommandAndGetOutputE(command Command) (string, error) {
return output.Combined(), nil
}

type ErrWithCmdOutput struct {
Underlying error
Output *output
}
// RunCommandAndGetStdOutE runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout
// and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging easier.
// Any returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error.
func RunCommandAndGetStdOutE(command Command) (string, error) {
output, err := runCommand(command)
if err != nil {
if output != nil {
return output.Stdout(), &ErrWithCmdOutput{err, output}
}
return "", &ErrWithCmdOutput{err, output}
}

func (e *ErrWithCmdOutput) Error() string {
return fmt.Sprintf("error while running command: %v; %s", e.Underlying, e.Output.Stderr())
return output.Stdout(), nil
}

// runCommand runs a shell command and stores each line from stdout and stderr in Output. Depending on the logger, the
Expand Down Expand Up @@ -240,22 +258,3 @@ func DoWithRetryInterfaceE(actionDescription string, maxRetries int, sleepBetwee

return output, MaxRetriesExceeded{Description: actionDescription, MaxRetries: maxRetries}
}

// MaxRetriesExceeded is an error that occurs when the maximum amount of retries is exceeded.
type MaxRetriesExceeded struct {
Description string
MaxRetries int
}

func (err MaxRetriesExceeded) Error() string {
return fmt.Sprintf("'%s' unsuccessful after %d retries", err.Description, err.MaxRetries)
}

// FatalError is a marker interface for errors that should not be retried.
type FatalError struct {
Underlying error
}

func (err FatalError) Error() string {
return fmt.Sprintf("FatalError{Underlying: %v}", err.Underlying)
}
7 changes: 3 additions & 4 deletions tests/main.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
resource null_resource date {
provisioner local-exec {
command = "date"
}
resource local_file foo {
content = "foo!"
filename = "${path.module}/foo.bar"
}
3 changes: 3 additions & 0 deletions tests/output.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output foo {
value = local_file.foo.content
}

0 comments on commit 9acd991

Please sign in to comment.