github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/client/inspect_image_test.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"testing"
     9  
    10  	"github.com/buildpacks/imgutil/fakes"
    11  	"github.com/buildpacks/lifecycle/launch"
    12  	"github.com/buildpacks/lifecycle/platform/files"
    13  	"github.com/golang/mock/gomock"
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/google/go-cmp/cmp/cmpopts"
    16  	"github.com/heroku/color"
    17  	"github.com/sclevine/spec"
    18  	"github.com/sclevine/spec/report"
    19  
    20  	"github.com/buildpacks/pack/pkg/image"
    21  	"github.com/buildpacks/pack/pkg/logging"
    22  	"github.com/buildpacks/pack/pkg/testmocks"
    23  	h "github.com/buildpacks/pack/testhelpers"
    24  )
    25  
    26  func TestInspectImage(t *testing.T) {
    27  	color.Disable(true)
    28  	defer color.Disable(false)
    29  	spec.Run(t, "InspectImage", testInspectImage, spec.Parallel(), spec.Report(report.Terminal{}))
    30  }
    31  
    32  // PlatformAPI should be ignored because it is not set in the metadata label
    33  var ignorePlatformAPI = []cmp.Option{
    34  	cmpopts.IgnoreFields(launch.Process{}, "PlatformAPI"),
    35  	cmpopts.IgnoreFields(launch.RawCommand{}, "PlatformAPI"),
    36  }
    37  
    38  func testInspectImage(t *testing.T, when spec.G, it spec.S) {
    39  	var (
    40  		subject                        *Client
    41  		mockImageFetcher               *testmocks.MockImageFetcher
    42  		mockDockerClient               *testmocks.MockCommonAPIClient
    43  		mockController                 *gomock.Controller
    44  		mockImage                      *testmocks.MockImage
    45  		mockImageNoRebasable           *testmocks.MockImage
    46  		mockImageRebasableWithoutLabel *testmocks.MockImage
    47  		mockImageWithExtension         *testmocks.MockImage
    48  		out                            bytes.Buffer
    49  	)
    50  
    51  	it.Before(func() {
    52  		mockController = gomock.NewController(t)
    53  		mockImageFetcher = testmocks.NewMockImageFetcher(mockController)
    54  		mockDockerClient = testmocks.NewMockCommonAPIClient(mockController)
    55  
    56  		var err error
    57  		subject, err = NewClient(WithLogger(logging.NewLogWithWriters(&out, &out)), WithFetcher(mockImageFetcher), WithDockerClient(mockDockerClient))
    58  		h.AssertNil(t, err)
    59  
    60  		mockImage = testmocks.NewImage("some/image", "", nil)
    61  		h.AssertNil(t, mockImage.SetWorkingDir("/test-workdir"))
    62  		h.AssertNil(t, mockImage.SetLabel("io.buildpacks.stack.id", "test.stack.id"))
    63  		h.AssertNil(t, mockImage.SetLabel("io.buildpacks.rebasable", "true"))
    64  		h.AssertNil(t, mockImage.SetLabel(
    65  			"io.buildpacks.lifecycle.metadata",
    66  			`{
    67    "stack": {
    68      "runImage": {
    69        "image": "some-run-image",
    70        "mirrors": [
    71          "some-mirror",
    72          "other-mirror"
    73        ]
    74      }
    75    },
    76    "runImage": {
    77      "topLayer": "some-top-layer",
    78      "reference": "some-run-image-reference"
    79    }
    80  }`,
    81  		))
    82  		h.AssertNil(t, mockImage.SetLabel(
    83  			"io.buildpacks.build.metadata",
    84  			`{
    85    "bom": [
    86      {
    87        "name": "some-bom-element"
    88      }
    89    ],
    90    "buildpacks": [
    91      {
    92        "id": "some-buildpack",
    93        "version": "some-version"
    94      },
    95      {
    96        "id": "other-buildpack",
    97        "version": "other-version"
    98      }
    99    ],
   100    "processes": [
   101      {
   102        "type": "other-process",
   103        "command": "/other/process",
   104        "args": ["opt", "1"],
   105        "direct": true
   106      },
   107      {
   108        "type": "web",
   109        "command": "/start/web-process",
   110        "args": ["-p", "1234"],
   111        "direct": false
   112      }
   113    ],
   114    "launcher": {
   115      "version": "0.5.0"
   116    }
   117  }`,
   118  		))
   119  
   120  		mockImageNoRebasable = testmocks.NewImage("some/imageNoRebasable", "", nil)
   121  		h.AssertNil(t, mockImageNoRebasable.SetWorkingDir("/test-workdir"))
   122  		h.AssertNil(t, mockImageNoRebasable.SetLabel("io.buildpacks.stack.id", "test.stack.id"))
   123  		h.AssertNil(t, mockImageNoRebasable.SetLabel("io.buildpacks.rebasable", "false"))
   124  		h.AssertNil(t, mockImageNoRebasable.SetLabel(
   125  			"io.buildpacks.lifecycle.metadata",
   126  			`{
   127    "stack": {
   128      "runImage": {
   129        "image": "some-run-image-no-rebasable",
   130        "mirrors": [
   131          "some-mirror",
   132          "other-mirror"
   133        ]
   134      }
   135    },
   136    "runImage": {
   137      "topLayer": "some-top-layer",
   138      "reference": "some-run-image-reference"
   139    }
   140  }`,
   141  		))
   142  		h.AssertNil(t, mockImageNoRebasable.SetLabel(
   143  			"io.buildpacks.build.metadata",
   144  			`{
   145    "bom": [
   146      {
   147        "name": "some-bom-element"
   148      }
   149    ],
   150    "buildpacks": [
   151      {
   152        "id": "some-buildpack",
   153        "version": "some-version"
   154      },
   155      {
   156        "id": "other-buildpack",
   157        "version": "other-version"
   158      }
   159    ],
   160    "processes": [
   161      {
   162        "type": "other-process",
   163        "command": "/other/process",
   164        "args": ["opt", "1"],
   165        "direct": true
   166      },
   167      {
   168        "type": "web",
   169        "command": "/start/web-process",
   170        "args": ["-p", "1234"],
   171        "direct": false
   172      }
   173    ],
   174    "launcher": {
   175      "version": "0.5.0"
   176    }
   177  }`,
   178  		))
   179  
   180  		mockImageRebasableWithoutLabel = testmocks.NewImage("some/imageRebasableWithoutLabel", "", nil)
   181  		h.AssertNil(t, mockImageNoRebasable.SetWorkingDir("/test-workdir"))
   182  		h.AssertNil(t, mockImageNoRebasable.SetLabel("io.buildpacks.stack.id", "test.stack.id"))
   183  		h.AssertNil(t, mockImageNoRebasable.SetLabel(
   184  			"io.buildpacks.lifecycle.metadata",
   185  			`{
   186    "stack": {
   187      "runImage": {
   188        "image": "some-run-image-no-rebasable",
   189        "mirrors": [
   190          "some-mirror",
   191          "other-mirror"
   192        ]
   193      }
   194    },
   195    "runImage": {
   196      "topLayer": "some-top-layer",
   197      "reference": "some-run-image-reference"
   198    }
   199  }`,
   200  		))
   201  		h.AssertNil(t, mockImageNoRebasable.SetLabel(
   202  			"io.buildpacks.build.metadata",
   203  			`{
   204    "bom": [
   205      {
   206        "name": "some-bom-element"
   207      }
   208    ],
   209    "buildpacks": [
   210      {
   211        "id": "some-buildpack",
   212        "version": "some-version"
   213      },
   214      {
   215        "id": "other-buildpack",
   216        "version": "other-version"
   217      }
   218    ],
   219    "processes": [
   220      {
   221        "type": "other-process",
   222        "command": "/other/process",
   223        "args": ["opt", "1"],
   224        "direct": true
   225      },
   226      {
   227        "type": "web",
   228        "command": "/start/web-process",
   229        "args": ["-p", "1234"],
   230        "direct": false
   231      }
   232    ],
   233    "launcher": {
   234      "version": "0.5.0"
   235    }
   236  }`,
   237  		))
   238  
   239  		mockImageWithExtension = testmocks.NewImage("some/imageWithExtension", "", nil)
   240  		h.AssertNil(t, mockImageWithExtension.SetWorkingDir("/test-workdir"))
   241  		h.AssertNil(t, mockImageWithExtension.SetLabel("io.buildpacks.stack.id", "test.stack.id"))
   242  		h.AssertNil(t, mockImageWithExtension.SetLabel("io.buildpacks.rebasable", "true"))
   243  		h.AssertNil(t, mockImageWithExtension.SetLabel(
   244  			"io.buildpacks.lifecycle.metadata",
   245  			`{
   246    "stack": {
   247      "runImage": {
   248        "image": "some-run-image",
   249        "mirrors": [
   250          "some-mirror",
   251          "other-mirror"
   252        ]
   253      }
   254    },
   255    "runImage": {
   256      "topLayer": "some-top-layer",
   257      "reference": "some-run-image-reference"
   258    }
   259  }`,
   260  		))
   261  		h.AssertNil(t, mockImageWithExtension.SetLabel(
   262  			"io.buildpacks.build.metadata",
   263  			`{
   264    "bom": [
   265      {
   266        "name": "some-bom-element"
   267      }
   268    ],
   269    "buildpacks": [
   270      {
   271        "id": "some-buildpack",
   272        "version": "some-version"
   273      },
   274      {
   275        "id": "other-buildpack",
   276        "version": "other-version"
   277      }
   278    ],
   279      "extensions": [
   280      {
   281        "id": "some-extension",
   282        "version": "some-version"
   283      },
   284      {
   285        "id": "other-extension",
   286        "version": "other-version"
   287      }
   288    ],
   289    "processes": [
   290      {
   291        "type": "other-process",
   292        "command": "/other/process",
   293        "args": ["opt", "1"],
   294        "direct": true
   295      },
   296      {
   297        "type": "web",
   298        "command": "/start/web-process",
   299        "args": ["-p", "1234"],
   300        "direct": false
   301      }
   302    ],
   303    "launcher": {
   304      "version": "0.5.0"
   305    }
   306  }`,
   307  		))
   308  	})
   309  
   310  	it.After(func() {
   311  		mockController.Finish()
   312  	})
   313  
   314  	when("the image exists", func() {
   315  		for _, useDaemon := range []bool{true, false} {
   316  			useDaemon := useDaemon
   317  			when(fmt.Sprintf("daemon is %t", useDaemon), func() {
   318  				it.Before(func() {
   319  					if useDaemon {
   320  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImage, nil).AnyTimes()
   321  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageNoRebasable", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImageNoRebasable, nil).AnyTimes()
   322  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageRebasableWithoutLabel", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImageRebasableWithoutLabel, nil).AnyTimes()
   323  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageWithExtension", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImageWithExtension, nil).AnyTimes()
   324  					} else {
   325  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/image", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(mockImage, nil).AnyTimes()
   326  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageNoRebasable", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(mockImageNoRebasable, nil).AnyTimes()
   327  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageRebasableWithoutLabel", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(mockImageRebasableWithoutLabel, nil).AnyTimes()
   328  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageWithExtension", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(mockImageWithExtension, nil).AnyTimes()
   329  					}
   330  				})
   331  
   332  				it("returns the stack ID", func() {
   333  					info, err := subject.InspectImage("some/image", useDaemon)
   334  					h.AssertNil(t, err)
   335  					h.AssertEq(t, info.StackID, "test.stack.id")
   336  				})
   337  
   338  				it("returns the stack ID with extension", func() {
   339  					infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon)
   340  					h.AssertNil(t, err)
   341  					h.AssertEq(t, infoWithExtension.StackID, "test.stack.id")
   342  				})
   343  
   344  				it("returns the stack from runImage.Image if set", func() {
   345  					h.AssertNil(t, mockImage.SetLabel(
   346  						"io.buildpacks.lifecycle.metadata",
   347  						`{
   348    "runImage": {
   349      "topLayer": "some-top-layer",
   350      "reference": "some-run-image-reference",
   351      "image":  "is everything"
   352    }
   353  }`,
   354  					))
   355  					info, err := subject.InspectImage("some/image", useDaemon)
   356  					h.AssertNil(t, err)
   357  					h.AssertEq(t, info.Stack,
   358  						files.Stack{RunImage: files.RunImageForExport{Image: "is everything"}})
   359  				})
   360  
   361  				it("returns the stack", func() {
   362  					info, err := subject.InspectImage("some/image", useDaemon)
   363  					h.AssertNil(t, err)
   364  					h.AssertEq(t, info.Stack,
   365  						files.Stack{
   366  							RunImage: files.RunImageForExport{
   367  								Image: "some-run-image",
   368  								Mirrors: []string{
   369  									"some-mirror",
   370  									"other-mirror",
   371  								},
   372  							},
   373  						},
   374  					)
   375  				})
   376  
   377  				it("returns the stack with extension", func() {
   378  					infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon)
   379  					h.AssertNil(t, err)
   380  					h.AssertEq(t, infoWithExtension.Stack,
   381  						files.Stack{
   382  							RunImage: files.RunImageForExport{
   383  								Image: "some-run-image",
   384  								Mirrors: []string{
   385  									"some-mirror",
   386  									"other-mirror",
   387  								},
   388  							},
   389  						},
   390  					)
   391  				})
   392  
   393  				it("returns the base image", func() {
   394  					info, err := subject.InspectImage("some/image", useDaemon)
   395  					h.AssertNil(t, err)
   396  					h.AssertEq(t, info.Base,
   397  						files.RunImageForRebase{
   398  							TopLayer:  "some-top-layer",
   399  							Reference: "some-run-image-reference",
   400  						},
   401  					)
   402  				})
   403  
   404  				it("returns the base image with extension", func() {
   405  					infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon)
   406  					h.AssertNil(t, err)
   407  					h.AssertEq(t, infoWithExtension.Base,
   408  						files.RunImageForRebase{
   409  							TopLayer:  "some-top-layer",
   410  							Reference: "some-run-image-reference",
   411  						},
   412  					)
   413  				})
   414  
   415  				it("returns the rebasable image", func() {
   416  					info, err := subject.InspectImage("some/image", useDaemon)
   417  					h.AssertNil(t, err)
   418  					h.AssertEq(t, info.Rebasable, true)
   419  				})
   420  
   421  				it("returns the rebasable image true if the label has not been set", func() {
   422  					info, err := subject.InspectImage("some/imageRebasableWithoutLabel", useDaemon)
   423  					h.AssertNil(t, err)
   424  					h.AssertEq(t, info.Rebasable, true)
   425  				})
   426  
   427  				it("returns the no rebasable image", func() {
   428  					info, err := subject.InspectImage("some/imageNoRebasable", useDaemon)
   429  					h.AssertNil(t, err)
   430  					h.AssertEq(t, info.Rebasable, false)
   431  				})
   432  
   433  				it("returns the rebasable image with Extension", func() {
   434  					infoRebasableWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon)
   435  					h.AssertNil(t, err)
   436  					h.AssertEq(t, infoRebasableWithExtension.Rebasable, true)
   437  				})
   438  
   439  				it("returns the BOM", func() {
   440  					info, err := subject.InspectImage("some/image", useDaemon)
   441  					h.AssertNil(t, err)
   442  
   443  					rawBOM, err := json.Marshal(info.BOM)
   444  					h.AssertNil(t, err)
   445  					h.AssertContains(t, string(rawBOM), `[{"name":"some-bom-element"`)
   446  				})
   447  
   448  				it("returns the BOM", func() {
   449  					infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon)
   450  					h.AssertNil(t, err)
   451  
   452  					rawBOM, err := json.Marshal(infoWithExtension.BOM)
   453  					h.AssertNil(t, err)
   454  					h.AssertContains(t, string(rawBOM), `[{"name":"some-bom-element"`)
   455  				})
   456  
   457  				it("returns the buildpacks", func() {
   458  					info, err := subject.InspectImage("some/image", useDaemon)
   459  					h.AssertNil(t, err)
   460  
   461  					h.AssertEq(t, len(info.Buildpacks), 2)
   462  					h.AssertEq(t, info.Buildpacks[0].ID, "some-buildpack")
   463  					h.AssertEq(t, info.Buildpacks[0].Version, "some-version")
   464  					h.AssertEq(t, info.Buildpacks[1].ID, "other-buildpack")
   465  					h.AssertEq(t, info.Buildpacks[1].Version, "other-version")
   466  				})
   467  
   468  				it("returns the buildpacks with extension", func() {
   469  					infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon)
   470  					h.AssertNil(t, err)
   471  
   472  					h.AssertEq(t, len(infoWithExtension.Buildpacks), 2)
   473  					h.AssertEq(t, infoWithExtension.Buildpacks[0].ID, "some-buildpack")
   474  					h.AssertEq(t, infoWithExtension.Buildpacks[0].Version, "some-version")
   475  					h.AssertEq(t, infoWithExtension.Buildpacks[1].ID, "other-buildpack")
   476  					h.AssertEq(t, infoWithExtension.Buildpacks[1].Version, "other-version")
   477  				})
   478  
   479  				it("returns the extensions", func() {
   480  					infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon)
   481  					h.AssertNil(t, err)
   482  
   483  					h.AssertEq(t, len(infoWithExtension.Extensions), 2)
   484  					h.AssertEq(t, infoWithExtension.Extensions[0].ID, "some-extension")
   485  					h.AssertEq(t, infoWithExtension.Extensions[0].Version, "some-version")
   486  					h.AssertEq(t, infoWithExtension.Extensions[1].ID, "other-extension")
   487  					h.AssertEq(t, infoWithExtension.Extensions[1].Version, "other-version")
   488  				})
   489  
   490  				it("returns the processes setting the web process as default", func() {
   491  					info, err := subject.InspectImage("some/image", useDaemon)
   492  					h.AssertNil(t, err)
   493  
   494  					h.AssertEq(t, info.Processes,
   495  						ProcessDetails{
   496  							DefaultProcess: &launch.Process{
   497  								Type:             "web",
   498  								Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   499  								Args:             []string{"-p", "1234"},
   500  								Direct:           false,
   501  								WorkingDirectory: "/test-workdir",
   502  							},
   503  							OtherProcesses: []launch.Process{
   504  								{
   505  									Type:             "other-process",
   506  									Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   507  									Args:             []string{"opt", "1"},
   508  									Direct:           true,
   509  									WorkingDirectory: "/test-workdir",
   510  								},
   511  							},
   512  						},
   513  						ignorePlatformAPI...)
   514  				})
   515  
   516  				when("Platform API < 0.4", func() {
   517  					when("CNB_PROCESS_TYPE is set", func() {
   518  						it.Before(func() {
   519  							h.AssertNil(t, mockImage.SetEnv("CNB_PROCESS_TYPE", "other-process"))
   520  						})
   521  
   522  						it("returns processes setting the correct default process", func() {
   523  							info, err := subject.InspectImage("some/image", useDaemon)
   524  							h.AssertNil(t, err)
   525  
   526  							h.AssertEq(t, info.Processes,
   527  								ProcessDetails{
   528  									DefaultProcess: &launch.Process{
   529  										Type:             "other-process",
   530  										Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   531  										Args:             []string{"opt", "1"},
   532  										Direct:           true,
   533  										WorkingDirectory: "/test-workdir",
   534  									},
   535  									OtherProcesses: []launch.Process{
   536  										{
   537  											Type:             "web",
   538  											Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   539  											Args:             []string{"-p", "1234"},
   540  											Direct:           false,
   541  											WorkingDirectory: "/test-workdir",
   542  										},
   543  									},
   544  								},
   545  								ignorePlatformAPI...)
   546  						})
   547  					})
   548  
   549  					when("CNB_PROCESS_TYPE is set, but doesn't match an existing process", func() {
   550  						it.Before(func() {
   551  							h.AssertNil(t, mockImage.SetEnv("CNB_PROCESS_TYPE", "missing-process"))
   552  						})
   553  
   554  						it("returns a nil default process", func() {
   555  							info, err := subject.InspectImage("some/image", useDaemon)
   556  							h.AssertNil(t, err)
   557  
   558  							h.AssertEq(t, info.Processes,
   559  								ProcessDetails{
   560  									DefaultProcess: nil,
   561  									OtherProcesses: []launch.Process{
   562  										{
   563  											Type:             "other-process",
   564  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   565  											Args:             []string{"opt", "1"},
   566  											Direct:           true,
   567  											WorkingDirectory: "/test-workdir",
   568  										},
   569  										{
   570  											Type:             "web",
   571  											Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   572  											Args:             []string{"-p", "1234"},
   573  											Direct:           false,
   574  											WorkingDirectory: "/test-workdir",
   575  										},
   576  									},
   577  								},
   578  								ignorePlatformAPI...)
   579  						})
   580  					})
   581  
   582  					it("returns a nil default process when CNB_PROCESS_TYPE is not set and there is no web process", func() {
   583  						h.AssertNil(t, mockImage.SetLabel(
   584  							"io.buildpacks.build.metadata",
   585  							`{
   586    "processes": [
   587      {
   588        "type": "other-process",
   589        "command": "/other/process",
   590        "args": ["opt", "1"],
   591        "direct": true
   592      }
   593    ]
   594  }`,
   595  						))
   596  
   597  						info, err := subject.InspectImage("some/image", useDaemon)
   598  						h.AssertNil(t, err)
   599  
   600  						h.AssertEq(t, info.Processes,
   601  							ProcessDetails{
   602  								DefaultProcess: nil,
   603  								OtherProcesses: []launch.Process{
   604  									{
   605  										Type:             "other-process",
   606  										Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   607  										Args:             []string{"opt", "1"},
   608  										Direct:           true,
   609  										WorkingDirectory: "/test-workdir",
   610  									},
   611  								},
   612  							},
   613  							ignorePlatformAPI...)
   614  					})
   615  				})
   616  
   617  				when("Platform API >= 0.4 and <= 0.8", func() {
   618  					it.Before(func() {
   619  						h.AssertNil(t, mockImage.SetEnv("CNB_PLATFORM_API", "0.4"))
   620  					})
   621  
   622  					when("CNB_PLATFORM_API set to bad value", func() {
   623  						it("errors", func() {
   624  							h.AssertNil(t, mockImage.SetEnv("CNB_PLATFORM_API", "not-semver"))
   625  							_, err := subject.InspectImage("some/image", useDaemon)
   626  							h.AssertError(t, err, "parsing platform api version")
   627  						})
   628  					})
   629  
   630  					when("Can't inspect Image entrypoint", func() {
   631  						it("errors", func() {
   632  							mockImage.EntrypointCall.Returns.Error = errors.New("some-error")
   633  
   634  							_, err := subject.InspectImage("some/image", useDaemon)
   635  							h.AssertError(t, err, "reading entrypoint")
   636  						})
   637  					})
   638  
   639  					when("ENTRYPOINT is empty", func() {
   640  						it("sets nil default process", func() {
   641  							info, err := subject.InspectImage("some/image", useDaemon)
   642  							h.AssertNil(t, err)
   643  
   644  							h.AssertEq(t, info.Processes,
   645  								ProcessDetails{
   646  									DefaultProcess: nil,
   647  									OtherProcesses: []launch.Process{
   648  										{
   649  											Type:             "other-process",
   650  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   651  											Args:             []string{"opt", "1"},
   652  											Direct:           true,
   653  											WorkingDirectory: "/test-workdir",
   654  										},
   655  										{
   656  											Type:             "web",
   657  											Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   658  											Args:             []string{"-p", "1234"},
   659  											Direct:           false,
   660  											WorkingDirectory: "/test-workdir",
   661  										},
   662  									},
   663  								},
   664  								ignorePlatformAPI...)
   665  						})
   666  					})
   667  
   668  					when("CNB_PROCESS_TYPE is set", func() {
   669  						it.Before(func() {
   670  							h.AssertNil(t, mockImage.SetEnv("CNB_PROCESS_TYPE", "other-process"))
   671  
   672  							mockImage.EntrypointCall.Returns.StringArr = []string{"/cnb/process/web"}
   673  						})
   674  
   675  						it("ignores it and sets the correct default process", func() {
   676  							info, err := subject.InspectImage("some/image", useDaemon)
   677  							h.AssertNil(t, err)
   678  
   679  							h.AssertEq(t, info.Processes,
   680  								ProcessDetails{
   681  									DefaultProcess: &launch.Process{
   682  										Type:             "web",
   683  										Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   684  										Args:             []string{"-p", "1234"},
   685  										Direct:           false,
   686  										WorkingDirectory: "/test-workdir",
   687  									},
   688  									OtherProcesses: []launch.Process{
   689  										{
   690  											Type:             "other-process",
   691  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   692  											Args:             []string{"opt", "1"},
   693  											Direct:           true,
   694  											WorkingDirectory: "/test-workdir",
   695  										},
   696  									},
   697  								},
   698  								ignorePlatformAPI...)
   699  						})
   700  					})
   701  
   702  					when("ENTRYPOINT is set, but doesn't match an existing process", func() {
   703  						it.Before(func() {
   704  							mockImage.EntrypointCall.Returns.StringArr = []string{"/cnb/process/unknown-process"}
   705  						})
   706  
   707  						it("returns nil default default process", func() {
   708  							info, err := subject.InspectImage("some/image", useDaemon)
   709  							h.AssertNil(t, err)
   710  
   711  							h.AssertEq(t, info.Processes,
   712  								ProcessDetails{
   713  									DefaultProcess: nil,
   714  									OtherProcesses: []launch.Process{
   715  										{
   716  											Type:             "other-process",
   717  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   718  											Args:             []string{"opt", "1"},
   719  											Direct:           true,
   720  											WorkingDirectory: "/test-workdir",
   721  										},
   722  										{
   723  											Type:             "web",
   724  											Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   725  											Args:             []string{"-p", "1234"},
   726  											Direct:           false,
   727  											WorkingDirectory: "/test-workdir",
   728  										},
   729  									},
   730  								},
   731  								ignorePlatformAPI...)
   732  						})
   733  					})
   734  
   735  					when("ENTRYPOINT set to /cnb/lifecycle/launcher", func() {
   736  						it("returns a nil default process", func() {
   737  							mockImage.EntrypointCall.Returns.StringArr = []string{"/cnb/lifecycle/launcher"}
   738  
   739  							h.AssertNil(t, mockImage.SetLabel(
   740  								"io.buildpacks.build.metadata",
   741  								`{
   742  					 "processes": [
   743  					   {
   744  					     "type": "other-process",
   745  					     "command": "/other/process",
   746  					     "args": ["opt", "1"],
   747  					     "direct": true
   748  					   }
   749  					 ]
   750  					}`,
   751  							))
   752  
   753  							info, err := subject.InspectImage("some/image", useDaemon)
   754  							h.AssertNil(t, err)
   755  
   756  							h.AssertEq(t, info.Processes,
   757  								ProcessDetails{
   758  									DefaultProcess: nil,
   759  									OtherProcesses: []launch.Process{
   760  										{
   761  											Type:             "other-process",
   762  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   763  											Args:             []string{"opt", "1"},
   764  											Direct:           true,
   765  											WorkingDirectory: "/test-workdir",
   766  										},
   767  									},
   768  								},
   769  								ignorePlatformAPI...)
   770  						})
   771  					})
   772  
   773  					when("Inspecting Windows images", func() {
   774  						when(`ENTRYPOINT set to c:\cnb\lifecycle\launcher.exe`, func() {
   775  							it("sets default process to nil", func() {
   776  								mockImage.EntrypointCall.Returns.StringArr = []string{`c:\cnb\lifecycle\launcher.exe`}
   777  
   778  								info, err := subject.InspectImage("some/image", useDaemon)
   779  								h.AssertNil(t, err)
   780  
   781  								h.AssertEq(t, info.Processes,
   782  									ProcessDetails{
   783  										DefaultProcess: nil,
   784  										OtherProcesses: []launch.Process{
   785  											{
   786  												Type:             "other-process",
   787  												Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   788  												Args:             []string{"opt", "1"},
   789  												Direct:           true,
   790  												WorkingDirectory: "/test-workdir",
   791  											},
   792  											{
   793  												Type:             "web",
   794  												Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   795  												Args:             []string{"-p", "1234"},
   796  												Direct:           false,
   797  												WorkingDirectory: "/test-workdir",
   798  											},
   799  										},
   800  									},
   801  									ignorePlatformAPI...)
   802  							})
   803  						})
   804  
   805  						when("ENTRYPOINT is set, but doesn't match an existing process", func() {
   806  							it("sets default process to nil", func() {
   807  								mockImage.EntrypointCall.Returns.StringArr = []string{`c:\cnb\process\unknown-process.exe`}
   808  
   809  								info, err := subject.InspectImage("some/image", useDaemon)
   810  								h.AssertNil(t, err)
   811  
   812  								h.AssertEq(t, info.Processes,
   813  									ProcessDetails{
   814  										DefaultProcess: nil,
   815  										OtherProcesses: []launch.Process{
   816  											{
   817  												Type:             "other-process",
   818  												Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   819  												Args:             []string{"opt", "1"},
   820  												Direct:           true,
   821  												WorkingDirectory: "/test-workdir",
   822  											},
   823  											{
   824  												Type:             "web",
   825  												Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   826  												Args:             []string{"-p", "1234"},
   827  												Direct:           false,
   828  												WorkingDirectory: "/test-workdir",
   829  											},
   830  										},
   831  									},
   832  									ignorePlatformAPI...)
   833  							})
   834  						})
   835  
   836  						when("ENTRYPOINT is set, and matches an existing process", func() {
   837  							it("sets default process to defined process", func() {
   838  								mockImage.EntrypointCall.Returns.StringArr = []string{`c:\cnb\process\other-process.exe`}
   839  
   840  								info, err := subject.InspectImage("some/image", useDaemon)
   841  								h.AssertNil(t, err)
   842  
   843  								h.AssertEq(t, info.Processes,
   844  									ProcessDetails{
   845  										DefaultProcess: &launch.Process{
   846  											Type:             "other-process",
   847  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   848  											Args:             []string{"opt", "1"},
   849  											Direct:           true,
   850  											WorkingDirectory: "/test-workdir",
   851  										},
   852  										OtherProcesses: []launch.Process{
   853  											{
   854  												Type:             "web",
   855  												Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   856  												Args:             []string{"-p", "1234"},
   857  												Direct:           false,
   858  												WorkingDirectory: "/test-workdir",
   859  											},
   860  										},
   861  									},
   862  									ignorePlatformAPI...)
   863  							})
   864  						})
   865  					})
   866  				})
   867  
   868  				when("Platform API > 0.8", func() {
   869  					when("working-dir is set", func() {
   870  						it("returns process with working directory if available", func() {
   871  							h.AssertNil(t, mockImage.SetLabel(
   872  								"io.buildpacks.build.metadata",
   873  								`{
   874  					 "processes": [
   875  					   {
   876  					     "type": "other-process",
   877  					     "command": "/other/process",
   878  					     "args": ["opt", "1"],
   879  					     "direct": true,
   880  						 "working-dir": "/other-workdir"
   881  					   }
   882  					 ]
   883  					}`,
   884  							))
   885  
   886  							info, err := subject.InspectImage("some/image", useDaemon)
   887  							h.AssertNil(t, err)
   888  							fmt.Print(info)
   889  
   890  							h.AssertEq(t, info.Processes,
   891  								ProcessDetails{
   892  									DefaultProcess: nil,
   893  									OtherProcesses: []launch.Process{
   894  										{
   895  											Type:             "other-process",
   896  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   897  											Args:             []string{"opt", "1"},
   898  											Direct:           true,
   899  											WorkingDirectory: "/other-workdir",
   900  										},
   901  									},
   902  								},
   903  								ignorePlatformAPI...)
   904  						})
   905  					})
   906  
   907  					when("working-dir is not set", func() {
   908  						it("returns process with working directory from image", func() {
   909  							info, err := subject.InspectImage("some/image", useDaemon)
   910  							h.AssertNil(t, err)
   911  
   912  							h.AssertEq(t, info.Processes,
   913  								ProcessDetails{
   914  									DefaultProcess: &launch.Process{
   915  										Type:             "web",
   916  										Command:          launch.RawCommand{Entries: []string{"/start/web-process"}},
   917  										Args:             []string{"-p", "1234"},
   918  										Direct:           false,
   919  										WorkingDirectory: "/test-workdir",
   920  									},
   921  									OtherProcesses: []launch.Process{
   922  										{
   923  											Type:             "other-process",
   924  											Command:          launch.RawCommand{Entries: []string{"/other/process"}},
   925  											Args:             []string{"opt", "1"},
   926  											Direct:           true,
   927  											WorkingDirectory: "/test-workdir",
   928  										},
   929  									},
   930  								},
   931  								ignorePlatformAPI...)
   932  						})
   933  					})
   934  				})
   935  			})
   936  		}
   937  	})
   938  
   939  	when("the image doesn't exist", func() {
   940  		it("returns nil", func() {
   941  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "not/some-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(nil, image.ErrNotFound)
   942  
   943  			info, err := subject.InspectImage("not/some-image", true)
   944  			h.AssertNil(t, err)
   945  			h.AssertNil(t, info)
   946  		})
   947  	})
   948  
   949  	when("there is an error fetching the image", func() {
   950  		it("returns the error", func() {
   951  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "not/some-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(nil, errors.New("some-error"))
   952  
   953  			_, err := subject.InspectImage("not/some-image", true)
   954  			h.AssertError(t, err, "some-error")
   955  		})
   956  	})
   957  
   958  	when("the image is missing labels", func() {
   959  		it("returns empty data", func() {
   960  			mockImageFetcher.EXPECT().
   961  				Fetch(gomock.Any(), "missing/labels", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).
   962  				Return(fakes.NewImage("missing/labels", "", nil), nil)
   963  			info, err := subject.InspectImage("missing/labels", true)
   964  			h.AssertNil(t, err)
   965  			h.AssertEq(t, info, &ImageInfo{Rebasable: true}, ignorePlatformAPI...)
   966  		})
   967  	})
   968  
   969  	when("the image has malformed labels", func() {
   970  		var badImage *fakes.Image
   971  
   972  		it.Before(func() {
   973  			badImage = fakes.NewImage("bad/image", "", nil)
   974  			mockImageFetcher.EXPECT().
   975  				Fetch(gomock.Any(), "bad/image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).
   976  				Return(badImage, nil)
   977  		})
   978  
   979  		it("returns an error when layers md cannot parse", func() {
   980  			h.AssertNil(t, badImage.SetLabel("io.buildpacks.lifecycle.metadata", "not   ----  json"))
   981  			_, err := subject.InspectImage("bad/image", true)
   982  			h.AssertError(t, err, "unmarshalling label 'io.buildpacks.lifecycle.metadata'")
   983  		})
   984  
   985  		it("returns an error when build md cannot parse", func() {
   986  			h.AssertNil(t, badImage.SetLabel("io.buildpacks.build.metadata", "not   ----  json"))
   987  			_, err := subject.InspectImage("bad/image", true)
   988  			h.AssertError(t, err, "unmarshalling label 'io.buildpacks.build.metadata'")
   989  		})
   990  	})
   991  
   992  	when("lifecycle version is 0.4.x or earlier", func() {
   993  		it("includes an empty base image reference", func() {
   994  			oldImage := fakes.NewImage("old/image", "", nil)
   995  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "old/image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(oldImage, nil)
   996  
   997  			h.AssertNil(t, oldImage.SetLabel(
   998  				"io.buildpacks.lifecycle.metadata",
   999  				`{
  1000    "runImage": {
  1001      "topLayer": "some-top-layer",
  1002      "reference": "some-run-image-reference"
  1003    }
  1004  }`,
  1005  			))
  1006  			h.AssertNil(t, oldImage.SetLabel(
  1007  				"io.buildpacks.build.metadata",
  1008  				`{
  1009    "launcher": {
  1010      "version": "0.4.0"
  1011    }
  1012  }`,
  1013  			))
  1014  
  1015  			info, err := subject.InspectImage("old/image", true)
  1016  			h.AssertNil(t, err)
  1017  			h.AssertEq(t, info.Base,
  1018  				files.RunImageForRebase{
  1019  					TopLayer:  "some-top-layer",
  1020  					Reference: "",
  1021  				},
  1022  			)
  1023  		})
  1024  	})
  1025  }