Skip to content

Commit

Permalink
Initial implementation of Helm Renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
tete17 authored and Miguel Sacristan committed Apr 21, 2020
1 parent 992ced4 commit b380029
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 28 deletions.
109 changes: 88 additions & 21 deletions pkg/skaffold/deploy/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -51,7 +50,7 @@ import (

var (
// versionRegex extracts version from "helm version --client", for instance: "2.14.0-rc.2"
versionRegex = regexp.MustCompile(`\"v(\d[\w\.\-\.]+)`)
versionRegex = regexp.MustCompile(`v(\d[\w.\-]+)`)

// helm3Version represents the version cut-off for helm3 behavior
helm3Version = semver.MustParse("3.0.0-beta.0")
Expand Down Expand Up @@ -127,10 +126,10 @@ func (h *HelmDeployer) Deploy(ctx context.Context, out io.Writer, builds []build

// Let's make sure that every image tag is set with `--set`.
// Otherwise, templates have no way to use the images that were built.
for _, build := range builds {
if !valuesSet[build.Tag] {
warnings.Printf("image [%s] is not used.", build.Tag)
warnings.Printf("image [%s] is used instead.", build.ImageName)
for _, b := range builds {
if !valuesSet[b.Tag] {
warnings.Printf("image [%s] is not used.", b.Tag)
warnings.Printf("image [%s] is used instead.", b.ImageName)
warnings.Printf("See helm sample for how to replace image names with their actual tags: https://github.com/GoogleContainerTools/skaffold/blob/master/examples/helm-deployment/skaffold.yaml")
}
}
Expand Down Expand Up @@ -214,8 +213,83 @@ func (h *HelmDeployer) Cleanup(ctx context.Context, out io.Writer) error {
}

// Render generates the Kubernetes manifests and writes them out
func (h *HelmDeployer) Render(context.Context, io.Writer, []build.Artifact, []Labeller, string) error {
return errors.New("not yet implemented")
func (h *HelmDeployer) Render(ctx context.Context, out io.Writer, builds []build.Artifact, labellers []Labeller, filepath string) error {
hv, err := h.binVer(ctx)
if err != nil {
return fmt.Errorf("binary version: %w", err)
}

var output io.Writer

if filepath != "" {
f, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return fmt.Errorf("opening file for writing manifests: %w", err)
}
defer f.Close()

output = f
} else {
output = out
}

for _, r := range h.Releases {
args := []string{"template", r.ChartPath}

if hv.GTE(helm3Version) {
// Helm 3 requires the name to be before the chart path
args = append(args[:1], append([]string{r.Name}, args[1:]...)...)
} else {
args = append(args, "--name", r.Name)
}

for _, vf := range r.ValuesFiles {
args = append(args, "--values", vf)
}

params := pairParamsToArtifacts(builds, r.Values)

for k, v := range params {
var value string

cfg := r.ImageStrategy.HelmImageConfig.HelmConventionConfig

value, err = imageSetFromConfig(cfg, k, v.Tag)
if err != nil {
return err
}

args = append(args, "--set-string", value)
}

for key, value := range r.Values {
args = append(args, "--set", fmt.Sprintf("%s=%s", key, value))
}

sortedKeys := make([]string, 0, len(r.SetValueTemplates))
for k := range r.SetValueTemplates {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)

for _, key := range sortedKeys {
v, err := util.ExpandEnvTemplate(r.SetValueTemplates[key], nil)
if err != nil {
return err
}
args = append(args, "--set", fmt.Sprintf("%s=%s", key, v))
}

if r.Namespace != "" {
args = append(args, "--namespace", r.Namespace)
}

if err := h.exec(ctx, output, false, args...); err != nil {
return err
}
}

return nil
}

// exec executes the helm command, writing combined stdout/stderr to the provided writer
Expand Down Expand Up @@ -400,21 +474,16 @@ func installArgs(r latest.HelmRelease, builds []build.Artifact, valuesSet map[st
args = append(args, "--namespace", o.namespace)
}

params, err := pairParamsToArtifacts(builds, r.Values)
if err != nil {
return nil, fmt.Errorf("matching build results to chart values: %w", err)
}
params := pairParamsToArtifacts(builds, r.Values)

if len(r.Overrides.Values) != 0 {
args = append(args, "-f", constants.HelmOverridesFilename)
}

for k, v := range params {
var value string

cfg := r.ImageStrategy.HelmImageConfig.HelmConventionConfig

value, err = imageSetFromConfig(cfg, k, v.Tag)
value, err := imageSetFromConfig(cfg, k, v.Tag)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -597,7 +666,7 @@ func imageSetFromConfig(cfg *latest.HelmConventionConfig, valueName string, tag
}

// pairParamsToArtifacts associates parameters to the build artifact it creates
func pairParamsToArtifacts(builds []build.Artifact, params map[string]string) (map[string]build.Artifact, error) {
func pairParamsToArtifacts(builds []build.Artifact, params map[string]string) map[string]build.Artifact {
imageToBuildResult := map[string]build.Artifact{}
for _, b := range builds {
imageToBuildResult[b.ImageName] = b
Expand All @@ -607,12 +676,10 @@ func pairParamsToArtifacts(builds []build.Artifact, params map[string]string) (m

for param, imageName := range params {
b, ok := imageToBuildResult[imageName]
if !ok {
return nil, fmt.Errorf("no build present for %s", imageName)
if ok {
paramToBuildResult[param] = b
}

paramToBuildResult[param] = b
}

return paramToBuildResult, nil
return paramToBuildResult
}
105 changes: 98 additions & 7 deletions pkg/skaffold/deploy/helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,16 +486,21 @@ func TestHelmDeploy(t *testing.T) {
builds: testBuilds,
},
{
description: "deploy should error for unmatched parameter",
description: "deploy should warn for unmatched parameter",
commands: testutil.
CmdRunWithOutput("helm version", version21).
AndRun("helm --kube-context kubecontext get skaffold-helm --kubeconfig kubeconfig").
AndRun("helm --kube-context kubecontext dep build examples/test --kubeconfig kubeconfig").
AndRun("helm --kube-context kubecontext upgrade skaffold-helm examples/test -f skaffold-overrides.yaml --set-string image=docker.io:5000/skaffold-helm:3605e7bc17cf46e53f4d81c4cbc24e5b4c495184 --set some.key=somevalue --kubeconfig kubeconfig").
AndRun("helm --kube-context kubecontext upgrade skaffold-helm examples/test --kubeconfig kubeconfig").
AndRun("helm --kube-context kubecontext get skaffold-helm --kubeconfig kubeconfig"),
runContext: makeRunContext(testDeployConfigParameterUnmatched, false),
builds: testBuilds,
shouldErr: true,
shouldErr: false,
expectedWarnings: []string{
"See helm sample for how to replace image names with their actual tags: https://github.com/GoogleContainerTools/skaffold/blob/master/examples/helm-deployment/skaffold.yaml",
"image [docker.io:5000/skaffold-helm:3605e7bc17cf46e53f4d81c4cbc24e5b4c495184] is not used.",
"image [skaffold-helm] is used instead.",
},
},
{
description: "deploy success remote chart with skipBuildDependencies",
Expand Down Expand Up @@ -975,17 +980,103 @@ func TestHelmRender(t *testing.T) {
tests := []struct {
description string
shouldErr bool
commands util.Command
runContext *runcontext.RunContext
outputFile string
expected string
builds []build.Artifact
}{
{
description: "calling render returns error",
description: "error if version can't be retrieved",
shouldErr: true,
commands: testutil.CmdRunErr("helm version", fmt.Errorf("yep not working")),
runContext: makeRunContext(testDeployConfig, false),
},
{
description: "normal render v2",
shouldErr: false,
commands: testutil.
CmdRunWithOutput("helm version", version21).
AndRun("helm --kube-context kubecontext template examples/test --name skaffold-helm --set-string image=skaffold-helm:tag1 --set image=skaffold-helm --kubeconfig kubeconfig"),
runContext: makeRunContext(testDeployConfig, false),
builds: []build.Artifact{
{
ImageName: "skaffold-helm",
Tag: "skaffold-helm:tag1",
}},
},
{
description: "normal render v3",
shouldErr: false,
commands: testutil.
CmdRunWithOutput("helm version", version31).
AndRun("helm --kube-context kubecontext template skaffold-helm examples/test --set-string image=skaffold-helm:tag1 --set image=skaffold-helm --kubeconfig kubeconfig"),
runContext: makeRunContext(testDeployConfig, false),
builds: []build.Artifact{
{
ImageName: "skaffold-helm",
Tag: "skaffold-helm:tag1",
}},
},
{
description: "render to a file",
shouldErr: false,
commands: testutil.
CmdRunWithOutput("helm version", version31).
AndRunWithOutput("helm --kube-context kubecontext template skaffold-helm examples/test --set-string image=skaffold-helm:tag1 --set image=skaffold-helm --kubeconfig kubeconfig",
"Dummy Output"),
runContext: makeRunContext(testDeployConfig, false),
outputFile: "dummy.yaml",
expected: "Dummy Output",
builds: []build.Artifact{
{
ImageName: "skaffold-helm",
Tag: "skaffold-helm:tag1",
}},
},
{
description: "render with templated config",
shouldErr: false,
commands: testutil.
CmdRunWithOutput("helm version", version31).
AndRun("helm --kube-context kubecontext template skaffold-helm examples/test --set-string image=skaffold-helm:tag1 --set image=skaffold-helm --set image.name=<no value> --set image.tag=<no value> --set missing.key=<no value> --set other.key=<no value> --set some.key=somevalue --kubeconfig kubeconfig"),
runContext: makeRunContext(testDeployConfigTemplated, false),
builds: []build.Artifact{
{
ImageName: "skaffold-helm",
Tag: "skaffold-helm:tag1",
}},
},
{
description: "render with namespace",
shouldErr: false,
commands: testutil.CmdRunWithOutput("helm version", version31).
AndRun("helm --kube-context kubecontext template skaffold-helm examples/test --set-string image=skaffold-helm:tag1 --set image=skaffold-helm --namespace testNamespace --kubeconfig kubeconfig"),
runContext: makeRunContext(testDeployNamespacedConfig, false),
builds: []build.Artifact{
{
ImageName: "skaffold-helm",
Tag: "skaffold-helm:tag1",
}},
},
}
for _, test := range tests {
testutil.Run(t, test.description, func(t *testutil.T) {
deployer := NewHelmDeployer(&runcontext.RunContext{})
actual := deployer.Render(context.Background(), ioutil.Discard, []build.Artifact{}, nil, "tmp/dir")
t.CheckError(test.shouldErr, actual)
file := ""
if test.outputFile != "" {
file = t.NewTempDir().Path(test.outputFile)
}

deployer := NewHelmDeployer(test.runContext)

t.Override(&util.DefaultExecCommand, test.commands)
err := deployer.Render(context.Background(), ioutil.Discard, test.builds, nil, file)
t.CheckError(test.shouldErr, err)

if file != "" {
dat, _ := ioutil.ReadFile(file)
t.CheckDeepEqual(string(dat), test.expected)
}
})
}
}
Expand Down

0 comments on commit b380029

Please sign in to comment.