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:
committed by
Scott Rigby
parent
9dfa4f21be
commit
de59b1cdfb
17
Gopkg.lock
generated
17
Gopkg.lock
generated
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"common"
|
||||
],
|
||||
"helm-extra-args": "--timeout 300",
|
||||
"upgrade": true,
|
||||
"namespace": "default",
|
||||
"release-label": "release"
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@ chart-dirs:
|
||||
excluded-charts:
|
||||
- common
|
||||
helm-extra-args: --timeout 300
|
||||
upgrade: true
|
||||
namespace: default
|
||||
release-label: release
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user