Skip to content

Commit

Permalink
Add helm values validation to test installation (#9839)
Browse files Browse the repository at this point in the history
* wip

* comments on glooctl test

* fail early if invalid values

* changelog

* remove logLevel

* changelog

* fix test

* rename import

* fix merge

* remove unsupported logLevel field

---------

Co-authored-by: soloio-bulldozer[bot] <48420018+soloio-bulldozer[bot]@users.noreply.github.com>
  • Loading branch information
npolshakova and soloio-bulldozer[bot] authored Aug 8, 2024
1 parent 052eafe commit 9bdd0d8
Show file tree
Hide file tree
Showing 34 changed files with 626 additions and 603 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changelog:
- type: NON_USER_FACING
description: >-
Validate e2e test values before installing Gloo and running e2e tests.
11 changes: 6 additions & 5 deletions install/test/5-gateway-validation-webhook-configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/golang/protobuf/ptypes/wrappers"
"github.com/onsi/gomega/types"
glootestutils "github.com/solo-io/gloo/test/testutils"
v1 "k8s.io/api/admissionregistration/v1"

. "github.com/onsi/ginkgo/v2"
Expand All @@ -31,7 +32,7 @@ var _ = Describe("WebhookValidationConfiguration helm test", func() {
testManifest TestManifest
//expectedChart *unstructured.Unstructured
)
prepareMakefile := func(namespace string, values helmValues) {
prepareMakefile := func(namespace string, values glootestutils.HelmValues) {
tm, err := rendererTestCase.renderer.RenderManifest(namespace, values)
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to render manifest")
testManifest = tm
Expand All @@ -45,8 +46,8 @@ var _ = Describe("WebhookValidationConfiguration helm test", func() {
expectedDeletes := 5 - expectedRemoved
expectedChart := generateExpectedChart(timeoutSeconds, resources, expectedDeletes)

prepareMakefile(namespace, helmValues{
valuesArgs: []string{
prepareMakefile(namespace, glootestutils.HelmValues{
ValuesArgs: []string{
fmt.Sprintf(`gateway.validation.webhook.timeoutSeconds=%d`, timeoutSeconds),
`gateway.validation.webhook.skipDeleteValidationResources={` + strings.Join(resources, ",") + `}`,
},
Expand Down Expand Up @@ -105,8 +106,8 @@ var _ = Describe("WebhookValidationConfiguration helm test", func() {
valuesArgs = append(valuesArgs, fmt.Sprintf(`gateway.validation.webhook.enablePolicyApi=%t`, testCase.enablePolicyApi.GetValue()))
}

prepareMakefile(namespace, helmValues{
valuesArgs: valuesArgs,
prepareMakefile(namespace, glootestutils.HelmValues{
ValuesArgs: valuesArgs,
})

testManifest.ExpectUnstructured(
Expand Down
11 changes: 6 additions & 5 deletions install/test/grpc_json_transcoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/types"
"github.com/solo-io/gloo/projects/gateway/pkg/defaults"
glootestutils "github.com/solo-io/gloo/test/testutils"
. "github.com/solo-io/k8s-utils/manifesttestutils"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
Expand Down Expand Up @@ -38,7 +39,7 @@ var _ = Describe("GrpcJsonTranscoder helm test", func() {
}
}
)
prepareManifest := func(namespace string, values helmValues) {
prepareManifest := func(namespace string, values glootestutils.HelmValues) {
GinkgoHelper()
tm, err := rendererTestCase.renderer.RenderManifest(namespace, values)
Expect(err).NotTo(HaveOccurred(), "Failed to render manifest")
Expand All @@ -47,8 +48,8 @@ var _ = Describe("GrpcJsonTranscoder helm test", func() {

Context("protoDescriptorBin field", func() {
BeforeEach(func() {
prepareManifest(namespace, helmValues{
valuesArgs: []string{
prepareManifest(namespace, glootestutils.HelmValues{
ValuesArgs: []string{
fmt.Sprintf("gatewayProxies.gatewayProxy.gatewaySettings.customHttpGateway.options.grpcJsonTranscoder.protoDescriptorBin=%s", protoDescriptor),
fmt.Sprintf("gatewayProxies.gatewayProxy.gatewaySettings.customHttpsGateway.options.grpcJsonTranscoder.protoDescriptorBin=%s", protoDescriptor),
},
Expand All @@ -65,8 +66,8 @@ var _ = Describe("GrpcJsonTranscoder helm test", func() {
})
Context("protoDescriptorConfigMap field", func() {
BeforeEach(func() {
prepareManifest(namespace, helmValues{
valuesArgs: []string{
prepareManifest(namespace, glootestutils.HelmValues{
ValuesArgs: []string{
"gatewayProxies.gatewayProxy.gatewaySettings.customHttpGateway.options.grpcJsonTranscoder.protoDescriptorConfigMap.configMapRef.name=my-config-map",
"gatewayProxies.gatewayProxy.gatewaySettings.customHttpGateway.options.grpcJsonTranscoder.protoDescriptorConfigMap.configMapRef.namespace=gloo-system",
"gatewayProxies.gatewayProxy.gatewaySettings.customHttpGateway.options.grpcJsonTranscoder.protoDescriptorConfigMap.key=my-key",
Expand Down
127 changes: 7 additions & 120 deletions install/test/helm_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package test

import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
Expand All @@ -16,25 +14,22 @@ import (
"github.com/solo-io/k8s-utils/installutils/kuberesource"
rbacv1 "k8s.io/api/rbac/v1"

"github.com/solo-io/gloo/install/helm/gloo/generate"

"github.com/ghodss/yaml"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/solo-io/gloo/pkg/cliutil/helm"
"github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/install"
"github.com/solo-io/gloo/projects/gloo/pkg/defaults"
glootestutils "github.com/solo-io/gloo/test/testutils"
soloHelm "github.com/solo-io/go-utils/helmutils"
"github.com/solo-io/go-utils/testutils"
. "github.com/solo-io/k8s-utils/manifesttestutils"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/strvals"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
k8syamlutil "sigs.k8s.io/yaml"
)

const (
Expand Down Expand Up @@ -143,14 +138,9 @@ func MustGetVersion() string {
return "version-not-found"
}

type helmValues struct {
valuesFile string
valuesArgs []string // each entry should look like `path.to.helm.field=value`
}

type ChartRenderer interface {
// returns a TestManifest containing all resources
RenderManifest(namespace string, values helmValues) (TestManifest, error)
RenderManifest(namespace string, values glootestutils.HelmValues) (TestManifest, error)
}

var _ ChartRenderer = &helm3Renderer{}
Expand All @@ -164,7 +154,7 @@ type helm3Renderer struct {
manifestOutputDir string
}

func (h3 helm3Renderer) RenderManifest(namespace string, values helmValues) (TestManifest, error) {
func (h3 helm3Renderer) RenderManifest(namespace string, values glootestutils.HelmValues) (TestManifest, error) {
rel, err := buildHelm3Release(h3.chartDir, namespace, values)
if err != nil {
return nil, errors.Errorf("failure in buildHelm3Release: %s", err.Error())
Expand Down Expand Up @@ -210,21 +200,21 @@ func (h3 helm3Renderer) RenderManifest(namespace string, values helmValues) (Tes
return NewTestManifest(testManifestFile.Name()), nil
}

func buildHelm3Release(chartDir, namespace string, values helmValues) (*release.Release, error) {
func buildHelm3Release(chartDir, namespace string, values glootestutils.HelmValues) (*release.Release, error) {
chartRequested, err := loader.Load(chartDir)
if err != nil {
return nil, errors.Errorf("failed to load chart directory: %s", err.Error())
}

helmValues, err := buildHelmValues(chartDir, values)
helmValues, err := glootestutils.BuildHelmValues(values)
if err != nil {
return nil, errors.Errorf("failure in buildHelmValues: %s", err.Error())
}

// Validate that the provided values match the Go types used to construct out docs
err = validateHelmValues(helmValues)
err = glootestutils.ValidateHelmValues(helmValues)
if err != nil {
return nil, errors.Errorf("failure in validateHelmValues: %s", err.Error())
return nil, errors.Errorf("failure in ValidateHelmValues: %s", err.Error())
}

// Install the chart
Expand All @@ -239,88 +229,6 @@ func buildHelm3Release(chartDir, namespace string, values helmValues) (*release.
return release, err
}

// each entry in valuesArgs should look like `path.to.helm.field=value`
func buildHelmValues(chartDir string, values helmValues) (map[string]interface{}, error) {
// read the chart's base values file first
finalValues, err := readValuesFile(path.Join(chartDir, "values.yaml"))
if err != nil {
return nil, err
}

for _, v := range values.valuesArgs {
err := strvals.ParseInto(v, finalValues)
if err != nil {
return nil, err
}
}

if values.valuesFile != "" {
// these lines ripped out of Helm internals
// https://github.com/helm/helm/blob/release-3.0/pkg/cli/values/options.go
mapFromFile, err := readValuesFile(values.valuesFile)
if err != nil {
return nil, err
}

// Merge with the previous map
finalValues = mergeMaps(finalValues, mapFromFile)
}

return finalValues, nil
}

// validateHelmValues ensures that the unstructured helm values that are provided
// to a chart match the Go type used to generate the Helm documentation
// Returns nil if all the provided values are all included in the Go struct
// Returns an error if a provided value is not included in the Go struct.
//
// Example:
//
// Failed to render manifest
// Unexpected error:
// <*errors.errorString | 0xc000fedf40>: {
// s: "error unmarshaling JSON: while decoding JSON: json: unknown field \"useTlsTagging\"",
// }
// error unmarshaling JSON: while decoding JSON: json: unknown field "useTlsTagging"
// occurred
//
// This means that the unstructured values provided to the Helm chart contain a field `useTlsTagging`
// but the Go struct does not contain that field.
func validateHelmValues(unstructuredHelmValues map[string]interface{}) error {
// This Go type is the source of truth for the Helm docs
var structuredHelmValues generate.HelmConfig

unstructuredHelmValueBytes, err := json.Marshal(unstructuredHelmValues)
if err != nil {
return err
}

// This ensures that an error will be raised if there is an unstructured helm value
// defined but there is not the equivalent type defined in our Go struct
//
// When an error occurs, this means the Go type needs to be amended
// to include the new field (which is the source of truth for our docs)
return k8syamlutil.UnmarshalStrict(unstructuredHelmValueBytes, &structuredHelmValues)
}

func readValuesFile(filePath string) (map[string]interface{}, error) {
mapFromFile := map[string]interface{}{}

bytes, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}

// NOTE: This is not the default golang yaml.Unmarshal, because that implementation
// does not unmarshal into a map[string]interface{}; it unmarshals the file into a map[interface{}]interface{}
// https://github.com/go-yaml/yaml/issues/139
if err := k8syamlutil.Unmarshal(bytes, &mapFromFile); err != nil {
return nil, err
}

return mapFromFile, nil
}

func createInstallAction(namespace string) (*action.Install, error) {
settings := install.NewCLISettings(namespace, "")
actionConfig := new(action.Configuration)
Expand All @@ -344,27 +252,6 @@ func createInstallAction(namespace string) (*action.Install, error) {
return renderer, nil
}

// stolen from Helm internals
// https://github.com/helm/helm/blob/release-3.0/pkg/cli/values/options.go#L88
func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
out[k] = v
}
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
out[k] = mergeMaps(bv, v)
continue
}
}
}
out[k] = v
}
return out
}

func makeUnstructured(yam string) *unstructured.Unstructured {
jsn, err := yaml.YAMLToJSON([]byte(yam))
ExpectWithOffset(1, err).NotTo(HaveOccurred())
Expand Down
Loading

0 comments on commit 9bdd0d8

Please sign in to comment.