Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement packaging for helm deployment #682

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 20 additions & 8 deletions examples/annotated-skaffold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 81 additions & 5 deletions pkg/skaffold/deploy/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
26 changes: 26 additions & 0 deletions pkg/skaffold/deploy/helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
10 changes: 10 additions & 0 deletions pkg/skaffold/schema/v1alpha2/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down