Skip to content

Commit

Permalink
Merge pull request #130 from simongdavies/command_outputs
Browse files Browse the repository at this point in the history
Add support for outputs to command driver
  • Loading branch information
carolynvs-msft authored Oct 8, 2019
2 parents 4068084 + 97f6634 commit 26ed63f
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 15 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@ coverage:
go test -v -coverprofile=coverage.txt -covermode count ./... 2>&1 | go-junit-report > report.xml
gocov convert coverage.txt > coverage.json
gocov-xml < coverage.json > coverage.xml

.PHONY: goimports
goimports:
find . -name "*.go" | fgrep -v vendor/ | xargs goimports -w -local github.com/$(ORG)/$(PROJECT)
68 changes: 63 additions & 5 deletions driver/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"

"github.com/deislabs/cnab-go/driver"
)

// Driver relies upon a system command to provide a driver implementation
type Driver struct {
Name string
Name string
outputDirName string
}

// Run executes the command
Expand Down Expand Up @@ -56,14 +59,27 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
added = append(added, k)
}
// Create a directory that can be used for outputs and then pass it as a command line argument
if len(op.Outputs) > 0 {
var err error
d.outputDirName, err = ioutil.TempDir("", "bundleoutput")
if err != nil {
return driver.OperationResult{}, err
}
defer os.RemoveAll(d.outputDirName)
// Set the env var CNAB_OUTPUT_DIR to the location of the folder
pairs = append(pairs, fmt.Sprintf("%s=%s", "CNAB_OUTPUT_DIR", d.outputDirName))
added = append(added, "CNAB_OUTPUT_DIR")
}

// CNAB_VARS is a list of variables we added to the env. This is to make
// it easier for shell script drivers to clone the env vars.
pairs = append(pairs, fmt.Sprintf("CNAB_VARS=%s", strings.Join(added, ",")))

data, err := json.Marshal(op)
if err != nil {
return driver.OperationResult{}, err
}

args := []string{}
cmd := exec.Command(d.cliName(), args...)
cmd.Dir, err = os.Getwd()
Expand All @@ -72,9 +88,7 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
}
cmd.Env = pairs
cmd.Stdin = bytes.NewBuffer(data)

// Make stdout and stderr from driver available immediately

stdout, err := cmd.StdoutPipe()
if err != nil {
return driver.OperationResult{}, fmt.Errorf("Setting up output handling for driver (%s) failed: %v", d.Name, err)
Expand All @@ -90,6 +104,7 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
if err != nil {
return driver.OperationResult{}, fmt.Errorf("Setting up error output handling for driver (%s) failed: %v", d.Name, err)
}

go func() {

// Errors not handled here as they only prevent output from the driver being shown, errors in the command execution are handled when command is executed
Expand All @@ -101,5 +116,48 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
return driver.OperationResult{}, fmt.Errorf("Start of driver (%s) failed: %v", d.Name, err)
}

return driver.OperationResult{}, cmd.Wait()
if err = cmd.Wait(); err != nil {
return driver.OperationResult{}, fmt.Errorf("Command driver (%s) failed executing bundle: %v", d.Name, err)
}

result, err := d.getOperationResult(op)
if err != nil {
return driver.OperationResult{}, fmt.Errorf("Command driver (%s) failed getting operation result: %v", d.Name, err)
}
return result, nil
}
func (d *Driver) getOperationResult(op *driver.Operation) (driver.OperationResult, error) {
opResult := driver.OperationResult{
Outputs: map[string]string{},
}
for _, item := range op.Outputs {
fileName := path.Join(d.outputDirName, item)
_, err := os.Stat(fileName)
if err != nil {
if os.IsNotExist(err) {
continue
}
return opResult, fmt.Errorf("Command driver (%s) failed checking for output file: %s Error: %v", d.Name, item, err)
}

contents, err := ioutil.ReadFile(fileName)
if err != nil {
return opResult, fmt.Errorf("Command driver (%s) failed reading output file: %s Error: %v", d.Name, item, err)
}

opResult.Outputs[item] = string(contents)
}
// Check if there are missing outputs and get default values if any
for name, output := range op.Bundle.Outputs {
if output.AppliesTo(op.Action) {
if _, exists := opResult.Outputs[output.Path]; !exists {
if outputDefinition, exists := op.Bundle.Definitions[output.Definition]; exists && outputDefinition.Default != nil {
opResult.Outputs[output.Path] = fmt.Sprintf("%v", outputDefinition.Default)
} else {
return opResult, fmt.Errorf("Command driver (%s) failed - required output %s for action %s is missing and has no default is defined", d.Name, name, op.Action)
}
}
}
}
return opResult, nil
}
176 changes: 176 additions & 0 deletions driver/command/command_nix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// +build !windows

package command

import (
"os"
"testing"

"github.com/deislabs/cnab-go/bundle"
"github.com/deislabs/cnab-go/bundle/definition"
"github.com/deislabs/cnab-go/driver"
"github.com/stretchr/testify/assert"
)

func TestCommandDriverOutputs(t *testing.T) {
content := `#!/bin/sh
mkdir -p "${CNAB_OUTPUT_DIR}/cnab/app/outputs"
echo "TEST_OUTPUT_1" >> "${CNAB_OUTPUT_DIR}/cnab/app/outputs/output1"
echo "TEST_OUTPUT_2" >> "${CNAB_OUTPUT_DIR}/cnab/app/outputs/output2"
`
name := "test-outputs-exist.sh"
testfunc := func(t *testing.T, cmddriver *Driver) {
if !cmddriver.CheckDriverExists() {
t.Fatalf("Expected driver %s to exist Driver Name %s ", name, cmddriver.Name)
}
op := driver.Operation{
Action: "install",
Installation: "test",
Parameters: map[string]interface{}{},
Image: bundle.InvocationImage{
BaseImage: bundle.BaseImage{
Image: "cnab/helloworld:latest",
ImageType: "docker",
},
},
Revision: "01DDY0MT808KX0GGZ6SMXN4TW",
Environment: map[string]string{},
Files: map[string]string{
"/cnab/app/image-map.json": "{}",
},
Outputs: []string{"/cnab/app/outputs/output1", "/cnab/app/outputs/output2"},
Out: os.Stdout,
Bundle: &bundle.Bundle{
Definitions: definition.Definitions{
"output1": &definition.Schema{},
"output2": &definition.Schema{},
},
Outputs: map[string]bundle.Output{
"output1": {
Definition: "output1",
Path: "/cnab/app/outputs/output1",
},
"output2": {
Definition: "output2",
Path: "/cnab/app/outputs/output2",
},
},
},
}
opResult, err := cmddriver.Run(&op)
if err != nil {
t.Fatalf("Driver Run failed %v", err)
}
assert.Equal(t, 2, len(opResult.Outputs), "Expecting two output files")
assert.Equal(t, map[string]string{
"/cnab/app/outputs/output1": "TEST_OUTPUT_1\n",
"/cnab/app/outputs/output2": "TEST_OUTPUT_2\n",
}, opResult.Outputs)
}
CreateAndRunTestCommandDriver(t, name, content, testfunc)
// Test for an output missing and no defaults
content = `#!/bin/sh
mkdir -p "${CNAB_OUTPUT_DIR}/cnab/app/outputs"
echo "TEST_OUTPUT_1" >> "${CNAB_OUTPUT_DIR}/cnab/app/outputs/output1"
`
name = "test-outputs-missing.sh"
testfunc = func(t *testing.T, cmddriver *Driver) {
if !cmddriver.CheckDriverExists() {
t.Fatalf("Expected driver %s to exist Driver Name %s ", name, cmddriver.Name)
}
op := driver.Operation{
Action: "install",
Installation: "test",
Parameters: map[string]interface{}{},
Image: bundle.InvocationImage{
BaseImage: bundle.BaseImage{
Image: "cnab/helloworld:latest",
ImageType: "docker",
},
},
Revision: "01DDY0MT808KX0GGZ6SMXN4TW",
Environment: map[string]string{},
Files: map[string]string{
"/cnab/app/image-map.json": "{}",
},
Outputs: []string{"/cnab/app/outputs/output1", "/cnab/app/outputs/output2"},
Out: os.Stdout,
Bundle: &bundle.Bundle{
Definitions: definition.Definitions{
"output1": &definition.Schema{},
"output2": &definition.Schema{},
},
Outputs: map[string]bundle.Output{
"output1": {
Definition: "output1",
Path: "/cnab/app/outputs/output1",
},
"output2": {
Definition: "output2",
Path: "/cnab/app/outputs/output2",
},
},
},
}
_, err := cmddriver.Run(&op)
assert.Errorf(t, err, "Command driver (test-outputs-missing.sh) failed for item: /cnab/app/outputs/output2 no output value found and no default value set")
}
CreateAndRunTestCommandDriver(t, name, content, testfunc)
// Test for an output missing with default value present
content = `#!/bin/sh
mkdir -p "${CNAB_OUTPUT_DIR}/cnab/app/outputs"
echo "TEST_OUTPUT_1" >> "${CNAB_OUTPUT_DIR}/cnab/app/outputs/output1"
`
name = "test-outputs-missing.sh"
testfunc = func(t *testing.T, cmddriver *Driver) {
if !cmddriver.CheckDriverExists() {
t.Fatalf("Expected driver %s to exist Driver Name %s ", name, cmddriver.Name)
}
op := driver.Operation{
Action: "install",
Installation: "test",
Parameters: map[string]interface{}{},
Image: bundle.InvocationImage{
BaseImage: bundle.BaseImage{
Image: "cnab/helloworld:latest",
ImageType: "docker",
},
},
Revision: "01DDY0MT808KX0GGZ6SMXN4TW",
Environment: map[string]string{},
Files: map[string]string{
"/cnab/app/image-map.json": "{}",
},
Outputs: []string{"/cnab/app/outputs/output1", "/cnab/app/outputs/output2"},
Out: os.Stdout,
Bundle: &bundle.Bundle{
Definitions: definition.Definitions{
"output1": &definition.Schema{},
"output2": &definition.Schema{
Default: "DEFAULT OUTPUT 2",
},
},
Outputs: map[string]bundle.Output{
"output1": {
Definition: "output1",
Path: "/cnab/app/outputs/output1",
},
"output2": {
Definition: "output2",
Path: "/cnab/app/outputs/output2",
},
},
},
}
opResult, err := cmddriver.Run(&op)
if err != nil {
t.Fatalf("Driver Run failed %v", err)
}
assert.Equal(t, 2, len(opResult.Outputs), "Expecting two output files")
assert.Equal(t, map[string]string{
"/cnab/app/outputs/output1": "TEST_OUTPUT_1\n",
"/cnab/app/outputs/output2": "DEFAULT OUTPUT 2",
}, opResult.Outputs)
}
CreateAndRunTestCommandDriver(t, name, content, testfunc)
}
21 changes: 16 additions & 5 deletions driver/command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ func TestCheckDriverExists(t *testing.T) {
}

name = "existing-driver"
cmddriver = &Driver{Name: name}
testfunc := func(t *testing.T, cmddriver *Driver) {
if !cmddriver.CheckDriverExists() {
t.Fatalf("Expected driver %s to exist", cmddriver.Name)
}

}
CreateAndRunTestCommandDriver(t, name, "", testfunc)
}
func CreateAndRunTestCommandDriver(t *testing.T, name string, content string, testfunc func(t *testing.T, d *Driver)) {
cmddriver := &Driver{Name: name}
dirname, err := ioutil.TempDir("", "cnab")
if err != nil {
t.Fatal(err)
Expand All @@ -33,14 +42,16 @@ func TestCheckDriverExists(t *testing.T) {
t.Fatal(err)
}

if len(content) > 0 {
newfile.WriteString(content)
}

newfile.Chmod(0755)
newfile.Close()
path := os.Getenv("PATH")
pathlist := []string{dirname, path}
newpath := strings.Join(pathlist, string(os.PathListSeparator))
defer os.Setenv("PATH", path)
os.Setenv("PATH", newpath)
if !cmddriver.CheckDriverExists() {
t.Fatalf("Expected driver %s to exist", name)
}

testfunc(t, cmddriver)
}
10 changes: 5 additions & 5 deletions driver/docker/docker_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestDriver_Run(t *testing.T) {
Installation: "example",
Action: "install",
Image: image,
Outputs: []string{
Outputs: []string{
"/cnab/app/outputs/output1",
"/cnab/app/outputs/output2",
"/cnab/app/outputs/missingApplicableOutputWithDefault",
Expand Down Expand Up @@ -65,7 +65,7 @@ func TestDriver_Run(t *testing.T) {
},
"missingNonApplicableOutputWithDefault": {
Definition: "missingApplicableOutputWithDefault",
ApplyTo: []string{"upgrade"},
ApplyTo: []string{"upgrade"},
},
},
},
Expand All @@ -86,8 +86,8 @@ func TestDriver_Run(t *testing.T) {
assert.Equal(t, "Install action\nAction install complete for example\n", output.String())
assert.Equal(t, 3, len(opResult.Outputs), "Expecting three output files")
assert.Equal(t, map[string]string{
"/cnab/app/outputs/output1": "SOME INSTALL CONTENT 1\n",
"/cnab/app/outputs/output2": "SOME INSTALL CONTENT 2\n",
"/cnab/app/outputs/output1": "SOME INSTALL CONTENT 1\n",
"/cnab/app/outputs/output2": "SOME INSTALL CONTENT 2\n",
"/cnab/app/outputs/missingApplicableOutputWithDefault": "foo",
}, opResult.Outputs)
}
Expand Down Expand Up @@ -115,7 +115,7 @@ func TestDriver_Run_MissingRequiredOutput(t *testing.T) {
Installation: "example",
Action: "install",
Image: image,
Outputs: []string{
Outputs: []string{
"/cnab/app/outputs/missingApplicableOutputSansDefault",
},
Bundle: &bundle.Bundle{
Expand Down

0 comments on commit 26ed63f

Please sign in to comment.