diff --git a/.gitignore b/.gitignore index 5cad884..dcbbea0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -example/packer_cache \ No newline at end of file +example/packer_cache +.envrc +packer-plugin-goss +tests/debug-goss-spec.yaml +tests/goss-spec.yaml +tests/goss.json \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2ad85ff --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +default: help + +.PHONY: help +help: ## list makefile targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: fmt +fmt: ## format go files + gofumpt -w . + gci write . + packer fmt -recursive -write . + +.PHONY: build +build: ## build the plugin + go build -ldflags="-X github.com/YaleUniversity/packer-provisioner-goss/version.VersionPrerelease=dev" -o packer-plugin-goss + +.PHONY: install +install: ## install the plugin + packer plugins install --path packer-plugin-goss github.com/YaleUniversity/goss + +.PHONY: local +local: clean build install ## build and install the plugin locally + cd tests && packer init . + cd tests && packer build local.pkr.hcl + +.PHONY: clean +clean: ## remove tmp files + rm -f packer-plugin-goss tests/goss.json tests/debug-goss-spec.yaml tests/goss-spec.yaml packer-plugin-goss + +.PHONY: generate +generate: ## go generate + go generate ./... \ No newline at end of file diff --git a/README.md b/README.md index 2cfe159..b189c25 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ build { # Provisioner Args arch ="amd64" download_path = "/tmp/goss-VERSION-linux-ARCH" - inspect = "{{ inspect_mode }}", + inspect = "{{ inspect_mode }}" password = "" skip_install = false url = "https://github.com/aelsabbahy/goss/releases/download/vVERSION/goss-linux-ARCH" diff --git a/main.go b/main.go index f7bd4d2..7efa7d4 100644 --- a/main.go +++ b/main.go @@ -4,15 +4,12 @@ import ( "fmt" "os" + "github.com/YaleUniversity/packer-provisioner-goss/provisioner/goss" "github.com/hashicorp/packer-plugin-sdk/plugin" "github.com/hashicorp/packer-plugin-sdk/version" - - "github.com/YaleUniversity/packer-provisioner-goss/provisioner/goss" ) -var ( - Version = "0.0.1" -) +var Version = "0.0.1" func main() { pps := plugin.NewSet() @@ -20,7 +17,6 @@ func main() { pps.SetVersion(version.InitializePluginVersion(Version, "")) err := pps.Run() - if err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) diff --git a/provisioner/goss/packer-provisioner-goss.go b/provisioner/goss/packer-provisioner-goss.go index e9a6bc9..76b6539 100644 --- a/provisioner/goss/packer-provisioner-goss.go +++ b/provisioner/goss/packer-provisioner-goss.go @@ -12,13 +12,13 @@ import ( "strings" "github.com/hashicorp/hcl/v2/hcldec" - "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" ) const ( + gossVersion = "0.4.7" gossSpecFile = "/tmp/goss-spec.yaml" gossDebugSpecFile = "/tmp/debug-goss-spec.yaml" linux = "Linux" @@ -83,6 +83,9 @@ type GossConfig struct { // Default: rspecish Format string `mapstructure:"format"` + // Destination of the file to write the output to, only works for linux + OutputFile string `mapstructure:"output_file"` + // The format options to use for printing test output // Available: [perfdata verbose pretty] // Default: verbose @@ -91,8 +94,10 @@ type GossConfig struct { ctx interpolate.Context } -var validFormats = []string{"documentation", "json", "json_oneline", "junit", "nagios", "nagios_verbose", "rspecish", "silent", "tap"} -var validFormatOptions = []string{"perfdata", "verbose", "pretty"} +var ( + validFormats = []string{"documentation", "json", "json_oneline", "junit", "nagios", "nagios_verbose", "rspecish", "silent", "tap"} + validFormatOptions = []string{"perfdata", "verbose", "pretty"} +) // Provisioner implements a packer Provisioner type Provisioner struct { @@ -117,7 +122,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } if p.config.Version == "" { - p.config.Version = "0.4.2" + p.config.Version = gossVersion } if p.config.Arch == "" { @@ -184,6 +189,13 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } } + // output file only works for linux + if p.config.OutputFile != "" { + if p.config.TargetOs != linux { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Output file only works for linux")) + } + } + if p.config.FormatOptions != "" { valid := false for _, candidate := range validFormatOptions { @@ -292,6 +304,13 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C return fmt.Errorf("Error running Goss: %s", err) } + if p.config.OutputFile != "" { + ui.Say("\n\n\nDownloading Goss test result file") + if err := p.downloadTestResults(ui, comm); err != nil { + return err + } + } + if !p.config.SkipDownload { ui.Say("\n\n\nDownloading spec file and debug info") if err := p.downloadSpecs(ui, comm); err != nil { @@ -304,6 +323,24 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C return nil } +// downloadSpecs downloads the Goss specs from the remote host to current working dir on local machine +func (p *Provisioner) downloadTestResults(ui packer.Ui, comm packer.Communicator) error { + ui.Message(fmt.Sprintf("Downloading Goss test results from %s to current dir", p.config.OutputFile)) + + f, err := os.Create(filepath.Base(p.config.OutputFile)) + if err != nil { + return fmt.Errorf("Error opening: %s", err) + } + + defer f.Close() + + if err := comm.Download(p.config.OutputFile, f); err != nil { + return fmt.Errorf("Error downloading %s: %s", p.config.OutputFile, err) + } + + return nil +} + // downloadSpecs downloads the Goss specs from the remote host to current working dir on local machine func (p *Provisioner) downloadSpecs(ui packer.Ui, comm packer.Communicator) error { ui.Message(fmt.Sprintf("Downloading Goss specs from, %s and %s to current dir", gossSpecFile, gossDebugSpecFile)) @@ -361,9 +398,9 @@ func (p *Provisioner) runGoss(ui packer.Ui, comm packer.Communicator) error { p.config.RemotePath, p.envVars(), goss, p.config.GossFile, p.vars(), p.inline_vars(), gossDebugSpecFile, ), - "validate": fmt.Sprintf("cd %s && %s %s %s %s %s %s validate --retry-timeout %s --sleep %s %s %s", + "validate": fmt.Sprintf("cd %s && %s %s %s %s %s %s validate --retry-timeout %s --sleep %s %s %s %s", p.config.RemotePath, p.enableSudo(), p.envVars(), goss, p.config.GossFile, - p.vars(), p.inline_vars(), p.retryTimeout(), p.sleep(), p.format(), p.formatOptions(), + p.vars(), p.inline_vars(), p.retryTimeout(), p.sleep(), p.format(), p.formatOptions(), p.outputFile(), ), } @@ -397,6 +434,14 @@ func (p *Provisioner) runGossCmd(ui packer.Ui, comm packer.Communicator, cmd *pa return nil } +func (p *Provisioner) outputFile() string { + if p.config.OutputFile == "" { + return "" + } + + return fmt.Sprintf("| tee %s", p.config.OutputFile) +} + func (p *Provisioner) retryTimeout() string { if p.config.RetryTimeout == "" { return "0s" // goss default @@ -479,7 +524,6 @@ func (p *Provisioner) envVars() string { default: sb.WriteString(fmt.Sprintf("%s=\"%s\" ", env_var, value)) } - } return sb.String() } diff --git a/provisioner/goss/packer-provisioner-goss.hcl2spec.go b/provisioner/goss/packer-provisioner-goss.hcl2spec.go index e6f13db..1a668d2 100644 --- a/provisioner/goss/packer-provisioner-goss.hcl2spec.go +++ b/provisioner/goss/packer-provisioner-goss.hcl2spec.go @@ -32,6 +32,7 @@ type FlatGossConfig struct { RemotePath *string `mapstructure:"remote_path" cty:"remote_path" hcl:"remote_path"` SkipDownload *bool `mapstructure:"skip_download" cty:"skip_download" hcl:"skip_download"` Format *string `mapstructure:"format" cty:"format" hcl:"format"` + OutputFile *string `mapstructure:"output_file" cty:"output_file" hcl:"output_file"` FormatOptions *string `mapstructure:"format_options" cty:"format_options" hcl:"format_options"` } @@ -69,6 +70,7 @@ func (*FlatGossConfig) HCL2Spec() map[string]hcldec.Spec { "remote_path": &hcldec.AttrSpec{Name: "remote_path", Type: cty.String, Required: false}, "skip_download": &hcldec.AttrSpec{Name: "skip_download", Type: cty.Bool, Required: false}, "format": &hcldec.AttrSpec{Name: "format", Type: cty.String, Required: false}, + "output_file": &hcldec.AttrSpec{Name: "output_file", Type: cty.String, Required: false}, "format_options": &hcldec.AttrSpec{Name: "format_options", Type: cty.String, Required: false}, } return s diff --git a/provisioner/goss/packer-provisioner-goss_test.go b/provisioner/goss/packer-provisioner-goss_test.go index 5104c84..12bed83 100644 --- a/provisioner/goss/packer-provisioner-goss_test.go +++ b/provisioner/goss/packer-provisioner-goss_test.go @@ -1,5 +1,3 @@ -//go:generate packer-sdc mapstructure-to-hcl2 -type GossConfig - package goss import ( @@ -27,8 +25,7 @@ func fakeContext() interpolate.Context { } func TestProvisioner_Prepare(t *testing.T) { - - var tests = []struct { + tests := []struct { name string input []interface{} wantErr bool @@ -43,10 +40,10 @@ func TestProvisioner_Prepare(t *testing.T) { }, wantErr: false, wantConfig: GossConfig{ - Version: "0.4.2", + Version: "0.4.7", Arch: "amd64", - URL: "https://github.com/goss-org/goss/releases/download/v0.4.2/goss-linux-amd64", - DownloadPath: "/tmp/goss-0.4.2-linux-amd64", + URL: "https://github.com/goss-org/goss/releases/download/v0.4.7/goss-linux-amd64", + DownloadPath: "/tmp/goss-0.4.7-linux-amd64", Username: "", Password: "", SkipInstall: false, @@ -65,6 +62,7 @@ func TestProvisioner_Prepare(t *testing.T) { RemotePath: "/tmp/goss", Format: "", FormatOptions: "", + OutputFile: "", ctx: fakeContext(), }, }, @@ -81,10 +79,10 @@ func TestProvisioner_Prepare(t *testing.T) { }, wantErr: false, wantConfig: GossConfig{ - Version: "0.4.2", + Version: "0.4.7", Arch: "amd64", - URL: "https://github.com/goss-org/goss/releases/download/v0.4.2/goss-alpha-windows-amd64.exe", - DownloadPath: "/tmp/goss-0.4.2-windows-amd64.exe", + URL: "https://github.com/goss-org/goss/releases/download/v0.4.7/goss-alpha-windows-amd64.exe", + DownloadPath: "/tmp/goss-0.4.7-windows-amd64.exe", Username: "", Password: "", SkipInstall: false, @@ -105,6 +103,7 @@ func TestProvisioner_Prepare(t *testing.T) { RemotePath: "/tmp/goss", Format: "", FormatOptions: "", + OutputFile: "", ctx: fakeContext(), }, }, @@ -118,10 +117,10 @@ func TestProvisioner_Prepare(t *testing.T) { }, wantErr: false, wantConfig: GossConfig{ - Version: "0.4.2", + Version: "0.4.7", Arch: "amd64", - URL: "https://github.com/goss-org/goss/releases/download/v0.4.2/goss-windows-amd64.exe", - DownloadPath: "/tmp/goss-0.4.2-windows-amd64.exe", + URL: "https://github.com/goss-org/goss/releases/download/v0.4.7/goss-windows-amd64.exe", + DownloadPath: "/tmp/goss-0.4.7-windows-amd64.exe", Username: "", Password: "", SkipInstall: false, @@ -140,6 +139,7 @@ func TestProvisioner_Prepare(t *testing.T) { RemotePath: "/tmp/goss", Format: "", FormatOptions: "", + OutputFile: "", ctx: fakeContext(), }, }, @@ -161,13 +161,11 @@ func TestProvisioner_Prepare(t *testing.T) { 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 diff --git a/tests/goss.yaml b/tests/goss.yaml new file mode 100644 index 0000000..b9b8c33 --- /dev/null +++ b/tests/goss.yaml @@ -0,0 +1,3 @@ +process: + sshd: + running: true \ No newline at end of file diff --git a/tests/local.pkr.hcl b/tests/local.pkr.hcl new file mode 100644 index 0000000..c883c9c --- /dev/null +++ b/tests/local.pkr.hcl @@ -0,0 +1,41 @@ +packer { + required_version = ">= 1.9.0" + + required_plugins { + goss = { + version = "v0.0.1" + source = "github.com/YaleUniversity/goss" + } + } +} + +variable "ssh_user" { + default = env("USER") +} + +variable "ssh_password" { + default = env("SSH_PASSWORD") +} + +variable "ssh_host" { + default = "127.0.0.1" +} + +source "null" "local" { + ssh_host = var.ssh_host + ssh_username = var.ssh_user + ssh_password = var.ssh_password +} + +build { + sources = ["null.local"] + + provisioner "goss" { + download_path = "/tmp/goss_install" + skip_install = true + tests = ["./goss.yaml"] + remote_path = "/tmp/goss" + format = "junit" + output_file = "/tmp/goss.json" + } +} \ No newline at end of file