Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
*.iml
bin
.tools

# Test reports
e2e-tests-report.xml
4 changes: 4 additions & 0 deletions apptrust/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const (
DeletePropertiesFlag = "delete-properties"
IncludeFilterFlag = "include-filter"
ExcludeFilterFlag = "exclude-filter"
PathMappingFlag = "path-mapping"
)

// Flag keys mapped to their corresponding components.Flag definition.
Expand Down Expand Up @@ -103,6 +104,7 @@ var flagsMap = map[string]components.Flag{
SourceTypeArtifactsFlag: components.NewStringFlag(SourceTypeArtifactsFlag, "List of semicolon-separated (;) artifacts in the form of 'path=repo/path/to/artifact1[, sha256=hash1]; path=repo/path/to/artifact2[, sha256=hash2]' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }),
PropertiesFlag: components.NewStringFlag(PropertiesFlag, "Sets or updates custom properties for the application version in format 'key1=value1[,value2,...];key2=value3[,value4,...]'", func(f *components.StringFlag) { f.Mandatory = false }),
DeletePropertiesFlag: components.NewStringFlag(DeletePropertiesFlag, "Remove a property key and all its values", func(f *components.StringFlag) { f.Mandatory = false }),
PathMappingFlag: components.NewStringFlag(PathMappingFlag, "List of semicolon-separated (;) path mapping rules in the form of 'input=(.*), output=stable-release/$1[, package-type=.*]; input=(.*\\.jar), output=jars/$1, package-type=maven'. Note: quote the value to prevent shell expansion of $1.", func(f *components.StringFlag) { f.Mandatory = false }),
}

var commandFlags = map[string][]string{
Expand Down Expand Up @@ -137,6 +139,7 @@ var commandFlags = map[string][]string{
IncludeReposFlag,
PropsFlag,
OverwriteStrategyFlag,
PathMappingFlag,
},
VersionRelease: {
url,
Expand All @@ -149,6 +152,7 @@ var commandFlags = map[string][]string{
IncludeReposFlag,
PropsFlag,
OverwriteStrategyFlag,
PathMappingFlag,
},
VersionDelete: {
url,
Expand Down
6 changes: 6 additions & 0 deletions apptrust/commands/version/promote_app_version_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ func (pv *promoteAppVersionCommand) buildRequestPayload(ctx *components.Context)
return nil, err
}

modifications, err := ParsePathMappings(ctx)
if err != nil {
return nil, err
}

return &model.PromoteAppVersionRequest{
Stage: stage,
CommonPromoteAppVersion: model.CommonPromoteAppVersion{
Expand All @@ -117,6 +122,7 @@ func (pv *promoteAppVersionCommand) buildRequestPayload(ctx *components.Context)
ExcludedRepositoryKeys: excludedRepos,
ArtifactAdditionalProperties: artifactProps,
OverwriteStrategy: overwriteStrategy,
Modifications: modifications,
},
}, nil
}
Expand Down
6 changes: 6 additions & 0 deletions apptrust/commands/version/release_app_version_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,18 @@ func (rv *releaseAppVersionCommand) buildRequestPayload(ctx *components.Context)
return nil, err
}

modifications, err := ParsePathMappings(ctx)
if err != nil {
return nil, err
}

return model.NewReleaseAppVersionRequest(
promotionType,
includedRepos,
excludedRepos,
artifactProps,
overwriteStrategy,
modifications,
), nil
}

Expand Down
2 changes: 2 additions & 0 deletions apptrust/commands/version/release_app_version_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestReleaseAppVersionCommand_Run(t *testing.T) {
nil, // excludedRepos
nil, // artifactProps
tt.overwriteStrategy,
nil, // modifications
)

mockVersionService := mockversions.NewMockVersionService(ctrl)
Expand Down Expand Up @@ -91,6 +92,7 @@ func TestReleaseAppVersionCommand_Run_Error(t *testing.T) {
nil, // excludedRepos
nil, // artifactProps
"", // overwriteStrategy
nil, // modifications
)
expectedError := errors.New("service error occurred")

Expand Down
52 changes: 52 additions & 0 deletions apptrust/commands/version/version_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,55 @@ func ParseOverwriteStrategy(ctx *components.Context) (string, error) {
// Convert to uppercase for API request
return strings.ToUpper(validatedStrategy), nil
}

// ParsePathMappings extracts path mapping rules from the --path-mapping flag.
// Format: "input=(.*), output=stable-release/$1[, package-type=.*]; input=(...), output=..."
// Returns nil if flag is not provided.
func ParsePathMappings(ctx *components.Context) (*model.PromotionModifications, error) {
const (
inputField = "input"
outputField = "output"
packageTypeField = "package-type"
)

flagValue := ctx.GetStringFlagValue(commands.PathMappingFlag)
if flagValue == "" {
return nil, nil
}

entries := utils.ParseSliceFlag(flagValue)
var mappings []model.PromotionPathMapping

for i, entry := range entries {
if entry == "" {
return nil, errorutils.CheckErrorf("--%s entry %d is empty", commands.PathMappingFlag, i+1)
}

entryMap, err := utils.ParseKeyValueString(entry, ",")
if err != nil {
return nil, errorutils.CheckErrorf("--%s entry %d: %s", commands.PathMappingFlag, i+1, err.Error())
}

input, hasInput := entryMap[inputField]
output, hasOutput := entryMap[outputField]

if !hasInput || input == "" {
return nil, errorutils.CheckErrorf("--%s entry %d: '%s' is required", commands.PathMappingFlag, i+1, inputField)
}
if !hasOutput || output == "" {
return nil, errorutils.CheckErrorf("--%s entry %d: '%s' is required", commands.PathMappingFlag, i+1, outputField)
}

mapping := model.PromotionPathMapping{
Input: input,
Output: output,
}
if pt, ok := entryMap[packageTypeField]; ok {
mapping.PackageType = pt
}

mappings = append(mappings, mapping)
}

return &model.PromotionModifications{Mappings: mappings}, nil
}
98 changes: 98 additions & 0 deletions apptrust/commands/version/version_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/jfrog/jfrog-cli-application/apptrust/model"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseOverwriteStrategy(t *testing.T) {
Expand Down Expand Up @@ -162,3 +163,100 @@ func TestBuildPromotionParams(t *testing.T) {
})
}
}

func TestParsePathMappings(t *testing.T) {
tests := []struct {
name string
flagValue string
expected *model.PromotionModifications
expectError bool
errContains string
}{
{
name: "no flag - returns nil",
expected: nil,
},
{
name: "single mapping without package type",
flagValue: "input=(.*), output=stable-release/$1",
expected: &model.PromotionModifications{
Mappings: []model.PromotionPathMapping{
{Input: "(.*)", Output: "stable-release/$1"},
},
},
},
{
name: "single mapping with package type",
flagValue: "input=(.*), output=stable-release/$1, package-type=.*",
expected: &model.PromotionModifications{
Mappings: []model.PromotionPathMapping{
{PackageType: ".*", Input: "(.*)", Output: "stable-release/$1"},
},
},
},
{
name: "multiple mappings",
flagValue: "input=(.*), output=release/$1, package-type=.*; input=(.*\\.jar), output=jars/$1, package-type=maven",
expected: &model.PromotionModifications{
Mappings: []model.PromotionPathMapping{
{PackageType: ".*", Input: "(.*)", Output: "release/$1"},
{PackageType: "maven", Input: "(.*\\.jar)", Output: "jars/$1"},
},
},
},
{
name: "mapping without package-type field",
flagValue: "input=(.*), output=release/$1; input=(.*\\.jar), output=jars/$1",
expected: &model.PromotionModifications{
Mappings: []model.PromotionPathMapping{
{Input: "(.*)", Output: "release/$1"},
{Input: "(.*\\.jar)", Output: "jars/$1"},
},
},
},
{
name: "missing input field - error",
flagValue: "output=target/$1",
expectError: true,
errContains: "'input' is required",
},
{
name: "missing output field - error",
flagValue: "input=(.*)",
expectError: true,
errContains: "'output' is required",
},
{
name: "empty entry from trailing semicolon - error",
flagValue: "input=(.*), output=release/$1;",
expectError: true,
errContains: "entry 2 is empty",
},
{
name: "invalid key-value format - error",
flagValue: "not-a-valid-format",
expectError: true,
errContains: "entry 1",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := &components.Context{}
if tt.flagValue != "" {
ctx.AddStringFlag(commands.PathMappingFlag, tt.flagValue)
}

result, err := ParsePathMappings(ctx)

if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
return
}

require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
21 changes: 16 additions & 5 deletions apptrust/model/promote_app_version_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,23 @@ var OverwriteStrategyValues = []string{
OverwriteStrategyAll,
}

type PromotionPathMapping struct {
PackageType string `json:"package_type,omitempty"`
Input string `json:"input"`
Output string `json:"output"`
}

type PromotionModifications struct {
Mappings []PromotionPathMapping `json:"mappings"`
}

type CommonPromoteAppVersion struct {
PromotionType string `json:"promotion_type,omitempty"`
IncludedRepositoryKeys []string `json:"included_repository_keys,omitempty"`
ExcludedRepositoryKeys []string `json:"excluded_repository_keys,omitempty"`
ArtifactAdditionalProperties []ArtifactProperty `json:"artifact_additional_properties,omitempty"`
OverwriteStrategy string `json:"overwrite_strategy,omitempty"`
PromotionType string `json:"promotion_type,omitempty"`
IncludedRepositoryKeys []string `json:"included_repository_keys,omitempty"`
ExcludedRepositoryKeys []string `json:"excluded_repository_keys,omitempty"`
ArtifactAdditionalProperties []ArtifactProperty `json:"artifact_additional_properties,omitempty"`
OverwriteStrategy string `json:"overwrite_strategy,omitempty"`
Modifications *PromotionModifications `json:"modifications,omitempty"`
}

type ArtifactProperty struct {
Expand Down
2 changes: 2 additions & 0 deletions apptrust/model/release_app_version_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func NewReleaseAppVersionRequest(
excludedRepositoryKeys []string,
artifactProperties []ArtifactProperty,
overwriteStrategy string,
modifications *PromotionModifications,
) *ReleaseAppVersionRequest {
return &ReleaseAppVersionRequest{
CommonPromoteAppVersion: CommonPromoteAppVersion{
Expand All @@ -23,6 +24,7 @@ func NewReleaseAppVersionRequest(
ExcludedRepositoryKeys: excludedRepositoryKeys,
ArtifactAdditionalProperties: artifactProperties,
OverwriteStrategy: overwriteStrategy,
Modifications: modifications,
},
}
}
4 changes: 4 additions & 0 deletions apptrust/service/versions/version_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ func TestReleaseAppVersion(t *testing.T) {
[]string{"repo3"},
[]model.ArtifactProperty{{Key: "key1", Values: []string{"value1"}}},
"",
nil,
),
sync: true,
expectedEndpoint: "/v1/applications/test-app/versions/1.0.0/release",
Expand All @@ -270,6 +271,7 @@ func TestReleaseAppVersion(t *testing.T) {
[]string{"repo3"},
[]model.ArtifactProperty{{Key: "key1", Values: []string{"value1"}}},
"",
nil,
),
sync: false,
expectedEndpoint: "/v1/applications/test-app/versions/1.0.0/release",
Expand All @@ -288,6 +290,7 @@ func TestReleaseAppVersion(t *testing.T) {
nil,
nil,
"",
nil,
),
sync: true,
expectedEndpoint: "/v1/applications/test-app/versions/1.0.0/release",
Expand All @@ -306,6 +309,7 @@ func TestReleaseAppVersion(t *testing.T) {
nil,
nil,
"",
nil,
),
sync: false,
expectedEndpoint: "/v1/applications/test-app/versions/1.0.0/release",
Expand Down
54 changes: 54 additions & 0 deletions e2e/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,60 @@ func TestPromoteVersion(t *testing.T) {
assert.Equal(t, targetStage, versionContent.CurrentStage)
}

func TestPromoteVersion_WithPathMappings(t *testing.T) {
t.Skip("Skipping until path-mapping backend is deployed")
// Prepare
appKey := utils.GenerateUniqueKey("app-promote-mapping")
utils.CreateBasicApplication(t, appKey)
defer utils.DeleteApplication(t, appKey)

testPackage := utils.GetTestPackage(t)
version := "1.0.60"

packageFlag := fmt.Sprintf("--source-type-packages=type=%s, name=%s, version=%s, repo-key=%s",
testPackage.PackageType, testPackage.PackageName, testPackage.PackageVersion, testPackage.RepoKey)
err := utils.AppTrustCli.Exec("version-create", appKey, version, packageFlag)
require.NoError(t, err)
defer utils.DeleteApplicationVersion(t, appKey, version)

// Execute - promote with path mapping flags
targetStage := "DEV"
err = utils.AppTrustCli.Exec("version-promote", appKey, version, targetStage,
`--path-mapping=input=(.*), output=promoted/$1, package-type=.*`)
require.NoError(t, err)

// Assert - promotion succeeded with mappings applied
versionContent, statusCode, err := utils.GetApplicationVersion(appKey, version)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, statusCode)
require.NotNil(t, versionContent)
assert.Equal(t, appKey, versionContent.ApplicationKey)
assert.Equal(t, version, versionContent.Version)
assert.Equal(t, targetStage, versionContent.CurrentStage)
}

func TestPromoteVersion_WithPathMappings_InvalidRegex(t *testing.T) {
t.Skip("Skipping until path-mapping backend is deployed")
// Prepare
appKey := utils.GenerateUniqueKey("app-promote-map-err")
utils.CreateBasicApplication(t, appKey)
defer utils.DeleteApplication(t, appKey)

testPackage := utils.GetTestPackage(t)
version := "1.0.61"

packageFlag := fmt.Sprintf("--source-type-packages=type=%s, name=%s, version=%s, repo-key=%s",
testPackage.PackageType, testPackage.PackageName, testPackage.PackageVersion, testPackage.RepoKey)
err := utils.AppTrustCli.Exec("version-create", appKey, version, packageFlag)
require.NoError(t, err)
defer utils.DeleteApplicationVersion(t, appKey, version)

targetStage := "DEV"
err = utils.AppTrustCli.Exec("version-promote", appKey, version, targetStage,
`--path-mapping=input=[unclosed, output=target/$1`)
assert.Error(t, err)
}

func TestReleaseVersion(t *testing.T) {
// Prepare
appKey := utils.GenerateUniqueKey("app-version-release")
Expand Down
Loading