From a38304dadf76edfd53c294e9edc7fa0bcf7bb7a7 Mon Sep 17 00:00:00 2001 From: Herve ESTEGUET Date: Tue, 19 Nov 2024 10:45:55 +0100 Subject: [PATCH] WKS-1193 - Use dynamic actions --- .github/workflows/analysis.yml | 21 +- .gitignore | 3 +- .golangci.yml | 11 +- Makefile | 14 +- commands/add_secret_cmd.go | 14 +- commands/add_secret_cmd_test.go | 37 +- commands/cmd_input_reader.go | 71 --- commands/commands_commons.go | 273 ----------- commands/commands_commons_for_itest.go | 13 - commands/common/actions_metadata.go | 51 ++ .../common/clean_import.go | 2 +- commands/common/cmd_api.go | 138 ++++++ commands/common/cmd_api_test.go | 139 ++++++ commands/common/cmd_commons.go | 90 ++++ .../cmd_commons_test.go} | 140 +----- commands/common/cmd_io.go | 112 +++++ commands/common/cmd_worker_api.go | 62 +++ commands/common/cmd_worker_api_test.go | 107 +++++ commands/common/manifest.go | 149 ++++++ {model => commands/common}/manifest_test.go | 107 +++-- {model => commands/common}/secrets.go | 12 +- {model => commands/common}/secrets_test.go | 5 +- commands/common/test_commons.go | 250 ++++++++++ commands/common/test_worker_server.go | 446 ++++++++++++++++++ .../testdata/actions/afterBuildInfoSave.json | 14 + .../common/testdata/actions/afterCreate.json | 16 + .../testdata/actions/afterDownload.json | 16 + .../common/testdata/actions/afterMove.json | 16 + .../common/testdata/actions/beforeCreate.json | 15 + .../testdata/actions/beforeCreateToken.json | 12 + .../common/testdata/actions/beforeDelete.json | 15 + .../testdata/actions/beforeDownload.json | 15 + .../common/testdata/actions/beforeMove.json | 15 + .../actions/beforePropertyCreate.json | 15 + .../actions/beforePropertyDelete.json | 15 + .../testdata/actions/beforeRevokeToken.json | 12 + .../common/testdata/actions/beforeUpload.json | 15 + .../common/testdata/actions/genericEvent.json | 11 + .../testdata/actions/scheduledEvent.json | 15 + commands/common/typescript.go | 67 +++ commands/common/typescript_test.go | 219 +++++++++ commands/deploy_cmd.go | 41 +- commands/deploy_cmd_test.go | 298 ++++-------- commands/dry_run_cmd.go | 57 ++- commands/dry_run_cmd_test.go | 305 +++--------- commands/execute_cmd.go | 24 +- commands/execute_cmd_test.go | 309 +++--------- commands/init_cmd.go | 141 ++++-- commands/init_cmd_test.go | 55 ++- commands/list_cmd.go | 98 ++-- commands/list_cmd_test.go | 228 +++------ commands/list_event_cmd.go | 17 +- commands/list_event_cmd_test.go | 162 +------ commands/remove_cmd.go | 12 +- commands/remove_cmd_test.go | 143 ++---- .../AFTER_BUILD_INFO_SAVE.spec.ts_template | 26 - .../AFTER_BUILD_INFO_SAVE.ts_template | 24 - .../templates/AFTER_CREATE.spec.ts_template | 25 - commands/templates/AFTER_CREATE.ts_template | 24 - .../templates/AFTER_DOWNLOAD.spec.ts_template | 25 - commands/templates/AFTER_DOWNLOAD.ts_template | 24 - .../templates/AFTER_MOVE.spec.ts_template | 25 - commands/templates/AFTER_MOVE.ts_template | 23 - .../BEFORE_CREATE_TOKEN.spec.ts_template | 26 - .../templates/BEFORE_CREATE_TOKEN.ts_template | 30 -- .../BEFORE_DOWNLOAD.spec.ts_template | 26 - .../templates/BEFORE_DOWNLOAD.ts_template | 30 -- .../BEFORE_PROPERTY_CREATE.spec.ts_template | 29 -- .../BEFORE_PROPERTY_CREATE.ts_template | 27 -- .../templates/BEFORE_UPLOAD.spec.ts_template | 29 -- commands/templates/BEFORE_UPLOAD.ts_template | 30 -- commands/templates/GENERIC_EVENT.ts_template | 50 -- commands/templates/manifest.json_template | 9 +- ...ec.ts_template => worker.spec.ts_template} | 28 +- commands/templates/worker.ts_template | 6 + go.mod | 56 +-- go.sum | 149 +++--- model/actions.go | 52 +- model/actions_test.go | 37 -- model/flags.go | 13 +- model/flags_test.go | 2 +- model/manifest.go | 144 +----- model/worker.go | 18 +- qa-plugin/Makefile | 2 +- qa-plugin/go.mod | 56 ++- qa-plugin/go.sum | 147 +++--- test/commands/deploy_cmd_test.go | 18 +- test/commands/dry_run_cmd_test.go | 18 +- test/commands/execute_cmd_test.go | 14 +- test/commands/list_cmd_test.go | 13 +- test/commands/remove_cmd_test.go | 4 +- test/infra/itest_runner.go | 12 +- test/infra/secrets_utils.go | 20 +- test/infra/test_utils.go | 60 --- 94 files changed, 3107 insertions(+), 2904 deletions(-) delete mode 100644 commands/cmd_input_reader.go delete mode 100644 commands/commands_commons.go delete mode 100644 commands/commands_commons_for_itest.go create mode 100644 commands/common/actions_metadata.go rename model/utils.go => commands/common/clean_import.go (95%) create mode 100644 commands/common/cmd_api.go create mode 100644 commands/common/cmd_api_test.go create mode 100644 commands/common/cmd_commons.go rename commands/{commands_commons_test.go => common/cmd_commons_test.go} (50%) create mode 100644 commands/common/cmd_io.go create mode 100644 commands/common/cmd_worker_api.go create mode 100644 commands/common/cmd_worker_api_test.go create mode 100644 commands/common/manifest.go rename {model => commands/common}/manifest_test.go (66%) rename {model => commands/common}/secrets.go (92%) rename {model => commands/common}/secrets_test.go (95%) create mode 100644 commands/common/test_commons.go create mode 100644 commands/common/test_worker_server.go create mode 100644 commands/common/testdata/actions/afterBuildInfoSave.json create mode 100644 commands/common/testdata/actions/afterCreate.json create mode 100644 commands/common/testdata/actions/afterDownload.json create mode 100644 commands/common/testdata/actions/afterMove.json create mode 100644 commands/common/testdata/actions/beforeCreate.json create mode 100644 commands/common/testdata/actions/beforeCreateToken.json create mode 100644 commands/common/testdata/actions/beforeDelete.json create mode 100644 commands/common/testdata/actions/beforeDownload.json create mode 100644 commands/common/testdata/actions/beforeMove.json create mode 100644 commands/common/testdata/actions/beforePropertyCreate.json create mode 100644 commands/common/testdata/actions/beforePropertyDelete.json create mode 100644 commands/common/testdata/actions/beforeRevokeToken.json create mode 100644 commands/common/testdata/actions/beforeUpload.json create mode 100644 commands/common/testdata/actions/genericEvent.json create mode 100644 commands/common/testdata/actions/scheduledEvent.json create mode 100644 commands/common/typescript.go create mode 100644 commands/common/typescript_test.go delete mode 100644 commands/templates/AFTER_BUILD_INFO_SAVE.spec.ts_template delete mode 100644 commands/templates/AFTER_BUILD_INFO_SAVE.ts_template delete mode 100644 commands/templates/AFTER_CREATE.spec.ts_template delete mode 100644 commands/templates/AFTER_CREATE.ts_template delete mode 100644 commands/templates/AFTER_DOWNLOAD.spec.ts_template delete mode 100644 commands/templates/AFTER_DOWNLOAD.ts_template delete mode 100644 commands/templates/AFTER_MOVE.spec.ts_template delete mode 100644 commands/templates/AFTER_MOVE.ts_template delete mode 100644 commands/templates/BEFORE_CREATE_TOKEN.spec.ts_template delete mode 100644 commands/templates/BEFORE_CREATE_TOKEN.ts_template delete mode 100644 commands/templates/BEFORE_DOWNLOAD.spec.ts_template delete mode 100644 commands/templates/BEFORE_DOWNLOAD.ts_template delete mode 100644 commands/templates/BEFORE_PROPERTY_CREATE.spec.ts_template delete mode 100644 commands/templates/BEFORE_PROPERTY_CREATE.ts_template delete mode 100644 commands/templates/BEFORE_UPLOAD.spec.ts_template delete mode 100644 commands/templates/BEFORE_UPLOAD.ts_template delete mode 100644 commands/templates/GENERIC_EVENT.ts_template rename commands/templates/{GENERIC_EVENT.spec.ts_template => worker.spec.ts_template} (56%) create mode 100644 commands/templates/worker.ts_template delete mode 100644 model/actions_test.go delete mode 100644 test/infra/test_utils.go diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 959b2d9..2134bc4 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -19,23 +19,6 @@ jobs: go-version-file: go.mod - name: Static Code Analysis - uses: golangci/golangci-lint-action@v5 + uses: golangci/golangci-lint-action@v6 with: - version: latest - - - Go-Sec: - runs-on: ubuntu-latest - steps: - - name: Checkout Source - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Run Gosec Security Scanner - uses: securego/gosec@v2.18.0 - with: - args: -exclude G204,G301,G302,G304,G306,G601,G101 -tests -exclude-dir \.*test\.* ./... \ No newline at end of file + version: latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff8084b..68984c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea .tools bin -*-nogit* \ No newline at end of file +*-nogit* +**/mocks/* \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 082b9bc..fca63ef 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -10,7 +10,7 @@ linters: disable-all: true enable: - errcheck - - exportloopref + - copyloopvar # - depguard # - gci - gofumpt @@ -19,7 +19,6 @@ linters: - govet - ineffassign - makezero - - megacheck - misspell - noctx - nolintlint @@ -31,11 +30,19 @@ linters: - unconvert - unused - wastedassign + - gosec linters-settings: staticcheck: # https://staticcheck.io/docs/options#checks checks: [ "all","-SA1019","-SA1029" ] + gosec: + excludes: ["G204", "G301", "G302", "G304", "G306", "G601", "G101", "G407"] + exclude-generated: true + exclude-test-files: true + config: + global: + nosec: true issues: exclude-use-default: false diff --git a/Makefile b/Makefile index 7090496..eb8d177 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ GOIMPORTS: ########## ANALYSE ########## GOLANGCI_LINT = ${TOOLS_DIR}/golangci-lint -GOLANGCI_LINT_VERSION = 1.55.2 +GOLANGCI_LINT_VERSION = 1.62.2 verify: GOLANGCI_LINT echo $(GO_SOURCES) @@ -59,7 +59,17 @@ build-install:: build ########## TEST ########## -test-prereq: prereq +.PHONY: clean-mock +clean-mock: + @echo Cleaning generated mock files + find . -path "*/mocks/*.go" -delete + +.PHONY: generate-mock +generate-mock: clean-mock + @echo Generating test mocks + go generate ./... + +test-prereq: prereq generate-mock mkdir -p target/reports test: PACKAGES=./... diff --git a/commands/add_secret_cmd.go b/commands/add_secret_cmd.go index 1a315b8..f282d74 100644 --- a/commands/add_secret_cmd.go +++ b/commands/add_secret_cmd.go @@ -4,6 +4,8 @@ import ( "fmt" "os" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" @@ -38,12 +40,12 @@ func GetAddSecretCommand() components.Command { } func (c *addSecretCommand) run() error { - manifest, err := model.ReadManifest() + manifest, err := common.ReadManifest() if err != nil { return err } - if err = manifest.Validate(); err != nil { + if err = common.ValidateManifest(manifest, nil); err != nil { return err } @@ -57,7 +59,7 @@ func (c *addSecretCommand) run() error { return err } - encryptionKey, err := model.ReadSecretPassword() + encryptionKey, err := common.ReadSecretPassword() if err != nil { return err } @@ -67,7 +69,7 @@ func (c *addSecretCommand) run() error { return err } - encryptedValue, err := model.EncryptSecret(encryptionKey, secretValue) + encryptedValue, err := common.EncryptSecret(encryptionKey, secretValue) if err != nil { return err } @@ -78,7 +80,7 @@ func (c *addSecretCommand) run() error { existingEncryptedSecrets[k] = v } - if err = manifest.DecryptSecrets(encryptionKey); err != nil { + if err = common.DecryptManifestSecrets(manifest, encryptionKey); err != nil { log.Debug("Cannot decrypt existing secrets: %+v", err) return fmt.Errorf("others secrets are encrypted with a different password, please use the same one") } else { @@ -91,7 +93,7 @@ func (c *addSecretCommand) run() error { manifest.Secrets[secretName] = encryptedValue } - err = manifest.Save() + err = common.SaveManifest(manifest) if err != nil { return err } diff --git a/commands/add_secret_cmd_test.go b/commands/add_secret_cmd_test.go index ae660f5..710c122 100644 --- a/commands/add_secret_cmd_test.go +++ b/commands/add_secret_cmd_test.go @@ -1,3 +1,6 @@ +//go:build test +// +build test + package commands import ( @@ -5,6 +8,8 @@ import ( "os" "testing" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,10 +33,10 @@ func TestAddSecretCmd(t *testing.T) { name: "add", secretName: "sec-1", secretValue: "val-1", - secretPassword: secretPassword, + secretPassword: common.SecretPassword, patchManifest: func(mf *model.Manifest) { mf.Secrets = model.Secrets{ - "sec-2": mustEncryptSecret(t, "val-2"), + "sec-2": common.MustEncryptSecret(t, "val-2"), } }, assert: assertSecrets(model.Secrets{ @@ -43,7 +48,7 @@ func TestAddSecretCmd(t *testing.T) { name: "add with nil manifest", secretName: "sec-1", secretValue: "val-1", - secretPassword: secretPassword, + secretPassword: common.SecretPassword, patchManifest: func(mf *model.Manifest) { mf.Secrets = nil }, @@ -55,9 +60,9 @@ func TestAddSecretCmd(t *testing.T) { name: "add with different password", secretName: "sec-1", secretValue: "val-1", - secretPassword: secretPassword, + secretPassword: common.SecretPassword, patchManifest: func(mf *model.Manifest) { - mf.Secrets["sec-2"] = mustEncryptSecret(t, "val-2", "other-password") + mf.Secrets["sec-2"] = common.MustEncryptSecret(t, "val-2", "other-password") }, wantErr: "others secrets are encrypted with a different password, please use the same one", }, @@ -65,11 +70,11 @@ func TestAddSecretCmd(t *testing.T) { name: "edit secret", secretName: "sec-1", secretValue: "val-1", - secretPassword: secretPassword, + secretPassword: common.SecretPassword, commandArgs: []string{fmt.Sprintf("--%s", model.FlagEdit)}, patchManifest: func(mf *model.Manifest) { mf.Secrets = model.Secrets{ - "sec-1": mustEncryptSecret(t, "val-1-before"), + "sec-1": common.MustEncryptSecret(t, "val-1-before"), } }, assert: assertSecrets(model.Secrets{"sec-1": "val-1"}), @@ -78,10 +83,10 @@ func TestAddSecretCmd(t *testing.T) { name: "fails if the secret exists", secretName: "sec-1", secretValue: "val-1", - secretPassword: secretPassword, + secretPassword: common.SecretPassword, patchManifest: func(mf *model.Manifest) { mf.Secrets = model.Secrets{ - "sec-1": mustEncryptSecret(t, "val-1-before"), + "sec-1": common.MustEncryptSecret(t, "val-1-before"), } }, wantErr: "sec-1 already exists, use --edit to overwrite", @@ -94,15 +99,17 @@ func TestAddSecretCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - workerDir, workerName := prepareWorkerDirForTest(t) + common.NewMockWorkerServer(t, common.NewServerStub(t).WithDefaultActionsMetadataEndpoint()) + + workerDir, workerName := common.PrepareWorkerDirForTest(t) - runCmd := createCliRunner(t, GetInitCommand(), GetAddSecretCommand()) + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetAddSecretCommand()) err := runCmd("worker", "init", "GENERIC_EVENT", workerName) require.NoError(t, err) if tt.patchManifest != nil { - patchManifest(t, tt.patchManifest) + common.PatchManifest(t, tt.patchManifest) } if tt.secretPassword != "" { @@ -121,7 +128,7 @@ func TestAddSecretCmd(t *testing.T) { }) } - manifestBefore, err := model.ReadManifest(workerDir) + manifestBefore, err := common.ReadManifest(workerDir) require.NoError(t, err) cmd := []string{"worker", "add-secret"} @@ -135,7 +142,7 @@ func TestAddSecretCmd(t *testing.T) { if tt.wantErr == "" { require.NoError(t, err) - manifestAfter, err := model.ReadManifest(workerDir) + manifestAfter, err := common.ReadManifest(workerDir) assert.NoError(t, err) tt.assert(t, manifestBefore, manifestAfter) } else { @@ -148,7 +155,7 @@ func TestAddSecretCmd(t *testing.T) { func assertSecrets(wantSecrets model.Secrets) addSecretAssertFunc { return func(t *testing.T, manifestBefore, manifestAfter *model.Manifest) { require.Equalf(t, len(wantSecrets), len(manifestAfter.Secrets), "Invalid secrets length") - require.NoError(t, manifestAfter.DecryptSecrets()) + require.NoError(t, common.DecryptManifestSecrets(manifestAfter)) assert.Equalf(t, wantSecrets, manifestAfter.Secrets, "Secrets mismatch") } } diff --git a/commands/cmd_input_reader.go b/commands/cmd_input_reader.go deleted file mode 100644 index a23cc9f..0000000 --- a/commands/cmd_input_reader.go +++ /dev/null @@ -1,71 +0,0 @@ -package commands - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "strings" - - "github.com/jfrog/jfrog-cli-core/v2/plugins/components" -) - -type cmdInputReader struct { - ctx *components.Context -} - -func (c *cmdInputReader) readData() (map[string]any, error) { - if len(c.ctx.Arguments) == 0 { - return nil, fmt.Errorf("missing json payload argument") - } - - // The input should always be the last argument - jsonPayload := c.ctx.Arguments[len(c.ctx.Arguments)-1] - - if jsonPayload == "-" { - return c.readDataFromStdin() - } - - if strings.HasPrefix(jsonPayload, "@") { - return c.readDataFromFile(jsonPayload[1:]) - } - - return c.unmarshalData([]byte(jsonPayload)) -} - -func (c *cmdInputReader) readDataFromStdin() (map[string]any, error) { - data := map[string]any{} - - decoder := json.NewDecoder(cliIn) - - err := decoder.Decode(&data) - if err != nil { - return nil, err - } - - return data, err -} - -func (c *cmdInputReader) readDataFromFile(filePath string) (map[string]any, error) { - if filePath == "" { - return nil, errors.New("missing file path") - } - - dataBytes, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - - return c.unmarshalData(dataBytes) -} - -func (c *cmdInputReader) unmarshalData(dataBytes []byte) (map[string]any, error) { - data := map[string]any{} - - err := json.Unmarshal(dataBytes, &data) - if err != nil { - return nil, fmt.Errorf("invalid json payload: %+v", err) - } - - return data, nil -} diff --git a/commands/commands_commons.go b/commands/commands_commons.go deleted file mode 100644 index 68b7323..0000000 --- a/commands/commands_commons.go +++ /dev/null @@ -1,273 +0,0 @@ -package commands - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - - "github.com/jfrog/jfrog-cli-core/v2/plugins/components" - "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/utils/log" - - "github.com/jfrog/jfrog-cli-platform-services/model" -) - -// Useful to capture output in tests -var ( - cliOut io.Writer = os.Stdout - cliIn io.Reader = os.Stdin -) - -func prettifyJson(in []byte) []byte { - var out bytes.Buffer - if err := json.Indent(&out, in, "", " "); err != nil { - return in - } - return out.Bytes() -} - -func outputApiResponse(res *http.Response, okStatus int) error { - return processApiResponse(res, func(responseBytes []byte, statusCode int) error { - var err error - - if res.StatusCode != okStatus { - err = fmt.Errorf("command failed with status %d", res.StatusCode) - } - - if err == nil { - _, err = cliOut.Write(prettifyJson(responseBytes)) - } else if len(responseBytes) > 0 { - // We will report the previous error, but we still want to display the response body - if _, writeErr := cliOut.Write(prettifyJson(responseBytes)); writeErr != nil { - log.Debug(fmt.Sprintf("Write error: %+v", writeErr)) - } - } - - return err - }) -} - -type stringFlagAware interface { - GetStringFlagValue(string) string -} - -// Extracts the project key and worker key from the command context. If the project key is not provided, it will be taken from the manifest. -// There workerKey could either be the first argument or the name in the manifest. -// The first argument will only be considered as the workerKey if total arguments are greater than minArgument. -func extractProjectAndKeyFromCommandContext(c stringFlagAware, args []string, minArguments int, onlyGeneric bool) (string, string, error) { - var workerKey string - - projectKey := c.GetStringFlagValue(model.FlagProjectKey) - - if len(args) > 0 && len(args) > minArguments { - workerKey = args[0] - } - - if workerKey == "" || projectKey == "" { - manifest, err := model.ReadManifest() - if err != nil { - return "", "", err - } - - if err = manifest.Validate(); err != nil { - return "", "", err - } - - if onlyGeneric && manifest.Action != "GENERIC_EVENT" { - return "", "", fmt.Errorf("only the GENERIC_EVENT actions are executable. Got %s", manifest.Action) - } - - if workerKey == "" { - workerKey = manifest.Name - } - - if projectKey == "" { - projectKey = manifest.ProjectKey - } - } - - return workerKey, projectKey, nil -} - -func discardApiResponse(res *http.Response, okStatus int) error { - return processApiResponse(res, func(content []byte, statusCode int) error { - var err error - if res.StatusCode != okStatus { - err = fmt.Errorf("command failed with status %d", res.StatusCode) - } - return err - }) -} - -func processApiResponse(res *http.Response, doWithContent func(content []byte, statusCode int) error) error { - var err error - var responseBytes []byte - - defer func() { - if err = res.Body.Close(); err != nil { - log.Debug(fmt.Sprintf("Error closing response body: %+v", err)) - } - }() - - if res.ContentLength > 0 { - responseBytes, err = io.ReadAll(res.Body) - if err != nil { - return err - } - } else { - _, _ = io.Copy(io.Discard, res.Body) - } - - if doWithContent == nil { - return nil - } - - return doWithContent(responseBytes, res.StatusCode) -} - -func callWorkerApi(c *components.Context, serverUrl string, serverToken string, method string, body []byte, queryParams map[string]string, api ...string) (*http.Response, func(), error) { - timeout, err := model.GetTimeoutParameter(c) - if err != nil { - return nil, nil, err - } - - apiEndpoint := fmt.Sprintf("%sworker/api/v1/%s", utils.AddTrailingSlashIfNeeded(serverUrl), strings.Join(api, "/")) - - if queryParams != nil { - var query string - for key, value := range queryParams { - if query != "" { - query += "&" - } - query += fmt.Sprintf("%s=%s", key, url.QueryEscape(value)) - } - if query != "" { - apiEndpoint += "?" + query - } - } - - reqCtx, cancelReq := context.WithTimeout(context.Background(), timeout) - - var bodyReader io.Reader - if body != nil { - bodyReader = bytes.NewBuffer(body) - } - - req, err := http.NewRequestWithContext(reqCtx, method, apiEndpoint, bodyReader) - if err != nil { - return nil, cancelReq, err - } - - req.Header.Add("Authorization", "Bearer "+strings.TrimSpace(serverToken)) - req.Header.Add("Content-Type", "application/json") - req.Header.Add("User-Agent", coreutils.GetCliUserAgent()) - - res, err := http.DefaultClient.Do(req) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return nil, cancelReq, fmt.Errorf("request timed out after %s", timeout) - } - return nil, cancelReq, err - } - - return res, cancelReq, nil -} - -func callWorkerApiWithOutput(c *components.Context, serverUrl string, serverToken string, method string, body []byte, okStatus int, queryParams map[string]string, api ...string) error { - res, discardReq, err := callWorkerApi(c, serverUrl, serverToken, method, body, queryParams, api...) - if discardReq != nil { - defer discardReq() - } - if err != nil { - return err - } - return outputApiResponse(res, okStatus) -} - -func callWorkerApiSilent(c *components.Context, serverUrl string, serverToken string, method string, body []byte, okStatus int, queryParams map[string]string, api ...string) error { - res, discardReq, err := callWorkerApi(c, serverUrl, serverToken, method, body, queryParams, api...) - if discardReq != nil { - defer discardReq() - } - if err != nil { - return err - } - return discardApiResponse(res, okStatus) -} - -// 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 *components.Context, serverUrl string, accessToken string, workerKey string, projectKey string) (*model.WorkerDetails, error) { - queryParams := make(map[string]string) - if projectKey != "" { - queryParams["projectKey"] = projectKey - } - - res, discardReq, err := callWorkerApi(c, serverUrl, accessToken, http.MethodGet, nil, queryParams, "workers", workerKey) - if discardReq != nil { - defer discardReq() - } - if err != nil { - return nil, err - } - - var details *model.WorkerDetails - - err = processApiResponse(res, func(content []byte, statusCode int) error { - if statusCode == http.StatusOK { - unmarshalled := new(model.WorkerDetails) - err := json.Unmarshal(content, unmarshalled) - if err == nil { - details = unmarshalled - return nil - } - return err - } - if statusCode != http.StatusNotFound { - return fmt.Errorf("fetch worker '%s' failed with status %d", workerKey, statusCode) - } - return nil - }) - if err != nil { - return nil, err - } - - return details, nil -} - -func prepareSecretsUpdate(mf *model.Manifest, existingWorker *model.WorkerDetails) []*model.Secret { - // We will detect removed secrets - removedSecrets := map[string]any{} - if existingWorker != nil { - for _, existingSecret := range existingWorker.Secrets { - removedSecrets[existingSecret.Key] = struct{}{} - } - } - - var secrets []*model.Secret - - // Secrets should have already been decoded - for secretName, secretValue := range mf.Secrets { - _, secretExists := removedSecrets[secretName] - if secretExists { - // To take into account the local value of a secret - secrets = append(secrets, &model.Secret{Key: secretName, MarkedForRemoval: true}) - } - delete(removedSecrets, secretName) - secrets = append(secrets, &model.Secret{Key: secretName, Value: secretValue}) - } - - for removedSecret := range removedSecrets { - secrets = append(secrets, &model.Secret{Key: removedSecret, MarkedForRemoval: true}) - } - - return secrets -} diff --git a/commands/commands_commons_for_itest.go b/commands/commands_commons_for_itest.go deleted file mode 100644 index ed3e0b8..0000000 --- a/commands/commands_commons_for_itest.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build itest - -package commands - -import "io" - -func SetCliIn(reader io.Reader) { - cliIn = reader -} - -func SetCliOut(writer io.Writer) { - cliOut = writer -} diff --git a/commands/common/actions_metadata.go b/commands/common/actions_metadata.go new file mode 100644 index 0000000..bffd3c7 --- /dev/null +++ b/commands/common/actions_metadata.go @@ -0,0 +1,51 @@ +package common + +import ( + "fmt" + + "github.com/jfrog/jfrog-cli-platform-services/model" +) + +type ActionsMetadata []*model.ActionMetadata + +func (c ActionsMetadata) ActionsNames() []string { + names := make([]string, len(c)) + for i, action := range c { + names[i] = action.Action.Name + } + return names +} + +func (c ActionsMetadata) FindAction(actionName string, service ...string) (*model.ActionMetadata, error) { + if len(c) == 0 { + return nil, fmt.Errorf("no actions found") + } + + application := "" + if len(service) > 0 { + application = service[0] + } + + var match []*model.ActionMetadata + + for _, action := range c { + if action.Action.Name == actionName && (application == "" || action.Action.Application == application) { + match = append(match, action) + } + } + + if len(match) == 1 { + return match[0], nil + } + + if len(match) > 1 { + return nil, fmt.Errorf("%d actions found with name '%s', please specify an application", len(match), actionName) + } + + return nil, fmt.Errorf("action '%s' not found", actionName) +} + +func (c ActionsMetadata) ActionNeedsCriteria(actionName string, service ...string) bool { + action, err := c.FindAction(actionName, service...) + return err == nil && action != nil && action.MandatoryFilter +} diff --git a/model/utils.go b/commands/common/clean_import.go similarity index 95% rename from model/utils.go rename to commands/common/clean_import.go index a031420..1111dbe 100644 --- a/model/utils.go +++ b/commands/common/clean_import.go @@ -1,4 +1,4 @@ -package model +package common import "regexp" diff --git a/commands/common/cmd_api.go b/commands/common/cmd_api.go new file mode 100644 index 0000000..785159d --- /dev/null +++ b/commands/common/cmd_api.go @@ -0,0 +1,138 @@ +package common + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strings" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/jfrog/jfrog-client-go/utils" +) + +type apiVersion int + +const ( + ApiVersionV1 apiVersion = iota + 1 + ApiVersionV2 apiVersion = 2 +) + +type ApiContentHandler func(content []byte) error + +type ApiError struct { + StatusCode int + Message string +} + +func (e *ApiError) Error() string { + return e.Message +} + +func apiError(status int, message string, args ...any) *ApiError { + return &ApiError{ + StatusCode: status, + Message: fmt.Sprintf(message, args...), + } +} + +type ApiCallParams struct { + Method string + ServerUrl string + ServerToken string + Body []byte + Query map[string]string + Path []string + ProjectKey string + ApiVersion apiVersion + OkStatuses []int + OnContent ApiContentHandler +} + +func CallWorkerApi(c model.IntFlagProvider, params ApiCallParams) error { + timeout, err := model.GetTimeoutParameter(c) + if err != nil { + return apiError(http.StatusInternalServerError, "%+v", err) + } + + apiVersion := ApiVersionV1 + if params.ApiVersion != 0 { + apiVersion = params.ApiVersion + } + + apiEndpoint := fmt.Sprintf("%sworker/api/v%d/%s", utils.AddTrailingSlashIfNeeded(params.ServerUrl), apiVersion, strings.Join(params.Path, "/")) + + q := url.Values{} + + if params.ProjectKey != "" { + q.Set("projectKey", params.ProjectKey) + } + + for key, value := range params.Query { + q.Set(key, value) + } + + reqCtx, cancelReq := context.WithTimeout(context.Background(), timeout) + defer cancelReq() + + var bodyReader io.Reader + if params.Body != nil { + bodyReader = bytes.NewBuffer(params.Body) + } + + if len(q) > 0 { + apiEndpoint += "?" + q.Encode() + } + + req, err := http.NewRequestWithContext(reqCtx, params.Method, apiEndpoint, bodyReader) + if err != nil { + return apiError(http.StatusInternalServerError, "failed to create request: %+v", err) + } + + req.Header.Add("Authorization", "Bearer "+strings.TrimSpace(params.ServerToken)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("User-Agent", coreutils.GetCliUserAgent()) + + res, err := http.DefaultClient.Do(req) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return apiError(http.StatusRequestTimeout, "request timed out after %s", timeout) + } + return apiError(http.StatusInternalServerError, "unexpected error: %+v", err) + } + + if slices.Index(params.OkStatuses, res.StatusCode) == -1 { + // We the response contains json content, we will print it + _ = processApiResponse(res, printJsonOrLogError) + return apiError(res.StatusCode, "command %s %s returned an unexpected status code %d", params.Method, apiEndpoint, res.StatusCode) + } + + return processApiResponse(res, params.OnContent) +} + +func processApiResponse(res *http.Response, doWithContent func(content []byte) error) error { + var err error + var responseBytes []byte + + defer CloseQuietly(res.Body) + + if res.ContentLength == 0 { + _, _ = io.Copy(io.Discard, res.Body) + } else { + responseBytes, err = io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("cannot read response content: %+v", err) + } + } + + if doWithContent == nil { + return nil + } + + return doWithContent(responseBytes) +} diff --git a/commands/common/cmd_api_test.go b/commands/common/cmd_api_test.go new file mode 100644 index 0000000..46ea2d7 --- /dev/null +++ b/commands/common/cmd_api_test.go @@ -0,0 +1,139 @@ +//go:build test +// +build test + +package common + +import ( + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCallWorkerApi(t *testing.T) { + tests := []struct { + name string + stub *ServerStub + params ApiCallParams + ctx model.IntFlagProvider + wantErr string + }{ + { + name: "success", + stub: NewServerStub(t).WithGetAllEndpoint(), + params: ApiCallParams{ + Method: "GET", + Path: []string{"workers"}, + OkStatuses: []int{http.StatusOK}, + }, + }, + { + name: "unexpected status", + stub: NewServerStub(t).WithGetAllEndpoint(), + params: ApiCallParams{ + Method: "GET", + Path: []string{"workers"}, + OkStatuses: []int{http.StatusNoContent}, + }, + wantErr: `command GET .+/worker/api/v1/workers returned an unexpected status code 200`, + }, + { + name: "cancel on timeout", + stub: NewServerStub(t).WithDelay(time.Second).WithGetAllEndpoint(), + params: ApiCallParams{ + Method: "GET", + Path: []string{"workers"}, + OkStatuses: []int{http.StatusNoContent}, + }, + ctx: IntFlagMap{model.FlagTimeout: 250}, + wantErr: `request timed out after 250ms`, + }, + { + name: "add query params", + stub: NewServerStub(t). + WithGetAllEndpoint(). + WithQueryParam("a", "1"). + WithQueryParam("b", "2"), + params: ApiCallParams{ + Method: "GET", + Path: []string{"workers"}, + OkStatuses: []int{http.StatusOK}, + Query: map[string]string{"a": "1", "b": "2"}, + }, + }, + { + name: "add project key", + stub: NewServerStub(t). + WithGetAllEndpoint(). + WithProjectKey("projectKey"), + params: ApiCallParams{ + Method: "GET", + Path: []string{"workers"}, + OkStatuses: []int{http.StatusOK}, + ProjectKey: "projectKey", + }, + }, + { + name: "add project key amongst query params", + stub: NewServerStub(t). + WithGetAllEndpoint(). + WithQueryParam("a", "1"). + WithProjectKey("projectKey"), + params: ApiCallParams{ + Method: "GET", + Path: []string{"workers"}, + OkStatuses: []int{http.StatusOK}, + ProjectKey: "projectKey", + Query: map[string]string{"a": "1"}, + }, + }, + { + name: "process response", + stub: NewServerStub(t).WithGetAllEndpoint().WithWorkers(&model.WorkerDetails{Key: "wk-0"}), + params: ApiCallParams{ + Method: "GET", + Path: []string{"workers"}, + OkStatuses: []int{http.StatusOK}, + OnContent: func(content []byte) error { + var allWorkers struct { + Workers []model.WorkerDetails `json:"workers"` + } + err := json.Unmarshal(content, &allWorkers) + if err != nil { + return err + } + + require.Len(t, allWorkers.Workers, 1) + assert.Equal(t, "wk-0", allWorkers.Workers[0].Key) + + return nil + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, token := NewMockWorkerServer(t, tt.stub.WithT(t)) + + tt.params.ServerUrl = s.BaseUrl() + tt.params.ServerToken = token + + ctx := tt.ctx + if ctx == nil { + ctx = IntFlagMap{} + } + + err := CallWorkerApi(ctx, tt.params) + if tt.wantErr == "" { + assert.NoError(t, err) + } else { + assert.Regexpf(t, tt.wantErr, err.Error(), "got: %+v", err) + } + }) + } +} diff --git a/commands/common/cmd_commons.go b/commands/common/cmd_commons.go new file mode 100644 index 0000000..c50ce99 --- /dev/null +++ b/commands/common/cmd_commons.go @@ -0,0 +1,90 @@ +package common + +//go:generate mockgen -source=${GOFILE} -destination=mocks/${GOFILE} + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/jfrog/jfrog-cli-platform-services/model" +) + +func PrettifyJson(in []byte) []byte { + var out bytes.Buffer + if err := json.Indent(&out, in, "", " "); err != nil { + return in + } + return out.Bytes() +} + +type stringFlagAware interface { + GetStringFlagValue(string) string +} + +// ExtractProjectAndKeyFromCommandContext Extracts the project key and worker key from the command context. If the project key is not provided, it will be taken from the manifest. +// The workerKey could either be the first argument or the name in the manifest. +// The first argument will only be considered as the workerKey if total arguments are greater than minArgument. +func ExtractProjectAndKeyFromCommandContext(c stringFlagAware, args []string, minArguments int, onlyGeneric bool) (string, string, error) { + var workerKey string + + projectKey := c.GetStringFlagValue(model.FlagProjectKey) + + if len(args) > 0 && len(args) > minArguments { + workerKey = args[0] + } + + if workerKey == "" || projectKey == "" { + manifest, err := ReadManifest() + if err != nil { + return "", "", err + } + + if err = ValidateManifest(manifest, nil); err != nil { + return "", "", err + } + + if onlyGeneric && manifest.Action != "GENERIC_EVENT" { + return "", "", fmt.Errorf("only the GENERIC_EVENT actions are executable. Got %s", manifest.Action) + } + + if workerKey == "" { + workerKey = manifest.Name + } + + if projectKey == "" { + projectKey = manifest.ProjectKey + } + } + + return workerKey, projectKey, nil +} + +func PrepareSecretsUpdate(mf *model.Manifest, existingWorker *model.WorkerDetails) []*model.Secret { + // We will detect removed secrets + removedSecrets := map[string]any{} + if existingWorker != nil { + for _, existingSecret := range existingWorker.Secrets { + removedSecrets[existingSecret.Key] = struct{}{} + } + } + + var secrets []*model.Secret + + // Secrets should have already been decoded + for secretName, secretValue := range mf.Secrets { + _, secretExists := removedSecrets[secretName] + if secretExists { + // To take into account the local value of a secret + secrets = append(secrets, &model.Secret{Key: secretName, MarkedForRemoval: true}) + } + delete(removedSecrets, secretName) + secrets = append(secrets, &model.Secret{Key: secretName, Value: secretValue}) + } + + for removedSecret := range removedSecrets { + secrets = append(secrets, &model.Secret{Key: removedSecret, MarkedForRemoval: true}) + } + + return secrets +} diff --git a/commands/commands_commons_test.go b/commands/common/cmd_commons_test.go similarity index 50% rename from commands/commands_commons_test.go rename to commands/common/cmd_commons_test.go index 0863208..fca7ba5 100644 --- a/commands/commands_commons_test.go +++ b/commands/common/cmd_commons_test.go @@ -1,27 +1,20 @@ -package commands +//go:build test +// +build test + +package common import ( - "bytes" - "encoding/json" - "os" - "path" "testing" - "text/template" "github.com/google/uuid" "github.com/stretchr/testify/assert" - "github.com/jfrog/jfrog-cli-core/v2/plugins" - "github.com/jfrog/jfrog-cli-core/v2/plugins/components" - "github.com/stretchr/testify/require" "github.com/jfrog/jfrog-cli-platform-services/model" ) -const secretPassword = "P@ssw0rd!" - func Test_cleanImports(t *testing.T) { tests := []struct { name string @@ -46,7 +39,7 @@ func Test_cleanImports(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := model.CleanImports(tt.source) + got := CleanImports(tt.source) assert.Equal(t, tt.want, got) }) } @@ -64,7 +57,7 @@ func Test_extractProjectAndKeyFromCommandContext(t *testing.T) { }{ { name: "project and worker key from args", - c: mockStringFlagAware{map[string]string{model.FlagProjectKey: "proj1"}}, + c: mockStringFlagAware{model.FlagProjectKey: "proj1"}, args: []string{"worker1"}, minArguments: 0, onlyGeneric: false, @@ -76,7 +69,7 @@ func Test_extractProjectAndKeyFromCommandContext(t *testing.T) { }, { name: "project and worker key from manifest", - c: mockStringFlagAware{map[string]string{}}, + c: mockStringFlagAware{}, args: []string{}, minArguments: 0, onlyGeneric: false, @@ -88,7 +81,7 @@ func Test_extractProjectAndKeyFromCommandContext(t *testing.T) { }, { name: "only generic event allowed", - c: mockStringFlagAware{map[string]string{model.FlagProjectKey: ""}}, + c: mockStringFlagAware{model.FlagProjectKey: ""}, args: []string{}, minArguments: 0, onlyGeneric: true, @@ -99,7 +92,7 @@ func Test_extractProjectAndKeyFromCommandContext(t *testing.T) { }, { name: "min arguments count not satisfied", - c: mockStringFlagAware{map[string]string{model.FlagProjectKey: ""}}, + c: mockStringFlagAware{model.FlagProjectKey: ""}, args: []string{"@jsonPayload.json"}, minArguments: 1, onlyGeneric: false, @@ -113,7 +106,7 @@ func Test_extractProjectAndKeyFromCommandContext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dir, manifestWorkerKey := prepareWorkerDirForTest(t) + dir, manifestWorkerKey := PrepareWorkerDirForTest(t) manifestProjectKey := uuid.NewString() mf := model.Manifest{ @@ -127,122 +120,17 @@ func Test_extractProjectAndKeyFromCommandContext(t *testing.T) { mf.Action = "GENERIC_EVENT" } - require.NoError(t, mf.Save(dir)) + require.NoError(t, SaveManifest(&mf, dir)) - workerKey, projectKey, err := extractProjectAndKeyFromCommandContext(tt.c, tt.args, tt.minArguments, tt.onlyGeneric) + workerKey, projectKey, err := ExtractProjectAndKeyFromCommandContext(tt.c, tt.args, tt.minArguments, tt.onlyGeneric) tt.assert(t, manifestWorkerKey, manifestProjectKey, workerKey, projectKey, err) }) } } -type mockStringFlagAware struct { - data map[string]string -} +type mockStringFlagAware map[string]string func (m mockStringFlagAware) GetStringFlagValue(flag string) string { - return m.data[flag] -} - -func prepareWorkerDirForTest(t *testing.T) (string, string) { - dir, err := os.MkdirTemp("", "worker-*-init") - require.NoError(t, err) - - t.Cleanup(func() { - _ = os.RemoveAll(dir) - }) - - oldPwd, err := os.Getwd() - require.NoError(t, err) - - err = os.Chdir(dir) - require.NoError(t, err) - - t.Cleanup(func() { - require.NoError(t, os.Chdir(oldPwd)) - }) - - workerName := path.Base(dir) - - return dir, workerName -} - -func generateForTest(t require.TestingT, action string, workerName string, templateName string, skipTests ...bool) string { - tpl, err := template.New(templateName).ParseFS(templates, "templates/"+templateName) - require.NoErrorf(t, err, "cannot initialize the template for %s", action) - - var out bytes.Buffer - err = tpl.Execute(&out, map[string]any{ - "Action": action, - "WorkerName": workerName, - "HasCriteria": model.ActionNeedsCriteria(action), - "HasTests": len(skipTests) == 0 || !skipTests[0], - }) - require.NoError(t, err) - - return out.String() -} - -func mustJsonMarshal(t *testing.T, data any) string { - out, err := json.Marshal(data) - require.NoError(t, err) - return string(out) -} - -func createTempFileWithContent(t *testing.T, content string) string { - file, err := os.CreateTemp("", "wks-cli-*.test") - require.NoError(t, err) - - t.Cleanup(func() { - // We do not care about this error - _ = os.Remove(file.Name()) - }) - - _, err = file.Write([]byte(content)) - require.NoError(t, err) - - return file.Name() -} - -func createCliRunner(t *testing.T, commands ...components.Command) func(args ...string) error { - app := components.App{} - app.Name = "worker" - app.Commands = commands - - runCli := plugins.RunCliWithPlugin(app) - - return func(args ...string) error { - oldArgs := os.Args - t.Cleanup(func() { - os.Args = oldArgs - }) - os.Args = args - return runCli() - } -} - -func patchManifest(t require.TestingT, applyPatch func(mf *model.Manifest), dir ...string) { - mf, err := model.ReadManifest(dir...) - require.NoError(t, err) - - applyPatch(mf) - - require.NoError(t, mf.Save(dir...)) -} - -func getActionSourceCode(t require.TestingT, actionName string) string { - templateName := actionName + ".ts_template" - content, err := templates.ReadFile("templates/" + templateName) - require.NoError(t, err) - return string(content) -} - -func mustEncryptSecret(t require.TestingT, secretValue string, password ...string) string { - key := secretPassword - if len(password) > 0 { - key = password[0] - } - encryptedValue, err := model.EncryptSecret(key, secretValue) - require.NoError(t, err) - return encryptedValue + return m[flag] } diff --git a/commands/common/cmd_io.go b/commands/common/cmd_io.go new file mode 100644 index 0000000..be6a843 --- /dev/null +++ b/commands/common/cmd_io.go @@ -0,0 +1,112 @@ +package common + +import ( + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/jfrog/jfrog-client-go/utils/log" + + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" +) + +// Useful to capture output in tests +var ( + cliOut io.Writer = os.Stdout + cliIn io.Reader = os.Stdin +) + +type InputReader struct { + ctx *components.Context +} + +func NewInputReader(ctx *components.Context) *InputReader { + return &InputReader{ctx} +} + +func NewCsvWriter() *csv.Writer { + return csv.NewWriter(cliOut) +} + +func Print(message string, args ...any) error { + _, err := fmt.Fprintf(cliOut, message, args...) + return err +} + +func PrintJson(data []byte) error { + _, err := cliOut.Write(PrettifyJson(data)) + return err +} + +func printJsonOrLogError(data []byte) error { + if _, writeErr := cliOut.Write(PrettifyJson(data)); writeErr != nil { + log.Debug(fmt.Sprintf("Write error: %+v", writeErr)) + } + return nil +} + +func (c *InputReader) ReadData() (map[string]any, error) { + if len(c.ctx.Arguments) == 0 { + return nil, fmt.Errorf("missing json payload argument") + } + + // The input should always be the last argument + jsonPayload := c.ctx.Arguments[len(c.ctx.Arguments)-1] + + if jsonPayload == "-" { + return c.ReadDataFromStdin() + } + + if strings.HasPrefix(jsonPayload, "@") { + return c.ReadDataFromFile(jsonPayload[1:]) + } + + return c.unmarshalData([]byte(jsonPayload)) +} + +func (c *InputReader) ReadDataFromStdin() (map[string]any, error) { + data := map[string]any{} + + decoder := json.NewDecoder(cliIn) + + err := decoder.Decode(&data) + if err != nil { + return nil, err + } + + return data, err +} + +func (c *InputReader) ReadDataFromFile(filePath string) (map[string]any, error) { + if filePath == "" { + return nil, errors.New("missing file path") + } + + dataBytes, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + return c.unmarshalData(dataBytes) +} + +func (c *InputReader) unmarshalData(dataBytes []byte) (map[string]any, error) { + data := map[string]any{} + + err := json.Unmarshal(dataBytes, &data) + if err != nil { + return nil, fmt.Errorf("invalid json payload: %w", err) + } + + return data, nil +} + +func CloseQuietly(c io.Closer) { + if err := c.Close(); err != nil { + log.Debug(fmt.Sprintf("Error closing resource: %+v", err)) + } +} diff --git a/commands/common/cmd_worker_api.go b/commands/common/cmd_worker_api.go new file mode 100644 index 0000000..318b461 --- /dev/null +++ b/commands/common/cmd_worker_api.go @@ -0,0 +1,62 @@ +package common + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +// 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, + ServerToken: accessToken, + OkStatuses: []int{http.StatusOK}, + ProjectKey: projectKey, + Path: []string{"workers", workerKey}, + OnContent: func(content []byte) error { + return json.Unmarshal(content, details) + }, + }) + if err != nil { + var apiErr *ApiError + if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + return nil, nil + } + return nil, err + } + + return details, nil +} + +func FetchActions(c model.IntFlagProvider, serverUrl string, accessToken string, projectKey string) (ActionsMetadata, error) { + metadata := make(ActionsMetadata, 0) + + err := CallWorkerApi(c, ApiCallParams{ + Method: http.MethodGet, + ServerUrl: serverUrl, + ServerToken: accessToken, + OkStatuses: []int{http.StatusOK}, + ProjectKey: projectKey, + ApiVersion: ApiVersionV2, + Path: []string{"actions"}, + OnContent: func(content []byte) error { + if len(content) == 0 { + log.Debug("No actions returned from the server") + return nil + } + return json.Unmarshal(content, &metadata) + }, + }) + if err != nil { + return nil, err + } + + return metadata, nil +} diff --git a/commands/common/cmd_worker_api_test.go b/commands/common/cmd_worker_api_test.go new file mode 100644 index 0000000..2cfb874 --- /dev/null +++ b/commands/common/cmd_worker_api_test.go @@ -0,0 +1,107 @@ +//go:build test +// +build test + +package common + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFetchWorkerDetails(t *testing.T) { + tests := []struct { + name string + ctx model.IntFlagProvider + workerKey string + projectKey string + stub *ServerStub + wantErr string + want *model.WorkerDetails + }{ + { + name: "success", + workerKey: "wk-1", + stub: NewServerStub(t).WithGetOneEndpoint().WithWorkers(&model.WorkerDetails{Key: "wk-1"}), + want: &model.WorkerDetails{Key: "wk-1"}, + }, + { + name: "no error if not found", + workerKey: "wk-1", + stub: NewServerStub(t).WithGetOneEndpoint(), + }, + { + name: "propagate projectKey", + workerKey: "wk-2", + projectKey: "prj-1", + stub: NewServerStub(t). + WithGetOneEndpoint(). + WithWorkers(&model.WorkerDetails{Key: "wk-2"}). + WithProjectKey("prj-1"), + want: &model.WorkerDetails{Key: "wk-2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, token := NewMockWorkerServer(t, tt.stub.WithT(t)) + + if tt.ctx == nil { + tt.ctx = IntFlagMap{} + } + + got, err := FetchWorkerDetails(tt.ctx, s.BaseUrl(), token, tt.workerKey, tt.projectKey) + + if tt.wantErr != "" { + require.Error(t, err) + assert.Regexp(t, tt.wantErr, err.Error()) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestFetchActions(t *testing.T) { + samples := LoadSampleActions(t) + + tests := []struct { + name string + projectKey string + stub *ServerStub + wantErr string + }{ + { + name: "success", + stub: NewServerStub(t).WithDefaultActionsMetadataEndpoint(), + }, + { + name: "propagate projectKey", + projectKey: "prj-1", + stub: NewServerStub(t). + WithDefaultActionsMetadataEndpoint(). + WithProjectKey("prj-1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, token := NewMockWorkerServer(t, tt.stub.WithT(t)) + + got, err := FetchActions(IntFlagMap{}, s.BaseUrl(), token, tt.projectKey) + + if tt.wantErr != "" { + require.Error(t, err) + assert.Regexp(t, tt.wantErr, err.Error()) + return + } + + require.NoError(t, err) + assert.Len(t, got, len(samples)) + }) + } +} diff --git a/commands/common/manifest.go b/commands/common/manifest.go new file mode 100644 index 0000000..9234e16 --- /dev/null +++ b/commands/common/manifest.go @@ -0,0 +1,149 @@ +package common + +//go:generate mockgen -source=${GOFILE} -destination=mocks/${GOFILE} + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +// ReadManifest reads a manifest from the working directory or from the directory provided as argument. +func ReadManifest(dir ...string) (*model.Manifest, error) { + manifestFile, err := getManifestFile(dir...) + if err != nil { + return nil, err + } + + log.Debug(fmt.Sprintf("Reading manifest from %s", manifestFile)) + + manifestBytes, err := os.ReadFile(manifestFile) + if err != nil { + return nil, err + } + + manifest := model.Manifest{} + + err = json.Unmarshal(manifestBytes, &manifest) + if err != nil { + return nil, err + } + + return &manifest, nil +} + +func getManifestFile(dir ...string) (string, error) { + var manifestFolder string + + if len(dir) > 0 { + manifestFolder = dir[0] + } else { + var err error + if manifestFolder, err = os.Getwd(); err != nil { + return "", err + } + } + + manifestFile := filepath.Join(manifestFolder, "manifest.json") + + return manifestFile, nil +} + +func SaveManifest(mf *model.Manifest, dir ...string) error { + manifestFile, err := getManifestFile(dir...) + if err != nil { + return err + } + + writer, err := os.OpenFile(manifestFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + + defer func() { + closeErr := writer.Close() + if closeErr != nil { + if err == nil { + err = errors.Join(err, closeErr) + } else { + err = closeErr + } + } + }() + + encoder := json.NewEncoder(writer) + encoder.SetIndent("", " ") + err = encoder.Encode(mf) + + return err +} + +// ReadSourceCode reads the content of the file pointed by SourceCodePath +func ReadSourceCode(mf *model.Manifest) (string, error) { + log.Debug(fmt.Sprintf("Reading source code from %s", mf.SourceCodePath)) + sourceBytes, err := os.ReadFile(mf.SourceCodePath) + if err != nil { + return "", err + } + return string(sourceBytes), nil +} + +func ValidateManifest(mf *model.Manifest, actionsMeta ActionsMetadata) error { + if mf.Name == "" { + return invalidManifestErr("missing name") + } + + if mf.SourceCodePath == "" { + return invalidManifestErr("missing source code path") + } + + if mf.Action == "" { + return invalidManifestErr("missing action") + } + + if len(actionsMeta) > 0 { + _, err := actionsMeta.FindAction(mf.Action, mf.Application) + if err != nil { + return invalidManifestErr(err.Error()) + } + } + + return nil +} + +func DecryptManifestSecrets(mf *model.Manifest, withPassword ...string) error { + if len(mf.Secrets) == 0 { + return nil + } + + var password string + if len(withPassword) > 0 { + password = withPassword[0] + } else { + var err error + password, err = ReadSecretPassword("Secrets Password: ") + if err != nil { + return err + } + } + + for name, value := range mf.Secrets { + clearValue, err := DecryptSecret(password, value) + if err != nil { + log.Debug(fmt.Sprintf("cannot decrypt secret '%s': %+v", name, err)) + return fmt.Errorf("cannot decrypt secret '%s', please check the manifest", name) + } + mf.Secrets[name] = clearValue + } + + return nil +} + +func invalidManifestErr(reason string) error { + return fmt.Errorf("invalid manifest: %s", reason) +} diff --git a/model/manifest_test.go b/commands/common/manifest_test.go similarity index 66% rename from model/manifest_test.go rename to commands/common/manifest_test.go index d8c9081..22aa6ca 100644 --- a/model/manifest_test.go +++ b/commands/common/manifest_test.go @@ -1,18 +1,21 @@ -package model +//go:build test +// +build test + +package common import ( "encoding/json" - "fmt" "os" "path/filepath" - "strings" "testing" + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var manifestSample = &Manifest{ +var manifestSample = &model.Manifest{ Name: "my-worker", Description: "my worker description", SourceCodePath: "./my-worker.ts", @@ -20,12 +23,12 @@ var manifestSample = &Manifest{ Enabled: true, Debug: true, ProjectKey: "a-project", - Secrets: Secrets{ + Secrets: model.Secrets{ "hidden1": "hidden1.value", "hidden2": "hidden2.value", }, - FilterCriteria: FilterCriteria{ - ArtifactFilterCriteria: ArtifactFilterCriteria{ + FilterCriteria: model.FilterCriteria{ + ArtifactFilterCriteria: model.ArtifactFilterCriteria{ RepoKeys: []string{ "my-repo-local", }, @@ -37,13 +40,13 @@ func TestReadManifest(t *testing.T) { tests := []struct { name string dirAsArg bool - sample *Manifest - assert func(t *testing.T, mf *Manifest, readErr error) + sample *model.Manifest + assert func(t *testing.T, mf *model.Manifest, readErr error) }{ { name: "in current dir", sample: manifestSample, - assert: func(t *testing.T, mf *Manifest, readErr error) { + assert: func(t *testing.T, mf *model.Manifest, readErr error) { require.NoError(t, readErr) assert.Equal(t, manifestSample, mf) }, @@ -52,14 +55,14 @@ func TestReadManifest(t *testing.T) { name: "with dir as argument", sample: manifestSample, dirAsArg: true, - assert: func(t *testing.T, mf *Manifest, readErr error) { + assert: func(t *testing.T, mf *model.Manifest, readErr error) { require.NoError(t, readErr) assert.Equal(t, manifestSample, mf) }, }, { name: "with missing manifest", - assert: func(t *testing.T, mf *Manifest, readErr error) { + assert: func(t *testing.T, mf *model.Manifest, readErr error) { require.Error(t, readErr) require.True(t, os.IsNotExist(readErr)) }, @@ -103,7 +106,7 @@ func TestManifest_ReadSourceCode(t *testing.T) { tests := []struct { name string sourceCode string - manifest *Manifest + manifest *model.Manifest want string wantErr assert.ErrorAssertionFunc }{ @@ -145,7 +148,7 @@ func TestManifest_ReadSourceCode(t *testing.T) { err = os.Chdir(manifestFolder) require.NoError(t, err) - got, err := manifestSample.ReadSourceCode() + got, err := ReadSourceCode(manifestSample) if !tt.wantErr(t, err, "ReadSourceCode()") { return } @@ -156,21 +159,26 @@ func TestManifest_ReadSourceCode(t *testing.T) { } func TestManifest_Validate(t *testing.T) { + sampleActions := LoadSampleActions(t) + tests := []struct { - name string - manifest *Manifest - assert func(t *testing.T, err error) + name string + manifest *model.Manifest + assert func(t *testing.T, err error) + actionsMeta ActionsMetadata }{ { - name: "valid", - manifest: manifestSample, + name: "valid", + manifest: manifestSample, + actionsMeta: sampleActions, assert: func(t *testing.T, err error) { assert.NoError(t, err) }, }, { - name: "missing name", - manifest: patchedManifestSample(func(mf *Manifest) { + name: "missing name", + actionsMeta: sampleActions, + manifest: patchedManifestSample(func(mf *model.Manifest) { mf.Name = "" }), assert: func(t *testing.T, err error) { @@ -178,8 +186,9 @@ func TestManifest_Validate(t *testing.T) { }, }, { - name: "missing source code path", - manifest: patchedManifestSample(func(mf *Manifest) { + name: "missing source code path", + actionsMeta: sampleActions, + manifest: patchedManifestSample(func(mf *model.Manifest) { mf.SourceCodePath = "" }), assert: func(t *testing.T, err error) { @@ -187,8 +196,9 @@ func TestManifest_Validate(t *testing.T) { }, }, { - name: "missing action", - manifest: patchedManifestSample(func(mf *Manifest) { + name: "missing action", + actionsMeta: sampleActions, + manifest: patchedManifestSample(func(mf *model.Manifest) { mf.Action = "" }), assert: func(t *testing.T, err error) { @@ -196,18 +206,29 @@ func TestManifest_Validate(t *testing.T) { }, }, { - name: "invalid action", - manifest: patchedManifestSample(func(mf *Manifest) { + name: "invalid action", + actionsMeta: sampleActions, + manifest: patchedManifestSample(func(mf *model.Manifest) { mf.Action = "HACK_ME" }), assert: func(t *testing.T, err error) { - assert.EqualError(t, err, invalidManifestErr(fmt.Sprintf("unknown action 'HACK_ME' expecting one of %v", strings.Split(ActionNames(), "|"))).Error()) + assert.EqualError(t, err, invalidManifestErr("action 'HACK_ME' not found").Error()) + }, + }, + { + name: "no action validation if no actions metadata", + manifest: patchedManifestSample(func(mf *model.Manifest) { + mf.Action = "HACK_ME" + }), + assert: func(t *testing.T, err error) { + assert.NoError(t, err) }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.assert(t, tt.manifest.Validate()) + tt.assert(t, ValidateManifest(tt.manifest, tt.actionsMeta)) }) } } @@ -215,19 +236,19 @@ func TestManifest_Validate(t *testing.T) { func TestManifest_DecryptSecrets(t *testing.T) { tests := []struct { name string - encryptSecrets Secrets - verbatimSecrets Secrets - assert func(t *testing.T, mf *Manifest, err error) + encryptSecrets model.Secrets + verbatimSecrets model.Secrets + assert func(t *testing.T, mf *model.Manifest, err error) }{ { name: "ok", - encryptSecrets: Secrets{ + encryptSecrets: model.Secrets{ "s1": "v1", "s2": "v2", }, - assert: func(t *testing.T, mf *Manifest, err error) { + assert: func(t *testing.T, mf *model.Manifest, err error) { require.NoError(t, err) - assert.Equal(t, Secrets{ + assert.Equal(t, model.Secrets{ "s1": "v1", "s2": "v2", }, mf.Secrets) @@ -235,24 +256,24 @@ func TestManifest_DecryptSecrets(t *testing.T) { }, { name: "with cleartext secrets", - verbatimSecrets: Secrets{ + verbatimSecrets: model.Secrets{ "s1": "v1", }, - assert: func(t *testing.T, mf *Manifest, err error) { + assert: func(t *testing.T, mf *model.Manifest, err error) { assert.EqualError(t, err, "cannot decrypt secret 's1', please check the manifest") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := os.Setenv(EnvKeySecretsPassword, "P@ssw0rd!") + err := os.Setenv(model.EnvKeySecretsPassword, "P@ssw0rd!") require.NoError(t, err) t.Cleanup(func() { - _ = os.Unsetenv(EnvKeySecretsPassword) + _ = os.Unsetenv(model.EnvKeySecretsPassword) }) - mf := patchedManifestSample(func(mf *Manifest) { - mf.Secrets = Secrets{} + mf := patchedManifestSample(func(mf *model.Manifest) { + mf.Secrets = model.Secrets{} var err error for key, val := range tt.encryptSecrets { @@ -265,12 +286,12 @@ func TestManifest_DecryptSecrets(t *testing.T) { } }) - tt.assert(t, mf, mf.DecryptSecrets()) + tt.assert(t, mf, DecryptManifestSecrets(mf)) }) } } -func patchedManifestSample(patch func(mf *Manifest)) *Manifest { +func patchedManifestSample(patch func(mf *model.Manifest)) *model.Manifest { patched := *manifestSample patch(&patched) return &patched diff --git a/model/secrets.go b/commands/common/secrets.go similarity index 92% rename from model/secrets.go rename to commands/common/secrets.go index f0b7fab..aa09478 100644 --- a/model/secrets.go +++ b/commands/common/secrets.go @@ -1,4 +1,4 @@ -package model +package common import ( "crypto/aes" @@ -9,6 +9,8 @@ import ( "fmt" "os" + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" "golang.org/x/crypto/scrypt" ) @@ -18,14 +20,8 @@ const ( encryptionKeyLength = 32 ) -type Secret struct { - Key string `json:"key"` - Value string `json:"value"` - MarkedForRemoval bool `json:"markedForRemoval"` -} - func ReadSecretPassword(prompt ...string) (string, error) { - passwordFromEnv, passwordInEnv := os.LookupEnv(EnvKeySecretsPassword) + passwordFromEnv, passwordInEnv := os.LookupEnv(model.EnvKeySecretsPassword) if passwordInEnv { return passwordFromEnv, nil } diff --git a/model/secrets_test.go b/commands/common/secrets_test.go similarity index 95% rename from model/secrets_test.go rename to commands/common/secrets_test.go index 53f5c7c..719aa44 100644 --- a/model/secrets_test.go +++ b/commands/common/secrets_test.go @@ -1,4 +1,7 @@ -package model +//go:build test +// +build test + +package common import ( "fmt" diff --git a/commands/common/test_commons.go b/commands/common/test_commons.go new file mode 100644 index 0000000..0d4841f --- /dev/null +++ b/commands/common/test_commons.go @@ -0,0 +1,250 @@ +//go:build test || itest + +package common + +import ( + "bytes" + "embed" + "encoding/json" + "fmt" + "io" + "os" + "path" + "sort" + "strings" + "testing" + "text/template" + + "github.com/jfrog/jfrog-cli-core/v2/plugins" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type Test interface { + require.TestingT + Cleanup(func()) +} + +const SecretPassword = "P@ssw0rd!" + +//go:embed testdata/actions/* +var sampleActions embed.FS + +func SetCliIn(reader io.Reader) { + cliIn = reader +} + +func SetCliOut(writer io.Writer) { + cliOut = writer +} + +func PrepareWorkerDirForTest(t *testing.T) (string, string) { + dir, err := os.MkdirTemp("", "worker-*-init") + require.NoError(t, err) + + t.Cleanup(func() { + _ = os.RemoveAll(dir) + }) + + oldPwd, err := os.Getwd() + require.NoError(t, err) + + err = os.Chdir(dir) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, os.Chdir(oldPwd)) + }) + + workerName := path.Base(dir) + + return dir, workerName +} + +func GenerateFromSamples(t require.TestingT, templates embed.FS, action string, workerName string, projectKey string, templateName string, skipTests ...bool) string { + tpl, err := template.New(templateName).ParseFS(templates, "templates/"+templateName) + require.NoErrorf(t, err, "cannot initialize the template for %s", action) + + actionsMeta := LoadSampleActions(t) + + actionMeta, err := actionsMeta.FindAction(action) + require.NoError(t, err) + + params := map[string]any{ + "Action": actionMeta.Action.Name, + "Application": actionMeta.Action.Application, + "WorkerName": workerName, + "HasRepoFilterCriteria": actionMeta.MandatoryFilter && actionMeta.FilterType == model.FilterTypeRepo, + "HasTests": len(skipTests) == 0 || !skipTests[0], + "HasRequestType": actionMeta.ExecutionRequestType != "", + "ExecutionRequestType": actionMeta.ExecutionRequestType, + "ProjectKey": projectKey, + } + + usedTypes := ExtractActionUsedTypes(actionMeta) + if len(usedTypes) > 0 { + params["UsedTypes"] = strings.Join(usedTypes, ", ") + } + + var out bytes.Buffer + + err = tpl.Execute(&out, params) + require.NoError(t, err) + + return out.String() +} + +func MustJsonMarshal(t *testing.T, data any) string { + out, err := json.Marshal(data) + require.NoError(t, err) + return string(out) +} + +func CreateTempFileWithContent(t Test, content string) string { + file, err := os.CreateTemp("", "wks-cli-*.test") + require.NoError(t, err) + + t.Cleanup(func() { + // We do not care about this error + _ = os.Remove(file.Name()) + }) + + _, err = file.Write([]byte(content)) + require.NoError(t, err) + + return file.Name() +} + +func CreateCliRunner(t Test, commands ...components.Command) func(args ...string) error { + app := components.App{} + app.Name = "worker" + app.Commands = commands + + runCli := plugins.RunCliWithPlugin(app) + + return func(args ...string) error { + oldArgs := os.Args + t.Cleanup(func() { + os.Args = oldArgs + }) + os.Args = args + return runCli() + } +} + +func PatchManifest(t require.TestingT, applyPatch func(mf *model.Manifest), dir ...string) { + mf, err := ReadManifest(dir...) + require.NoError(t, err) + + applyPatch(mf) + + require.NoError(t, SaveManifest(mf, dir...)) +} + +func MustEncryptSecret(t require.TestingT, secretValue string, password ...string) string { + key := SecretPassword + if len(password) > 0 { + key = password[0] + } + encryptedValue, err := EncryptSecret(key, secretValue) + require.NoError(t, err) + return encryptedValue +} + +func LoadSampleActions(t require.TestingT) ActionsMetadata { + var metadata ActionsMetadata + + actionsFiles, err := sampleActions.ReadDir("testdata/actions") + require.NoError(t, err) + + for _, file := range actionsFiles { + content, err := sampleActions.ReadFile("testdata/actions/" + file.Name()) + require.NoError(t, err) + + action := &model.ActionMetadata{} + err = json.Unmarshal(content, action) + require.NoError(t, err) + + metadata = append(metadata, action) + } + + return metadata +} + +func LoadSampleActionEvents(t require.TestingT) []string { + var events []string + + actionsMeta := LoadSampleActions(t) + for _, md := range actionsMeta { + events = append(events, md.Action.Name) + } + + return events +} + +func TestSetEnv(t Test, key, value string) { + err := os.Setenv(key, value) + require.NoError(t, err) + t.Cleanup(func() { + if err := os.Unsetenv(key); err != nil { + log.Warn(fmt.Sprintf("cannot unset %s: %+v", key, err)) + } + }) +} + +type AssertOutputFunc func(t *testing.T, stdOutput []byte, err error) + +func AssertOutputErrorRegexp(pattern string) AssertOutputFunc { + return func(t *testing.T, stdOutput []byte, err error) { + require.Error(t, err) + assert.Regexpf(t, pattern, err.Error(), "expected error to match pattern %q, got %+v", pattern, err) + } +} + +func AssertOutputError(errorMessage string, errorMessageArgs ...any) AssertOutputFunc { + return func(t *testing.T, stdOutput []byte, err error) { + require.Error(t, err) + assert.EqualError(t, err, fmt.Sprintf(errorMessage, errorMessageArgs...)) + } +} + +func AssertOutputJson[T any](wantResponse T) AssertOutputFunc { + return func(t *testing.T, output []byte, err error) { + require.NoError(t, err) + + outputData := new(T) + + err = json.Unmarshal(output, outputData) + require.NoError(t, err) + + assert.Equal(t, wantResponse, *outputData) + } +} + +func AssertOutputText(wantResponse string, message string, args ...any) AssertOutputFunc { + return func(t *testing.T, output []byte, err error) { + require.NoError(t, err) + assert.Equalf(t, strings.TrimSpace(wantResponse), strings.TrimSpace(string(output)), message, args...) + } +} + +type IntFlagMap map[string]int + +func (m IntFlagMap) GetIntFlagValue(key string) (int, error) { + val := m[key] + return val, nil +} + +func (m IntFlagMap) IsFlagSet(key string) bool { + _, ok := m[key] + return ok +} + +func SortWorkers(workers []*model.WorkerDetails) { + sort.Slice(workers, func(i, j int) bool { + return workers[i].Key < workers[j].Key + }) +} diff --git a/commands/common/test_worker_server.go b/commands/common/test_worker_server.go new file mode 100644 index 0000000..98eb4a4 --- /dev/null +++ b/commands/common/test_worker_server.go @@ -0,0 +1,446 @@ +//go:build test + +package common + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/jfrog/go-mockhttp" + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type BodyValidator func(t require.TestingT, content []byte) + +func ValidateJson(expected any) BodyValidator { + return ValidateJsonFunc(expected, func(in any) any { + return in + }) +} + +func ValidateJsonFunc(expected any, extractPayload func(in any) any) BodyValidator { + return func(t require.TestingT, content []byte) { + var got interface{} + err := json.Unmarshal(content, &got) + require.NoError(t, err) + payload := extractPayload(got) + assert.Equal(t, expected, payload) + } +} + +func NewMockWorkerServer(t *testing.T, stubs ...*ServerStub) (*mockhttp.Server, string) { + token := uuid.NewString() + + var allEndpoints []mockhttp.ServerEndpoint + + for _, stub := range stubs { + if stub.token == "" { + stub.token = token + } + for _, endpoint := range stub.endpoints { + allEndpoints = append(allEndpoints, endpoint) + } + } + + server := mockhttp.StartServer(mockhttp.WithEndpoints(allEndpoints...), mockhttp.WithName("worker")) + + TestSetEnv(t, model.EnvKeyServerUrl, server.BaseUrl()) + TestSetEnv(t, model.EnvKeyAccessToken, token) + TestSetEnv(t, model.EnvKeySecretsPassword, SecretPassword) + + t.Cleanup(server.Close) + + return server, token +} + +func NewServerStub(t *testing.T) *ServerStub { + return &ServerStub{ + test: t, + workers: map[string]*model.WorkerDetails{}, + queryParams: map[string]string{}, + } +} + +type ServerStub struct { + test *testing.T + waitFor time.Duration + token string + projectKey string + workers map[string]*model.WorkerDetails + endpoints []mockhttp.ServerEndpoint + queryParams map[string]string +} + +func (s *ServerStub) WithT(t *testing.T) *ServerStub { + s.test = t + return s +} + +func (s *ServerStub) WithDelay(waitFor time.Duration) *ServerStub { + s.waitFor = waitFor + return s +} + +func (s *ServerStub) WithToken(token string) *ServerStub { + s.token = token + return s +} + +func (s *ServerStub) WithProjectKey(projectKey string) *ServerStub { + s.projectKey = projectKey + return s +} + +func (s *ServerStub) WithQueryParam(name, value string) *ServerStub { + s.queryParams[name] = value + return s +} + +func (s *ServerStub) WithWorkers(workers ...*model.WorkerDetails) *ServerStub { + for _, worker := range workers { + s.workers[worker.Key] = worker + } + return s +} + +func (s *ServerStub) WithCreateEndpoint(validateBody BodyValidator) *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request().POST("/worker/api/v1/workers"), + ). + HandleWith(s.handleSave(http.StatusCreated, validateBody)), + ) + return s +} + +func (s *ServerStub) WithDefaultActionsMetadataEndpoint() *ServerStub { + return s.WithActionsMetadataEndpoint(LoadSampleActions(s.test)) +} + +func (s *ServerStub) WithActionsMetadataEndpoint(metadata ActionsMetadata) *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request().GET("/worker/api/v2/actions"), + ). + HandleWith(s.handleGetAllMetadata(metadata)), + ) + return s +} + +func (s *ServerStub) WithUpdateEndpoint(validateBody BodyValidator) *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request().POST("/worker/api/v1/workers"), + ). + HandleWith(s.handleSave(http.StatusNoContent, validateBody)), + ) + return s +} + +func (s *ServerStub) WithDeleteEndpoint() *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request(). + Method(http.MethodDelete). + PathMatches(regexp.MustCompile(`/worker/api/v1/workers/[^\\]+`)), + ). + HandleWith(s.handleDelete), + ) + return s +} + +func (s *ServerStub) WithTestEndpoint(validateBody BodyValidator, responseBody any, status ...int) *ServerStub { + okStatus := http.StatusOK + if len(status) > 0 { + okStatus = status[0] + } + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request(). + Method(http.MethodPost). + PathMatches(regexp.MustCompile(`/worker/api/v1/test/[^\\]+`)), + ). + HandleWith(s.handle(okStatus, validateBody, responseBody)), + ) + return s +} + +func (s *ServerStub) WithExecuteEndpoint(validateBody BodyValidator, responseBody any, status ...int) *ServerStub { + okStatus := http.StatusOK + if len(status) > 0 { + okStatus = status[0] + } + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request(). + Method(http.MethodPost). + PathMatches(regexp.MustCompile(`/worker/api/v1/execute/[^\\]+`)), + ). + HandleWith(s.handle(okStatus, validateBody, responseBody)), + ) + return s +} + +func (s *ServerStub) WithGetOneEndpoint() *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request(). + Method(http.MethodGet). + PathMatches(regexp.MustCompile(`/worker/api/v1/workers/[^\\]+`)), + ). + HandleWith(s.handleGetOne), + ) + return s +} + +func (s *ServerStub) WithGetAllEndpoint() *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request(). + Method(http.MethodGet). + PathMatches(regexp.MustCompile(`/worker/api/v1/workers(\?.+)?`)), + ). + HandleWith(s.handleGetAll), + ) + return s +} + +func (s *ServerStub) handleGetAll(res http.ResponseWriter, req *http.Request) { + s.applyDelay() + + if !s.validateToken(res, req) { + return + } + + if !s.validateProjectKey(res, req) { + return + } + + if !s.validateQueryParams(res, req) { + return + } + + res.WriteHeader(http.StatusOK) + + action := req.URL.Query().Get("action") + + workers := make([]*model.WorkerDetails, 0, len(s.workers)) + for _, worker := range s.workers { + if action == "" || worker.Action == action { + workers = append(workers, worker) + } + } + + _, err := res.Write([]byte(MustJsonMarshal(s.test, map[string]any{"workers": workers}))) + require.NoError(s.test, err) +} + +func (s *ServerStub) handleGetOne(res http.ResponseWriter, req *http.Request) { + s.applyDelay() + + if !s.validateToken(res, req) { + return + } + + if !s.validateProjectKey(res, req) { + return + } + + if !s.validateQueryParams(res, req) { + return + } + + var workerKey string + + path := strings.Split(req.URL.Path, "/") + if len(path) > 1 { + workerKey = path[len(path)-1] + } + + workerDetails, workerExists := s.workers[workerKey] + if !workerExists { + res.WriteHeader(http.StatusNotFound) + return + } + + _, err := res.Write([]byte(MustJsonMarshal(s.test, workerDetails))) + require.NoError(s.test, err) +} + +func (s *ServerStub) handleDelete(res http.ResponseWriter, req *http.Request) { + s.applyDelay() + + if !s.validateToken(res, req) { + return + } + + if !s.validateQueryParams(res, req) { + return + } + + var workerKey string + + path := strings.Split(req.URL.Path, "/") + if len(path) > 1 { + workerKey = path[len(path)-1] + } + + _, workerExists := s.workers[workerKey] + if !workerExists { + res.WriteHeader(http.StatusNotFound) + return + } + + res.WriteHeader(http.StatusNoContent) +} + +func (s *ServerStub) handleSave(status int, validateBody BodyValidator) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + s.applyDelay() + + if !s.validateToken(res, req) { + return + } + + if !s.validateQueryParams(res, req) { + return + } + + content, err := io.ReadAll(req.Body) + require.NoError(s.test, err) + + if validateBody != nil { + validateBody(s.test, content) + } + + workerDetails := &model.WorkerDetails{} + err = json.Unmarshal(content, workerDetails) + require.NoError(s.test, err) + + s.workers[workerDetails.Key] = workerDetails + + res.WriteHeader(status) + } +} + +func (s *ServerStub) handleGetAllMetadata(metadata ActionsMetadata) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + s.applyDelay() + + if !s.validateToken(res, req) { + return + } + + if !s.validateProjectKey(res, req) { + return + } + + if !s.validateQueryParams(res, req) { + return + } + + res.WriteHeader(http.StatusOK) + + res.Header().Set("Content-Type", "application/json") + + _, err := res.Write([]byte(MustJsonMarshal(s.test, metadata))) + 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() + + if !s.validateToken(res, req) { + return + } + + if !s.validateQueryParams(res, req) { + return + } + + if validateBody != nil { + content, err := io.ReadAll(req.Body) + require.NoError(s.test, err) + validateBody(s.test, content) + } + + res.WriteHeader(status) + + if responseBody != nil { + res.Header().Set("Content-Type", "application/json") + response, err := json.Marshal(responseBody) + require.NoError(s.test, err) + _, err = res.Write(response) + require.NoError(s.test, err) + } + } +} + +func (s *ServerStub) validateToken(res http.ResponseWriter, req *http.Request) bool { + if s.token != "" { + if req.Header.Get("Authorization") != "Bearer "+s.token { + res.WriteHeader(http.StatusForbidden) + return false + } + } + return true +} + +func (s *ServerStub) validateProjectKey(res http.ResponseWriter, req *http.Request) bool { + if s.projectKey != "" { + gotProjectKey := req.URL.Query().Get("projectKey") + if s.projectKey == gotProjectKey { + return true + } + res.WriteHeader(http.StatusForbidden) + assert.FailNow(s.test, "Invalid projectKey") + return false + } + return true +} + +func (s *ServerStub) validateQueryParams(res http.ResponseWriter, req *http.Request) bool { + for key, value := range s.queryParams { + gotValue := req.URL.Query().Get(key) + if value == gotValue { + return true + } + res.WriteHeader(http.StatusBadRequest) + assert.FailNow(s.test, fmt.Sprintf("Invalid query params %s want=%s, got=%s", key, value, gotValue)) + return false + } + return true +} + +func (s *ServerStub) validateHeader(res http.ResponseWriter, req *http.Request, name, value string) bool { + if req.Header.Get(name) != value { + res.WriteHeader(http.StatusBadRequest) + return false + } + return true +} + +func (s *ServerStub) applyDelay() { + if s.waitFor > 0 { + time.Sleep(s.waitFor) + } +} diff --git a/commands/common/testdata/actions/afterBuildInfoSave.json b/commands/common/testdata/actions/afterBuildInfoSave.json new file mode 100644 index 0000000..f19d2a8 --- /dev/null +++ b/commands/common/testdata/actions/afterBuildInfoSave.json @@ -0,0 +1,14 @@ +{ + "action": { + "application": "artifactory", + "name": "AFTER_BUILD_INFO_SAVE" + }, + "description": "After Build Info events are triggered after build info has been saved in the Artifactory storage.", + "samplePayload": "{\"build\":{\"name\":\"buildName\",\"number\":\"buildNumber\",\"started\":\"1980-01-01T00:00:00.000+0000\",\"buildAgent\":\"GENERIC/1.00.0\",\"agent\":\"jfrog-cli-go/1.00.0\",\"durationMillis\":1000,\"principal\":\"bob\",\"artifactoryPrincipal\":\"artifactoryPrincipal\",\"url\":\"url\",\"parentName\":\"parentName\",\"parentNumber\":\"parentNumber\",\"buildRepo\":\"buildRepo\",\"modules\":[{\"id\":\"module1\",\"artifacts\":[{\"name\":\"name\",\"type\":\"type\",\"sha1\":\"sha1\",\"sha256\":\"sha256\",\"md5\":\"md5\",\"remotePath\":\"remotePath\",\"properties\":\"properties\"}],\"dependencies\":[{\"id\":\"id\",\"scopes\":\"scopes\",\"requestedBy\":\"requestedBy\"}]}],\"releaseStatus\":\"releaseStatus\",\"promotionStatuses\":[{\"status\":\"status\",\"comment\":\"comment\",\"repository\":\"repository\",\"timestamp\":\"timestamp\",\"user\":\"user\",\"ciUser\":\"ciUser\"}]}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: AfterBuildInfoSaveRequest): Promise => {\n try {\n // The HTTP client facilitates calls to the JFrog Platform REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get(\"/artifactory/api/v1/system/readiness\");\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n console.log(\"Artifactory ping success\");\n } else {\n console.warn(`Request was successful and returned status code : ${res.status}`);\n }\n } catch (error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n console.error(`Request failed with status code ${error.status || \"\"} caused by : ${error.message}`);\n }\n\n return {\n message: \"proceed\",\n };\n};\n", + "typesDefinitions": "\ninterface AfterBuildInfoSaveRequest {\n /** Various immutable build run details */\n build: DetailedBuildRun | undefined;\n}\n\ninterface DetailedBuildRun {\n name: string;\n number: string;\n started: string;\n buildAgent: string;\n agent: string;\n durationMillis: number;\n principal: string;\n artifactoryPrincipal: string;\n url: string;\n parentName: string;\n parentNumber: string;\n buildRepo: string;\n modules: Module[];\n releaseStatus: string;\n promotionStatuses: PromotionStatus[];\n}\n\ninterface Module {\n id: string;\n artifacts: Artifact[];\n dependencies: Dependency[];\n}\n\ninterface Artifact {\n name: string;\n type: string;\n sha1: string;\n sha256: string;\n md5: string;\n remotePath: string;\n properties: string;\n}\n\ninterface Dependency {\n id: string;\n scopes: string;\n requestedBy: string;\n}\n\ninterface PromotionStatus {\n status: string;\n comment: string;\n repository: string;\n timestamp: string;\n user: string;\n ciUser: string;\n}\n\ninterface AfterBuildInfoSaveResponse {\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n", + "supportProjects": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-an-after-build-info-save-worker", + "async": true, + "executionRequestType": "AfterBuildInfoSaveRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/afterCreate.json b/commands/common/testdata/actions/afterCreate.json new file mode 100644 index 0000000..4565dd7 --- /dev/null +++ b/commands/common/testdata/actions/afterCreate.json @@ -0,0 +1,16 @@ +{ + "action": { + "application": "artifactory", + "name": "AFTER_CREATE" + }, + "description": "After Create events are triggered when an artifact is created in the Artifactory storage. For example, you can add a property value to the artifact immediately after it is created.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"contentLength\":100,\"lastModified\":0,\"trustServerChecksums\":false,\"servletContextUrl\":\"servlet.com\",\"skipJarIndexing\":false,\"disableRedirect\":false,\"repoType\":1},\"headers\":{\"key\":{\"key\":\"bla\",\"value\":\"bla\"}},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: AfterCreateRequest): Promise => {\n\n try {\n // The HTTP client facilitates calls to the JFrog Platform REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n console.log(\"Artifactory ping success\");\n } else {\n console.warn(`Request was successful and returned status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n message: 'proceed',\n }\n}\n", + "typesDefinitions": "\ninterface AfterCreateRequest {\n /** Various immutable upload metadata */\n metadata:\n | UploadMetadata\n | undefined;\n /** The immutable request headers */\n headers: { [key: string]: Header };\n /** The user context which sends the request */\n userContext: UserContext | undefined;\n}\n\ninterface UploadMetadata {\n /** The repoPath object of the request */\n repoPath:\n | RepoPath\n | undefined;\n /** The deploy request content length */\n contentLength: number;\n /** Last modification time that occurred */\n lastModified: number;\n /** Is the request trusting the server checksums */\n trustServerChecksums: boolean;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** Is it a request that skips jar indexing */\n skipJarIndexing: boolean;\n /** Is redirect disabled on this request */\n disableRedirect: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface AfterCreateResponse {\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface Header {\n value: string[];\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-an-after-create-worker", + "async": true, + "executionRequestType": "AfterCreateRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/afterDownload.json b/commands/common/testdata/actions/afterDownload.json new file mode 100644 index 0000000..3cae645 --- /dev/null +++ b/commands/common/testdata/actions/afterDownload.json @@ -0,0 +1,16 @@ +{ + "action": { + "application": "artifactory", + "name": "AFTER_DOWNLOAD" + }, + "description": "After Download events are triggered when Artifactory begins the download execution, it does not wait until after the download is completed.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"originalRepoPath\":{\"key\":\"local-repo\",\"path\":\"old/folder/subfolder/my-file\",\"id\":\"local-repo:old/folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"name\":\"my-file\",\"headOnly\":false,\"checksum\":false,\"recursive\":false,\"modificationTime\":0,\"directoryRequest\":false,\"metadata\":false,\"lastModified\":0,\"ifModifiedSince\":0,\"servletContextUrl\":\"https://jpd.jfrog.io/artifactory\",\"uri\":\"/artifactory/local-repo/folder/subfolder/my-file\",\"clientAddress\":\"100.100.100.100\",\"zipResourcePath\":\"\",\"zipResourceRequest\":false,\"replaceHeadRequestWithGet\":false,\"repoType\":1},\"headers\":{\"key\":{\"key\":\"bla\",\"value\":\"bla\"}},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: AfterDownloadRequest): Promise => {\n\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n console.log(\"Artifactory ping success\");\n } else {\n console.warn(`Request was successful and returned status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n message: 'proceed',\n }\n}\n", + "typesDefinitions": "\ninterface AfterDownloadRequest {\n /** Various immutable download metadata */\n metadata:\n | DownloadMetadata\n | undefined;\n /** The immutable request headers */\n headers: { [key: string]: Header };\n /** The user context which sends the request */\n userContext: UserContext | undefined;\n}\n\ninterface AfterDownloadResponse {\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\ninterface DownloadMetadata {\n /** The repoPath object of the request */\n repoPath:\n | RepoPath\n | undefined;\n /** The original repo path in case a virtual repo is involved */\n originalRepoPath:\n | RepoPath\n | undefined;\n /** The file name from path */\n name: string;\n /** Is it a head request */\n headOnly: boolean;\n /** Is it a checksum request */\n checksum: boolean;\n /** Is it a recursive request */\n recursive: boolean;\n /** When a modification has occurred */\n modificationTime: number;\n /** Is it a directory request */\n directoryRequest: boolean;\n /** Is it a metadata request */\n metadata: boolean;\n /** Last modification time that occurred */\n lastModified: number;\n /** If a modification happened since the last modification time */\n ifModifiedSince: number;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** The request URI */\n uri: string;\n /** The client address */\n clientAddress: string;\n /** The resource path of the requested zip */\n zipResourcePath: string;\n /** Is the request a zip resource request */\n zipResourceRequest: boolean;\n /** should replace the head request with get */\n replaceHeadRequestWithGet: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface Header {\n value: string[];\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface Header {\n value: string[];\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-an-after-download-worker", + "async": true, + "executionRequestType": "AfterDownloadRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/afterMove.json b/commands/common/testdata/actions/afterMove.json new file mode 100644 index 0000000..0670857 --- /dev/null +++ b/commands/common/testdata/actions/afterMove.json @@ -0,0 +1,16 @@ +{ + "action": { + "application": "artifactory", + "name": "AFTER_MOVE" + }, + "description": "After Move events are triggered when an artifact is moved from one repository to another.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"contentLength\":100,\"lastModified\":0,\"trustServerChecksums\":false,\"servletContextUrl\":\"https://jpd.jfrog.io/artifactory\",\"skipJarIndexing\":false,\"disableRedirect\":false,\"repoType\":1},\"targetRepoPath\":{\"key\":\"target-repo\",\"path\":\"new_folder/my-file\",\"id\":\"target-repo:new_folder/my-file\",\"isRoot\":false,\"isFolder\":false},\"artifactProperties\":{\"prop1\":{\"value\":[\"value1\",\"value2\"]}},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: AfterMoveRequest): Promise => {\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get(\"/artifactory/api/v1/system/readiness\");\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n console.log(\"Artifactory ping success\");\n } else {\n console.warn(`Request was successful and returned status code : ${res.status}`);\n }\n } catch (error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n console.error(`Request failed with status code ${error.status || \"\"} caused by : ${error.message}`);\n }\n\n return {\n message: \"proceed\",\n };\n};\n", + "typesDefinitions": "\ninterface AfterMoveRequest {\n /** Various immutable upload metadata */\n metadata: UploadMetadata | undefined;\n /** The immutable target repository path */\n targetRepoPath: RepoPath | undefined;\n /** The user context which sends the request */\n userContext: UserContext | undefined;\n /** The moved artifacts properties */\n artifactProperties: { [key: string]: ArtifactProperties };\n}\n\ninterface UploadMetadata {\n /** The repoPath object of the request */\n repoPath: RepoPath | undefined;\n /** The deploy request content length */\n contentLength: number;\n /** Last modification time that occurred */\n lastModified: number;\n /** Is the request trusting the server checksums */\n trustServerChecksums: boolean;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** Is it a request that skips jar indexing */\n skipJarIndexing: boolean;\n /** Is redirect disabled on this request */\n disableRedirect: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface AfterMoveResponse {\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\ninterface ArtifactProperties {\n /** The property values */\n value: string[];\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-an-after-move-worker", + "async": true, + "executionRequestType": "AfterMoveRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforeCreate.json b/commands/common/testdata/actions/beforeCreate.json new file mode 100644 index 0000000..9ed5c0a --- /dev/null +++ b/commands/common/testdata/actions/beforeCreate.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "artifactory", + "name": "BEFORE_CREATE" + }, + "description": "Before Create events are triggered before an artifact is created in the Artifactory storage. For example, you can terminate the creation of an artifact based on certain conditions.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"contentLength\":100,\"lastModified\":0,\"trustServerChecksums\":false,\"servletContextUrl\":\"servlet.com\",\"skipJarIndexing\":false,\"disableRedirect\":false,\"repoType\":1},\"itemInfo\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"name\":\"my-artifact\",\"created\":1,\"lastModified\":0},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforeCreateRequest): Promise => {\n\n let status: ActionStatus = ActionStatus.UNSPECIFIED;\n\n try {\n // The HTTP client facilitates calls to the JFrog Platform REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n status = ActionStatus.PROCEED;\n console.log(\"Artifactory ping success\");\n } else {\n status = ActionStatus.WARN;\n console.warn(`Request was successful and returned status code : ${ res.status }`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = ActionStatus.STOP;\n console.error(`Request failed with status code ${ error.status || '' } caused by : ${ error.message }`)\n }\n\n return {\n status,\n message: 'proceed'\n }\n}\n", + "typesDefinitions": "\ninterface BeforeCreateRequest {\n /** Various immutable upload metadata */\n metadata:\n | UploadMetadata\n | undefined;\n itemInfo: ItemInfo | undefined;\n /** The user context which sends the request */\n userContext: UserContext | undefined;\n}\n\ninterface UploadMetadata {\n /** The repoPath object of the request */\n repoPath:\n | RepoPath\n | undefined;\n /** The deploy request content length */\n contentLength: number;\n /** Last modification time that occurred */\n lastModified: number;\n /** Is the request trusting the server checksums */\n trustServerChecksums: boolean;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** Is it a request that skips jar indexing */\n skipJarIndexing: boolean;\n /** Is redirect disabled on this request */\n disableRedirect: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface BeforeCreateResponse {\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n /** The instruction of how to proceed */\n status: ActionStatus;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface ItemInfo {\n /** The repoPath object of the request */\n repoPath: RepoPath;\n /** Name of the Artifact **/\n name: string;\n /** Time of creation of the artifact **/\n created: number;\n /** Last modification time that occurred */\n lastModified: number;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n\nenum ActionStatus {\n UNSPECIFIED = 0,\n PROCEED = 1,\n STOP = 2,\n WARN = 3,\n UNRECOGNIZED = -1,\n}", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-create-worker", + "executionRequestType": "BeforeCreateRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforeCreateToken.json b/commands/common/testdata/actions/beforeCreateToken.json new file mode 100644 index 0000000..7b832a7 --- /dev/null +++ b/commands/common/testdata/actions/beforeCreateToken.json @@ -0,0 +1,12 @@ +{ + "action": { + "application": "access", + "name": "BEFORE_CREATE_TOKEN" + }, + "description": "Before Create Token events are triggered before a token is created in the Access service.", + "samplePayload": "{\"tokenSpec\":{\"subject\":\"user\",\"owner\":\"jfwks@000\",\"scope\":[\"applied-permissions/user\"],\"audience\":[\"*@*\"],\"expiresIn\":3600,\"refreshable\":false,\"extension\":\"extension\",\"description\":\"description\",\"includeReferenceToken\":true},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforeCreateTokenRequest): Promise => {\n\n let status: CreateTokenStatus = CreateTokenStatus.CREATE_TOKEN_UNSPECIFIED;\n let message = 'Overwritten by worker-service if an error occurs.';\n\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/access/api/v1/config/token/default_expiry');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n const defaultExpiry = res.data.default_token_expiration;\n const tokenExpiry = data.tokenSpec.expiresIn;\n console.log(`Got default token expiry ${defaultExpiry}`);\n if (data.tokenSpec.scope.includes('applied-permissions/admin')\n && defaultExpiry > 0\n && (!tokenExpiry || (tokenExpiry > defaultExpiry))) {\n status = CreateTokenStatus.CREATE_TOKEN_STOP;\n message = 'Admin token generation with expiry greater that default expiry is not allowed';\n } else {\n status = CreateTokenStatus.CREATE_TOKEN_PROCEED;\n }\n } else {\n status = CreateTokenStatus.CREATE_TOKEN_WARN;\n console.warn(`Request is successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = CreateTokenStatus.CREATE_TOKEN_STOP;\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`);\n }\n\n return {\n status,\n message,\n }\n};\n", + "typesDefinitions": "\ninterface BeforeCreateTokenRequest {\n /** The spec of the token to create */\n tokenSpec:\n | TokenSpec\n | undefined;\n /** The user context which sends the request */\n userContext:\n | UserContext\n | undefined;\n}\n\ninterface TokenSpec {\n /** The subject the token belongs to */\n subject: string;\n /** The owner of the token */\n owner: string;\n /** A list of application specific scopes to grant the user in the generated token */\n scope: string[];\n /** The audience (i.e. services) this token is aimed for. These services are expected to accept this token. */\n audience: string[];\n /** Specific expiry in seconds - i.e. for how long the token should be accepted */\n expiresIn: number;\n /** Set whether the generated token also has a refresh token. */\n refreshable: boolean;\n /** Optional payload to put in the token */\n extension: string;\n /** Optional free text to put in the token */\n description: string;\n /** Set whether the generated token also has a reference token. */\n includeReferenceToken: boolean;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\ninterface BeforeCreateTokenResponse {\n /** The instruction of how to proceed */\n status: CreateTokenStatus;\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\nenum CreateTokenStatus {\n CREATE_TOKEN_UNSPECIFIED = 0,\n CREATE_TOKEN_PROCEED = 1,\n CREATE_TOKEN_STOP = 2,\n CREATE_TOKEN_WARN = 3,\n UNRECOGNIZED = -1,\n}\n", + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-create-token-worker", + "executionRequestType": "BeforeCreateTokenRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforeDelete.json b/commands/common/testdata/actions/beforeDelete.json new file mode 100644 index 0000000..177b6e5 --- /dev/null +++ b/commands/common/testdata/actions/beforeDelete.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "artifactory", + "name": "BEFORE_DELETE" + }, + "description": "Before Delete events are triggered before Artifactory deletes an artifact. For example, you can prevent the deletion of an artifact based on specific conditions.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"repoType\":1,\"triggerMetadataCalculation\":false,\"allowAsyncDelete\":false,\"skipTrashcan\":false,\"isTriggeredByGc\":false,\"triggeredByMove\":false},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"},\"itemInfo\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"name\":\"my-artifact\",\"created\":1,\"lastModified\":0}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforeDeleteRequest): Promise => {\n let status: BeforeDeleteStatus = BeforeDeleteStatus.BEFORE_DELETE_UNSPECIFIED;\n\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n status = BeforeDeleteStatus.BEFORE_DELETE_PROCEED;\n console.log(\"Artifactory ping success\");\n } else {\n status = BeforeDeleteStatus.BEFORE_DELETE_WARN;\n console.warn(`Request is successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = BeforeDeleteStatus.BEFORE_DELETE_STOP;\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n message: \"proceed\",\n status\n }\n}\n", + "typesDefinitions": "\ninterface BeforeDeleteRequest {\n metadata: DeleteMetadata | undefined;\n userContext: UserContext | undefined;\n itemInfo: ItemInfo | undefined;\n}\n\ninterface DeleteMetadata {\n /** The repoPath object of the request */\n repoPath: RepoPath | undefined;\n /** Repository type */\n repoType: RepoType;\n /** Is it a request that trigger metadata calculation */\n triggerMetadataCalculation: boolean;\n /** Is the async delete operation allowed */\n allowAsyncDelete: boolean;\n /** Is the trash can skipped */\n skipTrashcan: boolean;\n /** Is the delete triggered by GC */\n isTriggeredByGc: boolean;\n /** Is the delete triggered by move operation */\n triggeredByMove: boolean;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\ninterface ItemInfo {\n /** The repoPath object of the request */\n repoPath: RepoPath;\n /** Name of the Artifact **/\n name: string;\n /** Time of creation of the artifact **/\n created: number;\n /** Last modification time that occurred */\n lastModified: number;\n}\n\ninterface BeforeDeleteResponse {\n /** The instruction of how to proceed */\n status: BeforeDeleteStatus;\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\nenum BeforeDeleteStatus {\n BEFORE_DELETE_UNSPECIFIED = 0,\n BEFORE_DELETE_PROCEED = 1,\n BEFORE_DELETE_STOP = 2,\n BEFORE_DELETE_WARN = 3,\n UNRECOGNIZED = -1,\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-delete-worker", + "executionRequestType": "BeforeDeleteRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforeDownload.json b/commands/common/testdata/actions/beforeDownload.json new file mode 100644 index 0000000..5c8d412 --- /dev/null +++ b/commands/common/testdata/actions/beforeDownload.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "artifactory", + "name": "BEFORE_DOWNLOAD" + }, + "description": "Before Download events are triggered before Artifactory executes the download request.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"originalRepoPath\":{\"key\":\"local-repo\",\"path\":\"old/folder/subfolder/my-file\",\"id\":\"local-repo:old/folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"name\":\"my-file\",\"headOnly\":false,\"checksum\":false,\"recursive\":false,\"modificationTime\":0,\"directoryRequest\":false,\"metadata\":false,\"lastModified\":1,\"ifModifiedSince\":0,\"servletContextUrl\":\"https://jpd.jfrog.io/artifactory\",\"uri\":\"/artifactory/local-repo/folder/subfolder/my-file\",\"clientAddress\":\"100.100.100.100\",\"zipResourcePath\":\"\",\"zipResourceRequest\":false,\"replaceHeadRequestWithGet\":false,\"repoType\":1},\"headers\":{\"key\":{\"key\":\"bla\",\"value\":\"bla\"}},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"},\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforeDownloadRequest): Promise => {\n\n let status: DownloadStatus = DownloadStatus.DOWNLOAD_UNSPECIFIED;\n\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n status = DownloadStatus.DOWNLOAD_PROCEED;\n console.log(\"Artifactory ping success\");\n } else {\n status = DownloadStatus.DOWNLOAD_WARN;\n console.warn(`Request is successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = DownloadStatus.DOWNLOAD_STOP;\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n status,\n message: 'Overwritten by worker-service if an error occurs.',\n }\n}\n", + "typesDefinitions": "\ninterface BeforeDownloadRequest {\n /** Various immutable download metadata */\n metadata:\n | DownloadMetadata\n | undefined;\n /** The immutable request headers */\n headers: { [key: string]: Header };\n /** The user context which sends the request */\n userContext:\n | UserContext\n | undefined;\n /** The response repoPath */\n repoPath: RepoPath | undefined;\n}\n\ninterface DownloadMetadata {\n /** The repoPath object of the request */\n repoPath:\n | RepoPath\n | undefined;\n /** The original repo path in case a virtual repo is involved */\n originalRepoPath:\n | RepoPath\n | undefined;\n /** The file name from path */\n name: string;\n /** Is it a head request */\n headOnly: boolean;\n /** Is it a checksum request */\n checksum: boolean;\n /** Is it a recursive request */\n recursive: boolean;\n /** When a modification has occurred */\n modificationTime: number;\n /** Is it a directory request */\n directoryRequest: boolean;\n /** Is it a metadata request */\n metadata: boolean;\n /** Last modification time that occurred */\n lastModified: number;\n /** If a modification happened since the last modification time */\n ifModifiedSince: number;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** The request URI */\n uri: string;\n /** The client address */\n clientAddress: string;\n /** The resource path of the requested zip */\n zipResourcePath: string;\n /** Is the request a zip resource request */\n zipResourceRequest: boolean;\n /** should replace the head request with get */\n replaceHeadRequestWithGet: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface Header {\n value: string[];\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n\ninterface BeforeDownloadResponse {\n /** The instruction of how to proceed */\n status: DownloadStatus;\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\nenum DownloadStatus {\n DOWNLOAD_UNSPECIFIED = 0,\n DOWNLOAD_PROCEED = 1,\n DOWNLOAD_STOP = 2,\n DOWNLOAD_WARN = 3,\n UNRECOGNIZED = -1,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-download-worker", + "executionRequestType": "BeforeDownloadRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforeMove.json b/commands/common/testdata/actions/beforeMove.json new file mode 100644 index 0000000..50bd4c3 --- /dev/null +++ b/commands/common/testdata/actions/beforeMove.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "artifactory", + "name": "BEFORE_MOVE" + }, + "description": "Before Move events are triggered before Artifactory moves an artifact. For example, you can prevent the move of an artifact based on specific conditions.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"contentLength\":100,\"lastModified\":0,\"trustServerChecksums\":false,\"servletContextUrl\":\"https://jpd.jfrog.io/artifactory\",\"skipJarIndexing\":false,\"disableRedirect\":false,\"repoType\":1},\"targetRepoPath\":{\"key\":\"target-repo\",\"path\":\"new_folder/my-file\",\"id\":\"target-repo:new_folder/my-file\",\"isRoot\":false,\"isFolder\":false},\"properties\":{\"prop1\":{\"value\":[\"value1\",\"value2\"]},\"size\":{\"value\":\"50Gb\"},\"shaResolution\":{\"value\":\"sha256\"}},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforeMoveRequest): Promise => {\n let status: ActionStatus = ActionStatus.UNSPECIFIED;\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get(\"/artifactory/api/v1/system/readiness\");\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n status = ActionStatus.PROCEED;\n console.log(\"Artifactory ping success\");\n } else {\n status = ActionStatus.WARN;\n console.warn(`Request was successful and returned status code : ${res.status}`);\n }\n } catch (error) {\n status = ActionStatus.STOP;\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n console.error(`Request failed with status code ${error.status || \"\"} caused by : ${error.message}`);\n }\n\n return {\n message: \"proceed\",\n status\n };\n};\n\n", + "typesDefinitions": "\ninterface BeforeMoveRequest {\n /** Various immutable upload metadata */\n metadata: UploadMetadata | undefined;\n /** The immutable target repository path */\n targetRepoPath: RepoPath | undefined;\n /** The user context which sends the request */\n userContext: UserContext | undefined;\n /** The moved artifacts properties */\n properties: { [key: string]: ArtifactProperties };\n}\n\n\ninterface UploadMetadata {\n /** The repoPath object of the request */\n repoPath: RepoPath | undefined;\n /** The deploy request content length */\n contentLength: number;\n /** Last modification time that occurred */\n lastModified: number;\n /** Is the request trusting the server checksums */\n trustServerChecksums: boolean;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** Is it a request that skips jar indexing */\n skipJarIndexing: boolean;\n /** Is redirect disabled on this request */\n disableRedirect: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface BeforeMoveResponse {\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n /** The instruction of how to proceed */\n status: ActionStatus;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\ninterface ArtifactProperties {\n /** The property values */\n value: string[];\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n\nenum ActionStatus {\n UNSPECIFIED = 0,\n PROCEED = 1,\n STOP = 2,\n WARN = 3,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-move-worker", + "executionRequestType": "BeforeMoveRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforePropertyCreate.json b/commands/common/testdata/actions/beforePropertyCreate.json new file mode 100644 index 0000000..956daa4 --- /dev/null +++ b/commands/common/testdata/actions/beforePropertyCreate.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "artifactory", + "name": "BEFORE_PROPERTY_CREATE" + }, + "description": "Before Property Create events are triggered before Artifactory creates properties. For example, verify if users have permissions to change properties before creating a property.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"contentLength\":100,\"lastModified\":0,\"trustServerChecksums\":false,\"servletContextUrl\":\"https://jpd.jfrog.io/artifactory\",\"skipJarIndexing\":false,\"disableRedirect\":false,\"repoType\":1},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"},\"itemInfo\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"name\":\"my-artifact\",\"created\":1,\"lastModified\":0},\"name\":\"property-name\",\"values\":[\"value1\",\"value2\"]}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforePropertyCreateRequest): Promise => {\n let status: BeforePropertyCreateStatus = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_UNSPECIFIED;\n\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n status = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_PROCEED;\n console.log(\"Artifactory ping success\");\n } else {\n status = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_WARN;\n console.warn(`Request is successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_STOP;\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n message: \"proceed\",\n status\n };\n};\n", + "typesDefinitions": "\ninterface BeforePropertyCreateRequest {\n metadata: UploadMetadata | undefined;\n userContext: UserContext | undefined;\n itemInfo: ItemInfo | undefined;\n name: string;\n values: string[];\n}\n\ninterface UploadMetadata {\n /** The repoPath object of the request */\n repoPath: RepoPath | undefined;\n /** The deploy request content length */\n contentLength: number;\n /** Last modification time that occurred */\n lastModified: number;\n /** Is the request trusting the server checksums */\n trustServerChecksums: boolean;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** Is it a request that skips jar indexing */\n skipJarIndexing: boolean;\n /** Is redirect disabled on this request */\n disableRedirect: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\ninterface ItemInfo {\n /** The repoPath object of the request */\n repoPath: RepoPath;\n /** Name of the Artifact **/\n name: string;\n /** Time of creation of the artifact **/\n created: number;\n /** Last modification time that occurred */\n lastModified: number;\n}\n\ninterface BeforePropertyCreateResponse {\n /** The instruction of how to proceed */\n status: BeforePropertyCreateStatus;\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\nenum BeforePropertyCreateStatus {\n BEFORE_PROPERTY_CREATE_UNSPECIFIED = 0,\n BEFORE_PROPERTY_CREATE_PROCEED = 1,\n BEFORE_PROPERTY_CREATE_STOP = 2,\n BEFORE_PROPERTY_CREATE_WARN = 3,\n UNRECOGNIZED = -1,\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-create-property-worker", + "executionRequestType": "BeforePropertyCreateRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforePropertyDelete.json b/commands/common/testdata/actions/beforePropertyDelete.json new file mode 100644 index 0000000..70d55e7 --- /dev/null +++ b/commands/common/testdata/actions/beforePropertyDelete.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "artifactory", + "name": "BEFORE_PROPERTY_DELETE" + }, + "description": "Before Property Delete events are triggered before Artifactory deletes a property from an artifact. For example, verify if users have permissions to change Artifactory properties before Artifactory deletes a property.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"contentLength\":100,\"lastModified\":0,\"trustServerChecksums\":false,\"servletContextUrl\":\"https://jpd.jfrog.io/artifactory\",\"skipJarIndexing\":false,\"disableRedirect\":false,\"repoType\":1},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"},\"itemInfo\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfolder/my-file\",\"id\":\"local-repo:folder/subfolder/my-file\",\"isRoot\":false,\"isFolder\":false},\"name\":\"my-artifact\",\"created\":1,\"lastModified\":0},\"name\":\"property-name\"}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforePropertyDeleteRequest): Promise => {\n let status: BeforePropertyDeleteStatus = BeforePropertyDeleteStatus.BEFORE_PROPERTY_DELETE_UNSPECIFIED;\n\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n status = BeforePropertyDeleteStatus.BEFORE_PROPERTY_DELETE_PROCEED;\n console.log(\"Artifactory ping success\");\n } else {\n status = BeforePropertyDeleteStatus.BEFORE_PROPERTY_DELETE_WARN;\n console.warn(`Request is successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = BeforePropertyDeleteStatus.BEFORE_PROPERTY_DELETE_STOP;\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n message: \"proceed\",\n status\n };\n}\n", + "typesDefinitions": "\ninterface BeforePropertyDeleteRequest {\n metadata: UploadMetadata | undefined;\n userContext: UserContext | undefined;\n itemInfo: ItemInfo | undefined;\n name: string;\n}\n\ninterface UploadMetadata {\n /** The repoPath object of the request */\n repoPath: RepoPath | undefined;\n /** The deploy request content length */\n contentLength: number;\n /** Last modification time that occurred */\n lastModified: number;\n /** Is the request trusting the server checksums */\n trustServerChecksums: boolean;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** Is it a request that skips jar indexing */\n skipJarIndexing: boolean;\n /** Is redirect disabled on this request */\n disableRedirect: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\ninterface ItemInfo {\n /** The repoPath object of the request */\n repoPath: RepoPath;\n /** Name of the Artifact **/\n name: string;\n /** Time of creation of the artifact **/\n created: number;\n /** Last modification time that occurred */\n lastModified: number;\n}\n\ninterface BeforePropertyDeleteResponse {\n /** The instruction of how to proceed */\n status: BeforePropertyDeleteStatus;\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\nenum BeforePropertyDeleteStatus {\n BEFORE_PROPERTY_DELETE_UNSPECIFIED = 0,\n BEFORE_PROPERTY_DELETE_PROCEED = 1,\n BEFORE_PROPERTY_DELETE_STOP = 2,\n BEFORE_PROPERTY_DELETE_WARN = 3,\n UNRECOGNIZED = -1,\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-delete-property-worker", + "executionRequestType": "BeforePropertyDeleteRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforeRevokeToken.json b/commands/common/testdata/actions/beforeRevokeToken.json new file mode 100644 index 0000000..01b6063 --- /dev/null +++ b/commands/common/testdata/actions/beforeRevokeToken.json @@ -0,0 +1,12 @@ +{ + "action": { + "application": "access", + "name": "BEFORE_REVOKE_TOKEN" + }, + "description": "Before Revoke Token events are triggered before a token is revoked in the Access service.", + "samplePayload": "{\"token\":{\"id\":\"id\",\"subject\":\"user\",\"owner\":\"jfwks@000\",\"scope\":\"applied-permissions/user\",\"audience\":\"*@*\",\"expirationTime\":1717171717,\"created\":1717161717,\"type\":\"generic\",\"username\":\"username\",\"description\":\"description\",\"projectKey\":\"projectKey\"},\"userContext\":{\"id\":\"id\",\"isToken\":false,\"realm\":\"realm\"}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforeRevokeTokenRequest): Promise => {\n\n let status: RevokeTokenStatus = RevokeTokenStatus.REVOKE_TOKEN_PROCEED;\n let message = 'Overwritten by worker-service if an error occurs.';\n\n if (data.token.description?.startsWith('protected')) {\n console.log(`Token description starts with 'protected'. Checking if it is the last protected token.`);\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/access/api/v1/tokens?description=protected*');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n const protectedTokensCount = res.data.tokens?.length;\n console.log(`Number of protected tokens: ${protectedTokensCount}`);\n // If request includes multiple tokens to revoke, worker code will be executed for each token\n // In such case the last protected token may be revoked\n if (protectedTokensCount <= 1) {\n status = RevokeTokenStatus.REVOKE_TOKEN_STOP;\n message = 'Revocation of the last protected token is not allowed';\n console.warn(message);\n }\n } else {\n status = RevokeTokenStatus.REVOKE_TOKEN_WARN;\n console.warn(`Request is successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = RevokeTokenStatus.REVOKE_TOKEN_STOP;\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`);\n }\n }\n\n return {\n status,\n message,\n }\n};\n", + "typesDefinitions": "\ninterface BeforeRevokeTokenRequest {\n /** The token to revoke */\n token:\n | Token\n | undefined;\n /** The user context which sends the request */\n userContext:\n | UserContext\n | undefined;\n}\n\ninterface Token {\n /** The token id to revoke */\n id: string\n /** The subject the token belongs to */\n subject: string;\n /** The owner of the token */\n owner: string;\n /** The scope of the token*/\n scope: string;\n /** The audience (i.e. services) this token is aimed for. These services are expected to accept this token */\n audience: string;\n /** The time (epoch) this token expires (optional if it has no expiration time) */\n expirationTime: number;\n /** The time (epoch) this token was created */\n created: number;\n /** Token type. Could be session or generic */\n type: string;\n /** Optional username derived from the token subject */\n username: string;\n /** Optional free text describing the token */\n description: string;\n /** The project key associated with the token */\n projectKey: string;\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\n\ninterface BeforeRevokeTokenResponse {\n /** The instruction of how to proceed */\n status: RevokeTokenStatus;\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n\nenum RevokeTokenStatus {\n REVOKE_TOKEN_UNSPECIFIED = 0,\n REVOKE_TOKEN_PROCEED = 1,\n REVOKE_TOKEN_STOP = 2,\n REVOKE_TOKEN_WARN = 3,\n UNRECOGNIZED = -1,\n}\n", + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-revoke-token-worker", + "executionRequestType": "BeforeRevokeTokenRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/beforeUpload.json b/commands/common/testdata/actions/beforeUpload.json new file mode 100644 index 0000000..6811da6 --- /dev/null +++ b/commands/common/testdata/actions/beforeUpload.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "artifactory", + "name": "BEFORE_UPLOAD" + }, + "description": "Before Upload events are triggered before Artifactory executes an upload request.", + "samplePayload": "{\"metadata\":{\"repoPath\":{\"key\":\"local-repo\",\"path\":\"folder/subfoder/my-file\",\"id\":\"local-repo:folder/subfoder/my-file\",\"isRoot\":false,\"isFolder\":false},\"contentLength\":100,\"lastModified\":0,\"trustServerChecksums\":false,\"servletContextUrl\":\"servlet.com\",\"skipJarIndexing\":false,\"disableRedirect\":false,\"repoType\":1},\"headers\":{\"key\":{\"key\":\"bla\",\"value\":\"bla\"}},\"userContext\":{\"id\":\"jffe@00xxxxxxxxxxxxxxxxxxxxxxxx/users/bob\",\"isToken\":true,\"realm\":\"realm\"},\"artifactProperties\":{\"anyProperty\":{\"value\":[\"anything\"]}}}", + "sampleCode": "\nexport default async (context: PlatformContext, data: BeforeUploadRequest): Promise => {\n let status: UploadStatus = UploadStatus.UPLOAD_UNSPECIFIED;\n\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n status = UploadStatus.UPLOAD_PROCEED;\n console.log(\"Artifactory ping success\");\n } else {\n status = UploadStatus.UPLOAD_WARN;\n console.warn(`Request was successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n status = UploadStatus.UPLOAD_STOP;\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n status,\n message: 'Overwritten by worker-service if an error occurs.',\n modifiedRepoPath: data.metadata.repoPath\n }\n}\n", + "typesDefinitions": "\ninterface BeforeUploadRequest {\n /** Various immutable upload metadata */\n metadata:\n | UploadMetadata\n | undefined;\n /** The immutable request headers */\n headers: { [key: string]: Header };\n /** The user context which sends the request */\n userContext:\n | UserContext\n | undefined;\n /** The properties of the request */\n artifactProperties: { [key: string]: ArtifactProperties };\n}\n\ninterface BeforeUploadResponse {\n /** The instruction of how to proceed */\n status: UploadStatus;\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n /** The modified repo path from the worker */\n modifiedRepoPath:\n | RepoPath\n | undefined;\n}\n\ninterface ArtifactProperties {\n value: string[];\n}\n\ninterface UploadMetadata {\n /** The repoPath object of the request */\n repoPath:\n | RepoPath\n | undefined;\n /** The deploy request content length */\n contentLength: number;\n /** Last modification time that occurred */\n lastModified: number;\n /** Is the request trusting the server checksums */\n trustServerChecksums: boolean;\n /** The url that points to artifactory */\n servletContextUrl: string;\n /** Is it a request that skips jar indexing */\n skipJarIndexing: boolean;\n /** Is redirect disabled on this request */\n disableRedirect: boolean;\n /** Repository type */\n repoType: RepoType;\n}\n\ninterface RepoPath {\n /** The repo key */\n key: string;\n /** The path itself */\n path: string;\n /** The key:path combination */\n id: string;\n /** Is the path the root */\n isRoot: boolean;\n /** Is the path a folder */\n isFolder: boolean;\n}\n\ninterface Header {\n value: string[];\n}\n\ninterface UserContext {\n /** The username or subject */\n id: string;\n /** Is the context an accessToken */\n isToken: boolean;\n /** The realm of the user */\n realm: string;\n}\n\nenum RepoType {\n REPO_TYPE_UNSPECIFIED = 0,\n REPO_TYPE_LOCAL = 1,\n REPO_TYPE_REMOTE = 2,\n REPO_TYPE_FEDERATED = 3,\n UNRECOGNIZED = -1,\n}\n\nenum UploadStatus {\n UPLOAD_UNSPECIFIED = 0,\n UPLOAD_PROCEED = 1,\n UPLOAD_STOP = 2,\n UPLOAD_WARN = 3,\n}\n", + "supportProjects": true, + "filterType": "FILTER_REPO", + "mandatoryFilter": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-before-upload-worker", + "executionRequestType": "BeforeUploadRequest" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/genericEvent.json b/commands/common/testdata/actions/genericEvent.json new file mode 100644 index 0000000..ab3250c --- /dev/null +++ b/commands/common/testdata/actions/genericEvent.json @@ -0,0 +1,11 @@ +{ + "action": { + "application": "worker", + "name": "GENERIC_EVENT" + }, + "description": "Generic events can execute custom code that is not triggered by any event in the JPD. You must execute these events manually through the REST API.", + "samplePayload": "{\"foo\":\"bar\"}", + "sampleCode": "\ntype CustomPayload = void;\n\ninterface CustomResponse {\n error: string | undefined // Valued with the cause in case of error\n repositories: Record // A list that contains the number of repositories per repository type\n}\n\ninterface RepoData {\n key: string\n type: string\n description: string\n url: string\n packageType: string\n}\n\nexport default async (context: PlatformContext, data: CustomPayload): Promise => {\n\n const response = {\n error: undefined,\n repositories: {},\n };\n \n try {\n // Ref: https://jfrog.com/help/r/jfrog-rest-apis/get-repositories\n const res = await context.clients.platformHttp.get('/artifactory/api/repositories');\n if (res.status === 200) {\n const repositories: RepoData[] = res.data;\n\n // The number of repositories mapped by repository type\n const repoCountRecord: Record = {};\n\n repositories.forEach(repository => {\n let count = repoCountRecord[repository.type] || 0;\n repoCountRecord[repository.type] = ++count;\n });\n\n response.repositories = repoCountRecord;\n console.log(\"Repository count success\");\n } else {\n response.error = `Request is successful but returned an unexpected status : ${res.status}`;\n console.warn(response.error);\n }\n } catch(error) {\n response.error = `Request failed with status code ${error.status || ''} caused by : ${error.message}`;\n console.error(response.error);\n }\n\n return response;\n}\n", + "supportProjects": true, + "wikiUrl": "https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-a-generic-worker" +} \ No newline at end of file diff --git a/commands/common/testdata/actions/scheduledEvent.json b/commands/common/testdata/actions/scheduledEvent.json new file mode 100644 index 0000000..559e5f2 --- /dev/null +++ b/commands/common/testdata/actions/scheduledEvent.json @@ -0,0 +1,15 @@ +{ + "action": { + "application": "worker", + "name": "SCHEDULED_EVENT" + }, + "description": "Scheduled events are triggered on schedule provided by the user.", + "samplePayload": "{\"triggerID\":\"triggerID\"}", + "sampleCode": "\nexport default async (context: PlatformContext, data: ScheduledEventRequest): Promise => {\n try {\n // The in-browser HTTP client facilitates making calls to the JFrog REST APIs\n //To call an external endpoint, use 'await context.clients.axios.get(\"https://foo.com\")'\n const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness');\n\n // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower)\n if (res.status === 200) {\n console.log(\"Artifactory ping success\");\n } else {\n console.warn(`Request is successful but returned status other than 200. Status code : ${res.status}`);\n }\n } catch(error) {\n // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher\n console.error(`Request failed with status code ${error.status || ''} caused by : ${error.message}`)\n }\n\n return {\n message: 'Overwritten by worker-service if an error occurs.',\n }\n}\n", + "typesDefinitions": "\ninterface ScheduledEventRequest {\n /** The trigger ID of the event */\n triggerID: string;\n}\n\ninterface ScheduledEventResponse {\n /** Message to print to the log, in case of an error it will be printed as a warning */\n message: string;\n}\n", + "supportProjects": true, + "filterType": "SCHEDULE", + "mandatoryFilter": true, + "wikiUrl": "TBD", + "async": true +} \ No newline at end of file diff --git a/commands/common/typescript.go b/commands/common/typescript.go new file mode 100644 index 0000000..712d44d --- /dev/null +++ b/commands/common/typescript.go @@ -0,0 +1,67 @@ +package common + +import ( + "regexp" + "slices" + "strings" + + "github.com/jfrog/jfrog-cli-platform-services/model" +) + +var ( + tsNewInstancePattern = regexp.MustCompile(`new\s+([A-Z][a-zA-Z0-9]+)\(`) + tsFieldTypePattern = regexp.MustCompile(`[a-zA-Z0-9]+\s*:\s*([A-Z][a-zA-Z0-9]+)`) + tsFieldAccessPattern = regexp.MustCompile(`\W([A-Z][a-zA-Z0-9]+)\.[a-zA-Z0-9]+`) + tsTypeInTypeParameters = regexp.MustCompile(`<([A-Z][a-zA-Z0-9]+)>`) + tsExcludeTypes = []string{"PlatformContext"} + tsUnexportedType = regexp.MustCompile(`^(class|type|interface|enum|const)\s+[A-Za-z_$][0-9A-Za-z_$]*`) +) + +// AddExportToTypesDeclarations Add export to (interface, class, enum, type) XXX found in the source. +func AddExportToTypesDeclarations(tsSource string) string { + lines := strings.Split(tsSource, "\n") + for i, line := range lines { + if tsUnexportedType.MatchString(strings.TrimSpace(line)) { + lines[i] = "export " + line + } + } + return strings.Join(lines, "\n") +} + +// ExtractActionUsedTypes extracts all the type used in an action's sampleCode and defined in the action's typesDefinitions. +func ExtractActionUsedTypes(md *model.ActionMetadata) []string { + var types []string + for _, typeName := range ExtractUsedTypes(md.SampleCode) { + if strings.Contains(md.TypesDefinitions, typeName) { + types = append(types, typeName) + } + } + return types +} + +// ExtractUsedTypes extracts types from a TypeScript source file. +func ExtractUsedTypes(tsSource string) []string { + var types []string + + handleMatch := func(m string) { + if slices.Index(types, m) == -1 && slices.Index(tsExcludeTypes, m) == -1 { + types = append(types, m) + } + } + + extractTypesByPattern(tsNewInstancePattern, tsSource, handleMatch) + extractTypesByPattern(tsFieldTypePattern, tsSource, handleMatch) + extractTypesByPattern(tsFieldAccessPattern, tsSource, handleMatch) + extractTypesByPattern(tsTypeInTypeParameters, tsSource, handleMatch) + + slices.Sort(types) + + return types +} + +func extractTypesByPattern(pattern *regexp.Regexp, source string, onMatch func(m string)) { + matches := pattern.FindAllStringSubmatch(source, -1) + for _, match := range matches { + onMatch(match[1]) + } +} diff --git a/commands/common/typescript_test.go b/commands/common/typescript_test.go new file mode 100644 index 0000000..53c8b11 --- /dev/null +++ b/commands/common/typescript_test.go @@ -0,0 +1,219 @@ +//go:build test +// +build test + +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddExportToTypesDeclarations(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "export interface", + input: "interface MyInterface {}; // some comment", + want: "export interface MyInterface {}; // some comment", + }, + { + name: "export class", + input: "class MyClass {}; // some comment", + want: "export class MyClass {}; // some comment", + }, + { + name: "export enum", + input: "enum MyEnum {}; // some comment", + want: "export enum MyEnum {}; // some comment", + }, + { + name: "export type", + input: "type MyType = {}; // some comment", + want: "export type MyType = {}; // some comment", + }, + { + name: "export const", + input: "const MyConst = {}; // some comment", + want: "export const MyConst = {}; // some comment", + }, + { + name: "export multiple types", + input: `type MyType = {}; +class MyClass = {};`, + want: `export type MyType = {}; +export class MyClass = {};`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := AddExportToTypesDeclarations(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExtractTypescriptTypes(t *testing.T) { + actionsMeta := LoadSampleActions(t) + + tests := []struct { + event string + want []string + }{ + { + event: "BEFORE_DOWNLOAD", + want: []string{"BeforeDownloadResponse", "BeforeDownloadRequest", "DownloadStatus"}, + }, + { + event: "BEFORE_UPLOAD", + want: []string{"BeforeUploadResponse", "BeforeUploadRequest", "UploadStatus"}, + }, + { + event: "AFTER_DOWNLOAD", + want: []string{"AfterDownloadResponse", "AfterDownloadRequest"}, + }, + { + event: "AFTER_BUILD_INFO_SAVE", + want: []string{"AfterBuildInfoSaveResponse", "AfterBuildInfoSaveRequest"}, + }, + { + event: "AFTER_CREATE", + want: []string{"AfterCreateResponse", "AfterCreateRequest"}, + }, + { + event: "AFTER_MOVE", + want: []string{"AfterMoveResponse", "AfterMoveRequest"}, + }, + { + event: "BEFORE_CREATE", + want: []string{"BeforeCreateResponse", "BeforeCreateRequest", "ActionStatus"}, + }, + { + event: "BEFORE_CREATE_TOKEN", + want: []string{"BeforeCreateTokenResponse", "BeforeCreateTokenRequest", "CreateTokenStatus"}, + }, + { + event: "BEFORE_REVOKE_TOKEN", + want: []string{"BeforeRevokeTokenResponse", "BeforeRevokeTokenRequest", "RevokeTokenStatus"}, + }, + { + event: "BEFORE_DELETE", + want: []string{"BeforeDeleteResponse", "BeforeDeleteRequest", "BeforeDeleteStatus"}, + }, + { + event: "BEFORE_MOVE", + want: []string{"BeforeMoveResponse", "BeforeMoveRequest", "ActionStatus"}, + }, + { + event: "BEFORE_PROPERTY_CREATE", + want: []string{"BeforePropertyCreateResponse", "BeforePropertyCreateRequest", "BeforePropertyCreateStatus"}, + }, + { + event: "BEFORE_PROPERTY_DELETE", + want: []string{"BeforePropertyDeleteResponse", "BeforePropertyDeleteRequest", "BeforePropertyDeleteStatus"}, + }, + { + event: "GENERIC_EVENT", + want: []string{"CustomPayload", "CustomResponse", "Record", "RepoData"}, + }, + { + event: "SCHEDULED_EVENT", + want: []string{"ScheduledEventRequest", "ScheduledEventResponse"}, + }, + } + + for _, tt := range tests { + t.Run(tt.event, func(t *testing.T) { + actionMeta, err := actionsMeta.FindAction(tt.event) + require.NoError(t, err) + + types := ExtractUsedTypes(actionMeta.SampleCode) + assert.ElementsMatch(t, tt.want, types) + }) + } +} + +func TestExtractActionUsedTypes(t *testing.T) { + actionsMeta := LoadSampleActions(t) + + tests := []struct { + event string + want []string + }{ + { + event: "BEFORE_DOWNLOAD", + want: []string{"BeforeDownloadResponse", "BeforeDownloadRequest", "DownloadStatus"}, + }, + { + event: "BEFORE_UPLOAD", + want: []string{"BeforeUploadResponse", "BeforeUploadRequest", "UploadStatus"}, + }, + { + event: "AFTER_DOWNLOAD", + want: []string{"AfterDownloadResponse", "AfterDownloadRequest"}, + }, + { + event: "AFTER_BUILD_INFO_SAVE", + want: []string{"AfterBuildInfoSaveResponse", "AfterBuildInfoSaveRequest"}, + }, + { + event: "AFTER_CREATE", + want: []string{"AfterCreateResponse", "AfterCreateRequest"}, + }, + { + event: "AFTER_MOVE", + want: []string{"AfterMoveResponse", "AfterMoveRequest"}, + }, + { + event: "BEFORE_CREATE", + want: []string{"BeforeCreateResponse", "BeforeCreateRequest", "ActionStatus"}, + }, + { + event: "BEFORE_CREATE_TOKEN", + want: []string{"BeforeCreateTokenResponse", "BeforeCreateTokenRequest", "CreateTokenStatus"}, + }, + { + event: "BEFORE_REVOKE_TOKEN", + want: []string{"BeforeRevokeTokenResponse", "BeforeRevokeTokenRequest", "RevokeTokenStatus"}, + }, + { + event: "BEFORE_DELETE", + want: []string{"BeforeDeleteResponse", "BeforeDeleteRequest", "BeforeDeleteStatus"}, + }, + { + event: "BEFORE_MOVE", + want: []string{"BeforeMoveResponse", "BeforeMoveRequest", "ActionStatus"}, + }, + { + event: "BEFORE_PROPERTY_CREATE", + want: []string{"BeforePropertyCreateResponse", "BeforePropertyCreateRequest", "BeforePropertyCreateStatus"}, + }, + { + event: "BEFORE_PROPERTY_DELETE", + want: []string{"BeforePropertyDeleteResponse", "BeforePropertyDeleteRequest", "BeforePropertyDeleteStatus"}, + }, + { + event: "GENERIC_EVENT", + want: []string{}, + }, + { + event: "SCHEDULED_EVENT", + want: []string{"ScheduledEventRequest", "ScheduledEventResponse"}, + }, + } + + for _, tt := range tests { + t.Run(tt.event, func(t *testing.T) { + actionMeta, err := actionsMeta.FindAction(tt.event) + require.NoError(t, err) + + types := ExtractActionUsedTypes(actionMeta) + assert.ElementsMatch(t, tt.want, types) + }) + } +} diff --git a/commands/deploy_cmd.go b/commands/deploy_cmd.go index 21b9b7a..0a131d0 100644 --- a/commands/deploy_cmd.go +++ b/commands/deploy_cmd.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" "github.com/jfrog/jfrog-client-go/utils/log" @@ -35,22 +37,27 @@ func GetDeployCommand() components.Command { model.GetNoSecretsFlag(), }, Action: func(c *components.Context) error { - manifest, err := model.ReadManifest() + server, err := model.GetServerDetails(c) if err != nil { return err } - if err = manifest.Validate(); err != nil { + manifest, err := common.ReadManifest() + if err != nil { return err } - server, err := model.GetServerDetails(c) + actionsMeta, err := common.FetchActions(c, server.GetUrl(), server.GetAccessToken(), manifest.ProjectKey) if err != nil { return err } + if err = common.ValidateManifest(manifest, actionsMeta); err != nil { + return err + } + if !c.GetBoolFlagValue(model.FlagNoSecrets) { - if err = manifest.DecryptSecrets(); err != nil { + if err = common.DecryptManifestSecrets(manifest); err != nil { return err } } @@ -61,7 +68,7 @@ func GetDeployCommand() components.Command { } func runDeployCommand(ctx *components.Context, manifest *model.Manifest, serverUrl string, token string) error { - existingWorker, err := fetchWorkerDetails(ctx, serverUrl, token, manifest.Name, manifest.ProjectKey) + existingWorker, err := common.FetchWorkerDetails(ctx, serverUrl, token, manifest.Name, manifest.ProjectKey) if err != nil { return err } @@ -78,7 +85,14 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, serverU if existingWorker == nil { log.Info(fmt.Sprintf("Deploying worker '%s'", manifest.Name)) - err = callWorkerApiWithOutput(ctx, serverUrl, token, http.MethodPost, bodyBytes, http.StatusCreated, nil, "workers") + err = common.CallWorkerApi(ctx, common.ApiCallParams{ + Method: http.MethodPost, + ServerUrl: serverUrl, + ServerToken: token, + Body: bodyBytes, + OkStatuses: []int{http.StatusCreated}, + Path: []string{"workers"}, + }) if err == nil { log.Info(fmt.Sprintf("Worker '%s' deployed", manifest.Name)) } @@ -86,7 +100,14 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, serverU } log.Info(fmt.Sprintf("Updating worker '%s'", manifest.Name)) - err = callWorkerApiWithOutput(ctx, serverUrl, token, http.MethodPut, bodyBytes, http.StatusNoContent, nil, "workers") + err = common.CallWorkerApi(ctx, common.ApiCallParams{ + Method: http.MethodPut, + ServerUrl: serverUrl, + ServerToken: token, + Body: bodyBytes, + OkStatuses: []int{http.StatusNoContent}, + Path: []string{"workers"}, + }) if err == nil { log.Info(fmt.Sprintf("Worker '%s' updated", manifest.Name)) } @@ -95,16 +116,16 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, serverU } func prepareDeployRequest(ctx *components.Context, manifest *model.Manifest, existingWorker *model.WorkerDetails) (*deployRequest, error) { - sourceCode, err := manifest.ReadSourceCode() + sourceCode, err := common.ReadSourceCode(manifest) if err != nil { return nil, err } - sourceCode = model.CleanImports(sourceCode) + sourceCode = common.CleanImports(sourceCode) var secrets []*model.Secret if !ctx.GetBoolFlagValue(model.FlagNoSecrets) { - secrets = prepareSecretsUpdate(manifest, existingWorker) + secrets = common.PrepareSecretsUpdate(manifest, existingWorker) } payload := &deployRequest{ diff --git a/commands/deploy_cmd_test.go b/commands/deploy_cmd_test.go index 1a1efa0..8b93f0f 100644 --- a/commands/deploy_cmd_test.go +++ b/commands/deploy_cmd_test.go @@ -1,18 +1,17 @@ +//go:build test +// +build test + package commands import ( - "context" "encoding/json" "errors" "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "regexp" "testing" "time" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,13 +19,15 @@ import ( ) func TestDeployCommand(t *testing.T) { + actionsMeta := common.LoadSampleActions(t) + tests := []struct { name string commandArgs []string token string workerAction string workerName string - serverBehavior deployServerStubBehavior + serverBehavior *common.ServerStub wantErr error patchManifest func(mf *model.Manifest) }{ @@ -34,21 +35,22 @@ func TestDeployCommand(t *testing.T) { name: "create", workerAction: "BEFORE_UPLOAD", workerName: "wk-0", - serverBehavior: deployServerStubBehavior{ - wantMethods: []string{http.MethodGet, http.MethodPost}, - wantRequestBody: getExpectedDeployRequestForAction( - t, - "wk-0", - "BEFORE_UPLOAD", - "", - &model.Secret{Key: "sec-1", Value: "val-1"}, - &model.Secret{Key: "sec-2", Value: "val-2"}, + serverBehavior: common.NewServerStub(t). + WithGetOneEndpoint(). + WithCreateEndpoint( + expectDeployRequest( + actionsMeta, + "wk-0", + "BEFORE_UPLOAD", + "", + &model.Secret{Key: "sec-1", Value: "val-1"}, + &model.Secret{Key: "sec-2", Value: "val-2"}, + ), ), - }, patchManifest: func(mf *model.Manifest) { mf.Secrets = model.Secrets{ - "sec-1": mustEncryptSecret(t, "val-1"), - "sec-2": mustEncryptSecret(t, "val-2"), + "sec-1": common.MustEncryptSecret(t, "val-1"), + "sec-2": common.MustEncryptSecret(t, "val-2"), } }, }, @@ -56,43 +58,53 @@ func TestDeployCommand(t *testing.T) { name: "update", workerAction: "GENERIC_EVENT", workerName: "wk-1", - serverBehavior: deployServerStubBehavior{ - wantMethods: []string{http.MethodGet, http.MethodPut}, - wantRequestBody: getExpectedDeployRequestForAction(t, "wk-1", "GENERIC_EVENT", ""), - existingWorkers: map[string]*model.WorkerDetails{ - "wk-1": {}, - }, - }, + serverBehavior: common.NewServerStub(t). + WithGetOneEndpoint(). + WithUpdateEndpoint( + expectDeployRequest(actionsMeta, "wk-1", "GENERIC_EVENT", ""), + ). + WithWorkers(&model.WorkerDetails{ + Key: "wk-1", + }), }, { name: "update with removed secrets", workerAction: "AFTER_MOVE", workerName: "wk-2", - serverBehavior: deployServerStubBehavior{ - wantMethods: []string{http.MethodGet, http.MethodPut}, - wantRequestBody: getExpectedDeployRequestForAction(t, "wk-2", "AFTER_MOVE", "", &model.Secret{Key: "sec-1", MarkedForRemoval: true}, &model.Secret{Key: "sec-1", Value: "val-1"}, &model.Secret{Key: "sec-2", MarkedForRemoval: true}), - existingWorkers: map[string]*model.WorkerDetails{ - "wk-2": { - Secrets: []*model.Secret{ - {Key: "sec-1"}, {Key: "sec-2"}, - }, + serverBehavior: common.NewServerStub(t). + WithGetOneEndpoint(). + WithUpdateEndpoint( + expectDeployRequest( + actionsMeta, + "wk-2", + "AFTER_MOVE", + "", + &model.Secret{Key: "sec-1", MarkedForRemoval: true}, + &model.Secret{Key: "sec-1", Value: "val-1"}, + &model.Secret{Key: "sec-2", MarkedForRemoval: true}, + ), + ). + WithWorkers(&model.WorkerDetails{ + Key: "wk-2", + Secrets: []*model.Secret{ + {Key: "sec-1"}, {Key: "sec-2"}, }, - }, - }, + }), patchManifest: func(mf *model.Manifest) { mf.Secrets = model.Secrets{ - "sec-1": mustEncryptSecret(t, "val-1"), + "sec-1": common.MustEncryptSecret(t, "val-1"), } }, }, { - name: "update with project key", + name: "create with project key", workerAction: "GENERIC_EVENT", workerName: "wk-1", - serverBehavior: deployServerStubBehavior{ - wantMethods: []string{http.MethodGet, http.MethodPost}, - wantRequestBody: getExpectedDeployRequestForAction(t, "wk-1", "GENERIC_EVENT", "proj-1"), - }, + serverBehavior: common.NewServerStub(t). + WithGetOneEndpoint(). + WithCreateEndpoint( + expectDeployRequest(actionsMeta, "wk-1", "GENERIC_EVENT", "proj-1"), + ), patchManifest: func(mf *model.Manifest) { mf.ProjectKey = "proj-1" }, @@ -100,26 +112,26 @@ func TestDeployCommand(t *testing.T) { { name: "fails if timeout exceeds", commandArgs: []string{"--" + model.FlagTimeout, "500"}, - serverBehavior: deployServerStubBehavior{ - waitFor: 5 * time.Second, - }, + serverBehavior: common.NewServerStub(t). + WithDelay(1 * time.Second). + WithCreateEndpoint(nil), wantErr: errors.New("request timed out after 500ms"), }, { - name: "fails if invalid timeout", - commandArgs: []string{"--" + model.FlagTimeout, "abc"}, - wantErr: errors.New("invalid timeout provided"), + name: "fails if invalid timeout", + serverBehavior: common.NewServerStub(t), + commandArgs: []string{"--" + model.FlagTimeout, "abc"}, + wantErr: errors.New("invalid timeout provided"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx, cancelCtx := context.WithCancel(context.Background()) - t.Cleanup(cancelCtx) + common.NewMockWorkerServer(t, tt.serverBehavior.WithT(t).WithDefaultActionsMetadataEndpoint()) - runCmd := createCliRunner(t, GetInitCommand(), GetDeployCommand()) + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetDeployCommand()) - _, workerName := prepareWorkerDirForTest(t) + _, workerName := common.PrepareWorkerDirForTest(t) if tt.workerName != "" { workerName = tt.workerName } @@ -133,38 +145,13 @@ func TestDeployCommand(t *testing.T) { require.NoError(t, err) if tt.patchManifest != nil { - patchManifest(t, tt.patchManifest) + common.PatchManifest(t, tt.patchManifest) } - err = os.Setenv(model.EnvKeyServerUrl, newDeployServerStub(t, ctx, &tt.serverBehavior)) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyServerUrl) - }) - - err = os.Setenv(model.EnvKeySecretsPassword, secretPassword) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeySecretsPassword) - }) - - if tt.token == "" && tt.serverBehavior.wantBearerToken == "" { - tt.token = t.Name() - tt.serverBehavior.wantBearerToken = t.Name() - } - - err = os.Setenv(model.EnvKeyAccessToken, tt.token) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyAccessToken) - }) - cmd := append([]string{"worker", "deploy"}, tt.commandArgs...) err = runCmd(cmd...) - cancelCtx() - if tt.wantErr == nil { assert.NoError(t, err) } else { @@ -174,142 +161,6 @@ func TestDeployCommand(t *testing.T) { } } -var deployUrlPattern = regexp.MustCompile(`^/worker/api/v1/workers(/[\S/]+)?$`) - -type deployServerStubBehavior struct { - waitFor time.Duration - responseStatus int - wantBearerToken string - wantProjectKey string - wantRequestBody *deployRequest - wantMethods []string - existingWorkers map[string]*model.WorkerDetails -} - -type deployServerStub struct { - t *testing.T - ctx context.Context - behavior *deployServerStubBehavior -} - -func newDeployServerStub(t *testing.T, ctx context.Context, behavior *deployServerStubBehavior) string { - stub := deployServerStub{t: t, behavior: behavior, ctx: ctx} - server := httptest.NewUnstartedServer(&stub) - t.Cleanup(server.Close) - server.Start() - return "http:" + "//" + server.Listener.Addr().String() -} - -func (s *deployServerStub) ServeHTTP(res http.ResponseWriter, req *http.Request) { - urlMatch := deployUrlPattern.FindAllStringSubmatch(req.URL.Path, -1) - if len(urlMatch) == 0 { - res.WriteHeader(http.StatusNotFound) - return - } - - if s.behavior.waitFor > 0 { - select { - case <-s.ctx.Done(): - return - case <-time.After(s.behavior.waitFor): - } - } - - // Validate the projectKey - if s.behavior.wantProjectKey != "" { - gotProjectKey := req.URL.Query().Get("projectKey") - if gotProjectKey != s.behavior.wantProjectKey { - s.t.Logf("Invalid projectKey want='%s', got='%s'", s.behavior.wantProjectKey, gotProjectKey) - res.WriteHeader(http.StatusBadRequest) - return - } - } - - // Validate method - var methodValid bool - for _, wantMethod := range s.behavior.wantMethods { - if methodValid = wantMethod == req.Method; methodValid { - break - } - } - - if !methodValid { - res.WriteHeader(http.StatusMethodNotAllowed) - return - } - - // Validate token - if req.Header.Get("authorization") != "Bearer "+s.behavior.wantBearerToken { - res.WriteHeader(http.StatusForbidden) - return - } - - if s.behavior.responseStatus > 0 { - res.WriteHeader(s.behavior.responseStatus) - return - } - - if http.MethodGet != req.Method { - if req.Header.Get("content-type") != "application/json" { - res.WriteHeader(http.StatusBadRequest) - return - } - - // Validate body if requested - if s.behavior.wantRequestBody != nil { - gotData, err := io.ReadAll(req.Body) - if err != nil { - s.t.Logf("Read request body error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - gotRequestBody := deployRequest{} - err = json.Unmarshal(gotData, &gotRequestBody) - if err != nil { - s.t.Logf("Unmarshall request body error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - assertDeployRequestEquals(s.t, s.behavior.wantRequestBody, &gotRequestBody) - } - } - - if http.MethodGet == req.Method { - var workerKey string - - if len(urlMatch[0]) < 1 { - res.WriteHeader(http.StatusNotFound) - return - } else { - workerKey = urlMatch[0][1][1:] - } - - workerDetails, workerExists := s.behavior.existingWorkers[workerKey] - if !workerExists { - res.WriteHeader(http.StatusNotFound) - return - } - - res.WriteHeader(http.StatusOK) - _, err := res.Write([]byte(mustJsonMarshal(s.t, workerDetails))) - require.NoError(s.t, err) - return - } - - // Assume updated or created - if http.MethodPut == req.Method { - res.WriteHeader(http.StatusNoContent) - return - } else if http.MethodPost == req.Method { - res.WriteHeader(http.StatusCreated) - return - } - - res.WriteHeader(http.StatusMethodNotAllowed) -} - func assertDeployRequestEquals(t require.TestingT, want, got *deployRequest) { assert.Equalf(t, want.Key, got.Key, "Key mismatch") assert.Equalf(t, want.Description, got.Description, "Description mismatch") @@ -330,18 +181,31 @@ func assertDeployRequestEquals(t require.TestingT, want, got *deployRequest) { assert.ElementsMatchf(t, wantSecrets, gotSecrets, "Secrets mismatch") } -func getExpectedDeployRequestForAction(t require.TestingT, workerName, actionName, projectKey string, secrets ...*model.Secret) *deployRequest { +func expectDeployRequest(actionsMeta common.ActionsMetadata, workerName, actionName, projectKey string, secrets ...*model.Secret) common.BodyValidator { + return func(t require.TestingT, body []byte) { + want := getExpectedDeployRequestForAction(t, actionsMeta, workerName, actionName, projectKey, secrets...) + got := &deployRequest{} + err := json.Unmarshal(body, got) + require.NoError(t, err) + assertDeployRequestEquals(t, want, got) + } +} + +func getExpectedDeployRequestForAction(t require.TestingT, actionsMeta common.ActionsMetadata, workerName, actionName, projectKey string, secrets ...*model.Secret) *deployRequest { r := &deployRequest{ Key: workerName, Description: "Run a script on " + actionName, Enabled: false, - SourceCode: model.CleanImports(getActionSourceCode(t, actionName)), + SourceCode: common.CleanImports(common.GenerateFromSamples(t, templates, actionName, workerName, "", "worker.ts_template")), Action: actionName, Secrets: secrets, ProjectKey: projectKey, } - if model.ActionNeedsCriteria(actionName) { + actionMeta, err := actionsMeta.FindAction(actionName) + require.NoError(t, err) + + if actionMeta.MandatoryFilter && actionMeta.FilterType == model.FilterTypeRepo { r.FilterCriteria = model.FilterCriteria{ ArtifactFilterCriteria: model.ArtifactFilterCriteria{ RepoKeys: []string{"example-repo-local"}, diff --git a/commands/dry_run_cmd.go b/commands/dry_run_cmd.go index 2a0a6a5..94b89d0 100644 --- a/commands/dry_run_cmd.go +++ b/commands/dry_run_cmd.go @@ -2,8 +2,11 @@ package commands import ( "encoding/json" + "fmt" "net/http" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/jfrog/jfrog-client-go/utils/log" plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" @@ -39,29 +42,34 @@ func GetDryRunCommand() components.Command { Action: func(c *components.Context) error { h := &dryRunHandler{c} - manifest, err := model.ReadManifest() + manifest, err := common.ReadManifest() if err != nil { return err } - if err = manifest.Validate(); err != nil { + server, err := model.GetServerDetails(c) + if err != nil { return err } - server, err := model.GetServerDetails(c) + actionsMeta, err := common.FetchActions(c, server.GetUrl(), server.GetAccessToken(), manifest.ProjectKey) if err != nil { return err } - inputReader := &cmdInputReader{c} + if err = common.ValidateManifest(manifest, actionsMeta); err != nil { + return err + } - data, err := inputReader.readData() + inputReader := common.NewInputReader(c) + + data, err := inputReader.ReadData() if err != nil { return err } if !c.GetBoolFlagValue(model.FlagNoSecrets) { - if err = manifest.DecryptSecrets(); err != nil { + if err = common.DecryptManifestSecrets(manifest); err != nil { return err } } @@ -76,8 +84,19 @@ func (c *dryRunHandler) run(manifest *model.Manifest, serverUrl string, token st if err != nil { return err } - queryParams := c.prepareQueryParams(manifest) - return callWorkerApiWithOutput(c.ctx, serverUrl, token, http.MethodPost, body, http.StatusOK, queryParams, "test", manifest.Name) + return common.CallWorkerApi(c.ctx, common.ApiCallParams{ + Method: http.MethodPost, + ServerUrl: serverUrl, + ServerToken: token, + Body: body, + ProjectKey: manifest.ProjectKey, + Query: map[string]string{ + "debug": fmt.Sprint(manifest.Debug), + }, + OkStatuses: []int{http.StatusOK}, + Path: []string{"test", manifest.Name}, + OnContent: common.PrintJson, + }) } func (c *dryRunHandler) preparePayload(manifest *model.Manifest, serverUrl string, token string, data map[string]any) ([]byte, error) { @@ -85,34 +104,20 @@ func (c *dryRunHandler) preparePayload(manifest *model.Manifest, serverUrl strin var err error - payload.Code, err = manifest.ReadSourceCode() + payload.Code, err = common.ReadSourceCode(manifest) if err != nil { return nil, err } - payload.Code = model.CleanImports(payload.Code) + payload.Code = common.CleanImports(payload.Code) - existingWorker, err := fetchWorkerDetails(c.ctx, serverUrl, token, manifest.Name, manifest.ProjectKey) + existingWorker, err := common.FetchWorkerDetails(c.ctx, serverUrl, token, manifest.Name, manifest.ProjectKey) if err != nil { log.Warn(err.Error()) } if !c.ctx.GetBoolFlagValue(model.FlagNoSecrets) { - payload.StagedSecrets = prepareSecretsUpdate(manifest, existingWorker) + payload.StagedSecrets = common.PrepareSecretsUpdate(manifest, existingWorker) } return json.Marshal(&payload) } - -func (c *dryRunHandler) prepareQueryParams(manifest *model.Manifest) map[string]string { - queryParams := make(map[string]string) - - if manifest.ProjectKey != "" { - queryParams["projectKey"] = manifest.ProjectKey - } - - if manifest.Debug { - queryParams["debug"] = "true" - } - - return queryParams -} diff --git a/commands/dry_run_cmd_test.go b/commands/dry_run_cmd_test.go index 08c38ef..8b27bde 100644 --- a/commands/dry_run_cmd_test.go +++ b/commands/dry_run_cmd_test.go @@ -1,160 +1,136 @@ +//go:build test +// +build test + package commands import ( "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" "os" - "reflect" - "regexp" "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/require" "github.com/jfrog/jfrog-cli-platform-services/model" ) -type dryRunAssertFunc func(t *testing.T, stdOutput []byte, err error, serverBehavior *dryRunServerStubBehavior) - func TestDryRun(t *testing.T) { tests := []struct { name string commandArgs []string - assert dryRunAssertFunc + assert common.AssertOutputFunc patchManifest func(mf *model.Manifest) - // Token to be sent in the request - token string // Use this workerKey instead of a random generated one workerKey string // The server behavior - serverBehavior *dryRunServerStubBehavior + serverStub *common.ServerStub // If provided the cliIn will be filled with this content stdInput string // If provided a temp file will be generated with this content and the file path will be added at the end of the command fileInput string }{ { - name: "nominal case", - token: "my-token", - serverBehavior: &dryRunServerStubBehavior{ - responseStatus: http.StatusOK, - responseBody: map[string]any{ - "my": "payload", - }, - requestToken: "my-token", - }, - commandArgs: []string{mustJsonMarshal(t, map[string]any{"my": "payload"})}, - assert: assertDryRunSucceed, + name: "nominal case", + serverStub: common.NewServerStub(t). + WithTestEndpoint( + validateTestPayloadData(map[string]any{"my": "payload"}), + map[string]any{"my": "payload"}, + ), + commandArgs: []string{common.MustJsonMarshal(t, map[string]any{"my": "payload"})}, + assert: common.AssertOutputJson(map[string]any{"my": "payload"}), }, { - name: "fails if not OK status", - token: "invalid-token", - serverBehavior: &dryRunServerStubBehavior{ - requestToken: "valid-token", - }, + name: "fails if not OK status", + serverStub: common.NewServerStub(t). + WithToken("invalid-token"). + WithTestEndpoint(nil, nil), commandArgs: []string{`{}`}, - assert: assertDryRunFail("command failed with status %d", http.StatusForbidden), + assert: common.AssertOutputErrorRegexp(`command\s.+returned\san\sunexpected\sstatus\scode\s403`), }, { name: "reads from stdin", - token: "valid-token", - stdInput: mustJsonMarshal(t, map[string]any{"my": "request"}), - serverBehavior: &dryRunServerStubBehavior{ - requestToken: "valid-token", - requestBody: map[string]any{"my": "request"}, - responseBody: map[string]any{"valid": "response"}, - responseStatus: http.StatusOK, - }, + stdInput: common.MustJsonMarshal(t, map[string]any{"my": "request"}), + serverStub: common.NewServerStub(t). + WithTestEndpoint( + validateTestPayloadData(map[string]any{"my": "request"}), + map[string]any{"valid": "response"}, + ), commandArgs: []string{"-"}, - assert: assertDryRunSucceed, + assert: common.AssertOutputJson(map[string]any{"valid": "response"}), }, { name: "reads from file", - token: "valid-token", - fileInput: mustJsonMarshal(t, map[string]any{"my": "file-content"}), - serverBehavior: &dryRunServerStubBehavior{ - requestToken: "valid-token", - requestBody: map[string]any{"my": "file-content"}, - responseBody: map[string]any{"valid": "response"}, - responseStatus: http.StatusOK, - }, - assert: assertDryRunSucceed, + fileInput: common.MustJsonMarshal(t, map[string]any{"my": "file-content"}), + serverStub: common.NewServerStub(t). + WithTestEndpoint( + validateTestPayloadData(map[string]any{"my": "file-content"}), + map[string]any{"valid": "response"}, + ), + assert: common.AssertOutputJson(map[string]any{"valid": "response"}), }, { name: "fails if invalid json from argument", commandArgs: []string{`{"my":`}, - assert: assertDryRunFail("invalid json payload: unexpected end of JSON input"), + assert: common.AssertOutputError("invalid json payload: unexpected end of JSON input"), }, { name: "fails if invalid json from file argument", fileInput: `{"my":`, - assert: assertDryRunFail("invalid json payload: unexpected end of JSON input"), + assert: common.AssertOutputError("invalid json payload: unexpected end of JSON input"), }, { name: "fails if invalid json from standard input", commandArgs: []string{"-"}, stdInput: `{"my":`, - assert: assertDryRunFail("unexpected EOF"), + assert: common.AssertOutputError("unexpected EOF"), }, { name: "fails if missing file", commandArgs: []string{"@non-existing-file.json"}, - assert: assertDryRunFail("open non-existing-file.json: no such file or directory"), + assert: common.AssertOutputError("open non-existing-file.json: no such file or directory"), }, { name: "fails if timeout exceeds", commandArgs: []string{"--" + model.FlagTimeout, "500", `{}`}, - serverBehavior: &dryRunServerStubBehavior{ - waitFor: 5 * time.Second, - }, - assert: assertDryRunFail("request timed out after 500ms"), + serverStub: common.NewServerStub(t).WithDelay(5*time.Second).WithTestEndpoint(nil, nil), + assert: common.AssertOutputError("request timed out after 500ms"), }, { name: "fails if invalid timeout", commandArgs: []string{"--" + model.FlagTimeout, "abc", `{}`}, - assert: assertDryRunFail("invalid timeout provided"), + assert: common.AssertOutputError("invalid timeout provided"), }, { name: "fails if empty file path", commandArgs: []string{"@"}, - assert: assertDryRunFail("missing file path"), + assert: common.AssertOutputError("missing file path"), }, { name: "should propagate projectKey", workerKey: "my-worker", - token: "valid-token", - serverBehavior: &dryRunServerStubBehavior{ - requestToken: "valid-token", - requestParams: map[string]string{ - "projectKey": "my-project", - }, - responseBody: map[string]any{"valid": "response"}, - responseStatus: http.StatusOK, - }, + serverStub: common.NewServerStub(t). + WithProjectKey("my-project"). + WithTestEndpoint( + validateTestPayloadData(map[string]any{}), + map[string]any{"valid": "response"}, + ), commandArgs: []string{"-"}, stdInput: `{}`, patchManifest: func(mf *model.Manifest) { mf.ProjectKey = "my-project" mf.Name = "my-worker" }, - assert: assertDryRunSucceed, + assert: common.AssertOutputJson(map[string]any{"valid": "response"}), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx, cancelCtx := context.WithCancel(context.Background()) - t.Cleanup(cancelCtx) - - runCmd := createCliRunner(t, GetInitCommand(), GetDryRunCommand()) + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetDryRunCommand()) - _, workerName := prepareWorkerDirForTest(t) + _, workerName := common.PrepareWorkerDirForTest(t) if tt.workerKey != "" { workerName = tt.workerKey @@ -164,185 +140,56 @@ func TestDryRun(t *testing.T) { require.NoError(t, err) if tt.patchManifest != nil { - patchManifest(t, tt.patchManifest) + common.PatchManifest(t, tt.patchManifest) } - serverResponseStubs := map[string]*dryRunServerStubBehavior{} - if tt.serverBehavior != nil { - key := workerName - serverResponseStubs[key] = tt.serverBehavior + if tt.serverStub == nil { + tt.serverStub = common.NewServerStub(t) } - err = os.Setenv(model.EnvKeyServerUrl, newDryRunServerStub(t, ctx, serverResponseStubs)) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyServerUrl) - }) - - err = os.Setenv(model.EnvKeyAccessToken, tt.token) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyAccessToken) - }) - - err = os.Setenv(model.EnvKeySecretsPassword, secretPassword) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeySecretsPassword) - }) + common.NewMockWorkerServer(t, + tt.serverStub. + WithT(t). + WithDefaultActionsMetadataEndpoint(). + WithGetOneEndpoint(). + WithWorkers(&model.WorkerDetails{ + Key: workerName, + }), + ) if tt.stdInput != "" { - cliIn = bytes.NewReader([]byte(tt.stdInput)) + common.SetCliIn(bytes.NewReader([]byte(tt.stdInput))) t.Cleanup(func() { - cliIn = os.Stdin + common.SetCliIn(os.Stdin) }) } if tt.fileInput != "" { - tt.commandArgs = append(tt.commandArgs, "@"+createTempFileWithContent(t, tt.fileInput)) + tt.commandArgs = append(tt.commandArgs, "@"+common.CreateTempFileWithContent(t, tt.fileInput)) } var output bytes.Buffer - cliOut = &output + common.SetCliOut(&output) t.Cleanup(func() { - cliOut = os.Stdout + common.SetCliOut(os.Stdout) }) cmd := append([]string{"worker", "dry-run"}, tt.commandArgs...) err = runCmd(cmd...) - cancelCtx() - - tt.assert(t, output.Bytes(), err, tt.serverBehavior) + tt.assert(t, output.Bytes(), err) }) } } -func assertDryRunSucceed(t *testing.T, output []byte, err error, serverBehavior *dryRunServerStubBehavior) { - require.NoError(t, err) - - outputData := map[string]any{} - - err = json.Unmarshal(output, &outputData) - require.NoError(t, err) - - assert.Equal(t, serverBehavior.responseBody, outputData) -} - -func assertDryRunFail(errorMessage string, errorMessageArgs ...any) dryRunAssertFunc { - return func(t *testing.T, stdOutput []byte, err error, serverResponse *dryRunServerStubBehavior) { - require.Error(t, err) - assert.EqualError(t, err, fmt.Sprintf(errorMessage, errorMessageArgs...)) - } -} - -var dryRunUrlPattern = regexp.MustCompile(`^/worker/api/v1/test/([^\s?]+)(\?.+)?$`) - -type dryRunServerStubBehavior struct { - waitFor time.Duration - responseStatus int - responseBody map[string]any - requestToken string - requestBody map[string]any - requestParams map[string]string -} - -type dryRunServerStub struct { - t *testing.T - ctx context.Context - stubs map[string]*dryRunServerStubBehavior -} - -func newDryRunServerStub(t *testing.T, ctx context.Context, responseStubs map[string]*dryRunServerStubBehavior) string { - stub := dryRunServerStub{stubs: responseStubs, ctx: ctx} - server := httptest.NewUnstartedServer(&stub) - t.Cleanup(server.Close) - server.Start() - return "http:" + "//" + server.Listener.Addr().String() -} - -func (s *dryRunServerStub) ServeHTTP(res http.ResponseWriter, req *http.Request) { - matches := dryRunUrlPattern.FindAllStringSubmatch(req.URL.Path, -1) - if len(matches) == 0 || len(matches[0][1]) < 1 { - res.WriteHeader(http.StatusNotFound) - return - } - - if req.Header.Get("content-type") != "application/json" { - res.WriteHeader(http.StatusBadRequest) - return - } - - workerName := matches[0][1] - - behavior, exists := s.stubs[workerName] - if !exists { - res.WriteHeader(http.StatusNotFound) - return - } - - if behavior.waitFor > 0 { - select { - case <-s.ctx.Done(): - return - case <-time.After(behavior.waitFor): +func validateTestPayloadData(data any) common.BodyValidator { + return common.ValidateJsonFunc(data, func(in any) any { + var gotData any + if m, isMap := data.(map[string]any); isMap { + gotData = m } - } - - // Validate token - if req.Header.Get("authorization") != "Bearer "+behavior.requestToken { - res.WriteHeader(http.StatusForbidden) - return - } - - // Validate body if requested - if behavior.requestBody != nil { - wantData, checkRequestData := behavior.responseBody["data"] - - if checkRequestData { - gotData, err := io.ReadAll(req.Body) - if err != nil { - s.t.Logf("Read request body error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - decodedData := map[string]any{} - err = json.Unmarshal(gotData, &decodedData) - if err != nil { - s.t.Logf("Unmarshall request body error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - if !reflect.DeepEqual(wantData, decodedData) { - res.WriteHeader(http.StatusBadRequest) - return - } - } - } - - // Validate the request params - for k, v := range behavior.requestParams { - if req.URL.Query().Get(k) != v { - res.WriteHeader(http.StatusBadRequest) - return - } - } - - bodyBytes, err := json.Marshal(behavior.responseBody) - if err != nil { - s.t.Logf("Marshall error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - res.WriteHeader(behavior.responseStatus) - _, err = res.Write(bodyBytes) - if err != nil { - s.t.Logf("Write error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - } + return gotData + }) } diff --git a/commands/execute_cmd.go b/commands/execute_cmd.go index 57f361d..ccae3c1 100644 --- a/commands/execute_cmd.go +++ b/commands/execute_cmd.go @@ -4,6 +4,8 @@ import ( "encoding/json" "net/http" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/jfrog/jfrog-client-go/utils/log" plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" @@ -31,7 +33,7 @@ func GetExecuteCommand() components.Command { } func runExecuteCommand(c *components.Context) error { - workerKey, projectKey, err := extractProjectAndKeyFromCommandContext(c, c.Arguments, 1, true) + workerKey, projectKey, err := common.ExtractProjectAndKeyFromCommandContext(c, c.Arguments, 1, true) if err != nil { return err } @@ -45,9 +47,9 @@ func runExecuteCommand(c *components.Context) error { return err } - inputReader := &cmdInputReader{c} + inputReader := common.NewInputReader(c) - data, err := inputReader.readData() + data, err := inputReader.ReadData() if err != nil { return err } @@ -57,10 +59,14 @@ func runExecuteCommand(c *components.Context) error { return err } - var queryParams map[string]string - if projectKey != "" { - queryParams = map[string]string{"projectKey": projectKey} - } - - return callWorkerApiWithOutput(c, server.GetUrl(), server.GetAccessToken(), http.MethodPost, body, http.StatusOK, queryParams, "execute", workerKey) + return common.CallWorkerApi(c, common.ApiCallParams{ + Method: http.MethodPost, + ServerUrl: server.GetUrl(), + ServerToken: server.GetAccessToken(), + OkStatuses: []int{http.StatusOK}, + Body: body, + ProjectKey: projectKey, + Path: []string{"execute", workerKey}, + OnContent: common.PrintJson, + }) } diff --git a/commands/execute_cmd_test.go b/commands/execute_cmd_test.go index 363525d..48a7103 100644 --- a/commands/execute_cmd_test.go +++ b/commands/execute_cmd_test.go @@ -1,38 +1,32 @@ +//go:build test +// +build test + package commands import ( "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" "os" - "reflect" - "regexp" "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/require" "github.com/jfrog/jfrog-cli-platform-services/model" ) -type executeAssertFunc func(t *testing.T, stdOutput []byte, err error, serverBehavior *executeServerStubBehavior) - func TestExecute(t *testing.T) { + payload := map[string]any{"my": "payload"} + tests := []struct { name string commandArgs []string - assert executeAssertFunc + assert common.AssertOutputFunc action string workerKey string - // Token to be sent in the request - token string // The server behavior - serverBehavior *executeServerStubBehavior + serverStub *common.ServerStub // If provided the cliIn will be filled with this content stdInput string // If provided a temp file will be generated with this content and the file path will be added at the end of the command @@ -41,133 +35,111 @@ func TestExecute(t *testing.T) { }{ { name: "execute from manifest", - serverBehavior: &executeServerStubBehavior{ - responseStatus: http.StatusOK, - responseBody: map[string]any{ - "my": "payload", - }, - }, - commandArgs: []string{mustJsonMarshal(t, map[string]any{"my": "payload"})}, - assert: assertExecuteSucceed, + serverStub: common.NewServerStub(t). + WithExecuteEndpoint(common.ValidateJson(payload), payload), + commandArgs: []string{common.MustJsonMarshal(t, payload)}, + assert: common.AssertOutputJson(payload), }, { name: "execute with workerKey", workerKey: "my-worker", - serverBehavior: &executeServerStubBehavior{ - responseStatus: http.StatusOK, - responseBody: map[string]any{ - "my": "payload", - }, - }, - commandArgs: []string{"my-worker", mustJsonMarshal(t, map[string]any{"my": "payload"})}, - assert: assertExecuteSucceed, + serverStub: common.NewServerStub(t). + WithExecuteEndpoint(common.ValidateJson(payload), payload), + commandArgs: []string{"my-worker", common.MustJsonMarshal(t, payload)}, + assert: common.AssertOutputJson(payload), }, { - name: "fails if not a GENERIC_EVENT", - action: "BEFORE_DOWNLOAD", - serverBehavior: &executeServerStubBehavior{}, - commandArgs: []string{`{}`}, - assert: assertExecuteFail("only the GENERIC_EVENT actions are executable. Got BEFORE_DOWNLOAD"), + name: "fails if not a GENERIC_EVENT", + action: "BEFORE_DOWNLOAD", + serverStub: common.NewServerStub(t), + commandArgs: []string{`{}`}, + assert: common.AssertOutputError("only the GENERIC_EVENT actions are executable. Got BEFORE_DOWNLOAD"), }, { - name: "fails if not OK status", - token: "invalid-token", - serverBehavior: &executeServerStubBehavior{ - requestToken: "valid-token", - }, + name: "fails if not OK status", + serverStub: common.NewServerStub(t).WithToken("invalid-token").WithExecuteEndpoint(nil, nil), commandArgs: []string{`{}`}, - assert: assertExecuteFail("command failed with status %d", http.StatusForbidden), + assert: common.AssertOutputErrorRegexp(`command\sPOST.+returned\san\sunexpected\sstatus\scode\s403`), }, { name: "reads from stdin", - stdInput: mustJsonMarshal(t, map[string]any{"my": "request"}), - serverBehavior: &executeServerStubBehavior{ - requestBody: map[string]any{"my": "request"}, - responseBody: map[string]any{"valid": "response"}, - responseStatus: http.StatusOK, - }, + stdInput: common.MustJsonMarshal(t, map[string]any{"my": "request"}), + serverStub: common.NewServerStub(t). + WithExecuteEndpoint( + common.ValidateJson(map[string]any{"my": "request"}), + map[string]any{"valid": "response"}, + ), commandArgs: []string{"-"}, - assert: assertExecuteSucceed, + assert: common.AssertOutputJson(map[string]any{"valid": "response"}), }, { name: "reads from file", - fileInput: mustJsonMarshal(t, map[string]any{"my": "file-content"}), - serverBehavior: &executeServerStubBehavior{ - requestBody: map[string]any{"my": "file-content"}, - responseBody: map[string]any{"valid": "response"}, - responseStatus: http.StatusOK, - }, - assert: assertExecuteSucceed, + fileInput: common.MustJsonMarshal(t, map[string]any{"my": "file-content"}), + serverStub: common.NewServerStub(t). + WithExecuteEndpoint( + common.ValidateJson(map[string]any{"my": "file-content"}), + map[string]any{"valid": "response"}, + ), + assert: common.AssertOutputJson(map[string]any{"valid": "response"}), }, { name: "should propagate projectKey", workerKey: "my-worker", - token: "valid-token", - serverBehavior: &executeServerStubBehavior{ - requestToken: "valid-token", - requestParams: map[string]string{ - "projectKey": "my-project", - }, - responseBody: map[string]any{"valid": "response"}, - responseStatus: http.StatusOK, - }, + serverStub: common.NewServerStub(t). + WithProjectKey("my-project"). + WithExecuteEndpoint(nil, payload), commandArgs: []string{"-"}, stdInput: `{}`, patchManifest: func(mf *model.Manifest) { mf.ProjectKey = "my-project" mf.Name = "my-worker" }, - assert: assertExecuteSucceed, + assert: common.AssertOutputJson(payload), }, { name: "fails if invalid json from argument", commandArgs: []string{`{"my":`}, - assert: assertExecuteFail("invalid json payload: unexpected end of JSON input"), + assert: common.AssertOutputError("invalid json payload: unexpected end of JSON input"), }, { name: "fails if invalid json from file argument", fileInput: `{"my":`, - assert: assertExecuteFail("invalid json payload: unexpected end of JSON input"), + assert: common.AssertOutputError("invalid json payload: unexpected end of JSON input"), }, { name: "fails if invalid json from standard input", commandArgs: []string{"-"}, stdInput: `{"my":`, - assert: assertExecuteFail("unexpected EOF"), + assert: common.AssertOutputError("unexpected EOF"), }, { name: "fails if missing file", commandArgs: []string{"@non-existing-file.json"}, - assert: assertExecuteFail("open non-existing-file.json: no such file or directory"), + assert: common.AssertOutputError("open non-existing-file.json: no such file or directory"), }, { name: "fails if timeout exceeds", commandArgs: []string{"--" + model.FlagTimeout, "500", `{}`}, - serverBehavior: &executeServerStubBehavior{ - waitFor: 5 * time.Second, - }, - assert: assertExecuteFail("request timed out after 500ms"), + serverStub: common.NewServerStub(t).WithDelay(5*time.Second).WithExecuteEndpoint(nil, nil), + assert: common.AssertOutputError("request timed out after 500ms"), }, { name: "fails if invalid timeout", commandArgs: []string{"--" + model.FlagTimeout, "abc", `{}`}, - assert: assertExecuteFail("invalid timeout provided"), + assert: common.AssertOutputError("invalid timeout provided"), }, { name: "fails if empty file path", commandArgs: []string{"@"}, - assert: assertExecuteFail("missing file path"), + assert: common.AssertOutputError("missing file path"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx, cancelCtx := context.WithCancel(context.Background()) - t.Cleanup(cancelCtx) + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetExecuteCommand()) - runCmd := createCliRunner(t, GetInitCommand(), GetExecuteCommand()) - - _, workerName := prepareWorkerDirForTest(t) + _, workerName := common.PrepareWorkerDirForTest(t) if tt.workerKey != "" { workerName = tt.workerKey @@ -182,185 +154,46 @@ func TestExecute(t *testing.T) { require.NoError(t, err) if tt.patchManifest != nil { - patchManifest(t, tt.patchManifest) - } - - serverResponseStubs := map[string]*executeServerStubBehavior{} - if tt.serverBehavior != nil { - serverResponseStubs[workerName] = tt.serverBehavior + common.PatchManifest(t, tt.patchManifest) } - if tt.token == "" { - tt.token = t.Name() - if tt.serverBehavior != nil && tt.serverBehavior.requestToken == "" { - tt.serverBehavior.requestToken = t.Name() - } + if tt.serverStub == nil { + tt.serverStub = common.NewServerStub(t) } - err = os.Setenv(model.EnvKeyServerUrl, newExecuteServerStub(t, ctx, serverResponseStubs)) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyServerUrl) - }) - - err = os.Setenv(model.EnvKeyAccessToken, tt.token) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyAccessToken) - }) + common.NewMockWorkerServer(t, + tt.serverStub. + WithT(t). + WithDefaultActionsMetadataEndpoint(). + WithGetOneEndpoint(). + WithWorkers(&model.WorkerDetails{ + Key: workerName, + }), + ) if tt.stdInput != "" { - cliIn = bytes.NewReader([]byte(tt.stdInput)) + common.SetCliIn(bytes.NewReader([]byte(tt.stdInput))) t.Cleanup(func() { - cliIn = os.Stdin + common.SetCliIn(os.Stdin) }) } if tt.fileInput != "" { - tt.commandArgs = append(tt.commandArgs, "@"+createTempFileWithContent(t, tt.fileInput)) + tt.commandArgs = append(tt.commandArgs, "@"+common.CreateTempFileWithContent(t, tt.fileInput)) } var output bytes.Buffer - cliOut = &output + common.SetCliOut(&output) t.Cleanup(func() { - cliOut = os.Stdout + common.SetCliOut(os.Stdout) }) cmd := append([]string{"worker", "execute"}, tt.commandArgs...) err = runCmd(cmd...) - cancelCtx() - - tt.assert(t, output.Bytes(), err, tt.serverBehavior) + tt.assert(t, output.Bytes(), err) }) } } - -func assertExecuteSucceed(t *testing.T, output []byte, err error, serverBehavior *executeServerStubBehavior) { - require.NoError(t, err) - - outputData := map[string]any{} - - err = json.Unmarshal(output, &outputData) - require.NoError(t, err) - - assert.Equal(t, serverBehavior.responseBody, outputData) -} - -func assertExecuteFail(errorMessage string, errorMessageArgs ...any) executeAssertFunc { - return func(t *testing.T, stdOutput []byte, err error, serverResponse *executeServerStubBehavior) { - require.Error(t, err) - assert.EqualError(t, err, fmt.Sprintf(errorMessage, errorMessageArgs...)) - } -} - -var executeUrlPattern = regexp.MustCompile(`^/worker/api/v1/execute/([\S/]+)$`) - -type executeServerStubBehavior struct { - waitFor time.Duration - responseStatus int - responseBody map[string]any - requestToken string - requestBody map[string]any - requestParams map[string]string -} - -type executeServerStub struct { - t *testing.T - ctx context.Context - stubs map[string]*executeServerStubBehavior -} - -func newExecuteServerStub(t *testing.T, ctx context.Context, responseStubs map[string]*executeServerStubBehavior) string { - stub := executeServerStub{stubs: responseStubs, ctx: ctx} - server := httptest.NewUnstartedServer(&stub) - t.Cleanup(server.Close) - server.Start() - return "http:" + "//" + server.Listener.Addr().String() -} - -func (s *executeServerStub) ServeHTTP(res http.ResponseWriter, req *http.Request) { - matches := executeUrlPattern.FindAllStringSubmatch(req.URL.Path, -1) - if len(matches) == 0 || len(matches[0][1]) < 1 { - res.WriteHeader(http.StatusNotFound) - return - } - - if req.Header.Get("content-type") != "application/json" { - res.WriteHeader(http.StatusBadRequest) - return - } - - workerName := matches[0][1] - - behavior, exists := s.stubs[workerName] - if !exists { - res.WriteHeader(http.StatusNotFound) - return - } - - if behavior.waitFor > 0 { - select { - case <-s.ctx.Done(): - return - case <-time.After(behavior.waitFor): - } - } - - // Validate token - if req.Header.Get("authorization") != "Bearer "+behavior.requestToken { - res.WriteHeader(http.StatusForbidden) - return - } - - // Validate the request params - for k, v := range behavior.requestParams { - if req.URL.Query().Get(k) != v { - res.WriteHeader(http.StatusBadRequest) - return - } - } - - // Validate body if requested - if behavior.requestBody != nil { - wantData, checkRequestData := behavior.responseBody["data"] - - if checkRequestData { - gotData, err := io.ReadAll(req.Body) - if err != nil { - s.t.Logf("Read request body error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - decodedData := map[string]any{} - err = json.Unmarshal(gotData, &decodedData) - if err != nil { - s.t.Logf("Unmarshall request body error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - if !reflect.DeepEqual(wantData, decodedData) { - res.WriteHeader(http.StatusBadRequest) - return - } - } - } - - bodyBytes, err := json.Marshal(behavior.responseBody) - if err != nil { - s.t.Logf("Marshall error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - return - } - - res.WriteHeader(behavior.responseStatus) - _, err = res.Write(bodyBytes) - if err != nil { - s.t.Logf("Write error: %+v", err) - res.WriteHeader(http.StatusInternalServerError) - } -} diff --git a/commands/init_cmd.go b/commands/init_cmd.go index b8e650a..bae1961 100644 --- a/commands/init_cmd.go +++ b/commands/init_cmd.go @@ -9,6 +9,9 @@ import ( "strings" "text/template" + plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/jfrog/jfrog-cli-platform-services/model" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" @@ -24,38 +27,73 @@ func GetInitCommand() components.Command { Description: "Initialize a worker", Aliases: []string{"i"}, Flags: []components.Flag{ - components.NewBoolFlag(model.FlagForce, "Whether or not to overwrite existing files"), + plugins_common.GetServerIdFlag(), + model.GetProjectKeyFlag(), + model.GetApplicationFlag(), model.GetNoTestFlag(), + model.GetTimeoutFlag(), + components.NewBoolFlag(model.FlagForce, "Whether or not to overwrite existing files"), }, Arguments: []components.Argument{ - {Name: "action", Description: fmt.Sprintf("The action that will trigger the worker (%s)", strings.Join(strings.Split(model.ActionNames(), "|"), ", "))}, + {Name: "action", Description: "The action that will trigger the worker. Use `jf worker list-event` to see the list of available actions."}, {Name: "worker-name", Description: "The name of the worker"}, }, Action: func(c *components.Context) error { - if len(c.Arguments) < 2 { - return fmt.Errorf("the action or worker name is missing, please see 'jf worker init --help'") - } - action := c.Arguments[0] - workerName := c.Arguments[1] - workingDir, err := os.Getwd() - if err != nil { - return err - } - if err := initWorker(workingDir, action, workerName, c.GetBoolFlagValue(model.FlagForce), c.GetBoolFlagValue(model.FlagNoTest)); err != nil { - return err - } - log.Info(fmt.Sprintf("Worker %s initialized", workerName)) - return nil + return (&initHandler{c}).run() }, } } -func initWorker(targetDir string, action string, workerName string, force bool, skipTests bool) error { - if !model.ActionIsValid(action) { - return fmt.Errorf("invalid action '%s' action should be one of: %s", action, strings.Split(model.ActionNames(), "|")) +type initHandler struct { + *components.Context +} + +func (c *initHandler) run() error { + if len(c.Arguments) < 2 { + return fmt.Errorf("the action or worker name is missing, please see 'jf worker init --help'") + } + + action := c.Arguments[0] + workerName := c.Arguments[1] + + workingDir, err := os.Getwd() + if err != nil { + return err + } + + projectKey := c.GetStringFlagValue(model.FlagProjectKey) + force := c.GetBoolFlagValue(model.FlagForce) + skipTests := c.GetBoolFlagValue(model.FlagNoTest) + + err = c.initWorker(workingDir, action, workerName, projectKey, force, skipTests) + if err != nil { + return err + } + + log.Info(fmt.Sprintf("Worker %s initialized", workerName)) + return nil +} + +func (c *initHandler) initWorker(targetDir string, action string, workerName string, projectKey string, force bool, skipTests bool) error { + server, err := model.GetServerDetails(c.Context) + if err != nil { + return err + } + + actionsMeta, err := common.FetchActions(c.Context, server.Url, server.AccessToken, projectKey) + if err != nil { + return err + } + + application := c.GetStringFlagValue(model.FlagApplication) + + actionMeta, err := actionsMeta.FindAction(action, application) + if err != nil { + log.Debug(fmt.Sprintf("Cannot not find action '%s': %+v", action, err)) + return fmt.Errorf("invalid action '%s' action should be one of: %s", action, actionsMeta.ActionsNames()) } - generate := initGenerator(targetDir, action, workerName, force, skipTests) + generate := c.initGenerator(targetDir, workerName, projectKey, force, skipTests, actionMeta) if err := generate("package.json_template", "package.json"); err != nil { return err @@ -69,20 +107,24 @@ func initWorker(targetDir string, action string, workerName string, force bool, return err } - if err := generate(action+".ts_template", "worker.ts"); err != nil { + if err := generate("worker.ts_template", "worker.ts"); err != nil { return err } if !skipTests { - if err := generate(action+".spec.ts_template", "worker.spec.ts"); err != nil { + if err := generate("worker.spec.ts_template", "worker.spec.ts"); err != nil { return err } } + if err := c.generateTypesFile(targetDir, actionMeta, force); err != nil { + return err + } + return nil } -func checkFileBeforeGenerate(filePath string, failIfExists bool) error { +func (c *initHandler) checkFileBeforeGenerate(filePath string, failIfExists bool) error { if _, err := os.Stat(filePath); err == nil || !errors.Is(err, os.ErrNotExist) { if failIfExists { return fmt.Errorf("%s already exists in %s, please use '--force' to overwrite if you know what you are doing", path.Base(filePath), path.Dir(filePath)) @@ -92,7 +134,24 @@ func checkFileBeforeGenerate(filePath string, failIfExists bool) error { return nil } -func initGenerator(targetDir string, action string, workerName string, force bool, skipTests bool) func(string, string) error { +func (c *initHandler) initGenerator(targetDir string, workerName string, projectKey string, force bool, skipTests bool, md *model.ActionMetadata) func(string, string) error { + params := map[string]any{ + "Action": md.Action.Name, + "Application": md.Action.Application, + "WorkerName": workerName, + "HasRepoFilterCriteria": md.MandatoryFilter && md.FilterType == model.FilterTypeRepo, + "HasTests": !skipTests, + "HasRequestType": md.ExecutionRequestType != "", + "ExecutionRequestType": md.ExecutionRequestType, + "ProjectKey": projectKey, + "SourceCode": md.SampleCode, + } + + usedTypes := common.ExtractActionUsedTypes(md) + if len(usedTypes) > 0 { + params["UsedTypes"] = strings.Join(usedTypes, ", ") + } + return func(templateName, outputFilename string) error { tpl, err := template.New(templateName).ParseFS(templates, "templates/"+templateName) if err != nil { @@ -101,7 +160,7 @@ func initGenerator(targetDir string, action string, workerName string, force boo filePath := path.Join(targetDir, outputFilename) - err = checkFileBeforeGenerate(filePath, !force) + err = c.checkFileBeforeGenerate(filePath, !force) if err != nil { return err } @@ -111,11 +170,31 @@ func initGenerator(targetDir string, action string, workerName string, force boo return err } - return tpl.Execute(out, map[string]any{ - "Action": action, - "WorkerName": workerName, - "HasCriteria": model.ActionNeedsCriteria(action), - "HasTests": !skipTests, - }) + defer common.CloseQuietly(out) + + return tpl.Execute(out, params) } } + +func (c *initHandler) generateTypesFile(targetDir string, actionMeta *model.ActionMetadata, force bool) error { + typesFilePath := path.Join(targetDir, "types.ts") + + err := c.checkFileBeforeGenerate(typesFilePath, !force) + if err != nil { + return err + } + + out, err := os.OpenFile(typesFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + + defer common.CloseQuietly(out) + + _, err = out.WriteString(common.AddExportToTypesDeclarations(actionMeta.TypesDefinitions)) + if err != nil { + return err + } + + return err +} diff --git a/commands/init_cmd_test.go b/commands/init_cmd_test.go index c4a6832..9cec44d 100644 --- a/commands/init_cmd_test.go +++ b/commands/init_cmd_test.go @@ -1,3 +1,6 @@ +//go:build test +// +build test + package commands import ( @@ -5,9 +8,10 @@ import ( "os" "path" "regexp" - "strings" "testing" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/jfrog/jfrog-cli-platform-services/model" "github.com/stretchr/testify/assert" @@ -16,7 +20,7 @@ import ( type runCommandFunc func(args ...string) error -func TestGetCommand(t *testing.T) { +func TestInitCommand(t *testing.T) { cmd := GetInitCommand() assert.Equalf(t, "init", cmd.Name, "Invalid command name") @@ -37,6 +41,7 @@ func TestGetCommand(t *testing.T) { func TestInitWorker(t *testing.T) { tests := []struct { name string + stub *common.ServerStub test func(t *testing.T, runCommand runCommandFunc) }{ { @@ -56,8 +61,17 @@ func TestInitWorker(t *testing.T) { { name: "invalid action", test: func(t *testing.T, runCommand runCommandFunc) { - err := runCommand("worker", "init", "HACK_SYSTEM", "root") - assert.EqualError(t, err, fmt.Sprintf("invalid action '%s' action should be one of: %s", "HACK_SYSTEM", strings.Split(model.ActionNames(), "|"))) + err := runCommand("worker", "init", "--timeout-ms", "60000", "HACK_SYSTEM", "root") + assert.Regexp(t, regexp.MustCompile(`^\s*invalid\s+action\s+'HACK_SYSTEM'\s+action\s+should\s+be\s+one\s+of:\s+\[[^]]+]\s*$`), err) + }, + }, + { + name: "should propagate projectKey", + stub: common.NewServerStub(t).WithDefaultActionsMetadataEndpoint().WithProjectKey("prj-1"), + test: func(t *testing.T, runCommand runCommandFunc) { + common.PrepareWorkerDirForTest(t) + err := runCommand("worker", "init", "--"+model.FlagProjectKey, "prj-1", "BEFORE_DOWNLOAD", "root") + assert.NoError(t, err) }, }, { @@ -107,19 +121,19 @@ func TestInitWorker(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.test(t, createCliRunner(t, GetInitCommand())) + stub := tt.stub + if stub == nil { + stub = common.NewServerStub(t).WithDefaultActionsMetadataEndpoint() + } + common.NewMockWorkerServer(t, stub) + tt.test(t, common.CreateCliRunner(t, GetInitCommand())) }) } } func testGenerateWithOverwrite(fileName string, overwrite bool) func(t *testing.T, runCommand runCommandFunc) { return func(t *testing.T, runCommand runCommandFunc) { - dir, err := os.MkdirTemp("", "worker-*.init") - require.NoError(t, err) - - t.Cleanup(func() { - _ = os.RemoveAll(dir) - }) + dir, _ := common.PrepareWorkerDirForTest(t) // Simulate an existing file f, err := os.OpenFile(path.Join(dir, fileName), os.O_CREATE|os.O_WRONLY, os.ModePerm) @@ -152,16 +166,14 @@ func testGenerateWithOverwrite(fileName string, overwrite bool) func(t *testing. assert.NoError(t, err) } else { require.NotNilf(t, err, "an error was expected") - errMatched, err := regexp.MatchString(fmt.Sprintf(`%s already exists in \S+/%s, please use '--force' to overwrite if you know what you are doing`, fileName, workerName), err.Error()) - require.NoError(t, err) - assert.True(t, errMatched) + assert.Regexp(t, fmt.Sprintf(`%s already exists in \S+/%s, please use '--force' to overwrite if you know what you are doing`, fileName, workerName), err.Error()) } } } func testGenerateAction(actionName string, withTests bool, runCommand runCommandFunc) func(t *testing.T) { return func(t *testing.T) { - dir, workerName := prepareWorkerDirForTest(t) + dir, workerName := common.PrepareWorkerDirForTest(t) manifestPath := path.Join(dir, "manifest.json") workerSourcePath := path.Join(dir, "worker.ts") @@ -169,11 +181,11 @@ func testGenerateAction(actionName string, withTests bool, runCommand runCommand packageJsonPath := path.Join(dir, "package.json") tsconfigJsonPath := path.Join(dir, "tsconfig.json") - wantManifest := generateForTest(t, actionName, workerName, "manifest.json_template", !withTests) - wantPackageJson := generateForTest(t, actionName, workerName, "package.json_template", !withTests) - wantWorkerSource := generateForTest(t, actionName, workerName, actionName+".ts_template", !withTests) - wantWorkerTestSource := generateForTest(t, actionName, workerName, actionName+".spec.ts_template", !withTests) - wantTsconfig := generateForTest(t, actionName, workerName, "tsconfig.json_template", !withTests) + wantManifest := common.GenerateFromSamples(t, templates, actionName, workerName, "", "manifest.json_template", !withTests) + wantPackageJson := common.GenerateFromSamples(t, templates, actionName, workerName, "", "package.json_template", !withTests) + wantWorkerSource := common.GenerateFromSamples(t, templates, actionName, workerName, "", "worker.ts_template", !withTests) + wantWorkerTestSource := common.GenerateFromSamples(t, templates, actionName, workerName, "", "worker.spec.ts_template", !withTests) + wantTsconfig := common.GenerateFromSamples(t, templates, actionName, workerName, "", "tsconfig.json_template", !withTests) commandArgs := []string{"worker", "init"} if !withTests { @@ -209,7 +221,8 @@ func testGenerateAction(actionName string, withTests bool, runCommand runCommand } func testGenerateAllActions(t *testing.T, runCommand runCommandFunc) { - for _, actionName := range strings.Split(model.ActionNames(), "|") { + actionsMeta := common.LoadSampleActions(t) + for _, actionName := range actionsMeta.ActionsNames() { t.Run(actionName, testGenerateAction(actionName, true, runCommand)) t.Run(actionName+" without tests", testGenerateAction(actionName, false, runCommand)) } diff --git a/commands/list_cmd.go b/commands/list_cmd.go index d23d133..b982173 100644 --- a/commands/list_cmd.go +++ b/commands/list_cmd.go @@ -1,17 +1,16 @@ package commands import ( - "encoding/csv" "encoding/json" "fmt" "net/http" "slices" "strings" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/jfrog/jfrog-cli-platform-services/model" ) @@ -33,7 +32,7 @@ func GetListCommand() components.Command { Arguments: []components.Argument{ { Name: "action", - Description: fmt.Sprintf("Only show workers of this type.\n\t\tShould be one of (%s).", strings.Join(strings.Split(model.ActionNames(), "|"), ", ")), + Description: "Only show workers of this type.\n\t\tUse `jf worker list-event` to see all available actions.", Optional: true, }, }, @@ -48,74 +47,59 @@ func GetListCommand() components.Command { } func runListCommand(ctx *components.Context, serverUrl string, token string) error { - api := "workers" params := make(map[string]string) - if len(ctx.Arguments) > 0 { - params["action"] = ctx.Arguments[0] - } - - projectKey := ctx.GetStringFlagValue(model.FlagProjectKey) - if projectKey != "" { - params["projectKey"] = projectKey - } + var action string - res, discardReq, err := callWorkerApi(ctx, serverUrl, token, http.MethodGet, nil, params, api) - if discardReq != nil { - defer discardReq() + if len(ctx.Arguments) > 0 { + action = strings.TrimSpace(ctx.Arguments[0]) } - if err != nil { - return err + if action != "" { + params["action"] = action } + contentHandler := printWorkerDetailsAsCsv if ctx.GetBoolFlagValue(model.FlagJsonOutput) { - return outputApiResponse(res, http.StatusOK) + contentHandler = common.PrintJson } - return formatListResponseAsCsv(res, http.StatusOK) + return common.CallWorkerApi(ctx, common.ApiCallParams{ + Method: http.MethodGet, + ServerUrl: serverUrl, + ServerToken: token, + Query: params, + OkStatuses: []int{http.StatusOK}, + Path: []string{"workers"}, + ProjectKey: ctx.GetStringFlagValue(model.FlagProjectKey), + OnContent: contentHandler, + }) } -func formatListResponseAsCsv(res *http.Response, okStatus int) error { - return processApiResponse(res, func(responseBytes []byte, statusCode int) error { - var err error - - if res.StatusCode != okStatus { - err = fmt.Errorf("command failed with status %d", res.StatusCode) - } - - if err == nil { - allWorkers := getAllResponse{} +func printWorkerDetailsAsCsv(responseBytes []byte) error { + var err error + allWorkers := getAllResponse{} - err = json.Unmarshal(responseBytes, &allWorkers) - if err != nil { - return nil - } - - writer := csv.NewWriter(cliOut) - - slices.SortFunc(allWorkers.Workers, func(a, b *model.WorkerDetails) int { - return strings.Compare(a.Key, b.Key) - }) + err = json.Unmarshal(responseBytes, &allWorkers) + if err != nil { + return nil + } - for _, wk := range allWorkers.Workers { - err = writer.Write([]string{ - wk.Key, wk.Action, wk.Description, fmt.Sprint(wk.Enabled), - }) - if err != nil { - return err - } - } + writer := common.NewCsvWriter() - writer.Flush() + slices.SortFunc(allWorkers.Workers, func(a, b *model.WorkerDetails) int { + return strings.Compare(a.Key, b.Key) + }) - return writer.Error() - } else if len(responseBytes) > 0 { - // We will report the previous error, but we still want to display the response body - if _, writeErr := cliOut.Write(prettifyJson(responseBytes)); writeErr != nil { - log.Debug(fmt.Sprintf("Write error: %+v", writeErr)) - } + for _, wk := range allWorkers.Workers { + err = writer.Write([]string{ + wk.Key, wk.Action, wk.Description, fmt.Sprint(wk.Enabled), + }) + if err != nil { + return err } + } - return err - }) + writer.Flush() + + return writer.Error() } diff --git a/commands/list_cmd_test.go b/commands/list_cmd_test.go index 1c99187..9512d30 100644 --- a/commands/list_cmd_test.go +++ b/commands/list_cmd_test.go @@ -1,18 +1,18 @@ +//go:build test +// +build test + package commands import ( "bytes" - "context" "encoding/json" - "errors" - "net/http" - "net/http/httptest" "os" - "regexp" - "strings" + "sort" "testing" "time" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,230 +21,136 @@ import ( func TestListCommand(t *testing.T) { tests := []struct { - name string - commandArgs []string - token string - serverBehavior listServerStubBehavior - wantErr error - assertOutput func(t *testing.T, content []byte) + name string + commandArgs []string + token string + serverStub *common.ServerStub + assert common.AssertOutputFunc }{ { name: "list", - serverBehavior: listServerStubBehavior{ - existingWorkers: []*model.WorkerDetails{ + serverStub: common.NewServerStub(t). + WithGetAllEndpoint(). + WithWorkers([]*model.WorkerDetails{ { Key: "wk-0", - Action: model.ActionAfterCreate, + Action: "AFTER_CREATE", Description: "run wk-0", Enabled: true, SourceCode: "export default async () => ({ 'S': 'OK'})", }, - }, - }, - assertOutput: func(t *testing.T, content []byte) { - assert.Equalf(t, "wk-0,AFTER_CREATE,run wk-0,true", strings.TrimSpace(string(content)), "invalid csv received") - }, + }...), + assert: common.AssertOutputText("wk-0,AFTER_CREATE,run wk-0,true", "invalid csv received"), }, { name: "list worker of type", commandArgs: []string{"AFTER_CREATE"}, - serverBehavior: listServerStubBehavior{ - wantAction: "AFTER_CREATE", - existingWorkers: []*model.WorkerDetails{ + serverStub: common.NewServerStub(t). + WithGetAllEndpoint(). + WithWorkers([]*model.WorkerDetails{ { Key: "wk-0", - Action: model.ActionAfterCreate, + Action: "AFTER_CREATE", Description: "run wk-0", Enabled: true, SourceCode: "export default async () => ({ 'S': 'OK'})", }, { Key: "wk-1", - Action: model.ActionBeforeDownload, + Action: "BEFORE_DOWNLOAD", Description: "run wk-1", Enabled: true, SourceCode: "export default async () => ({ 'S': 'OK'})", }, - }, - }, - assertOutput: func(t *testing.T, content []byte) { - assert.Equalf(t, "wk-0,AFTER_CREATE,run wk-0,true", strings.TrimSpace(string(content)), "invalid csv received") - }, + }...), + assert: common.AssertOutputText("wk-0,AFTER_CREATE,run wk-0,true", "invalid csv received"), }, { name: "list for JSON", commandArgs: []string{"--" + model.FlagJsonOutput}, - serverBehavior: listServerStubBehavior{ - existingWorkers: []*model.WorkerDetails{ + serverStub: common.NewServerStub(t). + WithGetAllEndpoint(). + WithWorkers([]*model.WorkerDetails{ { Key: "wk-1", - Action: model.ActionGenericEvent, + Action: "GENERIC_EVENT", Description: "run wk-1", Enabled: false, SourceCode: "export default async () => ({ 'S': 'OK'})", }, + }...), + assert: assertWorkerListOutput([]*model.WorkerDetails{ + { + Key: "wk-1", + Action: "GENERIC_EVENT", + Description: "run wk-1", + Enabled: false, + SourceCode: "export default async () => ({ 'S': 'OK'})", }, - }, - assertOutput: func(t *testing.T, content []byte) { - workers := getAllResponse{} - require.NoError(t, json.Unmarshal(content, &workers)) - assert.Len(t, workers.Workers, 1) - assert.Equalf(t, "wk-1", workers.Workers[0].Key, "Key mismatch") - assert.Equalf(t, model.ActionGenericEvent, workers.Workers[0].Action, "Action mismatch") - assert.Equalf(t, "run wk-1", workers.Workers[0].Description, "Descritption mismatch") - assert.Equalf(t, false, workers.Workers[0].Enabled, "Enabled mismatch") - assert.Equalf(t, "export default async () => ({ 'S': 'OK'})", workers.Workers[0].SourceCode, "Source Code mismatch") - }, + }), }, { name: "projectKey is passed to the request", - commandArgs: []string{"--" + model.FlagProjectKey, "my-project"}, - serverBehavior: listServerStubBehavior{ - wantProjectKey: "my-project", - }, + commandArgs: []string{"--" + model.FlagProjectKey, "my-project", "--" + model.FlagJsonOutput}, + serverStub: common.NewServerStub(t).WithProjectKey("my-project").WithGetAllEndpoint(), + assert: common.AssertOutputJson(map[string]any{"workers": []any{}}), }, { name: "fails if timeout exceeds", - commandArgs: []string{"--" + model.FlagTimeout, "500"}, - serverBehavior: listServerStubBehavior{ - waitFor: 5 * time.Second, - }, - wantErr: errors.New("request timed out after 500ms"), + commandArgs: []string{"--" + model.FlagTimeout, "500", `{}`}, + serverStub: common.NewServerStub(t).WithDelay(5 * time.Second).WithGetAllEndpoint(), + assert: common.AssertOutputError("request timed out after 500ms"), }, { name: "fails if invalid timeout", commandArgs: []string{"--" + model.FlagTimeout, "abc"}, - wantErr: errors.New("invalid timeout provided"), + assert: common.AssertOutputError("invalid timeout provided"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx, cancelCtx := context.WithCancel(context.Background()) - t.Cleanup(cancelCtx) - - runCmd := createCliRunner(t, GetListCommand()) - - err := os.Setenv(model.EnvKeyServerUrl, newListServerStub(t, ctx, &tt.serverBehavior)) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyServerUrl) - }) - - if tt.token == "" && tt.serverBehavior.wantBearerToken == "" { - tt.token = t.Name() - tt.serverBehavior.wantBearerToken = t.Name() + if tt.serverStub == nil { + tt.serverStub = common.NewServerStub(t) } - err = os.Setenv(model.EnvKeyAccessToken, tt.token) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyAccessToken) - }) + common.NewMockWorkerServer(t, tt.serverStub.WithT(t).WithDefaultActionsMetadataEndpoint()) + + runCmd := common.CreateCliRunner(t, GetListCommand()) var output bytes.Buffer - cliOut = &output + common.SetCliOut(&output) t.Cleanup(func() { - cliOut = os.Stdout + common.SetCliOut(os.Stdout) }) cmd := append([]string{"worker", "list"}, tt.commandArgs...) - err = runCmd(cmd...) - - cancelCtx() + err := runCmd(cmd...) - if tt.wantErr == nil { - assert.NoError(t, err) - } else { - assert.EqualError(t, tt.wantErr, err.Error()) - } - - if tt.assertOutput != nil { - tt.assertOutput(t, output.Bytes()) - } + tt.assert(t, output.Bytes(), err) }) } } -var listUrlPattern = regexp.MustCompile(`^/worker/api/v1/workers(/[\S/]+)?$`) - -type listServerStubBehavior struct { - waitFor time.Duration - responseStatus int - wantBearerToken string - wantAction string - wantProjectKey string - existingWorkers []*model.WorkerDetails -} - -type listServerStub struct { - t *testing.T - ctx context.Context - behavior *listServerStubBehavior -} +func assertWorkerListOutput(want []*model.WorkerDetails) common.AssertOutputFunc { + return func(t *testing.T, output []byte, err error) { + require.NoError(t, err) -func newListServerStub(t *testing.T, ctx context.Context, behavior *listServerStubBehavior) string { - stub := listServerStub{t: t, behavior: behavior, ctx: ctx} - server := httptest.NewUnstartedServer(&stub) - t.Cleanup(server.Close) - server.Start() - return "http:" + "//" + server.Listener.Addr().String() -} + var got getAllResponse -func (s *listServerStub) ServeHTTP(res http.ResponseWriter, req *http.Request) { - urlMatch := listUrlPattern.FindAllStringSubmatch(req.URL.Path, -1) - if len(urlMatch) == 0 { - res.WriteHeader(http.StatusNotFound) - return - } + err = json.Unmarshal(output, &got) + require.NoError(t, err) - if s.behavior.waitFor > 0 { - select { - case <-s.ctx.Done(): - return - case <-time.After(s.behavior.waitFor): - } - } - - // Validate method - if http.MethodGet != req.Method { - res.WriteHeader(http.StatusMethodNotAllowed) - return - } + sortWorkers(got.Workers) + sortWorkers(want) - // Validate token - if req.Header.Get("authorization") != "Bearer "+s.behavior.wantBearerToken { - res.WriteHeader(http.StatusForbidden) - return - } - - // Validate request params - if s.behavior.wantProjectKey != "" { - if req.URL.Query().Get("projectKey") != s.behavior.wantProjectKey { - res.WriteHeader(http.StatusBadRequest) - return - } - } - - if s.behavior.responseStatus > 0 { - res.WriteHeader(s.behavior.responseStatus) - return - } - - var workers []*model.WorkerDetails - - if s.behavior.wantAction == "" { - workers = s.behavior.existingWorkers - } else { - for _, wk := range s.behavior.existingWorkers { - if wk.Action == s.behavior.wantAction { - workers = append(workers, wk) - } - } + assert.Equal(t, want, got.Workers) } +} - res.WriteHeader(http.StatusOK) - _, err := res.Write([]byte(mustJsonMarshal(s.t, getAllResponse{Workers: workers}))) - require.NoError(s.t, err) +func sortWorkers(workers []*model.WorkerDetails) { + sort.Slice(workers, func(i, j int) bool { + return workers[i].Key < workers[j].Key + }) } diff --git a/commands/list_event_cmd.go b/commands/list_event_cmd.go index 5419966..70bffb4 100644 --- a/commands/list_event_cmd.go +++ b/commands/list_event_cmd.go @@ -1,7 +1,9 @@ package commands import ( - "net/http" + "strings" + + "github.com/jfrog/jfrog-cli-platform-services/commands/common" plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" @@ -27,12 +29,17 @@ func GetListEventsCommand() components.Command { projectKey := c.GetStringFlagValue(model.FlagProjectKey) - var queryParams map[string]string - if projectKey != "" { - queryParams = map[string]string{"projectKey": projectKey} + actionsMeta, err := common.FetchActions(c, server.Url, server.AccessToken, projectKey) + if err != nil { + return err + } + + var actions []string + for _, md := range actionsMeta { + actions = append(actions, md.Action.Name) } - return callWorkerApiWithOutput(c, server.GetUrl(), server.GetAccessToken(), http.MethodGet, nil, http.StatusOK, queryParams, "actions") + return common.Print("%s", strings.Join(actions, ", ")) }, } } diff --git a/commands/list_event_cmd_test.go b/commands/list_event_cmd_test.go index d5aed30..d324eb3 100644 --- a/commands/list_event_cmd_test.go +++ b/commands/list_event_cmd_test.go @@ -1,180 +1,66 @@ +//go:build test +// +build test + package commands import ( "bytes" - "context" - "encoding/json" - "errors" - "net/http" - "net/http/httptest" "os" - "regexp" + "strings" "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - + "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/require" ) func TestListEventCommand(t *testing.T) { tests := []struct { - name string - commandArgs []string - token string - serverBehavior listEventServerStubBehavior - wantErr error - assertOutput func(t *testing.T, content []byte) + name string + commandArgs []string + serverStub *common.ServerStub + assert common.AssertOutputFunc }{ { - name: "list", - serverBehavior: listEventServerStubBehavior{ - events: []string{"A", "B", "C"}, - }, - assertOutput: func(t *testing.T, content []byte) { - var events []string - require.NoError(t, json.Unmarshal(content, &events)) - assert.ElementsMatch(t, []string{"A", "B", "C"}, events) - }, + name: "list", + serverStub: common.NewServerStub(t).WithDefaultActionsMetadataEndpoint(), + assert: common.AssertOutputText(strings.Join(common.LoadSampleActionEvents(t), ", "), "invalid data "), }, { name: "fails if timeout exceeds", + serverStub: common.NewServerStub(t).WithDelay(2 * time.Second).WithDefaultActionsMetadataEndpoint(), commandArgs: []string{"--" + model.FlagTimeout, "500"}, - serverBehavior: listEventServerStubBehavior{ - waitFor: 5 * time.Second, - }, - wantErr: errors.New("request timed out after 500ms"), + assert: common.AssertOutputError("request timed out after 500ms"), }, { name: "should propagate projectKey", + serverStub: common.NewServerStub(t).WithProjectKey("proj-1").WithDefaultActionsMetadataEndpoint(), commandArgs: []string{"wk-1", "--" + model.FlagProjectKey, "proj-1"}, - serverBehavior: listEventServerStubBehavior{ - wantProjectKey: "proj-1", - }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx, cancelCtx := context.WithCancel(context.Background()) - t.Cleanup(cancelCtx) - - runCmd := createCliRunner(t, GetListEventsCommand()) + runCmd := common.CreateCliRunner(t, GetListEventsCommand()) - err := os.Setenv(model.EnvKeyServerUrl, newListEventServerStub(t, ctx, &tt.serverBehavior)) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyServerUrl) - }) - - if tt.token == "" && tt.serverBehavior.wantBearerToken == "" { - tt.token = t.Name() - tt.serverBehavior.wantBearerToken = t.Name() - } - - err = os.Setenv(model.EnvKeyAccessToken, tt.token) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyAccessToken) - }) + common.NewMockWorkerServer(t, tt.serverStub.WithT(t)) var output bytes.Buffer - cliOut = &output + common.SetCliOut(&output) t.Cleanup(func() { - cliOut = os.Stdout + common.SetCliOut(os.Stdout) }) cmd := append([]string{"worker", "list-event"}, tt.commandArgs...) - err = runCmd(cmd...) - - cancelCtx() + err := runCmd(cmd...) - if tt.wantErr == nil { - assert.NoError(t, err) + if tt.assert == nil { + require.NoError(t, err) } else { - assert.EqualError(t, tt.wantErr, err.Error()) - } - - if tt.assertOutput != nil { - tt.assertOutput(t, output.Bytes()) + tt.assert(t, output.Bytes(), err) } }) } } - -var listEventUrlPattern = regexp.MustCompile(`^/worker/api/v1/actions$`) - -type listEventServerStubBehavior struct { - waitFor time.Duration - responseStatus int - wantBearerToken string - wantProjectKey string - events []string -} - -type listEventServerStub struct { - t *testing.T - ctx context.Context - behavior *listEventServerStubBehavior -} - -func newListEventServerStub(t *testing.T, ctx context.Context, behavior *listEventServerStubBehavior) string { - stub := listEventServerStub{t: t, behavior: behavior, ctx: ctx} - server := httptest.NewUnstartedServer(&stub) - t.Cleanup(server.Close) - server.Start() - return "http:" + "//" + server.Listener.Addr().String() -} - -func (s *listEventServerStub) ServeHTTP(res http.ResponseWriter, req *http.Request) { - urlMatch := listEventUrlPattern.FindAllStringSubmatch(req.URL.Path, -1) - if len(urlMatch) == 0 { - res.WriteHeader(http.StatusNotFound) - return - } - - if s.behavior.waitFor > 0 { - select { - case <-s.ctx.Done(): - return - case <-time.After(s.behavior.waitFor): - } - } - - // Validate method - if http.MethodGet != req.Method { - res.WriteHeader(http.StatusMethodNotAllowed) - return - } - - // Validate token - if req.Header.Get("authorization") != "Bearer "+s.behavior.wantBearerToken { - res.WriteHeader(http.StatusForbidden) - return - } - - // Validate the projectKey - if s.behavior.wantProjectKey != "" { - gotProjectKey := req.URL.Query().Get("projectKey") - if gotProjectKey != s.behavior.wantProjectKey { - s.t.Logf("Invalid projectKey want='%s', got='%s'", s.behavior.wantProjectKey, gotProjectKey) - res.WriteHeader(http.StatusBadRequest) - return - } - } - - if s.behavior.responseStatus > 0 { - res.WriteHeader(s.behavior.responseStatus) - return - } - - res.WriteHeader(http.StatusOK) - - resBytes, err := json.Marshal(s.behavior.events) - require.NoError(s.t, err) - - _, err = res.Write(resBytes) - require.NoError(s.t, err) -} diff --git a/commands/remove_cmd.go b/commands/remove_cmd.go index 28ef6e4..9595943 100644 --- a/commands/remove_cmd.go +++ b/commands/remove_cmd.go @@ -4,6 +4,8 @@ import ( "fmt" "net/http" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" "github.com/jfrog/jfrog-client-go/utils/log" @@ -28,7 +30,7 @@ func GetRemoveCommand() components.Command { } func runRemoveCommand(c *components.Context) error { - workerKey, _, err := extractProjectAndKeyFromCommandContext(c, c.Arguments, 0, false) + workerKey, _, err := common.ExtractProjectAndKeyFromCommandContext(c, c.Arguments, 0, false) if err != nil { return err } @@ -40,7 +42,13 @@ func runRemoveCommand(c *components.Context) error { log.Info(fmt.Sprintf("Removing worker '%s' ...", workerKey)) - err = callWorkerApiSilent(c, server.GetUrl(), server.GetAccessToken(), http.MethodDelete, nil, http.StatusNoContent, nil, "workers", workerKey) + err = common.CallWorkerApi(c, common.ApiCallParams{ + Method: http.MethodDelete, + ServerUrl: server.GetUrl(), + ServerToken: server.GetAccessToken(), + OkStatuses: []int{http.StatusNoContent}, + Path: []string{"workers", workerKey}, + }) if err == nil { log.Info(fmt.Sprintf("Worker '%s' removed", workerKey)) } diff --git a/commands/remove_cmd_test.go b/commands/remove_cmd_test.go index dcde6a1..77251b5 100644 --- a/commands/remove_cmd_test.go +++ b/commands/remove_cmd_test.go @@ -1,15 +1,16 @@ +//go:build test +// +build test + package commands import ( - "context" - "errors" - "net/http" - "net/http/httptest" + "bytes" "os" - "regexp" "testing" "time" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,52 +21,52 @@ func TestRemoveCommand(t *testing.T) { tests := []struct { name string commandArgs []string - token string workerAction string workerName string patchManifest func(mf *model.Manifest) - serverBehavior removeServerStubBehavior - wantErr error + serverBehavior *common.ServerStub + assert common.AssertOutputFunc }{ { name: "undeploy from manifest", workerAction: "BEFORE_UPLOAD", workerName: "wk-0", - serverBehavior: removeServerStubBehavior{ - wantWorkerKey: "wk-0", - }, + serverBehavior: common.NewServerStub(t). + WithWorkers(&model.WorkerDetails{Key: "wk-0"}). + WithDeleteEndpoint(), }, { name: "undeploy from key", workerName: "wk-1", commandArgs: []string{"wk-1"}, - serverBehavior: removeServerStubBehavior{ - wantWorkerKey: "wk-1", - }, + serverBehavior: common.NewServerStub(t). + WithWorkers(&model.WorkerDetails{Key: "wk-1"}). + WithDeleteEndpoint(), }, { name: "fails if timeout exceeds", commandArgs: []string{"--" + model.FlagTimeout, "500"}, - serverBehavior: removeServerStubBehavior{ - waitFor: 5 * time.Second, - }, - wantErr: errors.New("request timed out after 500ms"), + serverBehavior: common.NewServerStub(t). + WithDelay(2 * time.Second). + WithDeleteEndpoint(), + assert: common.AssertOutputError("request timed out after 500ms"), }, { name: "fails if invalid timeout", commandArgs: []string{"--" + model.FlagTimeout, "abc"}, - wantErr: errors.New("invalid timeout provided"), + assert: common.AssertOutputError("invalid timeout provided"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx, cancelCtx := context.WithCancel(context.Background()) - t.Cleanup(cancelCtx) + if tt.serverBehavior != nil { + common.NewMockWorkerServer(t, tt.serverBehavior.WithT(t).WithDefaultActionsMetadataEndpoint()) + } - runCmd := createCliRunner(t, GetInitCommand(), GetRemoveCommand()) + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetRemoveCommand()) - _, workerName := prepareWorkerDirForTest(t) + _, workerName := common.PrepareWorkerDirForTest(t) if tt.workerName != "" { workerName = tt.workerName } @@ -79,108 +80,24 @@ func TestRemoveCommand(t *testing.T) { require.NoError(t, err) if tt.patchManifest != nil { - patchManifest(t, tt.patchManifest) + common.PatchManifest(t, tt.patchManifest) } - err = os.Setenv(model.EnvKeyServerUrl, newRemoveServerStub(t, ctx, &tt.serverBehavior)) - require.NoError(t, err) + var output bytes.Buffer + common.SetCliOut(&output) t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyServerUrl) - }) - - if tt.token == "" && tt.serverBehavior.wantBearerToken == "" { - tt.token = t.Name() - tt.serverBehavior.wantBearerToken = t.Name() - } - - err = os.Setenv(model.EnvKeyAccessToken, tt.token) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeyAccessToken) + common.SetCliOut(os.Stdout) }) cmd := append([]string{"worker", "undeploy"}, tt.commandArgs...) err = runCmd(cmd...) - cancelCtx() - - if tt.wantErr == nil { + if tt.assert == nil { assert.NoError(t, err) } else { - assert.EqualError(t, tt.wantErr, err.Error()) + tt.assert(t, output.Bytes(), err) } }) } } - -var removeUrlPattern = regexp.MustCompile(`^/worker/api/v1/workers/([\S/]+)$`) - -type removeServerStubBehavior struct { - waitFor time.Duration - responseStatus int - wantBearerToken string - wantWorkerKey string - requestParams map[string]string -} - -type removeServerStub struct { - t *testing.T - ctx context.Context - behavior *removeServerStubBehavior -} - -func newRemoveServerStub(t *testing.T, ctx context.Context, behavior *removeServerStubBehavior) string { - stub := removeServerStub{t: t, behavior: behavior, ctx: ctx} - server := httptest.NewUnstartedServer(&stub) - t.Cleanup(server.Close) - server.Start() - return "http:" + "//" + server.Listener.Addr().String() -} - -func (s *removeServerStub) ServeHTTP(res http.ResponseWriter, req *http.Request) { - urlMatch := removeUrlPattern.FindAllStringSubmatch(req.URL.Path, -1) - if len(urlMatch) == 0 && len(urlMatch[0]) < 2 { - res.WriteHeader(http.StatusNotFound) - return - } - - if s.behavior.wantWorkerKey != "" && s.behavior.wantWorkerKey != urlMatch[0][1] { - res.WriteHeader(http.StatusBadRequest) - return - } - - if s.behavior.waitFor > 0 { - select { - case <-s.ctx.Done(): - return - case <-time.After(s.behavior.waitFor): - } - } - - if req.Method != http.MethodDelete { - res.WriteHeader(http.StatusMethodNotAllowed) - return - } - - // Validate token - if req.Header.Get("authorization") != "Bearer "+s.behavior.wantBearerToken { - res.WriteHeader(http.StatusForbidden) - return - } - - // Validate the request params - for k, v := range s.behavior.requestParams { - if req.URL.Query().Get(k) != v { - res.WriteHeader(http.StatusBadRequest) - return - } - } - - if s.behavior.responseStatus > 0 { - res.WriteHeader(s.behavior.responseStatus) - return - } - - res.WriteHeader(http.StatusNoContent) -} diff --git a/commands/templates/AFTER_BUILD_INFO_SAVE.spec.ts_template b/commands/templates/AFTER_BUILD_INFO_SAVE.spec.ts_template deleted file mode 100644 index d69488b..0000000 --- a/commands/templates/AFTER_BUILD_INFO_SAVE.spec.ts_template +++ /dev/null @@ -1,26 +0,0 @@ -import { PlatformContext, AfterBuildInfoSaveRequest, PlatformClients, PlatformHttpClient, Status } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock(); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'proceed', - executionStatus: Status.STATUS_SUCCESS - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/AFTER_BUILD_INFO_SAVE.ts_template b/commands/templates/AFTER_BUILD_INFO_SAVE.ts_template deleted file mode 100644 index 87670ba..0000000 --- a/commands/templates/AFTER_BUILD_INFO_SAVE.ts_template +++ /dev/null @@ -1,24 +0,0 @@ -import { PlatformContext, AfterBuildInfoSaveRequest, AfterBuildInfoSaveResponse, Status } from 'jfrog-workers'; - -export default async (context: PlatformContext, data: AfterBuildInfoSaveRequest): Promise => { - try { - // The HTTP client facilitates calls to the JFrog Platform REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get("/artifactory/api/v1/system/readiness"); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - console.log("Artifactory ping success"); - } else { - console.warn(`Request was successful and returned status code : ${res.status}`); - } - } catch (error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - console.error(`Request failed with status code ${error.status || ""} caused by : ${error.message}`); - } - - return { - message: "proceed", - executionStatus: Status.STATUS_SUCCESS, - }; -}; diff --git a/commands/templates/AFTER_CREATE.spec.ts_template b/commands/templates/AFTER_CREATE.spec.ts_template deleted file mode 100644 index f48a34a..0000000 --- a/commands/templates/AFTER_CREATE.spec.ts_template +++ /dev/null @@ -1,25 +0,0 @@ -import { PlatformContext, AfterCreateRequest, PlatformClients, PlatformHttpClient } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock(); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'proceed' - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/AFTER_CREATE.ts_template b/commands/templates/AFTER_CREATE.ts_template deleted file mode 100644 index 0890d78..0000000 --- a/commands/templates/AFTER_CREATE.ts_template +++ /dev/null @@ -1,24 +0,0 @@ -import { PlatformContext, AfterCreateRequest, AfterCreateResponse } from 'jfrog-workers'; - -export default async (context: PlatformContext, data: AfterCreateRequest): Promise => { - - try { - // The HTTP client facilitates calls to the JFrog Platform REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness'); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - console.log("Artifactory ping success"); - } else { - console.warn(`Request was successful and returned status code : ${ res.status }`); - } - } catch(error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - console.error(`Request failed with status code ${ error.status || '' } caused by : ${ error.message }`) - } - - return { - message: 'proceed', - } -} diff --git a/commands/templates/AFTER_DOWNLOAD.spec.ts_template b/commands/templates/AFTER_DOWNLOAD.spec.ts_template deleted file mode 100644 index 07da10c..0000000 --- a/commands/templates/AFTER_DOWNLOAD.spec.ts_template +++ /dev/null @@ -1,25 +0,0 @@ -import { PlatformContext, AfterDownloadRequest, PlatformClients, PlatformHttpClient } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock(); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'proceed' - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/AFTER_DOWNLOAD.ts_template b/commands/templates/AFTER_DOWNLOAD.ts_template deleted file mode 100644 index dea36d4..0000000 --- a/commands/templates/AFTER_DOWNLOAD.ts_template +++ /dev/null @@ -1,24 +0,0 @@ -import { PlatformContext, AfterDownloadRequest, AfterDownloadResponse } from 'jfrog-workers'; - -export default async (context: PlatformContext, data: AfterDownloadRequest): Promise => { - - try { - // The in-browser HTTP client facilitates making calls to the JFrog REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness'); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - console.log("Artifactory ping success"); - } else { - console.warn(`Request was successful and returned status code : ${ res.status }`); - } - } catch(error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - console.error(`Request failed with status code ${ error.status || '' } caused by : ${ error.message }`) - } - - return { - message: 'proceed', - } -} diff --git a/commands/templates/AFTER_MOVE.spec.ts_template b/commands/templates/AFTER_MOVE.spec.ts_template deleted file mode 100644 index b36b0e1..0000000 --- a/commands/templates/AFTER_MOVE.spec.ts_template +++ /dev/null @@ -1,25 +0,0 @@ -import { PlatformContext, AfterMoveRequest, PlatformClients, PlatformHttpClient } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock(); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'proceed' - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/AFTER_MOVE.ts_template b/commands/templates/AFTER_MOVE.ts_template deleted file mode 100644 index b20458b..0000000 --- a/commands/templates/AFTER_MOVE.ts_template +++ /dev/null @@ -1,23 +0,0 @@ -import { PlatformContext, AfterMoveRequest, AfterMoveResponse } from 'jfrog-workers'; - -export default async (context: PlatformContext, data: AfterMoveRequest): Promise => { - try { - // The in-browser HTTP client facilitates making calls to the JFrog REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get("/artifactory/api/v1/system/readiness"); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - console.log("Artifactory ping success"); - } else { - console.warn(`Request was successful and returned status code : ${res.status}`); - } - } catch (error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - console.error(`Request failed with status code ${error.status || ""} caused by : ${error.message}`); - } - - return { - message: "proceed", - }; -}; diff --git a/commands/templates/BEFORE_CREATE_TOKEN.spec.ts_template b/commands/templates/BEFORE_CREATE_TOKEN.spec.ts_template deleted file mode 100644 index ce24159..0000000 --- a/commands/templates/BEFORE_CREATE_TOKEN.spec.ts_template +++ /dev/null @@ -1,26 +0,0 @@ -import { PlatformContext, BeforeCreateTokenRequest, PlatformClients, PlatformHttpClient, CreateTokenStatus } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock(); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'Overwritten by worker-service if an error occurs.', - status: CreateTokenStatus.CREATE_TOKEN_PROCEED - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/BEFORE_CREATE_TOKEN.ts_template b/commands/templates/BEFORE_CREATE_TOKEN.ts_template deleted file mode 100644 index a9f6076..0000000 --- a/commands/templates/BEFORE_CREATE_TOKEN.ts_template +++ /dev/null @@ -1,30 +0,0 @@ -import { PlatformContext, BeforeCreateTokenRequest, BeforeCreateTokenResponse, CreateTokenStatus } from 'jfrog-workers'; - -export default async (context: PlatformContext, data: BeforeCreateTokenRequest): Promise => { - - let status: CreateTokenStatus = CreateTokenStatus.CREATE_TOKEN_UNSPECIFIED; - - try { - // The in-browser HTTP client facilitates making calls to the JFrog REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness'); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - status = CreateTokenStatus.CREATE_TOKEN_PROCEED; - console.log("Artifactory ping success"); - } else { - status = CreateTokenStatus.CREATE_TOKEN_WARN; - console.warn(`Request is successful but returned status other than 200. Status code : ${ res.status }`); - } - } catch(error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - status = CreateTokenStatus.CREATE_TOKEN_STOP; - console.error(`Request failed with status code ${ error.status || '' } caused by : ${ error.message }`) - } - - return { - status, - message: 'Overwritten by worker-service if an error occurs.', - } -}; diff --git a/commands/templates/BEFORE_DOWNLOAD.spec.ts_template b/commands/templates/BEFORE_DOWNLOAD.spec.ts_template deleted file mode 100644 index 3b0ffe5..0000000 --- a/commands/templates/BEFORE_DOWNLOAD.spec.ts_template +++ /dev/null @@ -1,26 +0,0 @@ -import { PlatformContext, BeforeDownloadRequest, PlatformClients, PlatformHttpClient, DownloadStatus } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock(); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'Overwritten by worker-service if an error occurs.', - status: DownloadStatus.DOWNLOAD_PROCEED - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/BEFORE_DOWNLOAD.ts_template b/commands/templates/BEFORE_DOWNLOAD.ts_template deleted file mode 100644 index 15d237a..0000000 --- a/commands/templates/BEFORE_DOWNLOAD.ts_template +++ /dev/null @@ -1,30 +0,0 @@ -import { PlatformContext, BeforeDownloadRequest, BeforeDownloadResponse, DownloadStatus } from 'jfrog-workers'; - -export default async (context: PlatformContext, data: BeforeDownloadRequest): Promise => { - - let status: DownloadStatus = DownloadStatus.DOWNLOAD_UNSPECIFIED; - - try { - // The in-browser HTTP client facilitates making calls to the JFrog REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness'); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - status = DownloadStatus.DOWNLOAD_PROCEED; - console.log("Artifactory ping success"); - } else { - status = DownloadStatus.DOWNLOAD_WARN; - console.warn(`Request is successful but returned status other than 200. Status code : ${ res.status }`); - } - } catch(error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - status = DownloadStatus.DOWNLOAD_STOP; - console.error(`Request failed with status code ${ error.status || '' } caused by : ${ error.message }`) - } - - return { - status, - message: 'Overwritten by worker-service if an error occurs.', - } -} diff --git a/commands/templates/BEFORE_PROPERTY_CREATE.spec.ts_template b/commands/templates/BEFORE_PROPERTY_CREATE.spec.ts_template deleted file mode 100644 index 768cfe7..0000000 --- a/commands/templates/BEFORE_PROPERTY_CREATE.spec.ts_template +++ /dev/null @@ -1,29 +0,0 @@ -import { PlatformContext, BeforePropertyCreateRequest, PlatformClients, PlatformHttpClient, UploadStatus } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock({ - metadata: { repoPath: { key: 'my-repo', path: 'artifact.txt' } } - }); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'Overwritten by worker-service if an error occurs.', - status: UploadStatus.UPLOAD_PROCEED, - modifiedRepoPath: { key: 'my-repo', path: 'artifact.txt' } - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/BEFORE_PROPERTY_CREATE.ts_template b/commands/templates/BEFORE_PROPERTY_CREATE.ts_template deleted file mode 100644 index 3b71d2e..0000000 --- a/commands/templates/BEFORE_PROPERTY_CREATE.ts_template +++ /dev/null @@ -1,27 +0,0 @@ -export default async (context: PlatformContext, data: BeforePropertyCreateRequest): Promise => { - let status: BeforePropertyCreateStatus = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_UNSPECIFIED; - - try { - // The in-browser HTTP client facilitates making calls to the JFrog REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness'); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - status = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_PROCEED; - console.log("Artifactory ping success"); - } else { - status = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_WARN; - console.warn(`Request is successful but returned status other than 200. Status code : ${ res.status }`); - } - } catch(error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - status = BeforePropertyCreateStatus.BEFORE_PROPERTY_CREATE_STOP; - console.error(`Request failed with status code ${ error.status || '' } caused by : ${ error.message }`) - } - - return { - message: "proceed", - status - }; -}; diff --git a/commands/templates/BEFORE_UPLOAD.spec.ts_template b/commands/templates/BEFORE_UPLOAD.spec.ts_template deleted file mode 100644 index ba62040..0000000 --- a/commands/templates/BEFORE_UPLOAD.spec.ts_template +++ /dev/null @@ -1,29 +0,0 @@ -import { PlatformContext, BeforeUploadRequest, PlatformClients, PlatformHttpClient, UploadStatus } from 'jfrog-workers'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import runWorker from './worker'; - -describe("{{.WorkerName}} tests", () => { - let context: DeepMocked; - let request: DeepMocked; - - beforeEach(() => { - context = createMock({ - clients: createMock({ - platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ status: 200 }) - }) - }) - }); - request = createMock({ - metadata: { repoPath: { key: 'my-repo', path: 'artifact.txt' } } - }); - }) - - it('should run', async () => { - await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - message: 'Overwritten by worker-service if an error occurs.', - status: UploadStatus.UPLOAD_PROCEED, - modifiedRepoPath: { key: 'my-repo', path: 'artifact.txt' } - })) - }) -}); \ No newline at end of file diff --git a/commands/templates/BEFORE_UPLOAD.ts_template b/commands/templates/BEFORE_UPLOAD.ts_template deleted file mode 100644 index 9fe9e3c..0000000 --- a/commands/templates/BEFORE_UPLOAD.ts_template +++ /dev/null @@ -1,30 +0,0 @@ -import { PlatformContext, BeforeUploadRequest, BeforeUploadResponse, UploadStatus } from 'jfrog-workers'; - -export default async (context: PlatformContext, data: BeforeUploadRequest): Promise => { - let status: UploadStatus = UploadStatus.UPLOAD_UNSPECIFIED; - - try { - // The in-browser HTTP client facilitates making calls to the JFrog REST APIs - //To call an external endpoint, use 'await context.clients.axios.get("https://foo.com")' - const res = await context.clients.platformHttp.get('/artifactory/api/v1/system/readiness'); - - // You should reach this part if the HTTP request status is successful (HTTP Status 399 or lower) - if (res.status === 200) { - status = UploadStatus.UPLOAD_PROCEED; - console.log("Artifactory ping success"); - } else { - status = UploadStatus.UPLOAD_WARN; - console.warn(`Request was successful but returned status other than 200. Status code : ${ res.status }`); - } - } catch(error) { - // The platformHttp client throws PlatformHttpClientError if the HTTP request status is 400 or higher - status = UploadStatus.UPLOAD_STOP; - console.error(`Request failed with status code ${ error.status || '' } caused by : ${ error.message }`) - } - - return { - status, - message: 'Overwritten by worker-service if an error occurs.', - modifiedRepoPath: data.metadata.repoPath - } -} diff --git a/commands/templates/GENERIC_EVENT.ts_template b/commands/templates/GENERIC_EVENT.ts_template deleted file mode 100644 index 7df0467..0000000 --- a/commands/templates/GENERIC_EVENT.ts_template +++ /dev/null @@ -1,50 +0,0 @@ -import { PlatformContext } from 'jfrog-workers'; - -type CustomPayload = void; -type CustomResponse = { - error: string | undefined, // Valued with the cause in case of error - repositories: Record, // A list that contains the number of repositories per repository type -}; -type RepoData = { - "key": string, - "type": string, - "description": string, - "url": string, - "packageType": string -}; - -// This worker returns the number of repositories for each repository type. -export default async (context: PlatformContext, data: CustomPayload): Promise => { - - const response = { - error: undefined, - repositories: {}, - }; - - try { - // Ref: https://jfrog.com/help/r/jfrog-rest-apis/get-repositories - const res = await context.clients.platformHttp.get('/artifactory/api/repositories'); - if (res.status === 200) { - const repositories: RepoData[] = res.data; - - // The number of repositories mapped by repository type - const repoCountRecord: Record = {}; - - repositories.forEach(repository => { - let count = repoCountRecord[repository.type] || 0; - repoCountRecord[repository.type] = ++count; - }); - - response.repositories = repoCountRecord; - console.log("Repository count success"); - } else { - response.error = `Request is successful but returned an unexpected status : ${ res.status }`; - console.warn(response.error); - } - } catch(error) { - response.error = `Request failed with status code ${ error.status || '' } caused by : ${ error.message }`; - console.error(response.error); - } - - return response; -} diff --git a/commands/templates/manifest.json_template b/commands/templates/manifest.json_template index fcda59f..443b601 100644 --- a/commands/templates/manifest.json_template +++ b/commands/templates/manifest.json_template @@ -1,15 +1,18 @@ { "name": "{{.WorkerName}}", "description": "Run a script on {{.Action}}", -{{- if .HasCriteria }} +{{- if .HasRepoFilterCriteria }} "filterCriteria": { "artifactFilterCriteria": { "repoKeys": ["example-repo-local"] } }, -{{ end}} +{{- end }} "secrets": {}, "sourceCodePath": "./worker.ts", "action": "{{.Action}}", - "enabled": false + "enabled": false, + "debug": false, + "projectKey": "{{.ProjectKey}}", + "application": "{{.Application}}" } \ No newline at end of file diff --git a/commands/templates/GENERIC_EVENT.spec.ts_template b/commands/templates/worker.spec.ts_template similarity index 56% rename from commands/templates/GENERIC_EVENT.spec.ts_template rename to commands/templates/worker.spec.ts_template index 6152a5f..31adab2 100644 --- a/commands/templates/GENERIC_EVENT.spec.ts_template +++ b/commands/templates/worker.spec.ts_template @@ -1,34 +1,36 @@ import { PlatformContext, PlatformClients, PlatformHttpClient } from 'jfrog-workers'; +{{- if .HasRequestType}} +import { {{ .ExecutionRequestType }} } from './types'; +{{- end }} import { createMock, DeepMocked } from '@golevelup/ts-jest'; import runWorker from './worker'; describe("{{.WorkerName}} tests", () => { let context: DeepMocked; - const request: void = undefined; + {{- if .HasRequestType }} + let request: DeepMocked<{{ .ExecutionRequestType }}>; + {{- else }} + let request: any; + {{- end }} beforeEach(() => { context = createMock({ clients: createMock({ platformHttp: createMock({ - get: jest.fn().mockResolvedValue({ - status: 200, - data: [ - { type: 'a' }, - { type: 'a' }, - { type: 'b' }, - { type: 'c' } - ], - }) + get: jest.fn().mockResolvedValue({ status: 200 }) }) }) }); + {{- if .HasRequestType }} + request = createMock<{{ .ExecutionRequestType }}>(); + {{- else }} + request = {}; + {{- end }} }) it('should run', async () => { await expect(runWorker(context, request)).resolves.toEqual(expect.objectContaining({ - repositories: expect.objectContaining({ - a: 2, b: 1, c: 1 - }) + message: expect.anything(), })) }) }); \ No newline at end of file diff --git a/commands/templates/worker.ts_template b/commands/templates/worker.ts_template new file mode 100644 index 0000000..e69e89f --- /dev/null +++ b/commands/templates/worker.ts_template @@ -0,0 +1,6 @@ +import { PlatformContext } from 'jfrog-workers'; +{{- with .UsedTypes}} +import { {{ . }} } from './types'; +{{- end }} + +{{ .SourceCode -}} diff --git a/go.mod b/go.mod index c9ae675..f90e9e5 100644 --- a/go.mod +++ b/go.mod @@ -2,24 +2,26 @@ module github.com/jfrog/jfrog-cli-platform-services require ( github.com/google/uuid v1.6.0 - github.com/jfrog/jfrog-cli-core/v2 v2.51.0 - github.com/jfrog/jfrog-client-go v1.40.1 - github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.22.0 + github.com/jfrog/go-mockhttp v0.3.1 + github.com/jfrog/jfrog-cli-core/v2 v2.56.8 + github.com/jfrog/jfrog-client-go v1.48.0 + github.com/stretchr/testify v1.10.0 + go.uber.org/mock v0.5.0 + golang.org/x/crypto v0.29.0 ) require ( dario.cat/mergo v1.0.0 // indirect - github.com/BurntSushi/toml v1.3.2 // indirect - github.com/CycloneDX/cyclonedx-go v0.8.0 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/CycloneDX/cyclonedx-go v0.9.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/ProtonMail/go-crypto v1.1.2 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/c-bata/go-prompt v0.2.6 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect @@ -29,30 +31,30 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jedib0t/go-pretty/v6 v6.5.7 // indirect - github.com/jfrog/archiver/v3 v3.6.0 // indirect - github.com/jfrog/build-info-go v1.9.26 // indirect - github.com/jfrog/gofrog v1.7.1 // indirect + github.com/jedib0t/go-pretty/v6 v6.6.1 // indirect + github.com/jfrog/archiver/v3 v3.6.1 // indirect + github.com/jfrog/build-info-go v1.10.5 // indirect + github.com/jfrog/gofrog v1.7.6 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-tty v0.0.5 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/nwaples/rardecode v1.1.3 // indirect - github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect @@ -68,25 +70,25 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.18.2 // indirect + github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect - github.com/urfave/cli v1.22.14 // indirect + github.com/urfave/cli v1.22.16 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.27.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -go 1.22.2 +go 1.23.3 diff --git a/go.sum b/go.sum index b307874..89ccc5d 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M= -github.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/CycloneDX/cyclonedx-go v0.9.0 h1:inaif7qD8bivyxp7XLgxUYtOXWtDez7+j72qKTMQTb8= +github.com/CycloneDX/cyclonedx-go v0.9.0/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0Go3KS0= +github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -23,7 +23,6 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oM github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -35,12 +34,10 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -70,8 +67,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -87,23 +84,25 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.5.7 h1:28Z6UxnNyKCVISGdItMiCCc7A0mbDF+SYvgo3U8ZKuQ= -github.com/jedib0t/go-pretty/v6 v6.5.7/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= -github.com/jfrog/archiver/v3 v3.6.0 h1:OVZ50vudkIQmKMgA8mmFF9S0gA47lcag22N13iV3F1w= -github.com/jfrog/archiver/v3 v3.6.0/go.mod h1:fCAof46C3rAXgZurS8kNRNdSVMKBbZs+bNNhPYxLldI= -github.com/jfrog/build-info-go v1.9.26 h1:1Ddc6+Ecvhc+UMnKhRVG1jGM6fYNwA49207azTBGBc8= -github.com/jfrog/build-info-go v1.9.26/go.mod h1:8T7/ajM9aGshvgpwCtXwIFpyF/R6CEn4W+/FLryNXWw= -github.com/jfrog/gofrog v1.7.1 h1:ME1Meg4hukAT/7X6HUQCVSe4DNjMZACCP8aCY37EW/w= -github.com/jfrog/gofrog v1.7.1/go.mod h1:X7bjfWoQDN0Z4FQGbE91j3gbPP7Urwzm4Z8tkvrlbRI= -github.com/jfrog/jfrog-cli-core/v2 v2.51.0 h1:nESbCpSTPZx1av0W9tdmWLxKaPSL1SaZinbZGtYNeFI= -github.com/jfrog/jfrog-cli-core/v2 v2.51.0/go.mod h1:064wSSHVI3ZIVi/a94yJqzs+ACM+9JK/u9tQ1sfTK6A= -github.com/jfrog/jfrog-client-go v1.40.1 h1:ISSSV7/IUS8R+QCPfH2lVKLburbv2Xn07fvNyDc17rI= -github.com/jfrog/jfrog-client-go v1.40.1/go.mod h1:FprEW0Sqhj6ZSFTFk9NCni+ovFAYMA3zCBmNX4hGXgQ= +github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc= +github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= +github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= +github.com/jfrog/build-info-go v1.10.5 h1:cW03JlPlKv7RMUU896uLUxyLWXAmCgR5Y5QX0fwgz0Q= +github.com/jfrog/build-info-go v1.10.5/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= +github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= +github.com/jfrog/go-mockhttp v0.3.1/go.mod h1:LmKHex73SUZswM8ANS8kPxLihTOvtq44HVcCoTJKuqc= +github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= +github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= +github.com/jfrog/jfrog-cli-core/v2 v2.56.8 h1:UexulAwRVN20VmYACijkTFYKqtUq5myE4okEgmUrorw= +github.com/jfrog/jfrog-cli-core/v2 v2.56.8/go.mod h1:RY74eDpw1WBxruSfZ0HO1ax7c1NAj+rbBgA/hVOJNME= +github.com/jfrog/jfrog-client-go v1.48.0 h1:hx5B7+Wnobmzq4aFVZtALtbEVDFcjpn0Wb4q2m6H4KU= +github.com/jfrog/jfrog-client-go v1.48.0/go.mod h1:1a7bmQHkRmPEza9wva2+WVrYzrGbosrMymq57kyG5gU= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -133,8 +132,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4= github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= @@ -146,8 +145,8 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -185,8 +184,8 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -196,8 +195,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= @@ -205,10 +205,10 @@ github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1ump github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= -github.com/vbauerster/mpb/v7 v7.5.3 h1:BkGfmb6nMrrBQDFECR/Q7RkKCw7ylMetCb4079CGs4w= -github.com/vbauerster/mpb/v7 v7.5.3/go.mod h1:i+h4QY6lmLvBNK2ah1fSreiw3ajskRlBp9AhY/PnuOE= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= +github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -221,39 +221,24 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -267,40 +252,22 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/model/actions.go b/model/actions.go index 5bbbafc..df9328b 100644 --- a/model/actions.go +++ b/model/actions.go @@ -1,43 +1,27 @@ package model -import ( - "fmt" - "regexp" -) +type ActionFilterType string const ( - ActionUnspecified = "ACTION_UNSPECIFIED" - ActionBeforeDownload = "BEFORE_DOWNLOAD" - ActionAfterDownload = "AFTER_DOWNLOAD" - ActionBeforeUpload = "BEFORE_UPLOAD" - ActionAfterCreate = "AFTER_CREATE" - ActionAfterBuildInfoSave = "AFTER_BUILD_INFO_SAVE" - ActionAfterMove = "AFTER_MOVE" - ActionGenericEvent = "GENERIC_EVENT" - ActionBeforeCreateToken = "BEFORE_CREATE_TOKEN" - ActionBeforePropertyCreate = "BEFORE_PROPERTY_CREATE" -) - -var ( - actionsNames = fmt.Sprintf("%s|%s|%s|%s|%s|%s|%s|%s|%s", ActionBeforeDownload, ActionAfterDownload, ActionBeforeUpload, ActionAfterCreate, ActionAfterBuildInfoSave, ActionAfterMove, ActionGenericEvent, ActionBeforeCreateToken, ActionBeforePropertyCreate) - actionsNamesPattern = regexp.MustCompile("(" + actionsNames + ")") + FilterTypeRepo = "FILTER_REPO" + FilterTypeSchedule = "FILTER_SCHEDULE" ) -var actionsWithoutCriteria = map[string]any{ - ActionAfterBuildInfoSave: struct{}{}, - ActionGenericEvent: struct{}{}, - ActionBeforeCreateToken: struct{}{}, -} - -func ActionNames() string { - return actionsNames -} - -func ActionNeedsCriteria(actionName string) bool { - _, doNotNeedCriteria := actionsWithoutCriteria[actionName] - return !doNotNeedCriteria +type Action struct { + Application string `json:"application"` + Name string `json:"name"` } -func ActionIsValid(actionName string) bool { - return actionsNamesPattern.MatchString(actionName) +type ActionMetadata struct { + Action Action `json:"action"` + Description string `json:"description"` + SamplePayload string `json:"samplePayload"` + SampleCode string `json:"sampleCode"` + TypesDefinitions string `json:"typesDefinitions"` + SupportProjects bool `json:"supportProjects"` + FilterType ActionFilterType `json:"filterType"` + MandatoryFilter bool `json:"mandatoryFilter"` + WikiUrl string `json:"wikiUrl"` + Async bool `json:"async"` + ExecutionRequestType string `json:"executionRequestType"` } diff --git a/model/actions_test.go b/model/actions_test.go deleted file mode 100644 index 0e05f59..0000000 --- a/model/actions_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package model - -import ( - "regexp" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestActionNames(t *testing.T) { - names := ActionNames() - require.NotEmpty(t, names) - matched, err := regexp.MatchString(`[A-B_|]+`, names) - require.NoError(t, err) - assert.True(t, matched) -} - -func TestActionNeedsCriteria(t *testing.T) { - for _, action := range strings.Split(ActionNames(), "|") { - t.Run(action, func(t *testing.T) { - assert.Equalf(t, action != "AFTER_BUILD_INFO_SAVE" && action != "GENERIC_EVENT" && action != "BEFORE_CREATE_TOKEN", ActionNeedsCriteria(action), "ActionNeedsCriteria(%v)", action) - }) - } -} - -func TestActionIsValid(t *testing.T) { - t.Run("HACK_ME", func(t *testing.T) { - assert.Equalf(t, false, ActionIsValid("HACK_ME"), "ActionIsValid(%v)", "HACK_ME") - }) - for _, action := range strings.Split(ActionNames(), "|") { - t.Run(action, func(t *testing.T) { - assert.Equalf(t, true, ActionIsValid(action), "ActionIsValid(%v)", action) - }) - } -} diff --git a/model/flags.go b/model/flags.go index 7865c3e..9ea8858 100644 --- a/model/flags.go +++ b/model/flags.go @@ -21,6 +21,7 @@ const ( FlagJsonOutput = "json" FlagTimeout = "timeout-ms" FlagProjectKey = "project-key" + FlagApplication = "application" defaultTimeoutMillis = 5000 ) @@ -31,7 +32,7 @@ var ( EnvKeyAddSecretValue = "JFROG_WORKER_CLI_DEV_ADD_SECRET_VALUE" ) -type intFlagProvider interface { +type IntFlagProvider interface { IsFlagSet(name string) bool GetIntFlagValue(name string) (int, error) } @@ -83,7 +84,7 @@ func GetJsonPayloadArgument() components.Argument { } } -func GetTimeoutParameter(c intFlagProvider) (time.Duration, error) { +func GetTimeoutParameter(c IntFlagProvider) (time.Duration, error) { if !c.IsFlagSet(FlagTimeout) { return defaultTimeoutMillis * time.Millisecond, nil } @@ -95,6 +96,14 @@ func GetTimeoutParameter(c intFlagProvider) (time.Duration, error) { return time.Duration(value) * time.Millisecond, nil } +func GetApplicationFlag() components.StringFlag { + return components.NewStringFlag( + FlagApplication, + "The application that provides the event. If omitted worker will try to guess it and will raise an error if it cannot.", + components.WithStrDefaultValue(""), + ) +} + func GetServerDetails(c *components.Context) (*config.ServerDetails, error) { serverUrlFromEnv, envHasServerUrl := os.LookupEnv(EnvKeyServerUrl) accessTokenFromEnv, envHasAccessToken := os.LookupEnv(EnvKeyAccessToken) diff --git a/model/flags_test.go b/model/flags_test.go index e46e0b1..6839ab2 100644 --- a/model/flags_test.go +++ b/model/flags_test.go @@ -13,7 +13,7 @@ import ( func TestGetTimeoutParameter(t *testing.T) { tests := []struct { name string - flagProvider intFlagProvider + flagProvider IntFlagProvider want time.Duration wantErr string }{ diff --git a/model/manifest.go b/model/manifest.go index 45a9de2..5ff3923 100644 --- a/model/manifest.go +++ b/model/manifest.go @@ -1,16 +1,5 @@ package model -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/jfrog/jfrog-client-go/utils/log" -) - type ArtifactFilterCriteria struct { RepoKeys []string `json:"repoKeys,omitempty"` } @@ -31,136 +20,5 @@ type Manifest struct { ProjectKey string `json:"projectKey"` Secrets Secrets `json:"secrets"` FilterCriteria FilterCriteria `json:"filterCriteria,omitempty"` -} - -// ReadManifest reads a manifest from the working directory or from the directory provided as argument. -func ReadManifest(dir ...string) (*Manifest, error) { - manifestFile, err := getManifestFile(dir...) - if err != nil { - return nil, err - } - - log.Debug(fmt.Sprintf("Reading manifest from %s", manifestFile)) - - manifestBytes, err := os.ReadFile(manifestFile) - if err != nil { - return nil, err - } - - manifest := Manifest{} - - err = json.Unmarshal(manifestBytes, &manifest) - if err != nil { - return nil, err - } - - return &manifest, nil -} - -func getManifestFile(dir ...string) (string, error) { - var manifestFolder string - - if len(dir) > 0 { - manifestFolder = dir[0] - } else { - var err error - if manifestFolder, err = os.Getwd(); err != nil { - return "", err - } - } - - manifestFile := filepath.Join(manifestFolder, "manifest.json") - - return manifestFile, nil -} - -func (mf *Manifest) Save(dir ...string) error { - manifestFile, err := getManifestFile(dir...) - if err != nil { - return err - } - - writer, err := os.OpenFile(manifestFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) - if err != nil { - return err - } - - defer func() { - closeErr := writer.Close() - if closeErr != nil { - if err == nil { - err = errors.Join(err, closeErr) - } else { - err = closeErr - } - } - }() - - encoder := json.NewEncoder(writer) - encoder.SetIndent("", " ") - err = encoder.Encode(mf) - - return err -} - -// ReadSourceCode reads the content of the file pointed by SourceCodePath -func (mf *Manifest) ReadSourceCode() (string, error) { - log.Debug(fmt.Sprintf("Reading source code from %s", mf.SourceCodePath)) - sourceBytes, err := os.ReadFile(mf.SourceCodePath) - if err != nil { - return "", err - } - return string(sourceBytes), nil -} - -func (mf *Manifest) Validate() error { - if mf.Name == "" { - return invalidManifestErr("missing name") - } - - if mf.SourceCodePath == "" { - return invalidManifestErr("missing source code path") - } - - if mf.Action == "" { - return invalidManifestErr("missing action") - } - - if !ActionIsValid(mf.Action) { - return invalidManifestErr(fmt.Sprintf("unknown action '%s' expecting one of %v", mf.Action, strings.Split(ActionNames(), "|"))) - } - - return nil -} - -func (mf *Manifest) DecryptSecrets(withPassword ...string) error { - if len(mf.Secrets) == 0 { - return nil - } - - var password string - if len(withPassword) > 0 { - password = withPassword[0] - } else { - var err error - password, err = ReadSecretPassword("Secrets Password: ") - if err != nil { - return err - } - } - - for name, value := range mf.Secrets { - clearValue, err := DecryptSecret(password, value) - if err != nil { - log.Debug(fmt.Sprintf("cannot decrypt secret '%s': %+v", name, err)) - return fmt.Errorf("cannot decrypt secret '%s', please check the manifest", name) - } - mf.Secrets[name] = clearValue - } - - return nil -} - -func invalidManifestErr(reason string) error { - return fmt.Errorf("invalid manifest: %s", reason) + Application string `json:"application,omitempty"` } diff --git a/model/worker.go b/model/worker.go index 7141ee2..d9238f0 100644 --- a/model/worker.go +++ b/model/worker.go @@ -1,7 +1,10 @@ package model -import "strings" - +type Secret struct { + Key string `json:"key"` + Value string `json:"value"` + MarkedForRemoval bool `json:"markedForRemoval"` +} type WorkerDetails struct { Key string `json:"key"` Description string `json:"description"` @@ -13,14 +16,3 @@ type WorkerDetails struct { Secrets []*Secret `json:"secrets"` ProjectKey string `json:"projectKey"` } - -func (w *WorkerDetails) KeyWithProject() string { - projectKey := strings.TrimSpace(w.ProjectKey) - if projectKey != "" { - projectPrefix := projectKey + "-" - if !strings.HasPrefix(w.Key, projectPrefix) { - return projectPrefix + w.Key - } - } - return w.Key -} diff --git a/qa-plugin/Makefile b/qa-plugin/Makefile index 2439592..7142d03 100644 --- a/qa-plugin/Makefile +++ b/qa-plugin/Makefile @@ -49,7 +49,7 @@ build:: build-install:: build mkdir -p "${HOME}/.jfrog/plugins/worker-qa/bin" - mv ${BINARY_CLI}/worker-cli-plugin "${HOME}/.jfrog/plugins/worker-qa/bin/worker-qa" + mv -f ${BINARY_CLI}/worker-cli-plugin "${HOME}/.jfrog/plugins/worker-qa/bin/worker-qa" chmod +x "${HOME}/.jfrog/plugins/worker-qa/bin/worker-qa" .PHONY: $(MAKECMDGOALS) diff --git a/qa-plugin/go.mod b/qa-plugin/go.mod index a68e451..c1e6931 100644 --- a/qa-plugin/go.mod +++ b/qa-plugin/go.mod @@ -1,25 +1,26 @@ module github.com/jfrog/jfrog-cli-platform-services-qa -go 1.22.2 +go 1.23.3 require ( - github.com/jfrog/jfrog-cli-core/v2 v2.51.0 + github.com/jfrog/jfrog-cli-core/v2 v2.56.8 github.com/jfrog/jfrog-cli-platform-services v1.0.0 ) require ( dario.cat/mergo v1.0.0 // indirect - github.com/BurntSushi/toml v1.3.2 // indirect - github.com/CycloneDX/cyclonedx-go v0.8.0 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/CycloneDX/cyclonedx-go v0.9.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/ProtonMail/go-crypto v1.1.2 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/c-bata/go-prompt v0.2.6 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/forPelevin/gomoji v1.2.0 // indirect @@ -27,36 +28,38 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jedib0t/go-pretty/v6 v6.5.7 // indirect - github.com/jfrog/archiver/v3 v3.6.0 // indirect - github.com/jfrog/build-info-go v1.9.26 // indirect - github.com/jfrog/gofrog v1.7.1 // indirect - github.com/jfrog/jfrog-client-go v1.40.1 // indirect + github.com/jedib0t/go-pretty/v6 v6.6.1 // indirect + github.com/jfrog/archiver/v3 v3.6.1 // indirect + github.com/jfrog/build-info-go v1.10.5 // indirect + github.com/jfrog/go-mockhttp v0.3.1 // indirect + github.com/jfrog/gofrog v1.7.6 // indirect + github.com/jfrog/jfrog-client-go v1.48.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-tty v0.0.5 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/nwaples/rardecode v1.1.3 // indirect - github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -67,23 +70,24 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.18.2 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect - github.com/urfave/cli v1.22.14 // indirect + github.com/urfave/cli v1.22.16 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.27.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/qa-plugin/go.sum b/qa-plugin/go.sum index b307874..a5db3ff 100644 --- a/qa-plugin/go.sum +++ b/qa-plugin/go.sum @@ -1,14 +1,14 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M= -github.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/CycloneDX/cyclonedx-go v0.9.0 h1:inaif7qD8bivyxp7XLgxUYtOXWtDez7+j72qKTMQTb8= +github.com/CycloneDX/cyclonedx-go v0.9.0/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0Go3KS0= +github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -23,7 +23,6 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oM github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -35,12 +34,10 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -70,8 +67,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -87,23 +84,25 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.5.7 h1:28Z6UxnNyKCVISGdItMiCCc7A0mbDF+SYvgo3U8ZKuQ= -github.com/jedib0t/go-pretty/v6 v6.5.7/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= -github.com/jfrog/archiver/v3 v3.6.0 h1:OVZ50vudkIQmKMgA8mmFF9S0gA47lcag22N13iV3F1w= -github.com/jfrog/archiver/v3 v3.6.0/go.mod h1:fCAof46C3rAXgZurS8kNRNdSVMKBbZs+bNNhPYxLldI= -github.com/jfrog/build-info-go v1.9.26 h1:1Ddc6+Ecvhc+UMnKhRVG1jGM6fYNwA49207azTBGBc8= -github.com/jfrog/build-info-go v1.9.26/go.mod h1:8T7/ajM9aGshvgpwCtXwIFpyF/R6CEn4W+/FLryNXWw= -github.com/jfrog/gofrog v1.7.1 h1:ME1Meg4hukAT/7X6HUQCVSe4DNjMZACCP8aCY37EW/w= -github.com/jfrog/gofrog v1.7.1/go.mod h1:X7bjfWoQDN0Z4FQGbE91j3gbPP7Urwzm4Z8tkvrlbRI= -github.com/jfrog/jfrog-cli-core/v2 v2.51.0 h1:nESbCpSTPZx1av0W9tdmWLxKaPSL1SaZinbZGtYNeFI= -github.com/jfrog/jfrog-cli-core/v2 v2.51.0/go.mod h1:064wSSHVI3ZIVi/a94yJqzs+ACM+9JK/u9tQ1sfTK6A= -github.com/jfrog/jfrog-client-go v1.40.1 h1:ISSSV7/IUS8R+QCPfH2lVKLburbv2Xn07fvNyDc17rI= -github.com/jfrog/jfrog-client-go v1.40.1/go.mod h1:FprEW0Sqhj6ZSFTFk9NCni+ovFAYMA3zCBmNX4hGXgQ= +github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc= +github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= +github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= +github.com/jfrog/build-info-go v1.10.5 h1:cW03JlPlKv7RMUU896uLUxyLWXAmCgR5Y5QX0fwgz0Q= +github.com/jfrog/build-info-go v1.10.5/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= +github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= +github.com/jfrog/go-mockhttp v0.3.1/go.mod h1:LmKHex73SUZswM8ANS8kPxLihTOvtq44HVcCoTJKuqc= +github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= +github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= +github.com/jfrog/jfrog-cli-core/v2 v2.56.8 h1:UexulAwRVN20VmYACijkTFYKqtUq5myE4okEgmUrorw= +github.com/jfrog/jfrog-cli-core/v2 v2.56.8/go.mod h1:RY74eDpw1WBxruSfZ0HO1ax7c1NAj+rbBgA/hVOJNME= +github.com/jfrog/jfrog-client-go v1.48.0 h1:hx5B7+Wnobmzq4aFVZtALtbEVDFcjpn0Wb4q2m6H4KU= +github.com/jfrog/jfrog-client-go v1.48.0/go.mod h1:1a7bmQHkRmPEza9wva2+WVrYzrGbosrMymq57kyG5gU= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -133,8 +132,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4= github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= @@ -146,8 +145,8 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -185,8 +184,8 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -196,8 +195,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= @@ -205,10 +205,10 @@ github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1ump github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= -github.com/vbauerster/mpb/v7 v7.5.3 h1:BkGfmb6nMrrBQDFECR/Q7RkKCw7ylMetCb4079CGs4w= -github.com/vbauerster/mpb/v7 v7.5.3/go.mod h1:i+h4QY6lmLvBNK2ah1fSreiw3ajskRlBp9AhY/PnuOE= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= +github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -221,39 +221,22 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -267,40 +250,22 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/test/commands/deploy_cmd_test.go b/test/commands/deploy_cmd_test.go index 8ab428b..0707530 100644 --- a/test/commands/deploy_cmd_test.go +++ b/test/commands/deploy_cmd_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,7 +43,7 @@ func TestDeployCommand(t *testing.T) { Description: "My worker", Enabled: true, SourceCode: `export default async function() { return { "status": "OK" } }`, - Action: model.ActionGenericEvent, + Action: "GENERIC_EVENT", }, }, }), @@ -54,7 +56,7 @@ func TestDeployCommand(t *testing.T) { Description: "My worker", Enabled: true, SourceCode: `export default async function() { return { "status": "OK" } }`, - Action: model.ActionGenericEvent, + Action: "GENERIC_EVENT", Secrets: []*model.Secret{ {Key: "sec-1", Value: "val-1"}, {Key: "sec-2", Value: "val-2"}, }, @@ -62,7 +64,7 @@ func TestDeployCommand(t *testing.T) { }, patchManifest: func(mf *model.Manifest) { mf.Secrets = model.Secrets{ - "sec-3": infra.MustEncryptSecret(t, "val-3"), + "sec-3": common.MustEncryptSecret(t, "val-3"), } }, }), @@ -96,7 +98,7 @@ func deployTestSpec(tc deployTestCase) infra.TestDefinition { require.NoError(it, err) if tc.patchManifest != nil { - infra.PatchManifest(it, tc.patchManifest) + common.PatchManifest(it, tc.patchManifest) } infra.AddSecretPasswordToEnv(it) @@ -115,10 +117,10 @@ func deployTestSpec(tc deployTestCase) infra.TestDefinition { if tc.wantErr == nil { require.NoError(it, err) - mf, err := model.ReadManifest() + mf, err := common.ReadManifest() require.NoError(it, err) - require.NoError(it, mf.DecryptSecrets()) + require.NoError(it, common.DecryptManifestSecrets(mf)) assertWorkerDeployed(it, mf) } else { @@ -146,9 +148,9 @@ func assertWorkerDeployed(it *infra.Test, mf *model.Manifest) { assert.Equalf(it, mf.Description, deployed.Description, "Description mismatch") assert.Equalf(it, mf.Enabled, deployed.Enabled, "Enabled mismatch") - sourceCode, err := mf.ReadSourceCode() + sourceCode, err := common.ReadSourceCode(mf) require.NoError(it, err) - assert.Equalf(it, model.CleanImports(sourceCode), deployed.SourceCode, "SourceCode mismatch") + assert.Equalf(it, common.CleanImports(sourceCode), deployed.SourceCode, "SourceCode mismatch") require.Equalf(it, len(mf.Secrets), len(deployed.Secrets), "Secrets length mismatch") for _, deployedSecret := range deployed.Secrets { diff --git a/test/commands/dry_run_cmd_test.go b/test/commands/dry_run_cmd_test.go index 8083736..2436a0d 100644 --- a/test/commands/dry_run_cmd_test.go +++ b/test/commands/dry_run_cmd_test.go @@ -9,6 +9,8 @@ import ( "path/filepath" "testing" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,19 +38,19 @@ func TestDryRun(t *testing.T) { dryRunSpec(dryRunTestCase{ name: "nominal case", commandArgs: []string{ - infra.MustJsonMarshal(t, map[string]any{"my": "payload"}), + common.MustJsonMarshal(t, map[string]any{"my": "payload"}), }, assert: assertDryRunSucceed, }), dryRunSpec(dryRunTestCase{ name: "reads from stdin", - stdInput: infra.MustJsonMarshal(t, map[string]any{"my": "request"}), + stdInput: common.MustJsonMarshal(t, map[string]any{"my": "request"}), commandArgs: []string{"-"}, assert: assertDryRunSucceed, }), dryRunSpec(dryRunTestCase{ name: "reads from file", - fileInput: infra.MustJsonMarshal(t, map[string]any{"my": "file-content"}), + fileInput: common.MustJsonMarshal(t, map[string]any{"my": "file-content"}), assert: assertDryRunSucceed, }), dryRunSpec(dryRunTestCase{ @@ -93,7 +95,7 @@ func TestDryRun(t *testing.T) { Description: "My worker", Enabled: true, SourceCode: `export default async function() { return { "status": "OK" } }`, - Action: model.ActionGenericEvent, + Action: "GENERIC_EVENT", Secrets: []*model.Secret{ { Key: "sec-1", Value: "val-1", @@ -105,7 +107,7 @@ func TestDryRun(t *testing.T) { patchManifest: func(mf *model.Manifest) { mf.ProjectKey = "my-project" mf.Name = "wk-1" - mf.Secrets = model.Secrets{"sec-1": infra.MustEncryptSecret(t, "val-1-updated")} + mf.Secrets = model.Secrets{"sec-1": common.MustEncryptSecret(t, "val-1-updated")} }, assert: assertDryRunWithSecretsUpdate, }), @@ -123,7 +125,7 @@ func dryRunSpec(tc dryRunTestCase) infra.TestDefinition { for _, initialWorker := range tc.initWorkers { it.CreateWorker(initialWorker) it.Cleanup(func() { - it.DeleteWorker(initialWorker.KeyWithProject()) + it.DeleteWorker(initialWorker.Key) }) } @@ -144,14 +146,14 @@ func dryRunSpec(tc dryRunTestCase) infra.TestDefinition { infra.AddSecretPasswordToEnv(it) if tc.patchManifest != nil { - infra.PatchManifest(it, tc.patchManifest) + common.PatchManifest(it, tc.patchManifest) } cmd := []string{infra.AppName, "dry-run"} cmd = append(cmd, tc.commandArgs...) if tc.fileInput != "" { - cmd = append(cmd, "@"+infra.CreateTempFileWithContent(it, tc.fileInput)) + cmd = append(cmd, "@"+common.CreateTempFileWithContent(it, tc.fileInput)) } tc.assert(it, it.RunCommand(cmd...), &tc) diff --git a/test/commands/execute_cmd_test.go b/test/commands/execute_cmd_test.go index a2c3d49..d028a15 100644 --- a/test/commands/execute_cmd_test.go +++ b/test/commands/execute_cmd_test.go @@ -9,6 +9,8 @@ import ( "path/filepath" "testing" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -35,7 +37,7 @@ func TestExecute(t *testing.T) { executeSpec(executeTestCase{ name: "execute from manifest", commandArgs: []string{ - infra.MustJsonMarshal(t, map[string]any{"my": "payload"}), + common.MustJsonMarshal(t, map[string]any{"my": "payload"}), }, assert: assertExecuteSucceed, }), @@ -44,19 +46,19 @@ func TestExecute(t *testing.T) { workerKey: "my-worker", commandArgs: []string{ "my-worker", - infra.MustJsonMarshal(t, map[string]any{"my": "payload"}), + common.MustJsonMarshal(t, map[string]any{"my": "payload"}), }, assert: assertExecuteSucceed, }), executeSpec(executeTestCase{ name: "reads from stdin", - stdInput: infra.MustJsonMarshal(t, map[string]any{"my": "request"}), + stdInput: common.MustJsonMarshal(t, map[string]any{"my": "request"}), commandArgs: []string{"-"}, assert: assertExecuteSucceed, }), executeSpec(executeTestCase{ name: "reads from file", - fileInput: infra.MustJsonMarshal(t, map[string]any{"my": "file-content"}), + fileInput: common.MustJsonMarshal(t, map[string]any{"my": "file-content"}), assert: assertExecuteSucceed, }), executeSpec(executeTestCase{ @@ -132,7 +134,7 @@ func executeSpec(tc executeTestCase) infra.TestDefinition { } // We should enable the worker - infra.PatchManifest(it, func(mf *model.Manifest) { + common.PatchManifest(it, func(mf *model.Manifest) { mf.Name = workerName mf.Enabled = true }) @@ -149,7 +151,7 @@ func executeSpec(tc executeTestCase) infra.TestDefinition { cmd = append(cmd, tc.commandArgs...) if tc.fileInput != "" { - cmd = append(cmd, "@"+infra.CreateTempFileWithContent(it, tc.fileInput)) + cmd = append(cmd, "@"+common.CreateTempFileWithContent(it, tc.fileInput)) } tc.assert(it, it.RunCommand(cmd...), &tc) diff --git a/test/commands/list_cmd_test.go b/test/commands/list_cmd_test.go index 080f81f..21f2822 100644 --- a/test/commands/list_cmd_test.go +++ b/test/commands/list_cmd_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" "slices" "strings" "testing" @@ -36,21 +37,21 @@ func TestListCommand(t *testing.T) { Description: "My worker 0", Enabled: true, SourceCode: `export default async function() { return { "status": "OK" } }`, - Action: model.ActionGenericEvent, + Action: "GENERIC_EVENT", }, { Key: fmt.Sprintf("w%v", time.Now().Unix()+1), Description: "My worker 1", Enabled: true, SourceCode: `export default async function() { return { "status": "OK" } }`, - Action: model.ActionGenericEvent, + Action: "GENERIC_EVENT", }, { Key: fmt.Sprintf("w%v", time.Now().Unix()+2), Description: "My worker 2", Enabled: true, SourceCode: `export default async function() { return { "status": "OK" } }`, - Action: model.ActionBeforeDownload, + Action: "BEFORE_DOWNLOAD", FilterCriteria: model.FilterCriteria{ ArtifactFilterCriteria: model.ArtifactFilterCriteria{ RepoKeys: []string{"example-repo-local"}, @@ -65,7 +66,7 @@ func TestListCommand(t *testing.T) { Enabled: true, Debug: true, SourceCode: `export default async function() { return { "status": "OK" } }`, - Action: model.ActionGenericEvent, + Action: "GENERIC_EVENT", ProjectKey: "my-project", } @@ -168,8 +169,8 @@ func assertWorkerListJSON(workers ...*model.WorkerDetails) func(t require.Testin assert.Equalf(t, len(workers), len(gotWorkers.Workers), "Length mismatch") - infra.SortWorkers(workers) - infra.SortWorkers(gotWorkers.Workers) + common.SortWorkers(workers) + common.SortWorkers(gotWorkers.Workers) for i, wantWorker := range workers { gotWorker := gotWorkers.Workers[i] diff --git a/test/commands/remove_cmd_test.go b/test/commands/remove_cmd_test.go index 301b8ea..7153896 100644 --- a/test/commands/remove_cmd_test.go +++ b/test/commands/remove_cmd_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -72,7 +74,7 @@ func removeTestSpec(tc removeTestCase) infra.TestDefinition { if tc.wantErr == nil { require.NoError(it, err) - mf, err := model.ReadManifest() + mf, err := common.ReadManifest() require.NoError(it, err) assertWorkerRemoved(it, mf) diff --git a/test/infra/itest_runner.go b/test/infra/itest_runner.go index 6c5b46a..1b8d1c6 100644 --- a/test/infra/itest_runner.go +++ b/test/infra/itest_runner.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/jfrog/jfrog-cli-platform-services/model" "github.com/google/uuid" @@ -22,8 +24,6 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/stretchr/testify/require" - - "github.com/jfrog/jfrog-cli-platform-services/commands" ) type TestFunction func(it *Test) @@ -120,17 +120,17 @@ func runTest(t *testing.T, testSpec TestDefinition) { } if testSpec.Input != "" { - commands.SetCliIn(bytes.NewReader([]byte(testSpec.Input))) + common.SetCliIn(bytes.NewReader([]byte(testSpec.Input))) t.Cleanup(func() { - commands.SetCliIn(os.Stdin) + common.SetCliIn(os.Stdin) }) } if testSpec.CaptureOutput { var newOutput bytes.Buffer - commands.SetCliOut(&newOutput) + common.SetCliOut(&newOutput) t.Cleanup(func() { - commands.SetCliOut(os.Stdout) + common.SetCliOut(os.Stdout) }) it.output = &newOutput } diff --git a/test/infra/secrets_utils.go b/test/infra/secrets_utils.go index c870128..6273f6d 100644 --- a/test/infra/secrets_utils.go +++ b/test/infra/secrets_utils.go @@ -4,28 +4,16 @@ package infra import ( "context" - "os" "time" + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/jfrog/jfrog-cli-platform-services/model" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -const secretPassword = "P@ssw0rd!" - -func MustEncryptSecret(t require.TestingT, secretValue string) string { - encryptedValue, err := model.EncryptSecret(secretPassword, secretValue) - require.NoError(t, err) - return encryptedValue -} - -func AddSecretPasswordToEnv(t cliTestingT) { - err := os.Setenv(model.EnvKeySecretsPassword, secretPassword) - require.NoError(t, err) - t.Cleanup(func() { - _ = os.Unsetenv(model.EnvKeySecretsPassword) - }) +func AddSecretPasswordToEnv(t common.Test) { + common.TestSetEnv(t, model.EnvKeySecretsPassword, common.SecretPassword) } func AssertSecretValueFromServer(it *Test, workerKey string, secretKey string, wantValue string) { diff --git a/test/infra/test_utils.go b/test/infra/test_utils.go deleted file mode 100644 index 2685223..0000000 --- a/test/infra/test_utils.go +++ /dev/null @@ -1,60 +0,0 @@ -//go:build itest - -package infra - -import ( - "encoding/json" - "os" - "sort" - - "github.com/jfrog/jfrog-cli-platform-services/model" - - "github.com/stretchr/testify/require" -) - -type cliTestingT interface { - require.TestingT - Cleanup(func()) -} - -func MustJsonMarshal(t cliTestingT, data any) string { - out, err := json.Marshal(data) - require.NoError(t, err) - return string(out) -} - -func CreateTempFileWithContent(t cliTestingT, content string) string { - file, err := os.CreateTemp("", "wks-cli-*.test") - require.NoError(t, err) - - t.Cleanup(func() { - // We do not care about an error here - _ = os.Remove(file.Name()) - }) - - _, err = file.Write([]byte(content)) - require.NoError(t, err) - - return file.Name() -} - -func PatchManifest(t require.TestingT, applyPatch func(mf *model.Manifest), dir ...string) { - mf, err := model.ReadManifest(dir...) - require.NoError(t, err) - - applyPatch(mf) - - require.NoError(t, mf.Save(dir...)) -} - -func PatchWorker(s *model.WorkerDetails, applyPatch func(w *model.WorkerDetails)) *model.WorkerDetails { - t := *s - applyPatch(&t) - return &t -} - -func SortWorkers(workers []*model.WorkerDetails) { - sort.Slice(workers, func(i, j int) bool { - return workers[i].Key < workers[j].Key - }) -}