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 }