Skip to content

Commit

Permalink
Merge pull request #1365 from jguionnet/feat/add-remote-repo-support-…
Browse files Browse the repository at this point in the history
…in-helm

Feat: add remote repo support in helm module for unit testing
  • Loading branch information
denis256 authored Nov 6, 2023
2 parents 237816f + 8508fcb commit 3ca3294
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
38 changes: 38 additions & 0 deletions modules/helm/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,44 @@ func RenderTemplateE(t testing.TestingT, options *Options, chartDir string, rele
return RunHelmCommandAndGetStdOutE(t, options, "template", args...)
}

// RenderTemplate runs `helm template` to render a *remote* chart given the provided options and returns stdout/stderr from
// the template command. If you pass in templateFiles, this will only render those templates. This function will fail
// the test if there is an error rendering the template.
func RenderRemoteTemplate(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) string {
out, err := RenderRemoteTemplateE(t, options, chartURL, releaseName, templateFiles, extraHelmArgs...)
require.NoError(t, err)
return out
}

// RenderTemplate runs `helm template` to render a *remote* helm chart given the provided options and returns stdout/stderr from
// the template command. If you pass in templateFiles, this will only render those templates.
func RenderRemoteTemplateE(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, error) {
// Now construct the args
// We first construct the template args
args := []string{}
if options.KubectlOptions != nil && options.KubectlOptions.Namespace != "" {
args = append(args, "--namespace", options.KubectlOptions.Namespace)
}
args, err := getValuesArgsE(t, options, args...)
if err != nil {
return "", err
}
for _, templateFile := range templateFiles {
// As the helm command fails if a non valid template is given as input
// we do not check if the template file exists or not as we do for local charts
// as it would add unecessary networking calls
args = append(args, "--show-only", templateFile)
}
// deal extraHelmArgs
args = append(args, extraHelmArgs...)

// ... and add the helm chart name, the remote repo and chart URL at the end
args = append(args, releaseName, "--repo", chartURL)

// Finally, call out to helm template command
return RunHelmCommandAndGetStdOutE(t, options, "template", args...)
}

// UnmarshalK8SYaml is the same as UnmarshalK8SYamlE, but will fail the test if there is an error.
func UnmarshalK8SYaml(t testing.TestingT, yamlData string, destinationObj interface{}) {
require.NoError(t, UnmarshalK8SYamlE(t, yamlData, destinationObj))
Expand Down
67 changes: 67 additions & 0 deletions modules/helm/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//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. To avoid overloading the
// system, we run the kubernetes tests and helm tests separately from the others.

package helm

import (
"fmt"
"strings"
"testing"

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

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

// Test that we can render locally a remote chart (e.g bitnami/nginx)
func TestRemoteChartRender(t *testing.T) {
const (
remoteChartSource = "https://charts.bitnami.com/bitnami"
remoteChartName = "nginx"
remoteChartVersion = "13.2.23"
)

t.Parallel()

namespaceName := fmt.Sprintf(
"%s-%s",
strings.ToLower(t.Name()),
strings.ToLower(random.UniqueId()),
)

releaseName := remoteChartName

options := &Options{
SetValues: map[string]string{
"image.repository": remoteChartName,
"image.registry": "",
"image.tag": remoteChartVersion,
},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}

// 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.
output := RenderRemoteTemplate(t, options, remoteChartSource, releaseName, []string{"templates/deployment.yaml"})

// 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
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 := remoteChartName + ":" + remoteChartVersion
deploymentContainers := deployment.Spec.Template.Spec.Containers
require.Equal(t, len(deploymentContainers), 1)
require.Equal(t, deploymentContainers[0].Image, expectedContainerImage)
}
70 changes: 70 additions & 0 deletions test/helm_keda_remote_example_template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//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 (
"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 an example of how to use terratest to test *remote* helm chart template logic by rendering the templates
// using `helm template`, and then reading in the rendered templates.
// - TestHelmKedaRemoteExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the
// computed values.

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

// chart name
releaseName := "keda"

// 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:
options := &helm.Options{
SetValues: map[string]string{
"metricsServer.replicaCount": "999",
"resources.metricServer.limits.memory": "1234Mi",
},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}

// Run RenderTemplate to render the *remote* 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, we path a the templateFile for which we are setting test values to
// demonstrate how to select individual templates to render.
output := helm.RenderRemoteTemplate(t, options, "https://kedacore.github.io/charts", releaseName, []string{"templates/metrics-server/deployment.yaml"})

// 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
var expectedMetricsServerReplica int32
expectedMetricsServerReplica = 999
deploymentMetricsServerReplica := *deployment.Spec.Replicas
require.Equal(t, expectedMetricsServerReplica, deploymentMetricsServerReplica)
}

0 comments on commit 3ca3294

Please sign in to comment.