diff --git a/ci/tasks/test-chart-product.yaml b/ci/tasks/test-chart-product.yaml index cb70b76..c9a7cfd 100644 --- a/ci/tasks/test-chart-product.yaml +++ b/ci/tasks/test-chart-product.yaml @@ -27,5 +27,10 @@ run: mkpcli attach chart --product "${PRODUCT_SLUG}" --product-version "${VERSION}" --create-version \ --chart chart/*.tgz --instructions "helm install it" + # Also attach a meta file + echo "{\"data\": \"totally a real config file\"}" > config.json + mkpcli attach metafile --product "${PRODUCT_SLUG}" --product-version "${VERSION}" \ + --metafile config.json --metafile-type config + # Get the list of charts mkpcli product list-assets --type chart --product "${PRODUCT_SLUG}" --product-version "${VERSION}" | grep $(basename -s .tgz chart/*.tgz) diff --git a/ci/tasks/test-container-image-product.yaml b/ci/tasks/test-container-image-product.yaml index 57878eb..629e71e 100644 --- a/ci/tasks/test-container-image-product.yaml +++ b/ci/tasks/test-container-image-product.yaml @@ -39,5 +39,9 @@ run: --instructions "docker run ${TEST_IMAGE_REPO}:${TEST_IMAGE_TAG}" fi + # Also attach a meta file + mkpcli attach metafile --product "${PRODUCT_SLUG}" --product-version "${VERSION}" \ + --metafile $(which ls) --metafile-type cli --metafile-version 1.0.0 + # Get the list of container images mkpcli product list-assets --type image --product "${PRODUCT_SLUG}" --product-version "${VERSION}" | grep "${TEST_IMAGE_REPO}:${TEST_IMAGE_TAG}" diff --git a/ci/tasks/test-vm-product.yaml b/ci/tasks/test-vm-product.yaml index 868a1b5..0c04246 100644 --- a/ci/tasks/test-vm-product.yaml +++ b/ci/tasks/test-vm-product.yaml @@ -28,5 +28,10 @@ run: mkpcli attach vm --product "${PRODUCT_SLUG}" --product-version "${VERSION}" --create-version \ --file "${VM_FILE}" + # Also attach a meta file + echo "some other virtual machine" > other-vm.iso + mkpcli attach metafile --product "${PRODUCT_SLUG}" --product-version "${VERSION}" \ + --metafile other-vm.iso --metafile-type other --metafile-version 1.0.0 + # Get the list of virtual machine files mkpcli product list-assets --type vm --product "${PRODUCT_SLUG}" --product-version "${VERSION}" | grep $(basename -s .iso $(basename -s .ova "$VM_FILE")) diff --git a/cmd/attach.go b/cmd/attach.go index 6fc6168..29ed77a 100644 --- a/cmd/attach.go +++ b/cmd/attach.go @@ -26,6 +26,9 @@ var ( AttachContainerImageTag string AttachContainerImageTagType string + AttachMetaFile string + AttachMetaFileVersion string + AttachVMFile string AttachInstructions string @@ -37,6 +40,7 @@ func init() { rootCmd.AddCommand(AttachCmd) AttachCmd.AddCommand(AttachChartCmd) AttachCmd.AddCommand(AttachContainerImageCmd) + AttachCmd.AddCommand(AttachMetaFileCmd) AttachCmd.AddCommand(AttachVMCmd) AttachChartCmd.Flags().StringVarP(&AttachProductSlug, "product", "p", "", "Product slug (required)") @@ -64,6 +68,14 @@ func init() { AttachContainerImageCmd.Flags().BoolVar(&AttachCreateVersion, "create-version", false, "Create the product version, if it doesn't already exist") AttachContainerImageCmd.Flags().StringVar(&AttachPCAFile, "pca-file", "", "Path to a PCA file to upload") + AttachMetaFileCmd.Flags().StringVarP(&AttachProductSlug, "product", "p", "", "Product slug (required)") + _ = AttachMetaFileCmd.MarkFlagRequired("product") + AttachMetaFileCmd.Flags().StringVarP(&AttachProductVersion, "product-version", "v", "", "Product version (default to latest version)") + AttachMetaFileCmd.Flags().StringVar(&AttachMetaFile, "metafile", "", "Meta file to upload (required)") + _ = AttachMetaFileCmd.MarkFlagRequired("metafile") + AttachMetaFileCmd.Flags().StringVar(&MetaFileType, "metafile-type", "", "Meta file version (required, one of "+strings.Join(metaFileTypesList(), ", ")+")") + AttachMetaFileCmd.Flags().StringVar(&AttachMetaFileVersion, "metafile-version", "", "Meta file type (default is the product version)") + AttachVMCmd.Flags().StringVarP(&AttachProductSlug, "product", "p", "", "Product slug (required)") _ = AttachVMCmd.MarkFlagRequired("product") AttachVMCmd.Flags().StringVarP(&AttachProductVersion, "product-version", "v", "", "Product version (default to latest version)") @@ -190,6 +202,37 @@ var AttachContainerImageCmd = &cobra.Command{ }, } +var AttachMetaFileCmd = &cobra.Command{ + Use: "metafile", + Short: "Attach a meta file", + Long: "Upload and attach a meta file to a product in the VMware Marketplace", + Example: fmt.Sprintf("%s attach metafile -p hyperspace-database-vm1 -v 1.2.3 --file deploy.sh", AppName), + Args: cobra.NoArgs, + PreRunE: RunSerially(ValidateMetaFileType, GetRefreshToken), + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + product, version, err := Marketplace.GetProductWithVersion(AttachProductSlug, AttachProductVersion) + if err != nil { + if errors.Is(err, &pkg.VersionDoesNotExistError{}) && AttachCreateVersion { + version = product.NewVersion(AttachProductVersion) + } else { + return err + } + } + + if AttachMetaFileVersion == "" { + AttachMetaFileVersion = version.Number + } + updatedProduct, err := Marketplace.AttachMetaFile(AttachMetaFile, metaFileTypeMapping[MetaFileType], AttachMetaFileVersion, product, version) + if err != nil { + return err + } + + return Output.RenderAssets(pkg.GetAssets(updatedProduct, version.Number)) + }, +} + var AttachVMCmd = &cobra.Command{ Use: "vm", Short: "Attach a virtual machine file (ISO or OVA)", diff --git a/cmd/download.go b/cmd/download.go index 0c42f93..aa947ea 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -16,7 +16,6 @@ var ( DownloadProductVersion string DownloadFilter string DownloadFilename string - DownloadType string DownloadAcceptEULA bool ) @@ -27,7 +26,7 @@ func init() { _ = DownloadCmd.MarkFlagRequired("product") DownloadCmd.Flags().StringVarP(&DownloadProductVersion, "product-version", "v", "", "Product version (default to latest version)") DownloadCmd.Flags().StringVar(&DownloadFilter, "filter", "", "Filter assets by display name") - DownloadCmd.Flags().StringVarP(&DownloadType, "type", "t", "", "Filter assets by type (one of "+strings.Join(assetTypesList(), ", ")+")") + DownloadCmd.Flags().StringVarP(&AssetType, "type", "t", "", "Filter assets by type (one of "+strings.Join(assetTypesList(), ", ")+")") DownloadCmd.Flags().StringVarP(&DownloadFilename, "filename", "f", "", "Output file name") DownloadCmd.Flags().BoolVar(&DownloadAcceptEULA, "accept-eula", false, "Accept the product EULA") } @@ -57,11 +56,11 @@ var DownloadCmd = &cobra.Command{ } assetType := "" - if DownloadType != "" { - assetType = typeMapping[DownloadType] + " " + if AssetType != "" { + assetType = assetTypeMapping[AssetType] + " " } var asset *pkg.Asset - assets := pkg.GetAssetsByType(typeMapping[DownloadType], product, version.Number) + assets := pkg.GetAssetsByType(assetTypeMapping[AssetType], product, version.Number) if len(assets) == 0 { return fmt.Errorf("product %s %s does not have any downloadable %sassets", product.Slug, version.Number, assetType) } diff --git a/cmd/download_test.go b/cmd/download_test.go index fa5e7c9..c3441c2 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -36,7 +36,7 @@ var _ = Describe("DownloadCmd", func() { "0.0.0", // No assets "1.1.1", // One asset "3.3.3", // Multiple assets - "4.4.4", // VMs and Metafiles + "4.4.4", // VMs and MetaFiles ) product.AddFile(test.CreateFakeOVA("my-db.ova", "1.1.1")) @@ -58,7 +58,7 @@ var _ = Describe("DownloadCmd", func() { cmd.DownloadFilename = "" cmd.DownloadFilter = "" - cmd.DownloadType = "" + cmd.AssetType = "" }) It("downloads the asset", func() { @@ -115,7 +115,7 @@ var _ = Describe("DownloadCmd", func() { It("returns an error saying there are no assets", func() { cmd.DownloadProductSlug = "my-super-product" cmd.DownloadProductVersion = "1.1.1" - cmd.DownloadType = "addon" + cmd.AssetType = "addon" cmd.DownloadAcceptEULA = true err := cmd.DownloadCmd.RunE(cmd.DownloadCmd, []string{""}) Expect(err).To(HaveOccurred()) @@ -180,7 +180,7 @@ var _ = Describe("DownloadCmd", func() { It("returns a more specific error", func() { cmd.DownloadProductSlug = "my-super-product" cmd.DownloadProductVersion = "0.0.0" - cmd.DownloadType = "vm" + cmd.AssetType = "vm" cmd.DownloadAcceptEULA = true err := cmd.DownloadCmd.RunE(cmd.DownloadCmd, []string{""}) Expect(err).To(HaveOccurred()) @@ -209,7 +209,7 @@ var _ = Describe("DownloadCmd", func() { It("returns a more specific error", func() { cmd.DownloadProductSlug = "my-super-product" cmd.DownloadProductVersion = "3.3.3" - cmd.DownloadType = "vm" + cmd.AssetType = "vm" cmd.DownloadAcceptEULA = true err := cmd.DownloadCmd.RunE(cmd.DownloadCmd, []string{""}) Expect(err).To(HaveOccurred()) @@ -271,7 +271,7 @@ var _ = Describe("DownloadCmd", func() { cmd.DownloadProductSlug = "my-super-product" cmd.DownloadProductVersion = "3.3.3" cmd.DownloadFilter = "txt" - cmd.DownloadType = "vm" + cmd.AssetType = "vm" cmd.DownloadAcceptEULA = true err := cmd.DownloadCmd.RunE(cmd.DownloadCmd, []string{""}) Expect(err).To(HaveOccurred()) @@ -285,7 +285,7 @@ var _ = Describe("DownloadCmd", func() { It("downloads the asset of the given type", func() { cmd.DownloadProductSlug = "my-super-product" cmd.DownloadProductVersion = "4.4.4" - cmd.DownloadType = "metafile" + cmd.AssetType = "metafile" cmd.DownloadAcceptEULA = true err := cmd.DownloadCmd.RunE(cmd.DownloadCmd, []string{""}) Expect(err).ToNot(HaveOccurred()) diff --git a/cmd/products.go b/cmd/products.go index f384ecd..2296fe3 100644 --- a/cmd/products.go +++ b/cmd/products.go @@ -13,12 +13,11 @@ import ( ) var ( - allOrgs = false - searchTerm string - ProductSlug string - ProductVersion string - ListAssetsByType string - SetOSLFile string + allOrgs = false + searchTerm string + ProductSlug string + ProductVersion string + SetOSLFile string ) func init() { @@ -39,7 +38,7 @@ func init() { ListAssetsCmd.Flags().StringVarP(&ProductSlug, "product", "p", "", "Product slug (required)") _ = ListAssetsCmd.MarkFlagRequired("product") ListAssetsCmd.Flags().StringVarP(&ProductVersion, "product-version", "v", "", "Product version") - ListAssetsCmd.Flags().StringVarP(&ListAssetsByType, "type", "t", "", "Filter assets by type (one of "+strings.Join(assetTypesList(), ", ")+")") + ListAssetsCmd.Flags().StringVarP(&AssetType, "type", "t", "", "Filter assets by type (one of "+strings.Join(assetTypesList(), ", ")+")") ListProductVersionsCmd.Flags().StringVarP(&ProductSlug, "product", "p", "", "Product slug (required)") _ = ListProductVersionsCmd.MarkFlagRequired("product") @@ -128,11 +127,11 @@ var ListAssetsCmd = &cobra.Command{ } var assets []*pkg.Asset - if ListAssetsByType == "" { + if AssetType == "" { assets = pkg.GetAssets(product, version.Number) Output.PrintHeader(fmt.Sprintf("Assets for for %s %s:", product.DisplayName, version.Number)) } else { - assetType := typeMapping[ListAssetsByType] + assetType := assetTypeMapping[AssetType] assets = pkg.GetAssetsByType(assetType, product, version.Number) Output.PrintHeader(fmt.Sprintf("%s assets for for %s %s:", assetType, product.DisplayName, version.Number)) } diff --git a/cmd/products_test.go b/cmd/products_test.go index b93c54d..75780b9 100644 --- a/cmd/products_test.go +++ b/cmd/products_test.go @@ -166,7 +166,7 @@ var _ = Describe("Products", func() { It("outputs the list of assets", func() { cmd.ProductSlug = "my-super-product" cmd.ProductVersion = "1" - cmd.ListAssetsByType = "" + cmd.AssetType = "" err := cmd.ListAssetsCmd.RunE(cmd.ListAssetsCmd, []string{}) Expect(err).ToNot(HaveOccurred()) @@ -188,7 +188,7 @@ var _ = Describe("Products", func() { It("outputs the filtered list of assets", func() { cmd.ProductSlug = "my-super-product" cmd.ProductVersion = "1" - cmd.ListAssetsByType = "vm" + cmd.AssetType = "vm" err := cmd.ListAssetsCmd.RunE(cmd.ListAssetsCmd, []string{}) Expect(err).ToNot(HaveOccurred()) diff --git a/cmd/shared.go b/cmd/shared.go index 72461bf..a0184c9 100644 --- a/cmd/shared.go +++ b/cmd/shared.go @@ -16,18 +16,26 @@ import ( var ( Marketplace pkg.MarketplaceInterface Output output.Format - typeMapping = map[string]string{ + + AssetType string + assetTypeMapping = map[string]string{ "addon": pkg.AssetTypeAddon, "chart": pkg.AssetTypeChart, "image": pkg.AssetTypeContainerImage, "metafile": pkg.AssetTypeMetaFile, "vm": pkg.AssetTypeVM, } + MetaFileType string + metaFileTypeMapping = map[string]string{ + "cli": pkg.MetaFileTypeCLI, + "config": pkg.MetaFileTypeConfig, + "other": pkg.MetaFileTypeOther, + } ) func assetTypesList() []string { var assetTypes []string - for assetType := range typeMapping { + for assetType := range assetTypeMapping { assetTypes = append(assetTypes, assetType) } sort.Strings(assetTypes) @@ -35,11 +43,30 @@ func assetTypesList() []string { } func ValidateAssetTypeFilter(cmd *cobra.Command, args []string) error { - if ListAssetsByType == "" { + if AssetType == "" { + return nil + } + if assetTypeMapping[AssetType] != "" { + return nil + } + return fmt.Errorf("Unknown asset type: %s\nPlease use one of %s", AssetType, strings.Join(assetTypesList(), ", ")) +} + +func metaFileTypesList() []string { + var metaFileTypes []string + for metaFileType := range metaFileTypeMapping { + metaFileTypes = append(metaFileTypes, metaFileType) + } + sort.Strings(metaFileTypes) + return metaFileTypes +} + +func ValidateMetaFileType(cmd *cobra.Command, args []string) error { + if MetaFileType == "" { return nil } - if typeMapping[ListAssetsByType] != "" { + if metaFileTypeMapping[MetaFileType] != "" { return nil } - return fmt.Errorf("Unknown asset type: %s\nPlease use one of %s", ListAssetsByType, strings.Join(assetTypesList(), ", ")) + return fmt.Errorf("Unknown meta file type: %s\nPlease use one of %s", MetaFileType, strings.Join(metaFileTypesList(), ", ")) } diff --git a/cmd/shared_test.go b/cmd/shared_test.go index d9e6357..1fb106d 100644 --- a/cmd/shared_test.go +++ b/cmd/shared_test.go @@ -9,28 +9,46 @@ import ( "github.com/vmware-labs/marketplace-cli/v2/cmd" ) -var _ = Describe("Products", func() { - Describe("ValidateAssetTypeFilter", func() { - It("allows valid asset types", func() { - cmd.ListAssetsByType = "addon" - Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) - cmd.ListAssetsByType = "chart" - Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) - cmd.ListAssetsByType = "image" - Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) - cmd.ListAssetsByType = "metafile" - Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) - cmd.ListAssetsByType = "vm" - Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) +var _ = Describe("ValidateAssetTypeFilter", func() { + It("allows valid asset types", func() { + cmd.AssetType = "addon" + Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) + cmd.AssetType = "chart" + Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) + cmd.AssetType = "image" + Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) + cmd.AssetType = "metafile" + Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) + cmd.AssetType = "vm" + Expect(cmd.ValidateAssetTypeFilter(nil, nil)).To(Succeed()) + }) + + When("an invalid asset type is used", func() { + It("returns an error", func() { + cmd.AssetType = "dogfood" + err := cmd.ValidateAssetTypeFilter(nil, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("Unknown asset type: dogfood\nPlease use one of addon, chart, image, metafile, vm")) }) + }) +}) + +var _ = Describe("ValidateMetaFileType", func() { + It("allows valid meta file types", func() { + cmd.MetaFileType = "cli" + Expect(cmd.ValidateMetaFileType(nil, nil)).To(Succeed()) + cmd.MetaFileType = "config" + Expect(cmd.ValidateMetaFileType(nil, nil)).To(Succeed()) + cmd.MetaFileType = "other" + Expect(cmd.ValidateMetaFileType(nil, nil)).To(Succeed()) + }) - When("an invalid asset type is used", func() { - It("returns an error", func() { - cmd.ListAssetsByType = "dogfood" - err := cmd.ValidateAssetTypeFilter(nil, nil) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("Unknown asset type: dogfood\nPlease use one of addon, chart, image, metafile, vm")) - }) + When("an invalid meta file type is used", func() { + It("returns an error", func() { + cmd.MetaFileType = "dogfood" + err := cmd.ValidateMetaFileType(nil, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("Unknown meta file type: dogfood\nPlease use one of cli, config, other")) }) }) }) diff --git a/internal/internalfakes/fake_s3client.go b/internal/internalfakes/fake_s3client.go index 4e41275..b40f290 100644 --- a/internal/internalfakes/fake_s3client.go +++ b/internal/internalfakes/fake_s3client.go @@ -37,15 +37,16 @@ func (fake *FakeS3Client) PutObject(arg1 context.Context, arg2 *s3.PutObjectInpu arg2 *s3.PutObjectInput arg3 []func(*s3.Options) }{arg1, arg2, arg3}) + stub := fake.PutObjectStub + fakeReturns := fake.putObjectReturns fake.recordInvocation("PutObject", []interface{}{arg1, arg2, arg3}) fake.putObjectMutex.Unlock() - if fake.PutObjectStub != nil { - return fake.PutObjectStub(arg1, arg2, arg3...) + if stub != nil { + return stub(arg1, arg2, arg3...) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.putObjectReturns return fakeReturns.result1, fakeReturns.result2 } diff --git a/internal/internalfakes/fake_uploader.go b/internal/internalfakes/fake_uploader.go index 50db02a..70d43b0 100644 --- a/internal/internalfakes/fake_uploader.go +++ b/internal/internalfakes/fake_uploader.go @@ -23,6 +23,21 @@ type FakeUploader struct { result2 string result3 error } + UploadMetaFileStub func(string) (string, string, error) + uploadMetaFileMutex sync.RWMutex + uploadMetaFileArgsForCall []struct { + arg1 string + } + uploadMetaFileReturns struct { + result1 string + result2 string + result3 error + } + uploadMetaFileReturnsOnCall map[int]struct { + result1 string + result2 string + result3 error + } UploadProductFileStub func(string) (string, string, error) uploadProductFileMutex sync.RWMutex uploadProductFileArgsForCall []struct { @@ -48,15 +63,16 @@ func (fake *FakeUploader) UploadMediaFile(arg1 string) (string, string, error) { fake.uploadMediaFileArgsForCall = append(fake.uploadMediaFileArgsForCall, struct { arg1 string }{arg1}) + stub := fake.UploadMediaFileStub + fakeReturns := fake.uploadMediaFileReturns fake.recordInvocation("UploadMediaFile", []interface{}{arg1}) fake.uploadMediaFileMutex.Unlock() - if fake.UploadMediaFileStub != nil { - return fake.UploadMediaFileStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1, ret.result2, ret.result3 } - fakeReturns := fake.uploadMediaFileReturns return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 } @@ -108,21 +124,89 @@ func (fake *FakeUploader) UploadMediaFileReturnsOnCall(i int, result1 string, re }{result1, result2, result3} } +func (fake *FakeUploader) UploadMetaFile(arg1 string) (string, string, error) { + fake.uploadMetaFileMutex.Lock() + ret, specificReturn := fake.uploadMetaFileReturnsOnCall[len(fake.uploadMetaFileArgsForCall)] + fake.uploadMetaFileArgsForCall = append(fake.uploadMetaFileArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.UploadMetaFileStub + fakeReturns := fake.uploadMetaFileReturns + fake.recordInvocation("UploadMetaFile", []interface{}{arg1}) + fake.uploadMetaFileMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeUploader) UploadMetaFileCallCount() int { + fake.uploadMetaFileMutex.RLock() + defer fake.uploadMetaFileMutex.RUnlock() + return len(fake.uploadMetaFileArgsForCall) +} + +func (fake *FakeUploader) UploadMetaFileCalls(stub func(string) (string, string, error)) { + fake.uploadMetaFileMutex.Lock() + defer fake.uploadMetaFileMutex.Unlock() + fake.UploadMetaFileStub = stub +} + +func (fake *FakeUploader) UploadMetaFileArgsForCall(i int) string { + fake.uploadMetaFileMutex.RLock() + defer fake.uploadMetaFileMutex.RUnlock() + argsForCall := fake.uploadMetaFileArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeUploader) UploadMetaFileReturns(result1 string, result2 string, result3 error) { + fake.uploadMetaFileMutex.Lock() + defer fake.uploadMetaFileMutex.Unlock() + fake.UploadMetaFileStub = nil + fake.uploadMetaFileReturns = struct { + result1 string + result2 string + result3 error + }{result1, result2, result3} +} + +func (fake *FakeUploader) UploadMetaFileReturnsOnCall(i int, result1 string, result2 string, result3 error) { + fake.uploadMetaFileMutex.Lock() + defer fake.uploadMetaFileMutex.Unlock() + fake.UploadMetaFileStub = nil + if fake.uploadMetaFileReturnsOnCall == nil { + fake.uploadMetaFileReturnsOnCall = make(map[int]struct { + result1 string + result2 string + result3 error + }) + } + fake.uploadMetaFileReturnsOnCall[i] = struct { + result1 string + result2 string + result3 error + }{result1, result2, result3} +} + func (fake *FakeUploader) UploadProductFile(arg1 string) (string, string, error) { fake.uploadProductFileMutex.Lock() ret, specificReturn := fake.uploadProductFileReturnsOnCall[len(fake.uploadProductFileArgsForCall)] fake.uploadProductFileArgsForCall = append(fake.uploadProductFileArgsForCall, struct { arg1 string }{arg1}) + stub := fake.UploadProductFileStub + fakeReturns := fake.uploadProductFileReturns fake.recordInvocation("UploadProductFile", []interface{}{arg1}) fake.uploadProductFileMutex.Unlock() - if fake.UploadProductFileStub != nil { - return fake.UploadProductFileStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1, ret.result2, ret.result3 } - fakeReturns := fake.uploadProductFileReturns return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 } @@ -179,6 +263,8 @@ func (fake *FakeUploader) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.uploadMediaFileMutex.RLock() defer fake.uploadMediaFileMutex.RUnlock() + fake.uploadMetaFileMutex.RLock() + defer fake.uploadMetaFileMutex.RUnlock() fake.uploadProductFileMutex.RLock() defer fake.uploadProductFileMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/internal/models/product.go b/internal/models/product.go index f38f0d8..4d72056 100644 --- a/internal/models/product.go +++ b/internal/models/product.go @@ -346,7 +346,7 @@ func (product *Product) PrepForUpdate() { } product.Versions = product.AllVersions - product.ProductDeploymentFiles = []*ProductDeploymentFile{} + //product.ProductDeploymentFiles = []*ProductDeploymentFile{} } func (product *Product) SetPCAFile(version, pcaURL string) { diff --git a/internal/uploader.go b/internal/uploader.go index 264112b..0f6eed6 100644 --- a/internal/uploader.go +++ b/internal/uploader.go @@ -23,6 +23,7 @@ import ( const ( FolderMediaFiles = "media-files" + FolderMetaFiles = "meta-files" FolderProductFiles = "marketplace-product-files" ) @@ -39,6 +40,7 @@ func MakeUniqueFilename(filename string) string { //go:generate counterfeiter . Uploader type Uploader interface { UploadMediaFile(filePath string) (string, string, error) + UploadMetaFile(filePath string) (string, string, error) UploadProductFile(filePath string) (string, string, error) } @@ -87,6 +89,15 @@ func (u *S3Uploader) UploadMediaFile(filePath string) (string, string, error) { return filename, url, err } +func (u *S3Uploader) UploadMetaFile(filePath string) (string, string, error) { + filename := filepath.Base(filePath) + datestamp := now() + key := path.Join(u.orgID, FolderMetaFiles, datestamp, filename) + url := fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", u.bucket, u.region, key) + err := u.upload(filePath, key, types.ObjectCannedACLPrivate) + return filename, url, err +} + func (u *S3Uploader) UploadProductFile(filePath string) (string, string, error) { filename := MakeUniqueFilename(filepath.Base(filePath)) key := path.Join(u.orgID, FolderProductFiles, filename) diff --git a/internal/uploader_test.go b/internal/uploader_test.go index 10de19d..1cdca64 100644 --- a/internal/uploader_test.go +++ b/internal/uploader_test.go @@ -89,6 +89,51 @@ var _ = Describe("Uploader", func() { }) }) + Describe("UploadMetaFile", func() { + It("properly uploads a meta file", func() { + output := NewBuffer() + uploader := internal.NewS3Uploader("my-bucket", "my-region", "my-org", client, output) + filename, fileUrl, err := uploader.UploadMetaFile(file.Name()) + Expect(err).ToNot(HaveOccurred()) + + By("sending the object to S3", func() { + Expect(client.PutObjectCallCount()).To(Equal(1)) + _, putArg, options := client.PutObjectArgsForCall(0) + Expect(*putArg.Bucket).To(Equal("my-bucket")) + Expect(*putArg.Key).To(MatchRegexp("^my-org/meta-files/[0-9]+/mkpcli-test-uploader-file-[0-9]+.txt$")) + Expect(putArg.ContentLength).To(Equal(int64(len("file contents")))) + Expect(options).To(BeEmpty()) + }) + + By("writing to a progress bar", func() { + Expect(progressBarMaker.CallCount()).To(Equal(1)) + description, size, progressBarOutput := progressBarMaker.ArgsForCall(0) + Expect(description).To(MatchRegexp("^Uploading mkpcli-test-uploader-file-[0-9]+.txt$")) + Expect(size).To(Equal(int64(13))) + Expect(progressBarOutput).To(Equal(output)) + Expect(progressBar.WrapReaderCallCount()).To(Equal(1)) + }) + + By("returning the uploaded filename and url", func() { + Expect(filename).To(MatchRegexp("^mkpcli-test-uploader-file-[0-9]+.txt$")) + Expect(fileUrl).To(MatchRegexp("^https://my-bucket.s3.my-region.amazonaws.com/my-org/meta-files/[0-9]+/mkpcli-test-uploader-file-[0-9]+.txt$")) + }) + }) + + When("the upload fails", func() { + BeforeEach(func() { + client.PutObjectReturns(nil, errors.New("put object failed")) + }) + It("returns an error", func() { + output := NewBuffer() + uploader := internal.NewS3Uploader("my-bucket", "my-region", "my-org", client, output) + _, _, err := uploader.UploadMediaFile(file.Name()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to upload file: put object failed")) + }) + }) + }) + Describe("UploadProductFile", func() { It("properly uploads a product file", func() { output := NewBuffer() diff --git a/pkg/marketplace.go b/pkg/marketplace.go index 52032b8..9f3886f 100644 --- a/pkg/marketplace.go +++ b/pkg/marketplace.go @@ -51,6 +51,8 @@ type MarketplaceInterface interface { AttachLocalContainerImage(imageFile, image, tag, tagType, instructions string, product *models.Product, version *models.Version) (*models.Product, error) AttachPublicContainerImage(image, tag, tagType, instructions string, product *models.Product, version *models.Version) (*models.Product, error) + AttachMetaFile(metafile, metafileType, metafileVersion string, product *models.Product, version *models.Version) (*models.Product, error) + UploadVM(vmFile string, product *models.Product, version *models.Version) (*models.Product, error) } diff --git a/pkg/metafile.go b/pkg/metafile.go new file mode 100644 index 0000000..59da9ce --- /dev/null +++ b/pkg/metafile.go @@ -0,0 +1,47 @@ +// Copyright 2022 VMware, Inc. +// SPDX-License-Identifier: BSD-2-Clause + +package pkg + +import ( + "github.com/vmware-labs/marketplace-cli/v2/internal/models" +) + +const ( + MetaFileTypeCLI = "CLI" + MetaFileTypeConfig = "CONFIG" + MetaFileTypeOther = "MISC" +) + +func (m *Marketplace) AttachMetaFile(metafile, metafileType, metafileVersion string, product *models.Product, version *models.Version) (*models.Product, error) { + hashString, err := Hash(metafile, models.HashAlgoSHA1) + if err != nil { + return nil, err + } + + uploader, err := m.GetUploader(product.PublisherDetails.OrgId) + if err != nil { + return nil, err + } + filename, fileUrl, err := uploader.UploadMetaFile(metafile) + if err != nil { + return nil, err + } + + product.PrepForUpdate() + product.MetaFiles = append(product.MetaFiles, &models.MetaFile{ + FileType: metafileType, + Version: metafileVersion, + AppVersion: version.Number, + Objects: []*models.MetaFileObject{ + { + FileName: filename, + TempURL: fileUrl, + HashDigest: hashString, + HashAlgorithm: models.HashAlgoSHA1, + }, + }, + }) + + return m.PutProduct(product, version.IsNewVersion) +} diff --git a/pkg/pkgfakes/fake_marketplace_interface.go b/pkg/pkgfakes/fake_marketplace_interface.go index 132bf34..6c57d72 100644 --- a/pkg/pkgfakes/fake_marketplace_interface.go +++ b/pkg/pkgfakes/fake_marketplace_interface.go @@ -48,6 +48,23 @@ type FakeMarketplaceInterface struct { result1 *models.Product result2 error } + AttachMetaFileStub func(string, string, string, *models.Product, *models.Version) (*models.Product, error) + attachMetaFileMutex sync.RWMutex + attachMetaFileArgsForCall []struct { + arg1 string + arg2 string + arg3 string + arg4 *models.Product + arg5 *models.Version + } + attachMetaFileReturns struct { + result1 *models.Product + result2 error + } + attachMetaFileReturnsOnCall map[int]struct { + result1 *models.Product + result2 error + } AttachPublicChartStub func(*url.URL, string, *models.Product, *models.Version) (*models.Product, error) attachPublicChartMutex sync.RWMutex attachPublicChartArgsForCall []struct { @@ -451,6 +468,74 @@ func (fake *FakeMarketplaceInterface) AttachLocalContainerImageReturnsOnCall(i i }{result1, result2} } +func (fake *FakeMarketplaceInterface) AttachMetaFile(arg1 string, arg2 string, arg3 string, arg4 *models.Product, arg5 *models.Version) (*models.Product, error) { + fake.attachMetaFileMutex.Lock() + ret, specificReturn := fake.attachMetaFileReturnsOnCall[len(fake.attachMetaFileArgsForCall)] + fake.attachMetaFileArgsForCall = append(fake.attachMetaFileArgsForCall, struct { + arg1 string + arg2 string + arg3 string + arg4 *models.Product + arg5 *models.Version + }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.AttachMetaFileStub + fakeReturns := fake.attachMetaFileReturns + fake.recordInvocation("AttachMetaFile", []interface{}{arg1, arg2, arg3, arg4, arg5}) + fake.attachMetaFileMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeMarketplaceInterface) AttachMetaFileCallCount() int { + fake.attachMetaFileMutex.RLock() + defer fake.attachMetaFileMutex.RUnlock() + return len(fake.attachMetaFileArgsForCall) +} + +func (fake *FakeMarketplaceInterface) AttachMetaFileCalls(stub func(string, string, string, *models.Product, *models.Version) (*models.Product, error)) { + fake.attachMetaFileMutex.Lock() + defer fake.attachMetaFileMutex.Unlock() + fake.AttachMetaFileStub = stub +} + +func (fake *FakeMarketplaceInterface) AttachMetaFileArgsForCall(i int) (string, string, string, *models.Product, *models.Version) { + fake.attachMetaFileMutex.RLock() + defer fake.attachMetaFileMutex.RUnlock() + argsForCall := fake.attachMetaFileArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 +} + +func (fake *FakeMarketplaceInterface) AttachMetaFileReturns(result1 *models.Product, result2 error) { + fake.attachMetaFileMutex.Lock() + defer fake.attachMetaFileMutex.Unlock() + fake.AttachMetaFileStub = nil + fake.attachMetaFileReturns = struct { + result1 *models.Product + result2 error + }{result1, result2} +} + +func (fake *FakeMarketplaceInterface) AttachMetaFileReturnsOnCall(i int, result1 *models.Product, result2 error) { + fake.attachMetaFileMutex.Lock() + defer fake.attachMetaFileMutex.Unlock() + fake.AttachMetaFileStub = nil + if fake.attachMetaFileReturnsOnCall == nil { + fake.attachMetaFileReturnsOnCall = make(map[int]struct { + result1 *models.Product + result2 error + }) + } + fake.attachMetaFileReturnsOnCall[i] = struct { + result1 *models.Product + result2 error + }{result1, result2} +} + func (fake *FakeMarketplaceInterface) AttachPublicChart(arg1 *url.URL, arg2 string, arg3 *models.Product, arg4 *models.Version) (*models.Product, error) { fake.attachPublicChartMutex.Lock() ret, specificReturn := fake.attachPublicChartReturnsOnCall[len(fake.attachPublicChartArgsForCall)] @@ -1694,6 +1779,8 @@ func (fake *FakeMarketplaceInterface) Invocations() map[string][][]interface{} { defer fake.attachLocalChartMutex.RUnlock() fake.attachLocalContainerImageMutex.RLock() defer fake.attachLocalContainerImageMutex.RUnlock() + fake.attachMetaFileMutex.RLock() + defer fake.attachMetaFileMutex.RUnlock() fake.attachPublicChartMutex.RLock() defer fake.attachPublicChartMutex.RUnlock() fake.attachPublicContainerImageMutex.RLock() diff --git a/version b/version index 1a96df1..ac454c6 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.11.3 +0.12.0