Skip to content

Commit

Permalink
Showing 4 changed files with 362 additions and 16 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

Wouldn't it be nice if you could run [goss](https://github.com/aelsabbahy/goss) tests against an image during a packer build?

Well, I thought it would, so now you can! This currently only works for building a `linux` image since goss only runs in linux.
Well, I thought it would, so now you can!

This runs during the provisioning process since the machine being provisioned is only available at that time.

@@ -50,6 +50,7 @@ There is an example packer build with goss tests in the `example/` directory.
"format": "",
"goss_file": "",
"vars_file": "",
"targetOs": "Linux",
"vars_env": {
"ARCH": "amd64",
"PROVIDER": "{{user `cloud-provider`}}"
@@ -67,6 +68,10 @@ There is an example packer build with goss tests in the `example/` directory.
## Spec files
Goss spec file and debug spec file (`goss render -d`) are downloaded to `/tmp` folder on local machine from the remote VM. These files are exact specs GOSS validated on the VM. The downloaded GOSS spec can be used to validate any other VM image for equivalency.

## Windows support

This now has support for Windows. Set the optional parameter `targetOs` to `Windows`. Currently, the `vars_env` parameter must include `GOSS_USE_ALPHA=1` as specified in [goss's feature parity document](https://github.com/aelsabbahy/goss/blob/master/docs/platform-feature-parity.md#platform-feature-parity). In the future when goss come of of alpha for Windows this parameter will not be required.

## Installation

1. Download the most recent release for your platform from [here.](https://github.com/YaleUniversity/packer-provisioner-goss/releases).
@@ -91,6 +96,7 @@ Goss spec file and debug spec file (`goss render -d`) are downloaded to `/tmp` f

```bash
docker run --rm -it -v "$PWD":/usr/src/packer-provisioner-goss -w /usr/src/packer-provisioner-goss -e 'VERSION=v1.0.0' golang:1.13 bash
go test ./...
for GOOS in darwin linux windows; do
for GOARCH in 386 amd64; do
export GOOS GOARCH
94 changes: 79 additions & 15 deletions packer-provisioner-goss.go
Original file line number Diff line number Diff line change
@@ -19,8 +19,12 @@ import (
"github.com/hashicorp/packer/template/interpolate"
)

const gossSpecFile = "/tmp/goss-spec.yaml"
const gossDebugSpecFile = "/tmp/debug-goss-spec.yaml"
const (
gossSpecFile = "/tmp/goss-spec.yaml"
gossDebugSpecFile = "/tmp/debug-goss-spec.yaml"
linux = "Linux"
windows = "Windows"
)

// GossConfig holds the config data coming in from the packer template
type GossConfig struct {
@@ -33,6 +37,7 @@ type GossConfig struct {
Password string
SkipInstall bool
Inspect bool
TargetOs string

// An array of tests to run.
Tests []string
@@ -126,20 +131,31 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
p.config.Arch = "amd64"
}

if p.config.TargetOs == "" {
p.config.TargetOs = linux
}

if p.config.URL == "" {
p.config.URL = fmt.Sprintf(
"https://github.com/aelsabbahy/goss/releases/download/v%s/goss-linux-%s",
p.config.Version, p.config.Arch)
p.config.URL = p.getDownloadUrl()
}

if p.config.DownloadPath == "" {
os := strings.ToLower(p.config.TargetOs)
if p.config.URL == "" {
p.config.DownloadPath = fmt.Sprintf("/tmp/goss-%s-linux-%s", p.config.Version, p.config.Arch)
p.config.DownloadPath = fmt.Sprintf("/tmp/goss-%s-%s-%s", p.config.Version, os, p.config.Arch)
} else {
list := strings.Split(p.config.URL, "/")
arch := strings.Split(list[len(list)-1], "-")[2]

file := strings.Split(list[len(list)-1], "-")
arch := file[2]
if p.isGossAlpha() {
// The format of the alpha files includes an additional entry
// ex: goss-alpha-windows-amd64.exe
arch = file[3]
}

version := strings.TrimPrefix(list[len(list)-2], "v")
p.config.DownloadPath = fmt.Sprintf("/tmp/goss-%s-linux-%s", version, arch)
p.config.DownloadPath = fmt.Sprintf("/tmp/goss-%s-%s-%s", version, os, arch)
}
}

@@ -202,6 +218,11 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
}
}

if p.config.TargetOs != linux && p.config.TargetOs != windows {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Os must be %s or %s", linux, windows))
}

if errs != nil && len(errs.Errors) > 0 {
return errs
}
@@ -212,6 +233,12 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
// Provision runs the Goss Provisioner
func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.Communicator, generatedData map[string]interface{}) error {
ui.Say("Provisioning with Goss")
ui.Say(fmt.Sprintf("Configured to run on %s", string(p.config.TargetOs)))

// For Windows need to create the target directory before download
if err := p.createDir(ui, comm, p.config.RemotePath); err != nil {
return fmt.Errorf("Error creating remote directory: %s", err)
}

if !p.config.SkipInstall {
if err := p.installGoss(ui, comm); err != nil {
@@ -222,10 +249,6 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C
}

ui.Say("Uploading goss tests...")
if err := p.createDir(ui, comm, p.config.RemotePath); err != nil {
return fmt.Errorf("Error creating remote directory: %s", err)
}

if p.config.VarsFile != "" {
vf, err := os.Stat(p.config.VarsFile)
if err != nil {
@@ -416,18 +439,50 @@ func (p *Provisioner) inline_vars() string {
if len(p.config.VarsInline) != 0 {
inlineVarsJson, err := json.Marshal(p.config.VarsInline)
if err == nil {
return fmt.Sprintf("--vars-inline '%s'", string(inlineVarsJson))
switch p.config.TargetOs {
case windows:
// don't include single quotes which confused cmd parsing
return fmt.Sprintf("--vars-inline %s", string(inlineVarsJson))
default:
return fmt.Sprintf("--vars-inline '%s'", string(inlineVarsJson))
}
} else {
fmt.Errorf("Error converting inline vars to json string %v", err)
}
}
return ""
}

func (p *Provisioner) getDownloadUrl() string {
os := strings.ToLower(string(p.config.TargetOs))
filename := fmt.Sprintf("goss-%s-%s", os, p.config.Arch)

if p.isGossAlpha() {
filename = fmt.Sprintf("goss-alpha-%s-%s", os, p.config.Arch)
}

if p.config.TargetOs == windows {
filename = fmt.Sprintf("%s.exe", filename)
}

return fmt.Sprintf("https://github.com/aelsabbahy/goss/releases/download/v%s/%s", p.config.Version, filename)
}

func (p *Provisioner) isGossAlpha() bool {
return p.config.VarsEnv["GOSS_USE_ALPHA"] == "1"
}

func (p *Provisioner) envVars() string {
var sb strings.Builder
for env_var, value := range p.config.VarsEnv {
sb.WriteString(fmt.Sprintf("%s=\"%s\" ", env_var, value))
switch p.config.TargetOs {
case windows:
// Windows requires a call to "set" as separate command seperated by && for each env variable
sb.WriteString(fmt.Sprintf("set \"%s=%s\" && ", env_var, value))
default:
sb.WriteString(fmt.Sprintf("%s=\"%s\" ", env_var, value))
}

}
return sb.String()
}
@@ -481,7 +536,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri
ctx := context.TODO()

cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("mkdir -p '%s'", dir),
Command: p.mkDir(dir),
}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
return err
@@ -492,6 +547,15 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri
return nil
}

func (p *Provisioner) mkDir(dir string) string {
switch p.config.TargetOs {
case windows:
return fmt.Sprintf("powershell /c mkdir -p '%s'", dir)
default:
return fmt.Sprintf("mkdir -p '%s'", dir)
}
}

// uploadFile uploads a file
func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error {
f, err := os.Open(src)
2 changes: 2 additions & 0 deletions packer-provisioner-goss.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

274 changes: 274 additions & 0 deletions packer-provisioner-goss_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
//go:generate mapstructure-to-hcl2 -type GossConfig

package main

import (
"reflect"
"testing"

"github.com/hashicorp/packer/template/interpolate"
)

func fakeContext() interpolate.Context {
var data map[interface{}]interface{}
var funcs map[string]interface{}
var userVars map[string]string
var sensitiveVars []string
return interpolate.Context{
Data: data,
Funcs: funcs,
UserVariables: userVars,
SensitiveVariables: sensitiveVars,
EnableEnv: false,
BuildName: "",
BuildType: "",
TemplatePath: "",
}
}

func TestProvisioner_Prepare(t *testing.T) {

var tests = []struct {
name string
input []interface{}
wantErr bool
wantConfig GossConfig
}{
{
name: "defaults",
input: []interface{}{
map[string]interface{}{
"tests": []string{"example/goss"},
},
},
wantErr: false,
wantConfig: GossConfig{
Version: "0.3.9",
Arch: "amd64",
URL: "https://github.com/aelsabbahy/goss/releases/download/v0.3.9/goss-linux-amd64",
DownloadPath: "/tmp/goss-0.3.9-linux-amd64",
Username: "",
Password: "",
SkipInstall: false,
Inspect: false,
TargetOs: "Linux",
Tests: []string{"example/goss"},
RetryTimeout: "",
Sleep: "",
UseSudo: false,
SkipSSLChk: false,
GossFile: "",
VarsFile: "",
VarsInline: nil,
VarsEnv: nil,
RemoteFolder: "/tmp",
RemotePath: "/tmp/goss",
Format: "",
FormatOptions: "",
ctx: fakeContext(),
},
},
{
name: "Windows",
input: []interface{}{
map[string]interface{}{
"tests": []string{"example/goss"},
"targetOs": "Windows",
"vars_env": map[string]string{
"GOSS_USE_ALPHA": "1",
},
},
},
wantErr: false,
wantConfig: GossConfig{
Version: "0.3.9",
Arch: "amd64",
URL: "https://github.com/aelsabbahy/goss/releases/download/v0.3.9/goss-alpha-windows-amd64.exe",
DownloadPath: "/tmp/goss-0.3.9-windows-amd64.exe",
Username: "",
Password: "",
SkipInstall: false,
Inspect: false,
TargetOs: "Windows",
Tests: []string{"example/goss"},
RetryTimeout: "",
Sleep: "",
UseSudo: false,
SkipSSLChk: false,
GossFile: "",
VarsFile: "",
VarsInline: nil,
VarsEnv: map[string]string{
"GOSS_USE_ALPHA": "1",
},
RemoteFolder: "/tmp",
RemotePath: "/tmp/goss",
Format: "",
FormatOptions: "",
ctx: fakeContext(),
},
},
{
name: "Windows non alpha",
input: []interface{}{
map[string]interface{}{
"tests": []string{"example/goss"},
"targetOs": "Windows",
},
},
wantErr: false,
wantConfig: GossConfig{
Version: "0.3.9",
Arch: "amd64",
URL: "https://github.com/aelsabbahy/goss/releases/download/v0.3.9/goss-windows-amd64.exe",
DownloadPath: "/tmp/goss-0.3.9-windows-amd64.exe",
Username: "",
Password: "",
SkipInstall: false,
Inspect: false,
TargetOs: "Windows",
Tests: []string{"example/goss"},
RetryTimeout: "",
Sleep: "",
UseSudo: false,
SkipSSLChk: false,
GossFile: "",
VarsFile: "",
VarsInline: nil,
VarsEnv: nil,
RemoteFolder: "/tmp",
RemotePath: "/tmp/goss",
Format: "",
FormatOptions: "",
ctx: fakeContext(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provisioner{
config: GossConfig{
ctx: interpolate.Context{},
},
}
err := p.Prepare(tt.input...)
if (err != nil) != tt.wantErr {
t.Errorf("Provisioner.Prepare() error = %v, wantErr %v", err, tt.wantErr)
}

if err == nil && !reflect.DeepEqual(p.config, tt.wantConfig) {
t.Error("configs do not match")
t.Logf("got config= %v", p.config)
t.Logf("want config= %v", tt.wantConfig)
}

})
}
}

func TestProvisioner_envVars(t *testing.T) {

tests := []struct {
name string
config GossConfig
want string
}{
{
name: "Linux",
config: GossConfig{
TargetOs: "Linux",
VarsEnv: map[string]string{
"somevar": "1",
},
},
want: "somevar=\"1\" ",
},
{
name: "Windows",
config: GossConfig{
TargetOs: "Windows",
VarsEnv: map[string]string{
"GOSS_USE_ALPHA": "1",
},
},
want: "set \"GOSS_USE_ALPHA=1\" && ",
},
{
name: "no vars windows",
config: GossConfig{
TargetOs: "Windows",
VarsEnv: map[string]string{},
},
want: "",
},
{
name: "no vars linux",
config: GossConfig{
TargetOs: "Linux",
VarsEnv: map[string]string{},
},
want: "",
},
{
name: "no configured target os",
config: GossConfig{
VarsEnv: map[string]string{
"somevar": "1",
},
},
want: "somevar=\"1\" ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provisioner{
config: tt.config,
}
if got := p.envVars(); got != tt.want {
t.Errorf("Provisioner.envVars() = '%v', want '%v'", got, tt.want)
}
})
}
}

func TestProvisioner_mkDir(t *testing.T) {
tests := []struct {
name string
config GossConfig
dir string
wantcmd string
}{
{
name: "linux",
config: GossConfig{
TargetOs: linux,
},
dir: "/tmp",
wantcmd: "mkdir -p '/tmp'",
},
{
name: "windows",
config: GossConfig{
TargetOs: windows,
},
dir: "/tmp",
wantcmd: "powershell /c mkdir -p '/tmp'",
},
{
name: "no configured os",
config: GossConfig{},
dir: "/tmp",
wantcmd: "mkdir -p '/tmp'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provisioner{
config: tt.config,
}
if got := p.mkDir(tt.dir); got != tt.wantcmd {
t.Errorf("Provisioner.mkDir() = %v, want %v", got, tt.wantcmd)
}
})
}
}

0 comments on commit 0705d97

Please sign in to comment.