From 228603701ae9138c3c39abea601be4a8ff18709e Mon Sep 17 00:00:00 2001 From: Jacob LeGrone Date: Fri, 14 Dec 2018 09:30:36 -0500 Subject: [PATCH] Support installation to existing namespace (#59) This PR adds two flags to the `install` command: `namespace` and `release-label`. If `namespace` is specified, releases will target that namespace and `release-label` will be used to select deployments and pods for readiness and reading log output. Fixes #34 --- app/cmd/install.go | 13 +++-- doc/ct.md | 2 +- doc/ct_install.md | 8 +++- doc/ct_lint-and-install.md | 8 +++- doc/ct_lint.md | 2 +- doc/ct_version.md | 2 +- pkg/chart/chart.go | 95 +++++++++++++++++++++---------------- pkg/chart/chart_test.go | 29 ++++++----- pkg/config/config.go | 11 ++++- pkg/config/config_test.go | 5 +- pkg/config/test_config.json | 4 +- pkg/config/test_config.yaml | 2 + pkg/tool/helm.go | 24 ++++------ pkg/tool/kubectl.go | 4 +- 14 files changed, 125 insertions(+), 84 deletions(-) diff --git a/app/cmd/install.go b/app/cmd/install.go index 27c1ca34..397e098c 100644 --- a/app/cmd/install.go +++ b/app/cmd/install.go @@ -16,9 +16,10 @@ package cmd import ( "fmt" - "github.com/spf13/viper" "os" + "github.com/spf13/viper" + "github.com/MakeNowJust/heredoc" "github.com/helm/chart-testing/pkg/chart" "github.com/helm/chart-testing/pkg/config" @@ -61,7 +62,13 @@ func addInstallFlags(flags *flag.FlagSet) { the ID of a pull request. If not specified, the name of the chart is used`)) 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"`)) + (e.g. "--timeout 500 --tiller-namespace tiller"`)) + 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.`)) + 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.`)) } func install(cmd *cobra.Command, args []string) { @@ -89,6 +96,6 @@ func install(cmd *cobra.Command, args []string) { } func bindInstallFlags(flagSet *flag.FlagSet, v *viper.Viper) error { - options := []string{"build-id", "helm-extra-args"} + options := []string{"build-id", "helm-extra-args", "namespace", "release-label"} return bindFlags(options, flagSet, v) } diff --git a/doc/ct.md b/doc/ct.md index dac28c02..4de1db1f 100644 --- a/doc/ct.md +++ b/doc/ct.md @@ -25,4 +25,4 @@ in given chart directories. * [ct lint-and-install](ct_lint-and-install.md) - Lint, install, and test a chart * [ct version](ct_version.md) - Print version information -###### Auto generated by spf13/cobra on 6-Nov-2018 +###### Auto generated by spf13/cobra on 17-Nov-2018 diff --git a/doc/ct_install.md b/doc/ct_install.md index 51415537..36557115 100644 --- a/doc/ct_install.md +++ b/doc/ct_install.md @@ -43,8 +43,12 @@ ct install [flags] --excluded-charts strings Charts that should be skipped. May be specified multiple times or separate values with commas --helm-extra-args string Additional arguments for Helm. Must be passed as a single quoted string - (e. g. "--timeout 500 --tiller-namespace tiller" + (e.g. "--timeout 500 --tiller-namespace tiller" -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. + --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") --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") ``` @@ -53,4 +57,4 @@ ct install [flags] * [ct](ct.md) - The Helm chart testing tool -###### Auto generated by spf13/cobra on 6-Nov-2018 +###### Auto generated by spf13/cobra on 17-Nov-2018 diff --git a/doc/ct_lint-and-install.md b/doc/ct_lint-and-install.md index f8a97b8f..4035bfb6 100644 --- a/doc/ct_lint-and-install.md +++ b/doc/ct_lint-and-install.md @@ -35,11 +35,15 @@ ct lint-and-install [flags] --excluded-charts strings Charts that should be skipped. May be specified multiple times or separate values with commas --helm-extra-args string Additional arguments for Helm. Must be passed as a single quoted string - (e. g. "--timeout 500 --tiller-namespace tiller" + (e.g. "--timeout 500 --tiller-namespace tiller" -h, --help help for lint-and-install --lint-conf string The config file for YAML linting. If not specified, 'lintconf.yaml' 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. + --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") --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") --validate-maintainers Enabled validation of maintainer account names in chart.yml (default: true). @@ -50,4 +54,4 @@ ct lint-and-install [flags] * [ct](ct.md) - The Helm chart testing tool -###### Auto generated by spf13/cobra on 6-Nov-2018 +###### Auto generated by spf13/cobra on 17-Nov-2018 diff --git a/doc/ct_lint.md b/doc/ct_lint.md index c6f939a3..f1bd5c6c 100644 --- a/doc/ct_lint.md +++ b/doc/ct_lint.md @@ -58,4 +58,4 @@ ct lint [flags] * [ct](ct.md) - The Helm chart testing tool -###### Auto generated by spf13/cobra on 6-Nov-2018 +###### Auto generated by spf13/cobra on 17-Nov-2018 diff --git a/doc/ct_version.md b/doc/ct_version.md index f02c4eba..a8c6f68e 100644 --- a/doc/ct_version.md +++ b/doc/ct_version.md @@ -20,4 +20,4 @@ ct version [flags] * [ct](ct.md) - The Helm chart testing tool -###### Auto generated by spf13/cobra on 6-Nov-2018 +###### Auto generated by spf13/cobra on 17-Nov-2018 diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go index 8728e52f..10425679 100644 --- a/pkg/chart/chart.go +++ b/pkg/chart/chart.go @@ -16,11 +16,12 @@ package chart import ( "fmt" - "github.com/helm/chart-testing/pkg/exec" "path" "path/filepath" "strings" + "github.com/helm/chart-testing/pkg/exec" + "github.com/helm/chart-testing/pkg/config" "github.com/helm/chart-testing/pkg/tool" "github.com/helm/chart-testing/pkg/util" @@ -54,23 +55,20 @@ type Git interface { // // BuildDependencies builds the chart's dependencies // -// Lint runs `helm lint` for the given chart -// -// LintWithValues runs `helm lint` for the given chart using the specified values file -// -// Install runs `helm install` for the given chart +// LintWithValues runs `helm lint` for the given chart using the specified values file. +// Pass a zero value for valuesFile in order to run lint without specifying a values file. // -// InstallWithValues runs `helm install` for the given chart using the specified values file +// 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. // // DeleteRelease purges the specified Helm release. type Helm interface { Init() error AddRepo(name string, url string) error BuildDependencies(chart string) error - Lint(chart string) error LintWithValues(chart string, valuesFile string) error - Install(chart string, namespace string, release string) error InstallWithValues(chart string, valuesFile string, namespace string, release string) error + Test(release string) error DeleteRelease(release string) } @@ -93,7 +91,7 @@ type Helm interface { // GetContainers gets all containers of pod type Kubectl interface { DeleteNamespace(namespace string) - WaitForDeployments(namespace string) error + WaitForDeployments(namespace string, selector string) error GetPodsforDeployment(namespace string, deployment string) ([]string, error) GetPods(args ...string) ([]string, error) DescribePod(namespace string, pod string) error @@ -112,7 +110,7 @@ type Linter interface { Yamale(yamlFile string, schemaFile string) error } -// DiretoryLister is the interface +// DirectoryLister is the interface // // ListChildDirs lists direct child directories of parentDir given they pass the test function type DirectoryLister interface { @@ -162,13 +160,12 @@ type TestResult struct { // NewTesting creates a new Testing struct with the given config. func NewTesting(config config.Configuration) Testing { procExec := exec.NewProcessExecutor(config.Debug) - kubectl := tool.NewKubectl(procExec) extraArgs := strings.Fields(config.HelmExtraArgs) testing := Testing{ config: config, - helm: tool.NewHelm(procExec, kubectl, extraArgs), + helm: tool.NewHelm(procExec, extraArgs), git: tool.NewGit(procExec), - kubectl: kubectl, + kubectl: tool.NewKubectl(procExec), linter: tool.NewLinter(procExec), accountValidator: tool.AccountValidator{}, directoryLister: util.DirectoryLister{}, @@ -244,7 +241,7 @@ func (t *Testing) InstallCharts() ([]TestResult, error) { return t.processCharts(t.InstallChart) } -// LintAndInstallChart first lints and then installs charts (changed, all, specific) depending on the configuration. +// LintAndInstallCharts first lints and then installs charts (changed, all, specific) depending on the configuration. func (t *Testing) LintAndInstallCharts() ([]TestResult, error) { return t.processCharts(t.LintAndInstallChart) } @@ -303,16 +300,15 @@ func (t *Testing) LintChart(chart string, valuesFiles []string) TestResult { } } - if len(valuesFiles) > 0 { - for _, valuesFile := range valuesFiles { - if err := t.helm.LintWithValues(chart, valuesFile); err != nil { - result.Error = err - break - } - } - } else { - if err := t.helm.Lint(chart); err != nil { + // Lint with defaults if no values files are specified. + if len(valuesFiles) == 0 { + valuesFiles = append(valuesFiles, "") + } + + for _, valuesFile := range valuesFiles { + if err := t.helm.LintWithValues(chart, valuesFile); err != nil { result.Error = err + break } } @@ -326,28 +322,37 @@ func (t *Testing) InstallChart(chart string, valuesFiles []string) TestResult { result := TestResult{Chart: chart} - if len(valuesFiles) > 0 { - for _, valuesFile := range valuesFiles { - release, namespace := util.CreateInstallParams(chart, t.config.BuildId) + // Test with defaults if no values files are specified. + if len(valuesFiles) == 0 { + valuesFiles = append(valuesFiles, "") + } - defer t.kubectl.DeleteNamespace(namespace) - defer t.helm.DeleteRelease(release) - defer t.PrintPodDetailsAndLogs(namespace) + for _, valuesFile := range valuesFiles { + var namespace, release, releaseSelector string - if err := t.helm.InstallWithValues(chart, valuesFile, namespace, release); err != nil { - result.Error = err - break - } + 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) } - } else { - release, namespace := util.CreateInstallParams(chart, t.config.BuildId) - defer t.kubectl.DeleteNamespace(namespace) defer t.helm.DeleteRelease(release) - defer t.PrintPodDetailsAndLogs(namespace) + defer t.PrintPodDetailsAndLogs(namespace, releaseSelector) - if err := t.helm.Install(chart, namespace, release); err != nil { + if err := t.helm.InstallWithValues(chart, valuesFile, namespace, release); err != nil { + result.Error = err + break + } + if err := t.kubectl.WaitForDeployments(namespace, releaseSelector); err != nil { + result.Error = err + break + } + if err := t.helm.Test(release); err != nil { result.Error = err + break } } @@ -533,8 +538,16 @@ func (t *Testing) ValidateMaintainers(chart string) error { return nil } -func (t *Testing) PrintPodDetailsAndLogs(namespace string) { - pods, err := t.kubectl.GetPods("--no-headers", "--namespace", namespace, "--output", "jsonpath={.items[*].metadata.name}") +func (t *Testing) PrintPodDetailsAndLogs(namespace string, selector string) { + pods, err := t.kubectl.GetPods( + "--no-headers", + "--namespace", + namespace, + "--selector", + selector, + "--output", + "jsonpath={.items[*].metadata.name}", + ) if err != nil { fmt.Println("Error printing logs:", err) return diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go index 15954cac..f9ca9e7d 100644 --- a/pkg/chart/chart_test.go +++ b/pkg/chart/chart_test.go @@ -97,17 +97,20 @@ func (v fakeAccountValidator) Validate(repoDomain string, account string) error type fakeLinter struct{} func (l fakeLinter) YamlLint(yamlFile, configFile string) error { return nil } -func (l fakeLinter) Yamale(yamlFile, schemaFile string) error { return nil } +func (l fakeLinter) Yamale(yamlFile, schemaFile string) error { return nil } type fakeHelm struct{} -func (h fakeHelm) Init() error { return nil } -func (h fakeHelm) AddRepo(name, url string) error { return nil } -func (h fakeHelm) BuildDependencies(chart string) error { return nil } -func (h fakeHelm) Lint(chart string) error { return nil } +func (h fakeHelm) Init() error { return nil } +func (h fakeHelm) AddRepo(name, url string) error { return nil } +func (h fakeHelm) BuildDependencies(chart string) error { return nil } func (h fakeHelm) LintWithValues(chart string, valuesFile string) error { return nil } -func (h fakeHelm) Install(chart string, namespace string, release string) error { return nil } -func (h fakeHelm) InstallWithValues(chart string, valuesFile string, namespace string, release string) error { return nil } +func (h fakeHelm) InstallWithValues(chart string, valuesFile string, namespace string, release string) error { + return nil +} +func (h fakeHelm) Test(release string) error { + return nil +} func (h fakeHelm) DeleteRelease(release string) {} var ct Testing @@ -123,8 +126,8 @@ func init() { git: fakeGit{}, chartUtils: fakeChartUtils{}, accountValidator: fakeAccountValidator{}, - linter: fakeLinter{}, - helm: fakeHelm{}, + linter: fakeLinter{}, + helm: fakeHelm{}, } } @@ -166,9 +169,9 @@ func TestValidateMaintainers(t *testing.T) { func TestLintChartMaintainerValidation(t *testing.T) { type testData struct { - name string - chartDir string - expected bool + name string + chartDir string + expected bool } runTests := func(validate bool) { @@ -181,7 +184,7 @@ func TestLintChartMaintainerValidation(t *testing.T) { suffix = "without-validation" } - testCases := []testData { + testCases := []testData{ {fmt.Sprintf("maintainers-%s", suffix), "testdata/valid_maintainers", true}, {fmt.Sprintf("no-maintainers-%s", suffix), "testdata/no_maintainers", !validate}, } diff --git a/pkg/config/config.go b/pkg/config/config.go index f22687c5..9196278f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,11 +16,12 @@ package config import ( "fmt" - "github.com/mitchellh/go-homedir" "path" "reflect" "strings" + "github.com/mitchellh/go-homedir" + "github.com/helm/chart-testing/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -52,6 +53,8 @@ type Configuration struct { ExcludedCharts []string `mapstructure:"excluded-charts"` HelmExtraArgs string `mapstructure:"helm-extra-args"` Debug bool `mapstructure:"debug"` + Namespace string `mapstructure:"namespace"` + ReleaseLabel string `mapstructure:"release-label"` } func LoadConfiguration(cfgFile string, cmd *cobra.Command, bindFlagsFunc ...func(flagSet *flag.FlagSet, viper *viper.Viper) error) (*Configuration, error) { @@ -90,7 +93,11 @@ func LoadConfiguration(cfgFile string, cmd *cobra.Command, bindFlagsFunc ...func } if cfg.ProcessAllCharts && len(cfg.Charts) > 0 { - return nil, errors.New("Specifying both, '--all' and '--charts', is not allowed!") + return nil, errors.New("specifying both, '--all' and '--charts', is not allowed") + } + + if cfg.Namespace != "" && cfg.ReleaseLabel == "" { + return nil, errors.New("specifying '--namespace' without '--release-label' is not allowed") } isLint := strings.Contains(cmd.Use, "lint") diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 92b25f85..95166361 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -15,9 +15,10 @@ package config import ( + "testing" + "github.com/spf13/cobra" "github.com/stretchr/testify/require" - "testing" ) func TestUnmarshalYaml(t *testing.T) { @@ -43,4 +44,6 @@ 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, "default", cfg.Namespace) + require.Equal(t, "release", cfg.ReleaseLabel) } diff --git a/pkg/config/test_config.json b/pkg/config/test_config.json index 87f8109c..3b3f9724 100644 --- a/pkg/config/test_config.json +++ b/pkg/config/test_config.json @@ -18,5 +18,7 @@ "excluded-charts": [ "common" ], - "helm-extra-args": "--timeout 300" + "helm-extra-args": "--timeout 300", + "namespace": "default", + "release-label": "release" } diff --git a/pkg/config/test_config.yaml b/pkg/config/test_config.yaml index 9fd054d3..feaf213a 100644 --- a/pkg/config/test_config.yaml +++ b/pkg/config/test_config.yaml @@ -15,3 +15,5 @@ chart-dirs: excluded-charts: - common helm-extra-args: --timeout 300 +namespace: default +release-label: release diff --git a/pkg/tool/helm.go b/pkg/tool/helm.go index 6a2b2365..9b15537a 100644 --- a/pkg/tool/helm.go +++ b/pkg/tool/helm.go @@ -16,19 +16,18 @@ package tool import ( "fmt" + "github.com/helm/chart-testing/pkg/exec" ) type Helm struct { exec exec.ProcessExecutor - kubectl Kubectl extraArgs []string } -func NewHelm(exec exec.ProcessExecutor, kubectl Kubectl, extraArgs []string) Helm { +func NewHelm(exec exec.ProcessExecutor, extraArgs []string) Helm { return Helm{ exec: exec, - kubectl: kubectl, extraArgs: extraArgs, } } @@ -45,16 +44,13 @@ func (h Helm) BuildDependencies(chart string) error { return h.exec.RunProcess("helm", "dependency", "build", chart) } -func (h Helm) Lint(chart string) error { - return h.exec.RunProcess("helm", "lint", chart) -} - func (h Helm) LintWithValues(chart string, valuesFile string) error { - return h.exec.RunProcess("helm", "lint", chart, "--values", valuesFile) -} + var values []string + if valuesFile != "" { + values = []string{"--values", valuesFile} + } -func (h Helm) Install(chart string, namespace string, release string) error { - return h.InstallWithValues(chart, "", namespace, release) + return h.exec.RunProcess("helm", "lint", chart, values) } func (h Helm) InstallWithValues(chart string, valuesFile string, namespace string, release string) error { @@ -68,10 +64,10 @@ func (h Helm) InstallWithValues(chart string, valuesFile string, namespace strin return err } - if err := h.kubectl.WaitForDeployments(namespace); err != nil { - return err - } + return nil +} +func (h Helm) Test(release string) error { return h.exec.RunProcess("helm", "test", release, h.extraArgs) } diff --git a/pkg/tool/kubectl.go b/pkg/tool/kubectl.go index aefd12e2..bd027f9f 100644 --- a/pkg/tool/kubectl.go +++ b/pkg/tool/kubectl.go @@ -51,9 +51,9 @@ func (k Kubectl) DeleteNamespace(namespace string) { } } -func (k Kubectl) WaitForDeployments(namespace string) error { +func (k Kubectl) WaitForDeployments(namespace string, selector string) error { output, err := k.exec.RunProcessAndCaptureOutput( - "kubectl", "get", "deployments", "--namespace", namespace, "--output", "jsonpath={.items[*].metadata.name}") + "kubectl", "get", "deployments", "--namespace", namespace, "--selector", selector, "--output", "jsonpath={.items[*].metadata.name}") if err != nil { return err }