github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/lint/rules/template_test.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package rules
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/stefanmcshane/helm/internal/test/ensure"
    27  	"github.com/stefanmcshane/helm/pkg/chart"
    28  	"github.com/stefanmcshane/helm/pkg/chartutil"
    29  	"github.com/stefanmcshane/helm/pkg/lint/support"
    30  )
    31  
    32  const templateTestBasedir = "./testdata/albatross"
    33  
    34  func TestValidateAllowedExtension(t *testing.T) {
    35  	var failTest = []string{"/foo", "/test.toml"}
    36  	for _, test := range failTest {
    37  		err := validateAllowedExtension(test)
    38  		if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") {
    39  			t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test)
    40  		}
    41  	}
    42  	var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"}
    43  	for _, test := range successTest {
    44  		err := validateAllowedExtension(test)
    45  		if err != nil {
    46  			t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error())
    47  		}
    48  	}
    49  }
    50  
    51  var values = map[string]interface{}{"nameOverride": "", "httpPort": 80}
    52  
    53  const namespace = "testNamespace"
    54  const strict = false
    55  
    56  func TestTemplateParsing(t *testing.T) {
    57  	linter := support.Linter{ChartDir: templateTestBasedir}
    58  	Templates(&linter, values, namespace, strict)
    59  	res := linter.Messages
    60  
    61  	if len(res) != 1 {
    62  		t.Fatalf("Expected one error, got %d, %v", len(res), res)
    63  	}
    64  
    65  	if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") {
    66  		t.Errorf("Unexpected error: %s", res[0])
    67  	}
    68  }
    69  
    70  var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml")
    71  var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored")
    72  
    73  // Test a template with all the existing features:
    74  // namespaces, partial templates
    75  func TestTemplateIntegrationHappyPath(t *testing.T) {
    76  	// Rename file so it gets ignored by the linter
    77  	os.Rename(wrongTemplatePath, ignoredTemplatePath)
    78  	defer os.Rename(ignoredTemplatePath, wrongTemplatePath)
    79  
    80  	linter := support.Linter{ChartDir: templateTestBasedir}
    81  	Templates(&linter, values, namespace, strict)
    82  	res := linter.Messages
    83  
    84  	if len(res) != 0 {
    85  		t.Fatalf("Expected no error, got %d, %v", len(res), res)
    86  	}
    87  }
    88  
    89  func TestV3Fail(t *testing.T) {
    90  	linter := support.Linter{ChartDir: "./testdata/v3-fail"}
    91  	Templates(&linter, values, namespace, strict)
    92  	res := linter.Messages
    93  
    94  	if len(res) != 3 {
    95  		t.Fatalf("Expected 3 errors, got %d, %v", len(res), res)
    96  	}
    97  
    98  	if !strings.Contains(res[0].Err.Error(), ".Release.Time has been removed in v3") {
    99  		t.Errorf("Unexpected error: %s", res[0].Err)
   100  	}
   101  	if !strings.Contains(res[1].Err.Error(), "manifest is a crd-install hook") {
   102  		t.Errorf("Unexpected error: %s", res[1].Err)
   103  	}
   104  	if !strings.Contains(res[2].Err.Error(), "manifest is a crd-install hook") {
   105  		t.Errorf("Unexpected error: %s", res[2].Err)
   106  	}
   107  }
   108  
   109  func TestMultiTemplateFail(t *testing.T) {
   110  	linter := support.Linter{ChartDir: "./testdata/multi-template-fail"}
   111  	Templates(&linter, values, namespace, strict)
   112  	res := linter.Messages
   113  
   114  	if len(res) != 1 {
   115  		t.Fatalf("Expected 1 error, got %d, %v", len(res), res)
   116  	}
   117  
   118  	if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") {
   119  		t.Errorf("Unexpected error: %s", res[0].Err)
   120  	}
   121  }
   122  
   123  func TestValidateMetadataName(t *testing.T) {
   124  	tests := []struct {
   125  		obj     *K8sYamlStruct
   126  		wantErr bool
   127  	}{
   128  		// Most kinds use IsDNS1123Subdomain.
   129  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true},
   130  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   131  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
   132  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
   133  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
   134  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true},
   135  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false},
   136  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true},
   137  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true},
   138  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true},
   139  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true},
   140  		{&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
   141  		{&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   142  		{&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
   143  		{&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
   144  		{&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true},
   145  
   146  		// Service uses IsDNS1035Label.
   147  		{&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   148  		{&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true},
   149  		{&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true},
   150  
   151  		// Namespace uses IsDNS1123Label.
   152  		{&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   153  		{&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
   154  		{&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true},
   155  		{&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false},
   156  
   157  		// CertificateSigningRequest has no validation.
   158  		{&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false},
   159  		{&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
   160  		{&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false},
   161  
   162  		// RBAC uses path validation.
   163  		{&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   164  		{&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
   165  		{&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false},
   166  		{&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
   167  		{&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true},
   168  		{&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true},
   169  		{&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   170  		{&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
   171  		{&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false},
   172  		{&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
   173  		{&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true},
   174  		{&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true},
   175  		{&K8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
   176  		{&K8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false},
   177  
   178  		// Unknown Kind
   179  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true},
   180  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   181  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false},
   182  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true},
   183  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false},
   184  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true},
   185  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false},
   186  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true},
   187  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true},
   188  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true},
   189  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true},
   190  		{&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
   191  
   192  		// No kind
   193  		{&K8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false},
   194  		{&K8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true},
   195  	}
   196  	for _, tt := range tests {
   197  		t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) {
   198  			if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr {
   199  				t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr)
   200  			}
   201  		})
   202  	}
   203  }
   204  
   205  func TestDeprecatedAPIFails(t *testing.T) {
   206  	mychart := chart.Chart{
   207  		Metadata: &chart.Metadata{
   208  			APIVersion: "v2",
   209  			Name:       "failapi",
   210  			Version:    "0.1.0",
   211  			Icon:       "satisfy-the-linting-gods.gif",
   212  		},
   213  		Templates: []*chart.File{
   214  			{
   215  				Name: "templates/baddeployment.yaml",
   216  				Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n  name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
   217  			},
   218  			{
   219  				Name: "templates/goodsecret.yaml",
   220  				Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n  name: goodsecret"),
   221  			},
   222  		},
   223  	}
   224  	tmpdir := ensure.TempDir(t)
   225  	defer os.RemoveAll(tmpdir)
   226  
   227  	if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
   228  		t.Fatal(err)
   229  	}
   230  
   231  	linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
   232  	Templates(&linter, values, namespace, strict)
   233  	if l := len(linter.Messages); l != 1 {
   234  		for i, msg := range linter.Messages {
   235  			t.Logf("Message %d: %s", i, msg)
   236  		}
   237  		t.Fatalf("Expected 1 lint error, got %d", l)
   238  	}
   239  
   240  	err := linter.Messages[0].Err.(deprecatedAPIError)
   241  	if err.Deprecated != "apps/v1beta1 Deployment" {
   242  		t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated)
   243  	}
   244  }
   245  
   246  const manifest = `apiVersion: v1
   247  kind: ConfigMap
   248  metadata:
   249    name: foo
   250  data:
   251    myval1: {{default "val" .Values.mymap.key1 }}
   252    myval2: {{default "val" .Values.mymap.key2 }}
   253  `
   254  
   255  // TestStrictTemplateParsingMapError is a regression test.
   256  //
   257  // The template engine should not produce an error when a map in values.yaml does
   258  // not contain all possible keys.
   259  //
   260  // See https://github.com/helm/helm/issues/7483
   261  func TestStrictTemplateParsingMapError(t *testing.T) {
   262  
   263  	ch := chart.Chart{
   264  		Metadata: &chart.Metadata{
   265  			Name:       "regression7483",
   266  			APIVersion: "v2",
   267  			Version:    "0.1.0",
   268  		},
   269  		Values: map[string]interface{}{
   270  			"mymap": map[string]string{
   271  				"key1": "val1",
   272  			},
   273  		},
   274  		Templates: []*chart.File{
   275  			{
   276  				Name: "templates/configmap.yaml",
   277  				Data: []byte(manifest),
   278  			},
   279  		},
   280  	}
   281  	dir := ensure.TempDir(t)
   282  	defer os.RemoveAll(dir)
   283  	if err := chartutil.SaveDir(&ch, dir); err != nil {
   284  		t.Fatal(err)
   285  	}
   286  	linter := &support.Linter{
   287  		ChartDir: filepath.Join(dir, ch.Metadata.Name),
   288  	}
   289  	Templates(linter, ch.Values, namespace, strict)
   290  	if len(linter.Messages) != 0 {
   291  		t.Errorf("expected zero messages, got %d", len(linter.Messages))
   292  		for i, msg := range linter.Messages {
   293  			t.Logf("Message %d: %q", i, msg)
   294  		}
   295  	}
   296  }
   297  
   298  func TestValidateMatchSelector(t *testing.T) {
   299  	md := &K8sYamlStruct{
   300  		APIVersion: "apps/v1",
   301  		Kind:       "Deployment",
   302  		Metadata: k8sYamlMetadata{
   303  			Name: "mydeployment",
   304  		},
   305  	}
   306  	manifest := `
   307  	apiVersion: apps/v1
   308  kind: Deployment
   309  metadata:
   310    name: nginx-deployment
   311    labels:
   312      app: nginx
   313  spec:
   314    replicas: 3
   315    selector:
   316      matchLabels:
   317        app: nginx
   318    template:
   319      metadata:
   320        labels:
   321          app: nginx
   322      spec:
   323        containers:
   324        - name: nginx
   325          image: nginx:1.14.2
   326  	`
   327  	if err := validateMatchSelector(md, manifest); err != nil {
   328  		t.Error(err)
   329  	}
   330  	manifest = `
   331  	apiVersion: apps/v1
   332  kind: Deployment
   333  metadata:
   334    name: nginx-deployment
   335    labels:
   336      app: nginx
   337  spec:
   338    replicas: 3
   339    selector:
   340      matchExpressions:
   341        app: nginx
   342    template:
   343      metadata:
   344        labels:
   345          app: nginx
   346      spec:
   347        containers:
   348        - name: nginx
   349          image: nginx:1.14.2
   350  	`
   351  	if err := validateMatchSelector(md, manifest); err != nil {
   352  		t.Error(err)
   353  	}
   354  	manifest = `
   355  	apiVersion: apps/v1
   356  kind: Deployment
   357  metadata:
   358    name: nginx-deployment
   359    labels:
   360      app: nginx
   361  spec:
   362    replicas: 3
   363    template:
   364      metadata:
   365        labels:
   366          app: nginx
   367      spec:
   368        containers:
   369        - name: nginx
   370          image: nginx:1.14.2
   371  	`
   372  	if err := validateMatchSelector(md, manifest); err == nil {
   373  		t.Error("expected Deployment with no selector to fail")
   374  	}
   375  }
   376  
   377  func TestValidateTopIndentLevel(t *testing.T) {
   378  	for doc, shouldFail := range map[string]bool{
   379  		// Should not fail
   380  		"\n\n\n\t\n   \t\n":          false,
   381  		"apiVersion:foo\n  bar:baz":  false,
   382  		"\n\n\napiVersion:foo\n\n\n": false,
   383  		// Should fail
   384  		"  apiVersion:foo":         true,
   385  		"\n\n  apiVersion:foo\n\n": true,
   386  	} {
   387  		if err := validateTopIndentLevel(doc); (err == nil) == shouldFail {
   388  			t.Errorf("Expected %t for %q", shouldFail, doc)
   389  		}
   390  	}
   391  
   392  }
   393  
   394  // TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments
   395  // See https://github.com/helm/helm/issues/8621
   396  func TestEmptyWithCommentsManifests(t *testing.T) {
   397  	mychart := chart.Chart{
   398  		Metadata: &chart.Metadata{
   399  			APIVersion: "v2",
   400  			Name:       "emptymanifests",
   401  			Version:    "0.1.0",
   402  			Icon:       "satisfy-the-linting-gods.gif",
   403  		},
   404  		Templates: []*chart.File{
   405  			{
   406  				Name: "templates/empty-with-comments.yaml",
   407  				Data: []byte("#@formatter:off\n"),
   408  			},
   409  		},
   410  	}
   411  	tmpdir := ensure.TempDir(t)
   412  	defer os.RemoveAll(tmpdir)
   413  
   414  	if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
   415  		t.Fatal(err)
   416  	}
   417  
   418  	linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
   419  	Templates(&linter, values, namespace, strict)
   420  	if l := len(linter.Messages); l > 0 {
   421  		for i, msg := range linter.Messages {
   422  			t.Logf("Message %d: %s", i, msg)
   423  		}
   424  		t.Fatalf("Expected 0 lint errors, got %d", l)
   425  	}
   426  }
   427  func TestValidateListAnnotations(t *testing.T) {
   428  	md := &K8sYamlStruct{
   429  		APIVersion: "v1",
   430  		Kind:       "List",
   431  		Metadata: k8sYamlMetadata{
   432  			Name: "list",
   433  		},
   434  	}
   435  	manifest := `
   436  apiVersion: v1
   437  kind: List
   438  items:
   439    - apiVersion: v1
   440      kind: ConfigMap
   441      metadata:
   442        annotations:
   443          helm.sh/resource-policy: keep
   444  `
   445  
   446  	if err := validateListAnnotations(md, manifest); err == nil {
   447  		t.Fatal("expected list with nested keep annotations to fail")
   448  	}
   449  
   450  	manifest = `
   451  apiVersion: v1
   452  kind: List
   453  metadata:
   454    annotations:
   455      helm.sh/resource-policy: keep
   456  items:
   457    - apiVersion: v1
   458      kind: ConfigMap
   459  `
   460  
   461  	if err := validateListAnnotations(md, manifest); err != nil {
   462  		t.Fatalf("List objects keep annotations should pass. got: %s", err)
   463  	}
   464  }