From e7cd44ba1af509818298d05659c984089af4f7f9 Mon Sep 17 00:00:00 2001 From: Zymate Studio Date: Thu, 2 Nov 2023 14:12:27 +0530 Subject: [PATCH] WIP: added client logic for manifest cli * added client logic for manifest cli * added manifest push Signed-off-by: WYGIN --------- Signed-off-by: WYGIN --- internal/builder/testmocks/mock_lifecycle.go | 3 +- internal/commands/commands.go | 15 ++-- internal/commands/manifest_add.go | 23 +++++ internal/commands/manifest_annotate.go | 10 ++- internal/commands/manifest_create.go | 69 ++++++++++---- internal/commands/manifest_exists.go | 33 +++++++ internal/commands/manifest_exists_test.go | 1 + internal/commands/manifest_push.go | 24 ++++- internal/commands/manifest_remove.go | 3 + internal/commands/manifest_rm.go | 3 + .../mock_inspect_image_writer_factory.go | 3 +- .../commands/testmocks/mock_pack_client.go | 83 ++++++++++------- pkg/client/add_manifest.go | 89 ++++++++++++++++++- pkg/client/annotate_manifest.go | 88 +++++++++++++++++- pkg/client/client.go | 64 +++++++++++++ pkg/client/create_manifest.go | 72 ++++++++++++++- pkg/client/delete_manifest.go | 31 ++++++- pkg/client/exists_manifest.go | 27 ++++++ pkg/client/exists_manifest_test.go | 1 + pkg/client/inspect_manifest.go | 60 ++++++++++++- pkg/client/push_manifest.go | 33 ++++++- pkg/client/remove_manifest.go | 25 +++++- pkg/testmocks/mock_blob_downloader.go | 3 +- pkg/testmocks/mock_build_module.go | 3 +- pkg/testmocks/mock_buildpack_downloader.go | 3 +- pkg/testmocks/mock_image_fetcher.go | 3 +- 26 files changed, 685 insertions(+), 87 deletions(-) create mode 100644 internal/commands/manifest_exists.go create mode 100644 internal/commands/manifest_exists_test.go create mode 100644 pkg/client/exists_manifest.go create mode 100644 pkg/client/exists_manifest_test.go diff --git a/internal/builder/testmocks/mock_lifecycle.go b/internal/builder/testmocks/mock_lifecycle.go index 88aacd786..fabeab840 100644 --- a/internal/builder/testmocks/mock_lifecycle.go +++ b/internal/builder/testmocks/mock_lifecycle.go @@ -8,9 +8,8 @@ import ( io "io" reflect "reflect" - gomock "github.com/golang/mock/gomock" - builder "github.com/buildpacks/pack/internal/builder" + gomock "github.com/golang/mock/gomock" ) // MockLifecycle is a mock of Lifecycle interface. diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 286c2a43c..ebb6c06e9 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -22,13 +22,14 @@ type PackClient interface { InspectImage(string, bool) (*client.ImageInfo, error) Rebase(context.Context, client.RebaseOptions) error CreateBuilder(context.Context, client.CreateBuilderOptions) error - CreateManifest(context.Context, string, []string) (imageID string, err error) - AnnotateManifest() error - AddManifest() error - DeleteManifest() error - RemoveManifest() error - PushManifest() error - InspectManifest() error + CreateManifest(ctx context.Context, name string, images []string, opts client.CreateManifestOptions) (imageID string, err error) + AnnotateManifest(ctx context.Context, name string, image string, opts client.ManifestAnnotateOptions) error + ExistsManifest(ctx context.Context, image string) error + AddManifest(ctx context.Context, index string, images string, opts client.ManifestAddOptions) (imageID string, err error) + DeleteManifest(ctx context.Context, name []string) error + RemoveManifest(ctx context.Context, name string, images []string) error + PushManifest(ctx context.Context, index string, opts client.PushManifestOptions) (imageID string, err error) + InspectManifest(ctx context.Context, name string, opts client.InspectManifestOptions) error NewBuildpack(context.Context, client.NewBuildpackOptions) error PackageBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error PackageExtension(ctx context.Context, opts client.PackageBuildpackOptions) error diff --git a/internal/commands/manifest_add.go b/internal/commands/manifest_add.go index 04432fb37..af4e9b9be 100644 --- a/internal/commands/manifest_add.go +++ b/internal/commands/manifest_add.go @@ -3,6 +3,7 @@ package commands import ( "github.com/spf13/cobra" + // "github.com/buildpacks/pack/pkg/client" "github.com/buildpacks/pack/pkg/logging" ) @@ -26,6 +27,24 @@ func ManifestAdd(logger logging.Logger, pack PackClient) *cobra.Command { When a manifest list exits locally, user can add a new image to the manifest list using this command`, RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + // imageIndex := args[0] + // manifests := args[1:] + // if err := validateManifestAddFlags(flags); err != nil { + // return err + // } + + // imageID, err := pack.AddManifest(cmd.Context(), imageIndex, manifests, client.ManifestAddOptions{ + // OS: flags.os, + // Arch: flags.arch, + // Variant: flags.variant, + // All: flags.all, + // }) + + // if err != nil { + // return err + // } + + // logger.Infof(imageID) return nil }), } @@ -38,3 +57,7 @@ func ManifestAdd(logger logging.Logger, pack PackClient) *cobra.Command { AddHelpFlag(cmd, "add") return cmd } + +func validateManifestAddFlags(flags ManifestAddFlags) error { + return nil +} diff --git a/internal/commands/manifest_annotate.go b/internal/commands/manifest_annotate.go index 64bcd83e7..dd941c848 100644 --- a/internal/commands/manifest_annotate.go +++ b/internal/commands/manifest_annotate.go @@ -8,7 +8,8 @@ import ( // ManifestAnnotateFlags define flags provided to the ManifestAnnotate type ManifestAnnotateFlags struct { - os, arch, variant string + os, arch, variant, osVersion string + features, osFeatures, annotations []string } // ManifestAnnotate modifies a manifest list (Image index) and update the platform information for an image included in the manifest list. @@ -16,7 +17,7 @@ func ManifestAnnotate(logger logging.Logger, pack PackClient) *cobra.Command { var flags ManifestAnnotateFlags cmd := &cobra.Command{ - Use: "pack manifest annotate [OPTIONS] [flags]", + Use: "pack manifest annotate [OPTIONS] [...] [flags]", Args: cobra.MatchAll(cobra.ExactArgs(2), cobra.OnlyValidArgs), Short: "manifest annotate modifies a manifest list (Image index) and update the platform information for an image included in the manifest list.", Example: `pack manifest annotate cnbs/sample-package:hello-universe-multiarch \ @@ -32,6 +33,11 @@ func ManifestAnnotate(logger logging.Logger, pack PackClient) *cobra.Command { cmd.Flags().StringVar(&flags.os, "os", "", "Set the architecture") cmd.Flags().StringVar(&flags.arch, "arch", "", "Set the architecture") cmd.Flags().StringVar(&flags.variant, "variant", "", "Set the architecture") + cmd.Flags().StringVar(&flags.osVersion, "os-version", "", "override the os `version` of the specified image") + cmd.Flags().StringSliceVar(&flags.features, "features", nil, "override the `features` of the specified image") + cmd.Flags().StringSliceVar(&flags.osFeatures, "os-features", nil, "override the os `features` of the specified image") + cmd.Flags().StringSliceVar(&flags.annotations, "annotations", nil, "set an `annotation` for the specified image") + AddHelpFlag(cmd, "annotate") return cmd diff --git a/internal/commands/manifest_create.go b/internal/commands/manifest_create.go index 1af72d3b8..4883ffd13 100644 --- a/internal/commands/manifest_create.go +++ b/internal/commands/manifest_create.go @@ -1,15 +1,18 @@ package commands import ( + "fmt" + "github.com/spf13/cobra" + "github.com/buildpacks/pack/pkg/client" "github.com/buildpacks/pack/pkg/logging" ) // ManifestCreateFlags define flags provided to the ManifestCreate type ManifestCreateFlags struct { - format, registry string - insecure, publish bool + format, registry, os, arch string + insecure, publish, all, amend bool } // ManifestCreate creates an image-index/image-list for a multi-arch image @@ -23,33 +26,67 @@ func ManifestCreate(logger logging.Logger, pack PackClient) *cobra.Command { Example: `pack manifest create cnbs/sample-package:hello-multiarch-universe \ cnbs/sample-package:hello-universe \ cnbs/sample-package:hello-universe-windows`, - Long: `Create a manifest list or manifest index for the image to support muti architecture for the image, it create a new ManifestList or ManifestIndex with the given name/repoName and adds the list of Manifests to the newly created ManifestIndex or ManifestList + Long: `Create a manifest list or image index for the image to support muti architecture for the image, it create a new ManifestList or ImageIndex with the given name and adds the list of Manifests to the newly created ImageIndex or ManifestList If the already exists in the registry: pack will save a local copy of the remote manifest list, If the doestn't exist in a registry: pack will create a local representation of the manifest list that will only save on the remote registry if the user publish it`, RunE: logError(logger, func(cmd *cobra.Command, args []string) error { - // manifestList := args[0] - // manifests := args[1:] + imageIndex := args[0] + manifests := args[1:] + cmdFlags := cmd.Flags() + + if err := validateManifestCreateFlags(flags); err != nil { + return err + } - // if cmd.Flags().Changed("insecure") { - // flags.insecure = !flags.insecure - // } + if cmdFlags.Changed("insecure") { + flags.insecure = !flags.insecure + } - // if cmd.Flags().Changed("publish") { - // flags.publish = !flags.publish - // } + if cmdFlags.Changed("publish") { + flags.publish = !flags.publish + } - // id, err := pack.CreateManifest() + id, err := pack.CreateManifest(cmd.Context(), imageIndex, manifests, client.CreateManifestOptions{ + Format: flags.format, + Registry: flags.registry, + Insecure: flags.insecure, + Publish: flags.publish, + }) + + if err != nil { + return err + } + logger.Infof("Successfully created ImageIndex/ManifestList with imageID: '%s'", id) return nil }), } - cmd.Flags().StringVarP(&flags.format, "format", "f", "", "Format to save image index as ('OCI' or 'V2S2') (default 'v2s2')") - cmd.Flags().BoolVar(&flags.insecure, "insecure", false, "Allow publishing to insecure registry") - cmd.Flags().BoolVar(&flags.publish, "publish", false, "Publish to registry") - cmd.Flags().StringVarP(&flags.registry, "registry", "r", "", "Publish to registry") + cmdFlags := cmd.Flags() + + cmdFlags.StringVarP(&flags.format, "format", "f", "v2s2", "Format to save image index as ('OCI' or 'V2S2') (default 'v2s2')") + cmdFlags.StringVarP(&flags.registry, "registry", "r", "", "Publish to registry") + cmdFlags.StringVar(&flags.os, "os", "", "If any of the specified images is a list/index, choose the one for `os`") + if err := cmdFlags.MarkHidden("os"); err != nil { + panic(fmt.Sprintf("error marking --os as hidden: %v", err)) + } + cmdFlags.StringVar(&flags.arch, "arch", "", "If any of the specified images is a list/index, choose the one for `arch`") + if err := cmdFlags.MarkHidden("arch"); err != nil { + panic(fmt.Sprintf("error marking --arch as hidden: %v", err)) + } + cmdFlags.BoolVar(&flags.insecure, "insecure", false, "Allow publishing to insecure registry") + if err := cmdFlags.MarkHidden("insecure"); err != nil { + panic(fmt.Sprintf("error marking insecure as hidden: %v", err)) + } + cmdFlags.BoolVar(&flags.publish, "publish", false, "Publish to registry") + cmdFlags.BoolVar(&flags.all, "all", false, "Add all of the list's images if the images to add are lists/index") + cmdFlags.BoolVar(&flags.amend, "amend", false, "Modify an existing list/index if one with the desired name already exists") AddHelpFlag(cmd, "create") return cmd } + +func validateManifestCreateFlags(flags ManifestCreateFlags) error { + return nil +} \ No newline at end of file diff --git a/internal/commands/manifest_exists.go b/internal/commands/manifest_exists.go new file mode 100644 index 000000000..654459931 --- /dev/null +++ b/internal/commands/manifest_exists.go @@ -0,0 +1,33 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestDeleteFlags define flags provided to the ManifestDelete +// type ManifestDeleteFlags struct { +// } + +// ManifestExists checks if a manifest list exists in local storage +func ManifestExists(logger logging.Logger, pack PackClient) *cobra.Command { + // var flags ManifestDeleteFlags + + cmd := &cobra.Command{ + Use: "pack manifest exists [manifest-list]", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Short: "Delete one or more manifest lists from local storage", + Example: `pack manifest exists cnbs/sample-package:hello-multiarch-universe`, + Long: `Checks if a manifest list exists in local storage`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + if err := pack.ExistsManifest(cmd.Context(), args[0]); err != nil { + return err + } + return nil + }), + } + + AddHelpFlag(cmd, "remove") + return cmd +} diff --git a/internal/commands/manifest_exists_test.go b/internal/commands/manifest_exists_test.go new file mode 100644 index 000000000..fe7b8c53f --- /dev/null +++ b/internal/commands/manifest_exists_test.go @@ -0,0 +1 @@ +package commands_test \ No newline at end of file diff --git a/internal/commands/manifest_push.go b/internal/commands/manifest_push.go index ffe13e553..8eb4c9188 100644 --- a/internal/commands/manifest_push.go +++ b/internal/commands/manifest_push.go @@ -3,13 +3,14 @@ package commands import ( "github.com/spf13/cobra" + "github.com/buildpacks/pack/pkg/client" "github.com/buildpacks/pack/pkg/logging" ) // ManifestPushFlags define flags provided to the ManifestPush type ManifestPushFlags struct { format string - insecure, purge bool + insecure, purge, all, quite bool } // ManifestPush pushes a manifest list (Image index) to a registry. @@ -25,6 +26,21 @@ func ManifestPush(logger logging.Logger, pack PackClient) *cobra.Command { Once a manifest list is ready to be published into the registry, the push command can be used`, RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + if err := parseFalgs(flags); err != nil { + return err + } + + imageID, err := pack.PushManifest(cmd.Context(), args[0], client.PushManifestOptions{ + Format: flags.format, + Insecure: flags.insecure, + Purge: flags.purge, + }) + + if err != nil { + return err + } + + logger.Infof(imageID) return nil }), } @@ -32,7 +48,13 @@ func ManifestPush(logger logging.Logger, pack PackClient) *cobra.Command { cmd.Flags().StringVarP(&flags.format, "format", "f", "", "Format to save image index as ('OCI' or 'V2S2') (default 'v2s2')") cmd.Flags().BoolVar(&flags.insecure, "insecure", false, "Allow publishing to insecure registry") cmd.Flags().BoolVar(&flags.purge, "purge", false, "Delete the manifest list or image index from local storage if pushing succeeds") + cmd.Flags().BoolVar(&flags.all, "all", false, "Also push the images in the list") + cmd.Flags().BoolVarP(&flags.quite, "quite", "q", false, "Also push the images in the list") AddHelpFlag(cmd, "push") return cmd } + +func parseFalgs(flags ManifestPushFlags) error { + return nil +} \ No newline at end of file diff --git a/internal/commands/manifest_remove.go b/internal/commands/manifest_remove.go index 888ae1c98..aa86223cd 100644 --- a/internal/commands/manifest_remove.go +++ b/internal/commands/manifest_remove.go @@ -23,6 +23,9 @@ func ManifestDelete(logger logging.Logger, pack PackClient) *cobra.Command { When a manifest list exits locally, users can remove existing images from a manifest list`, RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + if err := pack.DeleteManifest(cmd.Context(), args); err != nil { + return err + } return nil }), } diff --git a/internal/commands/manifest_rm.go b/internal/commands/manifest_rm.go index 2562db2dd..235a814da 100644 --- a/internal/commands/manifest_rm.go +++ b/internal/commands/manifest_rm.go @@ -24,6 +24,9 @@ func ManifestRemove(logger logging.Logger, pack PackClient) *cobra.Command { Sometimes users can just experiment with the feature locally and they want to discard all the local information created by pack. 'rm' command just delete the local manifest list`, RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + if err := pack.RemoveManifest(cmd.Context(), args[0], args[1:]); err != nil { + return err + } return nil }), } diff --git a/internal/commands/testmocks/mock_inspect_image_writer_factory.go b/internal/commands/testmocks/mock_inspect_image_writer_factory.go index 9f5a65c54..afeaa5c20 100644 --- a/internal/commands/testmocks/mock_inspect_image_writer_factory.go +++ b/internal/commands/testmocks/mock_inspect_image_writer_factory.go @@ -7,9 +7,8 @@ package testmocks import ( reflect "reflect" - gomock "github.com/golang/mock/gomock" - writer "github.com/buildpacks/pack/internal/inspectimage/writer" + gomock "github.com/golang/mock/gomock" ) // MockInspectImageWriterFactory is a mock of InspectImageWriterFactory interface. diff --git a/internal/commands/testmocks/mock_pack_client.go b/internal/commands/testmocks/mock_pack_client.go index 56d872895..9060b7fb5 100644 --- a/internal/commands/testmocks/mock_pack_client.go +++ b/internal/commands/testmocks/mock_pack_client.go @@ -8,9 +8,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - client "github.com/buildpacks/pack/pkg/client" + gomock "github.com/golang/mock/gomock" ) // MockPackClient is a mock of PackClient interface. @@ -37,31 +36,32 @@ func (m *MockPackClient) EXPECT() *MockPackClientMockRecorder { } // AddManifest mocks base method. -func (m *MockPackClient) AddManifest() error { +func (m *MockPackClient) AddManifest(arg0 context.Context, arg1 string, arg2 []string, arg3 client.ManifestAddOptions) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddManifest") - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "AddManifest", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 } // AddManifest indicates an expected call of AddManifest. -func (mr *MockPackClientMockRecorder) AddManifest() *gomock.Call { +func (mr *MockPackClientMockRecorder) AddManifest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddManifest", reflect.TypeOf((*MockPackClient)(nil).AddManifest)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddManifest", reflect.TypeOf((*MockPackClient)(nil).AddManifest), arg0, arg1, arg2, arg3) } // AnnotateManifest mocks base method. -func (m *MockPackClient) AnnotateManifest() error { +func (m *MockPackClient) AnnotateManifest(arg0 context.Context, arg1, arg2 string, arg3 client.ManifestAnnotateOptions) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AnnotateManifest") + ret := m.ctrl.Call(m, "AnnotateManifest", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // AnnotateManifest indicates an expected call of AnnotateManifest. -func (mr *MockPackClientMockRecorder) AnnotateManifest() *gomock.Call { +func (mr *MockPackClientMockRecorder) AnnotateManifest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnnotateManifest", reflect.TypeOf((*MockPackClient)(nil).AnnotateManifest)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnnotateManifest", reflect.TypeOf((*MockPackClient)(nil).AnnotateManifest), arg0, arg1, arg2, arg3) } // Build mocks base method. @@ -93,32 +93,32 @@ func (mr *MockPackClientMockRecorder) CreateBuilder(arg0, arg1 interface{}) *gom } // CreateManifest mocks base method. -func (m *MockPackClient) CreateManifest(arg0 context.Context, arg1 string, arg2 []string) (string, error) { +func (m *MockPackClient) CreateManifest(arg0 context.Context, arg1 string, arg2 []string, arg3 client.CreateManifestOptions) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateManifest", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CreateManifest", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateManifest indicates an expected call of CreateManifest. -func (mr *MockPackClientMockRecorder) CreateManifest(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockPackClientMockRecorder) CreateManifest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateManifest", reflect.TypeOf((*MockPackClient)(nil).CreateManifest), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateManifest", reflect.TypeOf((*MockPackClient)(nil).CreateManifest), arg0, arg1, arg2, arg3) } // DeleteManifest mocks base method. -func (m *MockPackClient) DeleteManifest() error { +func (m *MockPackClient) DeleteManifest(arg0 context.Context, arg1 []string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteManifest") + ret := m.ctrl.Call(m, "DeleteManifest", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DeleteManifest indicates an expected call of DeleteManifest. -func (mr *MockPackClientMockRecorder) DeleteManifest() *gomock.Call { +func (mr *MockPackClientMockRecorder) DeleteManifest(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteManifest", reflect.TypeOf((*MockPackClient)(nil).DeleteManifest)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteManifest", reflect.TypeOf((*MockPackClient)(nil).DeleteManifest), arg0, arg1) } // DownloadSBOM mocks base method. @@ -135,6 +135,20 @@ func (mr *MockPackClientMockRecorder) DownloadSBOM(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadSBOM", reflect.TypeOf((*MockPackClient)(nil).DownloadSBOM), arg0, arg1) } +// ExistsManifest mocks base method. +func (m *MockPackClient) ExistsManifest(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExistsManifest", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ExistsManifest indicates an expected call of ExistsManifest. +func (mr *MockPackClientMockRecorder) ExistsManifest(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsManifest", reflect.TypeOf((*MockPackClient)(nil).ExistsManifest), arg0, arg1) +} + // InspectBuilder mocks base method. func (m *MockPackClient) InspectBuilder(arg0 string, arg1 bool, arg2 ...client.BuilderInspectionModifier) (*client.BuilderInfo, error) { m.ctrl.T.Helper() @@ -201,17 +215,17 @@ func (mr *MockPackClientMockRecorder) InspectImage(arg0, arg1 interface{}) *gomo } // InspectManifest mocks base method. -func (m *MockPackClient) InspectManifest() error { +func (m *MockPackClient) InspectManifest(arg0 context.Context, arg1 string, arg2 client.InspectManifestOptions) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InspectManifest") + ret := m.ctrl.Call(m, "InspectManifest", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // InspectManifest indicates an expected call of InspectManifest. -func (mr *MockPackClientMockRecorder) InspectManifest() *gomock.Call { +func (mr *MockPackClientMockRecorder) InspectManifest(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectManifest", reflect.TypeOf((*MockPackClient)(nil).InspectManifest)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectManifest", reflect.TypeOf((*MockPackClient)(nil).InspectManifest), arg0, arg1, arg2) } // NewBuildpack mocks base method. @@ -271,17 +285,18 @@ func (mr *MockPackClientMockRecorder) PullBuildpack(arg0, arg1 interface{}) *gom } // PushManifest mocks base method. -func (m *MockPackClient) PushManifest() error { +func (m *MockPackClient) PushManifest(arg0 context.Context, arg1 string, arg2 client.PushManifestOptions) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PushManifest") - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "PushManifest", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 } // PushManifest indicates an expected call of PushManifest. -func (mr *MockPackClientMockRecorder) PushManifest() *gomock.Call { +func (mr *MockPackClientMockRecorder) PushManifest(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushManifest", reflect.TypeOf((*MockPackClient)(nil).PushManifest)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushManifest", reflect.TypeOf((*MockPackClient)(nil).PushManifest), arg0, arg1, arg2) } // Rebase mocks base method. @@ -313,17 +328,17 @@ func (mr *MockPackClientMockRecorder) RegisterBuildpack(arg0, arg1 interface{}) } // RemoveManifest mocks base method. -func (m *MockPackClient) RemoveManifest() error { +func (m *MockPackClient) RemoveManifest(arg0 context.Context, arg1 string, arg2 []string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveManifest") + ret := m.ctrl.Call(m, "RemoveManifest", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // RemoveManifest indicates an expected call of RemoveManifest. -func (mr *MockPackClientMockRecorder) RemoveManifest() *gomock.Call { +func (mr *MockPackClientMockRecorder) RemoveManifest(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveManifest", reflect.TypeOf((*MockPackClient)(nil).RemoveManifest)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveManifest", reflect.TypeOf((*MockPackClient)(nil).RemoveManifest), arg0, arg1, arg2) } // YankBuildpack mocks base method. diff --git a/pkg/client/add_manifest.go b/pkg/client/add_manifest.go index 387651e8c..1ec7d5e80 100644 --- a/pkg/client/add_manifest.go +++ b/pkg/client/add_manifest.go @@ -1,6 +1,91 @@ package client +import ( + "context" + "fmt" + "strings" +) + + +type ManifestAddOptions struct { + ManifestAnnotateOptions + All bool +} // AddManifest implements commands.PackClient. -func (*Client) AddManifest() error { - panic("unimplemented") +func (c *Client) AddManifest(ctx context.Context, index string, image string, opts ManifestAddOptions) (indexID string, err error) { + imgIndex, err := c.runtime.LookupImageIndex(index) + if err != nil { + return + } + _, list, err := c.runtime.LoadFromImage(imgIndex.ID()) + if err != nil { + return + } + + ref, err := c.runtime.ParseReference(image) + if err != nil { + return + } + + digest, err := list.Add(ctx, ref, opts.All) + if err != nil { + if ref, _, err = c.runtime.FindImage(image); err != nil { + return indexID, fmt.Errorf("Error while trying to find image on local storage: %v", err) + } + digest, err = list.Add(ctx, ref, opts.All) + if err != nil { + return indexID, fmt.Errorf("Error while trying to add on manifest list: %v", err) + } + } + + if opts.OS != "" { + if _, err := list.SetOS(digest, opts.OS); err != nil { + return indexID, err + } + } + + if opts.OSArch != "" { + if _, err := list.SetArchitecture(digest, opts.OSArch); err != nil { + return indexID, err + } + } + + if opts.OSVariant != "" { + if _, err := list.SetVariant(digest, opts.OSVariant); err != nil { + return indexID, err + } + } + + if opts.OSVersion != "" { + if _, err := list.SetOSVersion(digest, opts.OSVersion); err != nil { + return indexID, err + } + } + + if len(opts.Features) != 0 { + if _, err := list.SetFeatures(digest, opts.Features); err != nil { + return indexID, err + } + } + + if len(opts.Annotations) != 0 { + annotations := make(map[string]string) + for _, annotationSpec := range opts.Annotations { + spec := strings.SplitN(annotationSpec, "=", 2) + if len(spec) != 2 { + return indexID, fmt.Errorf("no value given for annotation %q", spec[0]) + } + annotations[spec[0]] = spec[1] + } + if err := list.SetAnnotations(&digest, annotations); err != nil { + return err + } + } + + indexID, err = list.Save(imgIndex.ID(), nil, "") + if err == nil { + fmt.Printf("%s: %s\n", indexID, digest.String()) + } + + return indexID, err } diff --git a/pkg/client/annotate_manifest.go b/pkg/client/annotate_manifest.go index c489b1a0a..c79d2c481 100644 --- a/pkg/client/annotate_manifest.go +++ b/pkg/client/annotate_manifest.go @@ -1,6 +1,90 @@ package client +import ( + "context" + "fmt" + "strings" +) + +type ManifestAnnotateOptions struct { + OS, OSVersion, OSArch, OSVariant string + OSFeatures, Annotations, Features map[string]string +} + + // AnnotateManifest implements commands.PackClient. -func (*Client) AnnotateManifest() error { - panic("unimplemented") +func (c *Client) AnnotateManifest(ctx context.Context, name string, image string, opts ManifestAnnotateOptions) error { + manifestList, err := c.runtime.LookupImageIndex(name) + if err != nil { + return err + } + + _, list, err := c.runtime.LoadFromImage(manifestList.ID()) + if err != nil { + return err + } + + digest, err := c.runtime.ParseDigest(image) + if err != nil { + ref, _, err := c.runtime.FindImage(image) + if err != nil { + return err + } + digest , err = c.runtime.ParseDigest(ref.Name()) + if err != nil { + return err + } + } + + + if opts.OS != "" { + if err := list.SetOS(digest, opts.OS); err != nil { + return err + } + } + if opts.OSVersion != "" { + if err := list.SetOSVersion(digest, opts.OSVersion); err != nil { + return err + } + } + if len(opts.OSFeatures) != 0 { + if err := list.SetOSFeatures(digest, opts.OSFeatures); err != nil { + return err + } + } + if opts.OSArch != "" { + if err := list.SetArchitecture(digest, opts.OSArch); err != nil { + return err + } + } + if opts.OSVariant != "" { + if err := list.SetVariant(digest, opts.OSVariant); err != nil { + return err + } + } + if len(opts.Features) != 0 { + if err := list.SetFeatures(digest, opts.Features); err != nil { + return err + } + } + if len(opts.Annotations) != 0 { + annotations := make(map[string]string) + for _, annotationSpec := range opts.Annotations { + spec := strings.SplitN(annotationSpec, "=", 2) + if len(spec) != 2 { + return fmt.Errorf("no value given for annotation %q", spec[0]) + } + annotations[spec[0]] = spec[1] + } + if err := list.SetAnnotations(&digest, annotations); err != nil { + return err + } + } + + updatedListID, err := list.Save(manifestList.ID(), nil, "") + if err == nil { + fmt.Printf("%s: %s\n", updatedListID, digest.String()) + } + + return nil } diff --git a/pkg/client/client.go b/pkg/client/client.go index b3862cc4c..9e259bd37 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -20,10 +20,12 @@ import ( "path/filepath" "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/layout" "github.com/buildpacks/imgutil/local" "github.com/buildpacks/imgutil/remote" dockerClient "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "github.com/buildpacks/pack" @@ -34,6 +36,7 @@ import ( "github.com/buildpacks/pack/pkg/buildpack" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" + runtime "github.com/buildpacks/pack/pkg/runtime" ) //go:generate mockgen -package testmocks -destination ../testmocks/mock_docker_client.go github.com/docker/docker/client CommonAPIClient @@ -72,6 +75,37 @@ type ImageFactory interface { NewImage(repoName string, local bool, imageOS string) (imgutil.Image, error) } +// IndexFactory is an interface representing the ability to create a ImageIndex/ManifestList. +type IndexFactory interface { + NewIndex(reponame string, opts imgutil.IndexOptions) (imgutil.Index, error) +} + +type Runtime interface { + // LookupManifestList looks up a manifest list with the specified name in the + // containers storage. + LookupImageIndex(name string) (index runtime.ImageIndex, err error) + // LoadFromImage reads the manifest list or image index, and additional + // information about where the various instances that it contains live, from an + // image record with the specified ID in local storage. + LoadFromImage(name string) (imageID string, index imgutil.Index, err error) + // ExpandNames takes unqualified names, parses them as image names, and returns + // the fully expanded result, including a tag. Names which don't include a registry + // name will be marked for the most-preferred registry + ExpandIndexNames(names []string) (images []string, err error) + // ImageType returns the MediaType of the given image's format + ImageType(format string) (manifestType imgutil.MediaTypes) + // FindImage locates the locally-stored image which corresponds to a given name. + FindImage(name string) (name.Reference, imgutil.Image, error) + // parse name reference + ParseReference(image string) (name.Reference, error) + // parses the digest reference + ParseDigest(image string) (name.Digest, error) + // RemoveManifests will delete manifest/manifestList from the local stroage + RemoveManifests(ctx context.Context, names []string) (reports runtime.RemoveImageReport, errors []error) + // Fetch ManifestList from Registry with the given name + FetchIndex(name string) (imgutil.Index, error) +} + //go:generate mockgen -package testmocks -destination ../testmocks/mock_buildpack_downloader.go github.com/buildpacks/pack/pkg/client BuildpackDownloader // BuildpackDownloader is an interface for downloading and extracting buildpacks from various sources @@ -89,6 +123,8 @@ type Client struct { keychain authn.Keychain imageFactory ImageFactory + indexFactory IndexFactory + runtime Runtime imageFetcher ImageFetcher downloader BlobDownloader lifecycleExecutor LifecycleExecutor @@ -218,6 +254,10 @@ func NewClient(opts ...Option) (*Client, error) { client.imageFetcher = image.NewFetcher(client.logger, client.docker, image.WithRegistryMirrors(client.registryMirrors), image.WithKeychain(client.keychain)) } + if client.runtime == nil { + client.runtime = runtime.NewRuntime() + } + if client.imageFactory == nil { client.imageFactory = &imageFactory{ dockerClient: client.docker, @@ -225,6 +265,12 @@ func NewClient(opts ...Option) (*Client, error) { } } + if client.indexFactory == nil { + client.indexFactory = &indexFactory{ + keychain: client.keychain, + } + } + if client.buildpackDownloader == nil { client.buildpackDownloader = buildpack.NewDownloader( client.logger, @@ -273,3 +319,21 @@ func (f *imageFactory) NewImage(repoName string, daemon bool, imageOS string) (i return remote.NewImage(repoName, f.keychain, remote.WithDefaultPlatform(platform)) } + +type indexFactory struct { + keychain authn.Keychain +} + +func(f *indexFactory) NewIndex(name string, opts imgutil.IndexOptions) (index imgutil.Index, err error) { + + index, err = remote.NewIndex(name, f.keychain, opts) + if err != nil { + if opts.MediaType == imgutil.MediaTypes.OCI { + return layout.NewIndex(name, f.keychain, opts) + } else { + return local.NewIndex(name, f.keychain, opts) + } + } + + return index, err +} diff --git a/pkg/client/create_manifest.go b/pkg/client/create_manifest.go index a6e2a97ee..00a69a19c 100644 --- a/pkg/client/create_manifest.go +++ b/pkg/client/create_manifest.go @@ -1,8 +1,74 @@ package client -import "context" +import ( + "context" + "errors" + "fmt" + + "github.com/buildpacks/imgutil" + packErrors "github.com/buildpacks/pack/pkg/errors" +) + +type CreateManifestOptions struct { + Format, Registry string + Insecure, Publish, amend, all bool +} // CreateManifest implements commands.PackClient. -func (*Client) CreateManifest(context.Context, string, []string) (imageID string, err error) { - panic("unimplemented") +func (c *Client) CreateManifest(ctx context.Context, name string, images []string, opts CreateManifestOptions) (imageID string, err error) { + index, err := c.indexFactory.NewIndex(name, parseOptsToIndexOptions(opts)) + if err != nil { + return + } + index.Create() + names, err := c.runtime.ExpandIndexNames([]string{name}) + if err != nil { + return + } + var imageIndexID string + if imageIndexID, err = index.Save("", names, c.runtime.ImageType(opts.Format)); err != nil { + if errors.Is(err, packErrors.ErrDuplicateName) && opts.amend { + for _, idxName := range names { + imageIndex, err := c.runtime.LookupImageIndex(idxName) + if err != nil { + fmt.Printf("no list named %q found: %v", name, err) + } + if _, index, err = c.runtime.LoadFromImage(imageIndex.ID()); err != nil { + fmt.Printf("no list found in %q", idxName) + } + imageIndexID = imageIndex.ID() + break + } + + if index == nil { + return imageID, fmt.Errorf("--amend specified but no matching manifest list found with name %q", name) + } + } else { + return + } + } + + for _, img := range images { + ref, err := c.runtime.ParseReference(img) + if err != nil { + return imageID, err + } + if localRef, _, err := c.runtime.FindImage(img); err == nil { + ref = localRef + } + if _, err = index.Add(ctx, ref, opts.all); err != nil { + return imageID, err + } + } + + imageID, err = index.Save(imageIndexID, names, c.runtime.ImageType(opts.Format)) + if err == nil { + fmt.Printf("%s\n", imageID) + } + + return imageID, err +} + +func parseOptsToIndexOptions(opts CreateManifestOptions) imgutil.IndexOptions { + return opts } diff --git a/pkg/client/delete_manifest.go b/pkg/client/delete_manifest.go index 0ff257d82..c503bd961 100644 --- a/pkg/client/delete_manifest.go +++ b/pkg/client/delete_manifest.go @@ -1,6 +1,33 @@ package client +import ( + "context" + "fmt" + "strings" +) + // DeleteManifest implements commands.PackClient. -func (*Client) DeleteManifest() error { - panic("unimplemented") +func (c *Client) DeleteManifest(ctx context.Context, names []string) error { + rmiReports, rmiErrors := c.runtime.RemoveManifests(ctx, names) + for _, r := range rmiReports { + for _, u := range r.Untagged { + fmt.Printf("untagged: %s\n", u) + } + } + + for _, r := range rmiReports { + if r.Removed { + fmt.Printf("%s\n", r.ID) + } + } + var errors []string + for _, err := range rmiErrors { + errors = append(errors, err.Error()) + } + + if len(errors) == 0 { + return nil + } + + return fmt.Errorf(strings.Join(errors, "\n")) } diff --git a/pkg/client/exists_manifest.go b/pkg/client/exists_manifest.go new file mode 100644 index 000000000..d125ffeef --- /dev/null +++ b/pkg/client/exists_manifest.go @@ -0,0 +1,27 @@ +package client + +import ( + "context" + "os" + + "github.com/buildpacks/imgutil" + packErrors "github.com/buildpacks/pack/pkg/errors" + "github.com/pkg/errors" +) + +func(c *Client) ExistsManifest(ctx context.Context, image string) error { + index, err := c.indexFactory.NewIndex(image, imgutil.IndexOptions{}) + if err != nil { + return errors.Errorf("error while initializing index: %s", image) + } + + if _, err := c.runtime.LookupImageIndex(image); err != nil { + if errors.Is(err, packErrors.ErrImageUnknown) { + os.Exit(1) + } else { + return errors.Errorf("image '%s' is not found", image) + } + } + + return nil +} \ No newline at end of file diff --git a/pkg/client/exists_manifest_test.go b/pkg/client/exists_manifest_test.go new file mode 100644 index 000000000..e148123e6 --- /dev/null +++ b/pkg/client/exists_manifest_test.go @@ -0,0 +1 @@ +package client_test \ No newline at end of file diff --git a/pkg/client/inspect_manifest.go b/pkg/client/inspect_manifest.go index 5bb211a05..64b7ce41d 100644 --- a/pkg/client/inspect_manifest.go +++ b/pkg/client/inspect_manifest.go @@ -1,6 +1,62 @@ package client +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + packErrors "github.com/buildpacks/pack/pkg/errors" +) + +type InspectManifestOptions struct { + +} + // InspectManifest implements commands.PackClient. -func (*Client) InspectManifest() error { - panic("unimplemented") +func (c *Client) InspectManifest(ctx context.Context, name string, opts InspectManifestOptions) error { + printManifest := func(manifest []byte) error { + var b bytes.Buffer + err := json.Indent(&b, manifest, "", " ") + if err != nil { + return fmt.Errorf("rendering manifest for display: %w", err) + } + + fmt.Printf("%s\n", b.String()) + return nil + } + + // Before doing a remote lookup, attempt to resolve the manifest list + // locally. + manifestList, err := c.runtime.LookupImageIndex(name) + if err == nil { + schema2List, err := manifestList.Inspect() + if err != nil { + rawSchema2List, err := json.Marshal(schema2List) + if err != nil { + return err + } + + return printManifest(rawSchema2List) + } + if !errors.Is(err, packErrors.ErrImageUnknown) && !errors.Is(err, packErrors.ErrNotAManifestList) { + return err + } + + _, err = c.runtime.ParseReference(name) + if err != nil { + fmt.Printf("error parsing reference to image %q: %v", name, err) + } + + index, err := c.runtime.FetchIndex(name) + + if err != nil { + return err + } + + return printManifest(index) + } + + return fmt.Errorf("unable to locate manifest list locally or at registry") } diff --git a/pkg/client/push_manifest.go b/pkg/client/push_manifest.go index 16a0254de..f6ae71da0 100644 --- a/pkg/client/push_manifest.go +++ b/pkg/client/push_manifest.go @@ -1,6 +1,35 @@ package client +import ( + "context" + "github.com/buildpacks/imgutil" +) + +type PushManifestOptions struct { + Format string + Insecure, Purge bool +} // PushManifest implements commands.PackClient. -func (*Client) PushManifest() error { - panic("unimplemented") +func (c *Client) PushManifest(ctx context.Context, index string, opts PushManifestOptions) (imageID string, err error) { + manifestList, err := c.runtime.LookupImageIndex(index) + if err != nil { + return + } + + _, list, err := c.runtime.LoadFromImage(manifestList.ID()) + if err != nil { + return + } + + _, _, err = list.Push(ctx, parseFalgsForImgUtil(opts)) + + if err == nil && opts.Purge { + c.runtime.RemoveManifests(ctx, []string{manifestList.ID()}) + } + + return imageID, err +} + +func parseFalgsForImgUtil(opts PushManifestOptions) (idxOptions imgutil.IndexOptions) { + return idxOptions } diff --git a/pkg/client/remove_manifest.go b/pkg/client/remove_manifest.go index 83dcca078..9952f5267 100644 --- a/pkg/client/remove_manifest.go +++ b/pkg/client/remove_manifest.go @@ -1,6 +1,27 @@ package client +import ( + "context" + "fmt" +) + // RemoveManifest implements commands.PackClient. -func (*Client) RemoveManifest() error { - panic("unimplemented") +func (c *Client) RemoveManifest(ctx context.Context, name string, images []string) error { + imgIndex, err := c.runtime.LookupImageIndex(name) + if err != nil { + return err + } + + for _, image := range images { + d, err := c.runtime.ParseDigest(image) + if err != nil { + fmt.Errorf(`Invalid instance "%s": %v`, image, err) + } + if err := imgIndex.Remove(d); err != nil { + return err + } + fmt.Printf("%s: %s\n", imgIndex.ID(), d.String()) + } + + return nil } diff --git a/pkg/testmocks/mock_blob_downloader.go b/pkg/testmocks/mock_blob_downloader.go index 80a04d987..9a044edd2 100644 --- a/pkg/testmocks/mock_blob_downloader.go +++ b/pkg/testmocks/mock_blob_downloader.go @@ -8,9 +8,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - blob "github.com/buildpacks/pack/pkg/blob" + gomock "github.com/golang/mock/gomock" ) // MockBlobDownloader is a mock of BlobDownloader interface. diff --git a/pkg/testmocks/mock_build_module.go b/pkg/testmocks/mock_build_module.go index 5548ad6f6..35045ea9f 100644 --- a/pkg/testmocks/mock_build_module.go +++ b/pkg/testmocks/mock_build_module.go @@ -8,9 +8,8 @@ import ( io "io" reflect "reflect" - gomock "github.com/golang/mock/gomock" - buildpack "github.com/buildpacks/pack/pkg/buildpack" + gomock "github.com/golang/mock/gomock" ) // MockBuildModule is a mock of BuildModule interface. diff --git a/pkg/testmocks/mock_buildpack_downloader.go b/pkg/testmocks/mock_buildpack_downloader.go index d67ff548d..9758f24b2 100644 --- a/pkg/testmocks/mock_buildpack_downloader.go +++ b/pkg/testmocks/mock_buildpack_downloader.go @@ -8,9 +8,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - buildpack "github.com/buildpacks/pack/pkg/buildpack" + gomock "github.com/golang/mock/gomock" ) // MockBuildpackDownloader is a mock of BuildpackDownloader interface. diff --git a/pkg/testmocks/mock_image_fetcher.go b/pkg/testmocks/mock_image_fetcher.go index 281f28d04..df0e08ff7 100644 --- a/pkg/testmocks/mock_image_fetcher.go +++ b/pkg/testmocks/mock_image_fetcher.go @@ -9,9 +9,8 @@ import ( reflect "reflect" imgutil "github.com/buildpacks/imgutil" - gomock "github.com/golang/mock/gomock" - image "github.com/buildpacks/pack/pkg/image" + gomock "github.com/golang/mock/gomock" ) // MockImageFetcher is a mock of ImageFetcher interface.