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

Feat: add remote repo support in helm module for unit testing #1365

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}