diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index e103c1e0a..9a71ae5fd 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -1153,6 +1153,7 @@ func testAcceptance( "hello_args": helloArgs, "hello_args_prefix": helloArgsPrefix, "image_workdir": imageWorkdir, + "rebasable": true, }, ) @@ -1863,6 +1864,7 @@ func testAcceptance( "hello_args": helloArgs, "hello_args_prefix": helloArgsPrefix, "image_workdir": imageWorkdir, + "rebasable": true, }, ) diff --git a/acceptance/testdata/pack_fixtures/inspect_image_local_output.json b/acceptance/testdata/pack_fixtures/inspect_image_local_output.json index 94fb3afc8..08bee028c 100644 --- a/acceptance/testdata/pack_fixtures/inspect_image_local_output.json +++ b/acceptance/testdata/pack_fixtures/inspect_image_local_output.json @@ -3,6 +3,7 @@ "remote_info": null, "local_info": { "stack": "pack.test.stack", + "rebasable": false, "base_image": { "top_layer": "{{.base_image_top_layer}}", "reference": "{{.base_image_id}}" diff --git a/acceptance/testdata/pack_fixtures/inspect_image_local_output.toml b/acceptance/testdata/pack_fixtures/inspect_image_local_output.toml index 97be50630..4a2f816db 100644 --- a/acceptance/testdata/pack_fixtures/inspect_image_local_output.toml +++ b/acceptance/testdata/pack_fixtures/inspect_image_local_output.toml @@ -2,6 +2,7 @@ image_name = "{{.image_name}}" [local_info] stack = "pack.test.stack" +rebasable = false [local_info.base_image] top_layer = "{{.base_image_top_layer}}" diff --git a/acceptance/testdata/pack_fixtures/inspect_image_local_output.yaml b/acceptance/testdata/pack_fixtures/inspect_image_local_output.yaml index 05e8135e5..b3d8b757f 100644 --- a/acceptance/testdata/pack_fixtures/inspect_image_local_output.yaml +++ b/acceptance/testdata/pack_fixtures/inspect_image_local_output.yaml @@ -3,6 +3,7 @@ image_name: "{{.image_name}}" remote_info: local_info: stack: pack.test.stack + rebasable: false base_image: top_layer: "{{.base_image_top_layer}}" reference: "{{.base_image_id}}" diff --git a/acceptance/testdata/pack_fixtures/inspect_image_published_output.json b/acceptance/testdata/pack_fixtures/inspect_image_published_output.json index 38d2cc07a..4945e2f81 100644 --- a/acceptance/testdata/pack_fixtures/inspect_image_published_output.json +++ b/acceptance/testdata/pack_fixtures/inspect_image_published_output.json @@ -3,6 +3,7 @@ "local_info": null, "remote_info": { "stack": "pack.test.stack", + "rebasable": false, "base_image": { "top_layer": "{{.base_image_top_layer}}", "reference": "{{.base_image_ref}}" diff --git a/acceptance/testdata/pack_fixtures/inspect_image_published_output.toml b/acceptance/testdata/pack_fixtures/inspect_image_published_output.toml index 734eaad21..4d86d0d8f 100644 --- a/acceptance/testdata/pack_fixtures/inspect_image_published_output.toml +++ b/acceptance/testdata/pack_fixtures/inspect_image_published_output.toml @@ -2,6 +2,7 @@ image_name = "{{.image_name}}" [remote_info] stack = "pack.test.stack" +rebasable = false [remote_info.base_image] top_layer = "{{.base_image_top_layer}}" diff --git a/acceptance/testdata/pack_fixtures/inspect_image_published_output.yaml b/acceptance/testdata/pack_fixtures/inspect_image_published_output.yaml index 290f7a6bb..cbcdb3e6e 100644 --- a/acceptance/testdata/pack_fixtures/inspect_image_published_output.yaml +++ b/acceptance/testdata/pack_fixtures/inspect_image_published_output.yaml @@ -3,6 +3,7 @@ image_name: "{{.image_name}}" local_info: null remote_info: stack: pack.test.stack + rebasable: false base_image: top_layer: "{{.base_image_top_layer}}" reference: "{{.base_image_ref}}" diff --git a/internal/inspectimage/info_display.go b/internal/inspectimage/info_display.go index f7b1e4056..141ce3395 100644 --- a/internal/inspectimage/info_display.go +++ b/internal/inspectimage/info_display.go @@ -46,6 +46,7 @@ type InfoDisplay struct { Buildpacks []dist.ModuleInfo `json:"buildpacks" yaml:"buildpacks" toml:"buildpacks"` Extensions []dist.ModuleInfo `json:"extensions" yaml:"extensions" toml:"extensions"` Processes []ProcessDisplay `json:"processes" yaml:"processes" toml:"processes"` + Rebasable bool `json:"rebasable" yaml:"rebasable" toml:"rebasable"` } type InspectOutput struct { @@ -66,6 +67,7 @@ func NewInfoDisplay(info *client.ImageInfo, generalInfo GeneralInfo) *InfoDispla Buildpacks: displayBuildpacks(info.Buildpacks), Extensions: displayExtensions(info.Extensions), Processes: displayProcesses(info.Processes), + Rebasable: info.Rebasable, } } return &InfoDisplay{ @@ -74,6 +76,7 @@ func NewInfoDisplay(info *client.ImageInfo, generalInfo GeneralInfo) *InfoDispla RunImageMirrors: displayMirrors(info, generalInfo), Buildpacks: displayBuildpacks(info.Buildpacks), Processes: displayProcesses(info.Processes), + Rebasable: info.Rebasable, } } diff --git a/internal/inspectimage/writer/human_readable.go b/internal/inspectimage/writer/human_readable.go index 3883b2501..d9b58ee82 100644 --- a/internal/inspectimage/writer/human_readable.go +++ b/internal/inspectimage/writer/human_readable.go @@ -30,67 +30,110 @@ func (h *HumanReadable) Print( if local == nil && remote == nil { return fmt.Errorf("unable to find image '%s' locally or remotely", generalInfo.Name) } - localDisplay := inspectimage.NewInfoDisplay(local, generalInfo) - remoteDisplay := inspectimage.NewInfoDisplay(remote, generalInfo) logger.Infof("Inspecting image: %s\n", style.Symbol(generalInfo.Name)) - logger.Info("\nREMOTE:\n") - err := writeImageInfo(logger, remoteDisplay, remoteErr) - if err != nil { - return fmt.Errorf("writing remote builder info: %w", err) + if err := writeRemoteImageInfo(logger, generalInfo, remote, remoteErr); err != nil { + return err } - logger.Info("\nLOCAL:\n") - err = writeImageInfo(logger, localDisplay, localErr) - if err != nil { - return fmt.Errorf("writing local builder info: %w", err) + + if err := writeLocalImageInfo(logger, generalInfo, local, localErr); err != nil { + return err } return nil } -func writeImageInfo( +func writeLocalImageInfo( logger logging.Logger, - info *inspectimage.InfoDisplay, - err error, -) error { - imgTpl := template.Must(template.New("runImages"). - Funcs(template.FuncMap{"StringsJoin": strings.Join}). - Funcs(template.FuncMap{"StringsValueOrDefault": strs.ValueOrDefault}). - Parse(runImagesTemplate)) - imgTpl = template.Must(imgTpl.New("buildpacks"). - Parse(buildpacksTemplate)) - if info != nil && info.Extensions != nil { - imgTpl = template.Must(imgTpl.New("extensions").Parse(extensionsTemplate)) + generalInfo inspectimage.GeneralInfo, + local *client.ImageInfo, + localErr error) error { + logger.Info("\nLOCAL:\n") + + if localErr != nil { + logger.Errorf("%s\n", localErr) + return nil } - imgTpl = template.Must(imgTpl.New("processes"). - Parse(processesTemplate)) - if info != nil && info.Extensions != nil { - imgTpl = template.Must(imgTpl.New("image"). - Parse(imageWithExtensionTemplate)) - } else { - imgTpl = template.Must(imgTpl.New("image"). - Parse(imageTemplate)) + + localDisplay := inspectimage.NewInfoDisplay(local, generalInfo) + if localDisplay == nil { + logger.Info("(not present)\n") + return nil } + + err := writeImageInfo(logger, localDisplay) if err != nil { - logger.Errorf("%s\n", err) + return fmt.Errorf("writing local builder info: %w", err) + } + + return nil +} + +func writeRemoteImageInfo( + logger logging.Logger, + generalInfo inspectimage.GeneralInfo, + remote *client.ImageInfo, + remoteErr error) error { + logger.Info("\nREMOTE:\n") + + if remoteErr != nil { + logger.Errorf("%s\n", remoteErr) return nil } - if info == nil { + remoteDisplay := inspectimage.NewInfoDisplay(remote, generalInfo) + if remoteDisplay == nil { logger.Info("(not present)\n") return nil } - remoteOutput, err := inspectImageOutput(info, imgTpl) + + err := writeImageInfo(logger, remoteDisplay) + if err != nil { + return fmt.Errorf("writing remote builder info: %w", err) + } + + return nil +} + +func writeImageInfo( + logger logging.Logger, + info *inspectimage.InfoDisplay, +) error { + imgTpl := getImageTemplate(info) + remoteOutput, err := getInspectImageOutput(imgTpl, info) if err != nil { logger.Error(err.Error()) + return err } else { logger.Info(remoteOutput.String()) + return nil } - return nil } -func inspectImageOutput(info *inspectimage.InfoDisplay, tpl *template.Template) (*bytes.Buffer, error) { +func getImageTemplate(info *inspectimage.InfoDisplay) *template.Template { + imgTpl := template.Must(template.New("runImages"). + Funcs(template.FuncMap{"StringsJoin": strings.Join}). + Funcs(template.FuncMap{"StringsValueOrDefault": strs.ValueOrDefault}). + Parse(runImagesTemplate)) + imgTpl = template.Must(imgTpl.New("buildpacks").Parse(buildpacksTemplate)) + + imgTpl = template.Must(imgTpl.New("processes").Parse(processesTemplate)) + + imgTpl = template.Must(imgTpl.New("rebasable").Parse(rebasableTemplate)) + + if info != nil && info.Extensions != nil { + imgTpl = template.Must(imgTpl.New("extensions").Parse(extensionsTemplate)) + imgTpl = template.Must(imgTpl.New("image").Parse(imageWithExtensionTemplate)) + } else { + imgTpl = template.Must(imgTpl.New("image").Parse(imageTemplate)) + } + return imgTpl +} + +func getInspectImageOutput( + tpl *template.Template, + info *inspectimage.InfoDisplay) (*bytes.Buffer, error) { if info == nil { return bytes.NewBuffer([]byte("(not present)")), nil } @@ -158,6 +201,13 @@ Processes: {{- end }} {{- end }}` +var rebasableTemplate = ` + +Rebasable: +{{- if or .Info.Rebasable (eq .Info.Rebasable true) }} true +{{- else }} false +{{- end }}` + var imageTemplate = ` Stack: {{ .Info.StackID }} @@ -167,6 +217,7 @@ Base Image: {{- end}} Top Layer: {{ .Info.Base.TopLayer }} {{ template "runImages" . }} +{{- template "rebasable" . }} {{ template "buildpacks" . }}{{ template "processes" . }}` var imageWithExtensionTemplate = ` @@ -178,6 +229,7 @@ Base Image: {{- end}} Top Layer: {{ .Info.Base.TopLayer }} {{ template "runImages" . }} +{{- template "rebasable" . }} {{ template "buildpacks" . }} -{{ template "extensions" . }} +{{ template "extensions" . -}} {{ template "processes" . }}` diff --git a/internal/inspectimage/writer/human_readable_test.go b/internal/inspectimage/writer/human_readable_test.go index 54369b79d..e77cec639 100644 --- a/internal/inspectimage/writer/human_readable_test.go +++ b/internal/inspectimage/writer/human_readable_test.go @@ -32,11 +32,13 @@ func testHumanReadable(t *testing.T, when spec.G, it spec.S) { assert = h.NewAssertionManager(t) outBuf bytes.Buffer - remoteInfo *client.ImageInfo - localInfo *client.ImageInfo - + remoteInfo *client.ImageInfo remoteWithExtensionInfo *client.ImageInfo - localWithExtensionInfo *client.ImageInfo + remoteInfoNoRebasable *client.ImageInfo + + localInfo *client.ImageInfo + localWithExtensionInfo *client.ImageInfo + localInfoNoRebasable *client.ImageInfo expectedRemoteOutput = `REMOTE: @@ -52,6 +54,8 @@ Run Images: some-remote-mirror other-remote-mirror +Rebasable: true + Buildpacks: ID VERSION HOMEPAGE test.bp.one.remote 1.0.0 https://some-homepage-one @@ -62,31 +66,32 @@ Processes: TYPE SHELL COMMAND ARGS WORK DIR some-remote-type (default) bash /some/remote command some remote args /some-test-work-dir other-remote-type /other/remote/command other remote args /other-test-work-dir` + expectedRemoteNoRebasableOutput = `REMOTE: - expectedLocalOutput = `LOCAL: - -Stack: test.stack.id.local +Stack: test.stack.id.remote Base Image: - Reference: some-local-run-image-reference - Top Layer: some-local-top-layer + Reference: some-remote-run-image-reference + Top Layer: some-remote-top-layer Run Images: - user-configured-mirror-for-local (user-configured) - some-local-run-image - some-local-mirror - other-local-mirror + user-configured-mirror-for-remote (user-configured) + some-remote-run-image + some-remote-mirror + other-remote-mirror + +Rebasable: false Buildpacks: - ID VERSION HOMEPAGE - test.bp.one.local 1.0.0 https://some-homepage-one - test.bp.two.local 2.0.0 https://some-homepage-two - test.bp.three.local 3.0.0 - + ID VERSION HOMEPAGE + test.bp.one.remote 1.0.0 https://some-homepage-one + test.bp.two.remote 2.0.0 https://some-homepage-two + test.bp.three.remote 3.0.0 - Processes: - TYPE SHELL COMMAND ARGS WORK DIR - some-local-type (default) bash /some/local command some local args /some-test-work-dir - other-local-type /other/local/command other local args /other-test-work-dir` + TYPE SHELL COMMAND ARGS WORK DIR + some-remote-type (default) bash /some/remote command some remote args /some-test-work-dir + other-remote-type /other/remote/command other remote args /other-test-work-dir` expectedRemoteWithExtensionOutput = `REMOTE: @@ -102,6 +107,8 @@ Run Images: some-remote-mirror other-remote-mirror +Rebasable: true + Buildpacks: ID VERSION HOMEPAGE test.bp.one.remote 1.0.0 https://some-homepage-one @@ -114,12 +121,64 @@ Extensions: test.bp.two.remote 2.0.0 https://some-homepage-two test.bp.three.remote 3.0.0 - - Processes: TYPE SHELL COMMAND ARGS WORK DIR some-remote-type (default) bash /some/remote command some remote args /some-test-work-dir other-remote-type /other/remote/command other remote args /other-test-work-dir` + expectedLocalOutput = `LOCAL: + +Stack: test.stack.id.local + +Base Image: + Reference: some-local-run-image-reference + Top Layer: some-local-top-layer + +Run Images: + user-configured-mirror-for-local (user-configured) + some-local-run-image + some-local-mirror + other-local-mirror + +Rebasable: true + +Buildpacks: + ID VERSION HOMEPAGE + test.bp.one.local 1.0.0 https://some-homepage-one + test.bp.two.local 2.0.0 https://some-homepage-two + test.bp.three.local 3.0.0 - + +Processes: + TYPE SHELL COMMAND ARGS WORK DIR + some-local-type (default) bash /some/local command some local args /some-test-work-dir + other-local-type /other/local/command other local args /other-test-work-dir` + expectedLocalNoRebasableOutput = `LOCAL: + +Stack: test.stack.id.local + +Base Image: + Reference: some-local-run-image-reference + Top Layer: some-local-top-layer + +Run Images: + user-configured-mirror-for-local (user-configured) + some-local-run-image + some-local-mirror + other-local-mirror + +Rebasable: false + +Buildpacks: + ID VERSION HOMEPAGE + test.bp.one.local 1.0.0 https://some-homepage-one + test.bp.two.local 2.0.0 https://some-homepage-two + test.bp.three.local 3.0.0 - + +Processes: + TYPE SHELL COMMAND ARGS WORK DIR + some-local-type (default) bash /some/local command some local args /some-test-work-dir + other-local-type /other/local/command other local args /other-test-work-dir` + expectedLocalWithExtensionOutput = `LOCAL: Stack: test.stack.id.local @@ -134,6 +193,8 @@ Run Images: some-local-mirror other-local-mirror +Rebasable: true + Buildpacks: ID VERSION HOMEPAGE test.bp.one.local 1.0.0 https://some-homepage-one @@ -146,7 +207,6 @@ Extensions: test.bp.two.local 2.0.0 https://some-homepage-two test.bp.three.local 3.0.0 - - Processes: TYPE SHELL COMMAND ARGS WORK DIR some-local-type (default) bash /some/local command some local args /some-test-work-dir @@ -155,236 +215,13 @@ Processes: when("Print", func() { it.Before(func() { - type someData struct { - String string - Bool bool - Int int - Nested struct { - String string - } - } - - remoteInfo = &client.ImageInfo{ - StackID: "test.stack.id.remote", - Buildpacks: []buildpack.GroupElement{ - {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, - {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, - {ID: "test.bp.three.remote", Version: "3.0.0"}, - }, - Base: files.RunImageForRebase{ - TopLayer: "some-remote-top-layer", - Reference: "some-remote-run-image-reference", - }, - Stack: files.Stack{ - RunImage: files.RunImageForExport{ - Image: "some-remote-run-image", - Mirrors: []string{"some-remote-mirror", "other-remote-mirror"}, - }, - }, - BOM: []buildpack.BOMEntry{{ - Require: buildpack.Require{ - Name: "name-1", - Version: "version-1", - Metadata: map[string]interface{}{ - "RemoteData": someData{ - String: "aString", - Bool: true, - Int: 123, - Nested: struct { - String string - }{ - String: "anotherString", - }, - }, - }, - }, - Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"}, - }}, - Processes: client.ProcessDetails{ - DefaultProcess: &launch.Process{ - Type: "some-remote-type", - Command: launch.RawCommand{Entries: []string{"/some/remote command"}}, - Args: []string{"some", "remote", "args"}, - Direct: false, - WorkingDirectory: "/some-test-work-dir", - }, - OtherProcesses: []launch.Process{ - { - Type: "other-remote-type", - Command: launch.RawCommand{Entries: []string{"/other/remote/command"}}, - Args: []string{"other", "remote", "args"}, - Direct: true, - WorkingDirectory: "/other-test-work-dir", - }, - }, - }, - } - - localInfo = &client.ImageInfo{ - StackID: "test.stack.id.local", - Buildpacks: []buildpack.GroupElement{ - {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, - {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, - {ID: "test.bp.three.local", Version: "3.0.0"}, - }, - Base: files.RunImageForRebase{ - TopLayer: "some-local-top-layer", - Reference: "some-local-run-image-reference", - }, - Stack: files.Stack{ - RunImage: files.RunImageForExport{ - Image: "some-local-run-image", - Mirrors: []string{"some-local-mirror", "other-local-mirror"}, - }, - }, - BOM: []buildpack.BOMEntry{{ - Require: buildpack.Require{ - Name: "name-1", - Version: "version-1", - Metadata: map[string]interface{}{ - "LocalData": someData{ - Bool: false, - Int: 456, - }, - }, - }, - Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"}, - }}, - Processes: client.ProcessDetails{ - DefaultProcess: &launch.Process{ - Type: "some-local-type", - Command: launch.RawCommand{Entries: []string{"/some/local command"}}, - Args: []string{"some", "local", "args"}, - Direct: false, - WorkingDirectory: "/some-test-work-dir", - }, - OtherProcesses: []launch.Process{ - { - Type: "other-local-type", - Command: launch.RawCommand{Entries: []string{"/other/local/command"}}, - Args: []string{"other", "local", "args"}, - Direct: true, - WorkingDirectory: "/other-test-work-dir", - }, - }, - }, - } - - remoteWithExtensionInfo = &client.ImageInfo{ - StackID: "test.stack.id.remote", - Buildpacks: []buildpack.GroupElement{ - {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, - {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, - {ID: "test.bp.three.remote", Version: "3.0.0"}, - }, - Extensions: []buildpack.GroupElement{ - {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, - {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, - {ID: "test.bp.three.remote", Version: "3.0.0"}, - }, - Base: files.RunImageForRebase{ - TopLayer: "some-remote-top-layer", - Reference: "some-remote-run-image-reference", - }, - Stack: files.Stack{ - RunImage: files.RunImageForExport{ - Image: "some-remote-run-image", - Mirrors: []string{"some-remote-mirror", "other-remote-mirror"}, - }, - }, - BOM: []buildpack.BOMEntry{{ - Require: buildpack.Require{ - Name: "name-1", - Version: "version-1", - Metadata: map[string]interface{}{ - "RemoteData": someData{ - String: "aString", - Bool: true, - Int: 123, - Nested: struct { - String string - }{ - String: "anotherString", - }, - }, - }, - }, - Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"}, - }}, - Processes: client.ProcessDetails{ - DefaultProcess: &launch.Process{ - Type: "some-remote-type", - Command: launch.RawCommand{Entries: []string{"/some/remote command"}}, - Args: []string{"some", "remote", "args"}, - Direct: false, - WorkingDirectory: "/some-test-work-dir", - }, - OtherProcesses: []launch.Process{ - { - Type: "other-remote-type", - Command: launch.RawCommand{Entries: []string{"/other/remote/command"}}, - Args: []string{"other", "remote", "args"}, - Direct: true, - WorkingDirectory: "/other-test-work-dir", - }, - }, - }, - } - - localWithExtensionInfo = &client.ImageInfo{ - StackID: "test.stack.id.local", - Buildpacks: []buildpack.GroupElement{ - {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, - {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, - {ID: "test.bp.three.local", Version: "3.0.0"}, - }, - Extensions: []buildpack.GroupElement{ - {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, - {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, - {ID: "test.bp.three.local", Version: "3.0.0"}, - }, - Base: files.RunImageForRebase{ - TopLayer: "some-local-top-layer", - Reference: "some-local-run-image-reference", - }, - Stack: files.Stack{ - RunImage: files.RunImageForExport{ - Image: "some-local-run-image", - Mirrors: []string{"some-local-mirror", "other-local-mirror"}, - }, - }, - BOM: []buildpack.BOMEntry{{ - Require: buildpack.Require{ - Name: "name-1", - Version: "version-1", - Metadata: map[string]interface{}{ - "LocalData": someData{ - Bool: false, - Int: 456, - }, - }, - }, - Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"}, - }}, - Processes: client.ProcessDetails{ - DefaultProcess: &launch.Process{ - Type: "some-local-type", - Command: launch.RawCommand{Entries: []string{"/some/local command"}}, - Args: []string{"some", "local", "args"}, - Direct: false, - WorkingDirectory: "/some-test-work-dir", - }, - OtherProcesses: []launch.Process{ - { - Type: "other-local-type", - Command: launch.RawCommand{Entries: []string{"/other/local/command"}}, - Args: []string{"other", "local", "args"}, - Direct: true, - WorkingDirectory: "/other-test-work-dir", - }, - }, - }, - } + remoteInfo = getRemoteBasicImageInfo(t) + remoteWithExtensionInfo = getRemoteImageInfoWithExtension(t) + remoteInfoNoRebasable = getRemoteImageInfoNoRebasable(t) + + localInfo = getBasicLocalImageInfo(t) + localWithExtensionInfo = getLocalImageInfoWithExtension(t) + localInfoNoRebasable = getLocalImageInfoNoRebasable(t) outBuf = bytes.Buffer{} }) @@ -480,6 +317,34 @@ Processes: assert.Contains(outBuf.String(), expectedLocalOutput) assert.NotContains(outBuf.String(), expectedRemoteOutput) }) + it("prints local no rebasable image info in a human readable format", func() { + runImageMirrors := []config.RunImage{ + { + Image: "un-used-run-image", + Mirrors: []string{"un-used"}, + }, + { + Image: "some-local-run-image", + Mirrors: []string{"user-configured-mirror-for-local"}, + }, + { + Image: "some-remote-run-image", + Mirrors: []string{"user-configured-mirror-for-remote"}, + }, + } + sharedImageInfo := inspectimage.GeneralInfo{ + Name: "test-image", + RunImageMirrors: runImageMirrors, + } + humanReadableWriter := writer.NewHumanReadable() + + logger := logging.NewLogWithWriters(&outBuf, &outBuf) + err := humanReadableWriter.Print(logger, sharedImageInfo, localInfoNoRebasable, nil, nil, nil) + assert.Nil(err) + + assert.Contains(outBuf.String(), expectedLocalNoRebasableOutput) + assert.NotContains(outBuf.String(), expectedRemoteOutput) + }) }) when("only localWithExtension image exists", func() { @@ -542,6 +407,34 @@ Processes: assert.NotContains(outBuf.String(), expectedLocalOutput) assert.Contains(outBuf.String(), expectedRemoteOutput) }) + it("prints remote no rebasable image info in a human readable format", func() { + runImageMirrors := []config.RunImage{ + { + Image: "un-used-run-image", + Mirrors: []string{"un-used"}, + }, + { + Image: "some-local-run-image", + Mirrors: []string{"user-configured-mirror-for-local"}, + }, + { + Image: "some-remote-run-image", + Mirrors: []string{"user-configured-mirror-for-remote"}, + }, + } + sharedImageInfo := inspectimage.GeneralInfo{ + Name: "test-image", + RunImageMirrors: runImageMirrors, + } + humanReadableWriter := writer.NewHumanReadable() + + logger := logging.NewLogWithWriters(&outBuf, &outBuf) + err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteInfoNoRebasable, nil, nil) + assert.Nil(err) + + assert.NotContains(outBuf.String(), expectedLocalOutput) + assert.Contains(outBuf.String(), expectedRemoteNoRebasableOutput) + }) when("buildpack metadata is missing", func() { it.Before(func() { @@ -812,3 +705,215 @@ Processes: }) }) } + +func getRemoteBasicImageInfo(t testing.TB) *client.ImageInfo { + t.Helper() + return getRemoteImageInfo(t, false, true) +} +func getRemoteImageInfoWithExtension(t testing.TB) *client.ImageInfo { + t.Helper() + return getRemoteImageInfo(t, true, true) +} + +func getRemoteImageInfoNoRebasable(t testing.TB) *client.ImageInfo { + t.Helper() + return getRemoteImageInfo(t, false, false) +} + +func getRemoteImageInfo(t testing.TB, extension bool, rebasable bool) *client.ImageInfo { + t.Helper() + + mockedStackID := "test.stack.id.remote" + + mockedBuildpacks := []buildpack.GroupElement{ + {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + {ID: "test.bp.three.remote", Version: "3.0.0"}, + } + + mockedBase := files.RunImageForRebase{ + TopLayer: "some-remote-top-layer", + Reference: "some-remote-run-image-reference", + } + + mockedStack := files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-remote-run-image", + Mirrors: []string{"some-remote-mirror", "other-remote-mirror"}, + }, + } + + type someData struct { + String string + Bool bool + Int int + Nested struct { + String string + } + } + mockedMetadata := map[string]interface{}{ + "RemoteData": someData{ + String: "aString", + Bool: true, + Int: 123, + Nested: struct { + String string + }{ + String: "anotherString", + }, + }, + } + + mockedBOM := []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Metadata: mockedMetadata, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"}, + }} + + mockedProcesses := client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-remote-type", + Command: launch.RawCommand{Entries: []string{"/some/remote command"}}, + Args: []string{"some", "remote", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-remote-type", + Command: launch.RawCommand{Entries: []string{"/other/remote/command"}}, + Args: []string{"other", "remote", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + } + + mockedExtension := []buildpack.GroupElement{ + {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + {ID: "test.bp.three.remote", Version: "3.0.0"}, + } + + imageInfo := &client.ImageInfo{ + StackID: mockedStackID, + Buildpacks: mockedBuildpacks, + Base: mockedBase, + Stack: mockedStack, + BOM: mockedBOM, + Processes: mockedProcesses, + Rebasable: rebasable, + } + + if extension { + imageInfo.Extensions = mockedExtension + } + + return imageInfo +} + +func getBasicLocalImageInfo(t testing.TB) *client.ImageInfo { + t.Helper() + return getLocalImageInfo(t, false, true) +} + +func getLocalImageInfoWithExtension(t testing.TB) *client.ImageInfo { + t.Helper() + return getLocalImageInfo(t, true, true) +} + +func getLocalImageInfoNoRebasable(t testing.TB) *client.ImageInfo { + t.Helper() + return getLocalImageInfo(t, false, false) +} + +func getLocalImageInfo(t testing.TB, extension bool, rebasable bool) *client.ImageInfo { + t.Helper() + + mockedStackID := "test.stack.id.local" + + mockedBuildpacks := []buildpack.GroupElement{ + {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + {ID: "test.bp.three.local", Version: "3.0.0"}, + } + + mockedBase := files.RunImageForRebase{ + TopLayer: "some-local-top-layer", + Reference: "some-local-run-image-reference", + } + + mockedPlatform := files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-local-run-image", + Mirrors: []string{"some-local-mirror", "other-local-mirror"}, + }, + } + + type someData struct { + String string + Bool bool + Int int + Nested struct { + String string + } + } + mockedMetadata := map[string]interface{}{ + "LocalData": someData{ + Bool: false, + Int: 456, + }, + } + + mockedBOM := []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Version: "version-1", + Metadata: mockedMetadata, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"}, + }} + + mockedProcesses := client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-local-type", + Command: launch.RawCommand{Entries: []string{"/some/local command"}}, + Args: []string{"some", "local", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-local-type", + Command: launch.RawCommand{Entries: []string{"/other/local/command"}}, + Args: []string{"other", "local", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + } + + mockedExtension := []buildpack.GroupElement{ + {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + {ID: "test.bp.three.local", Version: "3.0.0"}, + } + + imageInfo := &client.ImageInfo{ + StackID: mockedStackID, + Buildpacks: mockedBuildpacks, + Base: mockedBase, + Stack: mockedPlatform, + BOM: mockedBOM, + Processes: mockedProcesses, + Rebasable: rebasable, + } + + if extension { + imageInfo.Extensions = mockedExtension + } + + return imageInfo +} diff --git a/internal/inspectimage/writer/json_test.go b/internal/inspectimage/writer/json_test.go index b08e1a108..fa61c3f18 100644 --- a/internal/inspectimage/writer/json_test.go +++ b/internal/inspectimage/writer/json_test.go @@ -30,12 +30,79 @@ func testJSON(t *testing.T, when spec.G, it spec.S) { assert = h.NewAssertionManager(t) outBuf bytes.Buffer - remoteInfo *client.ImageInfo - localInfo *client.ImageInfo + remoteInfo *client.ImageInfo + remoteInfoNoRebasable *client.ImageInfo + localInfo *client.ImageInfo + localInfoNoRebasable *client.ImageInfo expectedLocalOutput = `{ "local_info": { "stack": "test.stack.id.local", + "rebasable": true, + "base_image": { + "top_layer": "some-local-top-layer", + "reference": "some-local-run-image-reference" + }, + "run_images": [ + { + "name": "user-configured-mirror-for-local", + "user_configured": true + }, + { + "name": "some-local-run-image" + }, + { + "name": "some-local-mirror" + }, + { + "name": "other-local-mirror" + } + ], + "buildpacks": [ + { + "homepage": "https://some-homepage-one", + "id": "test.bp.one.local", + "version": "1.0.0" + }, + { + "homepage": "https://some-homepage-two", + "id": "test.bp.two.local", + "version": "2.0.0" + } + ], + "extensions": null, + "processes": [ + { + "type": "some-local-type", + "shell": "bash", + "command": "/some/local command", + "default": true, + "args": [ + "some", + "local", + "args" + ], + "working-dir": "/some-test-work-dir" + }, + { + "type": "other-local-type", + "shell": "", + "command": "/other/local/command", + "default": false, + "args": [ + "other", + "local", + "args" + ], + "working-dir": "/other-test-work-dir" + } + ] + } +}` + expectedLocalNoRebasableOutput = `{ + "local_info": { + "stack": "test.stack.id.local", + "rebasable": false, "base_image": { "top_layer": "some-local-top-layer", "reference": "some-local-run-image-reference" @@ -99,6 +166,71 @@ func testJSON(t *testing.T, when spec.G, it spec.S) { expectedRemoteOutput = `{ "remote_info": { "stack": "test.stack.id.remote", + "rebasable": true, + "base_image": { + "top_layer": "some-remote-top-layer", + "reference": "some-remote-run-image-reference" + }, + "run_images": [ + { + "name": "user-configured-mirror-for-remote", + "user_configured": true + }, + { + "name": "some-remote-run-image" + }, + { + "name": "some-remote-mirror" + }, + { + "name": "other-remote-mirror" + } + ], + "buildpacks": [ + { + "id": "test.bp.one.remote", + "version": "1.0.0", + "homepage": "https://some-homepage-one" + }, + { + "id": "test.bp.two.remote", + "version": "2.0.0", + "homepage": "https://some-homepage-two" + } + ], + "extensions": null, + "processes": [ + { + "type": "some-remote-type", + "shell": "bash", + "command": "/some/remote command", + "default": true, + "args": [ + "some", + "remote", + "args" + ], + "working-dir": "/some-test-work-dir" + }, + { + "type": "other-remote-type", + "shell": "", + "command": "/other/remote/command", + "default": false, + "args": [ + "other", + "remote", + "args" + ], + "working-dir": "/other-test-work-dir" + } + ] + } +}` + expectedRemoteNoRebasableOutput = `{ + "remote_info": { + "stack": "test.stack.id.remote", + "rebasable": false, "base_image": { "top_layer": "some-remote-top-layer", "reference": "some-remote-run-image-reference" @@ -225,6 +357,62 @@ func testJSON(t *testing.T, when spec.G, it spec.S) { }, }, }, + Rebasable: true, + } + remoteInfoNoRebasable = &client.ImageInfo{ + StackID: "test.stack.id.remote", + Buildpacks: []buildpack.GroupElement{ + {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + }, + Base: files.RunImageForRebase{ + TopLayer: "some-remote-top-layer", + Reference: "some-remote-run-image-reference", + }, + Stack: files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-remote-run-image", + Mirrors: []string{"some-remote-mirror", "other-remote-mirror"}, + }, + }, + BOM: []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Version: "version-1", + Metadata: map[string]interface{}{ + "RemoteData": someData{ + String: "aString", + Bool: true, + Int: 123, + Nested: struct { + String string + }{ + String: "anotherString", + }, + }, + }, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + }}, + Processes: client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-remote-type", + Command: launch.RawCommand{Entries: []string{"/some/remote command"}}, + Args: []string{"some", "remote", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-remote-type", + Command: launch.RawCommand{Entries: []string{"/other/remote/command"}}, + Args: []string{"other", "remote", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + }, + Rebasable: false, } localInfo = &client.ImageInfo{ @@ -274,6 +462,56 @@ func testJSON(t *testing.T, when spec.G, it spec.S) { }, }, }, + Rebasable: true, + } + localInfoNoRebasable = &client.ImageInfo{ + StackID: "test.stack.id.local", + Buildpacks: []buildpack.GroupElement{ + {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + }, + Base: files.RunImageForRebase{ + TopLayer: "some-local-top-layer", + Reference: "some-local-run-image-reference", + }, + Stack: files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-local-run-image", + Mirrors: []string{"some-local-mirror", "other-local-mirror"}, + }, + }, + BOM: []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Version: "version-1", + Metadata: map[string]interface{}{ + "LocalData": someData{ + Bool: false, + Int: 456, + }, + }, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + }}, + Processes: client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-local-type", + Command: launch.RawCommand{Entries: []string{"/some/local command"}}, + Args: []string{"some", "local", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-local-type", + Command: launch.RawCommand{Entries: []string{"/other/local/command"}}, + Args: []string{"other", "local", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + }, + Rebasable: false, } outBuf = bytes.Buffer{} @@ -309,6 +547,35 @@ func testJSON(t *testing.T, when spec.G, it spec.S) { assert.ContainsJSON(outBuf.String(), expectedLocalOutput) assert.ContainsJSON(outBuf.String(), expectedRemoteOutput) }) + it("prints both local and remote no rebasable images info in a JSON format", func() { + runImageMirrors := []config.RunImage{ + { + Image: "un-used-run-image", + Mirrors: []string{"un-used"}, + }, + { + Image: "some-local-run-image", + Mirrors: []string{"user-configured-mirror-for-local"}, + }, + { + Image: "some-remote-run-image", + Mirrors: []string{"user-configured-mirror-for-remote"}, + }, + } + sharedImageInfo := inspectimage.GeneralInfo{ + Name: "test-image", + RunImageMirrors: runImageMirrors, + } + jsonWriter := writer.NewJSON() + + logger := logging.NewLogWithWriters(&outBuf, &outBuf) + err := jsonWriter.Print(logger, sharedImageInfo, localInfoNoRebasable, remoteInfoNoRebasable, nil, nil) + assert.Nil(err) + + assert.ContainsJSON(outBuf.String(), `{ "image_name": "test-image" }`) + assert.ContainsJSON(outBuf.String(), expectedLocalNoRebasableOutput) + assert.ContainsJSON(outBuf.String(), expectedRemoteNoRebasableOutput) + }) }) when("only local image exists", func() { diff --git a/internal/inspectimage/writer/toml_test.go b/internal/inspectimage/writer/toml_test.go index 0e1406df6..27f92f4de 100644 --- a/internal/inspectimage/writer/toml_test.go +++ b/internal/inspectimage/writer/toml_test.go @@ -30,11 +30,69 @@ func testTOML(t *testing.T, when spec.G, it spec.S) { assert = h.NewAssertionManager(t) outBuf bytes.Buffer - remoteInfo *client.ImageInfo - localInfo *client.ImageInfo + remoteInfo *client.ImageInfo + remoteInfoNoRebasable *client.ImageInfo + localInfo *client.ImageInfo + localInfoNoRebasable *client.ImageInfo expectedLocalOutput = `[local_info] stack = 'test.stack.id.local' +rebasable = true + +[local_info.base_image] +top_layer = 'some-local-top-layer' +reference = 'some-local-run-image-reference' + +[[local_info.run_images]] +name = 'user-configured-mirror-for-local' +user_configured = true + +[[local_info.run_images]] +name = 'some-local-run-image' + +[[local_info.run_images]] +name = 'some-local-mirror' + +[[local_info.run_images]] +name = 'other-local-mirror' + +[[local_info.buildpacks]] +id = 'test.bp.one.local' +version = '1.0.0' +homepage = 'https://some-homepage-one' + +[[local_info.buildpacks]] +id = 'test.bp.two.local' +version = '2.0.0' +homepage = 'https://some-homepage-two' + +[[local_info.processes]] +type = 'some-local-type' +shell = 'bash' +command = '/some/local command' +default = true +args = [ + 'some', + 'local', + 'args', +] +working-dir = "/some-test-work-dir" + +[[local_info.processes]] +type = 'other-local-type' +shell = '' +command = '/other/local/command' +default = false +args = [ + 'other', + 'local', + 'args', +] +working-dir = "/other-test-work-dir" +` + expectedLocalNoRebasableOutput = `[local_info] +stack = 'test.stack.id.local' +rebasable = false [local_info.base_image] top_layer = 'some-local-top-layer' @@ -91,6 +149,63 @@ working-dir = "/other-test-work-dir" expectedRemoteOutput = ` [remote_info] stack = 'test.stack.id.remote' +rebasable = true + +[remote_info.base_image] +top_layer = 'some-remote-top-layer' +reference = 'some-remote-run-image-reference' + +[[remote_info.run_images]] +name = 'user-configured-mirror-for-remote' +user_configured = true + +[[remote_info.run_images]] +name = 'some-remote-run-image' + +[[remote_info.run_images]] +name = 'some-remote-mirror' + +[[remote_info.run_images]] +name = 'other-remote-mirror' + +[[remote_info.buildpacks]] +id = 'test.bp.one.remote' +version = '1.0.0' +homepage = 'https://some-homepage-one' + +[[remote_info.buildpacks]] +id = 'test.bp.two.remote' +version = '2.0.0' +homepage = 'https://some-homepage-two' + +[[remote_info.processes]] +type = 'some-remote-type' +shell = 'bash' +command = '/some/remote command' +default = true +args = [ + 'some', + 'remote', + 'args', +] +working-dir = "/some-test-work-dir" + +[[remote_info.processes]] +type = 'other-remote-type' +shell = '' +command = '/other/remote/command' +default = false +args = [ + 'other', + 'remote', + 'args', +] +working-dir = "/other-test-work-dir" +` + expectedRemoteNoRebasableOutput = ` +[remote_info] +stack = 'test.stack.id.remote' +rebasable = false [remote_info.base_image] top_layer = 'some-remote-top-layer' @@ -209,6 +324,62 @@ working-dir = "/other-test-work-dir" }, }, }, + Rebasable: true, + } + remoteInfoNoRebasable = &client.ImageInfo{ + StackID: "test.stack.id.remote", + Buildpacks: []buildpack.GroupElement{ + {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + }, + Base: files.RunImageForRebase{ + TopLayer: "some-remote-top-layer", + Reference: "some-remote-run-image-reference", + }, + Stack: files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-remote-run-image", + Mirrors: []string{"some-remote-mirror", "other-remote-mirror"}, + }, + }, + BOM: []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Version: "version-1", + Metadata: map[string]interface{}{ + "RemoteData": someData{ + String: "aString", + Bool: true, + Int: 123, + Nested: struct { + String string + }{ + String: "anotherString", + }, + }, + }, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + }}, + Processes: client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-remote-type", + Command: launch.RawCommand{Entries: []string{"/some/remote command"}}, + Args: []string{"some", "remote", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-remote-type", + Command: launch.RawCommand{Entries: []string{"/other/remote/command"}}, + Args: []string{"other", "remote", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + }, + Rebasable: false, } localInfo = &client.ImageInfo{ @@ -258,6 +429,56 @@ working-dir = "/other-test-work-dir" }, }, }, + Rebasable: true, + } + localInfoNoRebasable = &client.ImageInfo{ + StackID: "test.stack.id.local", + Buildpacks: []buildpack.GroupElement{ + {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + }, + Base: files.RunImageForRebase{ + TopLayer: "some-local-top-layer", + Reference: "some-local-run-image-reference", + }, + Stack: files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-local-run-image", + Mirrors: []string{"some-local-mirror", "other-local-mirror"}, + }, + }, + BOM: []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Version: "version-1", + Metadata: map[string]interface{}{ + "LocalData": someData{ + Bool: false, + Int: 456, + }, + }, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + }}, + Processes: client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-local-type", + Command: launch.RawCommand{Entries: []string{"/some/local command"}}, + Args: []string{"some", "local", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-local-type", + Command: launch.RawCommand{Entries: []string{"/other/local/command"}}, + Args: []string{"other", "local", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + }, + Rebasable: false, } outBuf = bytes.Buffer{} @@ -293,6 +514,35 @@ working-dir = "/other-test-work-dir" assert.ContainsTOML(outBuf.String(), expectedLocalOutput) assert.ContainsTOML(outBuf.String(), expectedRemoteOutput) }) + it("prints both local and remote no rebasable images info in a TOML format", func() { + runImageMirrors := []config.RunImage{ + { + Image: "un-used-run-image", + Mirrors: []string{"un-used"}, + }, + { + Image: "some-local-run-image", + Mirrors: []string{"user-configured-mirror-for-local"}, + }, + { + Image: "some-remote-run-image", + Mirrors: []string{"user-configured-mirror-for-remote"}, + }, + } + sharedImageInfo := inspectimage.GeneralInfo{ + Name: "test-image", + RunImageMirrors: runImageMirrors, + } + tomlWriter := writer.NewTOML() + + logger := logging.NewLogWithWriters(&outBuf, &outBuf) + err := tomlWriter.Print(logger, sharedImageInfo, localInfoNoRebasable, remoteInfoNoRebasable, nil, nil) + assert.Nil(err) + + assert.ContainsTOML(outBuf.String(), `image_name = "test-image"`) + assert.ContainsTOML(outBuf.String(), expectedLocalNoRebasableOutput) + assert.ContainsTOML(outBuf.String(), expectedRemoteNoRebasableOutput) + }) }) when("only local image exists", func() { diff --git a/internal/inspectimage/writer/yaml_test.go b/internal/inspectimage/writer/yaml_test.go index 3a422eb52..ee97ec7a0 100644 --- a/internal/inspectimage/writer/yaml_test.go +++ b/internal/inspectimage/writer/yaml_test.go @@ -30,12 +30,56 @@ func testYAML(t *testing.T, when spec.G, it spec.S) { assert = h.NewAssertionManager(t) outBuf bytes.Buffer - remoteInfo *client.ImageInfo - localInfo *client.ImageInfo + remoteInfo *client.ImageInfo + remoteInfoNoRebasable *client.ImageInfo + localInfo *client.ImageInfo + localInfoNoRebasable *client.ImageInfo expectedLocalOutput = `--- local_info: stack: test.stack.id.local + rebasable: true + base_image: + top_layer: some-local-top-layer + reference: some-local-run-image-reference + run_images: + - name: user-configured-mirror-for-local + user_configured: true + - name: some-local-run-image + - name: some-local-mirror + - name: other-local-mirror + buildpacks: + - homepage: https://some-homepage-one + id: test.bp.one.local + version: 1.0.0 + - homepage: https://some-homepage-two + id: test.bp.two.local + version: 2.0.0 + extensions: [] + processes: + - type: some-local-type + shell: bash + command: "/some/local command" + default: true + args: + - some + - local + - args + working-dir: /some-test-work-dir + - type: other-local-type + shell: '' + command: "/other/local/command" + default: false + args: + - other + - local + - args + working-dir: /other-test-work-dir +` + expectedLocalNoRebasableOutput = `--- +local_info: + stack: test.stack.id.local + rebasable: false base_image: top_layer: some-local-top-layer reference: some-local-run-image-reference @@ -76,6 +120,48 @@ local_info: expectedRemoteOutput = `--- remote_info: stack: test.stack.id.remote + rebasable: true + base_image: + top_layer: some-remote-top-layer + reference: some-remote-run-image-reference + run_images: + - name: user-configured-mirror-for-remote + user_configured: true + - name: some-remote-run-image + - name: some-remote-mirror + - name: other-remote-mirror + buildpacks: + - homepage: https://some-homepage-one + id: test.bp.one.remote + version: 1.0.0 + - homepage: https://some-homepage-two + id: test.bp.two.remote + version: 2.0.0 + extensions: [] + processes: + - type: some-remote-type + shell: bash + command: "/some/remote command" + default: true + args: + - some + - remote + - args + working-dir: /some-test-work-dir + - type: other-remote-type + shell: '' + command: "/other/remote/command" + default: false + args: + - other + - remote + - args + working-dir: /other-test-work-dir +` + expectedRemoteNoRebasableOutput = `--- +remote_info: + stack: test.stack.id.remote + rebasable: false base_image: top_layer: some-remote-top-layer reference: some-remote-run-image-reference @@ -179,6 +265,62 @@ remote_info: }, }, }, + Rebasable: true, + } + remoteInfoNoRebasable = &client.ImageInfo{ + StackID: "test.stack.id.remote", + Buildpacks: []buildpack.GroupElement{ + {ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + }, + Base: files.RunImageForRebase{ + TopLayer: "some-remote-top-layer", + Reference: "some-remote-run-image-reference", + }, + Stack: files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-remote-run-image", + Mirrors: []string{"some-remote-mirror", "other-remote-mirror"}, + }, + }, + BOM: []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Version: "version-1", + Metadata: map[string]interface{}{ + "RemoteData": someData{ + String: "aString", + Bool: true, + Int: 123, + Nested: struct { + String string + }{ + String: "anotherString", + }, + }, + }, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + }}, + Processes: client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-remote-type", + Command: launch.RawCommand{Entries: []string{"/some/remote command"}}, + Args: []string{"some", "remote", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-remote-type", + Command: launch.RawCommand{Entries: []string{"/other/remote/command"}}, + Args: []string{"other", "remote", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + }, + Rebasable: false, } localInfo = &client.ImageInfo{ @@ -228,6 +370,56 @@ remote_info: }, }, }, + Rebasable: true, + } + localInfoNoRebasable = &client.ImageInfo{ + StackID: "test.stack.id.local", + Buildpacks: []buildpack.GroupElement{ + {ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + {ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"}, + }, + Base: files.RunImageForRebase{ + TopLayer: "some-local-top-layer", + Reference: "some-local-run-image-reference", + }, + Stack: files.Stack{ + RunImage: files.RunImageForExport{ + Image: "some-local-run-image", + Mirrors: []string{"some-local-mirror", "other-local-mirror"}, + }, + }, + BOM: []buildpack.BOMEntry{{ + Require: buildpack.Require{ + Name: "name-1", + Version: "version-1", + Metadata: map[string]interface{}{ + "LocalData": someData{ + Bool: false, + Int: 456, + }, + }, + }, + Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"}, + }}, + Processes: client.ProcessDetails{ + DefaultProcess: &launch.Process{ + Type: "some-local-type", + Command: launch.RawCommand{Entries: []string{"/some/local command"}}, + Args: []string{"some", "local", "args"}, + Direct: false, + WorkingDirectory: "/some-test-work-dir", + }, + OtherProcesses: []launch.Process{ + { + Type: "other-local-type", + Command: launch.RawCommand{Entries: []string{"/other/local/command"}}, + Args: []string{"other", "local", "args"}, + Direct: true, + WorkingDirectory: "/other-test-work-dir", + }, + }, + }, + Rebasable: false, } outBuf = bytes.Buffer{} @@ -263,6 +455,35 @@ remote_info: assert.ContainsYAML(outBuf.String(), expectedLocalOutput) assert.ContainsYAML(outBuf.String(), expectedRemoteOutput) }) + it("prints both local and remote no rebasable images info in a YAML format", func() { + runImageMirrors := []config.RunImage{ + { + Image: "un-used-run-image", + Mirrors: []string{"un-used"}, + }, + { + Image: "some-local-run-image", + Mirrors: []string{"user-configured-mirror-for-local"}, + }, + { + Image: "some-remote-run-image", + Mirrors: []string{"user-configured-mirror-for-remote"}, + }, + } + sharedImageInfo := inspectimage.GeneralInfo{ + Name: "test-image", + RunImageMirrors: runImageMirrors, + } + yamlWriter := writer.NewYAML() + + logger := logging.NewLogWithWriters(&outBuf, &outBuf) + err := yamlWriter.Print(logger, sharedImageInfo, localInfoNoRebasable, remoteInfoNoRebasable, nil, nil) + assert.Nil(err) + + assert.ContainsYAML(outBuf.String(), `"image_name": "test-image"`) + assert.ContainsYAML(outBuf.String(), expectedLocalNoRebasableOutput) + assert.ContainsYAML(outBuf.String(), expectedRemoteNoRebasableOutput) + }) }) when("only local image exists", func() { @@ -293,9 +514,7 @@ remote_info: assert.ContainsYAML(outBuf.String(), `"image_name": "test-image"`) assert.ContainsYAML(outBuf.String(), expectedLocalOutput) - assert.NotContains(outBuf.String(), "test.stack.id.remote") - assert.ContainsYAML(outBuf.String(), expectedLocalOutput) }) }) diff --git a/pkg/client/inspect_image.go b/pkg/client/inspect_image.go index e56ef4b6b..ee5e5a6a8 100644 --- a/pkg/client/inspect_image.go +++ b/pkg/client/inspect_image.go @@ -55,6 +55,9 @@ type ImageInfo struct { // Processes lists all processes contributed by buildpacks. Processes ProcessDetails + + // If the image can be rebased + Rebasable bool } // ProcessDetails is a collection of all start command metadata @@ -120,6 +123,11 @@ func (c *Client) InspectImage(name string, daemon bool) (*ImageInfo, error) { return nil, err } + var rebasable bool + if _, err := dist.GetLabel(img, platform.RebasableLabel, &rebasable); err != nil { + return nil, err + } + platformAPI, err := img.Env(platformAPIEnv) if err != nil { return nil, errors.Wrap(err, "reading platform api") @@ -193,6 +201,7 @@ func (c *Client) InspectImage(name string, daemon bool) (*ImageInfo, error) { Buildpacks: buildMD.Buildpacks, Extensions: buildMD.Extensions, Processes: processDetails, + Rebasable: rebasable, }, nil } @@ -203,5 +212,6 @@ func (c *Client) InspectImage(name string, daemon bool) (*ImageInfo, error) { BOM: buildMD.BOM, Buildpacks: buildMD.Buildpacks, Processes: processDetails, + Rebasable: rebasable, }, nil } diff --git a/pkg/client/inspect_image_test.go b/pkg/client/inspect_image_test.go index fe71947aa..124c25902 100644 --- a/pkg/client/inspect_image_test.go +++ b/pkg/client/inspect_image_test.go @@ -42,6 +42,7 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { mockDockerClient *testmocks.MockCommonAPIClient mockController *gomock.Controller mockImage *testmocks.MockImage + mockImageNoRebasable *testmocks.MockImage mockImageWithExtension *testmocks.MockImage out bytes.Buffer ) @@ -58,6 +59,7 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { mockImage = testmocks.NewImage("some/image", "", nil) h.AssertNil(t, mockImage.SetWorkingDir("/test-workdir")) h.AssertNil(t, mockImage.SetLabel("io.buildpacks.stack.id", "test.stack.id")) + h.AssertNil(t, mockImage.SetLabel("io.buildpacks.rebasable", "true")) h.AssertNil(t, mockImage.SetLabel( "io.buildpacks.lifecycle.metadata", `{ @@ -114,9 +116,70 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { }`, )) + mockImageNoRebasable = testmocks.NewImage("some/imageNoRebasable", "", nil) + h.AssertNil(t, mockImageNoRebasable.SetWorkingDir("/test-workdir")) + h.AssertNil(t, mockImageNoRebasable.SetLabel("io.buildpacks.stack.id", "test.stack.id")) + h.AssertNil(t, mockImageNoRebasable.SetLabel("io.buildpacks.rebasable", "false")) + h.AssertNil(t, mockImageNoRebasable.SetLabel( + "io.buildpacks.lifecycle.metadata", + `{ + "stack": { + "runImage": { + "image": "some-run-image-no-rebasable", + "mirrors": [ + "some-mirror", + "other-mirror" + ] + } + }, + "runImage": { + "topLayer": "some-top-layer", + "reference": "some-run-image-reference" + } +}`, + )) + h.AssertNil(t, mockImage.SetLabel( + "io.buildpacks.build.metadata", + `{ + "bom": [ + { + "name": "some-bom-element" + } + ], + "buildpacks": [ + { + "id": "some-buildpack", + "version": "some-version" + }, + { + "id": "other-buildpack", + "version": "other-version" + } + ], + "processes": [ + { + "type": "other-process", + "command": "/other/process", + "args": ["opt", "1"], + "direct": true + }, + { + "type": "web", + "command": "/start/web-process", + "args": ["-p", "1234"], + "direct": false + } + ], + "launcher": { + "version": "0.5.0" + } +}`, + )) + mockImageWithExtension = testmocks.NewImage("some/imageWithExtension", "", nil) h.AssertNil(t, mockImageWithExtension.SetWorkingDir("/test-workdir")) h.AssertNil(t, mockImageWithExtension.SetLabel("io.buildpacks.stack.id", "test.stack.id")) + h.AssertNil(t, mockImageWithExtension.SetLabel("io.buildpacks.rebasable", "true")) h.AssertNil(t, mockImageWithExtension.SetLabel( "io.buildpacks.lifecycle.metadata", `{ @@ -195,9 +258,11 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { it.Before(func() { if useDaemon { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImage, nil).AnyTimes() + mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageNoRebasable", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImageNoRebasable, nil).AnyTimes() mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageWithExtension", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImageWithExtension, nil).AnyTimes() } else { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/image", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(mockImage, nil).AnyTimes() + mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageNoRebasable", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(mockImageNoRebasable, nil).AnyTimes() mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/imageWithExtension", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(mockImageWithExtension, nil).AnyTimes() } }) @@ -208,7 +273,7 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, info.StackID, "test.stack.id") }) - it("returns the stack ID", func() { + it("returns the stack ID with extension", func() { infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon) h.AssertNil(t, err) h.AssertEq(t, infoWithExtension.StackID, "test.stack.id") @@ -247,7 +312,7 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { ) }) - it("returns the stack", func() { + it("returns the stack with extension", func() { infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon) h.AssertNil(t, err) h.AssertEq(t, infoWithExtension.Stack, @@ -274,7 +339,7 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { ) }) - it("returns the base image", func() { + it("returns the base image with extension", func() { infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon) h.AssertNil(t, err) h.AssertEq(t, infoWithExtension.Base, @@ -285,6 +350,24 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { ) }) + it("returns the rebasable image", func() { + info, err := subject.InspectImage("some/image", useDaemon) + h.AssertNil(t, err) + h.AssertEq(t, info.Rebasable, true) + }) + + it("returns the no rebasable image", func() { + info, err := subject.InspectImage("some/imageNoRebasable", useDaemon) + h.AssertNil(t, err) + h.AssertEq(t, info.Rebasable, false) + }) + + it("returns the rebasable image with Extension", func() { + infoRebasableWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon) + h.AssertNil(t, err) + h.AssertEq(t, infoRebasableWithExtension.Rebasable, true) + }) + it("returns the BOM", func() { info, err := subject.InspectImage("some/image", useDaemon) h.AssertNil(t, err) @@ -314,7 +397,7 @@ func testInspectImage(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, info.Buildpacks[1].Version, "other-version") }) - it("returns the buildpacks", func() { + it("returns the buildpacks with extension", func() { infoWithExtension, err := subject.InspectImage("some/imageWithExtension", useDaemon) h.AssertNil(t, err)