Skip to content

Commit b7cc47f

Browse files
committed
Merge remote-tracking branch 'origin/main' into many-improvements
# Conflicts: # apptrust/commands/flags.go
2 parents f926a23 + b83aa2b commit b7cc47f

File tree

9 files changed

+359
-16
lines changed

9 files changed

+359
-16
lines changed

apptrust/commands/application/create_app_cmd.go

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package application
22

33
import (
4+
"encoding/json"
5+
46
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
57

68
"github.com/jfrog/jfrog-cli-application/apptrust/commands/utils"
@@ -9,7 +11,9 @@ import (
911
commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
1012
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
1113
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
14+
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
1215
"github.com/jfrog/jfrog-client-go/utils/errorutils"
16+
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
1317

1418
"github.com/jfrog/jfrog-cli-application/apptrust/app"
1519
"github.com/jfrog/jfrog-cli-application/apptrust/commands"
@@ -42,12 +46,31 @@ func (cac *createAppCommand) CommandName() string {
4246

4347
func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*model.AppDescriptor, error) {
4448
applicationKey := ctx.Arguments[0]
45-
applicationName := ctx.GetStringFlagValue(commands.ApplicationNameFlag)
46-
if applicationName == "" {
47-
// Default to the application key if application name is not provided
48-
applicationName = applicationKey
49+
50+
var appDescriptor *model.AppDescriptor
51+
var err error
52+
53+
if ctx.IsFlagSet(commands.SpecFlag) {
54+
appDescriptor, err = cac.loadFromSpec(ctx)
55+
} else {
56+
appDescriptor, err = cac.buildFromFlags(ctx)
57+
}
58+
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
appDescriptor.ApplicationKey = applicationKey
64+
if appDescriptor.ApplicationName == "" {
65+
appDescriptor.ApplicationName = applicationKey
4966
}
5067

68+
return appDescriptor, nil
69+
}
70+
71+
func (cac *createAppCommand) buildFromFlags(ctx *components.Context) (*model.AppDescriptor, error) {
72+
applicationName := ctx.GetStringFlagValue(commands.ApplicationNameFlag)
73+
5174
project := ctx.GetStringFlagValue(commands.ProjectFlag)
5275
if project == "" {
5376
return nil, errorutils.CheckErrorf("--%s is mandatory", commands.ProjectFlag)
@@ -83,7 +106,6 @@ func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*mode
83106

84107
return &model.AppDescriptor{
85108
ApplicationName: applicationName,
86-
ApplicationKey: applicationKey,
87109
Description: description,
88110
ProjectKey: project,
89111
MaturityLevel: maturityLevel,
@@ -94,9 +116,34 @@ func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*mode
94116
}, nil
95117
}
96118

119+
func (cac *createAppCommand) loadFromSpec(ctx *components.Context) (*model.AppDescriptor, error) {
120+
specFilePath := ctx.GetStringFlagValue(commands.SpecFlag)
121+
spec := new(model.AppDescriptor)
122+
specVars := coreutils.SpecVarsStringToMap(ctx.GetStringFlagValue(commands.SpecVarsFlag))
123+
content, err := fileutils.ReadFile(specFilePath)
124+
if errorutils.CheckError(err) != nil {
125+
return nil, err
126+
}
127+
128+
if len(specVars) > 0 {
129+
content = coreutils.ReplaceVars(content, specVars)
130+
}
131+
132+
err = json.Unmarshal(content, spec)
133+
if errorutils.CheckError(err) != nil {
134+
return nil, err
135+
}
136+
137+
if spec.ProjectKey == "" {
138+
return nil, errorutils.CheckErrorf("project_key is mandatory in spec file")
139+
}
140+
141+
return spec, nil
142+
}
143+
97144
func (cac *createAppCommand) prepareAndRunCommand(ctx *components.Context) error {
98-
if len(ctx.Arguments) != 1 {
99-
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
145+
if err := validateCreateAppContext(ctx); err != nil {
146+
return err
100147
}
101148

102149
var err error
@@ -113,6 +160,37 @@ func (cac *createAppCommand) prepareAndRunCommand(ctx *components.Context) error
113160
return commonCLiCommands.Exec(cac)
114161
}
115162

163+
func validateCreateAppContext(ctx *components.Context) error {
164+
if err := validateNoSpecAndFlagsTogether(ctx); err != nil {
165+
return err
166+
}
167+
if len(ctx.Arguments) != 1 {
168+
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
169+
}
170+
return nil
171+
}
172+
173+
func validateNoSpecAndFlagsTogether(ctx *components.Context) error {
174+
if ctx.IsFlagSet(commands.SpecFlag) {
175+
otherAppFlags := []string{
176+
commands.ApplicationNameFlag,
177+
commands.ProjectFlag,
178+
commands.DescriptionFlag,
179+
commands.BusinessCriticalityFlag,
180+
commands.MaturityLevelFlag,
181+
commands.LabelsFlag,
182+
commands.UserOwnersFlag,
183+
commands.GroupOwnersFlag,
184+
}
185+
for _, flag := range otherAppFlags {
186+
if ctx.IsFlagSet(flag) {
187+
return errorutils.CheckErrorf("the flag --%s is not allowed when --spec is provided.", flag)
188+
}
189+
}
190+
}
191+
return nil
192+
}
193+
116194
func GetCreateAppCommand(appContext app.Context) components.Command {
117195
cmd := &createAppCommand{
118196
applicationService: appContext.GetApplicationService(),

apptrust/commands/application/create_app_cmd_test.go

Lines changed: 223 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,47 @@ import (
1515
"go.uber.org/mock/gomock"
1616
)
1717

18-
func TestCreateAppCommand_Run(t *testing.T) {
18+
func TestCreateAppCommand_Run_Flags(t *testing.T) {
1919
ctrl := gomock.NewController(t)
2020
defer ctrl.Finish()
2121

22-
serverDetails := &config.ServerDetails{Url: "https://example.com"}
22+
ctx := &components.Context{
23+
Arguments: []string{"app-key"},
24+
}
25+
ctx.AddStringFlag("application-name", "test-app")
26+
ctx.AddStringFlag("project", "test-project")
27+
ctx.AddStringFlag("desc", "Test application")
28+
ctx.AddStringFlag("business-criticality", "high")
29+
ctx.AddStringFlag("maturity-level", "production")
30+
ctx.AddStringFlag("labels", "env=prod;region=us-east")
31+
ctx.AddStringFlag("user-owners", "john.doe;jane.smith")
32+
ctx.AddStringFlag("group-owners", "devops;security")
33+
ctx.AddStringFlag("url", "https://example.com")
34+
2335
requestPayload := &model.AppDescriptor{
24-
ApplicationKey: "app-key",
25-
ApplicationName: "app-name",
26-
ProjectKey: "proj-key",
36+
ApplicationKey: "app-key",
37+
ApplicationName: "test-app",
38+
ProjectKey: "test-project",
39+
Description: "Test application",
40+
BusinessCriticality: "high",
41+
MaturityLevel: "production",
42+
Labels: map[string]string{
43+
"env": "prod",
44+
"region": "us-east",
45+
},
46+
UserOwners: []string{"john.doe", "jane.smith"},
47+
GroupOwners: []string{"devops", "security"},
2748
}
2849

2950
mockAppService := mockapps.NewMockApplicationService(ctrl)
3051
mockAppService.EXPECT().CreateApplication(gomock.Any(), requestPayload).Return(nil).Times(1)
3152

3253
cmd := &createAppCommand{
3354
applicationService: mockAppService,
34-
serverDetails: serverDetails,
3555
requestBody: requestPayload,
3656
}
3757

38-
err := cmd.Run()
58+
err := cmd.prepareAndRunCommand(ctx)
3959
assert.NoError(t, err)
4060
}
4161

@@ -84,3 +104,199 @@ func TestCreateAppCommand_WrongNumberOfArguments(t *testing.T) {
84104
assert.Error(t, err)
85105
assert.Contains(t, err.Error(), "Wrong number of arguments")
86106
}
107+
108+
func TestCreateAppCommand_MissingProjectFlag(t *testing.T) {
109+
ctrl := gomock.NewController(t)
110+
defer ctrl.Finish()
111+
112+
ctx := &components.Context{
113+
Arguments: []string{"app-key"},
114+
}
115+
ctx.AddStringFlag("application-name", "test-app")
116+
ctx.AddStringFlag("url", "https://example.com")
117+
mockAppService := mockapps.NewMockApplicationService(ctrl)
118+
119+
cmd := &createAppCommand{
120+
applicationService: mockAppService,
121+
}
122+
123+
err := cmd.prepareAndRunCommand(ctx)
124+
assert.Error(t, err)
125+
assert.Contains(t, err.Error(), "--project is mandatory")
126+
}
127+
128+
func TestCreateAppCommand_Run_SpecFile(t *testing.T) {
129+
tests := []struct {
130+
name string
131+
specPath string
132+
args []string
133+
expectsError bool
134+
errorContains string
135+
expectsPayload *model.AppDescriptor
136+
}{
137+
{
138+
name: "minimal spec file",
139+
specPath: "./testfiles/minimal-spec.json",
140+
args: []string{"app-min"},
141+
expectsPayload: &model.AppDescriptor{
142+
ApplicationKey: "app-min",
143+
ApplicationName: "app-min",
144+
ProjectKey: "test-project",
145+
},
146+
},
147+
{
148+
name: "full spec file",
149+
specPath: "./testfiles/full-spec.json",
150+
args: []string{"app-full"},
151+
expectsPayload: &model.AppDescriptor{
152+
ApplicationKey: "app-full",
153+
ApplicationName: "test-app-full",
154+
ProjectKey: "test-project",
155+
Description: "A comprehensive test application",
156+
MaturityLevel: "production",
157+
BusinessCriticality: "high",
158+
Labels: map[string]string{
159+
"environment": "production",
160+
"region": "us-east-1",
161+
"team": "devops",
162+
},
163+
UserOwners: []string{"john.doe", "jane.smith"},
164+
GroupOwners: []string{"devops-team", "security-team"},
165+
},
166+
},
167+
{
168+
name: "invalid spec file",
169+
specPath: "./testfiles/invalid-spec.json",
170+
args: []string{"app-invalid"},
171+
expectsError: true,
172+
errorContains: "unexpected end of JSON input",
173+
},
174+
{
175+
name: "missing project key",
176+
specPath: "./testfiles/missing-project-spec.json",
177+
args: []string{"app-no-project"},
178+
expectsError: true,
179+
errorContains: "project_key is mandatory in spec file",
180+
},
181+
{
182+
name: "non-existent spec file",
183+
specPath: "./testfiles/non-existent.json",
184+
args: []string{"app-nonexistent"},
185+
expectsError: true,
186+
errorContains: "no such file or directory",
187+
},
188+
{
189+
name: "spec with application_key that should be ignored",
190+
specPath: "./testfiles/spec-with-app-key.json",
191+
args: []string{"command-line-app-key"},
192+
expectsPayload: &model.AppDescriptor{
193+
ApplicationKey: "command-line-app-key",
194+
ApplicationName: "test-app",
195+
ProjectKey: "test-project",
196+
Description: "A test application with application_key that should be ignored",
197+
},
198+
},
199+
}
200+
201+
for _, tt := range tests {
202+
t.Run(tt.name, func(t *testing.T) {
203+
ctrl := gomock.NewController(t)
204+
defer ctrl.Finish()
205+
206+
ctx := &components.Context{
207+
Arguments: tt.args,
208+
}
209+
ctx.AddStringFlag("url", "https://example.com")
210+
ctx.AddStringFlag("spec", tt.specPath)
211+
212+
var actualPayload *model.AppDescriptor
213+
mockAppService := mockapps.NewMockApplicationService(ctrl)
214+
if !tt.expectsError {
215+
mockAppService.EXPECT().CreateApplication(gomock.Any(), gomock.Any()).
216+
DoAndReturn(func(_ interface{}, req *model.AppDescriptor) error {
217+
actualPayload = req
218+
return nil
219+
}).Times(1)
220+
}
221+
222+
cmd := &createAppCommand{
223+
applicationService: mockAppService,
224+
}
225+
226+
err := cmd.prepareAndRunCommand(ctx)
227+
if tt.expectsError {
228+
assert.Error(t, err)
229+
if tt.errorContains != "" {
230+
assert.Contains(t, err.Error(), tt.errorContains)
231+
}
232+
} else {
233+
assert.NoError(t, err)
234+
assert.Equal(t, tt.expectsPayload, actualPayload)
235+
}
236+
})
237+
}
238+
}
239+
240+
func TestCreateAppCommand_Run_SpecVars(t *testing.T) {
241+
ctrl := gomock.NewController(t)
242+
defer ctrl.Finish()
243+
244+
expectedPayload := &model.AppDescriptor{
245+
ApplicationKey: "app-with-vars",
246+
ApplicationName: "test-app",
247+
ProjectKey: "test-project",
248+
Description: "A test application for production",
249+
MaturityLevel: "production",
250+
BusinessCriticality: "high",
251+
Labels: map[string]string{
252+
"environment": "production",
253+
"region": "us-east-1",
254+
},
255+
}
256+
257+
ctx := &components.Context{
258+
Arguments: []string{"app-with-vars"},
259+
}
260+
ctx.AddStringFlag("spec", "./testfiles/with-vars-spec.json")
261+
ctx.AddStringFlag("spec-vars", "PROJECT_KEY=test-project;APP_NAME=test-app;ENVIRONMENT=production;MATURITY_LEVEL=production;CRITICALITY=high;REGION=us-east-1")
262+
ctx.AddStringFlag("url", "https://example.com")
263+
264+
var actualPayload *model.AppDescriptor
265+
mockAppService := mockapps.NewMockApplicationService(ctrl)
266+
mockAppService.EXPECT().CreateApplication(gomock.Any(), gomock.Any()).
267+
DoAndReturn(func(_ interface{}, req *model.AppDescriptor) error {
268+
actualPayload = req
269+
return nil
270+
}).Times(1)
271+
272+
cmd := &createAppCommand{
273+
applicationService: mockAppService,
274+
}
275+
276+
err := cmd.prepareAndRunCommand(ctx)
277+
assert.NoError(t, err)
278+
assert.Equal(t, expectedPayload, actualPayload)
279+
}
280+
281+
func TestCreateAppCommand_Error_SpecAndFlags(t *testing.T) {
282+
ctrl := gomock.NewController(t)
283+
defer ctrl.Finish()
284+
285+
testSpecPath := "./testfiles/minimal-spec.json"
286+
ctx := &components.Context{
287+
Arguments: []string{"app-key"},
288+
}
289+
ctx.AddStringFlag("spec", testSpecPath)
290+
ctx.AddStringFlag("project", "test-project")
291+
ctx.AddStringFlag("url", "https://example.com")
292+
293+
mockAppService := mockapps.NewMockApplicationService(ctrl)
294+
295+
cmd := &createAppCommand{
296+
applicationService: mockAppService,
297+
}
298+
299+
err := cmd.prepareAndRunCommand(ctx)
300+
assert.Error(t, err)
301+
assert.Contains(t, err.Error(), "the flag --project is not allowed when --spec is provided")
302+
}

0 commit comments

Comments
 (0)