From df2d2a80b55584a0801edeeff275c907d55ff216 Mon Sep 17 00:00:00 2001 From: Robi Nino Date: Wed, 30 Aug 2023 16:00:57 +0300 Subject: [PATCH] Support Lifecycle Release Bundle Distribute (#797) --- README.md | 16 +- distribution/manager.go | 15 +- distribution/services/deleteremote.go | 13 +- distribution/services/distribute.go | 138 ++++++------------ distribution/services/getstatus.go | 17 ++- .../services/utils/distributionutils.go | 27 +--- .../services/utils/distributionutils_test.go | 34 ----- .../services/utils/releasebundlebody.go | 15 +- lifecycle/manager.go | 11 ++ lifecycle/services/distribute.go | 76 ++++++++++ tests/distribution_test.go | 47 +++--- tests/utils_test.go | 4 +- utils/distribution/distribute.go | 106 ++++++++++++++ .../utils => utils/distribution}/specutils.go | 2 +- utils/distribution/utils.go | 32 ++++ utils/distribution/utils_test.go | 41 ++++++ 16 files changed, 380 insertions(+), 214 deletions(-) create mode 100644 lifecycle/services/distribute.go create mode 100644 utils/distribution/distribute.go rename {distribution/services/utils => utils/distribution}/specutils.go (97%) create mode 100644 utils/distribution/utils.go create mode 100644 utils/distribution/utils_test.go diff --git a/README.md b/README.md index fb41807d2..43ca83755 100644 --- a/README.md +++ b/README.md @@ -1614,9 +1614,9 @@ summary, err := distManager.SignReleaseBundle(params) #### Async Distributing a Release Bundle v1 ```go -params := services.NewDistributeReleaseBundleParams("bundle-name", "1") -distributionRules := utils.DistributionCommonParams{SiteName: "Swamp-1", "CityName": "Tel-Aviv", "CountryCodes": []string{"123"}}} -params.DistributionRules = []*utils.DistributionCommonParams{distributionRules} +params := distribution.NewDistributeReleaseBundleParams("bundle-name", "1") +distributionRules := distribution.DistributionCommonParams{SiteName: "Swamp-1", "CityName": "Tel-Aviv", "CountryCodes": []string{"123"}}} +params.DistributionRules = []*distribution.DistributionCommonParams{distributionRules} // Auto-creating repository if it does not exist autoCreateRepo := true err := distManager.DistributeReleaseBundle(params, autoCreateRepo) @@ -1625,9 +1625,9 @@ err := distManager.DistributeReleaseBundle(params, autoCreateRepo) #### Sync Distributing a Release Bundle v1 ```go -params := services.NewDistributeReleaseBundleParams("bundle-name", "1") -distributionRules := utils.DistributionCommonParams{SiteName: "Swamp-1", "CityName": "Tel-Aviv", "CountryCodes": []string{"123"}}} -params.DistributionRules = []*utils.DistributionCommonParams{distributionRules} +params := distribution.NewDistributeReleaseBundleParams("bundle-name", "1") +distributionRules := distribution.DistributionCommonParams{SiteName: "Swamp-1", "CityName": "Tel-Aviv", "CountryCodes": []string{"123"}}} +params.DistributionRules = []*distribution.DistributionCommonParams{distributionRules} // Auto-creating repository if it does not exist autoCreateRepo := true // Wait up to 120 minutes for the release bundle v1 distribution @@ -1654,8 +1654,8 @@ status, err := distributeBundleService.GetStatus(params) ```go params := services.NewDeleteReleaseBundleParams("bundle-name", "1") params.DeleteFromDistribution = true -distributionRules := utils.DistributionCommonParams{SiteName: "Swamp-1", "CityName": "Tel-Aviv", "CountryCodes": []string{"123"}}} -params.DistributionRules = []*utils.DistributionCommonParams{distributionRules} +distributionRules := distribution.DistributionCommonParams{SiteName: "Swamp-1", "CityName": "Tel-Aviv", "CountryCodes": []string{"123"}}} +params.DistributionRules = []*distribution.DistributionCommonParams{distributionRules} // Set to true to enable sync deletion (the command execution will end when the deletion process ends). param.Sync = true // Max minutes to wait for sync deletion. diff --git a/distribution/manager.go b/distribution/manager.go index 6e7ecb592..ad2691cfd 100644 --- a/distribution/manager.go +++ b/distribution/manager.go @@ -5,6 +5,7 @@ import ( "github.com/jfrog/jfrog-client-go/distribution/services" "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/distribution" ) type DistributionServicesManager struct { @@ -57,22 +58,24 @@ func (sm *DistributionServicesManager) SignReleaseBundle(params services.SignBun return signBundleService.SignReleaseBundle(params) } -func (sm *DistributionServicesManager) DistributeReleaseBundle(params services.DistributionParams, autoCreateRepo bool) error { - distributeBundleService := services.NewDistributeReleaseBundleService(sm.client) +func (sm *DistributionServicesManager) DistributeReleaseBundle(params distribution.DistributionParams, autoCreateRepo bool) error { + distributeBundleService := services.NewDistributeReleaseBundleV1Service(sm.client) distributeBundleService.DistDetails = sm.config.GetServiceDetails() distributeBundleService.DryRun = sm.config.IsDryRun() distributeBundleService.AutoCreateRepo = autoCreateRepo - return distributeBundleService.Distribute(params) + distributeBundleService.DistributeParams = params + return distributeBundleService.Distribute() } -func (sm *DistributionServicesManager) DistributeReleaseBundleSync(params services.DistributionParams, maxWaitMinutes int, autoCreateRepo bool) error { - distributeBundleService := services.NewDistributeReleaseBundleService(sm.client) +func (sm *DistributionServicesManager) DistributeReleaseBundleSync(params distribution.DistributionParams, maxWaitMinutes int, autoCreateRepo bool) error { + distributeBundleService := services.NewDistributeReleaseBundleV1Service(sm.client) distributeBundleService.DistDetails = sm.config.GetServiceDetails() distributeBundleService.DryRun = sm.config.IsDryRun() distributeBundleService.MaxWaitMinutes = maxWaitMinutes distributeBundleService.Sync = true distributeBundleService.AutoCreateRepo = autoCreateRepo - return distributeBundleService.Distribute(params) + distributeBundleService.DistributeParams = params + return distributeBundleService.Distribute() } func (sm *DistributionServicesManager) GetDistributionStatus(params services.DistributionStatusParams) (*[]services.DistributionStatusResponse, error) { diff --git a/distribution/services/deleteremote.go b/distribution/services/deleteremote.go index 6fd0f601b..c73a5fcc1 100644 --- a/distribution/services/deleteremote.go +++ b/distribution/services/deleteremote.go @@ -7,6 +7,7 @@ import ( "github.com/jfrog/jfrog-client-go/auth" "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/distribution" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "net/http" @@ -43,9 +44,9 @@ func (dr *DeleteReleaseBundleService) IsDryRun() bool { } func (dr *DeleteReleaseBundleService) DeleteDistribution(deleteDistributionParams DeleteDistributionParams) error { - var distributionRules []DistributionRulesBody + var distributionRules []distribution.DistributionRulesBody for _, rule := range deleteDistributionParams.DistributionRules { - distributionRule := DistributionRulesBody{ + distributionRule := distribution.DistributionRulesBody{ SiteName: rule.GetSiteName(), CityName: rule.GetCityName(), CountryCodes: rule.GetCountryCodes(), @@ -61,7 +62,7 @@ func (dr *DeleteReleaseBundleService) DeleteDistribution(deleteDistributionParam } deleteDistribution := DeleteRemoteDistributionBody{ - DistributionBody: DistributionBody{ + ReleaseBundleDistributeV1Body: distribution.ReleaseBundleDistributeV1Body{ DryRun: dr.DryRun, DistributionRules: distributionRules, }, @@ -133,12 +134,12 @@ func (dr *DeleteReleaseBundleService) waitForDeletion(name, version string) erro } type DeleteRemoteDistributionBody struct { - DistributionBody + distribution.ReleaseBundleDistributeV1Body OnSuccess OnSuccess `json:"on_success"` } type DeleteDistributionParams struct { - DistributionParams + distribution.DistributionParams DeleteFromDistribution bool Sync bool // Max time in minutes to wait for sync distribution to finish. @@ -147,7 +148,7 @@ type DeleteDistributionParams struct { func NewDeleteReleaseBundleParams(name, version string) DeleteDistributionParams { return DeleteDistributionParams{ - DistributionParams: DistributionParams{ + DistributionParams: distribution.DistributionParams{ Name: name, Version: version, }, diff --git a/distribution/services/distribute.go b/distribution/services/distribute.go index 8146b08c8..db3db25b3 100644 --- a/distribution/services/distribute.go +++ b/distribution/services/distribute.go @@ -3,13 +3,10 @@ package services import ( "encoding/json" "fmt" - "net/http" - - artifactoryUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" "github.com/jfrog/jfrog-client-go/auth" - distributionUtils "github.com/jfrog/jfrog-client-go/distribution/services/utils" "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" - "github.com/jfrog/jfrog-client-go/utils" + clientUtils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/distribution" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -17,95 +14,77 @@ import ( const defaultMaxWaitMinutes = 60 // 1 hour const DefaultDistributeSyncSleepIntervalSeconds = 10 // 10 seconds -type DistributeReleaseBundleService struct { +type DistributeReleaseBundleV1Service struct { client *jfroghttpclient.JfrogHttpClient DistDetails auth.ServiceDetails DryRun bool Sync bool AutoCreateRepo bool // Max time in minutes to wait for sync distribution to finish. - MaxWaitMinutes int + MaxWaitMinutes int + DistributeParams distribution.DistributionParams } -func NewDistributeReleaseBundleService(client *jfroghttpclient.JfrogHttpClient) *DistributeReleaseBundleService { - return &DistributeReleaseBundleService{client: client} +func (dr *DistributeReleaseBundleV1Service) GetHttpClient() *jfroghttpclient.JfrogHttpClient { + return dr.client } -func (dr *DistributeReleaseBundleService) GetDistDetails() auth.ServiceDetails { +func (dr *DistributeReleaseBundleV1Service) ServiceDetails() auth.ServiceDetails { return dr.DistDetails } -func (dr *DistributeReleaseBundleService) Distribute(distributeParams DistributionParams) error { - var distributionRules []DistributionRulesBody - for _, spec := range distributeParams.DistributionRules { - distributionRule := DistributionRulesBody{ - SiteName: spec.GetSiteName(), - CityName: spec.GetCityName(), - CountryCodes: spec.GetCountryCodes(), - } - distributionRules = append(distributionRules, distributionRule) - } - distribution := &DistributionBody{ - DryRun: dr.DryRun, - DistributionRules: distributionRules, - AutoCreateRepo: dr.AutoCreateRepo, - } +func (dr *DistributeReleaseBundleV1Service) IsDryRun() bool { + return dr.DryRun +} - trackerId, err := dr.execDistribute(distributeParams.Name, distributeParams.Version, distribution) - if err != nil || !dr.Sync || dr.DryRun { - return err - } +func (dr *DistributeReleaseBundleV1Service) IsSync() bool { + return dr.Sync +} - // Sync distribution - return dr.waitForDistribution(&distributeParams, trackerId) +func (dr *DistributeReleaseBundleV1Service) GetMaxWaitMinutes() int { + return dr.MaxWaitMinutes } -func (dr *DistributeReleaseBundleService) execDistribute(name, version string, distribution *DistributionBody) (json.Number, error) { - httpClientsDetails := dr.DistDetails.CreateHttpClientDetails() - content, err := json.Marshal(distribution) - if err != nil { - return "", errorutils.CheckError(err) - } - dryRunStr := "" - if distribution.DryRun { - dryRunStr = "[Dry run] " - } - log.Info(dryRunStr + "Distributing: " + name + "/" + version) +func (dr *DistributeReleaseBundleV1Service) GetRestApi(name, version string) string { + return "api/v1/distribution/" + name + "/" + version +} - url := dr.DistDetails.GetUrl() + "api/v1/distribution/" + name + "/" + version - artifactoryUtils.SetContentType("application/json", &httpClientsDetails.Headers) - resp, body, err := dr.client.SendPost(url, content, &httpClientsDetails) - if err != nil { - return "", err - } - if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK, http.StatusAccepted); err != nil { - return "", err - } - response := DistributionResponseBody{} - err = json.Unmarshal(body, &response) - if err != nil { - return "", errorutils.CheckError(err) +func (dr *DistributeReleaseBundleV1Service) GetDistributeBody() any { + return distribution.CreateDistributeV1Body(dr.DistributeParams, dr.DryRun, dr.AutoCreateRepo) +} + +func (dr *DistributeReleaseBundleV1Service) GetDistributionParams() distribution.DistributionParams { + return dr.DistributeParams +} + +func NewDistributeReleaseBundleV1Service(client *jfroghttpclient.JfrogHttpClient) *DistributeReleaseBundleV1Service { + return &DistributeReleaseBundleV1Service{client: client} +} + +func (dr *DistributeReleaseBundleV1Service) Distribute() error { + trackerId, err := distribution.DoDistribute(dr) + if err != nil || !dr.IsSync() || dr.IsDryRun() { + return err } - log.Debug("Distribution response:", resp.Status) - log.Debug(utils.IndentJson(body)) - return response.TrackerId, nil + // Sync distribution + return dr.waitForDistribution(&dr.DistributeParams, trackerId) } -func (dr *DistributeReleaseBundleService) waitForDistribution(distributeParams *DistributionParams, trackerId json.Number) error { - distributeBundleService := NewDistributionStatusService(dr.client) - distributeBundleService.DistDetails = dr.GetDistDetails() +func (dr *DistributeReleaseBundleV1Service) waitForDistribution(distributeParams *distribution.DistributionParams, trackerId json.Number) error { + distributeBundleService := NewDistributionStatusService(dr.GetHttpClient()) + distributeBundleService.DistDetails = dr.ServiceDetails() distributionStatusParams := DistributionStatusParams{ Name: distributeParams.Name, Version: distributeParams.Version, TrackerId: trackerId.String(), } maxWaitMinutes := defaultMaxWaitMinutes - if dr.MaxWaitMinutes >= 1 { - maxWaitMinutes = dr.MaxWaitMinutes + if dr.GetMaxWaitMinutes() >= 1 { + maxWaitMinutes = dr.GetMaxWaitMinutes() } distributingMessage := fmt.Sprintf("Sync: Distributing %s/%s...", distributeParams.Name, distributeParams.Version) - retryExecutor := &utils.RetryExecutor{ + retryExecutor := &clientUtils.RetryExecutor{ MaxRetries: maxWaitMinutes * 60 / DefaultDistributeSyncSleepIntervalSeconds, RetriesIntervalMilliSecs: DefaultDistributeSyncSleepIntervalSeconds * 1000, ErrorMessage: "", @@ -120,7 +99,7 @@ func (dr *DistributeReleaseBundleService) waitForDistribution(distributeParams * if err != nil { return false, errorutils.CheckError(err) } - return false, errorutils.CheckErrorf("Distribution failed: " + utils.IndentJson(bytes)) + return false, errorutils.CheckErrorf("Distribution failed: " + clientUtils.IndentJson(bytes)) } if (*response)[0].Status == Completed { log.Info("Distribution Completed!") @@ -133,32 +112,3 @@ func (dr *DistributeReleaseBundleService) waitForDistribution(distributeParams * } return retryExecutor.Execute() } - -type DistributionBody struct { - DryRun bool `json:"dry_run"` - DistributionRules []DistributionRulesBody `json:"distribution_rules"` - AutoCreateRepo bool `json:"auto_create_missing_repositories,omitempty"` -} - -type DistributionRulesBody struct { - SiteName string `json:"site_name,omitempty"` - CityName string `json:"city_name,omitempty"` - CountryCodes []string `json:"country_codes,omitempty"` -} - -type DistributionResponseBody struct { - TrackerId json.Number `json:"id"` -} - -type DistributionParams struct { - DistributionRules []*distributionUtils.DistributionCommonParams - Name string - Version string -} - -func NewDistributeReleaseBundleParams(name, version string) DistributionParams { - return DistributionParams{ - Name: name, - Version: version, - } -} diff --git a/distribution/services/getstatus.go b/distribution/services/getstatus.go index 867d93215..aeea27944 100644 --- a/distribution/services/getstatus.go +++ b/distribution/services/getstatus.go @@ -3,6 +3,7 @@ package services import ( "encoding/json" "errors" + "github.com/jfrog/jfrog-client-go/utils/distribution" "net/http" "strings" @@ -109,14 +110,14 @@ const ( ) type DistributionStatusResponse struct { - Id json.Number `json:"distribution_id"` - FriendlyId json.Number `json:"distribution_friendly_id,omitempty"` - Type DistributionType `json:"type,omitempty"` - Name string `json:"release_bundle_name,omitempty"` - Version string `json:"release_bundle_version,omitempty"` - Status DistributionStatus `json:"status,omitempty"` - DistributionRules []DistributionRulesBody `json:"distribution_rules,omitempty"` - Sites []DistributionSiteStatus `json:"sites,omitempty"` + Id json.Number `json:"distribution_id"` + FriendlyId json.Number `json:"distribution_friendly_id,omitempty"` + Type DistributionType `json:"type,omitempty"` + Name string `json:"release_bundle_name,omitempty"` + Version string `json:"release_bundle_version,omitempty"` + Status DistributionStatus `json:"status,omitempty"` + DistributionRules []distribution.DistributionRulesBody `json:"distribution_rules,omitempty"` + Sites []DistributionSiteStatus `json:"sites,omitempty"` } type DistributionSiteStatus struct { diff --git a/distribution/services/utils/distributionutils.go b/distribution/services/utils/distributionutils.go index 617dbbac1..529b16309 100644 --- a/distribution/services/utils/distributionutils.go +++ b/distribution/services/utils/distributionutils.go @@ -1,10 +1,8 @@ package utils import ( - "github.com/jfrog/gofrog/stringutils" - "regexp" - rtUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/utils/distribution" ) type ReleaseNotesSyntax string @@ -15,8 +13,6 @@ const ( PlainText ReleaseNotesSyntax = "plain_text" ) -var fileSpecCaptureGroup = regexp.MustCompile(`({\d})`) - type ReleaseBundleParams struct { SpecFiles []*rtUtils.CommonParams Name string @@ -47,7 +43,7 @@ func CreateBundleBody(releaseBundleParams ReleaseBundleParams, dryRun bool) (*Re } // Create path mapping - pathMappings := createPathMappings(specFile) + pathMappings := distribution.CreatePathMappings(specFile.Pattern, specFile.Target) // Create added properties addedProps := createAddedProps(specFile) @@ -89,25 +85,6 @@ func createAql(specFile *rtUtils.CommonParams) (string, error) { return rtUtils.BuildQueryFromSpecFile(specFile, rtUtils.NONE), nil } -// Creat the path mapping from the input spec -func createPathMappings(specFile *rtUtils.CommonParams) []PathMapping { - if len(specFile.Target) == 0 { - return []PathMapping{} - } - - // Convert the file spec pattern and target to match the path mapping input and output specifications, respectfully. - return []PathMapping{{ - // The file spec pattern is wildcard based. Convert it to Regex: - Input: stringutils.WildcardPatternToRegExp(specFile.Pattern), - // The file spec target contain placeholders-style matching groups, like {1}. - // Convert it to REST API's matching groups style, like $1. - Output: fileSpecCaptureGroup.ReplaceAllStringFunc(specFile.Target, func(s string) string { - // Remove curly parenthesis and prepend $ - return "$" + s[1:2] - }), - }} -} - // Create the AddedProps array from the input TargetProps string func createAddedProps(specFile *rtUtils.CommonParams) []AddedProps { props := specFile.TargetProps diff --git a/distribution/services/utils/distributionutils_test.go b/distribution/services/utils/distributionutils_test.go index c99d1ae2c..e820ff0f3 100644 --- a/distribution/services/utils/distributionutils_test.go +++ b/distribution/services/utils/distributionutils_test.go @@ -55,37 +55,3 @@ func TestCreateBundleBodyQuery(t *testing.T) { } } } - -func TestCreatePathMappings(t *testing.T) { - tests := []struct { - specPattern string - specTarget string - expectedMappingInput string - expectedMappingOutput string - }{ - {"", "", "", ""}, - {"repo/path/file.in", "", "", ""}, - {"a/b/c", "a/b/x", "^a/b/c$", "a/b/x"}, - {"a/(b)/c", "a/d/c", "^a/(b)/c$", "a/d/c"}, - {"a/(*)/c", "a/d/c", "^a/(.*)/c$", "a/d/c"}, - {"a/(b)/c", "a/(d)/c", "^a/(b)/c$", "a/(d)/c"}, - {"a/(b)/c", "a/b/c/{1}", "^a/(b)/c$", "a/b/c/$1"}, - {"a/(b)/(c)", "a/b/c/{1}/{2}", "^a/(b)/(c)$", "a/b/c/$1/$2"}, - {"a/(b)/(c)", "a/b/c/{2}/{1}", "^a/(b)/(c)$", "a/b/c/$2/$1"}, - } - - for _, test := range tests { - t.Run(test.specPattern, func(t *testing.T) { - specFile := &utils.CommonParams{Pattern: test.specPattern, Target: test.specTarget} - pathMappings := createPathMappings(specFile) - if test.expectedMappingInput == "" { - assert.Empty(t, pathMappings) - return - } - assert.Len(t, pathMappings, 1) - actualPathMapping := pathMappings[0] - assert.Equal(t, test.expectedMappingInput, actualPathMapping.Input) - assert.Equal(t, test.expectedMappingOutput, actualPathMapping.Output) - }) - } -} diff --git a/distribution/services/utils/releasebundlebody.go b/distribution/services/utils/releasebundlebody.go index 515dc740a..e98a9fe26 100644 --- a/distribution/services/utils/releasebundlebody.go +++ b/distribution/services/utils/releasebundlebody.go @@ -1,5 +1,7 @@ package utils +import "github.com/jfrog/jfrog-client-go/utils/distribution" + // REST body for create and update a release bundle type ReleaseBundleBody struct { DryRun bool `json:"dry_run"` @@ -20,15 +22,10 @@ type BundleSpec struct { } type BundleQuery struct { - QueryName string `json:"query_name,omitempty"` - Aql string `json:"aql"` - PathMappings []PathMapping `json:"mappings,omitempty"` - AddedProps []AddedProps `json:"added_props,omitempty"` -} - -type PathMapping struct { - Input string `json:"input"` - Output string `json:"output"` + QueryName string `json:"query_name,omitempty"` + Aql string `json:"aql,omitempty"` + PathMappings []distribution.PathMapping `json:"mappings,omitempty"` + AddedProps []AddedProps `json:"added_props,omitempty"` } type AddedProps struct { diff --git a/lifecycle/manager.go b/lifecycle/manager.go index dd755c751..cd4eb1389 100644 --- a/lifecycle/manager.go +++ b/lifecycle/manager.go @@ -4,6 +4,7 @@ import ( "github.com/jfrog/jfrog-client-go/config" "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" lifecycle "github.com/jfrog/jfrog-client-go/lifecycle/services" + "github.com/jfrog/jfrog-client-go/utils/distribution" ) type LifecycleServicesManager struct { @@ -64,3 +65,13 @@ func (lcs *LifecycleServicesManager) DeleteReleaseBundle(rbDetails lifecycle.Rel rbService := lifecycle.NewReleaseBundlesService(lcs.config.GetServiceDetails(), lcs.client) return rbService.DeleteReleaseBundle(rbDetails, params) } + +func (lcs *LifecycleServicesManager) DistributeReleaseBundle(params distribution.DistributionParams, autoCreateRepo bool, pathMapping lifecycle.PathMapping) error { + distributeBundleService := lifecycle.NewDistributeReleaseBundleService(lcs.client) + distributeBundleService.LcDetails = lcs.config.GetServiceDetails() + distributeBundleService.DryRun = lcs.config.IsDryRun() + distributeBundleService.AutoCreateRepo = autoCreateRepo + distributeBundleService.DistributeParams = params + distributeBundleService.PathMapping = pathMapping + return distributeBundleService.Distribute() +} diff --git a/lifecycle/services/distribute.go b/lifecycle/services/distribute.go new file mode 100644 index 000000000..2f50bdc3d --- /dev/null +++ b/lifecycle/services/distribute.go @@ -0,0 +1,76 @@ +package services + +import ( + "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" + "github.com/jfrog/jfrog-client-go/utils/distribution" +) + +type DistributeReleaseBundleService struct { + client *jfroghttpclient.JfrogHttpClient + LcDetails auth.ServiceDetails + DryRun bool + AutoCreateRepo bool + DistributeParams distribution.DistributionParams + PathMapping +} + +func (dr *DistributeReleaseBundleService) GetHttpClient() *jfroghttpclient.JfrogHttpClient { + return dr.client +} + +func (dr *DistributeReleaseBundleService) ServiceDetails() auth.ServiceDetails { + return dr.LcDetails +} + +func (dr *DistributeReleaseBundleService) IsDryRun() bool { + return dr.DryRun +} + +func (dr *DistributeReleaseBundleService) IsAutoCreateRepo() bool { + return dr.AutoCreateRepo +} + +func (dr *DistributeReleaseBundleService) GetRestApi(name, version string) string { + return "api/v2/distribution/distribute/" + name + "/" + version +} + +func (dr *DistributeReleaseBundleService) GetDistributeBody() any { + return dr.createDistributeBody() +} + +func (dr *DistributeReleaseBundleService) GetDistributionParams() distribution.DistributionParams { + return dr.DistributeParams +} + +func NewDistributeReleaseBundleService(client *jfroghttpclient.JfrogHttpClient) *DistributeReleaseBundleService { + return &DistributeReleaseBundleService{client: client} +} + +func (dr *DistributeReleaseBundleService) Distribute() error { + _, err := distribution.DoDistribute(dr) + return err +} + +func (dr *DistributeReleaseBundleService) createDistributeBody() ReleaseBundleDistributeBody { + return ReleaseBundleDistributeBody{ + ReleaseBundleDistributeV1Body: distribution.CreateDistributeV1Body(dr.DistributeParams, dr.DryRun, dr.AutoCreateRepo), + Modifications: Modifications{ + PathMappings: distribution.CreatePathMappings(dr.Pattern, dr.Target), + }, + } +} + +type ReleaseBundleDistributeBody struct { + distribution.ReleaseBundleDistributeV1Body + Modifications `json:"modifications"` +} + +type Modifications struct { + PathMappings []distribution.PathMapping `json:"mappings"` +} + +type PathMapping struct { + Pattern string + Target string +} diff --git a/tests/distribution_test.go b/tests/distribution_test.go index 4db947aa2..34acf0ca9 100644 --- a/tests/distribution_test.go +++ b/tests/distribution_test.go @@ -3,19 +3,19 @@ package tests import ( "encoding/json" "fmt" - "net/http" - "os" - "path/filepath" - "testing" - "time" - artifactoryServices "github.com/jfrog/jfrog-client-go/artifactory/services" "github.com/jfrog/jfrog-client-go/artifactory/services/utils" "github.com/jfrog/jfrog-client-go/distribution/services" distributionServicesUtils "github.com/jfrog/jfrog-client-go/distribution/services/utils" "github.com/jfrog/jfrog-client-go/http/httpclient" + "github.com/jfrog/jfrog-client-go/utils/distribution" "github.com/jfrog/jfrog-client-go/utils/io/httputils" "github.com/stretchr/testify/assert" + "net/http" + "os" + "path/filepath" + "testing" + "time" ) type distributableDistributionStatus string @@ -181,11 +181,12 @@ func updateDryRun(updateBundleParams services.UpdateReleaseBundleParams) error { return err } -func distributeDryRun(distributionParams services.DistributionParams) error { +func distributeDryRun(distributionParams distribution.DistributionParams) error { defer setServicesToDryRunFalse() testsBundleDistributeService.DryRun = true testsBundleDistributeService.AutoCreateRepo = true - return testsBundleDistributeService.Distribute(distributionParams) + testsBundleDistributeService.DistributeParams = distributionParams + return testsBundleDistributeService.Distribute() } func setServicesToDryRunFalse() { @@ -272,8 +273,8 @@ func createSignDistributeDelete(t *testing.T) { assertReleaseBundleSigned(t, distributionResponse.State) // Create distribute params. - distributeBundleParams := services.NewDistributeReleaseBundleParams(bundleName, bundleVersion) - distributeBundleParams.DistributionRules = []*distributionServicesUtils.DistributionCommonParams{{SiteName: "*"}} + distributeBundleParams := distribution.NewDistributeReleaseBundleParams(bundleName, bundleVersion) + distributeBundleParams.DistributionRules = []*distribution.DistributionCommonParams{{SiteName: "*"}} // Create response params. distributionStatusParams := services.DistributionStatusParams{ @@ -293,7 +294,8 @@ func createSignDistributeDelete(t *testing.T) { // Distribute release bundle testsBundleDistributeService.AutoCreateRepo = true - err = testsBundleDistributeService.Distribute(distributeBundleParams) + testsBundleDistributeService.DistributeParams = distributeBundleParams + err = testsBundleDistributeService.Distribute() assert.NoError(t, err) waitForDistribution(t, bundleName) @@ -331,11 +333,12 @@ func createSignSyncDistributeDelete(t *testing.T) { assertReleaseBundleSigned(t, distributionResponse.State) // Distribute release bundle - distributeBundleParams := services.NewDistributeReleaseBundleParams(bundleName, bundleVersion) - distributeBundleParams.DistributionRules = []*distributionServicesUtils.DistributionCommonParams{{SiteName: "*"}} + distributeBundleParams := distribution.NewDistributeReleaseBundleParams(bundleName, bundleVersion) + distributeBundleParams.DistributionRules = []*distribution.DistributionCommonParams{{SiteName: "*"}} testsBundleDistributeService.Sync = true testsBundleDistributeService.AutoCreateRepo = true - err = testsBundleDistributeService.Distribute(distributeBundleParams) + testsBundleDistributeService.DistributeParams = distributeBundleParams + err = testsBundleDistributeService.Distribute() assert.NoError(t, err) // Assert release bundle in "completed" status @@ -365,12 +368,13 @@ func createDistributeMapping(t *testing.T) { verifyValidSha256(t, summary.GetSha256()) // Distribute release bundle - distributeBundleParams := services.NewDistributeReleaseBundleParams(bundleName, bundleVersion) - distributeBundleParams.DistributionRules = []*distributionServicesUtils.DistributionCommonParams{{SiteName: "*"}} + distributeBundleParams := distribution.NewDistributeReleaseBundleParams(bundleName, bundleVersion) + distributeBundleParams.DistributionRules = []*distribution.DistributionCommonParams{{SiteName: "*"}} testsBundleDistributeService.Sync = true // On distribution with path mapping, the target repository cannot be auto-created testsBundleDistributeService.AutoCreateRepo = false - err = testsBundleDistributeService.Distribute(distributeBundleParams) + testsBundleDistributeService.DistributeParams = distributeBundleParams + err = testsBundleDistributeService.Distribute() assert.NoError(t, err) // Make sure /b.out does exist in Artifactory @@ -400,12 +404,13 @@ func createDistributeMappingPlaceholder(t *testing.T) { verifyValidSha256(t, summary.GetSha256()) // Distribute release bundle - distributeBundleParams := services.NewDistributeReleaseBundleParams(bundleName, bundleVersion) - distributeBundleParams.DistributionRules = []*distributionServicesUtils.DistributionCommonParams{{SiteName: "*"}} + distributeBundleParams := distribution.NewDistributeReleaseBundleParams(bundleName, bundleVersion) + distributeBundleParams.DistributionRules = []*distribution.DistributionCommonParams{{SiteName: "*"}} testsBundleDistributeService.Sync = true // On distribution with path mapping, the target repository cannot be auto-created testsBundleDistributeService.AutoCreateRepo = false - err = testsBundleDistributeService.Distribute(distributeBundleParams) + testsBundleDistributeService.DistributeParams = distributeBundleParams + err = testsBundleDistributeService.Distribute() assert.NoError(t, err) // Make sure /b.out does exist in Artifactory @@ -556,7 +561,7 @@ func deleteRemoteAndLocalBundle(t *testing.T, bundleName string) { deleteBundleParams := services.NewDeleteReleaseBundleParams(bundleName, bundleVersion) // Delete also local release bundle deleteBundleParams.DeleteFromDistribution = true - deleteBundleParams.DistributionRules = []*distributionServicesUtils.DistributionCommonParams{{SiteName: "*"}} + deleteBundleParams.DistributionRules = []*distribution.DistributionCommonParams{{SiteName: "*"}} deleteBundleParams.Sync = true err := testsBundleDeleteRemoteService.DeleteDistribution(deleteBundleParams) artifactoryCleanup(t) diff --git a/tests/utils_test.go b/tests/utils_test.go index ce87597e7..34c89d12f 100644 --- a/tests/utils_test.go +++ b/tests/utils_test.go @@ -101,7 +101,7 @@ var ( testsBundleCreateService *distributionServices.CreateReleaseBundleService testsBundleUpdateService *distributionServices.UpdateReleaseBundleService testsBundleSignService *distributionServices.SignBundleService - testsBundleDistributeService *distributionServices.DistributeReleaseBundleService + testsBundleDistributeService *distributionServices.DistributeReleaseBundleV1Service testsBundleDistributionStatusService *distributionServices.DistributionStatusService testsBundleDeleteLocalService *distributionServices.DeleteLocalReleaseBundleService testsBundleDeleteRemoteService *distributionServices.DeleteReleaseBundleService @@ -258,7 +258,7 @@ func createDistributionManager() { testsBundleCreateService = distributionServices.NewCreateReleaseBundleService(client) testsBundleUpdateService = distributionServices.NewUpdateReleaseBundleService(client) testsBundleSignService = distributionServices.NewSignBundleService(client) - testsBundleDistributeService = distributionServices.NewDistributeReleaseBundleService(client) + testsBundleDistributeService = distributionServices.NewDistributeReleaseBundleV1Service(client) testsBundleDistributionStatusService = distributionServices.NewDistributionStatusService(client) testsBundleDeleteLocalService = distributionServices.NewDeleteLocalDistributionService(client) testsBundleSetSigningKeyService = distributionServices.NewSetSigningKeyService(client) diff --git a/utils/distribution/distribute.go b/utils/distribution/distribute.go new file mode 100644 index 000000000..47e80475f --- /dev/null +++ b/utils/distribution/distribute.go @@ -0,0 +1,106 @@ +package distribution + +import ( + "encoding/json" + artifactoryUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" + clientUtils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "net/http" +) + +type DistributeReleaseBundleExecutor interface { + GetHttpClient() *jfroghttpclient.JfrogHttpClient + ServiceDetails() auth.ServiceDetails + IsDryRun() bool + GetRestApi(name, version string) string + GetDistributeBody() any + GetDistributionParams() DistributionParams +} + +func CreateDistributeV1Body(distributeParams DistributionParams, dryRun, isAutoCreateRepo bool) ReleaseBundleDistributeV1Body { + var distributionRules []DistributionRulesBody + for _, spec := range distributeParams.DistributionRules { + distributionRule := DistributionRulesBody{ + SiteName: spec.GetSiteName(), + CityName: spec.GetCityName(), + CountryCodes: spec.GetCountryCodes(), + } + distributionRules = append(distributionRules, distributionRule) + } + body := ReleaseBundleDistributeV1Body{ + DryRun: dryRun, + DistributionRules: distributionRules, + AutoCreateRepo: isAutoCreateRepo, + } + return body +} + +func DoDistribute(dr DistributeReleaseBundleExecutor) (trackerId json.Number, err error) { + distributeParams := dr.GetDistributionParams() + return execDistribute(dr, distributeParams.Name, distributeParams.Version) +} + +func execDistribute(dr DistributeReleaseBundleExecutor, name, version string) (json.Number, error) { + httpClientsDetails := dr.ServiceDetails().CreateHttpClientDetails() + content, err := json.Marshal(dr.GetDistributeBody()) + if err != nil { + return "", errorutils.CheckError(err) + } + + dryRunStr := "" + if dr.IsDryRun() { + dryRunStr = "[Dry run] " + } + log.Info(dryRunStr + "Distributing: " + name + "/" + version) + + url := dr.ServiceDetails().GetUrl() + dr.GetRestApi(name, version) + artifactoryUtils.SetContentType("application/json", &httpClientsDetails.Headers) + resp, body, err := dr.GetHttpClient().SendPost(url, content, &httpClientsDetails) + if err != nil { + return "", err + } + if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK, http.StatusAccepted); err != nil { + return "", err + } + response := DistributionResponseBody{} + err = json.Unmarshal(body, &response) + if err != nil { + return "", errorutils.CheckError(err) + } + + log.Debug("Distribution response:", resp.Status) + log.Debug(clientUtils.IndentJson(body)) + return response.TrackerId, nil +} + +func NewDistributeReleaseBundleParams(name, version string) DistributionParams { + return DistributionParams{ + Name: name, + Version: version, + } +} + +type DistributionParams struct { + DistributionRules []*DistributionCommonParams + Name string + Version string +} + +type ReleaseBundleDistributeV1Body struct { + DryRun bool `json:"dry_run"` + DistributionRules []DistributionRulesBody `json:"distribution_rules"` + AutoCreateRepo bool `json:"auto_create_missing_repositories,omitempty"` +} + +type DistributionRulesBody struct { + SiteName string `json:"site_name,omitempty"` + CityName string `json:"city_name,omitempty"` + CountryCodes []string `json:"country_codes,omitempty"` +} + +type DistributionResponseBody struct { + TrackerId json.Number `json:"id"` +} diff --git a/distribution/services/utils/specutils.go b/utils/distribution/specutils.go similarity index 97% rename from distribution/services/utils/specutils.go rename to utils/distribution/specutils.go index bb954b229..1609c0b48 100644 --- a/distribution/services/utils/specutils.go +++ b/utils/distribution/specutils.go @@ -1,4 +1,4 @@ -package utils +package distribution type DistributionCommonParams struct { SiteName string diff --git a/utils/distribution/utils.go b/utils/distribution/utils.go new file mode 100644 index 000000000..2c6ef7921 --- /dev/null +++ b/utils/distribution/utils.go @@ -0,0 +1,32 @@ +package distribution + +import ( + "github.com/jfrog/gofrog/stringutils" + "regexp" +) + +var fileSpecCaptureGroup = regexp.MustCompile(`({\d})`) + +// Create the path mapping from the input spec +func CreatePathMappings(pattern, target string) []PathMapping { + if len(target) == 0 { + return []PathMapping{} + } + + // Convert the file spec pattern and target to match the path mapping input and output specifications, respectfully. + return []PathMapping{{ + // The file spec pattern is wildcard based. Convert it to Regex: + Input: stringutils.WildcardPatternToRegExp(pattern), + // The file spec target contain placeholders-style matching groups, like {1}. + // Convert it to REST-APIs matching groups style, like $1. + Output: fileSpecCaptureGroup.ReplaceAllStringFunc(target, func(s string) string { + // Remove curly parenthesis and prepend $ + return "$" + s[1:2] + }), + }} +} + +type PathMapping struct { + Input string `json:"input"` + Output string `json:"output"` +} diff --git a/utils/distribution/utils_test.go b/utils/distribution/utils_test.go new file mode 100644 index 000000000..6eeb5235c --- /dev/null +++ b/utils/distribution/utils_test.go @@ -0,0 +1,41 @@ +package distribution + +import ( + "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCreatePathMappings(t *testing.T) { + tests := []struct { + specPattern string + specTarget string + expectedMappingInput string + expectedMappingOutput string + }{ + {"", "", "", ""}, + {"repo/path/file.in", "", "", ""}, + {"a/b/c", "a/b/x", "^a/b/c$", "a/b/x"}, + {"a/(b)/c", "a/d/c", "^a/(b)/c$", "a/d/c"}, + {"a/(*)/c", "a/d/c", "^a/(.*)/c$", "a/d/c"}, + {"a/(b)/c", "a/(d)/c", "^a/(b)/c$", "a/(d)/c"}, + {"a/(b)/c", "a/b/c/{1}", "^a/(b)/c$", "a/b/c/$1"}, + {"a/(b)/(c)", "a/b/c/{1}/{2}", "^a/(b)/(c)$", "a/b/c/$1/$2"}, + {"a/(b)/(c)", "a/b/c/{2}/{1}", "^a/(b)/(c)$", "a/b/c/$2/$1"}, + } + + for _, test := range tests { + t.Run(test.specPattern, func(t *testing.T) { + specFile := &utils.CommonParams{Pattern: test.specPattern, Target: test.specTarget} + pathMappings := CreatePathMappings(specFile.Pattern, specFile.Target) + if test.expectedMappingInput == "" { + assert.Empty(t, pathMappings) + return + } + assert.Len(t, pathMappings, 1) + actualPathMapping := pathMappings[0] + assert.Equal(t, test.expectedMappingInput, actualPathMapping.Input) + assert.Equal(t, test.expectedMappingOutput, actualPathMapping.Output) + }) + } +}