diff --git a/application/cli/cli.go b/application/cli/cli.go index af131dc..96fc7cb 100644 --- a/application/cli/cli.go +++ b/application/cli/cli.go @@ -40,6 +40,7 @@ func GetJfrogApplicationCli() components.App { version.GetCreateAppVersionCommand(appContext), version.GetPromoteAppVersionCommand(appContext), application.GetCreateAppCommand(appContext), + application.GetUpdateAppCommand(appContext), }, ) return appEntity diff --git a/application/commands/application/create_app_cmd.go b/application/commands/application/create_app_cmd.go index fddf8ea..bb2232b 100644 --- a/application/commands/application/create_app_cmd.go +++ b/application/commands/application/create_app_cmd.go @@ -20,7 +20,7 @@ import ( type createAppCommand struct { serverDetails *coreConfig.ServerDetails applicationService applications.ApplicationService - requestBody *model.CreateAppRequest + requestBody *model.AppDescriptor } func (cac *createAppCommand) Run() error { @@ -40,7 +40,7 @@ func (cac *createAppCommand) CommandName() string { return commands.CreateApp } -func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*model.CreateAppRequest, error) { +func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*model.AppDescriptor, error) { applicationKey := ctx.Arguments[0] applicationName := ctx.GetStringFlagValue(commands.ApplicationNameFlag) if applicationName == "" { @@ -81,7 +81,7 @@ func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*mode return nil, err } - return &model.CreateAppRequest{ + return &model.AppDescriptor{ ApplicationName: applicationName, ApplicationKey: applicationKey, Description: description, diff --git a/application/commands/application/create_app_cmd_test.go b/application/commands/application/create_app_cmd_test.go index 9855df2..21fdd74 100644 --- a/application/commands/application/create_app_cmd_test.go +++ b/application/commands/application/create_app_cmd_test.go @@ -20,7 +20,7 @@ func TestCreateAppCommand_Run(t *testing.T) { defer ctrl.Finish() serverDetails := &config.ServerDetails{Url: "https://example.com"} - requestPayload := &model.CreateAppRequest{ + requestPayload := &model.AppDescriptor{ ApplicationKey: "app-key", ApplicationName: "app-name", ProjectKey: "proj-key", @@ -44,7 +44,7 @@ func TestCreateAppCommand_Error(t *testing.T) { defer ctrl.Finish() serverDetails := &config.ServerDetails{Url: "https://example.com"} - requestPayload := &model.CreateAppRequest{ + requestPayload := &model.AppDescriptor{ ApplicationKey: "app-key", ApplicationName: "app-name", ProjectKey: "proj-key", diff --git a/application/commands/application/update_app_cmd.go b/application/commands/application/update_app_cmd.go new file mode 100644 index 0000000..029bf80 --- /dev/null +++ b/application/commands/application/update_app_cmd.go @@ -0,0 +1,124 @@ +package application + +import ( + pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common" + + "github.com/jfrog/jfrog-cli-application/application/app" + "github.com/jfrog/jfrog-cli-application/application/commands" + "github.com/jfrog/jfrog-cli-application/application/commands/utils" + "github.com/jfrog/jfrog-cli-application/application/common" + "github.com/jfrog/jfrog-cli-application/application/model" + "github.com/jfrog/jfrog-cli-application/application/service" + "github.com/jfrog/jfrog-cli-application/application/service/applications" + commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" +) + +const UpdateApp = "update-app" + +type updateAppCommand struct { + serverDetails *coreConfig.ServerDetails + applicationService applications.ApplicationService + requestBody *model.AppDescriptor +} + +func (uac *updateAppCommand) Run() error { + ctx, err := service.NewContext(*uac.serverDetails) + if err != nil { + return err + } + + return uac.applicationService.UpdateApplication(ctx, uac.requestBody) +} + +func (uac *updateAppCommand) ServerDetails() (*coreConfig.ServerDetails, error) { + return uac.serverDetails, nil +} + +func (uac *updateAppCommand) CommandName() string { + return UpdateApp +} + +func (uac *updateAppCommand) buildRequestPayload(ctx *components.Context) (*model.AppDescriptor, error) { + applicationKey := ctx.Arguments[0] + applicationName := ctx.GetStringFlagValue(commands.ApplicationNameFlag) + + businessCriticalityStr := ctx.GetStringFlagValue(commands.BusinessCriticalityFlag) + businessCriticality, err := utils.ValidateEnumFlag( + commands.BusinessCriticalityFlag, + businessCriticalityStr, + "", + model.BusinessCriticalityValues) + if err != nil { + return nil, err + } + + maturityLevelStr := ctx.GetStringFlagValue(commands.MaturityLevelFlag) + maturityLevel, err := utils.ValidateEnumFlag( + commands.MaturityLevelFlag, + maturityLevelStr, + model.MaturityLevelUnspecified, + model.MaturityLevelValues) + if err != nil { + return nil, err + } + + description := ctx.GetStringFlagValue(commands.DescriptionFlag) + userOwners := utils.ParseSliceFlag(ctx.GetStringFlagValue(commands.UserOwnersFlag)) + groupOwners := utils.ParseSliceFlag(ctx.GetStringFlagValue(commands.GroupOwnersFlag)) + labelsMap, err := utils.ParseMapFlag(ctx.GetStringFlagValue(commands.LabelsFlag)) + if err != nil { + return nil, err + } + + return &model.AppDescriptor{ + ApplicationKey: applicationKey, + ApplicationName: applicationName, + Description: description, + MaturityLevel: maturityLevel, + BusinessCriticality: businessCriticality, + Labels: labelsMap, + UserOwners: userOwners, + GroupOwners: groupOwners, + }, nil +} + +func (uac *updateAppCommand) prepareAndRunCommand(ctx *components.Context) error { + if len(ctx.Arguments) != 1 { + return pluginsCommon.WrongNumberOfArgumentsHandler(ctx) + } + + var err error + uac.requestBody, err = uac.buildRequestPayload(ctx) + if err != nil { + return err + } + + uac.serverDetails, err = utils.ServerDetailsByFlags(ctx) + if err != nil { + return err + } + + return commonCLiCommands.Exec(uac) +} + +func GetUpdateAppCommand(appContext app.Context) components.Command { + cmd := &updateAppCommand{ + applicationService: appContext.GetApplicationService(), + } + return components.Command{ + Name: "update", + Description: "Update an existing application", + Category: common.CategoryApplication, + Arguments: []components.Argument{ + { + Name: "application-key", + Description: "The key of the application to update", + Optional: false, + }, + }, + Flags: commands.GetCommandFlags(commands.UpdateApp), + Action: cmd.prepareAndRunCommand, + } +} diff --git a/application/commands/application/update_app_cmd_test.go b/application/commands/application/update_app_cmd_test.go new file mode 100644 index 0000000..0089f2c --- /dev/null +++ b/application/commands/application/update_app_cmd_test.go @@ -0,0 +1,104 @@ +package application + +import ( + "errors" + "flag" + "testing" + + "github.com/urfave/cli" + + "github.com/jfrog/jfrog-cli-application/application/model" + mockapps "github.com/jfrog/jfrog-cli-application/application/service/applications/mocks" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestUpdateAppCommand_Run(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + serverDetails := &config.ServerDetails{Url: "https://example.com"} + appKey := "app-key" + requestPayload := &model.AppDescriptor{ + ApplicationKey: appKey, + ApplicationName: "app-name", + Description: "Updated description", + MaturityLevel: "production", + BusinessCriticality: "high", + Labels: map[string]string{ + "environment": "production", + "region": "us-east", + }, + UserOwners: []string{"JohnD", "Dave Rice"}, + GroupOwners: []string{"DevOps"}, + } + + mockAppService := mockapps.NewMockApplicationService(ctrl) + mockAppService.EXPECT().UpdateApplication(gomock.Any(), requestPayload).Return(nil).Times(1) + + cmd := &updateAppCommand{ + applicationService: mockAppService, + serverDetails: serverDetails, + requestBody: requestPayload, + } + + err := cmd.Run() + assert.NoError(t, err) +} + +func TestUpdateAppCommand_Run_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + serverDetails := &config.ServerDetails{Url: "https://example.com"} + appKey := "app-key" + requestPayload := &model.AppDescriptor{ + ApplicationKey: appKey, + ApplicationName: "app-name", + Description: "Updated description", + MaturityLevel: "production", + BusinessCriticality: "high", + Labels: map[string]string{ + "environment": "production", + "region": "us-east", + }, + UserOwners: []string{"JohnD", "Dave Rice"}, + GroupOwners: []string{"DevOps"}, + } + + mockAppService := mockapps.NewMockApplicationService(ctrl) + mockAppService.EXPECT().UpdateApplication(gomock.Any(), requestPayload).Return(errors.New("failed to update application. Status code: 500")).Times(1) + + cmd := &updateAppCommand{ + applicationService: mockAppService, + serverDetails: serverDetails, + requestBody: requestPayload, + } + + err := cmd.Run() + assert.Error(t, err) + assert.Equal(t, "failed to update application. Status code: 500", err.Error()) +} + +func TestUpdateAppCommand_WrongNumberOfArguments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + ctx := cli.NewContext(app, set, nil) + + mockAppService := mockapps.NewMockApplicationService(ctrl) + cmd := &updateAppCommand{ + applicationService: mockAppService, + } + + // Test with no arguments + context, err := components.ConvertContext(ctx) + assert.NoError(t, err) + + err = cmd.prepareAndRunCommand(context) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Wrong number of arguments") +} diff --git a/application/commands/flags.go b/application/commands/flags.go index c91048d..9ea7426 100644 --- a/application/commands/flags.go +++ b/application/commands/flags.go @@ -12,10 +12,11 @@ const ( CreateAppVersion = "create-app-version" PromoteAppVersion = "promote-app-version" CreateApp = "create-app" + UpdateApp = "update-app" ) const ( - ServerId = "server-id" + serverId = "server-id" url = "url" user = "user" accessToken = "access-token" @@ -42,7 +43,7 @@ const ( // Flag keys mapped to their corresponding components.Flag definition. var flagsMap = map[string]components.Flag{ // Common commands flags - ServerId: components.NewStringFlag(ServerId, "Server ID configured using the config command.", func(f *components.StringFlag) { f.Mandatory = false }), + serverId: components.NewStringFlag(serverId, "Server ID configured using the config command.", func(f *components.StringFlag) { f.Mandatory = false }), url: components.NewStringFlag(url, "JFrog Platform URL.", func(f *components.StringFlag) { f.Mandatory = false }), user: components.NewStringFlag(user, "JFrog username.", func(f *components.StringFlag) { f.Mandatory = false }), accessToken: components.NewStringFlag(accessToken, "JFrog access token.", func(f *components.StringFlag) { f.Mandatory = false }), @@ -71,7 +72,7 @@ var commandFlags = map[string][]string{ url, user, accessToken, - ServerId, + serverId, ApplicationKeyFlag, PackageTypeFlag, PackageNameFlag, @@ -84,7 +85,7 @@ var commandFlags = map[string][]string{ url, user, accessToken, - ServerId, + serverId, ApplicationKeyFlag, StageVarsFlag, }, @@ -93,14 +94,14 @@ var commandFlags = map[string][]string{ url, user, accessToken, - ServerId, + serverId, }, CreateApp: { url, user, accessToken, - ServerId, + serverId, ApplicationNameFlag, ProjectFlag, DescriptionFlag, @@ -113,6 +114,23 @@ var commandFlags = map[string][]string{ SpecFlag, SpecVarsFlag, }, + + UpdateApp: { + url, + user, + accessToken, + serverId, + ApplicationNameFlag, + DescriptionFlag, + BusinessCriticalityFlag, + MaturityLevelFlag, + LabelsFlag, + UserOwnersFlag, + GroupOwnersFlag, + SigningKeyFlag, + SpecFlag, + SpecVarsFlag, + }, } func GetCommandFlags(cmdKey string) []components.Flag { diff --git a/application/http/http_client.go b/application/http/http_client.go index ff25cca..bc476cc 100644 --- a/application/http/http_client.go +++ b/application/http/http_client.go @@ -7,6 +7,8 @@ import ( "fmt" "net/http" + "github.com/jfrog/jfrog-client-go/utils/log" + commonCliConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-client-go/auth" @@ -23,6 +25,7 @@ type AppHttpClient interface { GetHttpClient() *jfroghttpclient.JfrogHttpClient Post(path string, requestBody interface{}) (resp *http.Response, body []byte, err error) Get(path string) (resp *http.Response, body []byte, err error) + Patch(path string, requestBody interface{}) (resp *http.Response, body []byte, err error) } type appHttpClient struct { @@ -93,7 +96,7 @@ func (c *appHttpClient) Post(path string, requestBody interface{}) (resp *http.R return nil, nil, err } - println("url: ", url) + log.Debug("Sending POST request to:", url) return c.client.SendPost(url, requestContent, c.getJsonHttpClientDetails()) } @@ -103,10 +106,26 @@ func (c *appHttpClient) Get(path string) (resp *http.Response, body []byte, err return nil, nil, err } + log.Debug("Sending GET request to:", url) response, body, _, err := c.client.SendGet(url, false, c.getJsonHttpClientDetails()) return response, body, err } +func (c *appHttpClient) Patch(path string, requestBody interface{}) (resp *http.Response, body []byte, err error) { + url, err := utils.BuildUrl(c.serverDetails.Url, appTrustApiPath+path, nil) + if err != nil { + return nil, nil, err + } + + requestContent, err := c.toJsonBytes(requestBody) + if err != nil { + return nil, nil, err + } + + log.Debug("Sending PATCH request to:", url) + return c.client.SendPatch(url, requestContent, c.getJsonHttpClientDetails()) +} + func (c *appHttpClient) toJsonBytes(payload interface{}) ([]byte, error) { if payload == nil { return nil, fmt.Errorf("request payload is required") diff --git a/application/model/create_app_request.go b/application/model/app_descriptor.go similarity index 87% rename from application/model/create_app_request.go rename to application/model/app_descriptor.go index e7e143d..3bf5806 100644 --- a/application/model/create_app_request.go +++ b/application/model/app_descriptor.go @@ -30,10 +30,10 @@ var ( } ) -type CreateAppRequest struct { - ApplicationName string `json:"application_name"` +type AppDescriptor struct { ApplicationKey string `json:"application_key"` - ProjectKey string `json:"project_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"` diff --git a/application/service/applications/application_service.go b/application/service/applications/application_service.go index ab9e952..ec51544 100644 --- a/application/service/applications/application_service.go +++ b/application/service/applications/application_service.go @@ -13,7 +13,8 @@ import ( ) type ApplicationService interface { - CreateApplication(ctx service.Context, requestBody *model.CreateAppRequest) error + CreateApplication(ctx service.Context, requestBody *model.AppDescriptor) error + UpdateApplication(ctx service.Context, requestBody *model.AppDescriptor) error } type applicationService struct{} @@ -22,7 +23,7 @@ func NewApplicationService() ApplicationService { return &applicationService{} } -func (as *applicationService) CreateApplication(ctx service.Context, requestBody *model.CreateAppRequest) error { +func (as *applicationService) CreateApplication(ctx service.Context, requestBody *model.AppDescriptor) error { response, responseBody, err := ctx.GetHttpClient().Post("/v1/applications", requestBody) if err != nil { return err @@ -36,3 +37,19 @@ func (as *applicationService) CreateApplication(ctx service.Context, requestBody fmt.Println(string(responseBody)) return nil } + +func (as *applicationService) UpdateApplication(ctx service.Context, requestBody *model.AppDescriptor) error { + endpoint := fmt.Sprintf("/v1/applications/%s", requestBody.ApplicationKey) + response, responseBody, err := ctx.GetHttpClient().Patch(endpoint, requestBody) + if err != nil { + return err + } + + if response.StatusCode != http.StatusOK { + return errorutils.CheckErrorf("failed to update application. Status code: %d.\n%s", + response.StatusCode, responseBody) + } + + fmt.Println(string(responseBody)) + return nil +} diff --git a/application/service/applications/application_service_test.go b/application/service/applications/application_service_test.go index 36db89a..fcfb2e3 100644 --- a/application/service/applications/application_service_test.go +++ b/application/service/applications/application_service_test.go @@ -55,7 +55,7 @@ func TestApplicationService_CreateApplication(t *testing.T) { mockCtx.EXPECT().GetHttpClient().Return(mockHttpClient).Times(1) as := NewApplicationService() - err := as.CreateApplication(mockCtx, &model.CreateAppRequest{ApplicationKey: "app-123"}) + err := as.CreateApplication(mockCtx, &model.AppDescriptor{ApplicationKey: "app-123"}) if tt.expectedError != "" { assert.EqualError(t, err, tt.expectedError)