github.com/Datadog/cnab-go@v0.3.3-beta1.0.20191007143216-bba4b7e723d0/bundle/bundle_test.go (about) 1 package bundle 2 3 import ( 4 "bytes" 5 "io/ioutil" 6 "testing" 7 8 "github.com/deislabs/cnab-go/bundle/definition" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 12 yaml "gopkg.in/yaml.v2" 13 ) 14 15 func TestReadTopLevelProperties(t *testing.T) { 16 json := `{ 17 "schemaVersion": "v1.0.0-WD", 18 "name": "foo", 19 "version": "1.0", 20 "images": {}, 21 "credentials": {}, 22 "custom": {} 23 }` 24 bundle, err := Unmarshal([]byte(json)) 25 if err != nil { 26 t.Fatal(err) 27 } 28 assert.Equal(t, "v1.0.0-WD", bundle.SchemaVersion) 29 if bundle.Name != "foo" { 30 t.Errorf("Expected name 'foo', got '%s'", bundle.Name) 31 } 32 if bundle.Version != "1.0" { 33 t.Errorf("Expected version '1.0', got '%s'", bundle.Version) 34 } 35 if len(bundle.Images) != 0 { 36 t.Errorf("Expected no images, got %d", len(bundle.Images)) 37 } 38 if len(bundle.Credentials) != 0 { 39 t.Errorf("Expected no credentials, got %d", len(bundle.Credentials)) 40 } 41 if len(bundle.Custom) != 0 { 42 t.Errorf("Expected no custom extensions, got %d", len(bundle.Custom)) 43 } 44 } 45 46 func TestReadImageProperties(t *testing.T) { 47 data, err := ioutil.ReadFile("../testdata/bundles/foo.json") 48 if err != nil { 49 t.Errorf("cannot read bundle file: %v", err) 50 } 51 52 bundle, err := Unmarshal(data) 53 if err != nil { 54 t.Fatal(err) 55 } 56 if len(bundle.Images) != 2 { 57 t.Errorf("Expected 2 images, got %d", len(bundle.Images)) 58 } 59 image1 := bundle.Images["image1"] 60 if image1.Description != "image1" { 61 t.Errorf("Expected description 'image1', got '%s'", image1.Description) 62 } 63 if image1.Image != "urn:image1uri" { 64 t.Errorf("Expected Image 'urn:image1uri', got '%s'", image1.Image) 65 } 66 } 67 68 func TestReadCredentialProperties(t *testing.T) { 69 data, err := ioutil.ReadFile("../testdata/bundles/foo.json") 70 if err != nil { 71 t.Errorf("cannot read bundle file: %v", err) 72 } 73 74 bundle, err := Unmarshal(data) 75 if err != nil { 76 t.Fatal(err) 77 } 78 if len(bundle.Credentials) != 3 { 79 t.Errorf("Expected 3 credentials, got %d", len(bundle.Credentials)) 80 } 81 f := bundle.Credentials["foo"] 82 if f.Path != "pfoo" { 83 t.Errorf("Expected path 'pfoo', got '%s'", f.Path) 84 } 85 if f.EnvironmentVariable != "" { 86 t.Errorf("Expected env '', got '%s'", f.EnvironmentVariable) 87 } 88 b := bundle.Credentials["bar"] 89 if b.Path != "" { 90 t.Errorf("Expected path '', got '%s'", b.Path) 91 } 92 if b.EnvironmentVariable != "ebar" { 93 t.Errorf("Expected env 'ebar', got '%s'", b.EnvironmentVariable) 94 } 95 q := bundle.Credentials["quux"] 96 if q.Path != "pquux" { 97 t.Errorf("Expected path 'pquux', got '%s'", q.Path) 98 } 99 if q.EnvironmentVariable != "equux" { 100 t.Errorf("Expected env 'equux', got '%s'", q.EnvironmentVariable) 101 } 102 } 103 104 func TestValuesOrDefaults(t *testing.T) { 105 is := assert.New(t) 106 vals := map[string]interface{}{ 107 "port": 8080, 108 "host": "localhost", 109 "enabled": true, 110 } 111 b := &Bundle{ 112 Definitions: map[string]*definition.Schema{ 113 "portType": { 114 Type: "integer", 115 Default: 1234, 116 }, 117 "hostType": { 118 Type: "string", 119 Default: "locahost.localdomain", 120 }, 121 "replicaCountType": { 122 Type: "integer", 123 Default: 3, 124 }, 125 "enabledType": { 126 Type: "boolean", 127 Default: false, 128 }, 129 }, 130 Parameters: map[string]Parameter{ 131 "port": { 132 Definition: "portType", 133 }, 134 "host": { 135 Definition: "hostType", 136 }, 137 "enabled": { 138 Definition: "enabledType", 139 }, 140 "replicaCount": { 141 Definition: "replicaCountType", 142 }, 143 }, 144 } 145 146 vod, err := ValuesOrDefaults(vals, b) 147 148 is.NoError(err) 149 is.True(vod["enabled"].(bool)) 150 is.Equal(vod["host"].(string), "localhost") 151 is.Equal(vod["port"].(int), 8080) 152 is.Equal(vod["replicaCount"].(int), 3) 153 154 // This should err out because of type problem 155 vals["replicaCount"] = "banana" 156 _, err = ValuesOrDefaults(vals, b) 157 is.Error(err) 158 159 // Check for panic when zero value Bundle is passed 160 _, err = ValuesOrDefaults(vals, &Bundle{}) 161 is.NoError(err) 162 } 163 164 func TestValuesOrDefaults_NoParameter(t *testing.T) { 165 is := assert.New(t) 166 vals := map[string]interface{}{} 167 b := &Bundle{} 168 vod, err := ValuesOrDefaults(vals, b) 169 is.NoError(err) 170 is.Len(vod, 0) 171 } 172 173 func TestValuesOrDefaults_Required(t *testing.T) { 174 is := assert.New(t) 175 vals := map[string]interface{}{ 176 "enabled": true, 177 } 178 b := &Bundle{ 179 Definitions: map[string]*definition.Schema{ 180 "minType": { 181 Type: "integer", 182 }, 183 "enabledType": { 184 Type: "boolean", 185 Default: false, 186 }, 187 }, 188 Parameters: map[string]Parameter{ 189 "minimum": { 190 Definition: "minType", 191 Required: true, 192 }, 193 "enabled": { 194 Definition: "enabledType", 195 }, 196 }, 197 } 198 199 _, err := ValuesOrDefaults(vals, b) 200 is.Error(err) 201 202 // It is unclear what the outcome should be when the user supplies 203 // empty values on purpose. For now, we will assume those meet the 204 // minimum definition of "required", and that other rules will 205 // correct for empty values. 206 // 207 // Example: It makes perfect sense for a user to specify --set minimum=0 208 // and in so doing meet the requirement that a value be specified. 209 vals["minimum"] = 0 210 res, err := ValuesOrDefaults(vals, b) 211 is.NoError(err) 212 is.Equal(0, res["minimum"]) 213 } 214 215 func TestValidateVersionTag(t *testing.T) { 216 is := assert.New(t) 217 218 img := InvocationImage{BaseImage{}} 219 b := Bundle{ 220 Version: "latest", 221 SchemaVersion: "99.98", 222 InvocationImages: []InvocationImage{img}, 223 } 224 225 err := b.Validate() 226 is.EqualError(err, "'latest' is not a valid bundle version") 227 } 228 229 func TestValidateSchemaVersion(t *testing.T) { 230 is := assert.New(t) 231 232 img := InvocationImage{BaseImage{}} 233 b := Bundle{ 234 Version: "0.1.0", 235 SchemaVersion: "99.98", 236 InvocationImages: []InvocationImage{img}, 237 } 238 239 err := b.Validate() 240 is.Nil(err, "valid bundle schema failed to validate") 241 } 242 243 func TestValidateSchemaVersionWithPrefix(t *testing.T) { 244 is := assert.New(t) 245 246 img := InvocationImage{BaseImage{}} 247 b := Bundle{ 248 Version: "0.1.0", 249 SchemaVersion: "v99.98", 250 InvocationImages: []InvocationImage{img}, 251 } 252 253 err := b.Validate() 254 is.Nil(err, "valid bundle schema failed to validate") 255 } 256 257 func TestValidateMissingSchemaVersion(t *testing.T) { 258 is := assert.New(t) 259 260 img := InvocationImage{BaseImage{}} 261 b := Bundle{ 262 Version: "0.1.0", 263 InvocationImages: []InvocationImage{img}, 264 } 265 266 err := b.Validate() 267 is.EqualError(err, "invalid bundle schema version \"\": Invalid Semantic Version") 268 } 269 270 func TestValidateInvalidSchemaVersion(t *testing.T) { 271 is := assert.New(t) 272 273 img := InvocationImage{BaseImage{}} 274 b := Bundle{ 275 Version: "0.1.0", 276 SchemaVersion: ".1", 277 InvocationImages: []InvocationImage{img}, 278 } 279 280 err := b.Validate() 281 is.EqualError(err, "invalid bundle schema version \".1\": Invalid Semantic Version") 282 } 283 284 func TestValidateBundle_RequiresInvocationImage(t *testing.T) { 285 b := Bundle{ 286 Name: "bar", 287 SchemaVersion: "99.98", 288 Version: "0.1.0", 289 } 290 291 err := b.Validate() 292 if err == nil { 293 t.Fatal("Validate should have failed because the bundle has no invocation images") 294 } 295 296 b.InvocationImages = append(b.InvocationImages, InvocationImage{}) 297 298 err = b.Validate() 299 if err != nil { 300 t.Fatal(err) 301 } 302 } 303 304 func TestValidateRequiredExtensions(t *testing.T) { 305 is := assert.New(t) 306 307 img := InvocationImage{BaseImage{}} 308 b := Bundle{ 309 Version: "0.1.0", 310 SchemaVersion: "99.98", 311 InvocationImages: []InvocationImage{img}, 312 RequiredExtensions: []string{ 313 "my.custom.extension", 314 }, 315 } 316 317 // Verify the error when a required extension is not present in custom 318 err := b.Validate() 319 is.EqualError(err, "required extension 'my.custom.extension' is not defined in the Custom section of the bundle") 320 321 // Add corresponding entry in custom 322 b.Custom = map[string]interface{}{ 323 "my.custom.extension": true, 324 } 325 326 err = b.Validate() 327 is.NoError(err) 328 329 // Add duplicate required extension 330 b.RequiredExtensions = append(b.RequiredExtensions, "my.custom.extension") 331 332 err = b.Validate() 333 is.EqualError(err, "required extension 'my.custom.extension' is already declared") 334 } 335 336 func TestReadCustomAndRequiredExtensions(t *testing.T) { 337 data, err := ioutil.ReadFile("../testdata/bundles/foo.json") 338 if err != nil { 339 t.Errorf("cannot read bundle file: %v", err) 340 } 341 342 bundle, err := Unmarshal(data) 343 if err != nil { 344 t.Fatal(err) 345 } 346 347 if len(bundle.Custom) != 2 { 348 t.Errorf("Expected 2 custom extensions, got %d", len(bundle.Custom)) 349 } 350 351 duffleExtI, ok := bundle.Custom["com.example.duffle-bag"] 352 if !ok { 353 t.Fatal("Expected the com.example.duffle-bag extension") 354 } 355 duffleExt, ok := duffleExtI.(map[string]interface{}) 356 if !ok { 357 t.Fatalf("Expected the com.example.duffle-bag to be of type map[string]interface{} but got %T ", duffleExtI) 358 } 359 assert.Equal(t, "PNG", duffleExt["iconType"]) 360 assert.Equal(t, "https://example.com/icon.png", duffleExt["icon"]) 361 362 backupExtI, ok := bundle.Custom["com.example.backup-preferences"] 363 if !ok { 364 t.Fatal("Expected the com.example.backup-preferences extension") 365 } 366 backupExt, ok := backupExtI.(map[string]interface{}) 367 if !ok { 368 t.Fatalf("Expected the com.example.backup-preferences to be of type map[string]interface{} but got %T ", backupExtI) 369 } 370 assert.Equal(t, true, backupExt["enabled"]) 371 assert.Equal(t, "daily", backupExt["frequency"]) 372 373 if len(bundle.RequiredExtensions) != 1 { 374 t.Errorf("Expected 1 required extension, got %d", len(bundle.RequiredExtensions)) 375 } 376 assert.Equal(t, "com.example.duffle-bag", bundle.RequiredExtensions[0]) 377 } 378 379 func TestOutputs_Marshall(t *testing.T) { 380 bundleJSON := ` 381 { 382 "outputs":{ 383 "clientCert":{ 384 "contentEncoding":"base64", 385 "contentMediaType":"application/x-x509-user-cert", 386 "path":"/cnab/app/outputs/clientCert", 387 "definition":"clientCert" 388 }, 389 "hostName":{ 390 "applyTo":[ 391 "install" 392 ], 393 "description":"the hostname produced installing the bundle", 394 "path":"/cnab/app/outputs/hostname", 395 "definition":"hostType" 396 }, 397 "port":{ 398 "path":"/cnab/app/outputs/port", 399 "definition":"portType" 400 } 401 } 402 }` 403 404 bundle, err := Unmarshal([]byte(bundleJSON)) 405 assert.NoError(t, err, "should have unmarshalled the bundle") 406 require.NotNil(t, bundle.Outputs, "test must fail, not outputs found") 407 assert.Equal(t, 3, len(bundle.Outputs)) 408 409 clientCert, ok := bundle.Outputs["clientCert"] 410 require.True(t, ok, "expected clientCert to exist as an output") 411 assert.Equal(t, "clientCert", clientCert.Definition) 412 assert.Equal(t, "/cnab/app/outputs/clientCert", clientCert.Path, "clientCert path was not the expected value") 413 414 hostName, ok := bundle.Outputs["hostName"] 415 require.True(t, ok, "expected hostname to exist as an output") 416 assert.Equal(t, "hostType", hostName.Definition) 417 assert.Equal(t, "/cnab/app/outputs/hostname", hostName.Path, "hostName path was not the expected value") 418 419 port, ok := bundle.Outputs["port"] 420 require.True(t, ok, "expected port to exist as an output") 421 assert.Equal(t, "portType", port.Definition) 422 assert.Equal(t, "/cnab/app/outputs/port", port.Path, "port path was not the expected value") 423 } 424 425 var exampleCred = Credential{ 426 Description: "a password", 427 Location: Location{ 428 EnvironmentVariable: "PASSWORD", 429 Path: "/cnab/app/path", 430 }, 431 } 432 433 var exampleBundle = &Bundle{ 434 SchemaVersion: "v1.0.0-WD", 435 Name: "testBundle", 436 Description: "something", 437 Version: "1.0", 438 License: "MIT License", 439 Credentials: map[string]Credential{ 440 "password": exampleCred, 441 }, 442 Images: map[string]Image{ 443 "server": { 444 BaseImage: BaseImage{ 445 Image: "nginx:1.0", 446 ImageType: "docker", 447 }, 448 Description: "complicated", 449 }, 450 }, 451 InvocationImages: []InvocationImage{ 452 { 453 BaseImage: BaseImage{ 454 Image: "deislabs/invocation-image:1.0", 455 ImageType: "docker", 456 Labels: map[string]string{ 457 "os": "Linux", 458 }, 459 }, 460 }, 461 }, 462 Definitions: map[string]*definition.Schema{ 463 "portType": { 464 Type: "integer", 465 Default: 1234, 466 }, 467 "hostType": { 468 Type: "string", 469 Default: "locahost.localdomain", 470 }, 471 "replicaCountType": { 472 Type: "integer", 473 Default: 3, 474 }, 475 "enabledType": { 476 Type: "boolean", 477 Default: false, 478 }, 479 "clientCert": { 480 Type: "string", 481 ContentEncoding: "base64", 482 }, 483 "productKeyType": { 484 Type: "string", 485 }, 486 }, 487 Parameters: map[string]Parameter{ 488 "port": { 489 Definition: "portType", 490 Destination: &Location{ 491 EnvironmentVariable: "PORT", 492 Path: "/path/to/port", 493 }, 494 Required: true, 495 }, 496 "host": { 497 Definition: "hostType", 498 Destination: &Location{ 499 EnvironmentVariable: "HOST", 500 }, 501 Required: true, 502 }, 503 "enabled": { 504 Definition: "enabledType", 505 Destination: &Location{ 506 EnvironmentVariable: "ENABLED", 507 }, 508 }, 509 "replicaCount": { 510 Definition: "replicaCountType", 511 Destination: &Location{ 512 EnvironmentVariable: "REPLICA_COUNT", 513 }, 514 }, 515 "productKey": { 516 Definition: "productKeyType", 517 Destination: &Location{ 518 EnvironmentVariable: "PRODUCT_KEY", 519 }, 520 }, 521 }, 522 Outputs: map[string]Output{ 523 "clientCert": { 524 Path: "/cnab/app/outputs/blah", 525 Definition: "clientCert", 526 }, 527 }, 528 } 529 530 func TestBundleMarshallAllThings(t *testing.T) { 531 expectedJSON, err := ioutil.ReadFile("../testdata/bundles/canonical-bundle.json") 532 require.NoError(t, err, "couldn't read test data") 533 534 var buf bytes.Buffer 535 536 _, err = exampleBundle.WriteTo(&buf) 537 require.NoError(t, err, "test requires output") 538 assert.Equal(t, string(expectedJSON), buf.String(), "output should match expected canonical json") 539 } 540 541 func TestBundleYamlRoundtrip(t *testing.T) { 542 bytes, err := yaml.Marshal(exampleBundle) 543 require.NoError(t, err, "should have been able to yaml.Marshal bundle") 544 545 expectedYAML, err := ioutil.ReadFile("../testdata/bundles/bundle.yaml") 546 require.NoError(t, err, "couldn't read test data") 547 548 assert.Equal(t, string(expectedYAML), string(bytes), "marshaled bytes should match expected yaml representation") 549 550 var roundTripBun Bundle 551 err = yaml.UnmarshalStrict(bytes, &roundTripBun) 552 require.NoError(t, err, "should have been able to yaml.UnmarshalStrict bundle") 553 554 assert.Equal(t, exampleBundle, &roundTripBun, "after a roundtrip yaml marshal/unmarshal, the bundle does not match expected") 555 } 556 557 func TestValidateABundleAndParams(t *testing.T) { 558 559 bun, err := ioutil.ReadFile("../testdata/bundles/foo.json") 560 require.NoError(t, err, "couldn't read test bundle") 561 562 bundle, err := Unmarshal(bun) 563 require.NoError(t, err, "the bundle should have been valid") 564 565 def, ok := bundle.Definitions["complexThing"] 566 require.True(t, ok, "test failed because definition not found") 567 568 testData := struct { 569 Port int `json:"port"` 570 Host string `json:"hostName"` 571 }{ 572 Host: "validhost", 573 Port: 8080, 574 } 575 valErrors, err := def.Validate(testData) 576 assert.NoError(t, err, "validation should not have resulted in an error") 577 assert.Empty(t, valErrors, "validation should have been successful") 578 579 testData2 := struct { 580 Host string `json:"hostName"` 581 }{ 582 Host: "validhost", 583 } 584 valErrors, err = def.Validate(testData2) 585 assert.NoError(t, err, "validation should not have encountered an error") 586 assert.NotEmpty(t, valErrors, "validation should not have been successful") 587 588 testData3 := struct { 589 Port int `json:"port"` 590 Host string `json:"hostName"` 591 }{ 592 Host: "validhost", 593 Port: 80, 594 } 595 valErrors, err = def.Validate(testData3) 596 assert.NoError(t, err, "should not have encountered an error with the validator") 597 assert.NotEmpty(t, valErrors, "validation should not have been successful") 598 } 599 600 func TestBundle_RoundTrip(t *testing.T) { 601 testCases := []struct { 602 name string 603 testFile string 604 }{ 605 {name: "EmptyJson", testFile: "testdata/empty.json"}, 606 {name: "MinimalJson", testFile: "testdata/minimal.json"}, 607 } 608 for _, tc := range testCases { 609 t.Run(tc.name, func(t *testing.T) { 610 wantData, err := ioutil.ReadFile(tc.testFile) 611 if err != nil { 612 t.Fatal(err) 613 } 614 615 bun, err := Unmarshal(wantData) 616 if err != nil { 617 t.Fatal(err) 618 } 619 620 output := &bytes.Buffer{} 621 _, err = bun.WriteTo(output) 622 require.NoError(t, err, "writing the bundle to json failed") 623 624 gotData := output.String() 625 assert.Equal(t, string(wantData), gotData) 626 }) 627 } 628 } 629 630 func TestDigestPresent(t *testing.T) { 631 bun, err := ioutil.ReadFile("../testdata/bundles/digest.json") 632 require.NoError(t, err, "couldn't read test bundle") 633 634 bundle, err := Unmarshal(bun) 635 require.NoError(t, err, "the bundle should have been valid") 636 637 require.Equal(t, 1, len(bundle.InvocationImages), "there should be one invocation image in the bundle") 638 assert.Equal(t, 639 "sha256:decafbad71b4175951f29eb96035604c8cc372c99affa2e6d05cde6e8e20cc9a", 640 bundle.InvocationImages[0].Digest, 641 ) 642 643 image, ok := bundle.Images["my-microservice"] 644 require.True(t, ok, "there should been an image named my-microservice in the bundle") 645 assert.Equal( 646 t, 647 "sha256:beefcacef6c04336a17761db2004813982abe0e87ab727a376c291e09391ea61", 648 image.Digest, 649 ) 650 }