From 929d0234e76685978f55d8fb63f77a4f632f944a Mon Sep 17 00:00:00 2001 From: Asaf Gabai Date: Wed, 4 Mar 2026 17:38:28 +0200 Subject: [PATCH 1/5] Support multi-value labels --- .../commands/application/application_utils.go | 4 +-- apptrust/commands/utils/utils.go | 8 +++--- apptrust/model/app_descriptor.go | 26 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apptrust/commands/application/application_utils.go b/apptrust/commands/application/application_utils.go index 07e4724..2481a07 100644 --- a/apptrust/commands/application/application_utils.go +++ b/apptrust/commands/application/application_utils.go @@ -44,11 +44,11 @@ func populateApplicationFromFlags(ctx *components.Context, descriptor *model.App } if ctx.IsFlagSet(commands.LabelsFlag) { - labelsMap, err := utils.ParseMapFlag(ctx.GetStringFlagValue(commands.LabelsFlag)) + labels, err := utils.ParseLabelKeyValuePairs(ctx.GetStringFlagValue(commands.LabelsFlag)) if err != nil { return fmt.Errorf("failed to parse --%s: %w", commands.LabelsFlag, err) } - descriptor.Labels = &labelsMap + descriptor.Labels = &labels } // Only set LabelUpdates if at least one of add-labels or remove-labels flags is set diff --git a/apptrust/commands/utils/utils.go b/apptrust/commands/utils/utils.go index a1cfe15..1940ef9 100644 --- a/apptrust/commands/utils/utils.go +++ b/apptrust/commands/utils/utils.go @@ -168,12 +168,12 @@ func ParseListPropertiesFlag(propertiesStr string) (map[string][]string, error) return result, nil } -func ParseLabelKeyValuePairs(flagValue string) ([]model.LabelKeyValue, error) { +func ParseLabelKeyValuePairs(flagValue string) ([]model.LabelEntry, error) { if flagValue == "" { - return []model.LabelKeyValue{}, nil + return []model.LabelEntry{}, nil } - var result []model.LabelKeyValue + var result []model.LabelEntry pairs := strings.Split(flagValue, ";") for _, pair := range pairs { trimmedPair := strings.TrimSpace(pair) @@ -184,7 +184,7 @@ func ParseLabelKeyValuePairs(flagValue string) ([]model.LabelKeyValue, error) { if len(keyValue) != 2 { return nil, errorutils.CheckErrorf("invalid key-value pair: '%s' (expected format key=value)", pair) } - result = append(result, model.LabelKeyValue{ + result = append(result, model.LabelEntry{ Key: strings.TrimSpace(keyValue[0]), Value: strings.TrimSpace(keyValue[1]), }) diff --git a/apptrust/model/app_descriptor.go b/apptrust/model/app_descriptor.go index 3d28d1e..8948072 100644 --- a/apptrust/model/app_descriptor.go +++ b/apptrust/model/app_descriptor.go @@ -30,25 +30,25 @@ var ( } ) -type LabelKeyValue struct { +type LabelEntry struct { Key string `json:"key"` Value string `json:"value"` } type LabelUpdates struct { - Remove []LabelKeyValue `json:"remove,omitempty"` - Add []LabelKeyValue `json:"add,omitempty"` + Remove []LabelEntry `json:"remove,omitempty"` + Add []LabelEntry `json:"add,omitempty"` } type AppDescriptor struct { - ApplicationKey string `json:"application_key"` - ApplicationName string `json:"application_name,omitempty"` - ProjectKey string `json:"project_key,omitempty"` - Description *string `json:"description,omitempty"` - MaturityLevel *string `json:"maturity_level,omitempty"` - BusinessCriticality *string `json:"criticality,omitempty"` - Labels *map[string]string `json:"labels,omitempty"` - LabelUpdates *LabelUpdates `json:"label_updates,omitempty"` - UserOwners *[]string `json:"user_owners,omitempty"` - GroupOwners *[]string `json:"group_owners,omitempty"` + ApplicationKey string `json:"application_key"` + ApplicationName string `json:"application_name,omitempty"` + ProjectKey string `json:"project_key,omitempty"` + Description *string `json:"description,omitempty"` + MaturityLevel *string `json:"maturity_level,omitempty"` + BusinessCriticality *string `json:"criticality,omitempty"` + Labels *[]LabelEntry `json:"labels,omitempty"` + LabelUpdates *LabelUpdates `json:"label_updates,omitempty"` + UserOwners *[]string `json:"user_owners,omitempty"` + GroupOwners *[]string `json:"group_owners,omitempty"` } From 2e9b01a076123c52083c58373aab281e6928712a Mon Sep 17 00:00:00 2001 From: Asaf Gabai Date: Wed, 4 Mar 2026 17:41:09 +0200 Subject: [PATCH 2/5] Support multi-value labels --- .../application/update_app_cmd_test.go | 40 +++++++++---------- apptrust/commands/utils/utils_test.go | 26 ++++++------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/apptrust/commands/application/update_app_cmd_test.go b/apptrust/commands/application/update_app_cmd_test.go index 947db73..cc17d61 100644 --- a/apptrust/commands/application/update_app_cmd_test.go +++ b/apptrust/commands/application/update_app_cmd_test.go @@ -31,9 +31,9 @@ func TestUpdateAppCommand_Run(t *testing.T) { Description: &description, MaturityLevel: &maturityLevel, BusinessCriticality: &businessCriticality, - Labels: &map[string]string{ - "environment": "production", - "region": "us-east", + Labels: &[]model.LabelEntry{ + {Key: "environment", Value: "production"}, + {Key: "region", Value: "us-east"}, }, UserOwners: &[]string{"JohnD", "Dave Rice"}, GroupOwners: &[]string{"DevOps"}, @@ -67,9 +67,9 @@ func TestUpdateAppCommand_Run_Error(t *testing.T) { Description: &description, MaturityLevel: &maturityLevel, BusinessCriticality: &businessCriticality, - Labels: &map[string]string{ - "environment": "production", - "region": "us-east", + Labels: &[]model.LabelEntry{ + {Key: "environment", Value: "production"}, + {Key: "region", Value: "us-east"}, }, UserOwners: &[]string{"JohnD", "Dave Rice"}, GroupOwners: &[]string{"DevOps"}, @@ -127,7 +127,7 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{ + Add: []model.LabelEntry{ {Key: "environment", Value: "production"}, }, }, @@ -142,7 +142,7 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{ + Add: []model.LabelEntry{ {Key: "environment", Value: "production"}, {Key: "region", Value: "us-east"}, }, @@ -158,7 +158,7 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{ + Add: []model.LabelEntry{ {Key: "environment", Value: "production"}, {Key: "environment", Value: "staging"}, {Key: "region", Value: "us-east"}, @@ -175,7 +175,7 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Remove: []model.LabelKeyValue{ + Remove: []model.LabelEntry{ {Key: "infra-version", Value: "v1.0"}, }, }, @@ -190,7 +190,7 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Remove: []model.LabelKeyValue{ + Remove: []model.LabelEntry{ {Key: "infra-version", Value: "v1.0"}, {Key: "region", Value: "us-west"}, }, @@ -207,12 +207,12 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{ + Add: []model.LabelEntry{ {Key: "environment", Value: "production"}, {Key: "environment", Value: "staging"}, {Key: "region", Value: "us-east"}, }, - Remove: []model.LabelKeyValue{ + Remove: []model.LabelEntry{ {Key: "infra-version", Value: "v1.0"}, {Key: "region", Value: "us-west"}, }, @@ -246,10 +246,10 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { Description: stringPtr("Updated description"), MaturityLevel: stringPtr("production"), LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{ + Add: []model.LabelEntry{ {Key: "environment", Value: "production"}, }, - Remove: []model.LabelKeyValue{ + Remove: []model.LabelEntry{ {Key: "old-label", Value: "old-value"}, }, }, @@ -282,7 +282,7 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{ + Add: []model.LabelEntry{ {Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}, }, @@ -299,8 +299,8 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{}, - Remove: []model.LabelKeyValue{ + Add: []model.LabelEntry{}, + Remove: []model.LabelEntry{ {Key: "key", Value: "value"}, }, }, @@ -316,10 +316,10 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) { expectsPayload: &model.AppDescriptor{ ApplicationKey: "app-key", LabelUpdates: &model.LabelUpdates{ - Add: []model.LabelKeyValue{ + Add: []model.LabelEntry{ {Key: "key", Value: "value"}, }, - Remove: []model.LabelKeyValue{}, + Remove: []model.LabelEntry{}, }, }, }, diff --git a/apptrust/commands/utils/utils_test.go b/apptrust/commands/utils/utils_test.go index 0991fd7..b5387ab 100644 --- a/apptrust/commands/utils/utils_test.go +++ b/apptrust/commands/utils/utils_test.go @@ -169,62 +169,62 @@ func TestParseLabelKeyValuePairs(t *testing.T) { tests := []struct { name string input string - expected []model.LabelKeyValue + expected []model.LabelEntry expectErr bool errorMsg string }{ { name: "empty string", input: "", - expected: []model.LabelKeyValue{}, + expected: []model.LabelEntry{}, expectErr: false, }, { name: "single pair", input: "key1=value1", - expected: []model.LabelKeyValue{{Key: "key1", Value: "value1"}}, + expected: []model.LabelEntry{{Key: "key1", Value: "value1"}}, expectErr: false, }, { name: "multiple pairs", input: "key1=value1;key2=value2;key3=value3", - expected: []model.LabelKeyValue{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}, {Key: "key3", Value: "value3"}}, + expected: []model.LabelEntry{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}, {Key: "key3", Value: "value3"}}, expectErr: false, }, { name: "same key multiple values", input: "environment=production;environment=staging;region=us-east", - expected: []model.LabelKeyValue{{Key: "environment", Value: "production"}, {Key: "environment", Value: "staging"}, {Key: "region", Value: "us-east"}}, + expected: []model.LabelEntry{{Key: "environment", Value: "production"}, {Key: "environment", Value: "staging"}, {Key: "region", Value: "us-east"}}, expectErr: false, }, { name: "whitespace handling", input: " key1 = value1 ; key2 = value2 ", - expected: []model.LabelKeyValue{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, + expected: []model.LabelEntry{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, expectErr: false, }, { name: "empty pairs skipped", input: "key1=value1;;key2=value2", - expected: []model.LabelKeyValue{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, + expected: []model.LabelEntry{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, expectErr: false, }, { name: "leading and trailing separators", input: ";key1=value1;key2=value2;", - expected: []model.LabelKeyValue{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, + expected: []model.LabelEntry{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, expectErr: false, }, { name: "empty value", input: "key1=;key2=value2", - expected: []model.LabelKeyValue{{Key: "key1", Value: ""}, {Key: "key2", Value: "value2"}}, + expected: []model.LabelEntry{{Key: "key1", Value: ""}, {Key: "key2", Value: "value2"}}, expectErr: false, }, { name: "value with equals sign", input: "key1=value=with=equals;key2=normal", - expected: []model.LabelKeyValue{{Key: "key1", Value: "value=with=equals"}, {Key: "key2", Value: "normal"}}, + expected: []model.LabelEntry{{Key: "key1", Value: "value=with=equals"}, {Key: "key2", Value: "normal"}}, expectErr: false, }, { @@ -242,19 +242,19 @@ func TestParseLabelKeyValuePairs(t *testing.T) { { name: "empty key", input: "=value1;key2=value2", - expected: []model.LabelKeyValue{{Key: "", Value: "value1"}, {Key: "key2", Value: "value2"}}, + expected: []model.LabelEntry{{Key: "", Value: "value1"}, {Key: "key2", Value: "value2"}}, expectErr: false, }, { name: "whitespace only pairs skipped", input: "key1=value1; ;key2=value2", - expected: []model.LabelKeyValue{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, + expected: []model.LabelEntry{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}, expectErr: false, }, { name: "special characters in key and value", input: "env-name=prod-env;region=us-east-1", - expected: []model.LabelKeyValue{{Key: "env-name", Value: "prod-env"}, {Key: "region", Value: "us-east-1"}}, + expected: []model.LabelEntry{{Key: "env-name", Value: "prod-env"}, {Key: "region", Value: "us-east-1"}}, expectErr: false, }, } From a966d2b784e557c2fa73bdf5798006095f14c0af Mon Sep 17 00:00:00 2001 From: Asaf Gabai Date: Wed, 4 Mar 2026 17:44:02 +0200 Subject: [PATCH 3/5] Support multi-value labels --- .../application/create_app_cmd_test.go | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apptrust/commands/application/create_app_cmd_test.go b/apptrust/commands/application/create_app_cmd_test.go index 023854d..9be9097 100644 --- a/apptrust/commands/application/create_app_cmd_test.go +++ b/apptrust/commands/application/create_app_cmd_test.go @@ -43,9 +43,9 @@ func TestCreateAppCommand_Run_Flags(t *testing.T) { Description: &description, BusinessCriticality: &businessCriticality, MaturityLevel: &maturityLevel, - Labels: &map[string]string{ - "env": "prod", - "region": "us-east", + Labels: &[]model.LabelEntry{ + {Key: "env", Value: "prod"}, + {Key: "region", Value: "us-east"}, }, UserOwners: &[]string{"john.doe", "jane.smith"}, GroupOwners: &[]string{"devops", "security"}, @@ -149,10 +149,10 @@ func TestCreateAppCommand_Run_FullSpecFile(t *testing.T) { Description: &expectedDescription, MaturityLevel: &expectedMaturityLevel, BusinessCriticality: &expectedBusinessCriticality, - Labels: &map[string]string{ - "environment": "production", - "region": "us-east-1", - "team": "devops", + Labels: &[]model.LabelEntry{ + {Key: "environment", Value: "production"}, + {Key: "region", Value: "us-east-1"}, + {Key: "team", Value: "devops"}, }, UserOwners: &[]string{"john.doe", "jane.smith"}, GroupOwners: &[]string{"devops-team", "security-team"}, @@ -281,9 +281,9 @@ func TestCreateAppCommand_Run_SpecVars(t *testing.T) { Description: &expectedDescription, MaturityLevel: &expectedMaturityLevel, BusinessCriticality: &expectedBusinessCriticality, - Labels: &map[string]string{ - "environment": "production", - "region": "us-east-1", + Labels: &[]model.LabelEntry{ + {Key: "environment", Value: "production"}, + {Key: "region", Value: "us-east-1"}, }, } From 5a5953aa44f2ec1254604555713f45ca9979ce8e Mon Sep 17 00:00:00 2001 From: Asaf Gabai Date: Wed, 4 Mar 2026 17:51:38 +0200 Subject: [PATCH 4/5] Support multi-value labels --- .../application/create_app_cmd_test.go | 1 + .../application/testfiles/full-spec.json | 23 +++++++++++++++---- .../application/testfiles/with-vars-spec.json | 14 +++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/apptrust/commands/application/create_app_cmd_test.go b/apptrust/commands/application/create_app_cmd_test.go index 9be9097..0d11954 100644 --- a/apptrust/commands/application/create_app_cmd_test.go +++ b/apptrust/commands/application/create_app_cmd_test.go @@ -151,6 +151,7 @@ func TestCreateAppCommand_Run_FullSpecFile(t *testing.T) { BusinessCriticality: &expectedBusinessCriticality, Labels: &[]model.LabelEntry{ {Key: "environment", Value: "production"}, + {Key: "environment", Value: "staging"}, {Key: "region", Value: "us-east-1"}, {Key: "team", Value: "devops"}, }, diff --git a/apptrust/commands/application/testfiles/full-spec.json b/apptrust/commands/application/testfiles/full-spec.json index b65aefc..6c854cf 100644 --- a/apptrust/commands/application/testfiles/full-spec.json +++ b/apptrust/commands/application/testfiles/full-spec.json @@ -4,11 +4,24 @@ "description": "A comprehensive test application", "maturity_level": "production", "criticality": "high", - "labels": { - "environment": "production", - "region": "us-east-1", - "team": "devops" - }, + "labels": [ + { + "key": "environment", + "value": "production" + }, + { + "key": "environment", + "value": "staging" + }, + { + "key": "region", + "value": "us-east-1" + }, + { + "key": "team", + "value": "devops" + } + ], "user_owners": [ "john.doe", "jane.smith" diff --git a/apptrust/commands/application/testfiles/with-vars-spec.json b/apptrust/commands/application/testfiles/with-vars-spec.json index ebbbce5..2b5e125 100644 --- a/apptrust/commands/application/testfiles/with-vars-spec.json +++ b/apptrust/commands/application/testfiles/with-vars-spec.json @@ -4,8 +4,14 @@ "description": "A test application for ${ENVIRONMENT}", "maturity_level": "${MATURITY_LEVEL}", "criticality": "${CRITICALITY}", - "labels": { - "environment": "${ENVIRONMENT}", - "region": "${REGION}" - } + "labels": [ + { + "key": "environment", + "value": "${ENVIRONMENT}" + }, + { + "key": "region", + "value": "${REGION}" + } + ] } \ No newline at end of file From 360e926359b0aa2ef9ea37f450ac41805440c874 Mon Sep 17 00:00:00 2001 From: Asaf Gabai Date: Thu, 5 Mar 2026 18:14:42 +0200 Subject: [PATCH 5/5] Support multi-value labels --- e2e/application_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/e2e/application_test.go b/e2e/application_test.go index 51adf08..40a1b54 100644 --- a/e2e/application_test.go +++ b/e2e/application_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/jfrog/jfrog-cli-application/apptrust/model" "github.com/jfrog/jfrog-cli-application/e2e/utils" "github.com/stretchr/testify/assert" ) @@ -40,7 +41,7 @@ func TestCreateApp(t *testing.T) { assert.Equal(t, description, *app.Description) assert.Equal(t, businessCriticality, *app.BusinessCriticality) assert.Equal(t, maturityLevel, *app.MaturityLevel) - assert.Equal(t, map[string]string{"env": "prod", "team": "devops"}, *app.Labels) + assert.ElementsMatch(t, []model.LabelEntry{{Key: "env", Value: "prod"}, {Key: "team", Value: "devops"}}, *app.Labels) assert.Equal(t, userOwners, *app.UserOwners) assert.Equal(t, groupOwners, *app.GroupOwners) @@ -79,7 +80,7 @@ func TestUpdateApp(t *testing.T) { assert.Equal(t, updatedDescription, *app.Description) assert.Equal(t, updatedBusinessCriticality, *app.BusinessCriticality) assert.Equal(t, updatedMaturityLevel, *app.MaturityLevel) - assert.Equal(t, map[string]string{"env": "qa", "team": "dev"}, *app.Labels) + assert.ElementsMatch(t, []model.LabelEntry{{Key: "env", Value: "qa"}, {Key: "team", Value: "dev"}}, *app.Labels) assert.Equal(t, updatedUserOwners, *app.UserOwners) assert.Equal(t, updatedGroupOwners, *app.GroupOwners)