github.com/buildpacks/libcnb@v1.30.3/build_test.go (about) 1 /* 2 * Copyright 2018-2020 the original author or 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 * https://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 libcnb_test 18 19 import ( 20 "bytes" 21 "fmt" 22 "os" 23 "path/filepath" 24 "testing" 25 "text/template" 26 27 . "github.com/onsi/gomega" 28 "github.com/sclevine/spec" 29 "github.com/stretchr/testify/mock" 30 31 "github.com/buildpacks/libcnb" 32 "github.com/buildpacks/libcnb/internal" 33 "github.com/buildpacks/libcnb/mocks" 34 ) 35 36 func testBuild(t *testing.T, context spec.G, it spec.S) { 37 var ( 38 Expect = NewWithT(t).Expect 39 40 applicationPath string 41 builder *mocks.Builder 42 buildpackPath string 43 buildpackPlanPath string 44 bpTOMLContents string 45 commandPath string 46 environmentWriter *mocks.EnvironmentWriter 47 exitHandler *mocks.ExitHandler 48 layerContributor *mocks.LayerContributor 49 layersPath string 50 platformPath string 51 tomlWriter *mocks.TOMLWriter 52 buildpackTOML *template.Template 53 54 workingDir string 55 ) 56 57 it.Before(func() { 58 var err error 59 60 applicationPath = t.TempDir() 61 applicationPath, err = filepath.EvalSymlinks(applicationPath) 62 Expect(err).NotTo(HaveOccurred()) 63 64 builder = &mocks.Builder{} 65 66 buildpackPath = t.TempDir() 67 Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed()) 68 69 bpTOMLContents = ` 70 api = "{{.APIVersion}}" 71 72 [buildpack] 73 id = "test-id" 74 name = "test-name" 75 version = "1.1.1" 76 clear-env = true 77 description = "A test buildpack" 78 keywords = ["test", "buildpack"] 79 80 [[buildpack.licenses]] 81 type = "Apache-2.0" 82 uri = "https://spdx.org/licenses/Apache-2.0.html" 83 84 [[buildpack.licenses]] 85 type = "Apache-1.1" 86 uri = "https://spdx.org/licenses/Apache-1.1.html" 87 88 [[order]] 89 [[order.group]] 90 id = "test-id" 91 version = "2.2.2" 92 optional = true 93 94 [[stacks]] 95 id = "test-id" 96 mixins = ["test-name"] 97 98 [metadata] 99 test-key = "test-value" 100 ` 101 buildpackTOML, err = template.New("buildpack.toml").Parse(bpTOMLContents) 102 Expect(err).ToNot(HaveOccurred()) 103 104 var b bytes.Buffer 105 err = buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.6"}) 106 Expect(err).ToNot(HaveOccurred()) 107 108 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed()) 109 110 f, err := os.CreateTemp("", "build-buildpackplan-path") 111 Expect(err).NotTo(HaveOccurred()) 112 Expect(f.Close()).NotTo(HaveOccurred()) 113 buildpackPlanPath = f.Name() 114 115 Expect(os.WriteFile(buildpackPlanPath, 116 []byte(` 117 [[entries]] 118 name = "test-name" 119 version = "test-version" 120 121 [entries.metadata] 122 test-key = "test-value" 123 `), 124 0600), 125 ).To(Succeed()) 126 127 commandPath = filepath.Join("bin", "build") 128 129 environmentWriter = &mocks.EnvironmentWriter{} 130 environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil) 131 132 exitHandler = &mocks.ExitHandler{} 133 exitHandler.On("Error", mock.Anything) 134 135 layerContributor = &mocks.LayerContributor{} 136 137 layersPath = t.TempDir() 138 139 Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"), 140 []byte(` 141 [metadata] 142 test-key = "test-value" 143 `), 144 0600), 145 ).To(Succeed()) 146 147 platformPath = t.TempDir() 148 149 Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed()) 150 Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"), 151 []byte("test-secret-value"), 0600)).To(Succeed()) 152 153 Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed()) 154 Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)). 155 To(Succeed()) 156 157 tomlWriter = &mocks.TOMLWriter{} 158 tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil) 159 160 Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed()) 161 Expect(os.Setenv("CNB_LAYERS_DIR", layersPath)).To(Succeed()) 162 Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed()) 163 Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed()) 164 165 workingDir, err = os.Getwd() 166 Expect(err).NotTo(HaveOccurred()) 167 Expect(os.Chdir(applicationPath)).To(Succeed()) 168 }) 169 170 it.After(func() { 171 Expect(os.Chdir(workingDir)).To(Succeed()) 172 Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) 173 Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed()) 174 Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed()) 175 Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed()) 176 Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed()) 177 178 Expect(os.RemoveAll(applicationPath)).To(Succeed()) 179 Expect(os.RemoveAll(buildpackPath)).To(Succeed()) 180 Expect(os.RemoveAll(buildpackPlanPath)).To(Succeed()) 181 Expect(os.RemoveAll(layersPath)).To(Succeed()) 182 Expect(os.RemoveAll(platformPath)).To(Succeed()) 183 }) 184 185 context("buildpack API is not within the supported range", func() { 186 it.Before(func() { 187 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 188 []byte(` 189 api = "0.4" 190 191 [buildpack] 192 id = "test-id" 193 name = "test-name" 194 version = "1.1.1" 195 `), 196 0600), 197 ).To(Succeed()) 198 }) 199 200 it("fails", func() { 201 libcnb.Build(builder, 202 libcnb.WithBOMLabel(true), 203 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 204 libcnb.WithExitHandler(exitHandler), 205 ) 206 207 Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError( 208 fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion), 209 )) 210 }) 211 }) 212 213 context("errors if required env vars are not set for buildpack API >=0.8", func() { 214 for _, e := range []string{"CNB_LAYERS_DIR", "CNB_PLATFORM_DIR", "CNB_BP_PLAN_PATH"} { 215 // We need to do this assignment because of the way that spec binds variables 216 envVar := e 217 context(fmt.Sprintf("when %s is unset", envVar), func() { 218 it.Before(func() { 219 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 220 []byte(` 221 api = "0.8" 222 223 [buildpack] 224 id = "test-id" 225 name = "test-name" 226 version = "1.1.1" 227 `), 228 0600), 229 ).To(Succeed()) 230 os.Unsetenv(envVar) 231 }) 232 233 it("fails", func() { 234 libcnb.Build(builder, 235 libcnb.WithBOMLabel(true), 236 libcnb.WithArguments([]string{commandPath}), 237 libcnb.WithExitHandler(exitHandler), 238 ) 239 Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError( 240 fmt.Sprintf("expected %s to be set", envVar), 241 )) 242 }) 243 }) 244 } 245 }) 246 247 it("encounters the wrong number of arguments", func() { 248 builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil) 249 250 libcnb.Build(builder, 251 libcnb.WithBOMLabel(true), 252 libcnb.WithArguments([]string{commandPath}), 253 libcnb.WithExitHandler(exitHandler), 254 ) 255 256 Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected 3 arguments and received 0")) 257 }) 258 259 context("when BP API >= 0.8", func() { 260 it.Before(func() { 261 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 262 []byte(` 263 api = "0.8" 264 265 [buildpack] 266 id = "test-id" 267 name = "test-name" 268 version = "1.1.1" 269 `), 270 0600), 271 ).To(Succeed()) 272 }) 273 274 it("creates context", func() { 275 builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil) 276 277 libcnb.Build(builder, 278 libcnb.WithBOMLabel(true), 279 libcnb.WithArguments([]string{commandPath}), 280 ) 281 282 ctx := builder.Calls[0].Arguments[0].(libcnb.BuildContext) 283 Expect(ctx.Application).To(Equal(libcnb.Application{Path: applicationPath})) 284 Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{ 285 API: "0.8", 286 Info: libcnb.BuildpackInfo{ 287 ID: "test-id", 288 Name: "test-name", 289 Version: "1.1.1", 290 }, 291 Path: buildpackPath, 292 })) 293 Expect(ctx.Layers).To(Equal(libcnb.Layers{Path: layersPath})) 294 Expect(ctx.PersistentMetadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) 295 Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{ 296 Entries: []libcnb.BuildpackPlanEntry{ 297 { 298 Name: "test-name", 299 Metadata: map[string]interface{}{ 300 "test-key": "test-value", 301 }, 302 }, 303 }, 304 })) 305 Expect(ctx.Platform).To(Equal(libcnb.Platform{ 306 Bindings: libcnb.Bindings{ 307 libcnb.Binding{ 308 Name: "alpha", 309 Path: filepath.Join(platformPath, "bindings", "alpha"), 310 Secret: map[string]string{ 311 "test-secret-key": "test-secret-value", 312 }, 313 }, 314 }, 315 Environment: map[string]string{"TEST_ENV": "test-value"}, 316 Path: platformPath, 317 })) 318 Expect(ctx.StackID).To(Equal("test-stack-id")) 319 }) 320 }) 321 322 context("when BP API < 0.8", func() { 323 it.Before(func() { 324 Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed()) 325 Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed()) 326 Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed()) 327 }) 328 329 it("creates context", func() { 330 builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil) 331 332 libcnb.Build(builder, 333 libcnb.WithBOMLabel(true), 334 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 335 ) 336 337 ctx := builder.Calls[0].Arguments[0].(libcnb.BuildContext) 338 Expect(ctx.Application).To(Equal(libcnb.Application{Path: applicationPath})) 339 Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{ 340 API: "0.6", 341 Info: libcnb.BuildpackInfo{ 342 ID: "test-id", 343 Name: "test-name", 344 Version: "1.1.1", 345 ClearEnvironment: true, 346 Description: "A test buildpack", 347 Keywords: []string{"test", "buildpack"}, 348 Licenses: []libcnb.License{ 349 {Type: "Apache-2.0", URI: "https://spdx.org/licenses/Apache-2.0.html"}, 350 {Type: "Apache-1.1", URI: "https://spdx.org/licenses/Apache-1.1.html"}, 351 }, 352 }, 353 Path: buildpackPath, 354 Stacks: []libcnb.BuildpackStack{ 355 { 356 ID: "test-id", 357 Mixins: []string{"test-name"}, 358 }, 359 }, 360 Metadata: map[string]interface{}{"test-key": "test-value"}, 361 })) 362 Expect(ctx.Layers).To(Equal(libcnb.Layers{Path: layersPath})) 363 Expect(ctx.PersistentMetadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) 364 Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{ 365 Entries: []libcnb.BuildpackPlanEntry{ 366 { 367 Name: "test-name", 368 Metadata: map[string]interface{}{ 369 "test-key": "test-value", 370 }, 371 }, 372 }, 373 })) 374 Expect(ctx.Platform).To(Equal(libcnb.Platform{ 375 Bindings: libcnb.Bindings{ 376 libcnb.Binding{ 377 Name: "alpha", 378 Path: filepath.Join(platformPath, "bindings", "alpha"), 379 Secret: map[string]string{ 380 "test-secret-key": "test-secret-value", 381 }, 382 }, 383 }, 384 Environment: map[string]string{"TEST_ENV": "test-value"}, 385 Path: platformPath, 386 })) 387 Expect(ctx.StackID).To(Equal("test-stack-id")) 388 }) 389 }) 390 391 it("extracts buildpack path from command path if CNB_BUILDPACK_PATH is not set", func() { 392 Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) 393 394 builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil) 395 396 libcnb.Build(builder, 397 libcnb.WithBOMLabel(true), 398 libcnb.WithArguments([]string{filepath.Join(buildpackPath, commandPath), layersPath, platformPath, buildpackPlanPath}), 399 ) 400 401 ctx := builder.Calls[0].Arguments[0].(libcnb.BuildContext) 402 403 Expect(ctx.Buildpack.Path).To(Equal(buildpackPath)) 404 }) 405 406 it("handles error from BuildFunc", func() { 407 builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), fmt.Errorf("test-error")) 408 409 libcnb.Build(builder, 410 libcnb.WithBOMLabel(true), 411 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 412 libcnb.WithExitHandler(exitHandler), 413 ) 414 415 Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error")) 416 }) 417 418 it("calls layer contributor", func() { 419 layerContributor.On("Contribute", mock.Anything).Return(libcnb.Layer{}, nil) 420 layerContributor.On("Name").Return("test-name") 421 builder.On("Build", mock.Anything). 422 Return(libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}, nil) 423 424 libcnb.Build(builder, 425 libcnb.WithBOMLabel(true), 426 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 427 libcnb.WithTOMLWriter(tomlWriter), 428 ) 429 430 Expect(layerContributor.Calls).To(HaveLen(2)) 431 }) 432 433 it("writes env.build", func() { 434 layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), BuildEnvironment: libcnb.Environment{}} 435 layer.BuildEnvironment.Defaultf("test-build", "test-%s", "value") 436 layerContributor.On("Contribute", mock.Anything).Return(layer, nil) 437 layerContributor.On("Name").Return("test-name") 438 result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}} 439 builder.On("Build", mock.Anything).Return(result, nil) 440 441 libcnb.Build(builder, 442 libcnb.WithBOMLabel(true), 443 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 444 libcnb.WithEnvironmentWriter(environmentWriter), 445 ) 446 447 Expect(environmentWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "env.build"))) 448 Expect(environmentWriter.Calls[0].Arguments[1]).To(Equal(map[string]string{"test-build.default": "test-value"})) 449 }) 450 451 it("writes env.launch", func() { 452 layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), LaunchEnvironment: libcnb.Environment{}} 453 layer.LaunchEnvironment.Defaultf("test-launch", "test-%s", "value") 454 layerContributor.On("Contribute", mock.Anything).Return(layer, nil) 455 layerContributor.On("Name").Return("test-name") 456 result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}} 457 builder.On("Build", mock.Anything).Return(result, nil) 458 459 libcnb.Build(builder, 460 libcnb.WithBOMLabel(true), 461 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 462 libcnb.WithEnvironmentWriter(environmentWriter), 463 ) 464 465 Expect(environmentWriter.Calls[1].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "env.launch"))) 466 Expect(environmentWriter.Calls[1].Arguments[1]).To(Equal(map[string]string{"test-launch.default": "test-value"})) 467 }) 468 469 it("writes env", func() { 470 layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), SharedEnvironment: libcnb.Environment{}} 471 layer.SharedEnvironment.Defaultf("test-shared", "test-%s", "value") 472 layerContributor.On("Contribute", mock.Anything).Return(layer, nil) 473 layerContributor.On("Name").Return("test-name") 474 result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}} 475 builder.On("Build", mock.Anything).Return(result, nil) 476 477 libcnb.Build(builder, 478 libcnb.WithBOMLabel(true), 479 480 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 481 libcnb.WithEnvironmentWriter(environmentWriter), 482 ) 483 484 Expect(environmentWriter.Calls[2].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "env"))) 485 Expect(environmentWriter.Calls[2].Arguments[1]).To(Equal(map[string]string{"test-shared.default": "test-value"})) 486 }) 487 488 it("writes profile.d", func() { 489 layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), Profile: libcnb.Profile{}} 490 layer.Profile.Addf("test-profile", "test-%s", "value") 491 layerContributor.On("Contribute", mock.Anything).Return(layer, nil) 492 layerContributor.On("Name").Return("test-name") 493 result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}} 494 builder.On("Build", mock.Anything).Return(result, nil) 495 496 libcnb.Build(builder, 497 libcnb.WithBOMLabel(true), 498 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 499 libcnb.WithEnvironmentWriter(environmentWriter), 500 ) 501 502 Expect(environmentWriter.Calls[3].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "profile.d"))) 503 Expect(environmentWriter.Calls[3].Arguments[1]).To(Equal(map[string]string{"test-profile": "test-value"})) 504 }) 505 506 it("writes 0.5 layer metadata", func() { 507 var b bytes.Buffer 508 err := buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.5"}) 509 Expect(err).ToNot(HaveOccurred()) 510 511 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed()) 512 513 layer := libcnb.Layer{ 514 Name: "test-name", 515 Path: filepath.Join(layersPath, "test-name"), 516 LayerTypes: libcnb.LayerTypes{ 517 Build: true, 518 Cache: true, 519 Launch: true, 520 }, 521 Metadata: map[string]interface{}{"test-key": "test-value"}, 522 } 523 layerContributor.On("Contribute", mock.Anything).Return(layer, nil) 524 layerContributor.On("Name").Return("test-name") 525 result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}} 526 builder.On("Build", mock.Anything).Return(result, nil) 527 528 libcnb.Build(builder, 529 libcnb.WithBOMLabel(true), 530 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 531 libcnb.WithTOMLWriter(tomlWriter), 532 ) 533 534 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name.toml"))) 535 536 layer5, ok := tomlWriter.Calls[0].Arguments[1].(internal.LayerAPI5) 537 Expect(ok).To(BeTrue()) 538 Expect(layer5.Build).To(BeTrue()) 539 Expect(layer5.Cache).To(BeTrue()) 540 Expect(layer5.Launch).To(BeTrue()) 541 Expect(layer5.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) 542 }) 543 544 it("writes 0.6 layer metadata", func() { 545 layer := libcnb.Layer{ 546 Name: "test-name", 547 Path: filepath.Join(layersPath, "test-name"), 548 LayerTypes: libcnb.LayerTypes{ 549 Build: true, 550 Cache: true, 551 Launch: true, 552 }, 553 Metadata: map[string]interface{}{"test-key": "test-value"}, 554 } 555 layerContributor.On("Contribute", mock.Anything).Return(layer, nil) 556 layerContributor.On("Name").Return("test-name") 557 result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}} 558 builder.On("Build", mock.Anything).Return(result, nil) 559 560 libcnb.Build(builder, 561 libcnb.WithBOMLabel(true), 562 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 563 libcnb.WithTOMLWriter(tomlWriter), 564 ) 565 566 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name.toml"))) 567 568 layer, ok := tomlWriter.Calls[0].Arguments[1].(libcnb.Layer) 569 Expect(ok).To(BeTrue()) 570 Expect(layer.LayerTypes.Build).To(BeTrue()) 571 Expect(layer.LayerTypes.Cache).To(BeTrue()) 572 Expect(layer.LayerTypes.Launch).To(BeTrue()) 573 Expect(layer.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) 574 }) 575 576 it("writes launch.toml", func() { 577 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{ 578 BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{ 579 { 580 Name: "test-launch-bom-entry", 581 Metadata: map[string]interface{}{"test-key": "test-value"}, 582 Launch: true, 583 }, 584 { 585 Name: "test-build-bom-entry", 586 Metadata: map[string]interface{}{"test-key": "test-value"}, 587 }, 588 }}, 589 Labels: []libcnb.Label{ 590 { 591 Key: "test-key", 592 Value: "test-value", 593 }, 594 }, 595 Processes: []libcnb.Process{ 596 { 597 Type: "test-type", 598 Command: "test-command", 599 Default: true, 600 }, 601 }, 602 Slices: []libcnb.Slice{ 603 { 604 Paths: []string{"test-path"}, 605 }, 606 }, 607 }, nil) 608 609 libcnb.Build(builder, 610 libcnb.WithBOMLabel(true), 611 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 612 libcnb.WithTOMLWriter(tomlWriter), 613 ) 614 615 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml"))) 616 Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{ 617 Labels: []libcnb.Label{ 618 { 619 Key: "test-key", 620 Value: "test-value", 621 }, 622 }, 623 Processes: []libcnb.Process{ 624 { 625 Type: "test-type", 626 Command: "test-command", 627 Default: true, 628 }, 629 }, 630 Slices: []libcnb.Slice{ 631 { 632 Paths: []string{"test-path"}, 633 }, 634 }, 635 BOM: []libcnb.BOMEntry{ 636 { 637 Name: "test-launch-bom-entry", 638 Metadata: map[string]interface{}{"test-key": "test-value"}, 639 Launch: true, 640 }, 641 }, 642 })) 643 }) 644 645 it("ignore working-directory setting and writes launch.toml (API<0.8)", func() { 646 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{ 647 Processes: []libcnb.Process{ 648 { 649 Type: "test-type", 650 Command: "test-command-in-dir", 651 Default: true, 652 WorkingDirectory: "/my/directory/", 653 }, 654 }, 655 }, nil) 656 657 libcnb.Build(builder, 658 libcnb.WithBOMLabel(true), 659 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 660 libcnb.WithTOMLWriter(tomlWriter), 661 ) 662 663 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml"))) 664 Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{ 665 Processes: []libcnb.Process{ 666 { 667 Type: "test-type", 668 Command: "test-command-in-dir", 669 Default: true, 670 }, 671 }, 672 })) 673 }) 674 675 it("writes launch.toml with working-directory setting(API>=0.8)", func() { 676 var b bytes.Buffer 677 err := buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.8"}) 678 Expect(err).ToNot(HaveOccurred()) 679 680 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed()) 681 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{ 682 Processes: []libcnb.Process{ 683 { 684 Type: "test-type", 685 Command: "test-command-in-dir", 686 Default: true, 687 WorkingDirectory: "/my/directory/", 688 }, 689 }, 690 }, nil) 691 692 libcnb.Build(builder, 693 libcnb.WithBOMLabel(true), 694 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 695 libcnb.WithTOMLWriter(tomlWriter), 696 ) 697 698 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml"))) 699 Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{ 700 Processes: []libcnb.Process{ 701 { 702 Type: "test-type", 703 Command: "test-command-in-dir", 704 Default: true, 705 WorkingDirectory: "/my/directory/", 706 }, 707 }, 708 })) 709 }) 710 711 it("writes persistent metadata", func() { 712 m := map[string]interface{}{"test-key": "test-value"} 713 714 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{PersistentMetadata: m}, nil) 715 716 libcnb.Build(builder, 717 libcnb.WithBOMLabel(true), 718 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 719 libcnb.WithTOMLWriter(tomlWriter), 720 ) 721 722 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "store.toml"))) 723 Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.Store{Metadata: m})) 724 }) 725 726 it("does not write empty files", func() { 727 builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil) 728 729 libcnb.Build(builder, 730 libcnb.WithBOMLabel(true), 731 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 732 libcnb.WithTOMLWriter(tomlWriter), 733 ) 734 735 Expect(tomlWriter.Calls).To(HaveLen(0)) 736 }) 737 738 it("removes stale layers", func() { 739 Expect(os.WriteFile(filepath.Join(layersPath, "alpha.toml"), []byte(""), 0600)).To(Succeed()) 740 Expect(os.WriteFile(filepath.Join(layersPath, "bravo.toml"), []byte(""), 0600)).To(Succeed()) 741 Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"), []byte(""), 0600)).To(Succeed()) 742 743 layer := libcnb.Layer{Name: "alpha"} 744 layerContributor.On("Contribute", mock.Anything).Return(layer, nil) 745 layerContributor.On("Name").Return("alpha") 746 747 builder.On("Build", mock.Anything). 748 Return(libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}, nil) 749 750 libcnb.Build(builder, 751 libcnb.WithBOMLabel(true), 752 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 753 libcnb.WithTOMLWriter(tomlWriter), 754 ) 755 756 Expect(tomlWriter.Calls).To(HaveLen(1)) 757 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "alpha.toml"))) 758 Expect(filepath.Join(layersPath, "bravo.toml")).NotTo(BeARegularFile()) 759 Expect(filepath.Join(layersPath, "store.toml")).To(BeARegularFile()) 760 }) 761 762 it("writes build.toml", func() { 763 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{ 764 BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{ 765 { 766 Name: "test-build-bom-entry", 767 Metadata: map[string]interface{}{"test-key": "test-value"}, 768 Build: true, 769 }, 770 { 771 Name: "test-launch-bom-entry", 772 Metadata: map[string]interface{}{"test-key": "test-value"}, 773 Build: false, 774 }, 775 }}, 776 Unmet: []libcnb.UnmetPlanEntry{ 777 { 778 Name: "test-entry", 779 }, 780 }, 781 }, nil) 782 783 libcnb.Build(builder, 784 libcnb.WithBOMLabel(true), 785 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 786 libcnb.WithTOMLWriter(tomlWriter), 787 ) 788 789 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "build.toml"))) 790 Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.BuildTOML{ 791 BOM: []libcnb.BOMEntry{ 792 { 793 Name: "test-build-bom-entry", 794 Metadata: map[string]interface{}{"test-key": "test-value"}, 795 Build: true, 796 }, 797 }, 798 Unmet: []libcnb.UnmetPlanEntry{ 799 { 800 Name: "test-entry", 801 }, 802 }, 803 })) 804 }) 805 806 context("Config bomLabel is false", func() { 807 it.Before(func() { 808 var err error 809 810 buildpackTOML, err = template.New("buildpack.toml").Parse(bpTOMLContents) 811 Expect(err).ToNot(HaveOccurred()) 812 813 var b bytes.Buffer 814 err = buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.7"}) 815 Expect(err).ToNot(HaveOccurred()) 816 817 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed()) 818 }) 819 820 it("writes launch.toml without BOM entries", func() { 821 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{ 822 BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{ 823 { 824 Name: "test-launch-bom-entry", 825 Metadata: map[string]interface{}{"test-key": "test-value"}, 826 Launch: true, 827 }, 828 { 829 Name: "test-build-bom-entry", 830 Metadata: map[string]interface{}{"test-key": "test-value"}, 831 }, 832 }}, 833 Processes: []libcnb.Process{ 834 { 835 Type: "test-type", 836 Command: "test-command", 837 Default: true, 838 }, 839 }, 840 }, nil) 841 842 libcnb.Build(builder, 843 libcnb.WithBOMLabel(false), 844 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 845 libcnb.WithTOMLWriter(tomlWriter), 846 ) 847 848 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml"))) 849 Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{ 850 Processes: []libcnb.Process{ 851 { 852 Type: "test-type", 853 Command: "test-command", 854 Default: true, 855 }, 856 }, 857 BOM: nil, 858 })) 859 }) 860 861 it("writes build.toml without BOM entries", func() { 862 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{ 863 BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{ 864 { 865 Name: "test-build-bom-entry", 866 Metadata: map[string]interface{}{"test-key": "test-value"}, 867 Build: true, 868 }, 869 { 870 Name: "test-launch-bom-entry", 871 Metadata: map[string]interface{}{"test-key": "test-value"}, 872 Build: false, 873 }, 874 }}, 875 Unmet: []libcnb.UnmetPlanEntry{ 876 { 877 Name: "test-entry", 878 }, 879 }, 880 }, nil) 881 882 libcnb.Build(builder, 883 libcnb.WithBOMLabel(false), 884 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 885 libcnb.WithTOMLWriter(tomlWriter), 886 ) 887 888 Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "build.toml"))) 889 Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.BuildTOML{ 890 BOM: nil, 891 Unmet: []libcnb.UnmetPlanEntry{ 892 { 893 Name: "test-entry", 894 }, 895 }, 896 })) 897 }) 898 }) 899 900 context("Validates SBOM entries", func() { 901 it.Before(func() { 902 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 903 []byte(` 904 api = "0.7" 905 906 [buildpack] 907 id = "test-id" 908 name = "test-name" 909 version = "1.1.1" 910 sbom-formats = ["application/vnd.cyclonedx+json"] 911 `), 912 0600), 913 ).To(Succeed()) 914 915 builder.On("Build", mock.Anything).Return(libcnb.BuildResult{}, nil) 916 }) 917 918 it("has no SBOM files", func() { 919 libcnb.Build(builder, 920 libcnb.WithBOMLabel(true), 921 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 922 libcnb.WithExitHandler(exitHandler), 923 ) 924 925 Expect(exitHandler.Calls).To(BeEmpty()) 926 }) 927 928 it("has no accepted formats", func() { 929 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 930 []byte(` 931 api = "0.7" 932 933 [buildpack] 934 id = "test-id" 935 name = "test-name" 936 version = "1.1.1" 937 sbom-formats = [] 938 `), 939 0600), 940 ).To(Succeed()) 941 942 Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed()) 943 944 libcnb.Build(builder, 945 libcnb.WithBOMLabel(true), 946 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 947 libcnb.WithExitHandler(exitHandler), 948 ) 949 950 Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to find actual SBOM Type application/spdx+json in list of supported SBOM types []")) 951 }) 952 953 it("skips if API is not 0.7", func() { 954 Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 955 []byte(` 956 api = "0.6" 957 958 [buildpack] 959 id = "test-id" 960 name = "test-name" 961 version = "1.1.1" 962 sbom-formats = [] 963 `), 964 0600), 965 ).To(Succeed()) 966 967 Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed()) 968 969 libcnb.Build(builder, 970 libcnb.WithBOMLabel(true), 971 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 972 libcnb.WithExitHandler(exitHandler), 973 ) 974 975 Expect(exitHandler.Calls).To(BeEmpty()) 976 }) 977 978 it("has no matching formats", func() { 979 Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed()) 980 981 libcnb.Build(builder, 982 libcnb.WithBOMLabel(true), 983 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 984 libcnb.WithExitHandler(exitHandler), 985 ) 986 987 Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to find actual SBOM Type application/spdx+json in list of supported SBOM types [application/vnd.cyclonedx+json]")) 988 }) 989 990 it("has a matching format", func() { 991 Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.cdx.json"), []byte{}, 0600)).To(Succeed()) 992 Expect(os.WriteFile(filepath.Join(layersPath, "layer.sbom.cdx.json"), []byte{}, 0600)).To(Succeed()) 993 libcnb.Build(builder, 994 libcnb.WithBOMLabel(true), 995 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 996 libcnb.WithExitHandler(exitHandler), 997 ) 998 999 Expect(exitHandler.Calls).To(BeEmpty()) 1000 }) 1001 1002 it("has a junk format", func() { 1003 Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.random.json"), []byte{}, 0600)).To(Succeed()) 1004 Expect(os.WriteFile(filepath.Join(layersPath, "layer.sbom.cdx.json"), []byte{}, 0600)).To(Succeed()) 1005 libcnb.Build(builder, 1006 libcnb.WithBOMLabel(true), 1007 libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}), 1008 libcnb.WithExitHandler(exitHandler), 1009 ) 1010 1011 Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to parse SBOM unknown\nunable to translate from random.json to SBOMFormat")) 1012 }) 1013 }) 1014 }