diff --git a/examples/annotated-skaffold.yaml b/examples/annotated-skaffold.yaml index 89c8a5a13ee..bd9dc147f2d 100644 --- a/examples/annotated-skaffold.yaml +++ b/examples/annotated-skaffold.yaml @@ -106,15 +106,27 @@ deploy: # helm releases to deploy. # releases: # - name: skaffold-helm - # chartPath: skaffold-helm - # valuesFilePath: helm-skaffold-values.yaml - # values: - # image: skaffold-helm - # namespace: skaffold - # version: "" - # setValues get appended to the helm deploy with --set. - # setValues: + # chartPath: skaffold-helm + # valuesFilePath: helm-skaffold-values.yaml + # values: + # image: skaffold-helm + # namespace: skaffold + # version: "" + # + # # setValues get appended to the helm deploy with --set. + # setValues: # key: "value" + # + # # packaged section allows to package chart setting specific version + # # and/or appVersion using "helm package" command. + # packaged: + # # version is passed to "helm package --version" flag. + # # Note that you can specify both static string or dynamic template. + # version: {{ .CHART_VERSION }}-dirty + # # appVersion is passed to "helm package --app-version" flag. + # # Note that you can specify both static string or dynamic template. + # appVersion: {{ .CHART_VERSION }}-dirty + # profiles section has all the profile information which can be used to override any build or deploy configuration profiles: - name: gcb diff --git a/pkg/skaffold/deploy/helm.go b/pkg/skaffold/deploy/helm.go index 496766cbdaf..7824c620e93 100644 --- a/pkg/skaffold/deploy/helm.go +++ b/pkg/skaffold/deploy/helm.go @@ -24,6 +24,8 @@ import ( "io" "os" "os/exec" + "path/filepath" + "strings" // k8syaml "k8s.io/apimachinery/pkg/util/yaml" // "k8s.io/client-go/kubernetes/scheme" @@ -128,9 +130,29 @@ func (h *HelmDeployer) deployRelease(out io.Writer, r v1alpha2.HelmRelease, buil var args []string if !isInstalled { - args = append(args, "install", "--name", releaseName, r.ChartPath) + args = append(args, "install", "--name", releaseName) } else { - args = append(args, "upgrade", releaseName, r.ChartPath) + args = append(args, "upgrade", releaseName) + } + + // There are 2 strategies: + // 1) Deploy chart directly from filesystem path or from repository + // (like stable/kubernetes-dashboard). Version only applies to a + // chart from repository. + // 2) Package chart into a .tgz archive with specific version and then deploy + // that packaged chart. This way user can apply any version and appVersion + // for the chart. + if r.Packaged == nil { + if r.Version != "" { + args = append(args, "--version", r.Version) + } + args = append(args, r.ChartPath) + } else { + chartPath, err := h.packageChart(r) + if err != nil { + return nil, errors.WithMessage(err, "cannot package chart") + } + args = append(args, chartPath) } var ns string @@ -161,9 +183,6 @@ func (h *HelmDeployer) deployRelease(out io.Writer, r v1alpha2.HelmRelease, buil if r.ValuesFilePath != "" { args = append(args, "-f", r.ValuesFilePath) } - if r.Version != "" { - args = append(args, "--version", r.Version) - } if len(r.SetValues) != 0 { for k, v := range r.SetValues { @@ -184,6 +203,41 @@ func (h *HelmDeployer) deployRelease(out io.Writer, r v1alpha2.HelmRelease, buil return h.getDeployResults(ns, r.Name), helmErr } +// packageChart packages the chart and returns path to the chart archive file. +// If this function returns an error, it will always be wrapped. +func (h *HelmDeployer) packageChart(r v1alpha2.HelmRelease) (string, error) { + tmp := os.TempDir() + packageArgs := []string{"package", r.ChartPath, "--destination", tmp} + if r.Packaged.Version != "" { + v, err := concretize(r.Packaged.Version) + if err != nil { + return "", errors.Wrap(err, `concretize "packaged.version" template`) + } + packageArgs = append(packageArgs, "--version", v) + } + if r.Packaged.AppVersion != "" { + av, err := concretize(r.Packaged.AppVersion) + if err != nil { + return "", errors.Wrap(err, `concretize "packaged.appVersion" template`) + } + packageArgs = append(packageArgs, "--app-version", av) + } + + buf := &bytes.Buffer{} + err := h.helm(buf, packageArgs...) + output := strings.TrimSpace(buf.String()) + if err != nil { + return "", errors.Wrapf(err, "package chart into a .tgz archive (%s)", output) + } + + fpath, err := extractChartFilename(output, tmp) + if err != nil { + return "", err + } + + return filepath.Join(tmp, fpath), nil +} + func (h *HelmDeployer) getReleaseInfo(release string) (*bufio.Reader, error) { var releaseInfo bytes.Buffer if err := h.helm(&releaseInfo, "get", release); err != nil { @@ -225,3 +279,25 @@ func evaluateReleaseName(nameTemplate string) (string, error) { return util.ExecuteEnvTemplate(tmpl, nil) } + +// concretize parses and executes template s with OS environment variables. +// If s is not a template but a simple string, returns unchanged s. +func concretize(s string) (string, error) { + tmpl, err := util.ParseEnvTemplate(s) + if err != nil { + return "", errors.Wrap(err, "parsing template") + } + + tmpl.Option("missingkey=error") + return util.ExecuteEnvTemplate(tmpl, nil) +} + +func extractChartFilename(s, tmp string) (string, error) { + s = strings.TrimSpace(s) + idx := strings.Index(s, tmp) + if idx == -1 { + return "", errors.New("cannot locate packaged chart archive") + } + + return s[idx+len(tmp):], nil +} diff --git a/pkg/skaffold/deploy/helm_test.go b/pkg/skaffold/deploy/helm_test.go index 29ddce123bf..d71002163d1 100644 --- a/pkg/skaffold/deploy/helm_test.go +++ b/pkg/skaffold/deploy/helm_test.go @@ -295,3 +295,29 @@ func TestParseHelmRelease(t *testing.T) { }) } } + +func TestExtractChartFilename(t *testing.T) { + testCases := map[string]struct { + input string + tmp string + output string + shouldErr bool + }{ + "1": { + input: "Successfully packaged chart and saved it to: /var/folders/gm/rrs_712142x8vymmd7xq7h340000gn/T/foo-1.2.3-dirty.tgz\n", + tmp: "/var/folders/gm/rrs_712142x8vymmd7xq7h340000gn/T/", + output: "foo-1.2.3-dirty.tgz", + shouldErr: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + out, err := extractChartFilename(tc.input, tc.tmp) + testutil.CheckError(t, tc.shouldErr, err) + if out != tc.output { + t.Errorf("Expected output to be %q but got %q", tc.output, out) + } + }) + } +} diff --git a/pkg/skaffold/schema/v1alpha2/config.go b/pkg/skaffold/schema/v1alpha2/config.go index 4ccab6958c5..efcef2607d1 100644 --- a/pkg/skaffold/schema/v1alpha2/config.go +++ b/pkg/skaffold/schema/v1alpha2/config.go @@ -137,6 +137,16 @@ type HelmRelease struct { SetValues map[string]string `yaml:"setValues"` Wait bool `yaml:"wait"` Overrides map[string]interface{} `yaml:"overrides"` + Packaged *HelmPackaged `yaml:"packaged"` +} + +// HelmPackaged represents parameters for packaging helm chart. +type HelmPackaged struct { + // Version sets the version on the chart to this semver version. + Version string `yaml:"version"` + + // AppVersion set the appVersion on the chart to this version + AppVersion string `yaml:"appVersion"` } // Artifact represents items that need to be built, along with the context in which