diff --git a/.github/workflows/lifecycleTests.yml b/.github/workflows/lifecycleTests.yml new file mode 100644 index 000000000..01613e0af --- /dev/null +++ b/.github/workflows/lifecycleTests.yml @@ -0,0 +1,44 @@ +name: Lifecycle Tests +on: + push: + branches: + - '**' + tags-ignore: + - '**' + # Triggers the workflow on labeled PRs only. + pull_request_target: + types: [labeled] +# Ensures that only the latest commit is running for each PR at a time. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.ref }} + cancel-in-progress: true +jobs: + Lifecycle-Tests: + if: contains(github.event.pull_request.labels.*.name, 'safe to test') || github.event_name == 'push' + name: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v3 + with: + go-version: 1.20.x + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Go Cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-go- + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Run Lifecycle tests + run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.lifecycle --jfrog.url=${{ secrets.PLATFORM_URL }} --jfrog.adminToken=${{ secrets.PLATFORM_ADMIN_TOKEN }} --jfrog.user=${{ secrets.PLATFORM_USER }} --ci.runId=${{ runner.os }}-lifecycle diff --git a/distribution/cli.go b/distribution/cli.go index 682156767..0a0664a29 100644 --- a/distribution/cli.go +++ b/distribution/cli.go @@ -2,10 +2,6 @@ package distribution import ( "errors" - "os" - "path/filepath" - "strings" - "github.com/jfrog/jfrog-cli-core/v2/common/commands" "github.com/jfrog/jfrog-cli-core/v2/common/spec" distributionCommands "github.com/jfrog/jfrog-cli-core/v2/distribution/commands" @@ -18,10 +14,14 @@ import ( "github.com/jfrog/jfrog-cli/docs/artifactory/releasebundleupdate" "github.com/jfrog/jfrog-cli/docs/common" "github.com/jfrog/jfrog-cli/utils/cliutils" + "github.com/jfrog/jfrog-cli/utils/distribution" distributionServices "github.com/jfrog/jfrog-client-go/distribution/services" distributionServicesUtils "github.com/jfrog/jfrog-client-go/distribution/services/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/urfave/cli" + "os" + "path/filepath" + "strings" ) func GetCommands() []cli.Command { @@ -111,11 +111,11 @@ func releaseBundleCreateCmd(c *cli.Context) error { return err } releaseBundleCreateCmd := distributionCommands.NewReleaseBundleCreateCommand() - rtDetails, err := createArtifactoryDetailsByFlags(c) + dsDetails, err := createDistributionDetailsByFlags(c) if err != nil { return err } - releaseBundleCreateCmd.SetServerDetails(rtDetails).SetReleaseBundleCreateParams(params).SetSpec(releaseBundleCreateSpec).SetDryRun(c.Bool("dry-run")).SetDetailedSummary(c.Bool("detailed-summary")) + releaseBundleCreateCmd.SetServerDetails(dsDetails).SetReleaseBundleCreateParams(params).SetSpec(releaseBundleCreateSpec).SetDryRun(c.Bool("dry-run")).SetDetailedSummary(c.Bool("detailed-summary")) err = commands.Exec(releaseBundleCreateCmd) if releaseBundleCreateCmd.IsDetailedSummary() { @@ -153,11 +153,11 @@ func releaseBundleUpdateCmd(c *cli.Context) error { return err } releaseBundleUpdateCmd := distributionCommands.NewReleaseBundleUpdateCommand() - rtDetails, err := createArtifactoryDetailsByFlags(c) + dsDetails, err := createDistributionDetailsByFlags(c) if err != nil { return err } - releaseBundleUpdateCmd.SetServerDetails(rtDetails).SetReleaseBundleUpdateParams(params).SetSpec(releaseBundleUpdateSpec).SetDryRun(c.Bool("dry-run")).SetDetailedSummary(c.Bool("detailed-summary")) + releaseBundleUpdateCmd.SetServerDetails(dsDetails).SetReleaseBundleUpdateParams(params).SetSpec(releaseBundleUpdateSpec).SetDryRun(c.Bool("dry-run")).SetDetailedSummary(c.Bool("detailed-summary")) err = commands.Exec(releaseBundleUpdateCmd) if releaseBundleUpdateCmd.IsDetailedSummary() { @@ -177,11 +177,11 @@ func releaseBundleSignCmd(c *cli.Context) error { params.StoringRepository = c.String("repo") params.GpgPassphrase = c.String("passphrase") releaseBundleSignCmd := distributionCommands.NewReleaseBundleSignCommand() - rtDetails, err := createArtifactoryDetailsByFlags(c) + dsDetails, err := createDistributionDetailsByFlags(c) if err != nil { return err } - releaseBundleSignCmd.SetServerDetails(rtDetails).SetReleaseBundleSignParams(params).SetDetailedSummary(c.Bool("detailed-summary")) + releaseBundleSignCmd.SetServerDetails(dsDetails).SetReleaseBundleSignParams(params).SetDetailedSummary(c.Bool("detailed-summary")) err = commands.Exec(releaseBundleSignCmd) if releaseBundleSignCmd.IsDetailedSummary() { if summary := releaseBundleSignCmd.GetSummary(); summary != nil { @@ -192,37 +192,21 @@ func releaseBundleSignCmd(c *cli.Context) error { } func releaseBundleDistributeCmd(c *cli.Context) error { - if c.NArg() != 2 { - return cliutils.WrongNumberOfArgumentsHandler(c) - } - if c.IsSet("max-wait-minutes") && !c.IsSet("sync") { - return cliutils.PrintHelpAndReturnError("The --max-wait-minutes option can't be used without --sync", c) - } - var distributionRules *spec.DistributionRules - if c.IsSet("dist-rules") { - if c.IsSet("site") || c.IsSet("city") || c.IsSet("country-code") { - return cliutils.PrintHelpAndReturnError("The --dist-rules option can't be used with --site, --city or --country-code", c) - } - var err error - distributionRules, err = spec.CreateDistributionRulesFromFile(c.String("dist-rules")) - if err != nil { - return err - } - } else { - distributionRules = createDefaultDistributionRules(c) + if err := distribution.ValidateReleaseBundleDistributeCmd(c); err != nil { + return err } - params := distributionServices.NewDistributeReleaseBundleParams(c.Args().Get(0), c.Args().Get(1)) - releaseBundleDistributeCmd := distributionCommands.NewReleaseBundleDistributeCommand() - rtDetails, err := createArtifactoryDetailsByFlags(c) + dsDetails, err := createDistributionDetailsByFlags(c) if err != nil { return err } - maxWaitMinutes, err := cliutils.GetIntFlagValue(c, "max-wait-minutes", 60) + distributionRules, maxWaitMinutes, params, err := distribution.InitReleaseBundleDistributeCmd(c) if err != nil { return err } - releaseBundleDistributeCmd.SetServerDetails(rtDetails). + + distributeCmd := distributionCommands.NewReleaseBundleDistributeV1Command() + distributeCmd.SetServerDetails(dsDetails). SetDistributeBundleParams(params). SetDistributionRules(distributionRules). SetDryRun(c.Bool("dry-run")). @@ -230,7 +214,7 @@ func releaseBundleDistributeCmd(c *cli.Context) error { SetMaxWaitMinutes(maxWaitMinutes). SetAutoCreateRepo(c.Bool("create-repo")) - return commands.Exec(releaseBundleDistributeCmd) + return commands.Exec(distributeCmd) } func releaseBundleDeleteCmd(c *cli.Context) error { @@ -248,7 +232,7 @@ func releaseBundleDeleteCmd(c *cli.Context) error { return err } } else { - distributionRules = createDefaultDistributionRules(c) + distributionRules = distribution.CreateDefaultDistributionRules(c) } params := distributionServices.NewDeleteReleaseBundleParams(c.Args().Get(0), c.Args().Get(1)) @@ -260,11 +244,11 @@ func releaseBundleDeleteCmd(c *cli.Context) error { } params.MaxWaitMinutes = maxWaitMinutes distributeBundleCmd := distributionCommands.NewReleaseBundleDeleteParams() - rtDetails, err := createArtifactoryDetailsByFlags(c) + dsDetails, err := createDistributionDetailsByFlags(c) if err != nil { return err } - distributeBundleCmd.SetQuiet(cliutils.GetQuietValue(c)).SetServerDetails(rtDetails).SetDistributeBundleParams(params).SetDistributionRules(distributionRules).SetDryRun(c.Bool("dry-run")) + distributeBundleCmd.SetQuiet(cliutils.GetQuietValue(c)).SetServerDetails(dsDetails).SetDistributeBundleParams(params).SetDistributionRules(distributionRules).SetDryRun(c.Bool("dry-run")) return commands.Exec(distributeBundleCmd) } @@ -283,16 +267,6 @@ func createDefaultReleaseBundleSpec(c *cli.Context) *spec.SpecFiles { BuildSpec() } -func createDefaultDistributionRules(c *cli.Context) *spec.DistributionRules { - return &spec.DistributionRules{ - DistributionRules: []spec.DistributionRule{{ - SiteName: c.String("site"), - CityName: c.String("city"), - CountryCodes: cliutils.GetStringsArrFlagValue(c, "country-codes"), - }}, - } -} - func createReleaseBundleCreateUpdateParams(c *cli.Context, bundleName, bundleVersion string) (distributionServicesUtils.ReleaseBundleParams, error) { releaseBundleParams := distributionServicesUtils.NewReleaseBundleParams(bundleName, bundleVersion) releaseBundleParams.SignImmediately = c.Bool("sign") @@ -336,13 +310,13 @@ func populateReleaseNotesSyntax(c *cli.Context) (distributionServicesUtils.Relea return distributionServicesUtils.PlainText, nil } -func createArtifactoryDetailsByFlags(c *cli.Context) (*coreConfig.ServerDetails, error) { - artDetails, err := cliutils.CreateServerDetailsWithConfigOffer(c, true, cliutils.Ds) +func createDistributionDetailsByFlags(c *cli.Context) (*coreConfig.ServerDetails, error) { + dsDetails, err := cliutils.CreateServerDetailsWithConfigOffer(c, true, cliutils.Ds) if err != nil { return nil, err } - if artDetails.DistributionUrl == "" { + if dsDetails.DistributionUrl == "" { return nil, errors.New("the --dist-url option is mandatory") } - return artDetails, nil + return dsDetails, nil } diff --git a/distribution_test.go b/distribution_test.go index 9c23f7304..62cbea526 100644 --- a/distribution_test.go +++ b/distribution_test.go @@ -14,6 +14,7 @@ import ( distributionServices "github.com/jfrog/jfrog-client-go/distribution/services" clientDistUtils "github.com/jfrog/jfrog-client-go/distribution/services/utils" clientUtils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/distribution" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/io/httputils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -566,7 +567,7 @@ func TestDistributeSyncTimeout(t *testing.T) { testServer, mockServerDetails, _ := coreTestUtils.CreateDsRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.RequestURI == "/api/v1/distribution/"+tests.BundleName+"/"+bundleVersion { w.WriteHeader(http.StatusOK) - content, err := json.Marshal(distributionServices.DistributionResponseBody{TrackerId: json.Number(trackerId)}) + content, err := json.Marshal(distribution.DistributionResponseBody{TrackerId: json.Number(trackerId)}) assert.NoError(t, err) _, err = w.Write(content) assert.NoError(t, err) diff --git a/docs/lifecycle/distribute/help.go b/docs/lifecycle/distribute/help.go new file mode 100644 index 000000000..8a3c2d47f --- /dev/null +++ b/docs/lifecycle/distribute/help.go @@ -0,0 +1,15 @@ +package distribute + +var Usage = []string{"rbd [command options] "} + +func GetDescription() string { + return "Distribute a release bundle." +} + +func GetArguments() string { + return ` release bundle name + Name of the Release Bundle to distribute. + + release bundle version + Version of the Release Bundle to distribute.` +} diff --git a/go.mod b/go.mod index 225ba7f81..737d29c7d 100644 --- a/go.mod +++ b/go.mod @@ -124,8 +124,8 @@ require ( // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230828134416-f0db33dd9344 -// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20230828140932-e44caa02288e +replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20230830130857-c5a2b11b52be // replace github.com/jfrog/gofrog => github.com/jfrog/gofrog v1.2.6-0.20230418122323-2bf299dd6d27 -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230803140217-0a5f43783ae8 +replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230830130057-df2d2a80b555 diff --git a/go.sum b/go.sum index 18ca77ea6..d00b21ab6 100644 --- a/go.sum +++ b/go.sum @@ -241,10 +241,10 @@ github.com/jfrog/build-info-go v1.9.9 h1:YMA9okHawBNL8SrCWzqULSf5M4W+YnWyUhmkWSj github.com/jfrog/build-info-go v1.9.9/go.mod h1:t31QRpH5xUJKw8XkQlAA+Aq7aanyS1rrzpcK8xSNVts= github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= -github.com/jfrog/jfrog-cli-core/v2 v2.41.4 h1:+V35NN+UaKl6ZFSjAyZFZ4VijCgsORnGsHug02DROdE= -github.com/jfrog/jfrog-cli-core/v2 v2.41.4/go.mod h1:Mi3WFUzG2CU6tlLpGsMNRaKkhH/tIMuci4tjnPZ9S3M= -github.com/jfrog/jfrog-client-go v1.31.6 h1:uWuyT4BDm9s5ES6oDTBny9Gl6yf8iKFjcbmHSHQZrDc= -github.com/jfrog/jfrog-client-go v1.31.6/go.mod h1:icb00ZJN/mMMNkQduHDkzpqsXH9Flwi3f3COYexq3Nc= +github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20230830130857-c5a2b11b52be h1:MjbSKQy937o0WFBKCXtvkX4EUSPCaA1LIhGISJUjYbU= +github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20230830130857-c5a2b11b52be/go.mod h1:kaFzB3X83/jdzMcuGOYOaqnlz5MVTnDYt/asrOsCv18= +github.com/jfrog/jfrog-client-go v1.28.1-0.20230830130057-df2d2a80b555 h1:yaF5J4LNk+ws5+j+BFMJ43NMqpKuCtF7dUfCeGLttl8= +github.com/jfrog/jfrog-client-go v1.28.1-0.20230830130057-df2d2a80b555/go.mod h1:icb00ZJN/mMMNkQduHDkzpqsXH9Flwi3f3COYexq3Nc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jszwec/csvutil v1.8.0 h1:G7vS2LGdpZZDH1HmHeNbxOaJ/ZnJlpwGFvOkTkJzzNk= diff --git a/lifecycle/cli.go b/lifecycle/cli.go index 8caa332d2..647963c10 100644 --- a/lifecycle/cli.go +++ b/lifecycle/cli.go @@ -8,8 +8,10 @@ import ( coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli/docs/common" rbCreate "github.com/jfrog/jfrog-cli/docs/lifecycle/create" + rbDistribute "github.com/jfrog/jfrog-cli/docs/lifecycle/distribute" rbPromote "github.com/jfrog/jfrog-cli/docs/lifecycle/promote" "github.com/jfrog/jfrog-cli/utils/cliutils" + "github.com/jfrog/jfrog-cli/utils/distribution" "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/urfave/cli" @@ -43,6 +45,19 @@ func GetCommands() []cli.Command { Category: lcCategory, Action: promote, }, + { + Name: "release-bundle-distribute", + Aliases: []string{"rbd"}, + Flags: cliutils.GetCommandFlags(cliutils.ReleaseBundleDistribute), + Usage: rbDistribute.GetDescription(), + HelpName: coreCommon.CreateUsage("rbd", rbDistribute.GetDescription(), rbDistribute.Usage), + UsageText: rbDistribute.GetArguments(), + ArgsUsage: common.CreateEnvVars(), + BashComplete: coreCommon.CreateBashCompletionFunc(), + Category: lcCategory, + Hidden: true, + Action: distribute, + }, }) } @@ -77,7 +92,7 @@ func create(c *cli.Context) (err error) { return } - createCmd := lifecycle.NewReleaseBundleCreate().SetServerDetails(lcDetails).SetReleaseBundleName(c.Args().Get(0)). + createCmd := lifecycle.NewReleaseBundleCreateCommand().SetServerDetails(lcDetails).SetReleaseBundleName(c.Args().Get(0)). SetReleaseBundleVersion(c.Args().Get(1)).SetSigningKeyName(c.String(cliutils.SigningKey)).SetSync(c.Bool(cliutils.Sync)). SetReleaseBundleProject(cliutils.GetProject(c)).SetBuildsSpecPath(c.String(cliutils.Builds)). SetReleaseBundlesSpecPath(c.String(cliutils.ReleaseBundles)) @@ -102,12 +117,51 @@ func promote(c *cli.Context) error { return err } - createCmd := lifecycle.NewReleaseBundlePromote().SetServerDetails(lcDetails).SetReleaseBundleName(c.Args().Get(0)). + createCmd := lifecycle.NewReleaseBundlePromoteCommand().SetServerDetails(lcDetails).SetReleaseBundleName(c.Args().Get(0)). SetReleaseBundleVersion(c.Args().Get(1)).SetEnvironment(c.Args().Get(2)).SetSigningKeyName(c.String(cliutils.SigningKey)). SetSync(c.Bool(cliutils.Sync)).SetReleaseBundleProject(cliutils.GetProject(c)).SetOverwrite(c.Bool(cliutils.Overwrite)) return commands.Exec(createCmd) } +func distribute(c *cli.Context) error { + if err := validateDistributeCommand(c); err != nil { + return err + } + + lcDetails, err := createLifecycleDetailsByFlags(c) + if err != nil { + return err + } + distributionRules, _, params, err := distribution.InitReleaseBundleDistributeCmd(c) + if err != nil { + return err + } + + distributeCmd := lifecycle.NewReleaseBundleDistributeCommand() + distributeCmd.SetServerDetails(lcDetails). + SetDistributeBundleParams(params). + SetDistributionRules(distributionRules). + SetDryRun(c.Bool("dry-run")). + SetAutoCreateRepo(c.Bool(cliutils.CreateRepo)). + SetPathMappingPattern(c.String(cliutils.PathMappingPattern)). + SetPathMappingTarget(c.String(cliutils.PathMappingTarget)) + return commands.Exec(distributeCmd) +} + +func validateDistributeCommand(c *cli.Context) error { + if err := distribution.ValidateReleaseBundleDistributeCmd(c); err != nil { + return err + } + + mappingPatternProvided := c.IsSet(cliutils.PathMappingPattern) + mappingTargetProvided := c.IsSet(cliutils.PathMappingTarget) + if (mappingPatternProvided && !mappingTargetProvided) || + (!mappingPatternProvided && mappingTargetProvided) { + return errorutils.CheckErrorf("the options --%s and --%s must be provided together", cliutils.PathMappingPattern, cliutils.PathMappingTarget) + } + return nil +} + func assertSigningKeyProvided(c *cli.Context) error { if c.String(cliutils.SigningKey) == "" { return errorutils.CheckErrorf("the --%s option is mandatory", cliutils.SigningKey) diff --git a/lifecycle_test.go b/lifecycle_test.go index 24980c734..c81e2beba 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -23,14 +23,14 @@ import ( ) const ( - rbMinVersion = "7.45.0" - gpgKeyPairName = "lc-tests-key-pair" - lcTestdataPath = "lifecycle" - releaseBundlesSpec = "release-bundles-spec.json" - buildsSpec12 = "builds-spec-1-2.json" - buildsSpec3 = "builds-spec-3.json" - prodEnvironment = "PROD" - number1, number2, number3 = "111", "222", "333" + artifactoryLifecycleMinVersion = "7.65.0" + gpgKeyPairName = "lc-tests-key-pair" + lcTestdataPath = "lifecycle" + releaseBundlesSpec = "release-bundles-spec.json" + buildsSpec12 = "builds-spec-1-2.json" + buildsSpec3 = "builds-spec-3.json" + prodEnvironment = "PROD" + number1, number2, number3 = "111", "222", "333" ) var ( @@ -47,11 +47,11 @@ func TestLifecycle(t *testing.T) { deleteBuilds := uploadBuilds(t) defer deleteBuilds() - // Create release bundles from builds synchronously. + // Create release bundle from builds synchronously. createRb(t, buildsSpec12, cliutils.Builds, tests.LcRbName1, number1, true) defer deleteReleaseBundle(t, lcManager, tests.LcRbName1, number1) - // Create release bundles from builds asynchronously and assert status. + // Create release bundle from builds asynchronously and assert status. createRb(t, buildsSpec3, cliutils.Builds, tests.LcRbName2, number2, false) defer deleteReleaseBundle(t, lcManager, tests.LcRbName2, number2) assertStatusCompleted(t, lcManager, tests.LcRbName2, number2, "") @@ -67,6 +67,11 @@ func TestLifecycle(t *testing.T) { searchSpec, err := tests.CreateSpec(tests.SearchAllProdRepo) assert.NoError(t, err) inttestutils.VerifyExistInArtifactory(tests.GetExpectedLifecycleArtifacts(), searchSpec, serverDetails, t) + + distributeRb(t) + // Verify the artifacts were distributed correctly by the provided path mappings. + expected := append(tests.GetExpectedLifecycleArtifacts(), tests.GetExpectedLifecycleMappingArtifacts()...) + inttestutils.VerifyExistInArtifactory(expected, searchSpec, serverDetails, t) } func uploadBuilds(t *testing.T) func() { @@ -80,13 +85,13 @@ func uploadBuilds(t *testing.T) func() { } } -func createRb(t *testing.T, specName, sourceOption, buildName, buildNumber string, sync bool) { +func createRb(t *testing.T, specName, sourceOption, rbName, rbVersion string, sync bool) { specFile, err := getSpecFile(specName) assert.NoError(t, err) argsAndOptions := []string{ "rbc", - buildName, - buildNumber, + rbName, + rbVersion, getOption(sourceOption, specFile), getOption(cliutils.SigningKey, gpgKeyPairName), } @@ -97,6 +102,16 @@ func createRb(t *testing.T, specName, sourceOption, buildName, buildNumber strin assert.NoError(t, lcCli.Exec(argsAndOptions...)) } +func distributeRb(t *testing.T) { + distributionRulesPath := filepath.Join(tests.GetTestResourcesPath(), "distribution", tests.DistributionRules) + assert.NoError(t, lcCli.Exec( + "rbd", tests.LcRbName3, number3, + getOption(cliutils.DistRules, distributionRulesPath), + getOption(cliutils.PathMappingPattern, tests.RtProdRepo+"/(*)"), + getOption(cliutils.PathMappingTarget, tests.RtProdRepo+"/target/{1}"), + )) +} + func getOption(option, value string) string { return fmt.Sprintf("--%s=%s", option, value) } @@ -183,7 +198,7 @@ func initLifecycleTest(t *testing.T) { if !*tests.TestLifecycle { t.Skip("Skipping lifecycle test. To run release bundle test add the '-test.lc=true' option.") } - validateArtifactoryVersion(t, rbMinVersion) + validateArtifactoryVersion(t, artifactoryLifecycleMinVersion) if !isLifecycleSupported(t) { t.Skip("Skipping lifecycle test because the functionality is not enabled on the provided JPD.") diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 625841bc1..24bc36d46 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -131,8 +131,9 @@ const ( TransferInstall = "transfer-plugin-install" // Lifecycle commands keys - ReleaseBundleCreate = "release-bundle-create" - ReleaseBundlePromote = "release-bundle-promote" + ReleaseBundleCreate = "release-bundle-create" + ReleaseBundlePromote = "release-bundle-promote" + ReleaseBundleDistribute = "release-bundle-distribute" // *** Artifactory Commands' flags *** // Base flags @@ -424,14 +425,16 @@ const ( desc = "desc" releaseNotesPath = "release-notes-path" releaseNotesSyntax = "release-notes-syntax" - distRules = "dist-rules" - site = "site" - city = "city" - countryCodes = "country-codes" - sync = "sync" - maxWaitMinutes = "max-wait-minutes" deleteFromDist = "delete-from-dist" - createRepo = "create-repo" + + // Common release-bundle-* v1&v2 flags + DistRules = "dist-rules" + site = "site" + city = "city" + countryCodes = "country-codes" + sync = "sync" + maxWaitMinutes = "max-wait-minutes" + CreateRepo = "create-repo" // *** Xray Commands' flags *** // Base flags @@ -542,17 +545,22 @@ const ( InstallPluginHomeDir = "home-dir" // Unique lifecycle flags - lifecyclePrefix = "lc-" - lcUrl = lifecyclePrefix + url - lcSync = lifecyclePrefix + Sync - lcProject = lifecyclePrefix + project - Builds = "builds" - lcBuilds = lifecyclePrefix + Builds - ReleaseBundles = "release-bundles" - lcReleaseBundles = lifecyclePrefix + ReleaseBundles - SigningKey = "signing-key" - lcSigningKey = lifecyclePrefix + SigningKey - lcOverwrite = lifecyclePrefix + Overwrite + lifecyclePrefix = "lc-" + lcUrl = lifecyclePrefix + url + lcSync = lifecyclePrefix + Sync + lcProject = lifecyclePrefix + project + Builds = "builds" + lcBuilds = lifecyclePrefix + Builds + ReleaseBundles = "release-bundles" + lcReleaseBundles = lifecyclePrefix + ReleaseBundles + SigningKey = "signing-key" + lcSigningKey = lifecyclePrefix + SigningKey + lcOverwrite = lifecyclePrefix + Overwrite + PathMappingPattern = "mapping-pattern" + lcPathMappingPattern = lifecyclePrefix + PathMappingPattern + PathMappingTarget = "mapping-target" + lcPathMappingTarget = lifecyclePrefix + PathMappingTarget + lcDryRun = lifecyclePrefix + dryRun ) var flagsMap = map[string]cli.Flag{ @@ -1237,9 +1245,9 @@ var flagsMap = map[string]cli.Flag{ Name: repo, Usage: "[Optional] A repository name at source Artifactory to store release bundle artifacts in. If not provided, Artifactory will use the default one.` `", }, - distRules: cli.StringFlag{ - Name: distRules, - Usage: "Path to distribution rules.` `", + DistRules: cli.StringFlag{ + Name: DistRules, + Usage: "[Optional] Path to distribution rules.` `", }, site: cli.StringFlag{ Name: site, @@ -1509,8 +1517,8 @@ var flagsMap = map[string]cli.Flag{ Name: "format", Hidden: true, }, - createRepo: cli.BoolFlag{ - Name: createRepo, + CreateRepo: cli.BoolFlag{ + Name: CreateRepo, Usage: "[Default: false] Set to true to create the repository on the edge if it does not exist.` `", }, Filestore: cli.BoolFlag{ @@ -1613,6 +1621,19 @@ var flagsMap = map[string]cli.Flag{ Name: Overwrite, Usage: "[Default: false] Set to true to replace artifacts with the same name but a different checksum if such already exist at the promotion targets. By default, the promotion is stopped in a case of such conflict.` `", }, + lcPathMappingPattern: cli.StringFlag{ + Name: PathMappingPattern, + Usage: "[Optional] Specify along with '" + PathMappingTarget + "' to distribute artifacts to a different path on the edge node. You can use wildcards to specify multiple artifacts.` `", + }, + lcPathMappingTarget: cli.StringFlag{ + Name: PathMappingTarget, + Usage: "[Optional] The target path for distributed artifacts on the edge node. If not specified, the artifacts will have the same path and name on the edge node, as on the source Artifactory server. " + + "For flexibility in specifying the distribution path, you can include placeholders in the form of {1}, {2} which are replaced by corresponding tokens in the pattern path that are enclosed in parenthesis.` `", + }, + lcDryRun: cli.BoolFlag{ + Name: dryRun, + Usage: "[Default: false] Set to true to only simulate the distribution of the release bundle.` `", + }, } var commandFlags = map[string][]string{ @@ -1836,11 +1857,11 @@ var commandFlags = map[string][]string{ InsecureTls, rbDetailedSummary, }, ReleaseBundleV1Distribute: { - distUrl, user, password, accessToken, serverId, rbDryRun, distRules, - site, city, countryCodes, sync, maxWaitMinutes, InsecureTls, createRepo, + distUrl, user, password, accessToken, serverId, rbDryRun, DistRules, + site, city, countryCodes, sync, maxWaitMinutes, InsecureTls, CreateRepo, }, ReleaseBundleV1Delete: { - distUrl, user, password, accessToken, serverId, rbDryRun, distRules, + distUrl, user, password, accessToken, serverId, rbDryRun, DistRules, site, city, countryCodes, sync, maxWaitMinutes, InsecureTls, deleteFromDist, deleteQuiet, }, TemplateConsumer: { @@ -1897,6 +1918,10 @@ var commandFlags = map[string][]string{ ReleaseBundlePromote: { lcUrl, user, password, accessToken, serverId, lcSigningKey, lcSync, lcProject, lcOverwrite, }, + ReleaseBundleDistribute: { + lcUrl, user, password, accessToken, serverId, lcDryRun, DistRules, site, city, countryCodes, + InsecureTls, CreateRepo, lcPathMappingPattern, lcPathMappingTarget, + }, // Xray's commands OfflineUpdate: { licenseId, from, to, Version, target, Stream, Periodic, diff --git a/utils/distribution/distribute.go b/utils/distribution/distribute.go new file mode 100644 index 000000000..bbc82bae9 --- /dev/null +++ b/utils/distribution/distribute.go @@ -0,0 +1,52 @@ +package distribution + +import ( + "github.com/jfrog/jfrog-cli-core/v2/common/spec" + "github.com/jfrog/jfrog-cli/utils/cliutils" + distributionUtils "github.com/jfrog/jfrog-client-go/utils/distribution" + "github.com/urfave/cli" +) + +func CreateDefaultDistributionRules(c *cli.Context) *spec.DistributionRules { + return &spec.DistributionRules{ + DistributionRules: []spec.DistributionRule{{ + SiteName: c.String("site"), + CityName: c.String("city"), + CountryCodes: cliutils.GetStringsArrFlagValue(c, "country-codes"), + }}, + } +} + +func ValidateReleaseBundleDistributeCmd(c *cli.Context) error { + if c.NArg() != 2 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + if c.IsSet("max-wait-minutes") && !c.IsSet("sync") { + return cliutils.PrintHelpAndReturnError("The --max-wait-minutes option can't be used without --sync", c) + } + + if c.IsSet("dist-rules") && (c.IsSet("site") || c.IsSet("city") || c.IsSet("country-code")) { + return cliutils.PrintHelpAndReturnError("The --dist-rules option can't be used with --site, --city or --country-code", c) + } + + return nil +} + +func InitReleaseBundleDistributeCmd(c *cli.Context) (distributionRules *spec.DistributionRules, maxWaitMinutes int, params distributionUtils.DistributionParams, err error) { + if c.IsSet("dist-rules") { + distributionRules, err = spec.CreateDistributionRulesFromFile(c.String("dist-rules")) + if err != nil { + return + } + } else { + distributionRules = CreateDefaultDistributionRules(c) + } + + maxWaitMinutes, err = cliutils.GetIntFlagValue(c, "max-wait-minutes", 60) + if err != nil { + return + } + + params = distributionUtils.NewDistributeReleaseBundleParams(c.Args().Get(0), c.Args().Get(1)) + return +} diff --git a/utils/tests/consts.go b/utils/tests/consts.go index 8373be6d7..f415aa1f0 100644 --- a/utils/tests/consts.go +++ b/utils/tests/consts.go @@ -2099,6 +2099,20 @@ func GetExpectedLifecycleArtifacts() []string { } } +func GetExpectedLifecycleMappingArtifacts() []string { + return []string{ + RtProdRepo + "/target/a1.in", + RtProdRepo + "/target/a2.in", + RtProdRepo + "/target/a3.in", + RtProdRepo + "/target/b1.in", + RtProdRepo + "/target/b2.in", + RtProdRepo + "/target/b3.in", + RtProdRepo + "/target/c1.in", + RtProdRepo + "/target/c2.in", + RtProdRepo + "/target/c3.in", + } +} + func GetGoPublishWithExclusionsExpectedRepoGo() []string { var expected = []string{ GoRepo + "/github.com/jfrog/dependency/@v/v1.1.1.info", diff --git a/utils/tests/utils.go b/utils/tests/utils.go index d5ab82381..19540b144 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -106,7 +106,7 @@ func init() { TestXray = flag.Bool("test.xray", false, "Test Xray") TestAccess = flag.Bool("test.access", false, "Test Access") TestTransfer = flag.Bool("test.transfer", false, "Test files transfer") - TestLifecycle = flag.Bool("test.lc", false, "Test lifecycle") + TestLifecycle = flag.Bool("test.lifecycle", false, "Test lifecycle") ContainerRegistry = flag.String("test.containerRegistry", "localhost:8082", "Container registry") HideUnitTestLog = flag.Bool("test.hideUnitTestLog", false, "Hide unit tests logs and print it in a file") InstallDataTransferPlugin = flag.Bool("test.installDataTransferPlugin", false, "Install data-transfer plugin on the source Artifactory server")