diff --git a/apptrust/commands/flags.go b/apptrust/commands/flags.go index 25674bb..6246eb6 100644 --- a/apptrust/commands/flags.go +++ b/apptrust/commands/flags.go @@ -8,17 +8,18 @@ import ( ) const ( - Ping = "ping" - VersionCreate = "version-create" - VersionPromote = "version-promote" - VersionDelete = "version-delete" - VersionRelease = "version-release" - VersionUpdate = "version-update" - PackageBind = "package-bind" - PackageUnbind = "package-unbind" - AppCreate = "app-create" - AppUpdate = "app-update" - AppDelete = "app-delete" + Ping = "ping" + VersionCreate = "version-create" + VersionPromote = "version-promote" + VersionRollback = "version-rollback" + VersionDelete = "version-delete" + VersionRelease = "version-release" + VersionUpdate = "version-update" + PackageBind = "package-bind" + PackageUnbind = "package-unbind" + AppCreate = "app-create" + AppUpdate = "app-update" + AppDelete = "app-delete" ) const ( @@ -129,6 +130,12 @@ var commandFlags = map[string][]string{ accessToken, serverId, }, + VersionRollback: { + url, + user, + accessToken, + serverId, + }, VersionUpdate: { url, user, diff --git a/apptrust/commands/version/rollback_app_version_cmd.go b/apptrust/commands/version/rollback_app_version_cmd.go new file mode 100644 index 0000000..a58f31d --- /dev/null +++ b/apptrust/commands/version/rollback_app_version_cmd.go @@ -0,0 +1,88 @@ +package version + +//go:generate ${PROJECT_DIR}/scripts/mockgen.sh ${GOFILE} + +import ( + "github.com/jfrog/jfrog-cli-application/apptrust/app" + "github.com/jfrog/jfrog-cli-application/apptrust/commands" + "github.com/jfrog/jfrog-cli-application/apptrust/commands/utils" + "github.com/jfrog/jfrog-cli-application/apptrust/common" + "github.com/jfrog/jfrog-cli-application/apptrust/model" + "github.com/jfrog/jfrog-cli-application/apptrust/service" + "github.com/jfrog/jfrog-cli-application/apptrust/service/versions" + commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands" + pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" +) + +type rollbackAppVersionCommand struct { + versionService versions.VersionService + serverDetails *coreConfig.ServerDetails + applicationKey string + version string + requestPayload *model.RollbackAppVersionRequest + fromStage string +} + +func (rv *rollbackAppVersionCommand) Run() error { + ctx, err := service.NewContext(*rv.serverDetails) + if err != nil { + return err + } + + return rv.versionService.RollbackAppVersion(ctx, rv.applicationKey, rv.version, rv.requestPayload) +} + +func (rv *rollbackAppVersionCommand) ServerDetails() (*coreConfig.ServerDetails, error) { + return rv.serverDetails, nil +} + +func (rv *rollbackAppVersionCommand) CommandName() string { + return commands.VersionRollback +} + +func (rv *rollbackAppVersionCommand) prepareAndRunCommand(ctx *components.Context) error { + if len(ctx.Arguments) != 3 { + return pluginsCommon.WrongNumberOfArgumentsHandler(ctx) + } + + rv.applicationKey = ctx.Arguments[0] + rv.version = ctx.Arguments[1] + rv.fromStage = ctx.Arguments[2] + + serverDetails, err := utils.ServerDetailsByFlags(ctx) + if err != nil { + return err + } + rv.serverDetails = serverDetails + rv.requestPayload = model.NewRollbackAppVersionRequest(rv.fromStage) + + return commonCLiCommands.Exec(rv) +} + +func GetRollbackAppVersionCommand(appContext app.Context) components.Command { + cmd := &rollbackAppVersionCommand{ + versionService: appContext.GetVersionService(), + } + return components.Command{ + Name: commands.VersionRollback, + Description: "Roll back application version promotion.", + Category: common.CategoryVersion, + Aliases: []string{"vrb"}, + Arguments: []components.Argument{ + { + Name: "application-key", + Description: "The application key.", + Optional: false, + }, + { + Name: "version", + Description: "The version to roll back.", + Optional: false, + }, + }, + Flags: commands.GetCommandFlags(commands.VersionRollback), + Action: cmd.prepareAndRunCommand, + } +} diff --git a/apptrust/commands/version/rollback_app_version_cmd_test.go b/apptrust/commands/version/rollback_app_version_cmd_test.go new file mode 100644 index 0000000..2986a77 --- /dev/null +++ b/apptrust/commands/version/rollback_app_version_cmd_test.go @@ -0,0 +1,69 @@ +package version + +import ( + "errors" + "testing" + + mockversions "github.com/jfrog/jfrog-cli-application/apptrust/service/versions/mocks" + "go.uber.org/mock/gomock" + + "github.com/jfrog/jfrog-cli-application/apptrust/model" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/stretchr/testify/assert" +) + +func TestRollbackAppVersionCommand_Run(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + serverDetails := &config.ServerDetails{Url: "https://example.com"} + applicationKey := "video-encoder" + version := "1.5.0" + requestPayload := &model.RollbackAppVersionRequest{ + FromStage: "qa", + } + + mockVersionService := mockversions.NewMockVersionService(ctrl) + mockVersionService.EXPECT().RollbackAppVersion(gomock.Any(), applicationKey, version, requestPayload). + Return(nil).Times(1) + + cmd := &rollbackAppVersionCommand{ + versionService: mockVersionService, + serverDetails: serverDetails, + applicationKey: applicationKey, + version: version, + requestPayload: requestPayload, + } + + err := cmd.Run() + assert.NoError(t, err) +} + +func TestRollbackAppVersionCommand_Run_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + serverDetails := &config.ServerDetails{Url: "https://example.com"} + applicationKey := "video-encoder" + version := "1.5.0" + requestPayload := &model.RollbackAppVersionRequest{ + FromStage: "qa", + } + expectedError := errors.New("rollback service error occurred") + + mockVersionService := mockversions.NewMockVersionService(ctrl) + mockVersionService.EXPECT().RollbackAppVersion(gomock.Any(), applicationKey, version, requestPayload). + Return(expectedError).Times(1) + + cmd := &rollbackAppVersionCommand{ + versionService: mockVersionService, + serverDetails: serverDetails, + applicationKey: applicationKey, + version: version, + requestPayload: requestPayload, + } + + err := cmd.Run() + assert.Error(t, err) + assert.Contains(t, err.Error(), "rollback service error occurred") +} diff --git a/apptrust/model/rollback_app_version_request.go b/apptrust/model/rollback_app_version_request.go new file mode 100644 index 0000000..f7516d6 --- /dev/null +++ b/apptrust/model/rollback_app_version_request.go @@ -0,0 +1,19 @@ +package model + +type RollbackAppVersionRequest struct { + FromStage string `json:"from_stage"` +} + +type RollbackAppVersionResponse struct { + ApplicationKey string `json:"application_key"` + Version string `json:"version"` + ProjectKey string `json:"project_key"` + RollbackFromStage string `json:"rollback_from_stage"` + RollbackToStage string `json:"rollback_to_stage"` +} + +func NewRollbackAppVersionRequest(fromStage string) *RollbackAppVersionRequest { + return &RollbackAppVersionRequest{ + FromStage: fromStage, + } +} diff --git a/apptrust/service/versions/version_service.go b/apptrust/service/versions/version_service.go index eebd0d0..5521066 100644 --- a/apptrust/service/versions/version_service.go +++ b/apptrust/service/versions/version_service.go @@ -16,6 +16,7 @@ type VersionService interface { CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest) error PromoteAppVersion(ctx service.Context, applicationKey string, version string, payload *model.PromoteAppVersionRequest, sync bool) error ReleaseAppVersion(ctx service.Context, applicationKey string, version string, request *model.ReleaseAppVersionRequest, sync bool) error + RollbackAppVersion(ctx service.Context, applicationKey string, version string, request *model.RollbackAppVersionRequest) error DeleteAppVersion(ctx service.Context, applicationKey string, version string) error UpdateAppVersion(ctx service.Context, applicationKey string, version string, request *model.UpdateAppVersionRequest) error } @@ -71,6 +72,21 @@ func (vs *versionService) ReleaseAppVersion(ctx service.Context, applicationKey, return nil } +func (vs *versionService) RollbackAppVersion(ctx service.Context, applicationKey, version string, request *model.RollbackAppVersionRequest) error { + endpoint := fmt.Sprintf("/v1/applications/%s/versions/%s/rollback", applicationKey, version) + response, responseBody, err := ctx.GetHttpClient().Post(endpoint, request, map[string]string{}) + if err != nil { + return err + } + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted { + return fmt.Errorf("failed to rollback app version. Status code: %d. \n%s", + response.StatusCode, responseBody) + } + + return nil +} + func (vs *versionService) DeleteAppVersion(ctx service.Context, applicationKey, version string) error { url := fmt.Sprintf("/v1/applications/%s/versions/%s", applicationKey, version) response, responseBody, err := ctx.GetHttpClient().Delete(url) diff --git a/apptrust/service/versions/version_service_test.go b/apptrust/service/versions/version_service_test.go index fb38874..035e62a 100644 --- a/apptrust/service/versions/version_service_test.go +++ b/apptrust/service/versions/version_service_test.go @@ -415,3 +415,70 @@ func TestUpdateAppVersion(t *testing.T) { }) } } + +func TestRollbackAppVersion(t *testing.T) { + tests := []struct { + name string + applicationKey string + version string + payload *model.RollbackAppVersionRequest + expectedStatus int + expectedError bool + }{ + { + name: "successful rollback with 200", + applicationKey: "video-encoder", + version: "1.5.0", + payload: &model.RollbackAppVersionRequest{ + FromStage: "qa", + }, + expectedStatus: http.StatusOK, + expectedError: false, + }, + { + name: "successful rollback with 204", + applicationKey: "video-encoder", + version: "1.5.0", + payload: &model.RollbackAppVersionRequest{ + FromStage: "prod", + }, + expectedStatus: http.StatusAccepted, + expectedError: false, + }, + { + name: "failed rollback - bad request", + applicationKey: "invalid-app", + version: "1.0.0", + payload: &model.RollbackAppVersionRequest{ + FromStage: "nonexistent", + }, + expectedStatus: http.StatusBadRequest, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCtx := mockservice.NewMockContext(ctrl) + mockClient := mockhttp.NewMockApptrustHttpClient(ctrl) + mockCtx.EXPECT().GetHttpClient().Return(mockClient) + + expectedEndpoint := "/v1/applications/" + tt.applicationKey + "/versions/" + tt.version + "/rollback" + mockClient.EXPECT().Post(expectedEndpoint, tt.payload, map[string]string{}). + Return(&http.Response{StatusCode: tt.expectedStatus}, []byte(""), nil) + + service := NewVersionService() + err := service.RollbackAppVersion(mockCtx, tt.applicationKey, tt.version, tt.payload) + + if tt.expectedError { + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to rollback app version") + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cli/cli.go b/cli/cli.go index e5006a5..fc43351 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -45,6 +45,7 @@ func GetJfrogCliApptrustApp() components.App { system.GetPingCommand(appContext), version.GetCreateAppVersionCommand(appContext), version.GetPromoteAppVersionCommand(appContext), + version.GetRollbackAppVersionCommand(appContext), version.GetReleaseAppVersionCommand(appContext), version.GetDeleteAppVersionCommand(appContext), version.GetUpdateAppVersionCommand(appContext),