diff --git a/commands/common/cmd_worker_api.go b/commands/common/cmd_worker_api.go index e415a2c..b07af3a 100644 --- a/commands/common/cmd_worker_api.go +++ b/commands/common/cmd_worker_api.go @@ -12,7 +12,6 @@ import ( // FetchWorkerDetails Fetch a worker by its name. Returns nil if the worker does not exist (statusCode=404). Any other statusCode other than 200 will result as an error. func FetchWorkerDetails(c model.IntFlagProvider, serverUrl string, accessToken string, workerKey string, projectKey string) (*model.WorkerDetails, error) { details := new(model.WorkerDetails) - err := CallWorkerApi(c, ApiCallParams{ Method: http.MethodGet, ServerUrl: serverUrl, @@ -22,9 +21,9 @@ func FetchWorkerDetails(c model.IntFlagProvider, serverUrl string, accessToken s Path: []string{"workers", workerKey}, OnContent: func(content []byte) error { if len(content) == 0 { - log.Debug("No worker details returned from the server") return nil } + log.Info(fmt.Sprintf("Worker %s details returned from the server", details.Key)) return json.Unmarshal(content, details) }, }) @@ -33,9 +32,9 @@ func FetchWorkerDetails(c model.IntFlagProvider, serverUrl string, accessToken s } if details.Key == "" { + log.Info(fmt.Sprintf("Worker %s does not exist", workerKey)) return nil, nil } - return details, nil } @@ -64,3 +63,27 @@ func FetchActions(c model.IntFlagProvider, serverUrl string, accessToken string, return metadata, nil } + +func FetchOptions(c model.IntFlagProvider, serverUrl string, accessToken string) (*OptionsMetadata, error) { + metadata := new(OptionsMetadata) + + err := CallWorkerApi(c, ApiCallParams{ + Method: http.MethodGet, + ServerUrl: serverUrl, + ServerToken: accessToken, + OkStatuses: []int{http.StatusOK}, + ApiVersion: ApiVersionV1, + Path: []string{"options"}, + OnContent: func(content []byte) error { + if len(content) == 0 { + log.Debug("No options returned from the server") + return nil + } + return json.Unmarshal(content, &metadata) + }, + }) + if err != nil { + return nil, fmt.Errorf("cannot fetch options: %w", err) + } + return metadata, nil +} diff --git a/commands/common/cmd_worker_api_test.go b/commands/common/cmd_worker_api_test.go index 2cfb874..17a1ade 100644 --- a/commands/common/cmd_worker_api_test.go +++ b/commands/common/cmd_worker_api_test.go @@ -105,3 +105,11 @@ func TestFetchActions(t *testing.T) { }) } } + +func TestFetchOptions(t *testing.T) { + samples := LoadSampleOptions(t) + s, token := NewMockWorkerServer(t, NewServerStub(t).WithOptionsEndpoint().WithT(t)) + got, err := FetchOptions(IntFlagMap{}, s.BaseUrl(), token) + require.NoError(t, err) + assert.Equal(t, got, samples) +} diff --git a/commands/common/options.go b/commands/common/options.go new file mode 100644 index 0000000..edb4fde --- /dev/null +++ b/commands/common/options.go @@ -0,0 +1,16 @@ +package common + +type EditionOptions struct { + MaxCodeChars int `json:"maxCodeChars"` + MaxVersionNumberChars int `json:"maxVersionNumberChars"` + MaxVersionCommitShaChars int `json:"maxVersionCommitShaChars"` + MaxVersionDescriptionChars int `json:"maxVersionDescriptionChars"` +} + +type OptionsMetadata struct { + Edition EditionOptions `json:"edition"` + MinArtifactoryVersionForProjectSupport string `json:"minArtifactoryVersionForProjectSupport"` + IsTutorialAvailable bool `json:"isTutorialAvailable"` + IsFeedbackEnabled bool `json:"isFeedbackEnabled"` + IsHistoryEnabled bool `json:"isHistoryEnabled"` +} diff --git a/commands/common/test_commons.go b/commands/common/test_commons.go index c30403f..c204ea1 100644 --- a/commands/common/test_commons.go +++ b/commands/common/test_commons.go @@ -30,8 +30,8 @@ type Test interface { const SecretPassword = "P@ssw0rd!" -//go:embed testdata/actions/* -var sampleActions embed.FS +//go:embed testdata/* +var sampleFiles embed.FS func SetCliIn(reader io.Reader) { cliIn = reader @@ -159,11 +159,11 @@ func MustEncryptSecret(t require.TestingT, secretValue string, password ...strin func LoadSampleActions(t require.TestingT) ActionsMetadata { var metadata ActionsMetadata - actionsFiles, err := sampleActions.ReadDir("testdata/actions") + actionsFiles, err := sampleFiles.ReadDir("testdata/actions") require.NoError(t, err) for _, file := range actionsFiles { - content, err := sampleActions.ReadFile("testdata/actions/" + file.Name()) + content, err := sampleFiles.ReadFile("testdata/actions/" + file.Name()) require.NoError(t, err) action := &model.ActionMetadata{} @@ -187,6 +187,18 @@ func LoadSampleActionEvents(t require.TestingT) []string { return events } +func LoadSampleOptions(t require.TestingT) *OptionsMetadata { + // var metadata OptionsMetadata + + content, err := sampleFiles.ReadFile("testdata/options.json") + require.NoError(t, err) + options := &OptionsMetadata{} + err = json.Unmarshal(content, options) + require.NoError(t, err) + + return options +} + func TestSetEnv(t Test, key, value string) { err := os.Setenv(key, value) require.NoError(t, err) diff --git a/commands/common/test_worker_server.go b/commands/common/test_worker_server.go index ee5b559..1faa1ca 100644 --- a/commands/common/test_worker_server.go +++ b/commands/common/test_worker_server.go @@ -26,6 +26,19 @@ const ( type BodyValidator func(t require.TestingT, content []byte) +type workerDeployPayload struct { + Key string `json:"key"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + Debug bool `json:"debug"` + SourceCode string `json:"sourceCode"` + Action model.Action `json:"action"` + FilterCriteria *model.FilterCriteria `json:"filterCriteria,omitempty"` + Secrets []*model.Secret `json:"secrets"` + ProjectKey string `json:"projectKey"` + Version *model.Version `json:"version,omitempty"` +} + func ValidateJson(expected any) BodyValidator { return ValidateJsonFunc(expected, func(in any) any { return in @@ -150,7 +163,7 @@ func (s *ServerStub) WithCreateEndpoint(validateBody BodyValidator) *ServerStub s.endpoints = append(s.endpoints, mockhttp.NewServerEndpoint(). When( - mockhttp.Request().POST("/worker/api/v1/workers"), + mockhttp.Request().POST("/worker/api/v2/workers"), ). HandleWith(s.handleSave(http.StatusCreated, validateBody)), ) @@ -176,7 +189,7 @@ func (s *ServerStub) WithUpdateEndpoint(validateBody BodyValidator) *ServerStub s.endpoints = append(s.endpoints, mockhttp.NewServerEndpoint(). When( - mockhttp.Request().PUT("/worker/api/v1/workers"), + mockhttp.Request().PUT("/worker/api/v2/workers"), ). HandleWith(s.handleSave(http.StatusNoContent, validateBody)), ) @@ -269,6 +282,17 @@ func (s *ServerStub) WithGetExecutionHistoryEndpoint() *ServerStub { return s } +func (s *ServerStub) WithOptionsEndpoint() *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request().GET("/worker/api/v1/options"), + ). + HandleWith(s.handleGetOptions), + ) + return s +} + func (s *ServerStub) handleGetAll(res http.ResponseWriter, req *http.Request) { s.applyDelay() @@ -419,10 +443,12 @@ func (s *ServerStub) handleSave(status int, validateBody BodyValidator) http.Han validateBody(s.test, content) } - workerDetails := &model.WorkerDetails{} - err = json.Unmarshal(content, workerDetails) + worker := workerDeployPayload{} + + err = json.Unmarshal(content, &worker) require.NoError(s.test, err) + workerDetails := mapWorkerSentToWorkerDetails(worker) s.workers[workerDetails.Key] = workerDetails res.WriteHeader(status) @@ -456,6 +482,20 @@ func (s *ServerStub) handleGetAllMetadata(metadata ActionsMetadata) http.Handler } } +func (s *ServerStub) handleGetOptions(res http.ResponseWriter, req *http.Request) { + s.applyDelay() + + if !s.validateToken(res, req) { + return + } + + res.WriteHeader(http.StatusOK) + + options := LoadSampleOptions(s.test) + _, err := res.Write([]byte(MustJsonMarshal(s.test, options))) + require.NoError(s.test, err) +} + func (s *ServerStub) handle(status int, validateBody BodyValidator, responseBody any) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { s.applyDelay() @@ -543,3 +583,17 @@ func (s *ServerStub) applyDelay() { time.Sleep(s.waitFor) } } + +func mapWorkerSentToWorkerDetails(workerSent workerDeployPayload) *model.WorkerDetails { + return &model.WorkerDetails{ + Key: workerSent.Key, + Description: workerSent.Description, + Enabled: workerSent.Enabled, + Debug: workerSent.Debug, + SourceCode: workerSent.SourceCode, + Action: workerSent.Action.Name, // Map Action.Name + FilterCriteria: workerSent.FilterCriteria, + Secrets: workerSent.Secrets, + ProjectKey: workerSent.ProjectKey, + } +} diff --git a/commands/common/testdata/options.json b/commands/common/testdata/options.json new file mode 100644 index 0000000..aece9ee --- /dev/null +++ b/commands/common/testdata/options.json @@ -0,0 +1,12 @@ +{ + "edition": { + "maxCodeChars": 100000, + "maxVersionNumberChars": 64, + "maxVersionCommitShaChars": 128, + "maxVersionDescriptionChars": 256 + }, + "minArtifactoryVersionForProjectSupport": "7.95.0", + "isTutorialAvailable": false, + "isFeedbackEnabled": false, + "isHistoryEnabled": true +} \ No newline at end of file diff --git a/commands/common/version.go b/commands/common/version.go new file mode 100644 index 0000000..8b9d097 --- /dev/null +++ b/commands/common/version.go @@ -0,0 +1,20 @@ +package common + +import ( + "fmt" + + "github.com/jfrog/jfrog-cli-platform-services/model" +) + +func ValidateVersion(version *model.Version, options *OptionsMetadata) error { + if len(version.Number) > options.Edition.MaxVersionNumberChars { + return fmt.Errorf("version number exceeds maximum length of %d characters", options.Edition.MaxVersionNumberChars) + } + if len(version.CommitSha) > options.Edition.MaxVersionCommitShaChars { + return fmt.Errorf("commit sha exceeds maximum length of %d characters", options.Edition.MaxVersionCommitShaChars) + } + if len(version.Description) > options.Edition.MaxVersionDescriptionChars { + return fmt.Errorf("description exceeds maximum length of %d characters", options.Edition.MaxVersionDescriptionChars) + } + return nil +} diff --git a/commands/deploy_cmd.go b/commands/deploy_cmd.go index b37a6de..5f931a5 100644 --- a/commands/deploy_cmd.go +++ b/commands/deploy_cmd.go @@ -20,10 +20,11 @@ type deployRequest struct { Enabled bool `json:"enabled"` Debug bool `json:"debug"` SourceCode string `json:"sourceCode"` - Action string `json:"action"` + Action model.Action `json:"action"` FilterCriteria *model.FilterCriteria `json:"filterCriteria,omitempty"` Secrets []*model.Secret `json:"secrets"` ProjectKey string `json:"projectKey"` + Version *model.Version `json:"version,omitempty"` } func GetDeployCommand() components.Command { @@ -35,6 +36,9 @@ func GetDeployCommand() components.Command { plugins_common.GetServerIdFlag(), model.GetTimeoutFlag(), model.GetNoSecretsFlag(), + model.GetChangesVersionFlag(), + model.GetChangesDescriptionFlag(), + model.GetChangesCommitShaFlag(), }, Action: func(c *components.Context) error { server, err := model.GetServerDetails(c) @@ -71,18 +75,32 @@ func GetDeployCommand() components.Command { } } - return runDeployCommand(c, manifest, actionMeta, server.GetUrl(), server.GetAccessToken()) + version := &model.Version{ + Number: c.GetStringFlagValue(model.FlagChangesVersion), + Description: c.GetStringFlagValue(model.FlagChangesDescription), + CommitSha: c.GetStringFlagValue(model.FlagChangesCommitSha), + } + if !version.IsEmpty() { + options, err := common.FetchOptions(c, server.GetUrl(), server.GetAccessToken()) + if err != nil { + return err + } + if err = common.ValidateVersion(version, options); err != nil { + return err + } + } + return runDeployCommand(c, manifest, actionMeta, version, server.GetUrl(), server.GetAccessToken()) }, } } -func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, serverUrl string, token string) error { +func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, version *model.Version, serverUrl string, token string) error { existingWorker, err := common.FetchWorkerDetails(ctx, serverUrl, token, manifest.Name, manifest.ProjectKey) if err != nil { return err } - body, err := prepareDeployRequest(ctx, manifest, actionMeta, existingWorker) + body, err := prepareDeployRequest(ctx, manifest, actionMeta, version, existingWorker) if err != nil { return err } @@ -101,6 +119,7 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionM Body: bodyBytes, OkStatuses: []int{http.StatusCreated}, Path: []string{"workers"}, + ApiVersion: common.ApiVersionV2, }) if err == nil { log.Info(fmt.Sprintf("Worker '%s' deployed", manifest.Name)) @@ -116,6 +135,7 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionM Body: bodyBytes, OkStatuses: []int{http.StatusNoContent}, Path: []string{"workers"}, + ApiVersion: common.ApiVersionV2, }) if err == nil { log.Info(fmt.Sprintf("Worker '%s' updated", manifest.Name)) @@ -124,7 +144,7 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionM return err } -func prepareDeployRequest(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, existingWorker *model.WorkerDetails) (*deployRequest, error) { +func prepareDeployRequest(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, version *model.Version, existingWorker *model.WorkerDetails) (*deployRequest, error) { sourceCode, err := common.ReadSourceCode(manifest) if err != nil { return nil, err @@ -136,21 +156,20 @@ func prepareDeployRequest(ctx *components.Context, manifest *model.Manifest, act if !ctx.GetBoolFlagValue(model.FlagNoSecrets) { secrets = common.PrepareSecretsUpdate(manifest, existingWorker) } - payload := &deployRequest{ Key: manifest.Name, - Action: manifest.Action, + Action: actionMeta.Action, Description: manifest.Description, Enabled: manifest.Enabled, Debug: manifest.Debug, SourceCode: sourceCode, Secrets: secrets, ProjectKey: manifest.ProjectKey, + Version: version, } if actionMeta.MandatoryFilter { payload.FilterCriteria = manifest.FilterCriteria } - return payload, nil } diff --git a/commands/deploy_cmd_test.go b/commands/deploy_cmd_test.go index 9b9c6f1..293dacc 100644 --- a/commands/deploy_cmd_test.go +++ b/commands/deploy_cmd_test.go @@ -138,6 +138,39 @@ func TestDeployCommand(t *testing.T) { commandArgs: []string{"--" + model.FlagTimeout, "abc"}, wantErr: errors.New("invalid timeout provided"), }, + { + name: "create with version", + workerAction: "BEFORE_UPLOAD", + workerName: "wk-0", + serverBehavior: common.NewServerStub(t). + WithGetOneEndpoint(). + WithOptionsEndpoint(). + WithCreateEndpoint( + expectDeployRequest( + actionsMeta, + "wk-0", + "BEFORE_UPLOAD", + "", + ), + ), + commandArgs: []string{"--" + model.FlagChangesVersion, "version number", "--" + model.FlagChangesDescription, "version description", "--" + model.FlagChangesCommitSha, "version commitsha"}, + }, + { + name: "fails when version invalid", + serverBehavior: common.NewServerStub(t). + WithGetOneEndpoint(). + WithOptionsEndpoint(). + WithCreateEndpoint( + expectDeployRequest( + actionsMeta, + "wk-0", + "BEFORE_UPLOAD", + "", + ), + ), + commandArgs: []string{"--" + model.FlagChangesVersion, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod."}, + wantErr: errors.New("version number exceeds maximum length of 64 characters"), + }, } for _, tt := range tests { @@ -212,6 +245,9 @@ func getExpectedDeployRequestForAction( workerName, actionName, projectKey string, secrets ...*model.Secret, ) *deployRequest { + actionMeta, err := actionsMeta.FindAction(actionName) + require.NoError(t, err) + r := &deployRequest{ Key: workerName, Description: "Run a script on " + actionName, @@ -224,14 +260,11 @@ func getExpectedDeployRequestForAction( "", "worker.ts_template", )), - Action: actionName, + Action: actionMeta.Action, Secrets: secrets, ProjectKey: projectKey, } - actionMeta, err := actionsMeta.FindAction(actionName) - require.NoError(t, err) - if actionMeta.MandatoryFilter && actionMeta.FilterType == model.FilterTypeRepo { r.FilterCriteria = &model.FilterCriteria{ ArtifactFilterCriteria: &model.ArtifactFilterCriteria{ diff --git a/model/flags.go b/model/flags.go index 9ea8858..05dc06d 100644 --- a/model/flags.go +++ b/model/flags.go @@ -14,15 +14,18 @@ import ( ) const ( - FlagForce = "force" - FlagNoTest = "no-test" - FlagEdit = "edit" - FlagNoSecrets = "no-secrets" - FlagJsonOutput = "json" - FlagTimeout = "timeout-ms" - FlagProjectKey = "project-key" - FlagApplication = "application" - defaultTimeoutMillis = 5000 + FlagForce = "force" + FlagNoTest = "no-test" + FlagEdit = "edit" + FlagNoSecrets = "no-secrets" + FlagJsonOutput = "json" + FlagTimeout = "timeout-ms" + FlagProjectKey = "project-key" + FlagApplication = "application" + FlagChangesVersion = "changes-version" + FlagChangesDescription = "changes-description" + FlagChangesCommitSha = "changes-commitsha" + defaultTimeoutMillis = 5000 ) var ( @@ -104,6 +107,18 @@ func GetApplicationFlag() components.StringFlag { ) } +func GetChangesVersionFlag() components.StringFlag { + return components.NewStringFlag(FlagChangesVersion, "Version identifier for the worker.", components.WithStrDefaultValue("")) +} + +func GetChangesDescriptionFlag() components.StringFlag { + return components.NewStringFlag(FlagChangesDescription, "Description of your changes.", components.WithStrDefaultValue("")) +} + +func GetChangesCommitShaFlag() components.StringFlag { + return components.NewStringFlag(FlagChangesCommitSha, "Commit identifier or your change in your VCS.", components.WithStrDefaultValue("")) +} + func GetServerDetails(c *components.Context) (*config.ServerDetails, error) { serverUrlFromEnv, envHasServerUrl := os.LookupEnv(EnvKeyServerUrl) accessTokenFromEnv, envHasAccessToken := os.LookupEnv(EnvKeyAccessToken) diff --git a/model/version.go b/model/version.go new file mode 100644 index 0000000..493e4f2 --- /dev/null +++ b/model/version.go @@ -0,0 +1,11 @@ +package model + +type Version struct { + CommitSha string `json:"commitSha"` + Description string `json:"description"` + Number string `json:"versionNumber"` +} + +func (v *Version) IsEmpty() bool { + return v == nil || (v.CommitSha == "" && v.Description == "" && v.Number == "") +} diff --git a/test/commands/deploy_cmd_test.go b/test/commands/deploy_cmd_test.go index 8d14a02..974964b 100644 --- a/test/commands/deploy_cmd_test.go +++ b/test/commands/deploy_cmd_test.go @@ -35,6 +35,11 @@ func TestDeployCommand(t *testing.T) { name: "create", workerKey: "wk-0", }), + deployTestSpec(deployTestCase{ + name: "create with a version", + workerKey: "wk-0", + commandArgs: []string{"--" + model.FlagChangesVersion, "1.0.0", "--" + model.FlagChangesDescription, "Creation of test worker"}, + }), deployTestSpec(deployTestCase{ name: "deploy scheduled event", workerKey: "wk-1_1",