From 59b84f950f5121eacc361c4c92b8143ee1b5ca29 Mon Sep 17 00:00:00 2001 From: Siddhant N Trivedi <68370997+siddhant-deepsource@users.noreply.github.com> Date: Tue, 2 Nov 2021 13:55:06 +0530 Subject: [PATCH] chore: merge changes to master (#80) * Add boilerplate and version command (#50) * Add basic boilerplate with version command Signed-off-by: siddhant-deepsource * Add utility to dynamically read version info Signed-off-by: siddhant-deepsource * Add test Signed-off-by: siddhant-deepsource * Autofix issues in 4 files Resolved issues in the following files via DeepSource Autofix: 1. init.go 2. pkg/cmd/cmd.go 3. pkg/cmd/version/version.go 4. tests/report_workflow_test.go * Terminate Execute error in caller function Signed-off-by: siddhant-deepsource * Update pkg/cmd/cmd.go Co-authored-by: Vishnu Jayadevan <68588475+vishnu-deepsource@users.noreply.github.com> * Minor refactoring for CLI auth (#54) Signed-off-by: Vishnu Jayadevan * Fix grammatical error Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> Co-authored-by: Vishnu Jayadevan <68588475+vishnu-deepsource@users.noreply.github.com> Co-authored-by: Sanket Saurav * feat: cli authentication (#57) * cli login implementation Signed-off-by: siddhant-deepsource * minor improvements Signed-off-by: siddhant-deepsource * Refactor mutations and return error if login fails Signed-off-by: siddhant-deepsource * Remove unnecessary brace Signed-off-by: siddhant-deepsource * Remove unnecessary file Signed-off-by: siddhant-deepsource * Refactor api directory Signed-off-by: siddhant-deepsource * Implement config factory Signed-off-by: siddhant-deepsource * [WIP] Implement token verification Signed-off-by: siddhant-deepsource * Use machinebox/graphql and get deviceCode query working Signed-off-by: siddhant-deepsource * Use machinebox for fetching jwt Signed-off-by: siddhant-deepsource * Remove commented code Signed-off-by: siddhant-deepsource * Re-authenticate confirmation prompt Signed-off-by: siddhant-deepsource * Fix reauthentication Signed-off-by: siddhant-deepsource * Add logout command implementation Signed-off-by: siddhant-deepsource * Fix import error Signed-off-by: siddhant-deepsource * Add confirmation before logout Signed-off-by: siddhant-deepsource * Add refresh token query Signed-off-by: siddhant-deepsource * Implement auth refresh Signed-off-by: siddhant-deepsource * Implement auth status Signed-off-by: siddhant-deepsource * Implement repo status command Signed-off-by: siddhant-deepsource * Implement repo view command Signed-off-by: siddhant-deepsource * Add comment Signed-off-by: siddhant-deepsource * Merge config generator code Signed-off-by: siddhant-deepsource * Move survey prompt functions to cmdutils package Signed-off-by: siddhant-deepsource * Integrate analyzer and transformer apis into config generator Signed-off-by: siddhant-deepsource * Remove .deepsource.toml file Signed-off-by: siddhant-deepsource * Integrate analyzer and transformer apis into config validator Signed-off-by: siddhant-deepsource * Add queries for issue list Signed-off-by: siddhant-deepsource * Fix resolving owner Signed-off-by: siddhant-deepsource * Fix issue list Signed-off-by: siddhant-deepsource * Raise error when the limit is greater than 100 Signed-off-by: siddhant-deepsource * Push fix Signed-off-by: siddhant-deepsource * Last minute fixes Signed-off-by: siddhant-deepsource * Change api endpoint Signed-off-by: siddhant-deepsource * Remove stale deepsource.toml file (#58) * Fix retrieval of sentry DSN (#59) * Remove legacy files and fetch sentry dsn from ldflag Signed-off-by: siddhant-deepsource * Declare sentry dsn variable Signed-off-by: siddhant-deepsource * Use goreleaser supplied version flag (#60) Signed-off-by: siddhant-deepsource * Add version ldflag to goreleaser config (#61) Signed-off-by: siddhant-deepsource * Support darwin/arm64 and other arm architecture (#65) Signed-off-by: siddhant-deepsource * Add github action workflow for goreleaser (#63) * Add github action workflow for goreleaser Signed-off-by: siddhant-deepsource * Use workdir to switch to directory containing main package Signed-off-by: siddhant-deepsource * Accepts all tags for release creation Signed-off-by: siddhant-deepsource * Add goreleaser workflow to publish homebrew tap (#67) * Add goreleaser workflow to publish homebrew tap Signed-off-by: siddhant-deepsource * Add recipe name Signed-off-by: siddhant-deepsource * Fix the homebrew tap reponame Signed-off-by: siddhant-deepsource * Only target arm64 builds for now Signed-off-by: siddhant-deepsource * Supply github token for brew publishing Signed-off-by: siddhant-deepsource Use deepsourcebot for pushing taps (#69) Signed-off-by: siddhant-deepsource Fix token retrieval (#70) Signed-off-by: siddhant-deepsource Some changes to test brew tap (#71) Signed-off-by: siddhant-deepsource Supply token to brew workflow Signed-off-by: siddhant-deepsource Use different token for goreleaser Signed-off-by: siddhant-deepsource Restore permissions Signed-off-by: siddhant-deepsource Use same token for homebrew as well as goreleaser Signed-off-by: siddhant-deepsource Use PAT as GITHUB_TOKEN Signed-off-by: siddhant-deepsource Use different token for homebrew Signed-off-by: siddhant-deepsource * PLT-2365: Increase limit of uploads to 20 MB (#68) * feat: DeepSource independent SDK package and code refactoring (#66) * Move the api code to sdk directory Signed-off-by: siddhant-deepsource * Use deepsource sdk in config generation and validation workflows Signed-off-by: siddhant-deepsource * Add global package and modify auth commands w.r.t sdk package Signed-off-by: siddhant-deepsource * Fix the changes in auth w.r.t using sdk Signed-off-by: siddhant-deepsource * Modify the repo and issues commands according to the sdk Signed-off-by: siddhant-deepsource * Minor fixes Signed-off-by: siddhant-deepsource * Fix setting the tokenExpiry in the config Signed-off-by: siddhant-deepsource * Minor fixes in auth related changes Signed-off-by: siddhant-deepsource * Fix setting the tokenExpiry (again) Signed-off-by: siddhant-deepsource * Fix errors while fetching analyzers and transformers list Signed-off-by: siddhant-deepsource * Fix the config validator path extraction Signed-off-by: siddhant-deepsource * Fix issues listing Signed-off-by: siddhant-deepsource * Autofix issues in 3 files Resolved issues in the following files via DeepSource Autofix: 1. command/root.go 2. deepsource/client.go 3. deepsource/repo/queries/repo_status.go * Implement args validation Signed-off-by: siddhant-deepsource * Fix DeepSource issues Signed-off-by: siddhant-deepsource * Rename cmdutils to utils Signed-off-by: siddhant-deepsource * Refactor SDK response types and improve usage of global variables Signed-off-by: siddhant-deepsource * Refactor issue listing command Signed-off-by: siddhant-deepsource * More fixes related to issue listing and config workflows Signed-off-by: siddhant-deepsource * Fix repo status workflow Signed-off-by: siddhant-deepsource * More refactoring in commands Signed-off-by: siddhant-deepsource * Autofix issues in 5 files Resolved issues in the following files via DeepSource Autofix: 1. command/auth/status/status.go 2. command/config/generate/generic_input.go 3. command/repo/status/status.go 4. utils/fetch_remote.go 5. utils/prompt.go * Rename repo to repository and improve config reading and loading Signed-off-by: siddhant-deepsource * Add sentry to report error Signed-off-by: Siddhant N Trivedi * Delete internal/config package and add comments to SDK code Signed-off-by: Siddhant N Trivedi * More refactoring of auth subcommands Signed-off-by: Siddhant N Trivedi * More refactoring of config generate subcommand Signed-off-by: Siddhant N Trivedi * Address codereview for auth package Signed-off-by: Siddhant N Trivedi * Fetch analyzers and transformers data in a single utility Signed-off-by: Siddhant N Trivedi * Refactor remote selection logic and other code review suggested changes Signed-off-by: Siddhant N Trivedi * Fix errors Signed-off-by: Siddhant N Trivedi * Make the host configurable while authentication Signed-off-by: Siddhant N Trivedi * Use the host set by user in SDK client Signed-off-by: Siddhant N Trivedi * Fixes in setting SDK host and fix tokenexpiry timezone setting Signed-off-by: Siddhant N Trivedi * Improve refreshAuth response workflow and minor changes in utils Signed-off-by: Siddhant N Trivedi * Modify the retrieval of config and minor fixes Signed-off-by: Siddhant N Trivedi Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> * Remove unnecessary use of `fmt.Sprintf` (#73) * Remove unnecessary use of `fmt.Sprintf` * chore:fix DeepSource issue Signed-off-by: Siddhant N Trivedi Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> Co-authored-by: Siddhant N Trivedi * Remove unnecessary calls to fmt.Sprint (#74) Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> * Omit comparison with boolean constant (#75) Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> * Use plain channel send or receive (#76) Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> * test: add unit tests to configvalidator package (#77) * Add tests to config validator package Signed-off-by: Siddhant N Trivedi * tests: improve tests and add test names Signed-off-by: Siddhant N Trivedi * chore: revert changes in unmarshalling toml Signed-off-by: Siddhant N Trivedi * fix: fix DeepSource issue Signed-off-by: Siddhant N Trivedi * Format code with gofmt (#79) This commit fixes the style issues introduced in 9964fd1 according to the output from gofmt. Details: https://deepsource.io/gh/deepsourcelabs/cli/transform/c5e55d29-af89-4939-92a5-ee7ec1aab303/ Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> * feat(config-generation): Retrieve compulsory analyzer meta input based on API response (#72) * Dynamic analyzer meta input based on API response Signed-off-by: Siddhant N Trivedi * Autofix issues in 1 file Resolved issues in command/config/generate/analyzers_input.go via DeepSource Autofix * chore: refactor code into separate functions Signed-off-by: Siddhant N Trivedi * fix: Fix DeepSource issues Signed-off-by: Siddhant N Trivedi * chore: Rename Field to FieldName Signed-off-by: Siddhant N Trivedi * chore: Add comments Signed-off-by: Siddhant N Trivedi * Use gabs to parse analyzer meta JSON Signed-off-by: Siddhant N Trivedi * refactor: move the data extraction logic to separate function Signed-off-by: Siddhant N Trivedi * fix: restore analyzer meta required data to nil before checking for next analyzer Signed-off-by: Siddhant N Trivedi * chore: address code review Signed-off-by: Siddhant N Trivedi * chore: make changes according to code review Signed-off-by: Siddhant N Trivedi * chore: create array with length 0 and append data Signed-off-by: Siddhant N Trivedi Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> * feat: support enterprise server (#78) * chore: configure enterprise api endpoint Signed-off-by: Siddhant N Trivedi * fix: fix api endpoint Signed-off-by: Siddhant N Trivedi * chore: address code review Signed-off-by: Siddhant N Trivedi * chore: improve framing API client URL Signed-off-by: Siddhant N Trivedi * feat: interactive login flag Signed-off-by: Siddhant N Trivedi * chore: remove dependency of sdk package on config package Signed-off-by: Siddhant N Trivedi * refactor: rename ClientProperties as ClientOpts Signed-off-by: Siddhant N Trivedi * refactor: rename AnaData and TrData Signed-off-by: Siddhant N Trivedi * fix: use analyzer shortcode for filtering analyzerMeta Signed-off-by: Siddhant N Trivedi * chore: cleanup analyzer and transformer API data population logic Signed-off-by: Siddhant N Trivedi * chore: add spaces Signed-off-by: Siddhant N Trivedi * chore: delete api folder and modify workflow Signed-off-by: Siddhant N Trivedi * chore: tidy go modules Signed-off-by: Siddhant N Trivedi * chore: tidy go modules Signed-off-by: Siddhant N Trivedi * chore: fix and modify legacy test Signed-off-by: Siddhant N Trivedi * fix: fix error Signed-off-by: Siddhant N Trivedi * chore: address DeepSource issues Signed-off-by: Siddhant N Trivedi * fix: handle DSN parsing error Signed-off-by: Siddhant N Trivedi * fix: handle DSN parsing error at an earlier step as well Signed-off-by: Siddhant N Trivedi Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> Co-authored-by: Vishnu Jayadevan <68588475+vishnu-deepsource@users.noreply.github.com> Co-authored-by: Sanket Saurav Co-authored-by: Rahul Jha <68377773+rahul-deepsource@users.noreply.github.com> --- .deepsource.toml | 8 + .github/workflows/release.yml | 36 ++ .gitignore | 3 +- Makefile | 13 +- cmd/deepsource/main.go | 43 +++ command/auth/auth.go | 26 ++ command/auth/login/login.go | 116 ++++++ command/auth/login/login_flow.go | 117 ++++++ command/auth/logout/logout.go | 56 +++ command/auth/refresh/refresh.go | 82 ++++ command/auth/status/status.go | 48 +++ command/config/config.go | 22 ++ command/config/generate/analyzers_input.go | 165 ++++++++ command/config/generate/constants.go | 3 + command/config/generate/generate.go | 148 +++++++ command/config/generate/generic_input.go | 116 ++++++ command/config/generate/input.go | 51 +++ command/config/generate/transformers_input.go | 18 + command/config/generate/types.go | 24 ++ command/config/validate/validate.go | 202 ++++++++++ command/issues/issues.go | 20 + command/issues/list/list.go | 137 +++++++ command/repo/repo.go | 22 ++ command/repo/status/status.go | 80 ++++ command/repo/view/view.go | 96 +++++ constants.go => command/report/constants.go | 12 +- git.go => command/report/git.go | 2 +- query.go => command/report/query.go | 4 +- command/report/report.go | 258 +++++++++++++ .../report/tests}/dummy/python_coverage.xml | 0 .../report_graphql_error_response_body.json | 0 .../dummy/report_graphql_request_body.json | 0 .../report_graphql_success_response_body.json | 0 {tests => command/report/tests}/init_test.go | 0 .../report/tests}/report_workflow_test.go | 6 +- types.go => command/report/types.go | 2 +- command/root.go | 39 ++ command/version/command.go | 45 +++ command/version/command_test.go | 39 ++ config/config.go | 159 ++++++++ configvalidator/analyzer_config_validator.go | 101 +++++ .../analyzer_config_validator_test.go | 106 +++++ configvalidator/config_validator.go | 63 +++ configvalidator/config_validator_test.go | 173 +++++++++ configvalidator/generic_config_validator.go | 94 +++++ .../generic_config_validator_test.go | 181 +++++++++ .../transformer_config_validator.go | 45 +++ .../transformer_config_validator_test.go | 69 ++++ configvalidator/types.go | 24 ++ coverage.out | 1 + deepsource/analyzers/analyzers.go | 7 + deepsource/analyzers/queries/get_analyzers.go | 69 ++++ deepsource/auth/device.go | 10 + deepsource/auth/jwt.go | 13 + deepsource/auth/mutations/refresh_creds.go | 48 +++ deepsource/auth/mutations/register_device.go | 42 ++ deepsource/auth/mutations/request_jwt.go | 43 +++ deepsource/client.go | 189 +++++++++ deepsource/issues/issues_list.go | 22 ++ deepsource/issues/queries/list_file_issues.go | 122 ++++++ deepsource/issues/queries/list_issues.go | 100 +++++ .../repository/queries/repository_status.go | 79 ++++ deepsource/repository/repository.go | 8 + .../transformers/queries/get_transformers.go | 67 ++++ deepsource/transformers/transformers.go | 6 + go.mod | 25 +- go.sum | 363 ++++++++++++++++-- goreleaser.yaml | 17 +- init.go | 260 ------------- utils/cmd_validator.go | 66 ++++ utils/fetch_analyzers_transformers.go | 80 ++++ utils/fetch_remote.go | 121 ++++++ utils/prompt.go | 101 +++++ utils/remote_resolver.go | 94 +++++ version/version.go | 45 +++ version/version_test.go | 87 +++++ 76 files changed, 4843 insertions(+), 316 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 cmd/deepsource/main.go create mode 100644 command/auth/auth.go create mode 100644 command/auth/login/login.go create mode 100644 command/auth/login/login_flow.go create mode 100644 command/auth/logout/logout.go create mode 100644 command/auth/refresh/refresh.go create mode 100644 command/auth/status/status.go create mode 100644 command/config/config.go create mode 100644 command/config/generate/analyzers_input.go create mode 100644 command/config/generate/constants.go create mode 100644 command/config/generate/generate.go create mode 100644 command/config/generate/generic_input.go create mode 100644 command/config/generate/input.go create mode 100644 command/config/generate/transformers_input.go create mode 100644 command/config/generate/types.go create mode 100644 command/config/validate/validate.go create mode 100644 command/issues/issues.go create mode 100644 command/issues/list/list.go create mode 100644 command/repo/repo.go create mode 100644 command/repo/status/status.go create mode 100644 command/repo/view/view.go rename constants.go => command/report/constants.go (83%) rename git.go => command/report/git.go (99%) rename query.go => command/report/query.go (94%) create mode 100644 command/report/report.go rename {tests => command/report/tests}/dummy/python_coverage.xml (100%) rename {tests => command/report/tests}/dummy/report_graphql_error_response_body.json (100%) rename {tests => command/report/tests}/dummy/report_graphql_request_body.json (100%) rename {tests => command/report/tests}/dummy/report_graphql_success_response_body.json (100%) rename {tests => command/report/tests}/init_test.go (100%) rename {tests => command/report/tests}/report_workflow_test.go (95%) rename types.go => command/report/types.go (98%) create mode 100644 command/root.go create mode 100644 command/version/command.go create mode 100644 command/version/command_test.go create mode 100644 config/config.go create mode 100644 configvalidator/analyzer_config_validator.go create mode 100644 configvalidator/analyzer_config_validator_test.go create mode 100644 configvalidator/config_validator.go create mode 100644 configvalidator/config_validator_test.go create mode 100644 configvalidator/generic_config_validator.go create mode 100644 configvalidator/generic_config_validator_test.go create mode 100644 configvalidator/transformer_config_validator.go create mode 100644 configvalidator/transformer_config_validator_test.go create mode 100644 configvalidator/types.go create mode 100644 coverage.out create mode 100644 deepsource/analyzers/analyzers.go create mode 100644 deepsource/analyzers/queries/get_analyzers.go create mode 100644 deepsource/auth/device.go create mode 100644 deepsource/auth/jwt.go create mode 100644 deepsource/auth/mutations/refresh_creds.go create mode 100644 deepsource/auth/mutations/register_device.go create mode 100644 deepsource/auth/mutations/request_jwt.go create mode 100644 deepsource/client.go create mode 100644 deepsource/issues/issues_list.go create mode 100644 deepsource/issues/queries/list_file_issues.go create mode 100644 deepsource/issues/queries/list_issues.go create mode 100644 deepsource/repository/queries/repository_status.go create mode 100644 deepsource/repository/repository.go create mode 100644 deepsource/transformers/queries/get_transformers.go create mode 100644 deepsource/transformers/transformers.go delete mode 100644 init.go create mode 100644 utils/cmd_validator.go create mode 100644 utils/fetch_analyzers_transformers.go create mode 100644 utils/fetch_remote.go create mode 100644 utils/prompt.go create mode 100644 utils/remote_resolver.go create mode 100644 version/version.go create mode 100644 version/version_test.go diff --git a/.deepsource.toml b/.deepsource.toml index 49e0535f..86f09851 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -10,3 +10,11 @@ enabled = true [[analyzers]] name = "secrets" enabled = true + +[[analyzers]] +name = "test-coverage" +enabled = true + +[[transformers]] +name = "gofmt" +enabled = true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..bc5713ce --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: goreleaser + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + release-cli: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: master + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + distribution: goreleaser + version: latest + workdir: ./cmd/deepsource + args: release --rm-dist --config ../../goreleaser.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TOKEN: ${{ secrets.DS_BOT_PAT }} + DEEPSOURCE_CLI_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} diff --git a/.gitignore b/.gitignore index 718b10e5..2e7ae052 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ vendor # Executable -cli +cmd/deepsource/deepsource # Goreleaser artifacts dist + diff --git a/Makefile b/Makefile index 8fadf957..c3f98ad8 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,18 @@ build: - GOOS=linux GOARCH=amd64 go build -tags static_all -o /tmp/deepsource . + cd cmd/deepsource && GOOS=linux GOARCH=amd64 go build -tags static_all -o /tmp/deepsource . + +build_local: + cd cmd/deepsource && GOOS=darwin GOARCH=amd64 go build -tags static_all -o /tmp/deepsource . test: - CGO_ENABLED=0 go test -cover -coverprofile=coverage.out -v ./tests/... -run TestReportKeyValueWorkflow - CGO_ENABLED=0 go test --cover -coverprofile=coverage.out -v ./tests/... -run TestReportKeyValueFileWorkflow + CGO_ENABLED=0 go test --cover -coverprofile=coverage.out -v ./command/report/tests/... -run TestReportKeyValueWorkflow + CGO_ENABLED=0 go test --cover -coverprofile=coverage.out -v ./command/report/tests/... -run TestReportKeyValueFileWorkflow + echo "\n====TESTING CONFIG VALIDATOR PACKAGE====\n" + cd configvalidator && go test -v . -count=1 test_setup: mkdir -p ${CODE_PATH} cd ${CODE_PATH} && ls -A1 | xargs rm -rf git clone https://github.com/deepsourcelabs/cli ${CODE_PATH} chmod +x /tmp/deepsource - cp ./tests/dummy/python_coverage.xml /tmp + cp ./command/report/tests/dummy/python_coverage.xml /tmp diff --git a/cmd/deepsource/main.go b/cmd/deepsource/main.go new file mode 100644 index 00000000..b18b9d29 --- /dev/null +++ b/cmd/deepsource/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "log" + "os" + + "github.com/deepsourcelabs/cli/command" + v "github.com/deepsourcelabs/cli/version" + "github.com/getsentry/sentry-go" + "github.com/pterm/pterm" +) + +var ( + // Version is the build version. This is set using ldflags -X + version = "development" + + // Date is the build date. This is set using ldflags -X + Date = "YYYY-MM-DD" // YYYY-MM-DD + + // DSN used for sentry + SentryDSN string +) + +func main() { + + log.SetFlags(log.LstdFlags | log.Lshortfile) + + // Init sentry + err := sentry.Init(sentry.ClientOptions{ + Dsn: SentryDSN, + }) + if err != nil { + log.Println("Could not load sentry.") + } + v.SetBuildInfo(version, Date, "", "") + + if err := command.Execute(); err != nil { + // TODO: Handle exit codes here + pterm.Error.Println(err) + sentry.CaptureException(err) + os.Exit(1) + } +} diff --git a/command/auth/auth.go b/command/auth/auth.go new file mode 100644 index 00000000..182aaccf --- /dev/null +++ b/command/auth/auth.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/spf13/cobra" + + "github.com/deepsourcelabs/cli/command/auth/login" + "github.com/deepsourcelabs/cli/command/auth/logout" + "github.com/deepsourcelabs/cli/command/auth/refresh" + "github.com/deepsourcelabs/cli/command/auth/status" +) + +// Options holds the metadata. +type Options struct{} + +// NewCmdAuth handles the auth command which has various sub-commands like `login`, `logout`, `refresh` and `status` +func NewCmdAuth() *cobra.Command { + cmd := &cobra.Command{ + Use: "auth", + Short: "Authenticate with DeepSource", + } + cmd.AddCommand(login.NewCmdLogin()) + cmd.AddCommand(logout.NewCmdLogout()) + cmd.AddCommand(refresh.NewCmdRefresh()) + cmd.AddCommand(status.NewCmdStatus()) + return cmd +} diff --git a/command/auth/login/login.go b/command/auth/login/login.go new file mode 100644 index 00000000..ff28e4c6 --- /dev/null +++ b/command/auth/login/login.go @@ -0,0 +1,116 @@ +package login + +import ( + "fmt" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/utils" + "github.com/spf13/cobra" +) + +var accountTypes = []string{"DeepSource (deepsource.io)", "DeepSource Enterprise"} + +// LoginOptions hold the metadata related to login operation +type LoginOptions struct { + AuthTimedOut bool + TokenExpired bool + User string + HostName string + Interactive bool +} + +// NewCmdLogin handles the login functionality for the CLI +func NewCmdLogin() *cobra.Command { + + opts := LoginOptions{ + AuthTimedOut: false, + TokenExpired: true, + User: "", + HostName: "", + } + + cmd := &cobra.Command{ + Use: "login", + Short: "Login to DeepSource using Command Line Interface", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return opts.Run() + }, + } + + // --host, -h flag + cmd.Flags().StringVar(&opts.HostName, "hostname", "", "Authenticate with a specific DeepSource instance") + cmd.Flags().BoolVarP(&opts.Interactive, "interactive", "i", false, "Interactive login prompt for authenticating with DeepSource") + return cmd +} + +// Run executes the auth command and starts the login flow if not already authenticated +func (opts *LoginOptions) Run() (err error) { + + // Fetch config + cfg, _ := config.GetConfig() + opts.User = cfg.User + opts.TokenExpired = cfg.IsExpired() + + // Login using the interactive mode + if opts.Interactive { + err = opts.handleInteractiveLogin() + if err != nil { + return err + } + } + + // Checking if the user passed a hostname. If yes, storing it in the config + // Else using the default hostname (deepsource.io) + if opts.HostName != "" { + cfg.Host = opts.HostName + } else { + cfg.Host = config.DefaultHostName + } + + // Before starting the login workflow, check here for two conditions: + // Condition 1 : If the token has expired, display a message about it and re-authenticate user + // Condition 2 : If the token has not expired,does the user want to re-authenticate? + + // Checking for condition 1 + if !opts.TokenExpired { + // The user is already logged in, confirm re-authentication. + msg := fmt.Sprintf("You're already logged into DeepSource as %s. Do you want to re-authenticate?", opts.User) + response, err := utils.ConfirmFromUser(msg, "") + if err != nil { + return fmt.Errorf("Error in fetching response. Please try again.") + } + // If the response is No, it implies that the user doesn't want to re-authenticate + // In this case, just exit + if !response { + return nil + } + } + + // Condition 2 + // `startLoginFlow` implements the authentication flow for the CLI + return opts.startLoginFlow(cfg) +} + +func (opts *LoginOptions) handleInteractiveLogin() error { + // Prompt messages and help texts + loginPromptMessage := "Which account do you want to login into?" + loginPromptHelpText := "Select the type of account you want to authenticate" + hostPromptMessage := "Please enter the hostname:" + hostPromptHelpText := "The hostname of the DeepSource instance to authenticate with" + + // Display prompt to user + loginType, err := utils.SelectFromOptions(loginPromptMessage, loginPromptHelpText, accountTypes) + if err != nil { + return err + } + // Prompt the user for hostname only in the case of on-premise + if loginType == "DeepSource Enterprise" { + opts.HostName, err = utils.GetSingleLineInput(hostPromptMessage, hostPromptHelpText) + if err != nil { + return err + } + } + + return nil +} diff --git a/command/auth/login/login_flow.go b/command/auth/login/login_flow.go new file mode 100644 index 00000000..1fa49e45 --- /dev/null +++ b/command/auth/login/login_flow.go @@ -0,0 +1,117 @@ +package login + +import ( + "context" + "fmt" + "time" + + "github.com/cli/browser" + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/deepsource/auth" + "github.com/fatih/color" +) + +// Starts the login flow for the CLI +func (opts *LoginOptions) startLoginFlow(cfg *config.CLIConfig) error { + // Register the device and get a device code through the response + ctx := context.Background() + deviceRegistrationResponse, err := registerDevice(ctx) + if err != nil { + return err + } + + // Print the user code and the permission to open browser at verificationURI + c := color.New(color.FgCyan, color.Bold) + c.Printf("Please copy your one-time code: %s\n", deviceRegistrationResponse.UserCode) + c.Printf("Press enter to open deepsource.io in your browser...") + fmt.Scanln() + + // Having received the user code, open the browser at verificationURIComplete + err = browser.OpenURL(deviceRegistrationResponse.VerificationURIComplete) + if err != nil { + return err + } + + // Fetch the JWT using the device registration resonse + var jwtData *auth.JWT + jwtData, opts.AuthTimedOut, err = fetchJWT(ctx, deviceRegistrationResponse) + if err != nil { + return err + } + + // Check if it was a success poll or the Auth timed out + if opts.AuthTimedOut { + return fmt.Errorf("Authentication timed out") + } + + // Storing the useful data for future reference and usage + // in a global config object (Cfg) + cfg.User = jwtData.Payload.Email + cfg.Token = jwtData.Token + cfg.RefreshToken = jwtData.Refreshtoken + cfg.RefreshTokenExpiresIn = time.Unix(jwtData.RefreshExpiresIn, 0) + cfg.SetTokenExpiry(jwtData.Payload.Exp) + + // Having stored the data in the global Cfg object, write it into the config file present in the local filesystem + err = cfg.WriteFile() + if err != nil { + return fmt.Errorf("Error in writing authentication data to a file. Exiting...") + } + return nil +} + +func registerDevice(ctx context.Context) (*auth.Device, error) { + // Fetching DeepSource client in order to interact with SDK + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return nil, err + } + + // Send a mutation to register device and get the device code + res, err := deepsource.RegisterDevice(ctx) + if err != nil { + return nil, err + } + return res, nil +} + +func fetchJWT(ctx context.Context, deviceRegistrationData *auth.Device) (*auth.JWT, bool, error) { + var jwtData *auth.JWT + var err error + authTimedOut := true + + // Fetching DeepSource client in order to interact with SDK + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return nil, authTimedOut, err + } + + // Keep polling the mutation at a certain interval till the expiry timeperiod + ticker := time.NewTicker(time.Duration(deviceRegistrationData.Interval) * time.Second) + pollStartTime := time.Now() + + // Polling for fetching JWT + func() { + for range ticker.C { + jwtData, err = deepsource.Login(ctx, deviceRegistrationData.Code) + if err == nil { + authTimedOut = false + return + } + timeElapsed := time.Since(pollStartTime) + if timeElapsed >= time.Duration(deviceRegistrationData.ExpiresIn)*time.Second { + authTimedOut = true + return + } + } + }() + + return jwtData, authTimedOut, nil +} diff --git a/command/auth/logout/logout.go b/command/auth/logout/logout.go new file mode 100644 index 00000000..6d9e3e82 --- /dev/null +++ b/command/auth/logout/logout.go @@ -0,0 +1,56 @@ +package logout + +import ( + "errors" + "fmt" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/utils" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type LogoutOptions struct{} + +// NewCmdLogout handles the logout functionality for the CLI +func NewCmdLogout() *cobra.Command { + cmd := &cobra.Command{ + Use: "logout", + Short: "Logout of your active DeepSource account", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + opts := LogoutOptions{} + return opts.Run() + }, + } + return cmd +} + +func (opts *LogoutOptions) Run() error { + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + // Checking if the user has authenticated / logged in or not + if cfg.Token == "" { + return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.") + } + + // Confirm from the user if they want to logout + logoutConfirmationMsg := "Are you sure you want to log out of DeepSource account?" + response, err := utils.ConfirmFromUser(logoutConfirmationMsg, "") + if err != nil { + return err + } + + // If response is true, delete the config file => logged out the user + if response { + err := cfg.Delete() + if err != nil { + return err + } + } + pterm.Info.Println("Logged out from DeepSource (deepsource.io)") + return nil +} diff --git a/command/auth/refresh/refresh.go b/command/auth/refresh/refresh.go new file mode 100644 index 00000000..39d52c1f --- /dev/null +++ b/command/auth/refresh/refresh.go @@ -0,0 +1,82 @@ +package refresh + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/utils" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type RefreshOptions struct{} + +// NewCmdRefresh handles the refreshing of authentication credentials +func NewCmdRefresh() *cobra.Command { + opts := RefreshOptions{} + + cmd := &cobra.Command{ + Use: "refresh", + Short: "Refresh stored authentication credentials", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return opts.Run() + }, + } + return cmd +} + +func (opts *RefreshOptions) Run() error { + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + // Checking if the user has authenticated / logged in or not + if cfg.Token == "" { + return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.") + } + + // Check if token as well as refresh token are present since they + // are required for refreshing auth + if cfg.Token == "" || cfg.RefreshToken == "" { + // In case, there is no token as well as refresh token, ask the user to login instead + // TODO: Add ability to automatically run `login` command here + return fmt.Errorf("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.") + } + + // Fetching DS Client + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return err + } + ctx := context.Background() + // Use the SDK to fetch the new auth data + refreshedConfigData, err := deepsource.RefreshAuthCreds(ctx, cfg.RefreshToken) + if err != nil { + return err + } + + // Convert incoming config into the local CLI config format + cfg.User = refreshedConfigData.Payload.Email + cfg.Token = refreshedConfigData.Token + cfg.RefreshToken = refreshedConfigData.Refreshtoken + cfg.RefreshTokenExpiresIn = time.Unix(refreshedConfigData.RefreshExpiresIn, 0) + cfg.SetTokenExpiry(refreshedConfigData.Payload.Exp) + + // Having formatted the data, write it to the config file + err = cfg.WriteFile() + if err != nil { + fmt.Println("Error in writing authentication data to a file. Exiting...") + return err + } + pterm.Info.Println("Authentication successfully refreshed.") + return nil +} diff --git a/command/auth/status/status.go b/command/auth/status/status.go new file mode 100644 index 00000000..d9deacf8 --- /dev/null +++ b/command/auth/status/status.go @@ -0,0 +1,48 @@ +package status + +import ( + "errors" + "fmt" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/utils" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type AuthStatusOptions struct{} + +// NewCmdStatus handles the fetching of authentication status of CLI +func NewCmdStatus() *cobra.Command { + + cmd := &cobra.Command{ + Use: "status", + Short: "View the authentication status", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + opts := AuthStatusOptions{} + return opts.Run() + }, + } + return cmd +} + +func (opts *AuthStatusOptions) Run() error { + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + // Checking if the user has authenticated / logged in or not + if cfg.Token == "" { + return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.") + } + + // Check if the token has already expired + if !cfg.IsExpired() { + pterm.Info.Printf("Logged in to DeepSource as %s.\n", cfg.User) + } else { + pterm.Info.Println("The authentication has expired. Run \"deepsource auth refresh\" to refresh the credentials.") + } + return nil +} diff --git a/command/config/config.go b/command/config/config.go new file mode 100644 index 00000000..7aea4391 --- /dev/null +++ b/command/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "github.com/deepsourcelabs/cli/command/config/generate" + "github.com/deepsourcelabs/cli/command/config/validate" + "github.com/spf13/cobra" +) + +// Options holds the metadata. +type Options struct{} + +// NewCmdVersion returns the current version of cli being used +func NewCmdConfig() *cobra.Command { + cmd := &cobra.Command{ + Use: "config ", + Short: "Generate and Validate DeepSource config", + } + cmd.AddCommand(generate.NewCmdConfigGenerate()) + cmd.AddCommand(validate.NewCmdValidate()) + + return cmd +} diff --git a/command/config/generate/analyzers_input.go b/command/config/generate/analyzers_input.go new file mode 100644 index 00000000..d86d252b --- /dev/null +++ b/command/config/generate/analyzers_input.go @@ -0,0 +1,165 @@ +package generate + +import ( + "log" + + "github.com/Jeffail/gabs/v2" + "github.com/deepsourcelabs/cli/utils" +) + +// Struct to hold the data regarding the compulsary meta fields as required by analyzers +// Also, the userinput for that field +type AnalyzerMetadata struct { + FieldName string + Type string + Title string + Description string + Options []string + UserInput string +} + +// ========== +// Analyzers Input Prompt +// ========== +func (o *Options) collectAnalyzerInput() (err error) { + // Extracting languages and tools being used in the project for Analyzers + analyzerPromptMsg := "Which languages/tools does your project use?" + analyzerPromptHelpText := "Analyzers will find issues in your code. Add an analyzer by selecting a language you've written your code in." + + o.ActivatedAnalyzers, err = utils.SelectFromMultipleOptions(analyzerPromptMsg, analyzerPromptHelpText, utils.AnalyzersData.AnalyzerNames) + if err != nil { + return err + } + + // Extract the compulsary analyzer meta for analyzers + err = o.extractRequiredAnalyzerMetaFields() + if err != nil { + return err + } + + return nil +} + +// Checks if the field is present in the array containing list of `optional_required` +// analyzer meta fields +func isContains(requiredFieldsList []string, field string) bool { + for _, v := range requiredFieldsList { + if field == v { + return true + } + } + return false +} + +// Uses the `survey` prompt API to gather user input and store in `Options` struct +// `Options` struct is later used for config generation +func (o *Options) inputAnalyzerMeta(requiredFieldsData map[string][]AnalyzerMetadata) (err error) { + // Iterate over the map and fetch the input for the fields from the user + for analyzer, metaFields := range requiredFieldsData { + for i := 0; i < len(metaFields); i++ { + switch metaFields[i].Type { + case "boolean": + metaFields[i].UserInput = "true" + res, err := utils.ConfirmFromUser(metaFields[i].Title, metaFields[i].Description) + if err != nil { + return err + } + if !res { + metaFields[i].UserInput = "false" + } + case "enum": + metaFields[i].UserInput, err = utils.SelectFromOptions(metaFields[i].Title, metaFields[i].Description, metaFields[i].Options) + if err != nil { + return err + } + default: + metaFields[i].UserInput, err = utils.GetSingleLineInput(metaFields[i].Title, metaFields[i].Description) + if err != nil { + return err + } + } + } + requiredFieldsData[analyzer] = metaFields + } + o.AnalyzerMetaMap = requiredFieldsData + return nil +} + +// Extracts the fields that are compulsary according to the meta schema and require input +func populateMetadata(optionalFields []string, jsonParsed *gabs.Container) []AnalyzerMetadata { + requiredFieldsData := make([]AnalyzerMetadata, 0) + + // Iterate through the properties using the parsed json (jsonParsed) and extract the data of the + // required analyzer meta fields + for key, child := range jsonParsed.Search("properties").ChildrenMap() { + if !isContains(optionalFields, key) { + continue + } + propertyJSON, err := gabs.ParseJSON(child.Bytes()) + if err != nil { + log.Printf("Error occured while parsing analyzer meta property: %v\n", err) + continue + } + + individualFieldRequiredData := AnalyzerMetadata{ + FieldName: key, + Type: propertyJSON.Search("type").Data().(string), + Title: propertyJSON.Search("title").Data().(string), + Description: propertyJSON.Search("description").Data().(string), + } + + // Check for enum property + for _, child := range propertyJSON.Search("enum").Children() { + individualFieldRequiredData.Options = append(individualFieldRequiredData.Options, child.Data().(string)) + individualFieldRequiredData.Type = "enum" + } + + // Check for items property + itemsPath := propertyJSON.Path("items") + itemsJSON, _ := gabs.ParseJSON(itemsPath.Bytes()) + for _, child := range itemsJSON.Search("enum").Children() { + individualFieldRequiredData.Options = append(individualFieldRequiredData.Options, child.Data().(string)) + individualFieldRequiredData.Type = "enum" + } + requiredFieldsData = append(requiredFieldsData, individualFieldRequiredData) + } + return requiredFieldsData +} + +// The primary function to parse the API response of meta schema and filter out the `optional_required` fields +// Calls helper functions (mentioned above) to perform the required meta data extraction +// and handling prompt for inputting these fields +func (o *Options) extractRequiredAnalyzerMetaFields() error { + var optionalFields []string + var requiredMetaData []AnalyzerMetadata + var analyzerFieldsData = make(map[string][]AnalyzerMetadata) + + // Extract `optional_required` fields of analyzer meta of selected analyzers + for _, activatedAnalyzer := range o.ActivatedAnalyzers { + analyzerShortcode := utils.AnalyzersData.AnalyzersMap[activatedAnalyzer] + // Assigning optional fields to nil before checking for an analyzer + optionalFields = nil + requiredMetaData = nil + + analyzerMeta := utils.AnalyzersData.AnalyzersMetaMap[analyzerShortcode] + // Parse the analyzer meta of the analyzer using `gabs` + jsonParsed, err := gabs.ParseJSON([]byte(analyzerMeta)) + if err != nil { + log.Printf("Error occured while parsing meta for %s analyzer.\n", activatedAnalyzer) + return err + } + + // Search for "optional_required" fields in the meta-schema + for _, child := range jsonParsed.Search("optional_required").Children() { + optionalFields = append(optionalFields, child.Data().(string)) + } + // Move on to next analyzer if no "optional_required" fields found + if optionalFields == nil { + continue + } + // Extract the the data to be input for all the required analyzer meta properties + requiredMetaData = populateMetadata(optionalFields, jsonParsed) + analyzerFieldsData[activatedAnalyzer] = requiredMetaData + } + return o.inputAnalyzerMeta(analyzerFieldsData) +} diff --git a/command/config/generate/constants.go b/command/config/generate/constants.go new file mode 100644 index 00000000..96475763 --- /dev/null +++ b/command/config/generate/constants.go @@ -0,0 +1,3 @@ +package generate + +const DEEPSOURCE_TOML_VERSION = 1 diff --git a/command/config/generate/generate.go b/command/config/generate/generate.go new file mode 100644 index 00000000..0f4617a5 --- /dev/null +++ b/command/config/generate/generate.go @@ -0,0 +1,148 @@ +package generate + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/utils" + "github.com/fatih/color" + toml "github.com/pelletier/go-toml" + "github.com/spf13/cobra" +) + +// Options holds the metadata. +type Options struct { + ActivatedAnalyzers []string // Analyzers activated by user + AnalyzerMetaMap map[string][]AnalyzerMetadata + ActivatedTransformers []string // Transformers activated by the user + ExcludePatterns []string + TestPatterns []string + GeneratedConfig string +} + +// NewCmdConfigGenerate handles the generation of DeepSource config based on user inputs +func NewCmdConfigGenerate() *cobra.Command { + o := Options{} + + cmd := &cobra.Command{ + Use: "generate", + Short: "Generate config for DeepSource", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return o.Run() + }, + } + return cmd +} + +// Run executes the command. +func (o *Options) Run() error { + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + err = cfg.VerifyAuthentication() + if err != nil { + return err + } + + // Step 1: Collect user input + err = o.collectUserInput() + if err != nil { + fmt.Println("\nError occured while collecting input.Exiting...") + return err + } + + // Step 2: Generates config based on user input + err = o.generateDeepSourceConfig() + if err != nil { + fmt.Println("\nError occured while generating config from input.Exiting...") + return err + } + + // Step 3: Write the generated config to a file + err = o.writeConfigToFile() + if err != nil { + fmt.Println("\nError while writing config to project directory. Exiting...") + return err + } + + // Step 4: If everything is successfull, print the success message + cwd, _ := os.Getwd() + c := color.New(color.FgGreen) + successOutput := fmt.Sprintf("\nSuccessfully generated DeepSource config file at %s/.deepsource.toml", cwd) + c.Println(successOutput) + + return nil +} + +// Generates DeepSource config based on the inputs from the user in Options struct +func (o *Options) generateDeepSourceConfig() error { + // Copying version, exclude_patterns and test_patterns into the DSConfig based structure + config := DSConfig{ + Version: DEEPSOURCE_TOML_VERSION, + ExcludePatterns: o.ExcludePatterns, + TestPatterns: o.TestPatterns, + } + + // Copying activated analyzers from Options struct to DSConfig based "config" struct + for _, analyzer := range o.ActivatedAnalyzers { + // Configuring the analyzer meta data + metaMap := make(map[string]interface{}) + if o.AnalyzerMetaMap[analyzer] != nil { + for _, meta := range o.AnalyzerMetaMap[analyzer] { + metaMap[meta.FieldName] = meta.UserInput + } + } + + activatedAnalyzerData := Analyzer{ + Name: utils.AnalyzersData.AnalyzersMap[analyzer], + Enabled: true, + } + if len(metaMap) != 0 { + activatedAnalyzerData.Meta = metaMap + } + config.Analyzers = append(config.Analyzers, activatedAnalyzerData) + } + + // Copying activated transformers from Options struct to DSConfig based "config" struct + for _, transformer := range o.ActivatedTransformers { + config.Transformers = append(config.Transformers, Transformer{ + Name: utils.TransformersData.TransformerMap[transformer], + Enabled: true, + }) + } + + // Encoding the DSConfig based "config" struct to TOML + // and storing in GeneratedConfig of Options struct + var buf bytes.Buffer + err := toml.NewEncoder(&buf).Order(toml.OrderPreserve).Encode(config) + if err != nil { + return err + } + // Convert the TOML encoded buffer to string + o.GeneratedConfig = buf.String() + return nil +} + +// Writes the generated TOML config into a file +func (o *Options) writeConfigToFile() error { + // Creating file + cwd, _ := os.Getwd() + f, err := os.Create(filepath.Join(cwd, ".deepsource.toml")) + if err != nil { + return err + } + defer f.Close() + + // Writing the string to the file + _, writeError := f.WriteString(o.GeneratedConfig) + if writeError != nil { + return writeError + } + return nil +} diff --git a/command/config/generate/generic_input.go b/command/config/generate/generic_input.go new file mode 100644 index 00000000..bb147319 --- /dev/null +++ b/command/config/generate/generic_input.go @@ -0,0 +1,116 @@ +package generate + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/AlecAivazis/survey/v2" + "github.com/deepsourcelabs/cli/utils" +) + +// ========== +// Exclude Patterns Input Prompt +// ========== +func (o *Options) collectExcludePatterns() error { + excludePatternsMsg := "Would you like to add any exclude patterns?" + helpMsg := "Glob patterns of files that should not be analyzed such as auto-generated files, migrations, compatibility files." + + // Confirm from the user if they want to add an exclude pattern + response, err := utils.ConfirmFromUser(excludePatternsMsg, helpMsg) + if err != nil { + return err + } + + // If yes, keep entering patterns until they input n/N + if response { + err := o.inputFilePatterns("exclude", "Select exclude pattern", helpMsg) + if err != nil { + return err + } + } + return nil +} + +// ========== +// Test Patterns Input Prompt +// ========== +func (o *Options) collectTestPatterns() error { + testPatternsMsg := "Would you like to add any test patterns?" + helpMsg := "Glob patterns of the test files. This helps us reduce false positives." + + // Confirm from the user (y/N) if he/she wants to add test patterns + response, err := utils.ConfirmFromUser(testPatternsMsg, helpMsg) + if err != nil { + return err + } + + // If yes, keep entering patterns until they input n/N + if response { + err := o.inputFilePatterns("test", "Select test pattern", helpMsg) + if err != nil { + return err + } + } + return nil +} + +// Single utility function to help in inputting test as well as exclude patterns +// Keeps asking user for pattern and then confirms if they want to add more patterns +// Exits when user enters No (n/N) +func (o *Options) inputFilePatterns(field, msg, helpMsg string) error { + // Infinite loop to keep running until user wants to stop inputting + for { + var filePattern string + + // Input the pattern + filePatternsPrompt := &survey.Input{ + Renderer: survey.Renderer{}, + Message: msg, + Default: "", + Help: helpMsg, + Suggest: getMatchingFiles, + } + err := survey.AskOne(filePatternsPrompt, &filePattern) + if err != nil { + return err + } + + // Having taken the input of exclude_patterns/test_pattern, append it to the Options struct + if field == "test" { + o.TestPatterns = append(o.TestPatterns, filePattern) + } else { + o.ExcludePatterns = append(o.ExcludePatterns, filePattern) + } + + // Confirm from the user if the user wants to add more patterns + // Iterating this until user says no + // Here field contains : "test"/"exclude" depending upon the invoking + confirmationMsg := fmt.Sprintf("Add more %s patterns?", field) + response, err := utils.ConfirmFromUser(confirmationMsg, "") + if err != nil { + return err + } + if !response { + break + } + } + return nil +} + +// Receives a filepath and returns matching dirs and files +// Used for autocompleting input of "exclude_patterns" and "test_patterns" +func getMatchingFiles(path string) []string { + // Geting matching dirs and files using glob + files, _ := filepath.Glob(path + "*") + cwd, _ := os.Getwd() + + // Iterating over files and appending "/" to directories + for index, file := range files { + fileInfo, _ := os.Stat(filepath.Join(cwd, file)) + if fileInfo.IsDir() { + files[index] = files[index] + "/" + } + } + return files +} diff --git a/command/config/generate/input.go b/command/config/generate/input.go new file mode 100644 index 00000000..cc44df04 --- /dev/null +++ b/command/config/generate/input.go @@ -0,0 +1,51 @@ +package generate + +import ( + "context" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/utils" +) + +// Responsible for collecting user input for generating DeepSource config +func (o *Options) collectUserInput() error { + + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return err + } + ctx := context.Background() + + // Get the list of analyzers and transformers supported by DeepSource + err = utils.GetAnalyzersAndTransformersData(ctx, *deepsource) + if err != nil { + return err + } + + // Get input for analyzers to be activated + err = o.collectAnalyzerInput() + if err != nil { + return err + } + + err = o.collectTransformersInput() + if err != nil { + return err + } + + err = o.collectExcludePatterns() + if err != nil { + return err + } + + err = o.collectTestPatterns() + if err != nil { + return err + } + + return nil +} diff --git a/command/config/generate/transformers_input.go b/command/config/generate/transformers_input.go new file mode 100644 index 00000000..6e5a2468 --- /dev/null +++ b/command/config/generate/transformers_input.go @@ -0,0 +1,18 @@ +package generate + +import "github.com/deepsourcelabs/cli/utils" + +// ========== +// Transformers Input Prompt +// ========== +func (o *Options) collectTransformersInput() (err error) { + transformerPromptMsg := "Would you like to activate any Transformers for any languages?" + transformerPromptHelpText := "DeepSource Transformers automatically help to achieve auto-formatting of code. Add a transformer by selecting the code formatting tool of your choice." + + o.ActivatedTransformers, err = utils.SelectFromMultipleOptions(transformerPromptMsg, transformerPromptHelpText, utils.TransformersData.TransformerNames) + if err != nil { + return err + } + + return nil +} diff --git a/command/config/generate/types.go b/command/config/generate/types.go new file mode 100644 index 00000000..a60d7a19 --- /dev/null +++ b/command/config/generate/types.go @@ -0,0 +1,24 @@ +package generate + +// DSConfig is the struct for .deepsource.toml file +type Analyzer struct { + Name string `toml:"name,omitempty" json:"name,omitempty"` + RuntimeVersion string `toml:"runtime_version,omitempty" json:"runtime_version,omitempty"` + Enabled bool `toml:"enabled" json:"enabled"` + DependencyFilePaths []string `toml:"dependency_file_paths,omitempty" json:"dependency_file_paths,omitempty"` + Meta interface{} `toml:"meta,omitempty" json:"meta,omitempty"` + Thresholds interface{} `toml:"thresholds,omitempty" json:"thresholds,omitempty"` +} + +type Transformer struct { + Name string `toml:"name" json:"name"` + Enabled bool `toml:"enabled" json:"enabled"` +} + +type DSConfig struct { + Version int `toml:"version" json:"version"` + ExcludePatterns []string `toml:"exclude_patterns" json:"exclude_patterns,omitempty"` + TestPatterns []string `toml:"test_patterns" json:"test_patterns,omitempty"` + Analyzers []Analyzer `toml:"analyzers,omitempty" json:"analyzers,omitempty"` + Transformers []Transformer `toml:"transformers,omitempty" json:"transformers,omitempty"` +} diff --git a/command/config/validate/validate.go b/command/config/validate/validate.go new file mode 100644 index 00000000..99a65132 --- /dev/null +++ b/command/config/validate/validate.go @@ -0,0 +1,202 @@ +package validate + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/configvalidator" + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/utils" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// Options holds the metadata. +type Options struct{} + +// NewCmdValidate handles the validation of the DeepSource config (.deepsource.toml) +// Internally it uses the package `configvalidator` to validate the config +func NewCmdValidate() *cobra.Command { + + o := Options{} + cmd := &cobra.Command{ + Use: "validate", + Short: "Validate DeepSource config", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return o.Run() + }, + } + return cmd +} + +// Run executes the command. +func (o *Options) Run() error { + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + err = cfg.VerifyAuthentication() + if err != nil { + return err + } + + // Just an info + pterm.Info.Println("DeepSource config (.deepsource.toml) is mostly present in the root directory of the project.") + fmt.Println() + + // Extract the path of DeepSource config + configPath, err := extractDSConfigPath() + if err != nil { + return err + } + + // Read the config in the form of string and send it + content, err := ioutil.ReadFile(configPath) + if err != nil { + return errors.New("Error occured while reading DeepSource config file. Exiting...") + } + + // Fetch the client + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return err + } + ctx := context.Background() + // Fetch the list of supported analyzers and transformers' data + // using the SDK + err = utils.GetAnalyzersAndTransformersData(ctx, *deepsource) + if err != nil { + return err + } + + // Create an instance of ConfigValidator struct + var validator configvalidator.ConfigValidator + // Send the config contents to get validated + var result configvalidator.Result = validator.ValidateConfig(content) + + // Checking for all types of errors (due to viper/valid errors/no errors) + // and handling them + if result.ConfigReadError { + // handle printing viper error here + printViperError(content, result.Errors) + } else if !result.Valid { + // handle printing other errors here + printConfigErrors(result.Errors) + } else { + printValidConfig() + } + + return nil +} + +// Extracts the path of DeepSource config (.deepsource.toml) in the user repo +// Checks in the current working directory as well as the root directory +// of the project +func extractDSConfigPath() (string, error) { + var configPath string + + // Get current working directory of user from where this command is run + cwd, err := os.Getwd() + if err != nil { + return "", errors.New("Error occured while fetching current working directory. Exiting...") + } + + // Form the full path of cwd to search for .deepsource.toml + configPath = filepath.Join(cwd, ".deepsource.toml") + + // Check if there is a deepsource.toml file here + if _, err = os.Stat(configPath); err != nil { + // Since, no .deepsource.toml in the cwd, + // fetching the top level directory + output, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + return "", err + } + + // Removing trailing null characters + path := strings.TrimRight(string(output), "\000\n") + + // Check if the config exists on this path + if _, err = os.Stat(filepath.Join(path, ".deepsource.toml")); err != nil { + return "", errors.New("Error occured while looking for DeepSource config file. Exiting...") + } else { + // If found, use this as configpath + configPath = filepath.Join(path, "/.deepsource.toml") + } + } + return configPath, nil +} + +// Handles printing the output when viper fails to read TOML file due to bad syntax +func printViperError(fileContent []byte, errors []string) { + var errorString string + var errorLine int + + // Parsing viper error output and finding at which line bad syntax is present in + // DeepSource config TOML file + for _, error := range errors { + stripString1 := strings.Split(error, ": ") + errorString = stripString1[2] + errorLine, _ = strconv.Atoi(strings.Trim(strings.Split(stripString1[1], ", ")[0], "(")) + } + + // Read .deepsource.toml line by line and store in a var + lineText := strings.Split(string(fileContent), "\n") + fileLength := len(lineText) + + // Print error message + pterm.Error.WithShowLineNumber(false).Printf("Error while reading config : %s\n", errorString) + pterm.Println() + + // Preparing codeframe to show exactly at which line bad syntax is present in TOML file + if errorLine > 2 && errorLine+2 <= fileLength { + for i := errorLine - 2; i <= errorLine+2; i++ { + if i == errorLine { + errorStr := "" + if i >= 10 { + errorStr = fmt.Sprintf("> %d | %s", i, lineText[i-1]) + } else { + errorStr = fmt.Sprintf("> %d | %s", i, lineText[i-1]) + } + pterm.NewStyle(pterm.FgLightRed).Println(errorStr) + } else { + errorStr := "" + if i >= 10 { + errorStr = fmt.Sprintf(" %d | %s", i, lineText[i-1]) + } else { + errorStr = fmt.Sprintf(" %d | %s", i, lineText[i-1]) + } + pterm.NewStyle(pterm.FgLightYellow).Println(errorStr) + + } + } + } else { + errorStr := fmt.Sprintf("> %d | %s", errorLine, lineText[errorLine-1]) + pterm.NewStyle(pterm.FgLightRed).Println(errorStr) + } +} + +// Handles printing the errors in the DeepSource config (.deepsource.toml) +func printConfigErrors(errors []string) { + for _, error := range errors { + pterm.Error.WithShowLineNumber(false).Println(error) + } +} + +// Handles printing the valid config output +func printValidConfig() { + pterm.Success.Println("Config Valid") +} diff --git a/command/issues/issues.go b/command/issues/issues.go new file mode 100644 index 00000000..2d383b5d --- /dev/null +++ b/command/issues/issues.go @@ -0,0 +1,20 @@ +package issues + +import ( + "github.com/spf13/cobra" + + "github.com/deepsourcelabs/cli/command/issues/list" +) + +// Options holds the metadata. +type Options struct{} + +// NewCmdVersion returns the current version of cli being used +func NewCmdIssues() *cobra.Command { + cmd := &cobra.Command{ + Use: "issues", + Short: "Show the list of issues in a file in a repository", + } + cmd.AddCommand(list.NewCmdIssuesList()) + return cmd +} diff --git a/command/issues/list/list.go b/command/issues/list/list.go new file mode 100644 index 00000000..0ec1e3a8 --- /dev/null +++ b/command/issues/list/list.go @@ -0,0 +1,137 @@ +package list + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/deepsource/issues" + "github.com/deepsourcelabs/cli/utils" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +const MAX_ISSUE_LIMIT = 100 + +type IssuesListOptions struct { + FileArg string + RepoArg string + LimitArg int + SelectedRemote *utils.RemoteData + issuesData []issues.Issue + ptermTable [][]string +} + +func NewCmdIssuesList() *cobra.Command { + + opts := IssuesListOptions{ + FileArg: "", + RepoArg: "", + LimitArg: 30, + } + + cmd := &cobra.Command{ + Use: "list", + Short: "List issues reported by DeepSource", + Args: utils.MaxNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + opts.FileArg = args[0] + } + return opts.Run() + }, + } + + // --repo, -r flag + cmd.Flags().StringVarP(&opts.RepoArg, "repo", "r", "", "List the issues of the specified repository") + + // --limit, -l flag + cmd.Flags().IntVarP(&opts.LimitArg, "limit", "l", 30, "Fetch the issues upto the specified limit") + return cmd +} + +// Execute the command +func (opts *IssuesListOptions) Run() (err error) { + + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + err = cfg.VerifyAuthentication() + if err != nil { + return err + } + + // The current limit of querying issues at once is 100. + // If the limit passed by user is greater than 100, exit + // with an error message + if opts.LimitArg > MAX_ISSUE_LIMIT { + return fmt.Errorf("The maximum allowed limit to fetch issues is 100. Found %d", opts.LimitArg) + } + + // Get the remote repository URL for which issues have to + // be listed + opts.SelectedRemote, err = utils.ResolveRemote(opts.RepoArg) + if err != nil { + return err + } + + // Fetch the list of issues using SDK based on user input + ctx := context.Background() + return opts.getIssuesData(ctx) +} + +// Gets the data about issues using the SDK based on the user input +// i.e for a single file or for the whole project +func (opts *IssuesListOptions) getIssuesData(ctx context.Context) (err error) { + // Get the deepsource client for using the issue fetching SDK to fetch the list of issues + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return err + } + + // Fetch issues for a certain FileArg (filepath) passed by the user + // Example: `deepsource issues list api/hello.py` + if opts.FileArg != "" { + opts.issuesData, err = deepsource.GetIssuesForFile(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider, opts.FileArg, opts.LimitArg) + if err != nil { + return err + } + opts.showIssues() + return nil + } + + // Fetch list of issues for the whole project + opts.issuesData, err = deepsource.GetIssues(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider, opts.LimitArg) + if err != nil { + return err + } + opts.showIssues() + return nil +} + +// Parses the SDK response and formats the data in the form of a TAB separated table +// and renders it using pterm +func (opts *IssuesListOptions) showIssues() { + // A 2d array to contain list of issues details arrays + opts.ptermTable = make([][]string, len(opts.issuesData)) + + // Curating the data and appending to the 2d array + for index, issue := range opts.issuesData { + filePath := issue.Location.Path + beginLine := issue.Location.Position.BeginLine + issueLocation := fmt.Sprintf("%s:%d", filePath, beginLine) + analyzerShortcode := issue.Analyzer.Shortcode + issueCode := issue.IssueCode + issueTitle := issue.IssueText + + opts.ptermTable[index] = []string{issueLocation, analyzerShortcode, issueCode, issueTitle} + } + // Using pterm to render the list of list + pterm.DefaultTable.WithSeparator("\t").WithData(opts.ptermTable).Render() +} diff --git a/command/repo/repo.go b/command/repo/repo.go new file mode 100644 index 00000000..52223fc5 --- /dev/null +++ b/command/repo/repo.go @@ -0,0 +1,22 @@ +package repo + +import ( + "github.com/spf13/cobra" + + "github.com/deepsourcelabs/cli/command/repo/status" + "github.com/deepsourcelabs/cli/command/repo/view" +) + +// Options holds the metadata. +type Options struct{} + +// NewCmdVersion returns the current version of cli being used +func NewCmdRepo() *cobra.Command { + cmd := &cobra.Command{ + Use: "repo", + Short: "Operations related to the project repository", + } + cmd.AddCommand(status.NewCmdRepoStatus()) + cmd.AddCommand(view.NewCmdRepoView()) + return cmd +} diff --git a/command/repo/status/status.go b/command/repo/status/status.go new file mode 100644 index 00000000..e1458234 --- /dev/null +++ b/command/repo/status/status.go @@ -0,0 +1,80 @@ +package status + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/utils" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type RepoStatusOptions struct { + RepoArg string + TokenExpired bool + SelectedRemote *utils.RemoteData +} + +// NewCmdRepoStatus handles querying the activation status of the repo supplied as an arg +func NewCmdRepoStatus() *cobra.Command { + + opts := RepoStatusOptions{ + RepoArg: "", + TokenExpired: config.Cfg.IsExpired(), + } + + cmd := &cobra.Command{ + Use: "status", + Short: "Refresh stored authentication credentials", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return opts.Run() + }, + } + + // --repo, -r flag + cmd.Flags().StringVarP(&opts.RepoArg, "repo", "r", "", "Get the activation status of the specified repository") + return cmd +} + +func (opts *RepoStatusOptions) Run() (err error) { + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + err = cfg.VerifyAuthentication() + if err != nil { + return err + } + + // Get the remote repository URL for which issues have to + // be listed + opts.SelectedRemote, err = utils.ResolveRemote(opts.RepoArg) + if err != nil { + return err + } + // Use the SDK to find the activation status + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return err + } + ctx := context.Background() + statusResponse, err := deepsource.GetRepoStatus(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider) + if err != nil { + return err + } + + // Check response and show corresponding output + if statusResponse.Activated { + pterm.Info.Println("Analysis active on DeepSource (deepsource.io)") + } else { + pterm.Info.Println("DeepSource analysis is currently not actived on this repository.") + } + return nil +} diff --git a/command/repo/view/view.go b/command/repo/view/view.go new file mode 100644 index 00000000..33a92d64 --- /dev/null +++ b/command/repo/view/view.go @@ -0,0 +1,96 @@ +package view + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/cli/browser" + "github.com/deepsourcelabs/cli/config" + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/utils" + "github.com/spf13/cobra" +) + +var VCSMap = map[string]string{ + "GITHUB": "gh", + "GITLAB": "gl", + "BITBUCKET": "bb", +} + +type RepoViewOptions struct { + RepoArg string + TokenExpired bool + SelectedRemote *utils.RemoteData +} + +func NewCmdRepoView() *cobra.Command { + + opts := RepoViewOptions{ + RepoArg: "", + SelectedRemote: &utils.RemoteData{}, + } + + cmd := &cobra.Command{ + Use: "view", + Short: "Open the DeepSource dashboard of a repository", + Args: utils.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return opts.Run() + }, + } + + // --repo, -r flag + cmd.Flags().StringVarP(&opts.RepoArg, "repo", "r", "", "Open the DeepSource dashboard of the specified repository") + return cmd +} + +func (opts *RepoViewOptions) Run() (err error) { + // Fetch config + cfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("Error while reading DeepSource CLI config : %v", err) + } + err = cfg.VerifyAuthentication() + if err != nil { + return err + } + + // Get the remote repository URL for which issues have to + // be listed + opts.SelectedRemote, err = utils.ResolveRemote(opts.RepoArg) + if err != nil { + return err + } + + // Making the "isActivated" (repo status) query again just to confirm if the user has access to that repo + deepsource, err := deepsource.New(deepsource.ClientOpts{ + Token: config.Cfg.Token, + HostName: config.Cfg.Host, + }) + if err != nil { + return err + } + ctx := context.Background() + _, err = deepsource.GetRepoStatus(ctx, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName, opts.SelectedRemote.VCSProvider) + if err != nil { + if strings.Contains(err.Error(), "Signature has expired") { + return errors.New("The token has expired. Please refresh the token using the command `deepsource auth refresh`") + } + + if strings.Contains(err.Error(), "Repository matching query does not exist") { + return errors.New("Unauthorized access. Please login if you haven't using the command `deepsource auth login`") + } + } + + // If the user has access to repo, frame the full URL of the repo and open it on the + // default browser + VCSShortcode := VCSMap[opts.SelectedRemote.VCSProvider] + + // Framing the complete URL + dashboardURL := fmt.Sprintf("https://%s/%s/%s/%s/", config.Cfg.Host, VCSShortcode, opts.SelectedRemote.Owner, opts.SelectedRemote.RepoName) + fmt.Printf("Press Enter to open %s in your browser...", dashboardURL) + fmt.Scanln() + return browser.OpenURL(dashboardURL) +} diff --git a/constants.go b/command/report/constants.go similarity index 83% rename from constants.go rename to command/report/constants.go index f2736846..946d000c 100644 --- a/constants.go +++ b/command/report/constants.go @@ -1,17 +1,14 @@ -package main +package report const ( cliVersion = "v0.1.6" commonUsageMessage = ` Usage: deepsource [] - Available commands are: report Report an artifact to analyzer - Help: Use 'deepsource --help' for more information about the command. - Documentation: https://deepsource.io/docs/cli ` @@ -19,22 +16,19 @@ Documentation: reportUsageMessage = ` Usage: deepsource report [] - Available arguments are: --analyzer Shortcode of the analyzer --key Name of the artifact --value Value of the artifact --value-file Path to the artifact value file - Examples: deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml deepsource report --analyzer git --key lines-changed --value 22 - Notes: - Pass either '--value' or '--value-file'. If both are passed, contents of '--value' will be considered. - Documentation: https://deepsource.io/docs/cli#report ` - reportGraphqlQuery = "mutation($input: CreateArtifactInput!) {\r\n createArtifact(input: $input) {\r\n ok\r\n error\r\n }\r\n}" + reportGraphqlQuery = "mutation($input: CreateArtifactInput!) {\r\n createArtifact(input: $input) {\r\n ok\r\n error\r\n }\r\n}" + maxArtifactUploadSize = 20000000 ) diff --git a/git.go b/command/report/git.go similarity index 99% rename from git.go rename to command/report/git.go index 50fc5886..3d43c7ab 100644 --- a/git.go +++ b/command/report/git.go @@ -1,4 +1,4 @@ -package main +package report import ( "bytes" diff --git a/query.go b/command/report/query.go similarity index 94% rename from query.go rename to command/report/query.go index 027564c3..739463c4 100644 --- a/query.go +++ b/command/report/query.go @@ -1,4 +1,4 @@ -package main +package report import ( "bytes" @@ -14,7 +14,7 @@ func makeQuery(url string, body []byte, bodyMimeType string) ([]byte, error) { var resBody []byte httpClient := &http.Client{ - Timeout: time.Second * 10, + Timeout: time.Second * 60, } req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body)) diff --git a/command/report/report.go b/command/report/report.go new file mode 100644 index 00000000..725d5e89 --- /dev/null +++ b/command/report/report.go @@ -0,0 +1,258 @@ +package report + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/deepsourcelabs/cli/utils" + "github.com/getsentry/sentry-go" + "github.com/spf13/cobra" +) + +type ReportOptions struct { + Analyzer string + Key string + Value string + ValueFile string +} + +// NewCmdVersion returns the current version of cli being used +func NewCmdReport() *cobra.Command { + opts := ReportOptions{} + + cmd := &cobra.Command{ + Use: "report", + Short: "Report artifacts to DeepSource", + Args: utils.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + returnCode := opts.Run() + defer os.Exit(returnCode) + }, + } + + // --repo, -r flag + cmd.Flags().StringVar(&opts.Analyzer, "analyzer", "", "The analyzer shortcode whose test coverage is being reported") + + cmd.Flags().StringVar(&opts.Key, "key", "", "The artifact being reported.tcv for test-coverage") + + cmd.Flags().StringVar(&opts.ValueFile, "value-file", "", "Path to the value file") + + cmd.Flags().StringVar(&opts.Value, "value", "", "Value of the artifact") + + return cmd +} + +func (opts *ReportOptions) Run() int { + // Verify the env variables + dsn := os.Getenv("DEEPSOURCE_DSN") + if dsn == "" { + fmt.Println("DeepSource | Error | Environment variable DEEPSOURCE_DSN not set (or) is empty. You can find it under the repository settings page.") + return 1 + } + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetUser(sentry.User{ID: dsn}) + }) + + ///////////////////// + // Command: report // + ///////////////////// + + reportCommandAnalyzerShortcode := opts.Analyzer + reportCommandKey := opts.Key + reportCommandValue := opts.Value + reportCommandValueFile := opts.ValueFile + + // Get current path + currentDir, err := os.Getwd() + if err != nil { + fmt.Println("DeepSource | Error | Unable to identify current directory.") + sentry.CaptureException(err) + return 0 + } + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetExtra("currentDir", currentDir) + }) + + ////////////////// + // Validate DSN // + ////////////////// + + // Protocol + dsnSplitProtocolBody := strings.Split(dsn, "://") + + // Validate DSN parsing + if len(dsnSplitProtocolBody) != 2 { + err = errors.New("DeepSource | Error | Invalid DSN. Cross verify DEEPSOURCE_DSN value against the settings page of the repository.") + fmt.Println(err) + sentry.CaptureException(err) + return 1 + } + + // Check for valid protocol + if !strings.HasPrefix(dsnSplitProtocolBody[0], "http") { + err = errors.New("DeepSource | Error | DSN specified should start with http(s). Cross verify DEEPSOURCE_DSN value against the settings page of the repository.") + fmt.Println(err) + sentry.CaptureException(err) + return 1 + } + dsnProtocol := dsnSplitProtocolBody[0] + + // Parse body of the DSN + dsnSplitTokenHost := strings.Split(dsnSplitProtocolBody[1], "@") + + // Validate DSN parsing + if len(dsnSplitTokenHost) != 2 { + err = errors.New("DeepSource | Error | Invalid DSN. Cross verify DEEPSOURCE_DSN value against the settings page of the repository.") + fmt.Println(err) + sentry.CaptureException(err) + return 1 + } + + // Set values parsed from DSN + dsnHost := dsnSplitTokenHost[1] + + /////////////////////// + // Generate metadata // + /////////////////////// + + // Access token + dsnAccessToken := dsnSplitTokenHost[0] + + // Head Commit OID + headCommitOID, err := gitGetHead(currentDir) + if err != nil { + fmt.Println("DeepSource | Error | Unable to get commit OID HEAD. Make sure you are running the CLI from a git repository") + sentry.CaptureException(err) + return 1 + } + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetExtra("headCommitOID", headCommitOID) + }) + + // Flag validation + if reportCommandValue == "" && reportCommandValueFile == "" { + fmt.Println("DeepSource | Error | '--value' (or) '--value-file' not passed") + return 1 + } + + var analyzerShortcode string + var artifactKey string + var artifactValue string + + analyzerShortcode = reportCommandAnalyzerShortcode + artifactKey = reportCommandKey + + if reportCommandValue != "" { + artifactValue = reportCommandValue + } + + if reportCommandValueFile != "" { + // Check file size + fileStat, err := os.Stat(reportCommandValueFile) + if err != nil { + fmt.Println("DeepSource error | Error | Unable to read specified value file: " + reportCommandValueFile) + sentry.CaptureException(err) + return 1 + } + + if fileStat.Size() > maxArtifactUploadSize { + fmt.Println("DeepSource | Error | Value file too large. Should be less than 20 Megabytes") + return 1 + } + + valueBytes, err := ioutil.ReadFile(reportCommandValueFile) + if err != nil { + fmt.Println("DeepSource error| Error | Unable to read specified value file: ", reportCommandValueFile) + sentry.CaptureException(err) + return 1 + } + + artifactValue = string(valueBytes) + } + + //////////////////// + // Generate query // + //////////////////// + + reportMeta := make(map[string]string) + reportMeta["workDir"] = currentDir + + query := ReportQuery{ + Query: reportGraphqlQuery, + } + + queryInput := ReportQueryInput{ + AccessToken: dsnAccessToken, + CommitOID: headCommitOID, + ReporterName: "cli", + ReporterVersion: cliVersion, + Key: artifactKey, + Data: artifactValue, + AnalyzerShortcode: analyzerShortcode, + Metadata: reportMeta, + } + + query.Variables.Input = queryInput + + // Marshal request body + queryBodyBytes, err := json.Marshal(query) + if err != nil { + fmt.Println("DeepSource | Error | Unable to marshal query body.") + sentry.CaptureException(err) + return 0 + } + + queryResponseBody, err := makeQuery( + dsnProtocol+"://"+dsnHost+"/graphql/cli/", + queryBodyBytes, + "application/json", + ) + if err != nil { + fmt.Println("DeepSource | Error | Reporting failed | ", err) + sentry.CaptureException(err) + return 0 + } + + // Parse query response body + queryResponse := QueryResponse{} + + err = json.Unmarshal(queryResponseBody, &queryResponse) + if err != nil { + fmt.Println("DeepSource | Error | Unable to parse response body.") + sentry.CaptureException(err) + return 0 + } + + // Check for errors in response body + // Response format: + // { + // "data": { + // "createArtifact": { + // "ok": false, + // "error": "No repository found attached with the access token: dasdsds" + // } + // } + // } + + if !queryResponse.Data.CreateArtifact.Ok { + fmt.Println("DeepSource | Error | Reporting failed | ", queryResponse.Data.CreateArtifact.Error) + sentry.CaptureException(errors.New(queryResponse.Data.CreateArtifact.Error)) + return 0 + } + + if err != nil { + fmt.Println("DeepSource | Error | Unable to report results.") + sentry.CaptureException(err) + return 0 + } + + fmt.Printf("DeepSource | Artifact published successfully \n \n") + fmt.Printf("Analyzer %s \n", analyzerShortcode) + fmt.Printf("Key %s \n", artifactKey) + + return 0 +} diff --git a/tests/dummy/python_coverage.xml b/command/report/tests/dummy/python_coverage.xml similarity index 100% rename from tests/dummy/python_coverage.xml rename to command/report/tests/dummy/python_coverage.xml diff --git a/tests/dummy/report_graphql_error_response_body.json b/command/report/tests/dummy/report_graphql_error_response_body.json similarity index 100% rename from tests/dummy/report_graphql_error_response_body.json rename to command/report/tests/dummy/report_graphql_error_response_body.json diff --git a/tests/dummy/report_graphql_request_body.json b/command/report/tests/dummy/report_graphql_request_body.json similarity index 100% rename from tests/dummy/report_graphql_request_body.json rename to command/report/tests/dummy/report_graphql_request_body.json diff --git a/tests/dummy/report_graphql_success_response_body.json b/command/report/tests/dummy/report_graphql_success_response_body.json similarity index 100% rename from tests/dummy/report_graphql_success_response_body.json rename to command/report/tests/dummy/report_graphql_success_response_body.json diff --git a/tests/init_test.go b/command/report/tests/init_test.go similarity index 100% rename from tests/init_test.go rename to command/report/tests/init_test.go diff --git a/tests/report_workflow_test.go b/command/report/tests/report_workflow_test.go similarity index 95% rename from tests/report_workflow_test.go rename to command/report/tests/report_workflow_test.go index 7a2922f3..4282a385 100644 --- a/tests/report_workflow_test.go +++ b/command/report/tests/report_workflow_test.go @@ -17,7 +17,7 @@ import ( // Sample values to the run the analyzer on const ( - analyzer = "test_coverage" + analyzer = "test-coverage" dsn = "http://f59ab9314307@localhost:8081" commitOID = "c2d16c69dbcba139002757b6734ee43c714845a3" key = "python" @@ -102,7 +102,7 @@ func TestReportKeyValueWorkflow(t *testing.T) { err = cmd.Run() - outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes()) + outStr, errStr := stdout.String(), stderr.String() log.Printf("== Run deepsource CLI command ==\n%s\n%s\n", outStr, errStr) if err != nil { @@ -146,7 +146,7 @@ func TestReportKeyValueFileWorkflow(t *testing.T) { err := cmd.Run() - outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes()) + outStr, errStr := stdout.String(), stderr.String() log.Printf("== Run deepsource CLI command ==\n%s\n%s\n", outStr, errStr) if err != nil { diff --git a/types.go b/command/report/types.go similarity index 98% rename from types.go rename to command/report/types.go index 041a5cdf..45a7815c 100644 --- a/types.go +++ b/command/report/types.go @@ -1,4 +1,4 @@ -package main +package report // ReportQueryInput is the schema for variables of artifacts // report GraphQL query diff --git a/command/root.go b/command/root.go new file mode 100644 index 00000000..73e323b3 --- /dev/null +++ b/command/root.go @@ -0,0 +1,39 @@ +package command + +import ( + "github.com/deepsourcelabs/cli/command/auth" + "github.com/deepsourcelabs/cli/command/config" + "github.com/deepsourcelabs/cli/command/issues" + "github.com/deepsourcelabs/cli/command/repo" + "github.com/deepsourcelabs/cli/command/report" + "github.com/deepsourcelabs/cli/command/version" + "github.com/spf13/cobra" +) + +func NewCmdRoot() *cobra.Command { + cmd := &cobra.Command{ + Use: "deepsource [flags]", + Short: "DeepSource CLI", + Long: `Welcome to DeepSource CLI +Now ship good code directly from the command line. + +Login into DeepSource using the command : deepsource auth login`, + SilenceErrors: true, + SilenceUsage: true, + } + + // Child Commands + cmd.AddCommand(version.NewCmdVersion()) + cmd.AddCommand(config.NewCmdConfig()) + cmd.AddCommand(auth.NewCmdAuth()) + cmd.AddCommand(repo.NewCmdRepo()) + cmd.AddCommand(issues.NewCmdIssues()) + cmd.AddCommand(report.NewCmdReport()) + + return cmd +} + +func Execute() error { + cmd := NewCmdRoot() + return cmd.Execute() +} diff --git a/command/version/command.go b/command/version/command.go new file mode 100644 index 00000000..b7fff01c --- /dev/null +++ b/command/version/command.go @@ -0,0 +1,45 @@ +package version + +import ( + "fmt" + + "github.com/deepsourcelabs/cli/utils" + "github.com/deepsourcelabs/cli/version" + "github.com/spf13/cobra" +) + +// Options holds the metadata. +type Options struct{} + +// For testing. TODO: cleanup +var getBuildInfo = version.GetBuildInfo + +// NewCmdVersion returns the current version of cli being used +func NewCmdVersion() *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Get the version of the DeepSource CLI", + Args: utils.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + o := Options{} + fmt.Println(o.Run()) + }, + SilenceErrors: true, + SilenceUsage: true, + } + return cmd +} + +// Validate impletments the Validate method for the ICommand interface. +func (Options) Validate() error { + return nil +} + +// Run executest the command. +func (Options) Run() string { + buildInfo := getBuildInfo() + if buildInfo == nil { + return "" + } + return getBuildInfo().String() +} diff --git a/command/version/command_test.go b/command/version/command_test.go new file mode 100644 index 00000000..661ec296 --- /dev/null +++ b/command/version/command_test.go @@ -0,0 +1,39 @@ +package version + +import ( + "testing" + "time" + + "github.com/deepsourcelabs/cli/version" +) + +func TestOptions_Run(t *testing.T) { + date, _ := time.Parse("2006-01-02", "2021-01-21") + + getBuildInfo = func() *version.BuildInfo { + return &version.BuildInfo{ + Version: "1.5.0", + Date: date, + } + } + + tests := []struct { + name string + o Options + want string + }{ + { + name: "must return the string output for command", + o: Options{}, + want: "DeepSource CLI version 1.5.0 (2021-01-21)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := Options{} + if got := o.Run(); got != tt.want { + t.Errorf("Options.Run() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..09e1c8b8 --- /dev/null +++ b/config/config.go @@ -0,0 +1,159 @@ +package config + +import ( + "errors" + "os" + "path/filepath" + "time" + + "github.com/pelletier/go-toml" +) + +var configDirFn = os.UserHomeDir +var readFileFn = os.ReadFile + +const ( + ConfigDirName = "/.deepsource/" + ConfigFileName = "/config.toml" + DefaultHostName = "deepsource.io" +) + +type CLIConfig struct { + Host string + User string + Token string + RefreshToken string + TokenExpiresIn time.Time + RefreshTokenExpiresIn time.Time +} + +var Cfg CLIConfig + +// Sets the token expiry in the desired format +func (cfg *CLIConfig) SetTokenExpiry(str string) { + layout := "2006-01-02T15:04:05.999999999" + t, _ := time.Parse(layout, str) + timeStamp := t.Unix() + cfg.TokenExpiresIn = time.Unix(timeStamp, 0) +} + +// Checks if the token has expired or not +func (cfg CLIConfig) IsExpired() bool { + if cfg.TokenExpiresIn.IsZero() { + return true + } + return time.Now().After(cfg.TokenExpiresIn) +} + +// configDir returns the directory to store the config file. +func (CLIConfig) configDir() (string, error) { + home, err := configDirFn() + if err != nil { + return "", err + } + return filepath.Join(home, ConfigDirName), nil +} + +// configPath returns the file path to the config file. +func (cfg CLIConfig) configPath() (string, error) { + home, err := cfg.configDir() + if err != nil { + return "", err + } + return filepath.Join(home, ConfigFileName), nil +} + +//ReadFile reads the CLI config file. +func (cfg *CLIConfig) ReadConfigFile() error { + path, err := cfg.configPath() + if err != nil { + return err + } + + // check if config exists + _, err = os.Stat(path) + if err != nil { + return nil + } + + data, err := readFileFn(path) + if err != nil { + return err + } + err = toml.Unmarshal(data, cfg) + if err != nil { + return err + } + + return nil +} + +func GetConfig() (*CLIConfig, error) { + + if Cfg.Token != "" { + return &Cfg, nil + } + + err := Cfg.ReadConfigFile() + if err != nil { + return &Cfg, err + } + return &Cfg, nil +} + +// WriteFile writes the CLI config to file. +func (cfg *CLIConfig) WriteFile() error { + + data, err := toml.Marshal(cfg) + if err != nil { + return err + } + + configDir, err := cfg.configDir() + if err != nil { + return err + } + + if err := os.MkdirAll(configDir, os.ModePerm); err != nil { + return err + } + + path, err := cfg.configPath() + if err != nil { + return err + } + + // Create file + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write(data) + + return err +} + +// Deletes the config during logging out user +func (cfg *CLIConfig) Delete() error { + path, err := cfg.configPath() + if err != nil { + return err + } + return os.Remove(path) +} + +func (cfg *CLIConfig) VerifyAuthentication() error { + + // Checking if the user has authenticated / logged in or not + if cfg.Token == "" { + return errors.New("You are not logged into DeepSource. Run \"deepsource auth login\" to authenticate.") + } + + // Check if the token has already expired + if cfg.IsExpired() { + return errors.New("The authentication has expired. Run \"deepsource auth refresh\" to refresh the credentials.") + } + return nil +} diff --git a/configvalidator/analyzer_config_validator.go b/configvalidator/analyzer_config_validator.go new file mode 100644 index 00000000..31ce254f --- /dev/null +++ b/configvalidator/analyzer_config_validator.go @@ -0,0 +1,101 @@ +package configvalidator + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/deepsourcelabs/cli/utils" + "github.com/xeipuuv/gojsonschema" +) + +// Analyzer Config Validator +func (c *ConfigValidator) validateAnalyzersConfig() { + activatedAnalyzers := make(map[string]interface{}) + + // Analyzer array should not be empty + if len(c.Config.Analyzers) == 0 { + c.pushError("There must be atleast one activated `analyzer` in the config. Found: 0") + } + + // Analyzers should be an array + analyzersType := reflect.TypeOf(c.Config.Analyzers).Kind().String() + if analyzersType != "slice" { + c.pushError(fmt.Sprintf("Value of `analyzers` should be an array. Found: %v", analyzersType)) + } + + // Enabled must be boolean. And atleast one analyzer must be enabled among all mentioned in config + countEnabled := 0 + for _, analyzer := range c.Config.Analyzers { + enabledType := reflect.TypeOf(analyzer.Enabled).Kind().String() + if enabledType != "bool" { + c.pushError(fmt.Sprintf("The `enabled` property should be of boolean type. Found: %v", enabledType)) + } + + if analyzer.Enabled { + countEnabled++ + } + } + if countEnabled == 0 && len(c.Config.Analyzers) > 0 { + c.pushError("There must be atleast one enabled `analyzer`. Found: 0") + } + + // ==== Analyzer shortcode validation ==== + supported := false + for _, analyzer := range c.Config.Analyzers { + for _, supportedAnalyzer := range utils.AnalyzersData.AnalyzerShortcodes { + if analyzer.Name == supportedAnalyzer { + // Copy the meta of activated analyzer for usage in + // analyzer meta validation + if analyzer.Enabled { + activatedAnalyzers[analyzer.Name] = analyzer.Meta + } + supported = true + break + } + } + if !supported { + c.pushError(fmt.Sprintf("Analyzer for \"%s\" is not supported yet.", analyzer.Name)) + } + + supported = false + } + + // ==== Meta Schema Validation ==== + + // Contains the meta-schema of the particular activated analyzer + var analyzerMetaSchema string + // Contains the user supplied meta + var userActivatedSchema interface{} + + // Iterating over the activated analyzers and + // validating the meta_schema + for analyzer, meta := range activatedAnalyzers { + analyzerMetaSchema = utils.AnalyzersData.AnalyzersMetaMap[analyzer] + userActivatedSchema = meta + + // Loading the Meta Schema obtained from API + schema := gojsonschema.NewStringLoader(analyzerMetaSchema) + // Loading the Meta Schema of the user after converting it + // into a JSON string + jsonUserSchema, _ := json.Marshal(userActivatedSchema) + inputMeta := gojsonschema.NewStringLoader(string(jsonUserSchema)) + + // If there is no meta-schema, write empty object in the inputSchema + if string(jsonUserSchema) == "null" { + inputMeta = gojsonschema.NewStringLoader("{}") + } + + // Validate the Meta Schema + result, _ := gojsonschema.Validate(schema, inputMeta) + if result.Valid() { + continue + } + finalErrString := fmt.Sprintf("Errors found while validating meta of %s analyzer: ", analyzer) + for _, err := range result.Errors() { + errString := err.String() + finalErrString = finalErrString + errString + } + c.pushError(finalErrString) + } +} diff --git a/configvalidator/analyzer_config_validator_test.go b/configvalidator/analyzer_config_validator_test.go new file mode 100644 index 00000000..b764938a --- /dev/null +++ b/configvalidator/analyzer_config_validator_test.go @@ -0,0 +1,106 @@ +package configvalidator + +import ( + "reflect" + "testing" +) + +func TestValidateAnalyzersConfig(t *testing.T) { + setDummyAnalyzerTransformerData() + type test struct { + inputConfig string + result bool + } + + tests := map[string]test{ + "valid config": { + inputConfig: ` + [[analyzers]] + name = "python" + enabled = true`, + result: true, + }, + "name should be a string": { + inputConfig: ` + [[analyzers]] + name = 123 + enabled = true`, + result: false, + }, + "`analyzers` should be an array": { + inputConfig: ` + analyzers = "python" + enabled = true`, + result: false, + }, + "atleast one analyzer should be enabled": { + inputConfig: ` + [[analyzers]] + name = "python" + enabled = false`, + result: false, + }, + "name cannot be of an unsupported analyzer": { + inputConfig: ` + [[analyzers]] + name = "foobar" + enabled = true`, + result: false, + }, + "analyzer with meta config": { + inputConfig: ` + [[analyzers]] + name = "python" + enabled = true + + [analyzers.meta] + max_line_length = 100 + skip_doc_coverage = ["module", "magic", "class"]`, + result: true, + }, + "max_line_length meta property validation": { + inputConfig: ` + [[analyzers]] + name = "python" + enabled = true + + [analyzers.meta] + max_line_length = -100`, + result: false, + }, + "valid multiple analyzers": { + inputConfig: ` + [[analyzers]] + name = "python" + enabled = true + + [analyzers.meta] + max_line_length = 100 + + [[analyzers]] + name = "test-coverage" + enabled = true`, + result: true, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + testConfig, err := getConfig([]byte(tc.inputConfig)) + if err != nil { + t.Error(err) + } + c := &ConfigValidator{ + Config: *testConfig, + Result: Result{ + Valid: true, + Errors: []string{}, + ConfigReadError: false, + }, + } + c.validateAnalyzersConfig() + if !reflect.DeepEqual(tc.result, c.Result.Valid) { + t.Errorf("expected: %v, got: %v. Error: %v", tc.result, c.Result.Valid, c.Result.Errors) + } + }) + } +} diff --git a/configvalidator/config_validator.go b/configvalidator/config_validator.go new file mode 100644 index 00000000..3688c052 --- /dev/null +++ b/configvalidator/config_validator.go @@ -0,0 +1,63 @@ +package configvalidator + +import ( + "bytes" + + "github.com/spf13/viper" +) + +const ( + MAX_ALLOWED_VERSION = 1 +) + +type Result struct { + Valid bool + Errors []string + ConfigReadError bool +} + +// Struct to store the meta (Config) and output (Result) of config validation +type ConfigValidator struct { + Config DSConfig + Result Result +} + +// Entrypoint to the package `configvalidator` +// Accepts DeepSource config as a parameter and validates it +func (c *ConfigValidator) ValidateConfig(inputConfig []byte) Result { + // Base cases + c.Result.Valid = true + c.Result.ConfigReadError = false + + // Making a "config" struct based on DSConfig to store the DeepSource config + config := DSConfig{} + viper.SetConfigType("toml") + err := viper.ReadConfig(bytes.NewBuffer(inputConfig)) + if err != nil { + // Error while reading config + c.Result.Valid = false + c.Result.Errors = append(c.Result.Errors, err.Error()) + c.Result.ConfigReadError = true + return c.Result + } + // Unmarshaling the configdata into DSConfig struct + viper.UnmarshalExact(&config) + c.Config = config + + // Validate generic config which applies to all analyzers and transformers + // Includes : Version, Exclude Patterns, Test Patterns + c.validateGenericConfig() + + // Validate the Analyzers configuration + c.validateAnalyzersConfig() + + // Validate the Transformers configuration + c.validateTransformersConfig() + return c.Result +} + +// Utility function to push result string into the "ConfigValidator" struct +func (c *ConfigValidator) pushError(errorString string) { + c.Result.Errors = append(c.Result.Errors, errorString) + c.Result.Valid = false +} diff --git a/configvalidator/config_validator_test.go b/configvalidator/config_validator_test.go new file mode 100644 index 00000000..a9d5cf0e --- /dev/null +++ b/configvalidator/config_validator_test.go @@ -0,0 +1,173 @@ +package configvalidator + +import ( + "reflect" + "testing" + + "github.com/deepsourcelabs/cli/utils" +) + +func TestValidateConfig(t *testing.T) { + type test struct { + inputConfig string + valid bool + } + setDummyAnalyzerTransformerData() + + tests := map[string]test{ + "blank config": { + inputConfig: "", + valid: false, + }, + "analyzer should be array": { + inputConfig: ` + version = 1 + analyzers = "python", + enabled = true`, + valid: false, + }, + "zero analyzers": { + inputConfig: ` + version = 1 + + [[analyzers]] + name = "python" + enabled = false + + [[analyzers]] + name = "javascript" + enabled = true`, + valid: false, + }, + "transformer without analyzer": { + inputConfig: ` + version = 1 + + [[transformers]] + name = "black" + enabled = true`, + valid: false, + }, + "no analyzer/transformer activated": { + inputConfig: ` + version = 1 + + [[analyzers]] + name = "python" + enabled = false + + [[transformers]] + name = "black" + enabled = false + + [[transformers]] + name = "isort" + enabled = false`, + valid: false, + }, + "tranformers with analyzer disabled": { + inputConfig: ` + version = 1 + + [[analyzers]] + name = "python" + enabled = false + + [[transformers]] + name = "black" + enabled = true + + [[transformers]] + name = "isort" + enabled = true`, + valid: false, + }, + "non-supported transformer": { + inputConfig: ` + version = 1 + + [[analyzers]] + name = "python" + enabled = true + + [[transformers]] + name = "egg" + enabled = true`, + valid: false, + }, + "transformer must be an array": { + inputConfig: ` + version = 1 + + [[analyzers]] + name = "python" + enabled = true + + transformers = "egg" + enabled = true`, + valid: false, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + c := &ConfigValidator{} + c.ValidateConfig([]byte(tc.inputConfig)) + if !reflect.DeepEqual(tc.valid, c.Result.Valid) { + t.Errorf("%s: expected: %v, got: %v. Error: %v", testName, tc.valid, c.Result.Valid, c.Result.Errors) + } + }) + } +} + +func setDummyAnalyzerTransformerData() { + analyzersMetaMap := make(map[string]string) + utils.AnalyzersData.AnalyzerShortcodes = []string{"python", "test-coverage"} + utils.AnalyzersData.AnalyzersMeta = []string{`{ + "type": "object", + "properties": { + "max_line_length": { + "type": "integer", + "minimum": 79, + "title": "Maximum line length", + "description": "Customize this according to your project's conventions.", + "default": 100 + }, + "runtime_version": { + "enum": [ + "3.x.x", + "2.x.x" + ], + "type": "string", + "title": "Runtime version", + "description": "Set it to the least version of Python that your code runs on.", + "default": "3.x.x" + }, + "skip_doc_coverage": { + "type": "array", + "title": "Skip in doc coverage", + "description": "Types of objects that should be skipped while calculating documentation coverage.", + "items": { + "enum": [ + "magic", + "init", + "class", + "module", + "nonpublic" + ], + "type": "string" + }, + "additionalProperties": false + } + }, + "optional_required": [ + "runtime_version" + ], + "additionalProperties": false +}`, "{}"} + + analyzersMetaMap["python"] = utils.AnalyzersData.AnalyzersMeta[0] + analyzersMetaMap["test-coverage"] = utils.AnalyzersData.AnalyzersMeta[1] + utils.AnalyzersData.AnalyzersMetaMap = analyzersMetaMap + + utils.TransformersData.TransformerShortcodes = []string{"black", "prettier"} +} diff --git a/configvalidator/generic_config_validator.go b/configvalidator/generic_config_validator.go new file mode 100644 index 00000000..d523cbc3 --- /dev/null +++ b/configvalidator/generic_config_validator.go @@ -0,0 +1,94 @@ +package configvalidator + +import ( + "fmt" + "reflect" + "strconv" + + "github.com/spf13/viper" +) + +// Generic Config : +// - Version +// - Exclude_Patterns +// - Test_Patterns + +// Validates version field of the DeepSource config +func (c *ConfigValidator) validateVersion() { + if viper.Get("version") != nil { + // Value of version must be an integer + if reflect.TypeOf(viper.Get("version")).Kind().String() != "int64" { + c.pushError(fmt.Sprintf("Value of `version` must be an integer. Got %s", reflect.TypeOf(viper.Get("version")).Kind().String())) + return + } + + // Should not be zero + versionInt, _ := strconv.Atoi(viper.GetString("version")) + if versionInt < 1 { + c.pushError(fmt.Sprintf("Value for `version` cannot be less than 1. Got %d", versionInt)) + } + + // Must be less than MAX_ALLOWED VERSION + if versionInt > MAX_ALLOWED_VERSION { + c.pushError(fmt.Sprintf("Value for `version` cannot be greater than %d. Got %d", MAX_ALLOWED_VERSION, versionInt)) + } + return + } + // if version is nil(not present in config) + c.pushError("Property `version` is mandatory.") +} + +// Validates `exclude_patterns` field of the DeepSource config +func (c *ConfigValidator) validateExcludePatterns() { + excludePatterns := viper.Get("exclude_patterns") + + // Sometimes the user doesn't add `exclude_patterns` to the code + // Validate only if excludePatterns present + if excludePatterns != nil { + // Must be a slice of string + exPatternType := reflect.TypeOf(excludePatterns).Kind().String() + if exPatternType != "slice" { + c.pushError(fmt.Sprintf("Value of `exclude_patterns` should be an array of strings. Found: %v", exPatternType)) + return + } + + // Value of each exclude pattern can only be a string + for _, ex_pattern := range c.Config.ExcludePatterns { + numValue, err := strconv.Atoi(ex_pattern) + if err == nil { + c.pushError(fmt.Sprintf("Value of `exclude_patterns` paths can only be string. Found: %v", numValue)) + } + } + } +} + +// Validates `test_patterns` field of the DeepSource config +func (c *ConfigValidator) validateTestPatterns() { + testPatterns := viper.Get("test_patterns") + + // Sometimes the user doesn't add `test_patterns` to the code + // Validate only if testPatterns present + if testPatterns != nil { + // Must be a slice + testPatternType := reflect.TypeOf(testPatterns).Kind().String() + if testPatternType != "slice" { + c.pushError(fmt.Sprintf("Value of `test_patterns` should be an array of objects. Found: %v", testPatternType)) + } + + // Value of each test pattern can only be a string + for _, test_pattern := range c.Config.TestPatterns { + numValue, err := strconv.Atoi(test_pattern) + if err == nil { + c.pushError(fmt.Sprintf("Value of `test_patterns` paths can only be string. Found: %v", numValue)) + } + } + } +} + +// Validates generic DeepSource config +func (c *ConfigValidator) validateGenericConfig() { + c.validateVersion() + c.validateExcludePatterns() + c.validateTestPatterns() + +} diff --git a/configvalidator/generic_config_validator_test.go b/configvalidator/generic_config_validator_test.go new file mode 100644 index 00000000..13e5f874 --- /dev/null +++ b/configvalidator/generic_config_validator_test.go @@ -0,0 +1,181 @@ +package configvalidator + +import ( + "bytes" + "reflect" + "testing" + + "github.com/spf13/viper" +) + +func TestValidateVersion(t *testing.T) { + type test struct { + inputConfig string + valid bool + } + + tests := map[string]test{ + "valid config": { + inputConfig: "version = 1", + valid: true, + }, + "wrong version": { + inputConfig: "version = \"foobar\"", + valid: false, + }, + "version greater than maximum allowed": { + inputConfig: "version = 352", + valid: false, + }, + "version missing": { + inputConfig: "", + valid: false, + }, + "version of wrong type": { + inputConfig: "version = \"2\"", + valid: false, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + testConfig, err := getConfig([]byte(tc.inputConfig)) + if err != nil { + t.Error(err) + } + c := &ConfigValidator{ + Config: *testConfig, + Result: Result{ + Valid: true, + Errors: []string{}, + ConfigReadError: false, + }, + } + c.validateVersion() + if !reflect.DeepEqual(tc.valid, c.Result.Valid) { + t.Fatalf("%v: expected: %v, got: %v. Error: %v", testName, tc.valid, c.Result.Valid, c.Result.Errors) + } + }) + } +} + +func TestValidateExcludePatterns(t *testing.T) { + type test struct { + inputConfig string + valid bool + } + + tests := map[string]test{ + "valid exclude_patterns": { + inputConfig: "version= 1\nexclude_patterns = 23", + valid: false, + }, + "should be array of string": { + inputConfig: "version= 1\nexclude_patterns = [23,43]", + valid: false, + }, + "valid array of string": { + inputConfig: "version = 1\nexclude_patterns = ['hello', 'world']", + valid: true, + }, + "strings with double quotes": { + inputConfig: "exclude_patterns = [\"hello\",\"world\"]", + valid: true, + }, + "empty exclude_patterns": { + inputConfig: "exclude_patterns = []", + valid: true, + }, + "cannot be only string, should be an array": { + inputConfig: "version = 1\nexclude_patterns = 'hello'", + valid: false, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + testConfig, err := getConfig([]byte(tc.inputConfig)) + if err != nil { + t.Error(err) + } + c := &ConfigValidator{ + Config: *testConfig, + Result: Result{ + Valid: true, + Errors: []string{}, + ConfigReadError: false, + }, + } + c.validateExcludePatterns() + if !reflect.DeepEqual(tc.valid, c.Result.Valid) { + t.Fatalf("%v: Config : %v, expected: %v, got: %v. Error: %v", testName, tc.inputConfig, tc.valid, c.Result.Valid, c.Result.Errors) + } + }) + } +} + +func TestValidateTestPatterns(t *testing.T) { + type test struct { + inputConfig string + valid bool + } + + tests := map[string]test{ + "cannot be an integer": { + inputConfig: "test_patterns = 23", + valid: false, + }, + "cannot be an array of integers": { + inputConfig: "test_patterns = [23,43]", + valid: false, + }, + "should be array of strings": { + inputConfig: "test_patterns = ['hello', 'world']", + valid: true, + }, + "strings with double quotes": { + inputConfig: "test_patterns = [\"hello\",\"world\"]", + valid: true, + }, + "empty test_patterns": { + inputConfig: "test_patterns = []", + valid: true, + }, + "cannot be only string, should be an array of string": { + inputConfig: "test_patterns = 'hello'", + valid: false, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + testConfig, err := getConfig([]byte(tc.inputConfig)) + if err != nil { + t.Error(err) + } + c := &ConfigValidator{ + Config: *testConfig, + Result: Result{ + Valid: true, + Errors: []string{}, + ConfigReadError: false, + }, + } + c.validateTestPatterns() + if !reflect.DeepEqual(tc.valid, c.Result.Valid) { + t.Fatalf("%v: Config : %v, expected: %v, got: %v. Error: %v", testName, tc.inputConfig, tc.valid, c.Result.Valid, c.Result.Errors) + } + }) + } +} + +// Receives a string of DeepSource config and returns its +// representation in the form of a DSConfig struct +func getConfig(inputConfig []byte) (*DSConfig, error) { + config := DSConfig{} + viper.SetConfigType("toml") + err := viper.ReadConfig(bytes.NewBuffer(inputConfig)) + if err != nil { + return nil, err + } + // Unmarshaling the configdata into DSConfig struct + viper.UnmarshalExact(&config) + return &config, nil +} diff --git a/configvalidator/transformer_config_validator.go b/configvalidator/transformer_config_validator.go new file mode 100644 index 00000000..fc98aa27 --- /dev/null +++ b/configvalidator/transformer_config_validator.go @@ -0,0 +1,45 @@ +package configvalidator + +import ( + "fmt" + "reflect" + + "github.com/deepsourcelabs/cli/utils" + "github.com/spf13/viper" +) + +// Validates Transformers Config +func (c *ConfigValidator) validateTransformersConfig() { + // If no transformer activated by user, return without any errors + if viper.Get("transformers") == nil { + return + } + + // Transformers should be an array + transformersType := reflect.TypeOf(c.Config.Transformers).Kind().String() + if transformersType != "slice" { + c.pushError(fmt.Sprintf("Value of `transformers` should be an array. Found: %v", transformersType)) + } + + // Enabled property should be of boolean type + for _, transformer := range c.Config.Transformers { + enabledType := reflect.TypeOf(transformer.Enabled).Kind().String() + if enabledType != "bool" { + c.pushError(fmt.Sprintf("The `enabled` property should be of boolean type. Found: %v", enabledType)) + } + } + + // ==== Transformer shortcode validation ==== + supported := false + for _, activatedTransformer := range c.Config.Transformers { + for _, supportedTransformer := range utils.TransformersData.TransformerShortcodes { + if activatedTransformer.Name == supportedTransformer { + supported = true + break + } + } + if !supported { + c.pushError(fmt.Sprintf("The Tranformer %s is not supported yet.", activatedTransformer.Name)) + } + } +} diff --git a/configvalidator/transformer_config_validator_test.go b/configvalidator/transformer_config_validator_test.go new file mode 100644 index 00000000..50514503 --- /dev/null +++ b/configvalidator/transformer_config_validator_test.go @@ -0,0 +1,69 @@ +package configvalidator + +import ( + "reflect" + "testing" +) + +func TestValidateTransformersConfig(t *testing.T) { + setDummyAnalyzerTransformerData() + type test struct { + inputConfig string + result bool + } + + tests := map[string]test{ + "valid config": { + inputConfig: ` + [[transformers]] + name = "black" + enabled = true`, + result: true, + }, + "transformers are not mandatory lik}e analyzers": { + inputConfig: ` + [[transformers]] + name = "black" + enabled = false`, + result: true, + }, + "can't use unsupported analyzer": { + inputConfig: ` + [[transformers]] + name = "rick-astley" + enabled = true`, + result: false, + }, + "multiple transformers": { + inputConfig: ` + [[transformers]] + name = "black" + enabled = true + + [[transformers]] + name = "prettier" + enabled = true`, + result: true, + }, + } + for testName, tc := range tests { + t.Run(testName, func(t *testing.T) { + testConfig, err := getConfig([]byte(tc.inputConfig)) + if err != nil { + t.Error(err) + } + c := &ConfigValidator{ + Config: *testConfig, + Result: Result{ + Valid: true, + Errors: []string{}, + ConfigReadError: false, + }, + } + c.validateTransformersConfig() + if !reflect.DeepEqual(tc.result, c.Result.Valid) { + t.Errorf("expected: %v, got: %v. Error: %v", tc.result, c.Result.Valid, c.Result.Errors) + } + }) + } +} diff --git a/configvalidator/types.go b/configvalidator/types.go new file mode 100644 index 00000000..c33c7743 --- /dev/null +++ b/configvalidator/types.go @@ -0,0 +1,24 @@ +package configvalidator + +// DSConfig is the struct for .deepsource.toml file +type Analyzer struct { + Name string `mapstructure:"name,omitempty" json:"name,omitempty"` + RuntimeVersion string `mapstructure:"runtime_version,omitempty" json:"runtime_version,omitempty"` + Enabled bool `mapstructure:"enabled,omitempty" json:"enabled"` + DependencyFilePaths []string `mapstructure:"dependency_file_paths,omitempty" json:"dependency_file_paths,omitempty"` + Meta interface{} `mapstructure:"meta,omitempty" json:"meta,omitempty"` + Thresholds interface{} `mapstructure:"thresholds,omitempty" json:"thresholds,omitempty"` +} + +type Transformer struct { + Name string `mapstructure:"name,omitempty" json:"name,omitempty"` + Enabled bool `mapstructure:"enabled,omitempty" json:"enabled,omitempty"` +} + +type DSConfig struct { + Version int `mapstructure:"version,omitempty" json:"version"` + ExcludePatterns []string `mapstructure:"exclude_patterns,omitempty" json:"exclude_patterns,omitempty"` + TestPatterns []string `mapstructure:"test_patterns,omitempty" json:"test_patterns,omitempty"` + Analyzers []Analyzer `mapstructure:"analyzers,omitempty" json:"analyzers,omitempty"` + Transformers []Transformer `mapstructure:"transformers,omitempty" json:"transformers,omitempty"` +} diff --git a/coverage.out b/coverage.out new file mode 100644 index 00000000..5f02b111 --- /dev/null +++ b/coverage.out @@ -0,0 +1 @@ +mode: set diff --git a/deepsource/analyzers/analyzers.go b/deepsource/analyzers/analyzers.go new file mode 100644 index 00000000..a8f9ccec --- /dev/null +++ b/deepsource/analyzers/analyzers.go @@ -0,0 +1,7 @@ +package analyzers + +type Analyzer struct { + Name string // The name of the analyzer + Shortcode string // The shortcode of analyzer + MetaSchema string // Analyzer meta schema +} diff --git a/deepsource/analyzers/queries/get_analyzers.go b/deepsource/analyzers/queries/get_analyzers.go new file mode 100644 index 00000000..cf5c80d7 --- /dev/null +++ b/deepsource/analyzers/queries/get_analyzers.go @@ -0,0 +1,69 @@ +package analyzers + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/deepsource/analyzers" + "github.com/deepsourcelabs/graphql" +) + +// GraphQL query +const listAnalyzersQuery = ` +{ + analyzers { + edges { + node { + name + shortcode + metaSchema + } + } + } +}` + +type AnalyzersRequest struct{} + +type AnalyzersResponse struct { + Analyzers struct { + Edges []struct { + Node struct { + Name string `json:"name"` + Shortcode string `json:"shortcode"` + MetaSchema string `json:"metaSchema"` + } `json:"node"` + } `json:"edges"` + } `json:"analyzers"` +} + +// GraphQL client interface +type IGQLClient interface { + GQL() *graphql.Client + GetToken() string +} + +func (a AnalyzersRequest) Do(ctx context.Context, client IGQLClient) ([]analyzers.Analyzer, error) { + + req := graphql.NewRequest(listAnalyzersQuery) + + // set header fields + req.Header.Set("Cache-Control", "no-cache") + tokenHeader := fmt.Sprintf("JWT %s", client.GetToken()) + req.Header.Add("Authorization", tokenHeader) + + // run it and capture the response + var respData AnalyzersResponse + if err := client.GQL().Run(ctx, req, &respData); err != nil { + return nil, err + } + + // Formatting the query response w.r.t the output format + analyzersData := make([]analyzers.Analyzer, len(respData.Analyzers.Edges)) + for index, edge := range respData.Analyzers.Edges { + analyzersData[index].Name = edge.Node.Name + analyzersData[index].Shortcode = edge.Node.Shortcode + analyzersData[index].MetaSchema = edge.Node.MetaSchema + } + + return analyzersData, nil +} diff --git a/deepsource/auth/device.go b/deepsource/auth/device.go new file mode 100644 index 00000000..34c1f646 --- /dev/null +++ b/deepsource/auth/device.go @@ -0,0 +1,10 @@ +package auth + +type Device struct { + Code string `json:"deviceCode"` // Device Code + UserCode string `json:"userCode"` // 8 figure User code to be used while authentication + VerificationURI string `json:"verificationUri"` // URL to verify user code + VerificationURIComplete string `json:"verificationUriComplete"` // URL to verify user code with the user code being sent as a URL param + ExpiresIn int `json:"expiresIn"` // Time in which the device code expires + Interval int `json:"interval"` // Interval in which the client needs to poll at the endpoint to receive the JWT +} diff --git a/deepsource/auth/jwt.go b/deepsource/auth/jwt.go new file mode 100644 index 00000000..8551a453 --- /dev/null +++ b/deepsource/auth/jwt.go @@ -0,0 +1,13 @@ +package auth + +type JWT struct { + Payload struct { + Email string `json:"email"` // Email of the user + Exp string `json:"exp"` // Token Expiry timestamp + Origiat int64 `json:"origIat"` // The issued At claim : identifies the time at which the JWT was issued + } `json:"payload"` + Token string `json:"token"` // The JWT + Refreshtoken string `json:"refreshToken"` // Refresh token for refreshing auth creds when needed + TokenExpiresIn int64 `json:"tokenExpiresIn"` // Token expiry timestamp + RefreshExpiresIn int64 `json:"refreshExpiresIn"` // Refresh token expiry timestamp +} diff --git a/deepsource/auth/mutations/refresh_creds.go b/deepsource/auth/mutations/refresh_creds.go new file mode 100644 index 00000000..33305bba --- /dev/null +++ b/deepsource/auth/mutations/refresh_creds.go @@ -0,0 +1,48 @@ +package auth + +import ( + "context" + + "github.com/deepsourcelabs/cli/deepsource/auth" + "github.com/deepsourcelabs/graphql" +) + +// GraphQL query to refresh token +const refreshTokenQuery = ` +mutation RefreshToken($token: String!) { + refreshToken(refreshToken: $token) { + payload + token + refreshExpiresIn + refreshToken + } +}` + +type RefreshTokenParams struct { + RefreshToken string `json:"refreshToken"` +} + +type RefreshTokenRequest struct { + Params RefreshTokenParams +} + +type RefreshTokenResponse struct { + auth.JWT `json:"refreshToken"` +} + +func (r RefreshTokenRequest) Do(ctx context.Context, client IGQLClient) (*auth.JWT, error) { + + req := graphql.NewRequest(refreshTokenQuery) + + // set header fields + req.Header.Set("Cache-Control", "no-cache") + req.Var("token", r.Params.RefreshToken) + + // run it and capture the response + var respData RefreshTokenResponse + if err := client.GQL().Run(ctx, req, &respData); err != nil { + return nil, err + } + + return &respData.JWT, nil +} diff --git a/deepsource/auth/mutations/register_device.go b/deepsource/auth/mutations/register_device.go new file mode 100644 index 00000000..57292521 --- /dev/null +++ b/deepsource/auth/mutations/register_device.go @@ -0,0 +1,42 @@ +package auth + +import ( + "context" + + "github.com/deepsourcelabs/cli/deepsource/auth" + "github.com/deepsourcelabs/graphql" +) + +// GraphQL mutation to register Device get a device code +const registerDeviceMutation = `mutation register { + registerDevice(input:{}) { + deviceCode + userCode + verificationUri + verificationUriComplete + expiresIn + interval + } +}` + +type RegisterDeviceRequest struct{} + +type RegisterDeviceResponse struct { + auth.Device `json:"registerDevice"` +} + +type IGQLClient interface { + GQL() *graphql.Client +} + +func (r RegisterDeviceRequest) Do(ctx context.Context, client IGQLClient) (*auth.Device, error) { + req := graphql.NewRequest(registerDeviceMutation) + req.Header.Set("Cache-Control", "no-cache") + + var res RegisterDeviceResponse + if err := client.GQL().Run(ctx, req, &res); err != nil { + return nil, err + } + + return &res.Device, nil +} diff --git a/deepsource/auth/mutations/request_jwt.go b/deepsource/auth/mutations/request_jwt.go new file mode 100644 index 00000000..60e80795 --- /dev/null +++ b/deepsource/auth/mutations/request_jwt.go @@ -0,0 +1,43 @@ +package auth + +import ( + "context" + + "github.com/deepsourcelabs/cli/deepsource/auth" + "github.com/deepsourcelabs/graphql" +) + +type RequestJWTParams struct { + DeviceCode string `json:"deviceCode"` +} +type RequestJWTRequest struct { + Params RequestJWTParams +} + +// GraphQL mutation to request JWT +const requestJWTMutation = ` +mutation request($input:RequestJWTInput!) { + requestJwt(input:$input) { + payload + token + refreshToken + refreshExpiresIn + } +}` + +type RequestJWTResponse struct { + auth.JWT `json:"requestJwt"` +} + +func (r RequestJWTRequest) Do(ctx context.Context, client IGQLClient) (*auth.JWT, error) { + req := graphql.NewRequest(requestJWTMutation) + req.Header.Set("Cache-Control", "no-cache") + req.Var("input", r.Params) + + var res RequestJWTResponse + if err := client.GQL().Run(ctx, req, &res); err != nil { + return nil, err + } + + return &res.JWT, nil +} diff --git a/deepsource/client.go b/deepsource/client.go new file mode 100644 index 00000000..1b118198 --- /dev/null +++ b/deepsource/client.go @@ -0,0 +1,189 @@ +// DeepSource SDK +package deepsource + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/deepsource/analyzers" + analyzerQuery "github.com/deepsourcelabs/cli/deepsource/analyzers/queries" + "github.com/deepsourcelabs/cli/deepsource/auth" + authmut "github.com/deepsourcelabs/cli/deepsource/auth/mutations" + "github.com/deepsourcelabs/cli/deepsource/issues" + issuesQuery "github.com/deepsourcelabs/cli/deepsource/issues/queries" + "github.com/deepsourcelabs/cli/deepsource/repository" + repoQuery "github.com/deepsourcelabs/cli/deepsource/repository/queries" + "github.com/deepsourcelabs/cli/deepsource/transformers" + transformerQuery "github.com/deepsourcelabs/cli/deepsource/transformers/queries" + "github.com/deepsourcelabs/graphql" +) + +var defaultHostName = "deepsource.io" + +type ClientOpts struct { + Token string + HostName string +} + +type Client struct { + gql *graphql.Client + token string +} + +// Returns a GraphQL client which can be used to interact with the GQL APIs +func (c Client) GQL() *graphql.Client { + return c.gql +} + +// Returns the jWT which is required for authentication and thus, interacting with the APIs +func (c Client) GetToken() string { + return c.token +} + +// Returns a new GQLClient +func New(cp ClientOpts) (*Client, error) { + apiClientURL := getAPIClientURL(cp.HostName) + gql := graphql.NewClient(apiClientURL) + return &Client{ + gql: gql, + token: cp.Token, + }, nil +} + +// // Formats and returns the DeepSource Public API client URL +func getAPIClientURL(hostName string) string { + apiClientURL := fmt.Sprintf("https://api.%s/graphql/", defaultHostName) + + // Check if the domain is different from the default domain (In case of Enterprise users) + if hostName != defaultHostName { + apiClientURL = fmt.Sprintf("https://%s/api/graphql/", hostName) + } + return apiClientURL +} + +// Registers the device and allots it a device code which is further used for fetching +// the JWT and other authentication data +func (c Client) RegisterDevice(ctx context.Context) (*auth.Device, error) { + req := authmut.RegisterDeviceRequest{} + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + return res, nil +} + +// Logs in the client using the deviceCode and the user Code and returns the JWT, RefreshToken and other +// data which is required for authentication +func (c Client) Login(ctx context.Context, deviceCode string) (*auth.JWT, error) { + req := authmut.RequestJWTRequest{ + Params: authmut.RequestJWTParams{ + DeviceCode: deviceCode, + }, + } + + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + return res, nil +} + +// Refreshes the authentication credentials. Takes the refreshToken as a parameter. +func (c Client) RefreshAuthCreds(ctx context.Context, refreshToken string) (*auth.JWT, error) { + req := authmut.RefreshTokenRequest{ + Params: authmut.RefreshTokenParams{ + RefreshToken: refreshToken, + }, + } + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + return res, nil +} + +// Returns the list of Analyzers supported by DeepSource along with their meta like shortcode, metaschema. +func (c Client) GetSupportedAnalyzers(ctx context.Context) ([]analyzers.Analyzer, error) { + req := analyzerQuery.AnalyzersRequest{} + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + return res, nil +} + +// Returns the list of Transformers supported by DeepSource along with their meta like shortcode. +func (c Client) GetSupportedTransformers(ctx context.Context) ([]transformers.Transformer, error) { + req := transformerQuery.TransformersRequest{} + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + return res, nil +} + +// Returns the activation status of the repository whose data is sent as parameters. +// Owner : The username of the owner of the repository +// repoName : The name of the repository whose activation status has to be queried +// provider : The VCS provider which hosts the repo (GITHUB/GITLAB/BITBUCKET) +func (c Client) GetRepoStatus(ctx context.Context, owner, repoName, provider string) (*repository.Meta, error) { + req := repoQuery.RepoStatusRequest{ + Params: repoQuery.RepoStatusParams{ + Owner: owner, + RepoName: repoName, + Provider: provider, + }, + } + + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + return res, nil +} + +// Returns the list of issues for a certain repository whose data is sent as parameters. +// Owner : The username of the owner of the repository +// repoName : The name of the repository whose activation status has to be queried +// provider : The VCS provider which hosts the repo (GITHUB/GITLAB/BITBUCKET) +// limit : The amount of issues to be listed. The default limit is 30 while the maximum limit is currently 100. +func (c Client) GetIssues(ctx context.Context, owner, repoName, provider string, limit int) ([]issues.Issue, error) { + req := issuesQuery.IssuesListRequest{ + Params: issuesQuery.IssuesListParams{ + Owner: owner, + RepoName: repoName, + Provider: provider, + Limit: limit, + }, + } + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + + return res, nil +} + +// Returns the list of issues reported for a certain file in a certain repository whose data is sent as parameters. +// Owner : The username of the owner of the repository +// repoName : The name of the repository whose activation status has to be queried +// provider : The VCS provider which hosts the repo (GITHUB/GITLAB/BITBUCKET) +// filePath : The relative path of the file. Eg: "tests/mock.py" if a file `mock.py` is present in `tests` directory which in turn is present in the root dir +// limit : The amount of issues to be listed. The default limit is 30 while the maximum limit is currently 100. +func (c Client) GetIssuesForFile(ctx context.Context, owner, repoName, provider, filePath string, limit int) ([]issues.Issue, error) { + req := issuesQuery.FileIssuesListRequest{ + Params: issuesQuery.FileIssuesListParams{ + Owner: owner, + RepoName: repoName, + Provider: provider, + FilePath: filePath, + Limit: limit, + }, + } + + res, err := req.Do(ctx, c) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/deepsource/issues/issues_list.go b/deepsource/issues/issues_list.go new file mode 100644 index 00000000..f6b0dcd7 --- /dev/null +++ b/deepsource/issues/issues_list.go @@ -0,0 +1,22 @@ +package issues + +type Position struct { + BeginLine int // The line where the code covered under the issue starts + EndLine int // The line where the code covered under the issue starts +} + +type Location struct { + Path string // The filepath where the issue is reported + Position Position // The position info where the issue is raised +} + +type AnalyzerMeta struct { + Shortcode string // Analyzer shortcode +} + +type Issue struct { + IssueText string // The describing heading of the issue + IssueCode string // DeepSource code for the issue reported + Location Location // The location data for the issue reported + Analyzer AnalyzerMeta // The Analyzer which raised the issue +} diff --git a/deepsource/issues/queries/list_file_issues.go b/deepsource/issues/queries/list_file_issues.go new file mode 100644 index 00000000..7b4a60f7 --- /dev/null +++ b/deepsource/issues/queries/list_file_issues.go @@ -0,0 +1,122 @@ +// Lists the issues reported in a single file mentioned by the user +package issues + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/deepsource/issues" + "github.com/deepsourcelabs/graphql" +) + +// Query to fetch issues for a certain file specified by the user +const fetchFileIssuesQuery = ` +query($name:String!, $owner:String!, $provider:VCSProviderChoices!, $path:String!, $limit:Int!){ + repository(name:$name, owner:$owner, provider:$provider){ + file(path:$path){ + issues(first:$limit){ + edges{ + node{ + path + beginLine + endLine + concreteIssue{ + analyzer { + shortcode + } + title + shortcode + } + } + } + } + } + } +}` + +type FileIssuesListParams struct { + Owner string + RepoName string + Provider string + FilePath string + Limit int +} + +// Request struct +type FileIssuesListRequest struct { + Params FileIssuesListParams +} + +// Response struct +type FileIssuesResponse struct { + Repository struct { + File struct { + Issues struct { + Edges []struct { + Node struct { + Path string `json:"path"` + Beginline int `json:"beginLine"` + Endline int `json:"endLine"` + Concreteissue struct { + Analyzer struct { + Shortcode string `json:"shortcode"` + } `json:"analyzer"` + Title string `json:"title"` + Shortcode string `json:"shortcode"` + } `json:"concreteIssue"` + } `json:"node"` + } `json:"edges"` + } `json:"issues"` + } `json:"file"` + } `json:"repository"` +} + +// GraphQL client interface +type IGQLClient interface { + GQL() *graphql.Client + GetToken() string +} + +// Function to execute the query +func (f FileIssuesListRequest) Do(ctx context.Context, client IGQLClient) ([]issues.Issue, error) { + req := graphql.NewRequest(fetchFileIssuesQuery) + req.Header.Set("Cache-Control", "no-cache") + + req.Var("name", f.Params.RepoName) + req.Var("owner", f.Params.Owner) + req.Var("provider", f.Params.Provider) + req.Var("path", f.Params.FilePath) + req.Var("limit", f.Params.Limit) + + // set header fields + req.Header.Set("Cache-Control", "no-cache") + + // Adding jwt as header for auth + tokenHeader := fmt.Sprintf("JWT %s", client.GetToken()) + req.Header.Add("Authorization", tokenHeader) + + // run it and capture the response + var respData FileIssuesResponse + if err := client.GQL().Run(ctx, req, &respData); err != nil { + return nil, err + } + + // Formatting the query response w.r.t the output format of the SDK as specified in `issues_list.go` + var issuesData []issues.Issue + for index, edge := range respData.Repository.File.Issues.Edges { + + // Copying issue title and issue code + issuesData[index].IssueText = edge.Node.Concreteissue.Title + issuesData[index].IssueCode = edge.Node.Concreteissue.Shortcode + + // Copying position info + issuesData[index].Location.Path = edge.Node.Path + issuesData[index].Location.Position.BeginLine = edge.Node.Beginline + issuesData[index].Location.Position.EndLine = edge.Node.Endline + + // Copying the analyzer shortcode which raised the issue + issuesData[index].Analyzer.Shortcode = edge.Node.Concreteissue.Analyzer.Shortcode + } + + return issuesData, nil +} diff --git a/deepsource/issues/queries/list_issues.go b/deepsource/issues/queries/list_issues.go new file mode 100644 index 00000000..4060ebd7 --- /dev/null +++ b/deepsource/issues/queries/list_issues.go @@ -0,0 +1,100 @@ +// Lists the issues reported in the whole project +package issues + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/deepsource/issues" + "github.com/deepsourcelabs/graphql" +) + +const fetchAllIssuesQuery = ` +query GetAllIssues($name:String!, $owner:String!, $provider:VCSProviderChoices!, $limit:Int!){ + repository(name:$name, owner:$owner, provider:$provider){ + issues(first:$limit){ + edges{ + node{ + path + beginLine + endLine + concreteIssue{ + analyzer { + shortcode + } + title + shortcode + } + } + } + } + } +}` + +type IssuesListParams struct { + Owner string + RepoName string + Provider string + Limit int +} + +type IssuesListRequest struct { + Params IssuesListParams +} + +type IssuesListResponse struct { + Repository struct { + Issues struct { + Edges []struct { + Node struct { + Path string `json:"path"` + Beginline int `json:"beginLine"` + Endline int `json:"endLine"` + Concreteissue struct { + Analyzer struct { + Shortcode string `json:"shortcode"` + } `json:"analyzer"` + Title string `json:"title"` + Shortcode string `json:"shortcode"` + } `json:"concreteIssue"` + } `json:"node"` + } `json:"edges"` + } `json:"issues"` + } `json:"repository"` +} + +func (i IssuesListRequest) Do(ctx context.Context, client IGQLClient) ([]issues.Issue, error) { + + req := graphql.NewRequest(fetchAllIssuesQuery) + req.Var("name", i.Params.RepoName) + req.Var("owner", i.Params.Owner) + req.Var("provider", i.Params.Provider) + req.Var("limit", i.Params.Limit) + + // set header fields + req.Header.Set("Cache-Control", "no-cache") + // Adding jwt as header for auth + tokenHeader := fmt.Sprintf("JWT %s", client.GetToken()) + req.Header.Add("Authorization", tokenHeader) + + // run it and capture the response + var respData IssuesListResponse + if err := client.GQL().Run(ctx, req, &respData); err != nil { + return nil, err + } + + issuesData := make([]issues.Issue, len(respData.Repository.Issues.Edges)) + for index, edge := range respData.Repository.Issues.Edges { + + issuesData[index].IssueText = edge.Node.Concreteissue.Title + issuesData[index].IssueCode = edge.Node.Concreteissue.Shortcode + + issuesData[index].Location.Path = edge.Node.Path + issuesData[index].Location.Position.BeginLine = edge.Node.Beginline + issuesData[index].Location.Position.EndLine = edge.Node.Endline + + issuesData[index].Analyzer.Shortcode = edge.Node.Concreteissue.Analyzer.Shortcode + } + + return issuesData, nil +} diff --git a/deepsource/repository/queries/repository_status.go b/deepsource/repository/queries/repository_status.go new file mode 100644 index 00000000..a7940de9 --- /dev/null +++ b/deepsource/repository/queries/repository_status.go @@ -0,0 +1,79 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/deepsource/repository" + "github.com/deepsourcelabs/graphql" +) + +// Query to fetch the status of the repo data sent as param +const repoStatusQuery = `query RepoStatus($name: String!,$owner: String!, $provider: VCSProviderChoices!){ + repository(name:$name, owner:$owner, provider:$provider){ + isActivated + } + }` + +type RepoStatusParams struct { + Owner string + RepoName string + Provider string +} + +type RepoStatusRequest struct { + Params RepoStatusParams +} + +type RepoStatusResponse struct { + Repository struct { + Isactivated bool `json:"isActivated"` + } `json:"repository"` +} + +// GraphQL client interface +type IGQLClient interface { + GQL() *graphql.Client + GetToken() string +} + +func (r RepoStatusRequest) Do(ctx context.Context, client IGQLClient) (*repository.Meta, error) { + + req := graphql.NewRequest(repoStatusQuery) + req.Var("name", r.Params.RepoName) + req.Var("owner", r.Params.Owner) + req.Var("provider", r.Params.Provider) + + // set header fields + req.Header.Set("Cache-Control", "no-cache") + // Adding jwt as header for auth + tokenHeader := fmt.Sprintf("JWT %s", client.GetToken()) + req.Header.Add("Authorization", tokenHeader) + + // run it and capture the response + var respData RepoStatusResponse + if err := client.GQL().Run(ctx, req, &respData); err != nil { + return nil, err + } + + // Formatting the query response w.r.t the repository.Meta structure + // defined in `repository.go` + repositoryData := repository.Meta{ + Activated: false, + Name: r.Params.RepoName, + Owner: r.Params.Owner, + Provider: r.Params.Provider, + } + repositoryData.Name = r.Params.RepoName + repositoryData.Owner = r.Params.Owner + repositoryData.Provider = r.Params.Provider + + // Check and set the activation status + if respData.Repository.Isactivated { + repositoryData.Activated = true + } else { + repositoryData.Activated = false + } + + return &repositoryData, nil +} diff --git a/deepsource/repository/repository.go b/deepsource/repository/repository.go new file mode 100644 index 00000000..89fb3cf8 --- /dev/null +++ b/deepsource/repository/repository.go @@ -0,0 +1,8 @@ +package repository + +type Meta struct { + Activated bool // Activation status of the repository. True: Activated. False: Not Activated. + Name string // Name of the repository + Owner string // Owner username of the repository + Provider string // VCS host for the repo. Github/Gitlab/BitBucket supported yet. +} diff --git a/deepsource/transformers/queries/get_transformers.go b/deepsource/transformers/queries/get_transformers.go new file mode 100644 index 00000000..ed7291eb --- /dev/null +++ b/deepsource/transformers/queries/get_transformers.go @@ -0,0 +1,67 @@ +package transformers + +import ( + "context" + "fmt" + + "github.com/deepsourcelabs/cli/deepsource/transformers" + "github.com/deepsourcelabs/graphql" +) + +// Query to list supported Transformers +const listTransformersQuery = ` +{ + transformers{ + edges{ + node{ + name + shortcode + } + } + } +}` + +type TransformersRequest struct{} + +type TransformersResponse struct { + Transformers struct { + Edges []struct { + Node struct { + Name string `json:"name"` + Shortcode string `json:"shortcode"` + } `json:"node"` + } `json:"edges"` + } `json:"transformers"` +} + +// GraphQL client interface +type IGQLClient interface { + GQL() *graphql.Client + GetToken() string +} + +func (t TransformersRequest) Do(ctx context.Context, client IGQLClient) ([]transformers.Transformer, error) { + + req := graphql.NewRequest(listTransformersQuery) + + // set header fields + req.Header.Set("Cache-Control", "no-cache") + // Adding jwt as header for auth + tokenHeader := fmt.Sprintf("JWT %s", client.GetToken()) + req.Header.Add("Authorization", tokenHeader) + + // run it and capture the response + var respData TransformersResponse + if err := client.GQL().Run(ctx, req, &respData); err != nil { + return nil, err + } + + // Formatting the query response w.r.t the SDK response ([]transformers.Transformer) + transformersData := make([]transformers.Transformer, len(respData.Transformers.Edges)) + for index, edge := range respData.Transformers.Edges { + transformersData[index].Name = edge.Node.Name + transformersData[index].Shortcode = edge.Node.Shortcode + } + + return transformersData, nil +} diff --git a/deepsource/transformers/transformers.go b/deepsource/transformers/transformers.go new file mode 100644 index 00000000..a653bd0c --- /dev/null +++ b/deepsource/transformers/transformers.go @@ -0,0 +1,6 @@ +package transformers + +type Transformer struct { + Name string // Name of the Transformer + Shortcode string // Shortcode of the Transformer +} diff --git a/go.mod b/go.mod index 2e7b2076..3f1f9fa2 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,23 @@ module github.com/deepsourcelabs/cli go 1.16 require ( - github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible // indirect - github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7 // indirect - github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4 // indirect - github.com/getsentry/sentry-go v0.11.0 - github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0 // indirect - github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed // indirect + github.com/AlecAivazis/survey/v2 v2.2.12 + github.com/Jeffail/gabs/v2 v2.6.1 + github.com/cli/browser v1.1.0 + github.com/deepsourcelabs/graphql v0.2.2 + github.com/fatih/color v1.12.0 + github.com/getsentry/sentry-go v0.6.0 + github.com/google/go-cmp v0.5.5 // indirect + github.com/matryer/is v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/pelletier/go-toml v1.9.2 + github.com/pterm/pterm v0.12.23 + github.com/spf13/cobra v1.1.3 + github.com/spf13/viper v1.7.1 + github.com/xeipuuv/gojsonschema v1.2.0 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect + golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/go.sum b/go.sum index 1fa4717c..fdd706d9 100644 --- a/go.sum +++ b/go.sum @@ -1,138 +1,312 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AlecAivazis/survey/v2 v2.2.12 h1:5a07y93zA6SZ09gOa9wLVLznF5zTJMQ+pJ3cZK4IuO8= +github.com/AlecAivazis/survey/v2 v2.2.12/go.mod h1:6d4saEvBsfSHXeN1a5OA5m2+HJ2LuVokllnC77pAIKI= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= -github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/Jeffail/gabs/v2 v2.6.1 h1:wwbE6nTQTwIMsMxzi6XFQQYRZ6wDc1mSdxoAN+9U4Gk= +github.com/Jeffail/gabs/v2 v2.6.1/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY= +github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deepsourcelabs/graphql v0.2.2 h1:6CtKGvVSIY6Jnf72VyfXB77AaaoHATBzXsrh64irtVQ= +github.com/deepsourcelabs/graphql v0.2.2/go.mod h1:2hqi4vS0LxP9wMjbkbMOdR/fap2zwDlzqGGO8WEgyBA= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getsentry/sentry-go v0.6.0 h1:kPd+nr+dlXmaarUBg7xlC/qn+7wyMJL6PMsSn5fA+RM= github.com/getsentry/sentry-go v0.6.0/go.mod h1:0yZBuzSvbZwBnvaF9VwZIMen3kXscY8/uasKtAX1qG8= -github.com/getsentry/sentry-go v0.11.0 h1:qro8uttJGvNAMr5CLcFI9CHR0aDzXl0Vs3Pmw/oTPg8= -github.com/getsentry/sentry-go v0.11.0/go.mod h1:KBQIxiZAetw62Cj8Ri964vAEWVdgfaUCn30Q3bCvANo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= -github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= -github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= -github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= -github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= -github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= -github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= -github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= +github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= -github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.2 h1:7NiByeVF4jKSG1lDF3X8LTIkq2/bu+1uYbIm1eS5tzk= +github.com/pelletier/go-toml v1.9.2/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/pterm/pterm v0.12.23 h1:+PL0YqmmT0QiDLOpevE3e2HPb5UIDBxh6OlLm8jDhxg= +github.com/pterm/pterm v0.12.23/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -142,51 +316,184 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 h1:GkvMjFtXUmahfDtashnc1mnrCtuBVcwse5QV2lUk/tI= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/goreleaser.yaml b/goreleaser.yaml index 1e265993..2d6cb31d 100644 --- a/goreleaser.yaml +++ b/goreleaser.yaml @@ -14,8 +14,9 @@ builds: goarch: - 386 - amd64 + - arm64 ldflags: - - "-X main.SentryDSN={{ .Env.DEEPSOURCE_CLI_SENTRY_DSN }}" + - "-X 'main.version={{ .Version }}' -X 'main.SentryDSN={{ .Env.DEEPSOURCE_CLI_SENTRY_DSN }}'" archives: - replacements: @@ -30,3 +31,17 @@ changelog: filters: exclude: - '^tests:' + +brews: + - name: deepsource + - tap: + owner: deepsourcelabs + name: homebrew-cli + token: "{{ .Env.HOMEBREW_TOKEN }}" + commit_author: + name: deepsourcebot + email: bot@deepsource.io + homepage: "https://github.com/deepsourcelabs/cli" + description: "Command line interface to DeepSource" + license: "BSD 2-Clause Simplified License" + skip_upload: auto diff --git a/init.go b/init.go deleted file mode 100644 index 45b925ae..00000000 --- a/init.go +++ /dev/null @@ -1,260 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "io/ioutil" - "os" - "strings" - "time" - - "github.com/getsentry/sentry-go" -) - -var SentryDSN string - -func run() int { - // Print default stub message - fmt.Printf("DeepSource Command Line Interface " + cliVersion + "\n \n") - - err := sentry.Init(sentry.ClientOptions{ - Dsn: SentryDSN, - }) - if err != nil { - fmt.Println("Could not load sentry.") - } - - flag.Usage = func() { - fmt.Println(commonUsageMessage) - } - - // Verify the env variables - dsn := os.Getenv("DEEPSOURCE_DSN") - if dsn == "" { - fmt.Println("DeepSource | Error | Environment variable DEEPSOURCE_DSN not set (or) is empty. You can find it under the repository settings page.") - return 1 - } - sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetUser(sentry.User{ID: dsn}) - }) - - ///////////////////// - // Command: report // - ///////////////////// - - reportCommand := flag.NewFlagSet("report", flag.ExitOnError) - reportCommandAnalyzerShortcode := reportCommand.String("analyzer", "", "") - reportCommandKey := reportCommand.String("key", "default", "") - reportCommandValue := reportCommand.String("value", "", "") - reportCommandValueFile := reportCommand.String("value-file", "", "") - - reportCommand.Usage = func() { - fmt.Println(reportUsageMessage) - } - - // Parse flags to extract its values - flag.Parse() - - // Check for subcommands - if len(os.Args) == 1 { - fmt.Println(commonUsageMessage) - return 1 - } - - // Switch on the subcommand - switch os.Args[1] { - case "report": - reportCommand.Parse(os.Args[2:]) - default: - fmt.Println(reportUsageMessage) - return 1 - } - sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetExtra("Args", os.Args) - }) - - // Get current path - currentDir, err := os.Getwd() - if err != nil { - fmt.Println("DeepSource | Error | Unable to identify current directory.") - sentry.CaptureException(err) - return 0 - } - sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetExtra("currentDir", currentDir) - }) - - ////////////////// - // Validate DSN // - ////////////////// - - // Protocol - dsnSplitProtocolBody := strings.Split(dsn, "://") - - // Check for valid protocol - if strings.HasPrefix(dsnSplitProtocolBody[0], "http") == false { - err = errors.New("DeepSource | Error | DSN specified should start with http(s). Cross verify DEEPSOURCE_DSN value against the settings page of the repository.") - fmt.Println(err) - sentry.CaptureException(err) - return 1 - } - dsnProtocol := dsnSplitProtocolBody[0] - - // Parse body of the DSN - dsnSplitTokenHost := strings.Split(dsnSplitProtocolBody[1], "@") - - // Set values parsed from DSN - dsnHost := dsnSplitTokenHost[1] - - /////////////////////// - // Generate metadata // - /////////////////////// - - // Access token - dsnAccessToken := dsnSplitTokenHost[0] - - // Head Commit OID - headCommitOID, err := gitGetHead(currentDir) - if err != nil { - fmt.Println("DeepSource | Error | Unable to get commit OID HEAD. Make sure you are running the CLI from a git repository") - sentry.CaptureException(err) - return 1 - } - sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetExtra("headCommitOID", headCommitOID) - }) - - if reportCommand.Parsed() { - // Flag validation - if *reportCommandValue == "" && *reportCommandValueFile == "" { - fmt.Println("DeepSource | Error | '--value' (or) '--value-file' not passed") - return 1 - } - - var analyzerShortcode string - var artifactKey string - var artifactValue string - - analyzerShortcode = *reportCommandAnalyzerShortcode - artifactKey = *reportCommandKey - - if *reportCommandValue != "" { - artifactValue = *reportCommandValue - } - - if *reportCommandValueFile != "" { - // Check file size - fileStat, err := os.Stat(*reportCommandValueFile) - if err != nil { - fmt.Println("DeepSource | Error | Unable to read specified value file: " + *reportCommandValueFile) - sentry.CaptureException(err) - return 1 - } - - if fileStat.Size() > 5000000 { - fmt.Println("DeepSource | Error | Value file too large. Should be less than 5 Megabytes") - return 1 - } - - valueBytes, err := ioutil.ReadFile(*reportCommandValueFile) - if err != nil { - fmt.Println("DeepSource | Error | Unable to read specified value file: ", *reportCommandValueFile) - sentry.CaptureException(err) - return 1 - } - - artifactValue = string(valueBytes) - } - - //////////////////// - // Generate query // - //////////////////// - - reportMeta := make(map[string]string) - reportMeta["workDir"] = currentDir - - query := ReportQuery{ - Query: reportGraphqlQuery, - } - - queryInput := ReportQueryInput{ - AccessToken: dsnAccessToken, - CommitOID: headCommitOID, - ReporterName: "cli", - ReporterVersion: cliVersion, - Key: artifactKey, - Data: artifactValue, - AnalyzerShortcode: analyzerShortcode, - Metadata: reportMeta, - } - - query.Variables.Input = queryInput - - // Marshal request body - queryBodyBytes, err := json.Marshal(query) - if err != nil { - fmt.Println("DeepSource | Error | Unable to marshal query body.") - sentry.CaptureException(err) - return 0 - } - - queryResponseBody, err := makeQuery( - dsnProtocol+"://"+dsnHost+"/graphql/cli/", - queryBodyBytes, - "application/json", - ) - if err != nil { - fmt.Println("DeepSource | Error | Reporting failed | ", err) - sentry.CaptureException(err) - return 0 - } - - // Parse query response body - queryResponse := QueryResponse{} - - err = json.Unmarshal(queryResponseBody, &queryResponse) - if err != nil { - fmt.Println("DeepSource | Error | Unable to parse response body.") - sentry.CaptureException(err) - return 0 - } - - // Check for errors in response body - // Response format: - // { - // "data": { - // "createArtifact": { - // "ok": false, - // "error": "No repository found attached with the access token: dasdsds" - // } - // } - // } - - if queryResponse.Data.CreateArtifact.Ok != true { - fmt.Println("DeepSource | Error | Reporting failed | ", queryResponse.Data.CreateArtifact.Error) - sentry.CaptureException(errors.New(queryResponse.Data.CreateArtifact.Error)) - return 0 - } - - if err != nil { - fmt.Println("DeepSource | Error | Unable to report results.") - sentry.CaptureException(err) - return 0 - } - - fmt.Printf("DeepSource | Artifact published successfully \n \n") - fmt.Printf("Analyzer %s \n", analyzerShortcode) - fmt.Printf("Key %s \n", artifactKey) - - return 0 - } - return 0 -} - -func main() { - returnCode := run() - defer os.Exit(returnCode) - defer sentry.Flush(2 * time.Second) -} diff --git a/utils/cmd_validator.go b/utils/cmd_validator.go new file mode 100644 index 00000000..d1f60906 --- /dev/null +++ b/utils/cmd_validator.go @@ -0,0 +1,66 @@ +package utils + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" +) + +// Validates if the number of args passed to a command is exactly same as that required +func ExactArgs(count int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + + arg := "argument" + if count > 1 { + arg = "arguments" + } + + errorMsg := fmt.Sprintf("`%s` requires exactly %d %s. Got %d. Please see `%s --help` for the supported flags and their usage.", + cmd.CommandPath(), + count, + arg, + len(args), + cmd.CommandPath()) + + if len(args) != count { + return errors.New(errorMsg) + } + return nil + } +} + +func MaxNArgs(count int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + + arg := "argument" + if count > 1 { + arg = "arguments" + } + + errorMsg := fmt.Sprintf("`%s` requires maximum %d %s. Got %d. Please see `%s --help` for the supported flags and their usage.", + cmd.CommandPath(), + count, + arg, + len(args), + cmd.CommandPath()) + + if len(args) > count { + return errors.New(errorMsg) + } + return nil + } +} + +// Validates if there is any arg passed to a command which doesn't require any +func NoArgs(cmd *cobra.Command, args []string) error { + + errorMsg := fmt.Sprintf("`%s` does not require any argument. Please see `%s --help` for the supported flags and their usage.", + cmd.CommandPath(), + cmd.CommandPath()) + + if len(args) > 0 { + return errors.New(errorMsg) + } + return nil +} diff --git a/utils/fetch_analyzers_transformers.go b/utils/fetch_analyzers_transformers.go new file mode 100644 index 00000000..3e62d14e --- /dev/null +++ b/utils/fetch_analyzers_transformers.go @@ -0,0 +1,80 @@ +package utils + +import ( + "context" + + "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/deepsource/analyzers" + "github.com/deepsourcelabs/cli/deepsource/transformers" +) + +type DeepSourceAnalyzersData struct { + AnalyzerNames []string + AnalyzerShortcodes []string + AnalyzersMap map[string]string // Map for {analyzer name : shortcode} + AnalyzersMeta []string + AnalyzersMetaMap map[string]string // Map for {analyzer name: analyzer meta-schema} +} + +type DeepSourceTransformersData struct { + TransformerNames []string + TransformerShortcodes []string + TransformerMap map[string]string // Map for {transformer name:shortcode} +} + +var AnalyzersData DeepSourceAnalyzersData +var TransformersData DeepSourceTransformersData + +var analyzersAPIResponse []analyzers.Analyzer +var transformersAPIResponse []transformers.Transformer + +// Get the list of all the supported analyzers and transformers with +// their corresponding data like shortcode, metaschema etc. +func GetAnalyzersAndTransformersData(ctx context.Context, deepsource deepsource.Client) (err error) { + // Get supported analyzers and transformers data + AnalyzersData.AnalyzersMap = make(map[string]string) + TransformersData.TransformerMap = make(map[string]string) + + analyzersAPIResponse, err = deepsource.GetSupportedAnalyzers(ctx) + if err != nil { + return err + } + + transformersAPIResponse, err = deepsource.GetSupportedTransformers(ctx) + if err != nil { + return err + } + parseSDKResponse() + return nil +} + +// Parses the SDK response of analyzers and transformers data into the format required +// by the validator and generator package +func parseSDKResponse() { + analyzersMap := make(map[string]string) + analyzersMetaMap := make(map[string]string) + transformersMap := make(map[string]string) + + for _, analyzer := range analyzersAPIResponse { + analyzersMap[analyzer.Name] = analyzer.Shortcode + analyzersMetaMap[analyzer.Shortcode] = analyzer.MetaSchema + + AnalyzersData = DeepSourceAnalyzersData{ + AnalyzerNames: append(AnalyzersData.AnalyzerNames, analyzer.Name), + AnalyzerShortcodes: append(AnalyzersData.AnalyzerShortcodes, analyzer.Shortcode), + AnalyzersMap: analyzersMap, + AnalyzersMeta: append(AnalyzersData.AnalyzersMeta, analyzer.MetaSchema), + AnalyzersMetaMap: analyzersMetaMap, + } + } + + for _, transformer := range transformersAPIResponse { + transformersMap[transformer.Name] = transformer.Shortcode + + TransformersData = DeepSourceTransformersData{ + TransformerNames: append(TransformersData.TransformerNames, transformer.Name), + TransformerShortcodes: append(TransformersData.TransformerShortcodes, transformer.Shortcode), + TransformerMap: transformersMap, + } + } +} diff --git a/utils/fetch_remote.go b/utils/fetch_remote.go new file mode 100644 index 00000000..3ce66709 --- /dev/null +++ b/utils/fetch_remote.go @@ -0,0 +1,121 @@ +package utils + +import ( + "fmt" + "net/url" + "os/exec" + "regexp" + "strings" +) + +// 1. Run git remote -v +// 2. Parse the output and get the list of remotes +// 3. Do git config --get --local remote..url +// 4. Parse the urls to filter out reponame,owner,provider +// 5. Send them back + +// Returns a map of remotes to their urls +// { "origin":["reponame","owner","provider"]} +// { "upstream":["reponame","owner","provider"]} +func ListRemotes() (map[string][]string, error) { + + remoteMap := make(map[string][]string) + + remotes, err := runCmd("git", []string{"remote", "-v"}) + if err != nil { + return remoteMap, err + } + + // Split the remotes into single remote array + remoteList := strings.Split(string(remotes), "\n") + + if len(remoteList) <= 1 { + return remoteMap, fmt.Errorf("No remotes found") + } + + // Removing the last blank element + remoteList = remoteList[:len(remoteList)-1] + + // Iterating over the remotes to parse values + for _, remoteData := range remoteList { + + var VCSProvider string + + // Split the single remote to fetch the name of the remote + // TLDR; Fetch "origin" from "origin " + remoteParams := strings.Split(remoteData, "\t") + remoteName := remoteParams[0] + + // Making the argument for the upcoming command to fetch remote url + urlCmdString := fmt.Sprintf("remote.%s.url", remoteParams[0]) + + // Fetch the remote URL using the command "git config --get --local remote..url" + rUrl, err := runCmd("git", []string{"config", "--get", "--local", "--null", urlCmdString}) + if err != nil { + return remoteMap, err + } + remoteURL := string(rUrl) + + // Parsing out VCS Provider from the remote URL + if strings.Contains(remoteURL, "github") { + VCSProvider = "GITHUB" + } else if strings.Contains(remoteURL, "gitlab") { + VCSProvider = "GITLAB" + } else if strings.Contains(remoteURL, "bitbucket") { + VCSProvider = "BITBUCKET" + } else { + continue + } + + var RepoNameRegexp = regexp.MustCompile(`.+/([^/]+)(\.git)?$`) + + // Parsing out repository name from the remote URL using the above regex + matched := RepoNameRegexp.FindStringSubmatch(remoteURL) + repositoryName := strings.TrimSuffix(matched[1], ".git") + + var owner string + + // git@ ssh urls + if strings.HasPrefix(remoteURL, "git@") { + + pathURL := strings.Split(remoteURL, ":") + newPathURL := pathURL[1] + u, err := url.Parse(newPathURL) + if err != nil { + continue + } + splitPath := strings.Split(u.Path, "/") + owner = splitPath[0] + } else if strings.HasPrefix(remoteURL, "https://") { + u, err := url.Parse(remoteURL) + if err != nil { + continue + } + splitPath := strings.Split(u.Path, "/") + + owner = splitPath[0] + } + + // owner, err := runCmd("git", []string{"config", "--get", "--null", "user.name"}) + // if err != nil { + // continue + // } + + completeRepositoryName := fmt.Sprintf("%s/%s", owner, repositoryName) + remoteMap[remoteName] = []string{owner, repositoryName, VCSProvider, completeRepositoryName} + } + + return remoteMap, nil +} + +func runCmd(command string, args []string) (string, error) { + + output, err := exec.Command(command, args...).Output() + if err != nil { + return "", err + } + + // Removing trailing null characters + return strings.TrimRight(string(output), "\000"), nil + +} diff --git a/utils/prompt.go b/utils/prompt.go new file mode 100644 index 00000000..976cc059 --- /dev/null +++ b/utils/prompt.go @@ -0,0 +1,101 @@ +package utils + +import ( + "errors" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" +) + +// ========== +// Useful APIs of survey library +// ========== + +// Used for (Yes/No) questions +func ConfirmFromUser(msg, helpText string) (bool, error) { + response := false + confirmPrompt := &survey.Confirm{ + Renderer: survey.Renderer{}, + Message: msg, + Default: true, + Help: helpText, + } + + err := survey.AskOne(confirmPrompt, &response) + if err != nil { + return true, checkInterrupt(err) + } + return response, nil +} + +// Used for Single Option Selection from Multiple Options +// Being used for selecting Java version for configuring meta of Java analyzer +// > * 1 +// * 2 +// * 3 +func SelectFromOptions(msg, helpText string, opts []string) (string, error) { + var result string + prompt := &survey.Select{ + Renderer: survey.Renderer{}, + Message: msg, + Options: opts, + Default: nil, + Help: helpText, + } + err := survey.AskOne(prompt, &result) + if err != nil { + return "", checkInterrupt(err) + } + return result, nil +} + +// Used for Single Line Text Input +// Being used for getting "Import root" of user for configuring meta of Go analyzer +func GetSingleLineInput(msg, helpText string) (string, error) { + response := "" + prompt := &survey.Input{ + Renderer: survey.Renderer{}, + Message: msg, + Default: "", + Help: helpText, + } + + err := survey.AskOne(prompt, &response) + if err != nil { + return "", checkInterrupt(err) + } + return response, nil +} + +// Used for multiple inputs from the displayed options +// Example: +// ? Which languages/tools does your project use? +// > [ ] Shell +// [ ] Rust +// [ ] Test Coverage +// [ ] Python +// [ ] Go +func SelectFromMultipleOptions(msg, helpText string, options []string) ([]string, error) { + response := make([]string, 0) + // Extracting languages and tools being used in the project for Analyzers + analyzerPrompt := &survey.MultiSelect{ + Renderer: survey.Renderer{}, + Message: msg, + Options: options, + Help: helpText, + } + err := survey.AskOne(analyzerPrompt, &response, survey.WithValidator(survey.Required)) + if err != nil { + return nil, checkInterrupt(err) + } + return response, nil +} + +// Utility to check for Ctrl+C interrupts +// Survey library doesn't exit on Ctrl+c interrupt. This handler helps in that. +func checkInterrupt(err error) error { + if err == terminal.InterruptErr { + return errors.New("Interrupt received. Exiting...") + } + return err +} diff --git a/utils/remote_resolver.go b/utils/remote_resolver.go new file mode 100644 index 00000000..aab332fd --- /dev/null +++ b/utils/remote_resolver.go @@ -0,0 +1,94 @@ +package utils + +import ( + "fmt" + "strings" +) + +type RemoteData struct { + Owner string + RepoName string + VCSProvider string +} + +func ResolveRemote(repoArg string) (*RemoteData, error) { + var remote RemoteData + + // If the user supplied a --repo flag with the repo URL + if repoArg != "" { + repoData, err := RepoArgumentResolver(repoArg) + if err != nil { + return nil, err + } + remote.VCSProvider = repoData[0] + remote.Owner = repoData[1] + remote.RepoName = repoData[2] + return &remote, nil + } + + // If the user didn't pass --repo flag + // Figure out list of remotes by reading git config + remotesData, err := ListRemotes() + if err != nil { + if strings.Contains(err.Error(), "exit status 128") { + fmt.Println("This repository has not been initialized with git. Please initialize it with git using `git init`") + } + return nil, err + } + + // If there is only one remote, use it + if len(remotesData) == 1 { + for _, value := range remotesData { + remote.Owner = value[0] + remote.RepoName = value[1] + remote.VCSProvider = value[2] + } + return &remote, nil + } + + // If there are more than one remotes, give the option to user + // to select the one which they want + var promptOpts []string + // Preparing the options to show to the user + for _, value := range remotesData { + promptOpts = append(promptOpts, value[3]) + } + + selectedRemote, err := SelectFromOptions("Please select the repository:", "", promptOpts) + if err != nil { + return nil, err + } + + // Matching the list of remotes with the one selected by user + for _, value := range remotesData { + if value[3] == selectedRemote { + remote.Owner = value[0] + remote.RepoName = value[1] + remote.VCSProvider = value[2] + } + } + return &remote, nil +} + +// Utility to parse the --repo flag +func RepoArgumentResolver(arg string) ([]string, error) { + + // github.com/deepsourcelabs/cli or gh/deepsourcelabs/cli + + argComponents := strings.Split(arg, "/") + + switch argComponents[0] { + case "gh", "github.com": + argComponents[0] = "GITHUB" + + case "gl", "gitlab.com": + argComponents[0] = "GITLAB" + + case "bb", "bitbucket.com": + argComponents[0] = "BITBUCKET" + default: + return argComponents, fmt.Errorf("VCSProvider `%s` not supported", argComponents[0]) + } + + return argComponents, nil +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 00000000..992fbbc2 --- /dev/null +++ b/version/version.go @@ -0,0 +1,45 @@ +package version + +import ( + "fmt" + "time" +) + +var buildInfo *BuildInfo + +// BuildInfo describes the compile time information. +type BuildInfo struct { + // Version is the current semver. + Version string `json:"version,omitempty"` + // Date is the build date. + Date time.Time `json:"date,omitempty"` + // gitTreeState is the state of the git tree. + GitTreeState string `json:"git_tree_state,omitempty"` + // GitCommit is the git sha1. + GitCommit string `json:"git_commit,omitempty"` +} + +// Set's the build info as a package global. +func SetBuildInfo(version, dateStr, gitTreeState, gitCommit string) { + date, _ := time.Parse("2006-01-02", dateStr) + + buildInfo = &BuildInfo{ + Version: version, + Date: date, + GitTreeState: gitTreeState, + GitCommit: gitCommit, + } +} + +// GetBuildInfo returns the package global `buildInfo` +func GetBuildInfo() *BuildInfo { + return buildInfo +} + +func (bi BuildInfo) String() string { + if bi.Date.IsZero() { + return fmt.Sprintf("DeepSource CLI version %s", bi.Version) + } + dateStr := bi.Date.Format("2006-01-02") + return fmt.Sprintf("DeepSource CLI version %s (%s)", bi.Version, dateStr) +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 00000000..ab69fcac --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,87 @@ +package version + +import ( + "reflect" + "testing" + "time" +) + +func TestBuildInfo_String(t *testing.T) { + + date, _ := time.Parse("2006-01-02", "2021-01-21") + + type fields struct { + Version string + Date time.Time + GitTreeState string + GitCommit string + } + tests := []struct { + name string + fields fields + want string + }{ + { + "must return the correct version string when date and version is available", + fields{ + Version: "1.5.0", + Date: date, + }, + "DeepSource CLI version 1.5.0 (2021-01-21)", + }, + { + "must return the correct version string when only version is available", + fields{ + Version: "1.5.0", + }, + "DeepSource CLI version 1.5.0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bi := BuildInfo{ + Version: tt.fields.Version, + Date: tt.fields.Date, + GitTreeState: tt.fields.GitTreeState, + GitCommit: tt.fields.GitCommit, + } + if got := bi.String(); got != tt.want { + t.Errorf("BuildInfo.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSetBuildInfo(t *testing.T) { + date, _ := time.Parse("2006-01-02", "2021-01-21") + + want := &BuildInfo{ + Version: "1.0.0", + Date: date, + } + + type args struct { + version string + dateStr string + gitTreeState string + gitCommit string + } + tests := []struct { + name string + args args + }{ + { + "must set the buildInfo package global", + args{version: "1.0.0", dateStr: "2021-01-21"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + SetBuildInfo(tt.args.version, tt.args.dateStr, tt.args.gitTreeState, tt.args.gitCommit) + + }) + if !reflect.DeepEqual(buildInfo, want) { + t.Errorf("buildInfo = %v, want %v", buildInfo, want) + } + } +}