Skip to content

Commit

Permalink
Version requirements (#357)
Browse files Browse the repository at this point in the history
* modules can declare zero version requirements
  • Loading branch information
davidcheung authored Mar 25, 2021
1 parent 40cc108 commit 8fb6adb
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ builds:
- env:
- CGO_ENABLED=0
ldflags:
- -X github.com/commitdev/zero/cmd.appVersion={{.Version}} -X github.com/commitdev/zero/cmd.appBuild={{.ShortCommit}}
- -X github.com/commitdev/zero/version.AppVersion={{.Version}} -X github.com/commitdev/zero/version.AppBuild={{.ShortCommit}}
archives:
- replacements:
darwin: Darwin
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 3 additions & 7 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
},
}
19 changes: 11 additions & 8 deletions docs/module-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,3 +78,5 @@ For example if a user decide to not use circleCI, condition can be used to skip
| `type` | enum(string) | Currently supports [`regex`] |
| `value` | string | Regular expression string |
| `errorMessage` | string | Error message when validation fails |

[go-semver]: https://github.com/hashicorp/go-version/blob/master/README.md
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
65 changes: 64 additions & 1 deletion internal/config/moduleconfig/module_config.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
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/internal/constants"
"github.com/commitdev/zero/pkg/util/flog"
"github.com/commitdev/zero/version"
"github.com/iancoleman/strcase"
)

Expand All @@ -20,11 +24,39 @@ 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 {
if mc.ZeroVersion.String() == "" {
return true
}

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"`
Expand Down Expand Up @@ -60,6 +92,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
Expand Down Expand Up @@ -107,6 +143,13 @@ func LoadModuleConfig(filePath string) (ModuleConfig, error) {
log.Fatal("")
}

if !ValidateZeroVersion(config) {
constraint := config.ZeroVersion.Constraints.String()
errTpl := `Module(%s) requires Zero to be version %s. Your current Zero version is: %s
Please update your Zero version to %s.
Please check %s for available releases.`
return config, errors.New(fmt.Sprintf(errTpl, config.Name, constraint, version.AppVersion, constraint, constants.ZeroReleaseURL))
}
return config, nil
}

Expand Down Expand Up @@ -214,3 +257,23 @@ 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
}
if versionString != "" {
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
}
1 change: 1 addition & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ const (
MaxPnameLength = 16
RegexValidation = "regex"
FunctionValidation = "function"
ZeroReleaseURL = "https://github.com/commitdev/zero/releases"
)
71 changes: 71 additions & 0 deletions internal/module/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
})
Expand Down Expand Up @@ -86,6 +89,74 @@ 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 TestModuleWithNoVersionConstraint(t *testing.T) {
testModuleSource := "../../tests/test_data/modules/no-version-constraint"
var mod moduleconfig.ModuleConfig
var err error

t.Run("Parsing Module with no version constraint", func(t *testing.T) {
mod, err = module.ParseModuleConfig(testModuleSource)
assert.Equal(t, "", mod.ZeroVersion.String())
assert.Nil(t, err)
})

t.Run("Should pass Validation if constraint not specified", func(t *testing.T) {
isValid := moduleconfig.ValidateZeroVersion(mod)
assert.Equal(t, true, isValid, "Module with no constraint should pass version validation")
})
}

func findParameter(params []moduleconfig.Parameter, field string) (moduleconfig.Parameter, error) {
Expand Down
1 change: 1 addition & 0 deletions tests/test_data/modules/ci/zero-module.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: "CI description"
author: "CI author"
icon: ""
thumbnail: ""
zeroVersion: ">= 3.0.0, < 4.0.0"

requiredCredentials:
- aws
Expand Down
19 changes: 19 additions & 0 deletions tests/test_data/modules/no-version-constraint/zero-module.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: "Test module"
description: "a module for testing, with no zero version requirement"
author: "Test module author"
icon: ""
thumbnail: ""

template:
delimiters:
- "<%"
- "%>"
inputDir: templates
outputDir: test-module-output

requiredCredentials:
- aws
- circleci
- github

parameters:
7 changes: 7 additions & 0 deletions version/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package version

// These values are overridden by makefile during build
var (
AppVersion = "SNAPSHOT"
AppBuild = "SNAPSHOT"
)

0 comments on commit 8fb6adb

Please sign in to comment.