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
1 change: 1 addition & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
allow-unsafe-pr-checkout: true

- name: Setup Go
uses: actions/setup-go@v6
Expand Down
8 changes: 8 additions & 0 deletions apptrust/commands/application/application_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,13 @@ func populateApplicationFromFlags(ctx *components.Context, descriptor *model.App
descriptor.GroupOwners = &groupOwners
}

if ctx.IsFlagSet(commands.MonitorPolicyFlag) {
monitorPolicy, err := utils.ParseMonitorPolicyFlag(ctx.GetStringFlagValue(commands.MonitorPolicyFlag))
if err != nil {
return fmt.Errorf("failed to parse --%s: %w", commands.MonitorPolicyFlag, err)
}
descriptor.MonitorPolicy = monitorPolicy
}

return nil
}
5 changes: 4 additions & 1 deletion apptrust/commands/application/create_app_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ func validateNoSpecAndFlagsTogether(ctx *components.Context) error {
commands.LabelsFlag,
commands.UserOwnersFlag,
commands.GroupOwnersFlag,
commands.MonitorPolicyFlag,
}
for _, flag := range otherAppFlags {
if ctx.IsFlagSet(flag) {
Expand Down Expand Up @@ -197,12 +198,14 @@ Common patterns:
$ jf apptrust app-create my-app --project=default --application-name="My App" --desc="Service X"
$ jf apptrust app-create my-app --project=default --business-criticality=high --maturity-level=production
$ jf apptrust app-create my-app --project=default --labels="team=core;area=platform" --user-owners="alice;bob"
$ jf apptrust app-create my-app --project=default --monitor-policy="type=version_count, value=5"
$ jf apptrust app-create my-app --spec=app-spec.json --spec-vars="ENV=prod"

Gotchas:
- --spec is mutually exclusive with --application-name, --project, --desc, --business-criticality, --maturity-level, --labels, --user-owners, --group-owners.
- --spec is mutually exclusive with --application-name, --project, --desc, --business-criticality, --maturity-level, --labels, --user-owners, --group-owners, --monitor-policy.
- If --application-name is omitted, the application-key is used as the display name.
- --labels uses semicolon separators (not commas) and key=value pairs.
- --monitor-policy takes 'type=<type>[, value=<n>]'. 'value' is required (positive integer) when type is "time_frame_in_months" or "version_count", and must be omitted when type is "none".

Related: jf apptrust app-update, jf apptrust app-delete, jf apptrust version-create`,
Category: common.CategoryApplication,
Expand Down
7 changes: 5 additions & 2 deletions apptrust/commands/application/create_app_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestCreateAppCommand_Run_Flags(t *testing.T) {
description := "Test application"
businessCriticality := "high"
maturityLevel := "production"
monitorPolicyValue := 5

ctx := &components.Context{
Arguments: []string{"app-key"},
Expand All @@ -34,6 +35,7 @@ func TestCreateAppCommand_Run_Flags(t *testing.T) {
ctx.AddStringFlag("labels", "env=prod;region=us-east")
ctx.AddStringFlag("user-owners", "john.doe;jane.smith")
ctx.AddStringFlag("group-owners", "devops;security")
ctx.AddStringFlag("monitor-policy", "type=version_count, value=5")
ctx.AddStringFlag("url", "https://example.com")

requestPayload := &model.AppDescriptor{
Expand All @@ -47,8 +49,9 @@ func TestCreateAppCommand_Run_Flags(t *testing.T) {
{Key: "env", Value: "prod"},
{Key: "region", Value: "us-east"},
},
UserOwners: &[]string{"john.doe", "jane.smith"},
GroupOwners: &[]string{"devops", "security"},
UserOwners: &[]string{"john.doe", "jane.smith"},
GroupOwners: &[]string{"devops", "security"},
MonitorPolicy: &model.MonitorPolicy{Type: model.MonitorPolicyTypeVersionCount, Value: &monitorPolicyValue},
}

mockAppService := mockapps.NewMockApplicationService(ctrl)
Expand Down
2 changes: 2 additions & 0 deletions apptrust/commands/application/update_app_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,12 @@ Common patterns:
$ jf apptrust app-update my-app --add-labels="env=prod;tier=critical"
$ jf apptrust app-update my-app --remove-labels="env=staging"
$ jf apptrust app-update my-app --user-owners="alice;bob" --group-owners="platform-team"
$ jf apptrust app-update my-app --monitor-policy="type=version_count, value=5"

Gotchas:
- --labels replaces the full label set; --add-labels and --remove-labels modify incrementally.
- --user-owners / --group-owners take a semicolon-separated list and send exactly the owners you specify; there are no incremental add/remove-owner flags (unlike --add-labels / --remove-labels for labels).
- --monitor-policy takes 'type=<type>[, value=<n>]'. 'value' is required (positive integer) when type is "time_frame_in_months" or "version_count", and must be omitted when type is "none". When --monitor-policy is not provided, the current policy is left unchanged.
- Application key cannot be changed; use app-delete and app-create if you need a different key.

Related: jf apptrust app-create, jf apptrust app-delete`,
Expand Down
13 changes: 13 additions & 0 deletions apptrust/commands/application/update_app_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ func TestUpdateAppCommand_WrongNumberOfArguments(t *testing.T) {
}

func TestUpdateAppCommand_FlagsSuite(t *testing.T) {
monitorPolicyValue := 5

tests := []struct {
name string
ctxSetup func(*components.Context)
Expand Down Expand Up @@ -255,6 +257,17 @@ func TestUpdateAppCommand_FlagsSuite(t *testing.T) {
},
},
},
{
name: "monitor-policy",
ctxSetup: func(ctx *components.Context) {
ctx.Arguments = []string{"app-key"}
ctx.AddStringFlag(commands.MonitorPolicyFlag, "type=version_count, value=5")
},
expectsPayload: &model.AppDescriptor{
ApplicationKey: "app-key",
MonitorPolicy: &model.MonitorPolicy{Type: model.MonitorPolicyTypeVersionCount, Value: &monitorPolicyValue},
},
},
{
name: "invalid add-labels format - missing equals",
ctxSetup: func(ctx *components.Context) {
Expand Down
4 changes: 4 additions & 0 deletions apptrust/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const (
RemoveLabelsFlag = "remove-labels"
UserOwnersFlag = "user-owners"
GroupOwnersFlag = "group-owners"
MonitorPolicyFlag = "monitor-policy"
SyncFlag = "sync"
PromotionTypeFlag = "promotion-type"
DryRunFlag = "dry-run"
Expand Down Expand Up @@ -97,6 +98,7 @@ var flagsMap = map[string]components.Flag{
RemoveLabelsFlag: components.NewStringFlag(RemoveLabelsFlag, "List of semicolon-separated (;) labels to remove in the form of \"key1=value1;key2=value2;...\" (wrapped by quotes).", func(f *components.StringFlag) { f.Mandatory = false }),
UserOwnersFlag: components.NewStringFlag(UserOwnersFlag, "semicolon-separated (;) list of user owners in the form of \"user1;user2;...\" (wrapped by quotes).", func(f *components.StringFlag) { f.Mandatory = false }),
GroupOwnersFlag: components.NewStringFlag(GroupOwnersFlag, "semicolon-separated (;) list of group owners in the form of \"group1;group2;...\" (wrapped by quotes).", func(f *components.StringFlag) { f.Mandatory = false }),
MonitorPolicyFlag: components.NewStringFlag(MonitorPolicyFlag, "Defines how long application versions remain monitored, in the form of 'type=<type>[, value=<n>]'. Supported types: "+coreutils.ListToText(model.MonitorPolicyTypeValues)+". For '"+model.MonitorPolicyTypeTimeframe+"' or '"+model.MonitorPolicyTypeVersionCount+"', 'value' is the number of months or versions to keep monitoring (a positive integer). For '"+model.MonitorPolicyTypeNone+"', monitoring is disabled and 'value' must be omitted.", func(f *components.StringFlag) { f.Mandatory = false }),
SyncFlag: components.NewBoolFlag(SyncFlag, "Whether to synchronize the operation.", components.WithBoolDefaultValueTrue()),
PromotionTypeFlag: components.NewStringFlag(PromotionTypeFlag, "The promotion type. The following values are supported: "+coreutils.ListToText(model.PromotionTypeValues), func(f *components.StringFlag) { f.Mandatory = false; f.DefaultValue = model.PromotionTypeCopy }),
DryRunFlag: components.NewBoolFlag(DryRunFlag, "Perform a simulation of the operation.", components.WithBoolDefaultValueFalse()),
Expand Down Expand Up @@ -278,6 +280,7 @@ var commandFlags = map[string][]string{
LabelsFlag,
UserOwnersFlag,
GroupOwnersFlag,
MonitorPolicyFlag,
SpecFlag,
SpecVarsFlag,
},
Expand All @@ -296,6 +299,7 @@ var commandFlags = map[string][]string{
RemoveLabelsFlag,
UserOwnersFlag,
GroupOwnersFlag,
MonitorPolicyFlag,
},

AppDelete: {
Expand Down
45 changes: 45 additions & 0 deletions apptrust/commands/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package utils
import (
"fmt"
"slices"
"strconv"
"strings"

"github.com/jfrog/jfrog-cli-application/apptrust/model"
Expand Down Expand Up @@ -168,6 +169,50 @@ func ParseListPropertiesFlag(propertiesStr string) (map[string][]string, error)
return result, nil
}

func ParseMonitorPolicyFlag(flagValue string) (*model.MonitorPolicy, error) {
fields, err := ParseKeyValueString(flagValue, ",")
if err != nil {
return nil, err
}

for key := range fields {
if key != "type" && key != "value" {
return nil, errorutils.CheckErrorf("invalid field '%s' (supported fields: type, value)", key)
}
}

policyType, typeSet := fields["type"]
if !typeSet {
return nil, errorutils.CheckErrorf("missing required 'type' field")
}
if !slices.Contains(model.MonitorPolicyTypeValues, policyType) {
return nil, errorutils.CheckErrorf("invalid type '%s' (supported types: %s)", policyType, coreutils.ListToText(model.MonitorPolicyTypeValues))
}

valueStr, valueSet := fields["value"]
isNone := policyType == model.MonitorPolicyTypeNone
if isNone && valueSet {
return nil, errorutils.CheckErrorf("'value' must not be set when type is '%s'", model.MonitorPolicyTypeNone)
}
if !isNone && !valueSet {
return nil, errorutils.CheckErrorf("'value' is required when type is '%s'", policyType)
}

policy := &model.MonitorPolicy{Type: policyType}
if valueSet {
value, err := strconv.Atoi(valueStr)
if err != nil {
return nil, errorutils.CheckErrorf("'value' must be an integer: %s", err.Error())
}
if value <= 0 {
return nil, errorutils.CheckErrorf("'value' must be a positive integer")
}
policy.Value = &value
}

return policy, nil
}

func ParseLabelKeyValuePairs(flagValue string) ([]model.LabelEntry, error) {
if flagValue == "" {
return []model.LabelEntry{}, nil
Expand Down
104 changes: 104 additions & 0 deletions apptrust/commands/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,110 @@ func TestParseNameVersionPairs(t *testing.T) {
}
}

func TestParseMonitorPolicyFlag(t *testing.T) {
valueFive := 5
valueThree := 3

tests := []struct {
name string
input string
expected *model.MonitorPolicy
expectErr bool
errorMsg string
}{
{
name: "version count",
input: "type=version_count, value=5",
expected: &model.MonitorPolicy{Type: model.MonitorPolicyTypeVersionCount, Value: &valueFive},
},
{
name: "time frame in months",
input: "type=time_frame_in_months, value=3",
expected: &model.MonitorPolicy{Type: model.MonitorPolicyTypeTimeframe, Value: &valueThree},
},
{
name: "none without value",
input: "type=none",
expected: &model.MonitorPolicy{Type: model.MonitorPolicyTypeNone},
},
{
name: "missing type",
input: "value=5",
expectErr: true,
errorMsg: "missing required 'type' field",
},
{
name: "invalid type",
input: "type=bogus, value=5",
expectErr: true,
errorMsg: "invalid type 'bogus'",
},
{
name: "missing value for enabled type",
input: "type=version_count",
expectErr: true,
errorMsg: "'value' is required",
},
{
name: "value set for none",
input: "type=none, value=5",
expectErr: true,
errorMsg: "'value' must not be set",
},
{
name: "non-integer value",
input: "type=version_count, value=abc",
expectErr: true,
errorMsg: "must be an integer",
},
{
name: "zero value",
input: "type=version_count, value=0",
expectErr: true,
errorMsg: "must be a positive integer",
},
{
name: "negative value",
input: "type=version_count, value=-1",
expectErr: true,
errorMsg: "must be a positive integer",
},
{
name: "unknown field",
input: "type=version_count, value=5, foo=bar",
expectErr: true,
errorMsg: "invalid field 'foo'",
},
{
name: "empty string",
input: "",
expectErr: true,
errorMsg: "missing required 'type' field",
},
{
name: "invalid key-value pair",
input: "type",
expectErr: true,
errorMsg: "invalid key-value pair",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseMonitorPolicyFlag(tt.input)
if tt.expectErr {
assert.Error(t, err, "ParseMonitorPolicyFlag(%q) expected error, got nil", tt.input)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg, "error message should contain %q", tt.errorMsg)
}
return
}
assert.NoError(t, err, "ParseMonitorPolicyFlag(%q) unexpected error: %v", tt.input, err)
assert.Equal(t, tt.expected, result, "ParseMonitorPolicyFlag(%q) = %v, want %v", tt.input, result, tt.expected)
})
}
}

func TestParseLabelKeyValuePairs(t *testing.T) {
tests := []struct {
name string
Expand Down
36 changes: 26 additions & 10 deletions apptrust/model/app_descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const (
MaturityLevelExperimental = "experimental"
MaturityLevelProduction = "production"
MaturityLevelEndOfLife = "end_of_life"

MonitorPolicyTypeNone = "none"
MonitorPolicyTypeTimeframe = "time_frame_in_months"
MonitorPolicyTypeVersionCount = "version_count"
)

var (
Expand All @@ -28,27 +32,39 @@ var (
MaturityLevelProduction,
MaturityLevelEndOfLife,
}

MonitorPolicyTypeValues = []string{
MonitorPolicyTypeNone,
MonitorPolicyTypeTimeframe,
MonitorPolicyTypeVersionCount,
}
)

type LabelEntry struct {
Key string `json:"key"`
Value string `json:"value"`
}

type MonitorPolicy struct {
Type string `json:"type"`
Value *int `json:"value,omitempty"`
}

type LabelUpdates struct {
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 *[]LabelEntry `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"`
MonitorPolicy *MonitorPolicy `json:"monitor_policy,omitempty"`
}
Loading
Loading