1
0
mirror of https://github.com/helm/chart-testing.git synced 2026-02-05 09:45:14 +01:00

Test chart upgrades (#103)

* Test chart upgrades against previous version

Upgrade testing will be run if the --upgrade flag is set (default true)
and chart version increment does not indicate a breaking change
according to the SemVer 2.0 spec.

Any releases associated with previous chart versions which fail to roll
out or for which an initial `helm test` fails will be ignored.

Signed-off-by: Jacob LeGrone <git@jacob.work>

* fix(dep): add version constraint for github.com/otiai10/copy

Signed-off-by: Jacob LeGrone <git@jacob.work>

* refactor(git): checkout whole repository with worktree

Signed-off-by: Jacob LeGrone <git@jacob.work>

* Rename test* to do*

Signed-off-by: Jacob LeGrone <git@jacob.work>

* Return bool, error from BreakingChangeAllowed

Signed-off-by: Jacob LeGrone <git@jacob.work>

* Use errors.Wrapf

Co-Authored-By: jlegrone <jlegrone@users.noreply.github.com>
Signed-off-by: Jacob LeGrone <git@jacob.work>

* Explicitly disable upgrade when not install

Signed-off-by: Jacob LeGrone <git@jacob.work>
This commit is contained in:
Jacob LeGrone
2019-03-18 11:11:57 -04:00
committed by Scott Rigby
parent 9dfa4f21be
commit de59b1cdfb
20 changed files with 435 additions and 71 deletions

17
Gopkg.lock generated
View File

@@ -41,6 +41,14 @@
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:8e3bd93036b4a925fe2250d3e4f38f21cadb8ef623561cd80c3c50c114b13201"
name = "github.com/hashicorp/errwrap"
packages = ["."]
pruneopts = ""
revision = "8a6fb523712970c966eefc6b39ed2c5e74880354"
version = "v1.0.0"
[[projects]]
digest = "1:05334858a0cfb538622a066e065287f63f42bee26a7fda93a789674225057201"
name = "github.com/hashicorp/go-cleanhttp"
@@ -49,6 +57,14 @@
revision = "e8ab9daed8d1ddd2d3c4efba338fe2eeae2e4f18"
version = "v0.5.0"
[[projects]]
digest = "1:72308fdd6d5ef61106a95be7ca72349a5565809042b6426a3cfb61d99483b824"
name = "github.com/hashicorp/go-multierror"
packages = ["."]
pruneopts = ""
revision = "886a7fbe3eb1c874d46f623bfa70af45f425b3d1"
version = "v1.0.0"
[[projects]]
digest = "1:776139dc18d63ef223ffaca5d8e9a3057174890f84393d3c881e934100b66dbc"
name = "github.com/hashicorp/go-retryablehttp"
@@ -251,6 +267,7 @@
input-imports = [
"github.com/MakeNowJust/heredoc",
"github.com/Masterminds/semver",
"github.com/hashicorp/go-multierror",
"github.com/hashicorp/go-retryablehttp",
"github.com/mitchellh/go-homedir",
"github.com/pkg/errors",

View File

@@ -31,13 +31,17 @@ func newInstallCmd() *cobra.Command {
Use: "install",
Short: "Install and test a chart",
Long: heredoc.Doc(`
Run 'helm install' and ' helm test' on
Run 'helm install', 'helm test', and optionally 'helm upgrade' on
* changed charts (default)
* specific charts (--charts)
* all charts (--all)
in given chart directories.
in given chart directories. If upgrade (--upgrade) is true, then this
command will validate that 'helm test' passes for the following upgrade paths:
* previous chart revision => current chart version (if non-breaking SemVer change)
* current chart version => current chart version
Charts may have multiple custom values files matching the glob pattern
'*-values.yaml' in a directory named 'ci' in the root of the chart's
@@ -61,12 +65,15 @@ func addInstallFlags(flags *flag.FlagSet) {
flags.String("helm-extra-args", "", heredoc.Doc(`
Additional arguments for Helm. Must be passed as a single quoted string
(e.g. "--timeout 500 --tiller-namespace tiller"`))
flags.Bool("upgrade", false, heredoc.Doc(`
Whether to test an in-place upgrade of each chart from its previous revision if the
current version should not introduce a breaking change according to the SemVer spec`))
flags.String("namespace", "", heredoc.Doc(`
Namespace to install the release(s) into. If not specified, each release will be
installed in its own randomly generated namespace.`))
installed in its own randomly generated namespace`))
flags.String("release-label", "app.kubernetes.io/instance", heredoc.Doc(`
The label to be used as a selector when inspecting resources created by charts.
This is only used if namespace is specified.`))
This is only used if namespace is specified`))
}
func install(cmd *cobra.Command, args []string) {

View File

@@ -16,9 +16,10 @@ package cmd
import (
"fmt"
"github.com/MakeNowJust/heredoc"
"os"
"github.com/MakeNowJust/heredoc"
"github.com/helm/chart-testing/pkg/chart"
"github.com/helm/chart-testing/pkg/config"
"github.com/spf13/cobra"

View File

@@ -26,4 +26,4 @@ in given chart directories.
* [ct list-changed](ct_list-changed.md) - List changed charts
* [ct version](ct_version.md) - Print version information
###### Auto generated by spf13/cobra on 31-Jan-2019
###### Auto generated by spf13/cobra on 26-Feb-2019

View File

@@ -4,13 +4,17 @@ Install and test a chart
### Synopsis
Run 'helm install' and ' helm test' on
Run 'helm install', 'helm test', and optionally 'helm upgrade' on
* changed charts (default)
* specific charts (--charts)
* all charts (--all)
in given chart directories.
in given chart directories. If upgrade (--upgrade) is true, then this
command will validate that 'helm test' passes for the following upgrade paths:
* previous chart revision => current chart version (if non-breaking SemVer change)
* current chart version => current chart version
Charts may have multiple custom values files matching the glob pattern
'*-values.yaml' in a directory named 'ci' in the root of the chart's
@@ -51,15 +55,17 @@ ct install [flags]
multiple times or separate values with commas
-h, --help help for install
--namespace string Namespace to install the release(s) into. If not specified, each release will be
installed in its own randomly generated namespace.
installed in its own randomly generated namespace
--release-label string The label to be used as a selector when inspecting resources created by charts.
This is only used if namespace is specified. (default "app.kubernetes.io/instance")
This is only used if namespace is specified (default "app.kubernetes.io/instance")
--remote string The name of the Git remote used to identify changed charts (default "origin")
--target-branch string The name of the target branch used to identify changed charts (default "master")
--upgrade Whether to test an in-place upgrade of each chart from its previous revision if the
current version should not introduce a breaking change according to the SemVer spec
```
### SEE ALSO
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 31-Jan-2019
###### Auto generated by spf13/cobra on 26-Feb-2019

View File

@@ -46,11 +46,13 @@ ct lint-and-install [flags]
is searched in the current directory, '$HOME/.ct', and '/etc/ct', in
that order
--namespace string Namespace to install the release(s) into. If not specified, each release will be
installed in its own randomly generated namespace.
installed in its own randomly generated namespace
--release-label string The label to be used as a selector when inspecting resources created by charts.
This is only used if namespace is specified. (default "app.kubernetes.io/instance")
This is only used if namespace is specified (default "app.kubernetes.io/instance")
--remote string The name of the Git remote used to identify changed charts (default "origin")
--target-branch string The name of the target branch used to identify changed charts (default "master")
--upgrade Whether to test an in-place upgrade of each chart from its previous revision if the
current version should not introduce a breaking change according to the SemVer spec
--validate-chart-schema Enable schema validation of 'Chart.yaml' using Yamale (default: true) (default true)
--validate-maintainers Enable validation of maintainer account names in chart.yml (default: true).
Works for GitHub, GitLab, and Bitbucket (default true)
@@ -61,4 +63,4 @@ ct lint-and-install [flags]
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 31-Jan-2019
###### Auto generated by spf13/cobra on 26-Feb-2019

View File

@@ -65,4 +65,4 @@ ct lint [flags]
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 31-Jan-2019
###### Auto generated by spf13/cobra on 26-Feb-2019

View File

@@ -28,4 +28,4 @@ ct list-changed [flags]
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 31-Jan-2019
###### Auto generated by spf13/cobra on 26-Feb-2019

View File

@@ -20,4 +20,4 @@ ct version [flags]
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 31-Jan-2019
###### Auto generated by spf13/cobra on 26-Feb-2019

View File

@@ -34,17 +34,27 @@ import (
//
// Show returns the contents of file on the specified remote/branch.
//
// AddWorkingTree checks out the contents of the repository at a commit ref into the specified path.
//
// RemoveWorkingTree removes the working tree at the specified path.
//
// MergeBase returns the SHA1 of the merge base of commit1 and commit2.
//
// ListChangedFilesInDirs diffs commit against HEAD and returns changed files for the specified dirs.
//
// GetUrlForRemote returns the repo URL for the specified remote.
//
// ValidateRepository checks that the current working directory is a valid git repository,
// and returns nil if valid.
type Git interface {
FileExistsOnBranch(file string, remote string, branch string) bool
Show(file string, remote string, branch string) (string, error)
AddWorkingTree(path string, ref string) error
RemoveWorkingTree(path string) error
MergeBase(commit1 string, commit2 string) (string, error)
ListChangedFilesInDirs(commit string, dirs ...string) ([]string, error)
GetUrlForRemote(remote string) (string, error)
ValidateRepository() error
}
// Helm is the interface that wraps Helm operations
@@ -61,6 +71,11 @@ type Git interface {
// InstallWithValues runs `helm install` for the given chart using the specified values file.
// Pass a zero value for valuesFile in order to run install without specifying a values file.
//
// Upgrade runs `helm upgrade` against an existing release, and re-uses the previously computed values.
//
// Test runs `helm test` against an existing release. Set the cleanup argument to true in order
// to clean up test pods created by helm after the test command completes.
//
// DeleteRelease purges the specified Helm release.
type Helm interface {
Init() error
@@ -68,7 +83,8 @@ type Helm interface {
BuildDependencies(chart string) error
LintWithValues(chart string, valuesFile string) error
InstallWithValues(chart string, valuesFile string, namespace string, release string) error
Test(release string) error
Upgrade(chart string, release string) error
Test(release string, cleanup bool) error
DeleteRelease(release string)
}
@@ -161,7 +177,7 @@ type TestResult struct {
func NewTesting(config config.Configuration) Testing {
procExec := exec.NewProcessExecutor(config.Debug)
extraArgs := strings.Fields(config.HelmExtraArgs)
testing := Testing{
return Testing{
config: config,
helm: tool.NewHelm(procExec, extraArgs),
git: tool.NewGit(procExec),
@@ -171,7 +187,14 @@ func NewTesting(config config.Configuration) Testing {
directoryLister: util.DirectoryLister{},
chartUtils: util.ChartUtils{},
}
return testing
}
const ctPreviousRevisionTree = "ct_previous_revision"
// computePreviousRevisionPath converts any file or directory path to the same path in the
// previous revision's working tree.
func computePreviousRevisionPath(dir string) string {
return path.Join(ctPreviousRevisionTree, dir)
}
func (t *Testing) processCharts(action func(chart string, valuesFiles []string) TestResult) ([]TestResult, error) {
@@ -222,6 +245,23 @@ func (t *Testing) processCharts(action func(chart string, valuesFiles []string)
TestResults: results,
}
// Checkout previous chart revisions and build their dependencies
if t.config.Upgrade {
mergeBase, err := t.computeMergeBase()
if err != nil {
return results, errors.Wrap(err, "Error identifying merge base")
}
t.git.AddWorkingTree(ctPreviousRevisionTree, mergeBase)
defer t.git.RemoveWorkingTree(ctPreviousRevisionTree)
for _, chart := range charts {
if err := t.helm.BuildDependencies(computePreviousRevisionPath(chart)); err != nil {
// Only print error (don't exit) if building dependencies for previous revision fails.
fmt.Println(errors.Wrapf(err, "Error building dependencies for previous revision of chart '%s'\n", chart))
}
}
}
for _, chart := range charts {
valuesFiles := t.FindValuesFilesForCI(chart)
@@ -336,10 +376,57 @@ func (t *Testing) LintChart(chart string, valuesFiles []string) TestResult {
// InstallChart installs the specified chart into a new namespace, waits for resources to become ready, and eventually
// uninstalls it and deletes the namespace again.
func (t *Testing) InstallChart(chart string, valuesFiles []string) TestResult {
fmt.Printf("Installing chart '%s'...\n", chart)
var result TestResult
if t.config.Upgrade {
// Test upgrade from previous version
result = t.UpgradeChart(chart)
if result.Error != nil {
return result
}
// Test upgrade of current version (related: https://github.com/helm/chart-testing/issues/19)
if err := t.doUpgrade(chart, chart, true); err != nil {
result.Error = err
return result
}
}
result = TestResult{Chart: chart}
if err := t.doInstall(chart); err != nil {
result.Error = err
}
return result
}
// UpgradeChart tests in-place upgrades of the specified chart relative to its previous revisions. If the
// initial install or helm test of a previous revision of the chart fails, that release is ignored and no
// error will be returned. If the latest revision of the chart introduces a potentially breaking change
// according to the SemVer specification, upgrade testing will be skipped.
func (t *Testing) UpgradeChart(chart string) TestResult {
result := TestResult{Chart: chart}
breakingChangeAllowed, err := t.checkBreakingChangeAllowed(chart)
if breakingChangeAllowed {
if err != nil {
fmt.Println(errors.Wrap(err, fmt.Sprintf("Skipping upgrade test of '%s' because", chart)))
}
return result
} else if err != nil {
fmt.Printf("Error comparing chart versions for '%s'\n", chart)
result.Error = err
return result
}
result.Error = t.doUpgrade(computePreviousRevisionPath(chart), chart, false)
return result
}
func (t *Testing) doInstall(chart string) error {
fmt.Printf("Installing chart '%s'...\n", chart)
valuesFiles := t.FindValuesFilesForCI(chart)
// Test with defaults if no values files are specified.
if len(valuesFiles) == 0 {
valuesFiles = append(valuesFiles, "")
@@ -349,45 +436,104 @@ func (t *Testing) InstallChart(chart string, valuesFiles []string) TestResult {
if valuesFile != "" {
fmt.Printf("\nInstalling chart with values file '%s'...\n\n", valuesFile)
}
var namespace, release, releaseSelector string
// Use anonymous function. Otherwise deferred calls would pile up
// and be executed in reverse order after the loop.
fun := func() error {
if t.config.Namespace != "" {
namespace = t.config.Namespace
release, _ = util.CreateInstallParams(chart, t.config.BuildId)
releaseSelector = fmt.Sprintf("%s=%s", t.config.ReleaseLabel, release)
} else {
release, namespace = util.CreateInstallParams(chart, t.config.BuildId)
defer t.kubectl.DeleteNamespace(namespace)
}
defer t.helm.DeleteRelease(release)
defer t.PrintPodDetailsAndLogs(namespace, releaseSelector)
namespace, release, releaseSelector, cleanup := t.generateInstallConfig(chart)
defer cleanup()
if err := t.helm.InstallWithValues(chart, valuesFile, namespace, release); err != nil {
result.Error = err
return err
}
if err := t.kubectl.WaitForDeployments(namespace, releaseSelector); err != nil {
result.Error = err
return err
}
if err := t.helm.Test(release); err != nil {
result.Error = err
return err
}
return nil
return t.testRelease(release, namespace, releaseSelector, false)
}
if err := fun(); err != nil {
break
return err
}
}
return result
return nil
}
func (t *Testing) doUpgrade(oldChart, newChart string, oldChartMustPass bool) error {
fmt.Printf("Testing upgrades of chart '%s' relative to previous revision '%s'...\n", newChart, oldChart)
valuesFiles := t.FindValuesFilesForCI(oldChart)
if len(valuesFiles) == 0 {
valuesFiles = append(valuesFiles, "")
}
for _, valuesFile := range valuesFiles {
if valuesFile != "" {
fmt.Printf("\nInstalling chart '%s' with values file '%s'...\n\n", oldChart, valuesFile)
}
// Use anonymous function. Otherwise deferred calls would pile up
// and be executed in reverse order after the loop.
fun := func() error {
namespace, release, releaseSelector, cleanup := t.generateInstallConfig(oldChart)
defer cleanup()
// Install previous version of chart. If installation fails, ignore this release.
if err := t.helm.InstallWithValues(oldChart, valuesFile, namespace, release); err != nil {
if oldChartMustPass {
return err
}
fmt.Println(errors.Wrap(err, fmt.Sprintf("Upgrade testing for release '%s' skipped because of previous revision installation error", release)))
return nil
}
if err := t.testRelease(release, namespace, releaseSelector, true); err != nil {
if oldChartMustPass {
return err
}
fmt.Println(errors.Wrap(err, fmt.Sprintf("Upgrade testing for release '%s' skipped because of previous revision testing error", release)))
return nil
}
if err := t.helm.Upgrade(oldChart, release); err != nil {
return err
}
return t.testRelease(release, namespace, releaseSelector, false)
}
if err := fun(); err != nil {
return err
}
}
return nil
}
func (t *Testing) testRelease(release, namespace, releaseSelector string, cleanupHelmTests bool) error {
if err := t.kubectl.WaitForDeployments(namespace, releaseSelector); err != nil {
return err
}
if err := t.helm.Test(release, cleanupHelmTests); err != nil {
return err
}
return nil
}
func (t *Testing) generateInstallConfig(chart string) (namespace, release, releaseSelector string, cleanup func()) {
if t.config.Namespace != "" {
namespace = t.config.Namespace
release, _ = util.CreateInstallParams(chart, t.config.BuildId)
releaseSelector = fmt.Sprintf("%s=%s", t.config.ReleaseLabel, release)
cleanup = func() {
t.PrintPodDetailsAndLogs(namespace, releaseSelector)
t.helm.DeleteRelease(release)
}
} else {
release, namespace = util.CreateInstallParams(chart, t.config.BuildId)
cleanup = func() {
t.PrintPodDetailsAndLogs(namespace, releaseSelector)
t.helm.DeleteRelease(release)
t.kubectl.DeleteNamespace(namespace)
}
}
return
}
// LintAndInstallChart first lints and then installs the specified chart.
@@ -418,15 +564,24 @@ func (t *Testing) FindValuesFilesForCI(chart string) []string {
return matches
}
func (t *Testing) computeMergeBase() (string, error) {
err := t.git.ValidateRepository()
if err != nil {
return "", fmt.Errorf("Must be in a git repository")
}
return t.git.MergeBase(fmt.Sprintf("%s/%s", t.config.Remote, t.config.TargetBranch), "HEAD")
}
// ComputeChangedChartDirectories takes the merge base of HEAD and the configured remote and target branch and computes a
// slice of changed charts from that in the configured chart directories excluding those configured to be excluded.
func (t *Testing) ComputeChangedChartDirectories() ([]string, error) {
cfg := t.config
mergeBase, err := t.git.MergeBase(fmt.Sprintf("%s/%s", cfg.Remote, cfg.TargetBranch), "HEAD")
mergeBase, err := t.computeMergeBase()
if err != nil {
return nil, errors.Wrap(err, "Error identifying merge base")
return nil, err
}
allChangedChartFiles, err := t.git.ListChangedFilesInDirs(mergeBase, cfg.ChartDirs...)
if err != nil {
return nil, errors.Wrap(err, "Error creating diff")
@@ -509,6 +664,24 @@ func (t *Testing) CheckVersionIncrement(chart string) error {
return nil
}
func (t *Testing) checkBreakingChangeAllowed(chart string) (allowed bool, err error) {
oldVersion, err := t.GetOldChartVersion(chart)
if err != nil {
return false, err
}
if oldVersion == "" {
// new chart, skip upgrade check
return true, fmt.Errorf("chart has no previous revision")
}
newVersion, err := t.GetNewChartVersion(chart)
if err != nil {
return false, err
}
return util.BreakingChangeAllowed(oldVersion, newVersion)
}
// GetOldChartVersion gets the version of the old Chart.yaml file from the target branch.
func (t *Testing) GetOldChartVersion(chart string) (string, error) {
cfg := t.config

View File

@@ -51,10 +51,22 @@ func (g fakeGit) ListChangedFilesInDirs(commit string, dirs ...string) ([]string
}, nil
}
func (g fakeGit) AddWorkingTree(path string, ref string) error {
return nil
}
func (g fakeGit) RemoveWorkingTree(path string) error {
return nil
}
func (g fakeGit) GetUrlForRemote(remote string) (string, error) {
return "git@github.com/helm/chart-testing", nil
}
func (g fakeGit) ValidateRepository() error {
return nil
}
type fakeAccountValidator struct{}
func (v fakeAccountValidator) Validate(repoDomain string, account string) error {
@@ -86,7 +98,10 @@ func (h fakeHelm) LintWithValues(chart string, valuesFile string) error { return
func (h fakeHelm) InstallWithValues(chart string, valuesFile string, namespace string, release string) error {
return nil
}
func (h fakeHelm) Test(release string) error {
func (h fakeHelm) Upgrade(chart string, release string) error {
return nil
}
func (h fakeHelm) Test(release string, cleanup bool) error {
return nil
}
func (h fakeHelm) DeleteRelease(release string) {}
@@ -99,9 +114,12 @@ func init() {
ChartDirs: []string{"test_charts", "."},
}
fakeMockLinter := new(fakeLinter)
ct = newTestingMock(cfg)
}
ct = Testing{
func newTestingMock(cfg config.Configuration) Testing {
fakeMockLinter := new(fakeLinter)
return Testing{
config: cfg,
directoryLister: util.DirectoryLister{},
git: fakeGit{},
@@ -274,3 +292,55 @@ func TestLintYamlValidation(t *testing.T) {
runTests(true, 2, 0)
runTests(false, 0, 0)
}
func TestGenerateInstallConfig(t *testing.T) {
type testData struct {
name string
cfg config.Configuration
chartDir string
}
testCases := []testData{
{
"custom namespace",
config.Configuration{
Namespace: "default",
ReleaseLabel: "app.kubernetes.io/instance",
},
"test_charts/bar",
},
{
"random namespace",
config.Configuration{
ReleaseLabel: "app.kubernetes.io/instance",
},
"test_charts/bar",
},
{
"long chart name",
config.Configuration{
ReleaseLabel: "app.kubernetes.io/instance",
},
"test_charts/barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar",
},
}
for _, testData := range testCases {
t.Run(testData.name, func(t *testing.T) {
ct := newTestingMock(testData.cfg)
namespace, release, releaseSelector, _ := ct.generateInstallConfig(testData.chartDir)
assert.NotEqual(t, "", namespace)
assert.NotEqual(t, "", release)
assert.True(t, len(release) < 64, "release should be less than 64 chars")
assert.True(t, len(namespace) < 64, "namespace should be less than 64 chars")
if testData.cfg.Namespace != "" {
assert.Equal(t, testData.cfg.Namespace, namespace)
assert.Equal(t, fmt.Sprintf("%s=%s", testData.cfg.ReleaseLabel, release), releaseSelector)
} else {
assert.Equal(t, "", releaseSelector)
assert.Contains(t, namespace, release)
}
})
}
}

View File

@@ -56,6 +56,7 @@ type Configuration struct {
HelmExtraArgs string `mapstructure:"helm-extra-args"`
HelmRepoExtraArgs []string `mapstructure:"helm-repo-extra-args"`
Debug bool `mapstructure:"debug"`
Upgrade bool `mapstructure:"upgrade"`
Namespace string `mapstructure:"namespace"`
ReleaseLabel string `mapstructure:"release-label"`
}
@@ -97,6 +98,9 @@ func LoadConfiguration(cfgFile string, cmd *cobra.Command, printConfig bool) (*C
}
}
isLint := strings.Contains(cmd.Use, "lint")
isInstall := strings.Contains(cmd.Use, "install")
cfg := &Configuration{}
if err := v.Unmarshal(cfg); err != nil {
return nil, errors.Wrap(err, "Error unmarshaling configuration")
@@ -110,7 +114,13 @@ func LoadConfiguration(cfgFile string, cmd *cobra.Command, printConfig bool) (*C
return nil, errors.New("specifying '--namespace' without '--release-label' is not allowed")
}
isLint := strings.Contains(cmd.Use, "lint")
// Disable upgrade (this does some expensive dependency building on previous revisions)
// when neither "install" nor "lint-and-install" have not been specified.
cfg.Upgrade = isInstall && cfg.Upgrade
if (cfg.TargetBranch == "" || cfg.Remote == "") && cfg.Upgrade {
return nil, errors.New("specifying '--upgrade=true' without '--target-branch' or '--remote', is not allowed")
}
chartYamlSchemaPath := cfg.ChartYamlSchema
if chartYamlSchemaPath == "" {
var err error

View File

@@ -30,7 +30,9 @@ func TestUnmarshalJson(t *testing.T) {
}
func loadAndAssertConfigFromFile(t *testing.T, configFile string) {
cfg, _ := LoadConfiguration(configFile, &cobra.Command{}, true)
cfg, _ := LoadConfiguration(configFile, &cobra.Command{
Use: "install",
}, true)
require.Equal(t, "origin", cfg.Remote)
require.Equal(t, "master", cfg.TargetBranch)
@@ -47,6 +49,7 @@ func loadAndAssertConfigFromFile(t *testing.T, configFile string) {
require.Equal(t, []string{"stable", "incubator"}, cfg.ChartDirs)
require.Equal(t, []string{"common"}, cfg.ExcludedCharts)
require.Equal(t, "--timeout 300", cfg.HelmExtraArgs)
require.Equal(t, true, cfg.Upgrade)
require.Equal(t, "default", cfg.Namespace)
require.Equal(t, "release", cfg.ReleaseLabel)
}

View File

@@ -24,6 +24,7 @@
"common"
],
"helm-extra-args": "--timeout 300",
"upgrade": true,
"namespace": "default",
"release-label": "release"
}

View File

@@ -19,5 +19,6 @@ chart-dirs:
excluded-charts:
- common
helm-extra-args: --timeout 300
upgrade: true
namespace: default
release-label: release

View File

@@ -38,6 +38,14 @@ func (g Git) FileExistsOnBranch(file string, remote string, branch string) bool
return err == nil
}
func (g Git) AddWorkingTree(path string, ref string) error {
return g.exec.RunProcess("git", "worktree", "add", path, ref)
}
func (g Git) RemoveWorkingTree(path string) error {
return g.exec.RunProcess("git", "worktree", "remove", path)
}
func (g Git) Show(file string, remote string, branch string) (string, error) {
fileSpec := fmt.Sprintf("%s/%s:%s", remote, branch, file)
return g.exec.RunProcessAndCaptureOutput("git", "show", fileSpec)
@@ -62,3 +70,7 @@ func (g Git) ListChangedFilesInDirs(commit string, dirs ...string) ([]string, er
func (g Git) GetUrlForRemote(remote string) (string, error) {
return g.exec.RunProcessAndCaptureOutput("git", "ls-remote", "--get-url", remote)
}
func (g Git) ValidateRepository() error {
return g.exec.RunProcess("git", "rev-parse", "--is-inside-work-tree")
}

View File

@@ -67,8 +67,17 @@ func (h Helm) InstallWithValues(chart string, valuesFile string, namespace strin
return nil
}
func (h Helm) Test(release string) error {
return h.exec.RunProcess("helm", "test", release, h.extraArgs)
func (h Helm) Upgrade(chart string, release string) error {
if err := h.exec.RunProcess("helm", "upgrade", release, chart, "--reuse-values",
"--wait", h.extraArgs); err != nil {
return err
}
return nil
}
func (h Helm) Test(release string, cleanup bool) error {
return h.exec.RunProcess("helm", "test", release, h.extraArgs, fmt.Sprintf("--cleanup=%t", cleanup))
}
func (h Helm) DeleteRelease(release string) {

View File

@@ -151,21 +151,16 @@ func (k Kubectl) WaitForDeployments(namespace string, selector string) error {
// 'kubectl rollout status' does not return a non-zero exit code when rollouts fail.
// We, thus, need to double-check here.
pods, err := k.GetPodsforDeployment(namespace, deployment)
//
// Just after rollout, pods from the previous deployment revision may still be in a
// terminating state.
unavailable, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "deployment", deployment, "--namespace", namespace, "--output",
`jsonpath={.status.unavailableReplicas}`)
if err != nil {
return err
}
for _, pod := range pods {
pod = strings.Trim(pod, "'")
ready, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "pod", pod, "--namespace", namespace, "--output",
`jsonpath={.status.conditions[?(@.type=="Ready")].status}`)
if err != nil {
return err
}
if ready != "True" {
return errors.New(fmt.Sprintf("Pods '%s' did not reach ready state!", pod))
}
if unavailable != "" && unavailable != "0" {
return fmt.Errorf("%s replicas unavailable", unavailable)
}
}

View File

@@ -16,9 +16,6 @@ package util
import (
"fmt"
"github.com/Masterminds/semver"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"io/ioutil"
"math/rand"
"net"
@@ -27,6 +24,11 @@ import (
"path/filepath"
"strings"
"time"
"github.com/Masterminds/semver"
multierror "github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
const chars = "1234567890abcdefghijklmnopqrstuvwxyz"
@@ -177,6 +179,33 @@ func CompareVersions(left string, right string) (int, error) {
return leftVersion.Compare(rightVersion), nil
}
func BreakingChangeAllowed(left string, right string) (bool, error) {
leftVersion, err := semver.NewVersion(left)
if err != nil {
return false, errors.Wrap(err, "Error parsing semantic version")
}
rightVersion, err := semver.NewVersion(right)
if err != nil {
return false, errors.Wrap(err, "Error parsing semantic version")
}
constraintOp := "^"
if leftVersion.Major() == 0 {
constraintOp = "~"
}
c, err := semver.NewConstraint(fmt.Sprintf("%s %s", constraintOp, leftVersion.String()))
if err != nil {
return false, errors.Wrap(err, "Error parsing semantic version constraint")
}
minor, reasons := c.Validate(rightVersion)
if len(reasons) > 0 {
err = multierror.Append(err, reasons...)
}
return !minor, err
}
func CreateInstallParams(chart string, buildId string) (release string, namespace string) {
release = path.Base(chart)
namespace = release

View File

@@ -16,8 +16,9 @@ package util
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFlatten(t *testing.T) {
@@ -88,3 +89,30 @@ func TestTruncateLeft(t *testing.T) {
})
}
}
func TestBreakingChangeAllowed(t *testing.T) {
var testDataSlice = []struct {
left string
right string
breaking bool
}{
{"0.1.0", "0.1.0", false},
{"0.1.0", "0.1.1", false},
{"0.1.0", "0.2.0", true},
{"0.1.0", "0.2.1", true},
{"1.2.3", "1.2.3", false},
{"1.2.3", "1.2.4", false},
{"1.2.3", "1.3.0", false},
{"1.2.3", "2.0.0", true},
{"1.2.3", "10.0.0", true},
{"foo", "1.0.0", false}, // version parse error
{"1.0.0", "bar", false}, // version parse error
}
for index, testData := range testDataSlice {
t.Run(string(index), func(t *testing.T) {
actual, _ := BreakingChangeAllowed(testData.left, testData.right)
assert.Equal(t, testData.breaking, actual, fmt.Sprintf("input: %s,%s\n", testData.left, testData.right))
})
}
}