Skip to content

Commit

Permalink
Merge pull request #1092 from xhu-cloudera/issue-342
Browse files Browse the repository at this point in the history
Issue 342: Add support for helm chart dependency
  • Loading branch information
denis256 authored May 29, 2022
2 parents 1dc5806 + a466396 commit 5913a29
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 1 deletion.
2 changes: 2 additions & 0 deletions examples/helm-dependency-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
charts/
requirements.lock
10 changes: 10 additions & 0 deletions examples/helm-dependency-example/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: v1
name: helm-dependency-example
description: A minimal Helm chart to demonstrate how to use terratest to test helm charts with dependency
version: 0.0.1
dependencies:
- name: helm-basic-example
alias: basic
repository: file://../helm-basic-example
condition: basic.enabled
version: 0.0.1
31 changes: 31 additions & 0 deletions examples/helm-dependency-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Helm Dependency Example

This folder contains a minimal helm chart to demonstrate how you can use Terratest to test your helm charts with dependencies.

There are two kinds of tests you can perform on a helm chart:

- Helm Template tests are tests designed to test the logic of the templates. These tests should run `helm template` with
various input values and parse the yaml to validate any logic embedded in the templates (e.g by reading them in using
client-go). Since templates are not statically typed, the goal of these tests is to promote fast cycle time

The helm chart deploys a single replica `Deployment` resource given the container image spec and a `Service` that
exposes it. This chart requires the `containerImageRepo` and `containerImageTag` input values.

See the corresponding terratest code for an example of how to test this chart:

- [helm_basic_example_template_test.go](/test/helm_basic_example_template_test.go): the template tests for this chart.

## Running automated tests against this Helm Chart

1. Install and setup [helm](https://docs.helm.sh/using_helm/#installing-helm)
1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.
1. `cd test`
1. `dep ensure`
1. `go test -v -tags helm -run TestHelmDependencyExampleTemplate` for the template test

**NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm
tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm
can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests
start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes
tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.
We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.
32 changes: 32 additions & 0 deletions examples/helm-dependency-example/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "helm-dependency-example.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "helm-dependency-example.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "helm-dependency-example.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
31 changes: 31 additions & 0 deletions examples/helm-dependency-example/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "helm-dependency-example.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
# These labels are required by helm. You can read more about required labels in the chart best pracices guide:
# https://docs.helm.sh/chart_best_practices/#standard-labels
helm.sh/chart: {{ include "helm-dependency-example.chart" . }}
app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
containers:
- name: app
{{- $repo := required "containerImageRepo is required" .Values.containerImageRepo }}
{{- $tag := required "containerImageTag is required" .Values.containerImageTag }}
image: "{{ $repo }}:{{ $tag }}"
ports:
- containerPort: 80
20 changes: 20 additions & 0 deletions examples/helm-dependency-example/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "helm-dependency-example.fullname" . }}
labels:
# These labels are required by helm. You can read more about required labels in the chart best practices guide:
# https://docs.helm.sh/chart_best_practices/#standard-labels
helm.sh/chart: {{ include "helm-dependency-example.chart" . }}
app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
selector:
app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
type: NodePort
ports:
- protocol: TCP
targetPort: 80
port: 80
8 changes: 8 additions & 0 deletions examples/helm-dependency-example/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This chart purposefully does not provide any values, to demonstrate how to test required values.
# Note that the following two values must be specified if you wish to deploy this chart:

# containerImageRepo is a string that describes the image repository to pull the container image from.
# containerImageRepo: nginx

# containerImageTag is a string that describes the image tag to use when pulling the container image.
# containerImageTag: v1.15.4
8 changes: 7 additions & 1 deletion modules/helm/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package helm
import (
"encoding/json"
"path/filepath"
"strings"

"github.com/ghodss/yaml"
"github.com/gruntwork-io/go-commons/errors"
Expand Down Expand Up @@ -33,6 +34,11 @@ func RenderTemplateE(t testing.TestingT, options *Options, chartDir string, rele
return "", errors.WithStackTrace(ChartNotFoundError{chartDir})
}

// check chart dependencies
if _, err := RunHelmCommandAndGetOutputE(t, &Options{}, "dependency", "build", chartDir); err != nil {
return "", errors.WithStackTrace(err)
}

// Now construct the args
// We first construct the template args
args := []string{}
Expand All @@ -46,7 +52,7 @@ func RenderTemplateE(t testing.TestingT, options *Options, chartDir string, rele
for _, templateFile := range templateFiles {
// validate this is a valid template file
absTemplateFile := filepath.Join(absChartDir, templateFile)
if !files.FileExists(absTemplateFile) {
if !strings.HasPrefix(templateFile, "charts") && !files.FileExists(absTemplateFile) {
return "", errors.WithStackTrace(TemplateFileNotFoundError{Path: templateFile, ChartDir: absChartDir})
}

Expand Down
179 changes: 179 additions & 0 deletions test/helm_dependency_example_template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//go:build kubeall || helm
// +build kubeall helm

// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm
// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm
// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests
// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes
// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.
// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.

package test

import (
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"

"github.com/gruntwork-io/terratest/modules/helm"
"github.com/gruntwork-io/terratest/modules/k8s"
"github.com/gruntwork-io/terratest/modules/logger"
"github.com/gruntwork-io/terratest/modules/random"
)

// This file contains examples of how to use terratest to test helm chart template logic by rendering the templates
// using `helm template`, and then reading in the rendered templates.
// There are two tests:
// - TestHelmBasicExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the
// computed values.
// - TestHelmBasicExampleTemplateRequiredTemplateArgs: An example of how to check that the required args are indeed
// required for the template to render.

// An example of how to verify the rendered template object of a Helm Chart given various inputs.
func TestHelmDependencyExampleTemplateRenderedDeployment(t *testing.T) {
t.Parallel()

// Path to the helm chart we will test
helmChartPath, err := filepath.Abs("../examples/helm-dependency-example")
releaseName := "helm-dependency"
require.NoError(t, err)

// Since we aren't deploying any resources, there is no need to setup kubectl authentication or helm home.

// Set up the namespace; confirm that the template renders the expected value for the namespace.
namespaceName := "medieval-" + strings.ToLower(random.UniqueId())
logger.Logf(t, "Namespace: %s\n", namespaceName)

// Setup the args. For this test, we will set the following input values:
// - containerImageRepo=nginx
// - containerImageTag=1.15.8
options := &helm.Options{
SetValues: map[string]string{
"containerImageRepo": "nginx",
"containerImageTag": "1.15.8",
"basic.containerImageRepo": "nginx",
"basic.containerImageTag": "1.15.8",
},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}

testCases := []struct {
name string
templateName string
}{
{
"dependent chart",
"templates/deployment.yaml",
},
{
"basic chart",
"charts/basic/templates/deployment.yaml",
},
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(subT *testing.T) {
// subT.Parallel()
// Run RenderTemplate to render the template and capture the output. Note that we use the version without `E`, since
// we want to assert that the template renders without any errors.
// Additionally, although we know there is only one yaml file in the template, we deliberately path a templateFiles
// arg to demonstrate how to select individual templates to render.
output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{testCase.templateName})

// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will
// ensure the Deployment resource is rendered correctly.
var deployment appsv1.Deployment
helm.UnmarshalK8SYaml(t, output, &deployment)

// Verify the namespace matches the expected supplied namespace.
require.Equal(t, namespaceName, deployment.Namespace)

// Finally, we verify the deployment pod template spec is set to the expected container image value
expectedContainerImage := "nginx:1.15.8"
deploymentContainers := deployment.Spec.Template.Spec.Containers
require.Equal(t, len(deploymentContainers), 1)
require.Equal(t, deploymentContainers[0].Image, expectedContainerImage)

})
}
}

// An example of how to verify required values for a helm chart.
func TestHelmDependencyExampleTemplateRequiredTemplateArgs(t *testing.T) {
t.Parallel()

// Path to the helm chart we will test
helmChartPath, err := filepath.Abs("../examples/helm-dependency-example")
releaseName := "helm-dependency"
require.NoError(t, err)

// Since we aren't deploying any resources, there is no need to setup kubectl authentication, helm home, or
// namespaces

// Here, we use a table driven test to iterate through all the required values as subtests. You can learn more about
// go subtests here: https://blog.golang.org/subtests
// The struct captures the inputs that we will pass to helm template and a human friendly name so we can identify it
// in the test output. In this case, each test case will be a complete values input except for one of the required
// values missing, to test that neglecting a required value will cause the template rendering to fail.
testCases := []struct {
name string
values map[string]string
}{
{
"MissingContainerImageRepo in dependent chart",
map[string]string{
"containerImageTag": "1.15.8",
"basic.containerImageRepo": "nginx",
"basic.containerImageTag": "1.15.8",
},
},
{
"MissingContainerImageRepo in basic chart",
map[string]string{
"basic.containerImageTag": "1.15.8",
"containerImageRepo": "nginx",
"containerImageTag": "1.15.8",
},
},
{
"MissingContainerImageTag in dependent chart",
map[string]string{
"containerImageRepo": "nginx",
"basic.containerImageRepo": "nginx",
"basic.containerImageTag": "1.15.8",
},
},
{
"MissingContainerImageTag in basic chart",
map[string]string{
"basic.containerImageRepo": "nginx",
"containerImageRepo": "nginx",
"containerImageTag": "1.15.8",
},
},
}

// Now we iterate over each test case and spawn a sub test
for _, testCase := range testCases {
// Here, we capture the range variable and force it into the scope of this block. If we don't do this, when the
// subtest switches contexts (because of t.Parallel), the testCase value will have been updated by the for loop
// and will be the next testCase!
testCase := testCase

// The actual sub test spawning. We name the sub test using the human friendly name. Note that we name the sub
// test T struct to subT to make it clear which T struct corresponds to which test. However, in most cases you
// will not reference the main test T so you can name it the same.
t.Run(testCase.name, func(subT *testing.T) {
// subT.Parallel()

// Now we try rendering the template, but verify we get an error
options := &helm.Options{SetValues: testCase.values}
_, err := helm.RenderTemplateE(t, options, helmChartPath, releaseName, []string{})
require.Error(t, err)
})
}
}

0 comments on commit 5913a29

Please sign in to comment.