github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/client/inspect_builder_test.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "fmt" 6 "testing" 7 8 pubbldr "github.com/buildpacks/pack/builder" 9 10 "github.com/buildpacks/imgutil/fakes" 11 "github.com/buildpacks/lifecycle/api" 12 "github.com/golang/mock/gomock" 13 "github.com/google/go-cmp/cmp" 14 "github.com/heroku/color" 15 "github.com/pkg/errors" 16 "github.com/sclevine/spec" 17 "github.com/sclevine/spec/report" 18 19 "github.com/buildpacks/pack/internal/builder" 20 "github.com/buildpacks/pack/pkg/dist" 21 "github.com/buildpacks/pack/pkg/image" 22 "github.com/buildpacks/pack/pkg/logging" 23 "github.com/buildpacks/pack/pkg/testmocks" 24 h "github.com/buildpacks/pack/testhelpers" 25 ) 26 27 func TestInspectBuilder(t *testing.T) { 28 color.Disable(true) 29 defer color.Disable(false) 30 spec.Run(t, "InspectBuilder", testInspectBuilder, spec.Parallel(), spec.Report(report.Terminal{})) 31 } 32 33 func testInspectBuilder(t *testing.T, when spec.G, it spec.S) { 34 var ( 35 subject *Client 36 mockImageFetcher *testmocks.MockImageFetcher 37 mockController *gomock.Controller 38 builderImage *fakes.Image 39 out bytes.Buffer 40 assert = h.NewAssertionManager(t) 41 ) 42 43 it.Before(func() { 44 mockController = gomock.NewController(t) 45 mockImageFetcher = testmocks.NewMockImageFetcher(mockController) 46 47 subject = &Client{ 48 logger: logging.NewLogWithWriters(&out, &out), 49 imageFetcher: mockImageFetcher, 50 } 51 52 builderImage = fakes.NewImage("some/builder", "", nil) 53 assert.Succeeds(builderImage.SetLabel("io.buildpacks.stack.id", "test.stack.id")) 54 assert.Succeeds(builderImage.SetLabel( 55 "io.buildpacks.stack.mixins", 56 `["mixinOne", "build:mixinTwo", "mixinThree", "build:mixinFour"]`, 57 )) 58 assert.Succeeds(builderImage.SetEnv("CNB_USER_ID", "1234")) 59 assert.Succeeds(builderImage.SetEnv("CNB_GROUP_ID", "4321")) 60 }) 61 62 it.After(func() { 63 mockController.Finish() 64 }) 65 66 when("the image exists", func() { 67 for _, useDaemon := range []bool{true, false} { 68 useDaemon := useDaemon 69 when(fmt.Sprintf("daemon is %t", useDaemon), func() { 70 it.Before(func() { 71 if useDaemon { 72 mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/builder", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(builderImage, nil) 73 } else { 74 mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/builder", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(builderImage, nil) 75 } 76 }) 77 78 when("only deprecated lifecycle apis are present", func() { 79 it.Before(func() { 80 assert.Succeeds(builderImage.SetLabel( 81 "io.buildpacks.builder.metadata", 82 `{"lifecycle": {"version": "1.2.3", "api": {"buildpack": "1.2","platform": "2.3"}}}`, 83 )) 84 }) 85 86 it("returns has both deprecated and new fields", func() { 87 builderInfo, err := subject.InspectBuilder("some/builder", useDaemon) 88 assert.Nil(err) 89 90 assert.Equal(builderInfo.Lifecycle, builder.LifecycleDescriptor{ 91 Info: builder.LifecycleInfo{ 92 Version: builder.VersionMustParse("1.2.3"), 93 }, 94 API: builder.LifecycleAPI{ 95 BuildpackVersion: api.MustParse("1.2"), 96 PlatformVersion: api.MustParse("2.3"), 97 }, 98 APIs: builder.LifecycleAPIs{ 99 Buildpack: builder.APIVersions{Supported: builder.APISet{api.MustParse("1.2")}}, 100 Platform: builder.APIVersions{Supported: builder.APISet{api.MustParse("2.3")}}, 101 }, 102 }) 103 }) 104 }) 105 106 when("the builder image has appropriate metadata labels", func() { 107 it.Before(func() { 108 assert.Succeeds(builderImage.SetLabel("io.buildpacks.builder.metadata", `{ 109 "description": "Some description", 110 "stack": { 111 "runImage": { 112 "image": "some/run-image", 113 "mirrors": [ 114 "gcr.io/some/default" 115 ] 116 } 117 }, 118 "buildpacks": [ 119 { 120 "id": "test.nested", 121 "version": "test.nested.version", 122 "homepage": "http://geocities.com/top-bp" 123 }, 124 { 125 "id": "test.bp.one", 126 "version": "test.bp.one.version", 127 "homepage": "http://geocities.com/cool-bp", 128 "name": "one" 129 }, 130 { 131 "id": "test.bp.two", 132 "version": "test.bp.two.version" 133 }, 134 { 135 "id": "test.bp.two", 136 "version": "test.bp.two.version" 137 } 138 ], 139 "lifecycle": {"version": "1.2.3", "api": {"buildpack": "0.1","platform": "2.3"}, "apis": { 140 "buildpack": {"deprecated": ["0.1"], "supported": ["1.2", "1.3"]}, 141 "platform": {"deprecated": [], "supported": ["2.3", "2.4"]} 142 }}, 143 "createdBy": {"name": "pack", "version": "1.2.3"}, 144 "images": [ 145 { 146 "image": "some/run-image", 147 "mirrors": [ 148 "gcr.io/some/default" 149 ] 150 } 151 ] 152 }`)) 153 154 assert.Succeeds(builderImage.SetLabel( 155 "io.buildpacks.buildpack.order", 156 `[ 157 { 158 "group": 159 [ 160 { 161 "id": "test.nested", 162 "version": "test.nested.version", 163 "optional": false 164 }, 165 { 166 "id": "test.bp.two", 167 "optional": true 168 } 169 ] 170 } 171 ]`, 172 )) 173 174 assert.Succeeds(builderImage.SetLabel( 175 "io.buildpacks.buildpack.layers", 176 `{ 177 "test.nested": { 178 "test.nested.version": { 179 "api": "0.2", 180 "order": [ 181 { 182 "group": [ 183 { 184 "id": "test.bp.one", 185 "version": "test.bp.one.version" 186 }, 187 { 188 "id": "test.bp.two", 189 "version": "test.bp.two.version" 190 } 191 ] 192 } 193 ], 194 "layerDiffID": "sha256:test.nested.sha256", 195 "homepage": "http://geocities.com/top-bp" 196 } 197 }, 198 "test.bp.one": { 199 "test.bp.one.version": { 200 "api": "0.2", 201 "stacks": [ 202 { 203 "id": "test.stack.id" 204 } 205 ], 206 "layerDiffID": "sha256:test.bp.one.sha256", 207 "homepage": "http://geocities.com/cool-bp", 208 "name": "one" 209 } 210 }, 211 "test.bp.two": { 212 "test.bp.two.version": { 213 "api": "0.2", 214 "stacks": [ 215 { 216 "id": "test.stack.id" 217 } 218 ], 219 "layerDiffID": "sha256:test.bp.two.sha256" 220 } 221 } 222 }`)) 223 }) 224 225 it("returns the builder with the given name with information from the label", func() { 226 builderInfo, err := subject.InspectBuilder("some/builder", useDaemon) 227 assert.Nil(err) 228 apiVersion, err := api.NewVersion("0.2") 229 assert.Nil(err) 230 231 want := BuilderInfo{ 232 Description: "Some description", 233 Stack: "test.stack.id", 234 Mixins: []string{"mixinOne", "mixinThree", "build:mixinTwo", "build:mixinFour"}, 235 RunImages: []pubbldr.RunImageConfig{{Image: "some/run-image", Mirrors: []string{"gcr.io/some/default"}}}, 236 Buildpacks: []dist.ModuleInfo{ 237 { 238 ID: "test.bp.one", 239 Version: "test.bp.one.version", 240 Name: "one", 241 Homepage: "http://geocities.com/cool-bp", 242 }, 243 { 244 ID: "test.bp.two", 245 Version: "test.bp.two.version", 246 }, 247 { 248 ID: "test.nested", 249 Version: "test.nested.version", 250 Homepage: "http://geocities.com/top-bp", 251 }, 252 }, 253 Order: pubbldr.DetectionOrder{ 254 { 255 GroupDetectionOrder: pubbldr.DetectionOrder{ 256 { 257 ModuleRef: dist.ModuleRef{ 258 ModuleInfo: dist.ModuleInfo{ID: "test.nested", Version: "test.nested.version"}, 259 Optional: false, 260 }, 261 }, 262 { 263 ModuleRef: dist.ModuleRef{ 264 ModuleInfo: dist.ModuleInfo{ID: "test.bp.two"}, 265 Optional: true, 266 }, 267 }, 268 }, 269 }, 270 }, 271 BuildpackLayers: map[string]map[string]dist.ModuleLayerInfo{ 272 "test.nested": { 273 "test.nested.version": { 274 API: apiVersion, 275 Order: dist.Order{ 276 { 277 Group: []dist.ModuleRef{ 278 { 279 ModuleInfo: dist.ModuleInfo{ 280 ID: "test.bp.one", 281 Version: "test.bp.one.version", 282 }, 283 Optional: false, 284 }, 285 { 286 ModuleInfo: dist.ModuleInfo{ 287 ID: "test.bp.two", 288 Version: "test.bp.two.version", 289 }, 290 Optional: false, 291 }, 292 }, 293 }, 294 }, 295 LayerDiffID: "sha256:test.nested.sha256", 296 Homepage: "http://geocities.com/top-bp", 297 }, 298 }, 299 "test.bp.one": { 300 "test.bp.one.version": { 301 API: apiVersion, 302 Stacks: []dist.Stack{ 303 { 304 ID: "test.stack.id", 305 }, 306 }, 307 LayerDiffID: "sha256:test.bp.one.sha256", 308 Homepage: "http://geocities.com/cool-bp", 309 Name: "one", 310 }, 311 }, 312 "test.bp.two": { 313 "test.bp.two.version": { 314 API: apiVersion, 315 Stacks: []dist.Stack{ 316 { 317 ID: "test.stack.id", 318 }, 319 }, 320 LayerDiffID: "sha256:test.bp.two.sha256", 321 }, 322 }, 323 }, 324 Lifecycle: builder.LifecycleDescriptor{ 325 Info: builder.LifecycleInfo{ 326 Version: builder.VersionMustParse("1.2.3"), 327 }, 328 API: builder.LifecycleAPI{ 329 BuildpackVersion: api.MustParse("0.1"), 330 PlatformVersion: api.MustParse("2.3"), 331 }, 332 APIs: builder.LifecycleAPIs{ 333 Buildpack: builder.APIVersions{ 334 Deprecated: builder.APISet{api.MustParse("0.1")}, 335 Supported: builder.APISet{api.MustParse("1.2"), api.MustParse("1.3")}, 336 }, 337 Platform: builder.APIVersions{ 338 Deprecated: builder.APISet{}, 339 Supported: builder.APISet{api.MustParse("2.3"), api.MustParse("2.4")}, 340 }, 341 }, 342 }, 343 CreatedBy: builder.CreatorMetadata{ 344 Name: "pack", 345 Version: "1.2.3", 346 }, 347 } 348 349 if diff := cmp.Diff(want, *builderInfo); diff != "" { 350 t.Errorf("InspectBuilder() mismatch (-want +got):\n%s", diff) 351 } 352 }) 353 354 when("order detection depth is higher than None", func() { 355 it("shows subgroup order as part of order", func() { 356 builderInfo, err := subject.InspectBuilder( 357 "some/builder", 358 useDaemon, 359 WithDetectionOrderDepth(pubbldr.OrderDetectionMaxDepth), 360 ) 361 h.AssertNil(t, err) 362 363 want := pubbldr.DetectionOrder{ 364 { 365 GroupDetectionOrder: pubbldr.DetectionOrder{ 366 { 367 ModuleRef: dist.ModuleRef{ 368 ModuleInfo: dist.ModuleInfo{ID: "test.nested", Version: "test.nested.version"}, 369 Optional: false, 370 }, 371 GroupDetectionOrder: pubbldr.DetectionOrder{ 372 { 373 ModuleRef: dist.ModuleRef{ 374 ModuleInfo: dist.ModuleInfo{ 375 ID: "test.bp.one", 376 Version: "test.bp.one.version", 377 }, 378 }, 379 }, 380 { 381 ModuleRef: dist.ModuleRef{ 382 ModuleInfo: dist.ModuleInfo{ 383 ID: "test.bp.two", 384 Version: "test.bp.two.version", 385 }, 386 }, 387 }, 388 }, 389 }, 390 { 391 ModuleRef: dist.ModuleRef{ 392 ModuleInfo: dist.ModuleInfo{ID: "test.bp.two"}, 393 Optional: true, 394 }, 395 }, 396 }, 397 }, 398 } 399 400 if diff := cmp.Diff(want, builderInfo.Order); diff != "" { 401 t.Errorf("\"InspectBuilder() mismatch (-want +got):\b%s", diff) 402 } 403 }) 404 }) 405 }) 406 407 // TODO add test case when builder is flattened 408 }) 409 } 410 }) 411 412 when("the image does not exist", func() { 413 it.Before(func() { 414 notFoundImage := fakes.NewImage("", "", nil) 415 notFoundImage.Delete() 416 mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/builder", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(nil, errors.Wrap(image.ErrNotFound, "some-error")) 417 }) 418 419 it("return nil metadata", func() { 420 metadata, err := subject.InspectBuilder("some/builder", true) 421 assert.Nil(err) 422 assert.Nil(metadata) 423 }) 424 }) 425 }