From 725a6b218bceb50836bd3bc7df9d47b432df8537 Mon Sep 17 00:00:00 2001 From: David Cheung Date: Tue, 23 Mar 2021 21:39:36 -0400 Subject: [PATCH] modules can declare zero version requirements --- Makefile | 2 +- cmd/version.go | 10 +--- docs/module-definition.md | 17 +++--- go.mod | 1 + go.sum | 2 + internal/config/moduleconfig/module_config.go | 56 ++++++++++++++++++- internal/module/module_test.go | 54 ++++++++++++++++++ tests/test_data/modules/ci/zero-module.yml | 1 + version/version.go | 7 +++ 9 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 version/version.go diff --git a/Makefile b/Makefile index 020f2f8ba..06e87a943 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ VERSION = 0.0.1 BUILD ?=$(shell git rev-parse --short HEAD) PKG ?=github.com/commitdev/zero -BUILD_ARGS=-v -trimpath -ldflags=all="-X ${PKG}/cmd.appVersion=${VERSION} -X ${PKG}/cmd.appBuild=${BUILD}" +BUILD_ARGS=-v -trimpath -ldflags=all="-X ${PKG}/version.AppVersion=${VERSION} -X ${PKG}/version.AppBuild=${BUILD}" deps: go mod download diff --git a/cmd/version.go b/cmd/version.go index 4cc76ac50..abd492467 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,14 +3,10 @@ package cmd import ( "fmt" + "github.com/commitdev/zero/version" "github.com/spf13/cobra" ) -var ( - appVersion = "SNAPSHOT" - appBuild = "SNAPSHOT" -) - func init() { rootCmd.AddCommand(versionCmd) } @@ -19,7 +15,7 @@ var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number of zero", Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("version: %v\n", appVersion) - fmt.Printf("build: %v\n", appBuild) + fmt.Printf("version: %v\n", version.AppVersion) + fmt.Printf("build: %v\n", version.AppBuild) }, } diff --git a/docs/module-definition.md b/docs/module-definition.md index 53eca7a0b..d32aee4a0 100644 --- a/docs/module-definition.md +++ b/docs/module-definition.md @@ -2,14 +2,15 @@ This file is the definition of a Zero module. It contains a list of all the required parameters to be able to prompt a user for choices during `zero init`, information about how to template the contents of the module during `zero create`, and the information needed for the module to run (`zero apply`). It also declares the module's dependencies to determine the order of execution in relation to other modules. -| Parameters | type | Description | -|---------------|-----------------|--------------------------------------------------| -| `name` | string | Name of module | -| `description` | string | Description of the module | -| `template` | template | default settings for templating out the module | -| `author` | string | Author of the module | -| `icon` | string | Path to logo image | -| `parameters` | list(Parameter) | Parameters to prompt users | +| Parameters | type | Description | +|---------------|------------------|--------------------------------------------------| +| `name` | string | Name of module | +| `description` | string | Description of the module | +| `template` | template | default settings for templating out the module | +| `author` | string | Author of the module | +| `icon` | string | Path to logo image | +| `parameters` | list(Parameter) | Parameters to prompt users | +| `zeroVersion` | string(go-semver)| Zero versions its compatible with | ### Template diff --git a/go.mod b/go.mod index 04972d7d5..2f63fd44d 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/go-cmp v0.3.1 github.com/google/uuid v1.1.1 github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 + github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/terraform v0.12.26 github.com/iancoleman/strcase v0.1.2 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect diff --git a/go.sum b/go.sum index 28bd08b05..48554991f 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PF github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/internal/config/moduleconfig/module_config.go b/internal/config/moduleconfig/module_config.go index 58bc13fd1..403ec94ef 100644 --- a/internal/config/moduleconfig/module_config.go +++ b/internal/config/moduleconfig/module_config.go @@ -1,16 +1,19 @@ package moduleconfig import ( + "errors" "fmt" "io/ioutil" "log" "reflect" "strings" + goVerson "github.com/hashicorp/go-version" yaml "gopkg.in/yaml.v2" "github.com/commitdev/zero/internal/config/projectconfig" "github.com/commitdev/zero/pkg/util/flog" + "github.com/commitdev/zero/version" "github.com/iancoleman/strcase" ) @@ -20,11 +23,35 @@ type ModuleConfig struct { Author string DependsOn []string `yaml:"dependsOn,omitempty"` TemplateConfig `yaml:"template"` - RequiredCredentials []string `yaml:"requiredCredentials"` + RequiredCredentials []string `yaml:"requiredCredentials"` + ZeroVersion VersionConstraints `yaml:"zeroVersion,omitempty"` Parameters []Parameter Conditions []Condition `yaml:"conditions,omitempty"` } +func checkVersionAgainstConstrains(vc VersionConstraints, versionString string) bool { + v, err := goVerson.NewVersion(versionString) + if err != nil { + return false + } + + return vc.Check(v) +} + +// ValidateZeroVersion receives a module config, and returns whether the running zero's binary +// is compatible with the module +func ValidateZeroVersion(mc ModuleConfig) bool { + zeroVersion := version.AppVersion + flog.Debugf("Checking Zero version (%s) against %s", zeroVersion, mc.ZeroVersion) + + // Unreleased versions or test runs, defaults to SNAPSHOT when not declared + if zeroVersion == "SNAPSHOT" { + return true + } + + return checkVersionAgainstConstrains(mc.ZeroVersion, zeroVersion) +} + type Parameter struct { Field string Label string `yaml:"label,omitempty"` @@ -60,6 +87,10 @@ type TemplateConfig struct { OutputDir string `yaml:"outputDir"` } +type VersionConstraints struct { + goVerson.Constraints +} + // A "nice" wrapper around findMissing() func (cfg ModuleConfig) collectMissing() []string { var missing []string @@ -107,6 +138,10 @@ func LoadModuleConfig(filePath string) (ModuleConfig, error) { log.Fatal("") } + if !ValidateZeroVersion(config) { + constraint := config.ZeroVersion.Constraints.String() + return config, errors.New(fmt.Sprintf("Module's zero requirement not satisfied: expected %s received: %s", constraint, version.AppVersion)) + } return config, nil } @@ -214,3 +249,22 @@ func SummarizeConditions(module ModuleConfig) []projectconfig.Condition { } return moduleConditions } + +// UnmarshalYAML Parses a version constraint string into go-version constraint during yaml parsing +func (semVer *VersionConstraints) UnmarshalYAML(unmarshal func(interface{}) error) error { + var versionString string + err := unmarshal(&versionString) + if err != nil { + return err + } + + constraints, constErr := goVerson.NewConstraint(versionString) + // If an invalid constraint is declared in a module + // instead of erroring out we just print a warning message + if constErr != nil { + flog.Warnf("Zero version constraint invalid format: %s", constErr) + } + + *semVer = VersionConstraints{constraints} + return nil +} diff --git a/internal/module/module_test.go b/internal/module/module_test.go index f0f55a50a..b3de3b19d 100644 --- a/internal/module/module_test.go +++ b/internal/module/module_test.go @@ -2,12 +2,14 @@ package module_test import ( "errors" + "fmt" "testing" "github.com/commitdev/zero/internal/config/moduleconfig" "github.com/stretchr/testify/assert" "github.com/commitdev/zero/internal/module" + "github.com/commitdev/zero/version" ) func TestGetSourceDir(t *testing.T) { @@ -33,6 +35,7 @@ func TestParseModuleConfig(t *testing.T) { t.Run("Loading module from source", func(t *testing.T) { mod, _ = module.ParseModuleConfig(testModuleSource) + moduleconfig.ValidateZeroVersion(mod) assert.Equal(t, "CI templates", mod.Name) }) @@ -86,6 +89,57 @@ func TestParseModuleConfig(t *testing.T) { assert.Equal(t, []string{"<%", "%>"}, mod.TemplateConfig.Delimiters) }) + t.Run("Parsing zero version constraints", func(t *testing.T) { + moduleConstraints := mod.ZeroVersion.Constraints.String() + assert.Equal(t, ">= 3.0.0, < 4.0.0", moduleConstraints) + }) + + t.Run("Should Fail against old zero version", func(t *testing.T) { + moduleConstraints := mod.ZeroVersion.Constraints.String() + + // Mocking zero's version, testing against ">= 3.0.0, <= 4.0.0" + originalVersion := version.AppVersion + version.AppVersion = "2.0.0" + defer func() { version.AppVersion = originalVersion }() + // end of mock + + isValid := moduleconfig.ValidateZeroVersion(mod) + assert.Equal(t, false, isValid, fmt.Sprintf("Version should satisfy %s", moduleConstraints)) + }) + + t.Run("Should Fail against too new zero version", func(t *testing.T) { + moduleConstraints := mod.ZeroVersion.Constraints.String() + + // Mocking zero's version, testing against ">= 3.0.0, <= 4.0.0" + originalVersion := version.AppVersion + version.AppVersion = "4.0.0" + defer func() { version.AppVersion = originalVersion }() + // end of mock + + isValid := moduleconfig.ValidateZeroVersion(mod) + assert.Equal(t, false, isValid, fmt.Sprintf("Version should satisfy %s", moduleConstraints)) + }) + + t.Run("Should validate against valid versions", func(t *testing.T) { + moduleConstraints := mod.ZeroVersion.Constraints.String() + + // Mocking zero's version, testing against ">= 3.0.0, <= 4.0.0" + const newZeroVersion = "3.0.5" + originalVersion := version.AppVersion + version.AppVersion = newZeroVersion + defer func() { version.AppVersion = originalVersion }() + // end of mock + + isValid := moduleconfig.ValidateZeroVersion(mod) + assert.Equal(t, true, isValid, fmt.Sprintf("Version should satisfy %s", moduleConstraints)) + }) + + t.Run("default to SNAPSHOT version passes tests", func(t *testing.T) { + assert.Equal(t, "SNAPSHOT", version.AppVersion) + isValid := moduleconfig.ValidateZeroVersion(mod) + assert.Equal(t, true, isValid, "default test run should pass version constraint") + }) + } func findParameter(params []moduleconfig.Parameter, field string) (moduleconfig.Parameter, error) { diff --git a/tests/test_data/modules/ci/zero-module.yml b/tests/test_data/modules/ci/zero-module.yml index d96d17416..7e5081368 100644 --- a/tests/test_data/modules/ci/zero-module.yml +++ b/tests/test_data/modules/ci/zero-module.yml @@ -3,6 +3,7 @@ description: "CI description" author: "CI author" icon: "" thumbnail: "" +zeroVersion: ">= 3.0.0, < 4.0.0" requiredCredentials: - aws diff --git a/version/version.go b/version/version.go new file mode 100644 index 000000000..434058ccd --- /dev/null +++ b/version/version.go @@ -0,0 +1,7 @@ +package version + +// These values are overridden by makefile during build +var ( + AppVersion = "SNAPSHOT" + AppBuild = "SNAPSHOT" +)