From d7c06067e2ee325268c0e4d9368781b68c0bfb4f Mon Sep 17 00:00:00 2001 From: Tom Morelly Date: Sat, 29 Jun 2024 21:57:37 +1000 Subject: [PATCH] feat(output_file): add option to specify the test result output file --- .gitignore | 7 ++- Makefile | 27 +++++++++ README.md | 2 +- main.go | 8 +-- provisioner/goss/packer-provisioner-goss.go | 58 ++++++++++++++++--- .../goss/packer-provisioner-goss.hcl2spec.go | 2 + .../goss/packer-provisioner-goss_test.go | 26 ++++----- tests/goss.yaml | 3 + tests/local.pkr.hcl | 41 +++++++++++++ 9 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 Makefile create mode 100644 tests/goss.yaml create mode 100644 tests/local.pkr.hcl 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..c7f20b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +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 . + +.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 \ 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..a7bde95 100644 --- a/provisioner/goss/packer-provisioner-goss.hcl2spec.go +++ b/provisioner/goss/packer-provisioner-goss.hcl2spec.go @@ -33,6 +33,7 @@ type FlatGossConfig struct { SkipDownload *bool `mapstructure:"skip_download" cty:"skip_download" hcl:"skip_download"` Format *string `mapstructure:"format" cty:"format" hcl:"format"` FormatOptions *string `mapstructure:"format_options" cty:"format_options" hcl:"format_options"` + OutputFile *string `mapstructure:"output_file" cty:"output_file" hcl:"output_file"` } // FlatMapstructure returns a new FlatGossConfig. @@ -70,6 +71,7 @@ func (*FlatGossConfig) HCL2Spec() map[string]hcldec.Spec { "skip_download": &hcldec.AttrSpec{Name: "skip_download", Type: cty.Bool, Required: false}, "format": &hcldec.AttrSpec{Name: "format", Type: cty.String, Required: false}, "format_options": &hcldec.AttrSpec{Name: "format_options", Type: cty.String, Required: false}, + "output_file": &hcldec.AttrSpec{Name: "output_file", 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..7f9926c 100644 --- a/provisioner/goss/packer-provisioner-goss_test.go +++ b/provisioner/goss/packer-provisioner-goss_test.go @@ -27,8 +27,7 @@ func fakeContext() interpolate.Context { } func TestProvisioner_Prepare(t *testing.T) { - - var tests = []struct { + tests := []struct { name string input []interface{} wantErr bool @@ -43,10 +42,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 +64,7 @@ func TestProvisioner_Prepare(t *testing.T) { RemotePath: "/tmp/goss", Format: "", FormatOptions: "", + OutputFile: "", ctx: fakeContext(), }, }, @@ -81,10 +81,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 +105,7 @@ func TestProvisioner_Prepare(t *testing.T) { RemotePath: "/tmp/goss", Format: "", FormatOptions: "", + OutputFile: "", ctx: fakeContext(), }, }, @@ -118,10 +119,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 +141,7 @@ func TestProvisioner_Prepare(t *testing.T) { RemotePath: "/tmp/goss", Format: "", FormatOptions: "", + OutputFile: "", ctx: fakeContext(), }, }, @@ -161,13 +163,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..9a6f2c3 --- /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