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  }