Skip to content

Commit

Permalink
feat(*): add custom action support
Browse files Browse the repository at this point in the history
  • Loading branch information
vdice committed Aug 10, 2019
1 parent 0d24b85 commit ce98907
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 0 deletions.
22 changes: 22 additions & 0 deletions cmd/terraform/invoke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"github.com/deislabs/porter-terraform/pkg/terraform"
"github.com/spf13/cobra"
)

func buildInvokeCommand(mixin *terraform.Mixin) *cobra.Command {
opts := terraform.ExecuteCommandOptions{}

cmd := &cobra.Command{
Use: "invoke",
Short: "Execute the invoke functionality of this mixin",
RunE: func(cmd *cobra.Command, args []string) error {
return mixin.Execute(opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.Action, "action", "", "Custom action name to invoke.")

return cmd
}
1 change: 1 addition & 0 deletions cmd/terraform/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func buildRootCommand(in io.Reader) *cobra.Command {
cmd.AddCommand(buildSchemaCommand(m))
cmd.AddCommand(buildBuildCommand(m))
cmd.AddCommand(buildInstallCommand(m))
cmd.AddCommand(buildInvokeCommand(m))
cmd.AddCommand(buildUninstallCommand(m))
cmd.AddCommand(buildUpgradeCommand(m))
cmd.AddCommand(buildStatusCommand(m))
Expand Down
2 changes: 2 additions & 0 deletions examples/basic-tf-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Dockerfile
.cnab
78 changes: 78 additions & 0 deletions examples/basic-tf-example/porter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
mixins:
- terraform

name: basic-tf-example
version: 0.1.0
invocationImage: basic-tf-example:latest
tag: deislabs/basic-tf-example-bundle:v0.1.0

outputs:
- name: a
type: string
applyTo:
- install
- status
- upgrade
- name: b
type: string
applyTo:
- status
- upgrade
- name: c
type: string
applyTo:
- install
- status

customActions:
plan:
description: "Invoke 'terraform plan'"
modifies: false
stateless: true
show:
description: "Invoke 'terraform show'"
modifies: false
stateless: true
printVersion:
description: "Invoke 'terraform version'"
modifies: false
stateless: true

install:
- terraform:
description: "Install Terraform assets"
autoApprove: true
outputs:
- name: a
- name: b
- name: c

upgrade:
- terraform:
description: "Upgrade Terraform assets"
autoApprove: true
outputs:
- name: a
- name: b
- name: c

show:
- terraform:
description: "Invoke 'terraform show'"

plan:
- terraform:
description: "Invoke 'terraform plan'"

# Note: this can't be 'version:' as this would conflict with top-level field
# Hence the need for the 'command:' override
printVersion:
- terraform:
description: "Invoke 'terraform version'"
command: "version"

uninstall:
- terraform:
autoApprove: true
description: "Uninstall Terraform assets"

Empty file.
11 changes: 11 additions & 0 deletions examples/basic-tf-example/terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
output "a" {
value = "a"
}

output "b" {
value = "b"
}

output "c" {
value = "c"
}
120 changes: 120 additions & 0 deletions pkg/terraform/execute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package terraform

import (
"fmt"
"os"
"strings"

"github.com/pkg/errors"

yaml "gopkg.in/yaml.v2"
)

type ExecuteCommandOptions struct {
Action string
}

type ExecuteAction struct {
Steps []ExecuteStep // using UnmarshalYAML so that we don't need a custom type per action
}

// UnmarshalYAML takes any yaml in this form
// ACTION:
// - terraform: ...
// and puts the steps into the Action.Steps field
func (a *ExecuteAction) UnmarshalYAML(unmarshal func(interface{}) error) error {
actionMap := map[interface{}][]interface{}{}
err := unmarshal(&actionMap)
if err != nil {
return errors.Wrap(err, "could not unmarshal yaml into an action map of terraform steps")
}

for _, stepMaps := range actionMap {
b, err := yaml.Marshal(stepMaps)
if err != nil {
return err
}

var steps []ExecuteStep
err = yaml.Unmarshal(b, &steps)
if err != nil {
return err
}

a.Steps = append(a.Steps, steps...)
}

return nil
}

type ExecuteStep struct {
ExecuteInstruction `yaml:"terraform"`
}

type ExecuteInstruction struct {
// InstallAguments contains the usual terraform command args for re-use here
InstallArguments `yaml:",inline"`

// Command allows an override of the actual terraform command
Command string `yaml:"command,omitempty"`
}

// Execute will reapply manifests using kubectl
func (m *Mixin) Execute(opts ExecuteCommandOptions) error {

payload, err := m.getPayloadData()
if err != nil {
return err
}

var action ExecuteAction
err = yaml.Unmarshal(payload, &action)
if err != nil {
return err
}

if len(action.Steps) != 1 {
return errors.Errorf("expected a single step, but got %d", len(action.Steps))
}

step := action.Steps[0]

if step.LogLevel != "" {
os.Setenv("TF_LOG", step.LogLevel)
}

// First, change to specified working dir
if err := os.Chdir(m.WorkingDir); err != nil {
return fmt.Errorf("could not change directory to specified working dir: %s", err)
}

// Initialize Terraform
fmt.Println("Initializing Terraform...")
err = m.Init(step.BackendConfig)
if err != nil {
return fmt.Errorf("could not init terraform, %s", err)
}

command := opts.Action
if step.Command != "" {
command = step.Command
}
cmd := m.NewCommand("terraform", command)

cmd.Stdout = m.Out
cmd.Stderr = m.Err

prettyCmd := fmt.Sprintf("%s %s", cmd.Path, strings.Join(cmd.Args, " "))
fmt.Fprintln(m.Out, prettyCmd)

err = cmd.Start()
if err != nil {
return fmt.Errorf("could not execute command, %s: %s", prettyCmd, err)
}
err = cmd.Wait()
if err != nil {
return err
}

return m.handleOutputs(step.Outputs)
}
85 changes: 85 additions & 0 deletions pkg/terraform/execute_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package terraform

import (
"bytes"
"fmt"
"os"
"strings"
"testing"

"github.com/deislabs/porter/pkg/test"
"github.com/stretchr/testify/require"

yaml "gopkg.in/yaml.v2"
)

type ExecuteTest struct {
expectedCommand string
executeAction ExecuteAction
}

func TestMixin_ExecuteStep(t *testing.T) {

defaultAction := "foo"

executeTests := []ExecuteTest{
{
expectedCommand: fmt.Sprintf("terraform %s", defaultAction),
executeAction: ExecuteAction{
Steps: []ExecuteStep{
ExecuteStep{
ExecuteInstruction: ExecuteInstruction{
InstallArguments: InstallArguments{
Step: Step{
Description: "My Custom Terraform Action",
},
},
},
},
},
},
},
{
expectedCommand: "terraform version",
executeAction: ExecuteAction{
Steps: []ExecuteStep{
ExecuteStep{
ExecuteInstruction: ExecuteInstruction{
Command: "version",
InstallArguments: InstallArguments{
Step: Step{
Description: "My Custom Terraform Action",
},
},
},
},
},
},
},
}

defer os.Unsetenv(test.ExpectedCommandEnv)
for _, executeTest := range executeTests {
t.Run(executeTest.expectedCommand, func(t *testing.T) {
os.Setenv(test.ExpectedCommandEnv, strings.Join([]string{
"terraform init",
executeTest.expectedCommand,
}, "\n"))

b, err := yaml.Marshal(executeTest.executeAction)
require.NoError(t, err)

h := NewTestMixin(t)
h.In = bytes.NewReader(b)

// Set up working dir as current dir
h.WorkingDir, err = os.Getwd()
require.NoError(t, err)

err = h.Execute(ExecuteCommandOptions{
Action: defaultAction,
})
require.NoError(t, err)
})
}
}

0 comments on commit ce98907

Please sign in to comment.