diff --git a/.circleci/config.yml b/.circleci/config.yml index edd87a19d..8a1b5456c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -137,6 +137,10 @@ jobs: working_directory: integration_tests environment: TESTING: "true" + - run: + name: "Make sure simple command do not cause any timeout" + command: circleci version + test: executor: go steps: @@ -378,12 +382,14 @@ workflows: - devex-release - deploy: requires: + - cucumber - test - test_mac - coverage - lint - deploy-test - shellcheck/check + - vulnerability-scan filters: branches: only: main diff --git a/Makefile b/Makefile index 604d50148..a51a58c4a 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ GOOS=$(shell go env GOOS) GOARCH=$(shell go env GOARCH) build: always - go build -o build/$(GOOS)/$(GOARCH)/circleci + go build -o build/$(GOOS)/$(GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=https://api.segment.io' build-all: build/linux/amd64/circleci build/darwin/amd64/circleci diff --git a/README.md b/README.md index a955cff70..b57b73873 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,20 @@ Please see the [documentation](https://circleci-public.github.io/circleci-cli) o ## Server compatibility -There are some difference of behavior depending on the version you use: - - config validation will use the GraphQL API until **Server v4.0.5, v4.1.3, v4.2.0**. The above versions will use the new route `compile-config-with-defaults` - - `circleci orb validate` will only allow you to validate orbs using other private orbs with the option `--org-slug` from version **Server v4.2.0** +| Functionality | Impacted commands | Change description | Compatibility with Server | +| --- | --- | --- | --- | +| Config compilation and validation | <ul><li>`circleci config validate`</li><li>`circleci config process`</li><li>`circleci local execute`</li> | The config validation has been moved from the GraphQL API to a specific API endpoint | <ul><li>**Server v4.0.5, v4.1.3, v4.2.0 and above**: Commands use the new specific endpoint</li><li>**Previous version**: Commands use the GraphQL API</li></ul> | +| Orb compilation and validation of orb using private orbs | <ul><li>`circleci orb process`</li><li>`circleci orb validate`</li></ul> | To support the validation of orbs requesting private orbs (see [issue](https://github.com/CircleCI-Public/circleci-cli/issues/751)). A field `ownerId` has been added to the GraphQL orb validation endpoint. Thus allowing the `Impacted commands` to use the `--org-id` parameter to enable the orb compilation / validation | <ul><li>**Server v4.2.0 and above**: The field is accessible so you can use the parameter</li><li>**Previous versions**: The field does not exist making the functionality unavailable</li></ul> | + +## Telemetry + +The CircleCI CLI includes a telemetry feature that collects basic errors and feature usage data in order to help us improve the experience for everyone. + +Telemetry works on an opt-in basis: when running a command for the first time, you will be asked for consent to enable telemetry. For non-TTY STDIN, telemetry is disabled by default, ensuring that scripts that use the CLI run smoothly. + +You can disable or enable telemetry anytime in one of the following ways: + +* Run the commands `circleci telemetry enable` or `circleci telemetry disable` + +* Set the `CIRCLECI_CLI_TELEMETRY_OPTOUT` environment variable to `1` or `true` to disable it + diff --git a/Taskfile.yml b/Taskfile.yml index ad2d19df9..3cb8bfffc 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -47,14 +47,16 @@ tasks: build: desc: Build main cmds: - - go build -v -o build/darwin/amd64/circleci . - + # LDFlags sets the segment endpoint to an empty string thus letting the analytics library set the default endpoint on its own + # Not setting the `SegmentEndpoint` variable would let the value in the code ie "http://localhost" + - go build -v -o build/$(go env GOOS)/$(go env GOARCH)/circleci -ldflags='-X github.com/CircleCI-Public/circleci-cli/telemetry.SegmentEndpoint=https://api.segment.io' . + build-linux: desc: Build main cmds: - go build -v -o build/linux/amd64/circleci . - + cover: desc: tests and generates a cover profile cmds: - - TESTING=true go test -race -coverprofile=coverage.txt ./... \ No newline at end of file + - TESTING=true go test -race -coverprofile=coverage.txt ./... diff --git a/api/api.go b/api/api.go index 2dbaca0bd..846626465 100644 --- a/api/api.go +++ b/api/api.go @@ -6,11 +6,13 @@ import ( "io" "log" "net/http" + "net/url" "os" "sort" "strings" "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/references" "github.com/CircleCI-Public/circleci-cli/settings" "github.com/Masterminds/semver" @@ -1818,3 +1820,20 @@ func FollowProject(config settings.Config, vcs string, owner string, projectName return fr, nil } + +type Me struct { + ID string `json:"id"` + Login string `json:"login"` + Name string `json:"name"` +} + +func GetMe(client *rest.Client) (Me, error) { + req, err := client.NewRequest("GET", &url.URL{Path: "me"}, nil) + if err != nil { + return Me{}, errors.Wrap(err, "Unable to get user info") + } + + var me Me + _, err = client.DoRequest(req, &me) + return me, err +} diff --git a/clitest/clitest.go b/clitest/clitest.go index 6a0010d5c..24b0b25bc 100644 --- a/clitest/clitest.go +++ b/clitest/clitest.go @@ -11,9 +11,11 @@ import ( "runtime" "github.com/CircleCI-Public/circleci-cli/api/graphql" + "github.com/CircleCI-Public/circleci-cli/settings" "github.com/onsi/gomega/gexec" "github.com/onsi/gomega/ghttp" "github.com/onsi/gomega/types" + "gopkg.in/yaml.v3" "github.com/onsi/gomega" ) @@ -30,10 +32,12 @@ func ShouldFail() types.GomegaMatcher { // TempSettings contains useful settings for testing the CLI type TempSettings struct { - Home string - TestServer *ghttp.Server - Config *TmpFile - Update *TmpFile + Home string + TestServer *ghttp.Server + Config *TmpFile + Update *TmpFile + Telemetry *TmpFile + TelemetryDestPath string } // Close should be called in an AfterEach and cleans up the temp directory and server process @@ -68,6 +72,16 @@ func WithTempSettings() *TempSettings { gomega.Expect(os.Mkdir(settingsPath, 0700)).To(gomega.Succeed()) tempSettings.Config = OpenTmpFile(settingsPath, "cli.yml") + tempSettings.Telemetry = OpenTmpFile(settingsPath, "telemetry.yml") + content, err := yaml.Marshal(settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + }) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + _, err = tempSettings.Telemetry.File.Write(content) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + tempSettings.TelemetryDestPath = filepath.Join(tempSettings.Home, "telemetry-content") + tempSettings.Update = OpenTmpFile(settingsPath, "update_check.yml") tempSettings.TestServer = ghttp.NewServer() diff --git a/clitest/telemetry.go b/clitest/telemetry.go new file mode 100644 index 000000000..fc21f85ce --- /dev/null +++ b/clitest/telemetry.go @@ -0,0 +1,19 @@ +package clitest + +import ( + "encoding/json" + "os" + + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/onsi/gomega" +) + +func CompareTelemetryEvent(settings *TempSettings, expected []telemetry.Event) { + content, err := os.ReadFile(settings.TelemetryDestPath) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + result := []telemetry.Event{} + err = json.Unmarshal(content, &result) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(result).To(gomega.Equal(expected)) +} diff --git a/cmd/build.go b/cmd/build.go index c32ed969c..df4d110c4 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/CircleCI-Public/circleci-cli/local" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" ) @@ -16,7 +17,14 @@ func newLocalExecuteCommand(config *settings.Config) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, _ []string) error { - return local.Execute(cmd.Flags(), config, args) + err := local.Execute(cmd.Flags(), config, args) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateLocalExecuteEvent(telemetry.GetCommandInformation(cmd, true))) + } + + return err }, Args: cobra.MinimumNArgs(1), } diff --git a/cmd/cmd_suite_test.go b/cmd/cmd_suite_test.go index 5f126d986..5e54304ca 100644 --- a/cmd/cmd_suite_test.go +++ b/cmd/cmd_suite_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "testing" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -14,6 +15,8 @@ import ( var pathCLI string var _ = BeforeSuite(func() { + SetDefaultEventuallyTimeout(time.Second * 30) + var err error pathCLI, err = gexec.Build("github.com/CircleCI-Public/circleci-cli") Ω(err).ShouldNot(HaveOccurred()) diff --git a/cmd/completion.go b/cmd/completion.go index bc0ff6d53..edeaa58d4 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -3,13 +3,21 @@ package cmd import ( "os" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" ) -func newCompletionCommand() *cobra.Command { +func newCompletionCommand(config *settings.Config) *cobra.Command { completionCmd := &cobra.Command{ Use: "completion", Short: "Generate shell completion scripts", + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateCompletionCommand(telemetry.GetCommandInformation(cmd, true))) + } + }, Run: func(cmd *cobra.Command, _ []string) { err := cmd.Help() if err != nil { diff --git a/cmd/config.go b/cmd/config.go index 690fc78d1..8fdba6194 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,14 +8,14 @@ import ( "github.com/CircleCI-Public/circleci-config/labeling" "github.com/CircleCI-Public/circleci-config/labeling/codebase" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - "github.com/CircleCI-Public/circleci-cli/config" "github.com/CircleCI-Public/circleci-cli/filetree" "github.com/CircleCI-Public/circleci-cli/proxy" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) // Path to the config.yml file to operate on. @@ -37,8 +37,14 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { packCommand := &cobra.Command{ Use: "pack <path>", Short: "Pack up your CircleCI configuration into a single file.", - RunE: func(_ *cobra.Command, args []string) error { - return packConfig(args) + RunE: func(cmd *cobra.Command, args []string) error { + err := packConfig(args) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err)) + } + return err }, Args: cobra.ExactArgs(1), Annotations: make(map[string]string), @@ -64,13 +70,20 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { if len(args) == 1 { path = args[0] } - return compiler.ValidateConfig(config.ValidateConfigOpts{ + + err = compiler.ValidateConfig(config.ValidateConfigOpts{ ConfigPath: path, OrgID: orgID, OrgSlug: orgSlug, IgnoreDeprecatedImages: ignoreDeprecatedImages, VerboseOutput: verboseOutput, }) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err)) + } + + return err }, Args: cobra.MaximumNArgs(1), Annotations: make(map[string]string), @@ -112,6 +125,10 @@ func newConfigCommand(globalConfig *settings.Config) *cobra.Command { PipelineParamsFilePath: pipelineParamsFilePath, VerboseOutput: verboseOutput, }) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateConfigEvent(telemetry.GetCommandInformation(cmd, true), err)) + } if err != nil { return err } diff --git a/cmd/config_test.go b/cmd/config_test.go index 9168b4146..86c5e8f6d 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -29,12 +30,41 @@ var _ = Describe("Config", func() { tempSettings.Close() }) + Describe("telemetry", func() { + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + command = commandWithHome(pathCLI, tempSettings.Home, + "config", "pack", + "--skip-update-check", + filepath.Join("testdata", "hugo-pack", ".circleci"), + ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + }) + + AfterEach(func() { + tempSettings.Close() + }) + + It("should send telemetry event", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateConfigEvent(telemetry.CommandInfo{ + Name: "pack", + LocalArgs: map[string]string{"help": "false"}, + }, nil), + }) + }) + }) + Describe("a .circleci folder with config.yml and local orbs folder containing the hugo orb", func() { BeforeEach(func() { command = exec.Command(pathCLI, "config", "pack", "--skip-update-check", - "testdata/hugo-pack/.circleci") + filepath.Join("testdata", "hugo-pack", ".circleci")) results = golden.Get(GinkgoT(), filepath.FromSlash("hugo-pack/result.yml")) }) diff --git a/cmd/create_telemetry.go b/cmd/create_telemetry.go new file mode 100644 index 000000000..645dfe675 --- /dev/null +++ b/cmd/create_telemetry.go @@ -0,0 +1,206 @@ +package cmd + +import ( + "fmt" + "os" + "runtime" + + "github.com/CircleCI-Public/circleci-cli/api" + "github.com/CircleCI-Public/circleci-cli/api/rest" + "github.com/CircleCI-Public/circleci-cli/prompt" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/CircleCI-Public/circleci-cli/version" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "golang.org/x/term" +) + +var ( + CreateUUID = func() string { return uuid.New().String() } + isStdinATTY = term.IsTerminal(int(os.Stdin.Fd())) + isStdoutATTY = term.IsTerminal(int(os.Stdout.Fd())) + anonymousUser = telemetry.User{ + UniqueID: "cli-anonymous-telemetry", + } +) + +type telemetryUI interface { + AskUserToApproveTelemetry(message string) bool +} + +type telemetryInteractiveUI struct{} + +func (telemetryInteractiveUI) AskUserToApproveTelemetry(message string) bool { + return prompt.AskUserToConfirmWithDefault(message, true) +} + +type TelemetryAPIClient interface { + GetMyUserId() (string, error) +} + +func CreateAPIClient(config *settings.Config) TelemetryAPIClient { + return telemetryCircleCIAPI{ + cli: rest.NewFromConfig(config.Host, config), + } +} + +type telemetryCircleCIAPI struct { + cli *rest.Client +} + +func (client telemetryCircleCIAPI) GetMyUserId() (string, error) { + me, err := api.GetMe(client.cli) + if err != nil { + return "", err + } + return me.ID, nil +} + +type nullTelemetryAPIClient struct{} + +func (client nullTelemetryAPIClient) GetMyUserId() (string, error) { + panic("Should not be called") +} + +// Make sure the user gave their approval for the telemetry and +func CreateTelemetry(config *settings.Config) telemetry.Client { + mockTelemetry := config.MockTelemetry + if mockTelemetry == "" { + mockTelemetry = os.Getenv("MOCK_TELEMETRY") + } + if mockTelemetry != "" { + return telemetry.CreateFileTelemetry(mockTelemetry) + } + + if config.IsTelemetryDisabled || len(os.Getenv("CIRCLECI_CLI_TELEMETRY_OPTOUT")) != 0 { + return telemetry.CreateClient(telemetry.User{}, false) + } + + var apiClient TelemetryAPIClient = nullTelemetryAPIClient{} + if config.HTTPClient != nil { + apiClient = telemetryCircleCIAPI{ + cli: rest.NewFromConfig(config.Host, config), + } + } + ui := telemetryInteractiveUI{} + + telemetrySettings := settings.TelemetrySettings{} + user := telemetry.User{ + IsSelfHosted: config.Host != defaultHost, + OS: runtime.GOOS, + Version: version.Version, + TeamName: "devex", + } + + loadTelemetrySettings(&telemetrySettings, &user, apiClient, ui) + client := telemetry.CreateClient(user, telemetrySettings.IsEnabled) + + return client +} + +func loadTelemetrySettings(settings *settings.TelemetrySettings, user *telemetry.User, apiClient TelemetryAPIClient, ui telemetryUI) { + err := settings.Load() + if err != nil && !os.IsNotExist(err) { + fmt.Printf("Error loading telemetry configuration: %s\n", err) + } + + user.UniqueID = settings.UniqueID + user.UserID = settings.UserID + + // If we already have telemetry information or that telemetry is explicitly disabled, skip + if settings.HasAnsweredPrompt { + // If we have no user id, we try requesting the user id again + if settings.UserID == "" && settings.IsEnabled { + myID, err := apiClient.GetMyUserId() + if err == nil { + settings.UserID = myID + user.UserID = myID + if err := settings.Write(); err != nil { + fmt.Printf("Error writing telemetry settings to disk: %s\n", err) + } + } + } + + return + } + + // If stdin is not a TTY, stdout is not a TTY or we are in a CI environment, send telemetry event, disable telemetry and return + isDisabledByDefault := !isStdinATTY || !isStdoutATTY || os.Getenv("CI") == trueString + if isDisabledByDefault { + settings.IsEnabled = false + err := telemetry.SendTelemetryApproval(anonymousUser, telemetry.NoStdin) + if err != nil { + fmt.Printf("Error while sending telemetry approval %s\n", err) + } + return + } + + // Else ask user for telemetry approval + fmt.Println("CircleCI would like to collect CLI usage data for product improvement purposes.") + fmt.Println("") + fmt.Println("Participation is voluntary, and your choice can be changed at any time through the command `cli telemetry enable` and `cli telemetry disable`.") + fmt.Println("For more information, please see our privacy policy at https://circleci.com/legal/privacy/.") + fmt.Println("") + settings.IsEnabled = ui.AskUserToApproveTelemetry("Enable telemetry?") + settings.HasAnsweredPrompt = true + + // Make sure we have user info and set them + if settings.IsEnabled { + if settings.UniqueID == "" { + settings.UniqueID = CreateUUID() + } + user.UniqueID = settings.UniqueID + + if settings.UserID == "" { + myID, err := apiClient.GetMyUserId() + if err == nil { + settings.UserID = myID + } + } + user.UserID = settings.UserID + } else { + *user = anonymousUser + } + + // Send telemetry approval event + approval := telemetry.Enabled + if !settings.IsEnabled { + approval = telemetry.Disabled + } + + if err := telemetry.SendTelemetryApproval(*user, approval); err != nil { + fmt.Printf("Unable to send approval telemetry event: %s\n", err) + } + + // Write telemetry + if err := settings.Write(); err != nil { + fmt.Printf("Error writing telemetry settings to disk: %s\n", err) + } +} + +// Utility function used when creating telemetry events. +// It takes a cobra Command and creates a telemetry.CommandInfo of it +// If getParent is true, puts both the command's args in `LocalArgs` and the parent's args +// Else only put the command's args +// Note: child flags overwrite parent flags with same name +func GetCommandInformation(cmd *cobra.Command, getParent bool) telemetry.CommandInfo { + localArgs := map[string]string{} + + parent := cmd.Parent() + if getParent && parent != nil { + parent.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + } + + cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + + return telemetry.CommandInfo{ + Name: cmd.Name(), + LocalArgs: localArgs, + } +} diff --git a/cmd/create_telemetry_test.go b/cmd/create_telemetry_test.go new file mode 100644 index 000000000..92b5b9e9f --- /dev/null +++ b/cmd/create_telemetry_test.go @@ -0,0 +1,321 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/spf13/afero" + "gotest.tools/v3/assert" +) + +func TestLoadTelemetrySettings(t *testing.T) { + // Delete env.CI variable + oldEnvCIValue := os.Getenv("CI") + os.Setenv("CI", "") + defer os.Setenv("CI", oldEnvCIValue) + + // Mock HTTP + userId := "id" + uniqueId := "unique-id" + + // Mock create UUID + oldUUIDCreate := CreateUUID + CreateUUID = func() string { return uniqueId } + defer (func() { CreateUUID = oldUUIDCreate })() + + // Create test cases + type args struct { + closeStdin bool + closeStdout bool + env map[string]string + promptApproval bool + settings settings.TelemetrySettings + } + type want struct { + settings settings.TelemetrySettings + fileNotCreated bool + telemetryEvents []telemetry.Event + } + type testCase struct { + name string + args args + want want + } + + testCases := []testCase{ + { + name: "Prompt approval should be saved in settings", + args: args{ + promptApproval: true, + settings: settings.TelemetrySettings{}, + }, + want: want{ + settings: settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UserID: userId, + UniqueID: uniqueId, + }, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "enabled", + Properties: map[string]interface{}{ + "UUID": uniqueId, + "user_id": userId, + "is_self_hosted": false, + }}, + }, + }, + }, + { + name: "Prompt disapproval should be saved in settings", + args: args{ + promptApproval: false, + settings: settings.TelemetrySettings{}, + }, + want: want{ + settings: settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + }, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "disabled", Properties: map[string]interface{}{ + "UUID": "cli-anonymous-telemetry", + "is_self_hosted": false, + }}, + }, + }, + }, + { + name: "Does not recreate a unique ID if there is one", + args: args{ + promptApproval: true, + settings: settings.TelemetrySettings{ + UniqueID: "other-id", + }, + }, + want: want{ + settings: settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UserID: userId, + UniqueID: "other-id", + }, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "enabled", Properties: map[string]interface{}{ + "UUID": "other-id", + "user_id": userId, + "is_self_hosted": false, + }}, + }, + }, + }, + { + name: "Does not change telemetry settings if user already answered prompt", + args: args{ + settings: settings.TelemetrySettings{ + HasAnsweredPrompt: true, + }, + }, + want: want{ + settings: settings.TelemetrySettings{ + HasAnsweredPrompt: true, + }, + fileNotCreated: true, + }, + }, + { + name: "Does not change telemetry settings if stdin is not a TTY", + args: args{closeStdin: true}, + want: want{ + fileNotCreated: true, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{ + "UUID": "cli-anonymous-telemetry", + "is_self_hosted": false, + }}, + }, + }, + }, + { + name: "Does not change telemetry settings if stdout is not a TTY", + args: args{closeStdout: true}, + want: want{ + fileNotCreated: true, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{ + "UUID": "cli-anonymous-telemetry", + "is_self_hosted": false, + }}, + }, + }, + }, + { + name: "Does not change telemetry settings if env.CI == true", + args: args{env: map[string]string{"CI": "true"}}, + want: want{ + fileNotCreated: true, + telemetryEvents: []telemetry.Event{ + {Object: "cli-telemetry", Action: "disabled_default", Properties: map[string]interface{}{ + "UUID": "cli-anonymous-telemetry", + "is_self_hosted": false, + }}, + }, + }, + }, + { + name: "Should try loading user id if user already answered prompt but has no user id", + args: args{ + settings: settings.TelemetrySettings{ + HasAnsweredPrompt: true, + IsEnabled: true, + }, + }, + want: want{ + settings: settings.TelemetrySettings{ + HasAnsweredPrompt: true, + IsEnabled: true, + UserID: userId, + }, + }, + }, + { + name: "Should consider has_answered_prompt before disabled-default scenarios", + args: args{ + env: map[string]string{"CI": "true"}, + settings: settings.TelemetrySettings{ + HasAnsweredPrompt: true, + IsEnabled: true, + }, + }, + want: want{ + settings: settings.TelemetrySettings{ + HasAnsweredPrompt: true, + IsEnabled: true, + UserID: userId, + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // Mock FS + oldFS := settings.FS.Fs + settings.FS.Fs = afero.NewMemMapFs() + defer (func() { settings.FS.Fs = oldFS })() + + // Mock stdin + oldIsStdinOpen := isStdinATTY + isStdinATTY = !tt.args.closeStdin + defer (func() { isStdinATTY = oldIsStdinOpen })() + + // Mock stdout + oldIsStdoutOpen := isStdoutATTY + isStdoutATTY = !tt.args.closeStdout + defer (func() { isStdoutATTY = oldIsStdoutOpen })() + + // Mock telemetry + telemetryClient := testTelemetry{events: make([]telemetry.Event, 0)} + oldCreateActiveTelemetry := telemetry.CreateActiveTelemetry + telemetry.CreateActiveTelemetry = func(user telemetry.User) telemetry.Client { + telemetryClient.User = user + return &telemetryClient + } + defer (func() { telemetry.CreateActiveTelemetry = oldCreateActiveTelemetry })() + + // Mock env + if tt.args.env != nil { + for k, v := range tt.args.env { + oldEnvValue := os.Getenv(k) + os.Setenv(k, v) + defer os.Setenv(k, oldEnvValue) + } + } + + // Run tested function + loadTelemetrySettings(&tt.args.settings, &telemetry.User{}, telemetryTestAPIClient{userId, nil}, telemetryTestUI{tt.args.promptApproval}) + assert.DeepEqual(t, &tt.args.settings, &tt.want.settings) + + // Verify good telemetry events were sent + expectedEvents := tt.want.telemetryEvents + if expectedEvents == nil { + expectedEvents = []telemetry.Event{} + } + assert.DeepEqual(t, telemetryClient.events, expectedEvents) + + // Verify if settings file exist + exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml")) + assert.NilError(t, err) + assert.Equal(t, exist, !tt.want.fileNotCreated) + if tt.want.fileNotCreated { + return + } + + // Verify settings file content + loaded := settings.TelemetrySettings{} + err = loaded.Load() + assert.NilError(t, err) + assert.DeepEqual(t, &loaded, &tt.want.settings) + }) + } +} + +type testTelemetry struct { + events []telemetry.Event + User telemetry.User +} + +func (cli *testTelemetry) Close() error { return nil } + +func (cli *testTelemetry) Track(event telemetry.Event) error { + newEvent := event + properties := map[string]interface{}{} + if cli.User.UniqueID != "" { + properties["UUID"] = cli.User.UniqueID + } + + if cli.User.UserID != "" { + properties["user_id"] = cli.User.UserID + } + + properties["is_self_hosted"] = cli.User.IsSelfHosted + + if cli.User.OS != "" { + properties["os"] = cli.User.OS + } + + if cli.User.Version != "" { + properties["cli_version"] = cli.User.Version + } + + if cli.User.TeamName != "" { + properties["team_name"] = cli.User.TeamName + } + + if len(properties) > 0 { + newEvent.Properties = properties + } + + cli.events = append(cli.events, newEvent) + return nil +} + +type telemetryTestUI struct { + Approved bool +} + +func (ui telemetryTestUI) AskUserToApproveTelemetry(message string) bool { + return ui.Approved +} + +type telemetryTestAPIClient struct { + id string + err error +} + +func (me telemetryTestAPIClient) GetMyUserId() (string, error) { + return me.id, me.err +} diff --git a/cmd/diagnostic.go b/cmd/diagnostic.go index 071ceb257..b8053d63f 100644 --- a/cmd/diagnostic.go +++ b/cmd/diagnostic.go @@ -7,6 +7,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" ) @@ -28,8 +29,15 @@ func newDiagnosticCommand(config *settings.Config) *cobra.Command { opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, - RunE: func(_ *cobra.Command, _ []string) error { - return diagnostic(opts) + RunE: func(cmd *cobra.Command, _ []string) error { + err := diagnostic(opts) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateDiagnosticEvent(err)) + } + + return err }, } diff --git a/cmd/diagnostic_test.go b/cmd/diagnostic_test.go index f3d48e5d5..21071d3e3 100644 --- a/cmd/diagnostic_test.go +++ b/cmd/diagnostic_test.go @@ -8,6 +8,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" @@ -79,6 +80,24 @@ var _ = Describe("Diagnostic", func() { tempSettings.Close() }) + Describe("telemetry", func() { + It("should send telemetry event", func() { + command = commandWithHome(pathCLI, tempSettings.Home, + "diagnostic", + "--skip-update-check", + "--host", tempSettings.TestServer.URL()) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + tempSettings.Config.Write([]byte(`token: mytoken`)) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateDiagnosticEvent(nil), + }) + }) + }) + Describe("existing config file", func() { Describe("token set in config file", func() { BeforeEach(func() { diff --git a/cmd/follow.go b/cmd/follow.go index 31eb3bbae..9512423ee 100644 --- a/cmd/follow.go +++ b/cmd/follow.go @@ -6,6 +6,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/git" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -53,8 +54,15 @@ func followProjectCommand(config *settings.Config) *cobra.Command { followCommand := &cobra.Command{ Use: "follow", Short: "Attempt to follow the project for the current git repository.", - RunE: func(_ *cobra.Command, _ []string) error { - return followProject(opts) + RunE: func(cmd *cobra.Command, _ []string) error { + err := followProject(opts) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateFollowEvent(err)) + } + + return err }, } return followCommand diff --git a/cmd/info/info.go b/cmd/info/info.go index 1ed3028fe..fce0bad59 100644 --- a/cmd/info/info.go +++ b/cmd/info/info.go @@ -4,6 +4,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/info" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" @@ -41,7 +42,14 @@ func orgInfoCommand(client info.InfoClient, opts infoOptions) *cobra.Command { Long: `View your Organizations' names and ids.`, PreRunE: opts.validator, RunE: func(cmd *cobra.Command, _ []string) error { - return getOrgInformation(cmd, client) + err := getOrgInformation(cmd, client) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateInfoEvent(telemetry.GetCommandInformation(cmd, true), err)) + } + + return err }, Annotations: make(map[string]string), Example: `circleci info org`, diff --git a/cmd/info/info_test.go b/cmd/info/info_test.go index 19bda1417..7b49c8784 100644 --- a/cmd/info/info_test.go +++ b/cmd/info/info_test.go @@ -2,6 +2,7 @@ package info import ( "bytes" + "context" "fmt" "net/http" "net/http/httptest" @@ -9,6 +10,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/spf13/cobra" "gotest.tools/v3/assert" ) @@ -106,6 +108,55 @@ func TestFailedValidator(t *testing.T) { assert.Error(t, err, errorMessage) } +type testTelemetryClient struct { + events []telemetry.Event +} + +func (cli *testTelemetryClient) Track(event telemetry.Event) error { + cli.events = append(cli.events, event) + return nil +} + +func (cli *testTelemetryClient) Close() error { return nil } + +func TestTelemetry(t *testing.T) { + telemetryClient := testTelemetryClient{make([]telemetry.Event, 0)} + // Test server + var serverHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[{"id":"id", "name":"name"}]`)) + } + server := httptest.NewServer(serverHandler) + defer server.Close() + + // Test command + config := &settings.Config{ + Token: "testtoken", + HTTPClient: http.DefaultClient, + Host: server.URL, + } + cmd := NewInfoCommand(config, nil) + cmd.SetArgs([]string{"org"}) + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + cmd.SetContext(telemetry.NewContext(ctx, &telemetryClient)) + + // Execute + err := cmd.Execute() + + assert.NilError(t, err) + + // Read the telemetry events and compare them + assert.DeepEqual(t, telemetryClient.events, []telemetry.Event{ + telemetry.CreateInfoEvent(telemetry.CommandInfo{ + Name: "org", + LocalArgs: map[string]string{"help": "false"}, + }, nil), + }) +} + func defaultValidator(cmd *cobra.Command, args []string) error { return nil } @@ -115,9 +166,10 @@ func scaffoldCMD( validator validator.Validator, ) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) { config := &settings.Config{ - Token: "testtoken", - HTTPClient: http.DefaultClient, - Host: baseURL, + Token: "testtoken", + HTTPClient: http.DefaultClient, + Host: baseURL, + IsTelemetryDisabled: true, } cmd := NewInfoCommand(config, validator) diff --git a/cmd/namespace.go b/cmd/namespace.go index 0e5fb8cc5..37cf6cbab 100644 --- a/cmd/namespace.go +++ b/cmd/namespace.go @@ -8,6 +8,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/google/uuid" "github.com/spf13/cobra" ) @@ -74,7 +75,14 @@ Please note that at this time all namespaces created in the registry are world-r } } - return createNamespace(cmd, opts) + err := createNamespace(cmd, opts) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateNamespaceEvent(telemetry.GetCommandInformation(cmd, true))) + } + + return err }, Args: cobra.RangeArgs(1, 3), Annotations: make(map[string]string), diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go index 189e4901e..216817c96 100644 --- a/cmd/namespace_test.go +++ b/cmd/namespace_test.go @@ -6,6 +6,7 @@ import ( "os/exec" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" @@ -27,6 +28,42 @@ var _ = Describe("Namespace integration tests", func() { tempSettings.Close() }) + Describe("telemetry", func() { + It("sends expected event", func() { + command = exec.Command(pathCLI, + "namespace", "create", + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + "--integration-testing", + "foo-ns", + "--org-id", `"bb604b45-b6b0-4b81-ad80-796f15eddf87"`, + ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + + tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + _, _ = res.Write([]byte(`{"data":{"organization":{"name":"test-org","id":"bb604b45-b6b0-4b81-ad80-796f15eddf87"}}}`)) + }) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateNamespaceEvent(telemetry.CommandInfo{ + Name: "create", + LocalArgs: map[string]string{ + "help": "false", + "integration-testing": "true", + "no-prompt": "false", + "org-id": "\"bb604b45-b6b0-4b81-ad80-796f15eddf87\"", + }, + }), + }) + }) + }) + Context("create, with interactive prompts", func() { Describe("registering a namespace with orgID", func() { BeforeEach(func() { diff --git a/cmd/open.go b/cmd/open.go index 6f1772b1d..2d9183550 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/CircleCI-Public/circleci-cli/git" + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/pkg/browser" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -13,7 +15,7 @@ import ( // errorMessage string containing the error message displayed in both the open command and the follow command var errorMessage = ` -This command is intended to be run from a git repository with a remote named 'origin' that is hosted on Github or Bitbucket only. +This command is intended to be run from a git repository with a remote named 'origin' that is hosted on Github or Bitbucket only. We are not currently supporting any other hosts.` // projectUrl uses the provided values to create the url to open @@ -39,12 +41,19 @@ func openProjectInBrowser() error { } // newOpenCommand creates the cli command open -func newOpenCommand() *cobra.Command { +func newOpenCommand(config *settings.Config) *cobra.Command { openCommand := &cobra.Command{ Use: "open", Short: "Open the current project in the browser.", - RunE: func(_ *cobra.Command, _ []string) error { - return openProjectInBrowser() + RunE: func(cmd *cobra.Command, _ []string) error { + err := openProjectInBrowser() + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateOpenEvent(err)) + } + + return err }, } return openCommand diff --git a/cmd/orb.go b/cmd/orb.go index d7a011eb3..8a82fe60a 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -26,6 +26,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/references" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" "github.com/fatih/color" "github.com/pkg/errors" @@ -389,6 +390,11 @@ Please note that at this time all orbs created in the registry are world-readabl opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateOrbEvent(telemetry.GetCommandInformation(cmd, true))) + } + // PersistentPreRunE overwrites the inherited persistent hook from rootCmd // So we explicitly call it here to retain that behavior. // As of writing this comment, that is only for daily update checks. diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 4804d1d6a..9d26392d7 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -16,6 +16,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" @@ -23,6 +24,57 @@ import ( "github.com/onsi/gomega/gexec" ) +var _ = Describe("Orb telemetry", func() { + var ( + command *exec.Cmd + orb *clitest.TmpFile + tempSettings *clitest.TempSettings + ) + + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + orb = clitest.OpenTmpFile(tempSettings.Home, "orb.yml") + command = exec.Command(pathCLI, + "orb", "validate", orb.Path, + "--skip-update-check", + "--token", "token", + "--host", tempSettings.TestServer.URL(), + ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + }) + + AfterEach(func() { + orb.Close() + tempSettings.Close() + }) + + It("works", func() { + orb.Write([]byte(`{}`)) + + mockOrbIntrospection(true, "", tempSettings) + + tempSettings.TestServer.AppendHandlers(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + _, _ = res.Write([]byte(`{"orbConfig": {"sourceYaml": "{}", "valid": true, "errors": []} }`)) + }) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateOrbEvent(telemetry.CommandInfo{ + Name: "validate", + LocalArgs: map[string]string{ + "org-slug": "", + "help": "false", + "org-id": "", + }, + }), + }) + }) +}) + var _ = Describe("Orb integration tests", func() { Describe("Orb help text", func() { It("shows a link to the docs", func() { diff --git a/cmd/policy/policy.go b/cmd/policy/policy.go index 6ccab181c..19147fbc3 100644 --- a/cmd/policy/policy.go +++ b/cmd/policy/policy.go @@ -26,15 +26,27 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/rest" "github.com/CircleCI-Public/circleci-cli/cmd/validator" "github.com/CircleCI-Public/circleci-cli/config" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/CircleCI-Public/circleci-cli/settings" ) // NewCommand creates the root policy command with all policy subcommands attached. func NewCommand(globalConfig *settings.Config, preRunE validator.Validator) *cobra.Command { cmd := &cobra.Command{ - Use: "policy", - PersistentPreRunE: preRunE, - Short: "Manage security policies", + Use: "policy", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreatePolicyEvent(telemetry.GetCommandInformation(cmd, true))) + } + + if preRunE != nil { + return preRunE(cmd, args) + } + return nil + }, + Short: "Manage security policies", Long: `Policies ensures security of build configs via security policy management framework. This group of commands allows the management of polices to be verified against build configs.`, } diff --git a/cmd/policy/policy_test.go b/cmd/policy/policy_test.go index 227a6a66d..d4c503afc 100644 --- a/cmd/policy/policy_test.go +++ b/cmd/policy/policy_test.go @@ -57,7 +57,7 @@ func TestPushPolicyWithPrompt(t *testing.T) { })) defer svr.Close() - config := &settings.Config{Token: "testtoken", HTTPClient: http.DefaultClient} + config := &settings.Config{Token: "testtoken", HTTPClient: http.DefaultClient, IsTelemetryDisabled: true} cmd := NewCommand(config, nil) buffer := makeSafeBuffer() @@ -1389,10 +1389,11 @@ func TestTestRunner(t *testing.T) { func makeCMD(circleHost string, token string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) { config := &settings.Config{ - Host: circleHost, - Token: token, - RestEndpoint: "/api/v2", - HTTPClient: http.DefaultClient, + Host: circleHost, + Token: token, + RestEndpoint: "/api/v2", + HTTPClient: http.DefaultClient, + IsTelemetryDisabled: true, } cmd := NewCommand(config, nil) diff --git a/cmd/root.go b/cmd/root.go index 942278c69..05e028a99 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "log" "os" @@ -14,6 +15,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/data" "github.com/CircleCI-Public/circleci-cli/md_docs" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -36,16 +38,23 @@ var rootOptions *settings.Config // rootTokenFromFlag stores the value passed in through the flag --token var rootTokenFromFlag string -// Execute adds all child commands to rootCmd and -// sets flags appropriately. This function is called -// by main.main(). It only needs to happen once to -// the rootCmd. -func Execute() { +// Execute adds all child commands to rootCmd, sets the flags appropriately +// and put the telemetry client in the command context. This function is +// called by main.main(). It only needs to happen once to the rootCmd +func Execute() error { header.SetCommandStr(CommandStr()) command := MakeCommands() - if err := command.Execute(); err != nil { - os.Exit(-1) + + telemetryClient := CreateTelemetry(rootOptions) + defer telemetryClient.Close() + + cmdContext := command.Context() + if cmdContext == nil { + cmdContext = context.Background() } + command.SetContext(telemetry.NewContext(cmdContext, telemetryClient)) + + return command.Execute() } // Returns a string (e.g. "circleci context list") indicating what @@ -123,7 +132,7 @@ func MakeCommands() *cobra.Command { Use: "circleci", Long: longHelp, Short: rootHelpShort(rootOptions), - PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { return rootCmdPreRun(rootOptions) }, } @@ -144,7 +153,7 @@ func MakeCommands() *cobra.Command { return validateToken(rootOptions) } - rootCmd.AddCommand(newOpenCommand()) + rootCmd.AddCommand(newOpenCommand(rootOptions)) rootCmd.AddCommand(newTestsCommand()) rootCmd.AddCommand(newContextCommand(rootOptions)) rootCmd.AddCommand(project.NewProjectCommand(rootOptions, validator)) @@ -173,8 +182,9 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newStepCommand(rootOptions)) rootCmd.AddCommand(newSwitchCommand(rootOptions)) rootCmd.AddCommand(newAdminCommand(rootOptions)) - rootCmd.AddCommand(newCompletionCommand()) + rootCmd.AddCommand(newCompletionCommand(rootOptions)) rootCmd.AddCommand(newEnvCmd()) + rootCmd.AddCommand(newTelemetryCommand(rootOptions)) flags := rootCmd.PersistentFlags() @@ -184,8 +194,9 @@ func MakeCommands() *cobra.Command { flags.StringVar(&rootOptions.Endpoint, "endpoint", rootOptions.Endpoint, "URI to your CircleCI GraphQL API endpoint") flags.StringVar(&rootOptions.GitHubAPI, "github-api", "https://api.github.com/", "Change the default endpoint to GitHub API for retrieving updates") flags.BoolVar(&rootOptions.SkipUpdateCheck, "skip-update-check", skipUpdateByDefault(), "Skip the check for updates check run before every command.") + flags.StringVar(&rootOptions.MockTelemetry, "mock-telemetry", "", "The path where telemetry must be written") - hidden := []string{"github-api", "debug", "endpoint"} + hidden := []string{"github-api", "debug", "endpoint", "mock-telemetry"} for _, f := range hidden { if err := flags.MarkHidden(f); err != nil { @@ -227,6 +238,7 @@ func rootCmdPreRun(rootOptions *settings.Config) error { fmt.Printf("Error checking for updates: %s\n", err) fmt.Printf("Please contact support.\n\n") } + return nil } diff --git a/cmd/root_test.go b/cmd/root_test.go index c23982b18..d05a85419 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { Describe("subcommands", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(24)) + Expect(len(commands.Commands())).To(Equal(25)) }) }) @@ -52,8 +52,7 @@ var _ = Describe("Root", func() { Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out).Should(gbytes.Say("update This command is unavailable on your platform")) - + Eventually(session.Out).Should(gbytes.Say("update\\s+This command is unavailable on your platform")) Eventually(session).Should(gexec.Exit(0)) }) @@ -92,8 +91,7 @@ var _ = Describe("Root", func() { Eventually(session.Err.Contents()).Should(BeEmpty()) - Eventually(session.Out).Should(gbytes.Say("update Update the tool to the latest version")) - + Eventually(session.Out).Should(gbytes.Say("update\\s+Update the tool to the latest version")) Eventually(session).Should(gexec.Exit(0)) }) }) diff --git a/cmd/runner/instance.go b/cmd/runner/instance.go index 28f260937..a4da71338 100644 --- a/cmd/runner/instance.go +++ b/cmd/runner/instance.go @@ -9,6 +9,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/runner" "github.com/CircleCI-Public/circleci-cli/cmd/validator" + "github.com/CircleCI-Public/circleci-cli/telemetry" ) func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command { @@ -25,7 +26,17 @@ func newRunnerInstanceCommand(o *runnerOpts, preRunE validator.Validator) *cobra Aliases: []string{"ls"}, Args: cobra.ExactArgs(1), PreRunE: preRunE, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + var err error + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + // We defer the call to be sure the `err` has been filled + defer (func() { + _ = telemetryClient.Track(telemetry.CreateRunnerInstanceEvent(telemetry.GetCommandInformation(cmd, true), err)) + })() + } + runners, err := o.r.GetRunnerInstances(args[0]) if err != nil { return err diff --git a/cmd/runner/resource_class.go b/cmd/runner/resource_class.go index 1a727737e..749428a74 100644 --- a/cmd/runner/resource_class.go +++ b/cmd/runner/resource_class.go @@ -8,6 +8,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/runner" "github.com/CircleCI-Public/circleci-cli/cmd/validator" + "github.com/CircleCI-Public/circleci-cli/telemetry" ) func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command { @@ -16,12 +17,23 @@ func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra. Short: "Operate on runner resource-classes", } + telemetryWrappedPreRunE := func(cmd *cobra.Command, args []string) error { + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateRunnerResourceClassEvent(telemetry.GetCommandInformation(cmd, true))) + } + if preRunE != nil { + return preRunE(cmd, args) + } + return nil + } + genToken := false createCmd := &cobra.Command{ Use: "create <resource-class> <description>", Short: "Create a resource-class", Args: cobra.ExactArgs(2), - PreRunE: preRunE, + PreRunE: telemetryWrappedPreRunE, RunE: func(_ *cobra.Command, args []string) error { cmd.PrintErr(terms) @@ -54,7 +66,7 @@ func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra. Short: "Delete a resource-class", Aliases: []string{"rm"}, Args: cobra.ExactArgs(1), - PreRunE: preRunE, + PreRunE: telemetryWrappedPreRunE, RunE: func(_ *cobra.Command, args []string) error { rc, err := o.r.GetResourceClassByName(args[0]) if err != nil { @@ -72,7 +84,7 @@ func newResourceClassCommand(o *runnerOpts, preRunE validator.Validator) *cobra. Short: "List resource-classes for a namespace", Aliases: []string{"ls"}, Args: cobra.ExactArgs(1), - PreRunE: preRunE, + PreRunE: telemetryWrappedPreRunE, RunE: func(_ *cobra.Command, args []string) error { rcs, err := o.r.GetResourceClassesByNamespace(args[0]) if err != nil { diff --git a/cmd/runner/telemetry_test.go b/cmd/runner/telemetry_test.go new file mode 100644 index 000000000..dcd12874c --- /dev/null +++ b/cmd/runner/telemetry_test.go @@ -0,0 +1,62 @@ +package runner + +import ( + "bytes" + "context" + "testing" + + "github.com/CircleCI-Public/circleci-cli/telemetry" + "gotest.tools/v3/assert" +) + +type testTelemetryClient struct { + events []telemetry.Event +} + +func (c *testTelemetryClient) Track(event telemetry.Event) error { + c.events = append(c.events, event) + return nil +} + +func (c *testTelemetryClient) Close() error { return nil } + +func Test_RunnerTelemetry(t *testing.T) { + t.Run("resource-class", func(t *testing.T) { + telemetryClient := &testTelemetryClient{make([]telemetry.Event, 0)} + runner := runnerMock{} + cmd := newResourceClassCommand(&runnerOpts{r: &runner}, nil) + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + cmd.SetOut(stdout) + cmd.SetErr(stderr) + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + cmd.SetContext(telemetry.NewContext(ctx, telemetryClient)) + + defer runner.reset() + defer stdout.Reset() + defer stderr.Reset() + + cmd.SetArgs([]string{ + "create", + "my-namespace/my-other-resource-class", + "my-description", + "--generate-token", + }) + + err := cmd.Execute() + assert.NilError(t, err) + + assert.DeepEqual(t, telemetryClient.events, []telemetry.Event{ + telemetry.CreateRunnerResourceClassEvent(telemetry.CommandInfo{ + Name: "create", + LocalArgs: map[string]string{ + "generate-token": "true", + "help": "false", + }, + }), + }) + }) +} diff --git a/cmd/runner/token.go b/cmd/runner/token.go index 4771b37e9..32cb11741 100644 --- a/cmd/runner/token.go +++ b/cmd/runner/token.go @@ -4,6 +4,7 @@ import ( "time" "github.com/CircleCI-Public/circleci-cli/cmd/validator" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -14,11 +15,23 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command Short: "Operate on runner tokens", } + telemetryWrappedPreRunE := func(cmd *cobra.Command, args []string) error { + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateRunnerTokenEvent(telemetry.GetCommandInformation(cmd, true))) + } + + if preRunE != nil { + return preRunE(cmd, args) + } + return nil + } + cmd.AddCommand(&cobra.Command{ Use: "create <resource-class> <nickname>", Short: "Create a token for a resource-class", Args: cobra.ExactArgs(2), - PreRunE: preRunE, + PreRunE: telemetryWrappedPreRunE, RunE: func(_ *cobra.Command, args []string) error { token, err := o.r.CreateToken(args[0], args[1]) if err != nil { @@ -33,7 +46,7 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command Short: "Delete a token", Aliases: []string{"rm"}, Args: cobra.ExactArgs(1), - PreRunE: preRunE, + PreRunE: telemetryWrappedPreRunE, RunE: func(_ *cobra.Command, args []string) error { return o.r.DeleteToken(args[0]) }, @@ -44,7 +57,7 @@ func newTokenCommand(o *runnerOpts, preRunE validator.Validator) *cobra.Command Aliases: []string{"ls"}, Short: "List tokens for a resource-class", Args: cobra.ExactArgs(1), - PreRunE: preRunE, + PreRunE: telemetryWrappedPreRunE, RunE: func(_ *cobra.Command, args []string) error { tokens, err := o.r.GetRunnerTokensByResourceClass(args[0]) if err != nil { diff --git a/cmd/setup.go b/cmd/setup.go index 6a2d068b5..efc30288a 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -7,6 +7,7 @@ import ( "github.com/CircleCI-Public/circleci-cli/api/graphql" "github.com/CircleCI-Public/circleci-cli/prompt" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -123,7 +124,13 @@ func newSetupCommand(config *settings.Config) *cobra.Command { opts.args = args opts.cl = graphql.NewClient(config.HTTPClient, config.Host, config.Endpoint, config.Token, config.Debug) }, - RunE: func(_ *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + // We defer the call to make sure the `opts.cfg.Host` has been filled + defer (func() { _ = telemetryClient.Track(telemetry.CreateSetupEvent(opts.cfg.Host != defaultHost)) })() + } + if opts.integrationTesting { opts.tty = setupTestUI{ host: "boondoggle", diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 591a6197d..8ae09473c 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -9,12 +9,44 @@ import ( "runtime" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/gexec" ) +var _ = Describe("Setup telemetry", func() { + var ( + command *exec.Cmd + tempSettings *clitest.TempSettings + ) + + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + command = commandWithHome(pathCLI, tempSettings.Home, + "setup", + "--integration-testing", + "--skip-update-check", + ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + }) + + AfterEach(func() { + tempSettings.Close() + }) + + It("should send telemetry event", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateSetupEvent(true), + }) + }) +}) + var _ = Describe("Setup with prompts", func() { var ( command *exec.Cmd diff --git a/cmd/telemetry.go b/cmd/telemetry.go new file mode 100644 index 000000000..85e231f74 --- /dev/null +++ b/cmd/telemetry.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "os" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func newTelemetryCommand(config *settings.Config) *cobra.Command { + apiClient := CreateAPIClient(config) + + telemetryEnable := &cobra.Command{ + Use: "enable", + Short: "Allow telemetry events to be sent to CircleCI servers", + RunE: func(cmd *cobra.Command, _ []string) error { + err := setIsTelemetryActive(apiClient, true) + if telemetryClient, ok := telemetry.FromContext(cmd.Context()); ok { + _ = telemetryClient.Track(telemetry.CreateChangeTelemetryStatusEvent("enabled", "telemetry-command", err)) + } + return err + }, + Args: cobra.ExactArgs(0), + } + + telemetryDisable := &cobra.Command{ + Use: "disable", + Short: "Make sure no telemetry events is sent to CircleCI servers", + RunE: func(cmd *cobra.Command, _ []string) error { + err := setIsTelemetryActive(apiClient, false) + if telemetryClient, ok := telemetry.FromContext(cmd.Context()); ok { + _ = telemetryClient.Track(telemetry.CreateChangeTelemetryStatusEvent("disabled", "telemetry-command", err)) + } + return err + }, + Args: cobra.ExactArgs(0), + } + + telemetryCommand := &cobra.Command{ + Use: "telemetry", + Short: "Configure telemetry preferences", + Long: `Configure telemetry preferences. + +Note: If you have not configured your telemetry preferences and call the CLI with a closed stdin, telemetry will be disabled`, + } + + telemetryCommand.AddCommand(telemetryEnable) + telemetryCommand.AddCommand(telemetryDisable) + + return telemetryCommand +} + +func setIsTelemetryActive(apiClient TelemetryAPIClient, isActive bool) error { + settings := settings.TelemetrySettings{} + if err := settings.Load(); err != nil && !os.IsNotExist(err) { + return errors.Wrap(err, "Loading telemetry configuration") + } + + settings.HasAnsweredPrompt = true + settings.IsEnabled = isActive + + if settings.UniqueID == "" { + settings.UniqueID = CreateUUID() + } + + if settings.UserID == "" { + if myID, err := apiClient.GetMyUserId(); err == nil { + settings.UserID = myID + } + } + + if err := settings.Write(); err != nil { + return errors.Wrap(err, "Writing telemetry configuration") + } + + return nil +} diff --git a/cmd/telemetry_test.go b/cmd/telemetry_test.go new file mode 100644 index 000000000..fb8d334db --- /dev/null +++ b/cmd/telemetry_test.go @@ -0,0 +1,58 @@ +package cmd_test + +import ( + "fmt" + "os/exec" + + "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" + "github.com/onsi/gomega/gexec" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Telemetry events on telemetry commands", func() { + var ( + tempSettings *clitest.TempSettings + command *exec.Cmd + ) + + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + }) + + AfterEach(func() { + tempSettings.Close() + }) + + Describe("telemetry enable", func() { + It("should send an event", func() { + command = exec.Command(pathCLI, "telemetry", "enable") + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateChangeTelemetryStatusEvent("enabled", "telemetry-command", nil), + }) + }) + }) + + Describe("telemetry disable", func() { + It("should send an event", func() { + command = exec.Command(pathCLI, "telemetry", "disable") + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateChangeTelemetryStatusEvent("disabled", "telemetry-command", nil), + }) + }) + }) +}) diff --git a/cmd/telemetry_unit_test.go b/cmd/telemetry_unit_test.go new file mode 100644 index 000000000..468e1ce9d --- /dev/null +++ b/cmd/telemetry_unit_test.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/spf13/afero" + "gotest.tools/v3/assert" +) + +func TestSetIsTelemetryActive(t *testing.T) { + type args struct { + apiClient TelemetryAPIClient + isActive bool + settings *settings.TelemetrySettings + } + type want struct { + settings *settings.TelemetrySettings + } + + type testCase struct { + name string + args args + want want + } + + userId := "user-id" + uniqueId := "unique-id" + + testCases := []testCase{ + { + name: "Enabling telemetry with settings should just update the is active field", + args: args{ + apiClient: telemetryTestAPIClient{}, + isActive: true, + settings: &settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Enabling telemetry without settings should fill the settings fields", + args: args{ + apiClient: telemetryTestAPIClient{id: userId, err: nil}, + isActive: true, + settings: nil, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Disabling telemetry with settings should just update the is active field", + args: args{ + apiClient: telemetryTestAPIClient{}, + isActive: false, + settings: &settings.TelemetrySettings{ + IsEnabled: true, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + { + name: "Enabling telemetry without settings should fill the settings fields", + args: args{ + apiClient: telemetryTestAPIClient{id: userId, err: nil}, + isActive: false, + settings: nil, + }, + want: want{ + settings: &settings.TelemetrySettings{ + IsEnabled: false, + HasAnsweredPrompt: true, + UniqueID: uniqueId, + UserID: userId, + }, + }, + }, + } + + // Mock create UUID + oldUUIDCreate := CreateUUID + CreateUUID = func() string { return uniqueId } + defer (func() { CreateUUID = oldUUIDCreate })() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // Mock FS + oldFS := settings.FS.Fs + settings.FS.Fs = afero.NewMemMapFs() + defer (func() { settings.FS.Fs = oldFS })() + + if tt.args.settings != nil { + err := tt.args.settings.Write() + assert.NilError(t, err) + } + + err := setIsTelemetryActive(tt.args.apiClient, tt.args.isActive) + assert.NilError(t, err) + + exist, err := settings.FS.Exists(filepath.Join(settings.SettingsPath(), "telemetry.yml")) + assert.NilError(t, err) + if tt.want.settings == nil { + assert.Equal(t, exist, false) + } else { + assert.Equal(t, exist, true) + + loadedSettings := &settings.TelemetrySettings{} + err := loadedSettings.Load() + assert.NilError(t, err) + + assert.DeepEqual(t, tt.want.settings, loadedSettings) + } + }) + } +} diff --git a/cmd/update.go b/cmd/update.go index c43bf33d4..5ef2bcc44 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -5,6 +5,7 @@ import ( "time" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/update" "github.com/CircleCI-Public/circleci-cli/version" @@ -28,8 +29,13 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { update := &cobra.Command{ Use: "update", Short: "Update the tool to the latest version", - PersistentPreRun: func(_ *cobra.Command, _ []string) { + PersistentPreRun: func(cmd *cobra.Command, _ []string) { opts.cfg.SkipUpdateCheck = true + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateUpdateEvent(telemetry.GetCommandInformation(cmd, cmd.Name() != "update"))) + } }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args @@ -43,9 +49,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Use: "check", Hidden: true, Short: "Check if there are any updates available", - PersistentPreRun: func(_ *cobra.Command, _ []string) { - opts.cfg.SkipUpdateCheck = true - }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args opts.dryRun = true @@ -59,9 +62,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Use: "install", Hidden: true, Short: "Update the tool to the latest version", - PersistentPreRun: func(_ *cobra.Command, _ []string) { - opts.cfg.SkipUpdateCheck = true - }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args }, @@ -74,9 +74,6 @@ func newUpdateCommand(config *settings.Config) *cobra.Command { Use: "build-agent", Hidden: true, Short: "This command has no effect, and is kept for backwards compatibility", - PersistentPreRun: func(_ *cobra.Command, _ []string) { - opts.cfg.SkipUpdateCheck = true - }, PreRun: func(cmd *cobra.Command, args []string) { opts.args = args }, diff --git a/cmd/update_test.go b/cmd/update_test.go index 431987119..6ed7aa5dd 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -1,11 +1,13 @@ package cmd_test import ( + "fmt" "net/http" "os/exec" "path/filepath" "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" @@ -70,6 +72,69 @@ var _ = Describe("Update", func() { tempSettings.Close() }) + Describe("telemetry", func() { + It("should send telemetry event when calling parent command", func() { + updateCLI, err := gexec.Build("github.com/CircleCI-Public/circleci-cli") + Expect(err).ShouldNot(HaveOccurred()) + + command = exec.Command(updateCLI, + "update", + "--github-api", tempSettings.TestServer.URL(), + ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + + assetBytes := golden.Get(GinkgoT(), filepath.FromSlash("update/foo.zip")) + assetResponse := string(assetBytes) + + tempSettings.TestServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest(http.MethodGet, "/repos/CircleCI-Public/circleci-cli/releases"), + ghttp.RespondWith(http.StatusOK, response), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest(http.MethodGet, "/repos/CircleCI-Public/circleci-cli/releases/assets/1"), + ghttp.RespondWith(http.StatusOK, assetResponse), + ), + ) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateUpdateEvent(telemetry.CommandInfo{ + Name: "update", + LocalArgs: map[string]string{ + "check": "false", + "help": "false", + }, + }), + }) + }) + + It("should send telemetry event when calling child command", func() { + command = exec.Command(pathCLI, + "update", + "check", + "--github-api", tempSettings.TestServer.URL(), + ) + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateUpdateEvent(telemetry.CommandInfo{ + Name: "check", + LocalArgs: map[string]string{ + "check": "false", + "help": "false", + }, + }), + }) + }) + }) + Describe("update --check", func() { BeforeEach(func() { command = exec.Command(pathCLI, diff --git a/cmd/version.go b/cmd/version.go index 756e5e6ce..e2cd566ce 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/CircleCI-Public/circleci-cli/settings" + "github.com/CircleCI-Public/circleci-cli/telemetry" "github.com/CircleCI-Public/circleci-cli/version" "github.com/spf13/cobra" ) @@ -24,11 +25,18 @@ func newVersionCommand(config *settings.Config) *cobra.Command { PersistentPreRun: func(_ *cobra.Command, _ []string) { opts.cfg.SkipUpdateCheck = true }, - PreRun: func(cmd *cobra.Command, args []string) { + PreRun: func(_ *cobra.Command, args []string) { opts.args = args }, - Run: func(_ *cobra.Command, _ []string) { - fmt.Printf("%s+%s (%s)\n", version.Version, version.Commit, version.PackageManager()) + Run: func(cmd *cobra.Command, _ []string) { + version := fmt.Sprintf("%s+%s (%s)", version.Version, version.Commit, version.PackageManager()) + + telemetryClient, ok := telemetry.FromContext(cmd.Context()) + if ok { + _ = telemetryClient.Track(telemetry.CreateVersionEvent(version)) + } + + fmt.Printf("%s\n", version) }, } } diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 000000000..e64cf051e --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,39 @@ +package cmd_test + +import ( + "fmt" + "os/exec" + + "github.com/CircleCI-Public/circleci-cli/clitest" + "github.com/CircleCI-Public/circleci-cli/telemetry" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Version telemetry", func() { + var ( + command *exec.Cmd + tempSettings *clitest.TempSettings + ) + + BeforeEach(func() { + tempSettings = clitest.WithTempSettings() + command = commandWithHome(pathCLI, tempSettings.Home, "version") + command.Env = append(command.Env, fmt.Sprintf("MOCK_TELEMETRY=%s", tempSettings.TelemetryDestPath)) + }) + + AfterEach(func() { + tempSettings.Close() + }) + + It("should send a telemetry event", func() { + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + Eventually(session).Should(gexec.Exit(0)) + clitest.CompareTelemetryEvent(tempSettings, []telemetry.Event{ + telemetry.CreateVersionEvent("0.0.0-dev+dirty-local-tree (source)"), + }) + }) +}) diff --git a/go.mod b/go.mod index f305f8565..710386a9d 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,8 @@ require ( github.com/charmbracelet/lipgloss v0.5.0 github.com/erikgeiser/promptkit v0.7.0 github.com/hexops/gotextdiff v1.0.3 + github.com/segmentio/analytics-go v3.1.0+incompatible + github.com/spf13/afero v1.9.5 github.com/stretchr/testify v1.8.3 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 golang.org/x/term v0.10.0 @@ -50,6 +52,7 @@ require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/charmbracelet/bubbles v0.11.0 // indirect github.com/charmbracelet/bubbletea v0.21.0 // indirect @@ -94,6 +97,7 @@ require ( github.com/prometheus/procfs v0.11.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/segmentio/backo-go v1.0.1 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.1.1 // indirect @@ -101,6 +105,7 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect diff --git a/go.sum b/go.sum index 7ab560a5e..4d5d49640 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,45 @@ +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.44.3/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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI= github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= +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/CircleCI-Public/circle-policy-agent v0.0.683 h1:EzZaLy9mUGl4dwDNWceBHeDb3X0KAAjV4eFOk3C7lts= github.com/CircleCI-Public/circle-policy-agent v0.0.683/go.mod h1:72U4Q4OtvAGRGGo/GqlCCO0tARg1cSG9xwxWyz3ktQI= github.com/CircleCI-Public/circleci-config v0.0.0-20230629192034-c469d9e9936b h1:emg7uU3bRjVMlwSpOATBiybaBPXNWUIiFE/qbQQXZtE= @@ -32,11 +72,14 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -47,9 +90,16 @@ github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -65,6 +115,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikgeiser/promptkit v0.7.0 h1:Yi28iN6JRs8/0x+wjQRPfWb+vWz1pFmZ5fu2uoFipD8= github.com/erikgeiser/promptkit v0.7.0/go.mod h1:Jj9bhN+N8RbMjB1jthkr9A4ydmczZ1WZJ8xTXnP12dg= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -87,6 +143,9 @@ github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw4 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -98,26 +157,51 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/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/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +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/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +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.3.1/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.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -125,17 +209,39 @@ github.com/google/go-github v15.0.0+incompatible h1:jlPg2Cpsxb/FyEV/MFiIE9tW/2RA github.com/google/go-github v15.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +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/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= +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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= @@ -144,11 +250,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 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/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -220,10 +330,12 @@ github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= @@ -237,10 +349,15 @@ github.com/rhysd/go-github-selfupdate v0.0.0-20180520142321-41c1bbb0804a/go.mod github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80= +github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= +github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= +github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -248,6 +365,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -273,10 +392,21 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMc github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/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/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= @@ -292,26 +422,87 @@ go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZE go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/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-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-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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/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/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -320,31 +511,73 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -368,46 +601,180 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-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-20190524140312-2c0ae7006135/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-20190624222133-a101b041ded4/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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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/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/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +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/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +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/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20230724170836-66ad5b6ff146 h1:0yJBPCSj/Hy/vQsNSrYtRLuJSNKoDzDXMu1q1ePGdus= google.golang.org/genproto v0.0.0-20230724170836-66ad5b6ff146/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= google.golang.org/genproto/googleapis/api v0.0.0-20230724170836-66ad5b6ff146 h1:P60zJj7Yxq1VhZIxpRO7A5lDFyy07D6Dqa+HCixuFBM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230724170836-66ad5b6ff146 h1:0PjALPu/U/4OVXKQM2P8b8NJGd4V+xbZSP+uuBJpGm0= +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= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -423,3 +790,13 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go index 8948a918d..9a733aa63 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,14 @@ package main import ( + "os" + "github.com/CircleCI-Public/circleci-cli/cmd" ) func main() { // See cmd/root.go for Execute() - cmd.Execute() + if err := cmd.Execute(); err != nil { + os.Exit(-1) + } } diff --git a/prompt/prompt.go b/prompt/prompt.go index cb2bd52dd..c9613386c 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -36,3 +36,14 @@ func AskUserToConfirm(message string) bool { result, err := input.RunPrompt() return err == nil && result } + +func AskUserToConfirmWithDefault(message string, defaultValue bool) bool { + def := confirmation.No + if defaultValue { + def = confirmation.Yes + } + + input := confirmation.New(message, def) + result, err := input.RunPrompt() + return err == nil && result +} diff --git a/settings/settings.go b/settings/settings.go index 36928fac5..7a9e7f0e4 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -17,25 +17,37 @@ import ( yaml "gopkg.in/yaml.v3" "github.com/CircleCI-Public/circleci-cli/data" + "github.com/spf13/afero" +) + +var ( + FS afero.Afero = afero.Afero{ + Fs: afero.NewOsFs(), + } ) // Config is used to represent the current state of a CLI instance. type Config struct { - Host string `yaml:"host"` - DlHost string `yaml:"-"` - Endpoint string `yaml:"endpoint"` - Token string `yaml:"token"` - RestEndpoint string `yaml:"rest_endpoint"` - TLSCert string `yaml:"tls_cert"` - TLSInsecure bool `yaml:"tls_insecure"` - HTTPClient *http.Client `yaml:"-"` - Data *data.DataBag `yaml:"-"` - Debug bool `yaml:"-"` - Address string `yaml:"-"` - FileUsed string `yaml:"-"` - GitHubAPI string `yaml:"-"` - SkipUpdateCheck bool `yaml:"-"` - OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"` + Host string `yaml:"host"` + DlHost string `yaml:"-"` + Endpoint string `yaml:"endpoint"` + Token string `yaml:"token"` + RestEndpoint string `yaml:"rest_endpoint"` + TLSCert string `yaml:"tls_cert"` + TLSInsecure bool `yaml:"tls_insecure"` + HTTPClient *http.Client `yaml:"-"` + Data *data.DataBag `yaml:"-"` + Debug bool `yaml:"-"` + Address string `yaml:"-"` + FileUsed string `yaml:"-"` + GitHubAPI string `yaml:"-"` + SkipUpdateCheck bool `yaml:"-"` + // Parameter used to disable telemetry from tests + IsTelemetryDisabled bool `yaml:"-"` + // If this value is defined, the telemetry will write all its events a file + // The value of this field is the path where the telemetry will be written + MockTelemetry string `yaml:"-"` + OrbPublishing OrbPublishingInfo `yaml:"orb_publishing"` } type OrbPublishingInfo struct { @@ -50,6 +62,14 @@ type UpdateCheck struct { FileUsed string `yaml:"-"` } +// TelemetrySettings is used to represent telemetry related settings +type TelemetrySettings struct { + IsEnabled bool `yaml:"is_enabled"` + HasAnsweredPrompt bool `yaml:"has_answered_prompt"` + UniqueID string `yaml:"unique_id"` + UserID string `yaml:"user_id"` +} + // Load will read the update check settings from the user's disk and then deserialize it into the current instance. func (upd *UpdateCheck) Load() error { path := filepath.Join(SettingsPath(), updateCheckFilename()) @@ -80,6 +100,35 @@ func (upd *UpdateCheck) WriteToDisk() error { return err } +// Load will read the telemetry settings from the user's disk and then deserialize it into the current instance. +func (tel *TelemetrySettings) Load() error { + path := filepath.Join(SettingsPath(), telemetryFilename()) + + if err := ensureSettingsFileExists(path); err != nil { + return err + } + + content, err := FS.ReadFile(path) // #nosec + if err != nil { + return err + } + + err = yaml.Unmarshal(content, &tel) + return err +} + +// WriteToDisk will write the telemetry settings to disk by serializing the YAML +func (tel *TelemetrySettings) Write() error { + enc, err := yaml.Marshal(&tel) + if err != nil { + return err + } + + path := filepath.Join(SettingsPath(), telemetryFilename()) + err = FS.WriteFile(path, enc, 0600) + return err +} + // Load will read the config from the user's disk and then evaluate possible configuration from the environment. func (cfg *Config) Load() error { if err := cfg.LoadFromDisk(); err != nil { @@ -161,6 +210,11 @@ func configFilename() string { return "cli.yml" } +// telemetryFilename returns the name of the cli telemetry file +func telemetryFilename() string { + return "telemetry.yml" +} + // settingsPath returns the path of the CLI settings directory func SettingsPath() string { // TODO: Make this configurable diff --git a/telemetry/context.go b/telemetry/context.go new file mode 100644 index 000000000..4e245f101 --- /dev/null +++ b/telemetry/context.go @@ -0,0 +1,16 @@ +package telemetry + +import "context" + +type contextKey string + +const telemetryClientContextKey contextKey = "telemetryClientContextKey" + +func NewContext(ctx context.Context, client Client) context.Context { + return context.WithValue(ctx, telemetryClientContextKey, client) +} + +func FromContext(ctx context.Context) (Client, bool) { + client, ok := ctx.Value(telemetryClientContextKey).(Client) + return client, ok +} diff --git a/telemetry/events.go b/telemetry/events.go new file mode 100644 index 000000000..652b111ac --- /dev/null +++ b/telemetry/events.go @@ -0,0 +1,149 @@ +package telemetry + +import "fmt" + +// This file contains all the telemetry event constructors +// All the events are referenced in the following file: +// https://circleci.atlassian.net/wiki/spaces/DE/pages/6760694125/CLI+segment+event+tracking +// If you want to add an event, first make sure it appears in this file + +type CommandInfo struct { + Name string + LocalArgs map[string]string +} + +func createEventFromCommandInfo(name string, cmdInfo CommandInfo) Event { + properties := map[string]interface{}{} + for key, value := range cmdInfo.LocalArgs { + properties[fmt.Sprintf("cmd.flag.%s", key)] = value + } + properties["has_been_executed"] = false + + return Event{ + Object: fmt.Sprintf("cli-%s", name), + Action: cmdInfo.Name, + Properties: properties, + } +} + +func errorToProperties(err error) map[string]interface{} { + properties := map[string]interface{}{ + "has_been_executed": true, + } + + if err != nil { + properties["error"] = err.Error() + } + return properties +} + +func CreateSetupEvent(isServerCustomer bool) Event { + return Event{ + Object: "cli-setup", + Action: "setup", + Properties: map[string]interface{}{ + "is_server_customer": isServerCustomer, + "has_been_executed": true, + }, + } +} + +func CreateVersionEvent(version string) Event { + return Event{ + Object: "cli-version", + Action: "version", + Properties: map[string]interface{}{ + "version": version, + "has_been_executed": true, + }, + } +} + +func CreateUpdateEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("update", cmdInfo) +} + +func CreateDiagnosticEvent(err error) Event { + return Event{ + Object: "cli-diagnostic", Action: "diagnostic", Properties: errorToProperties(err), + } +} + +func CreateFollowEvent(err error) Event { + return Event{ + Object: "cli-follow", Action: "follow", Properties: errorToProperties(err), + } +} + +func CreateOpenEvent(err error) Event { + return Event{Object: "cli-open", Action: "open", Properties: errorToProperties(err)} +} + +func CreateCompletionCommand(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("completion", cmdInfo) +} + +func CreateConfigEvent(cmdInfo CommandInfo, err error) Event { + event := createEventFromCommandInfo("config", cmdInfo) + if err != nil { + event.Properties["error"] = err.Error() + event.Properties["has_been_executed"] = true + } + return event +} + +func CreateLocalExecuteEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("local", cmdInfo) +} + +func CreateNamespaceEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("namespace", cmdInfo) +} + +func CreateOrbEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("orb", cmdInfo) +} + +func CreatePolicyEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("policy", cmdInfo) +} + +func CreateRunnerInstanceEvent(cmdInfo CommandInfo, err error) Event { + event := createEventFromCommandInfo("runner-instance", cmdInfo) + if err != nil { + event.Properties["error"] = err.Error() + event.Properties["has_been_executed"] = true + } + return event +} + +func CreateRunnerResourceClassEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("runner-resource-class", cmdInfo) +} + +func CreateRunnerTokenEvent(cmdInfo CommandInfo) Event { + return createEventFromCommandInfo("runner-token", cmdInfo) +} + +func CreateInfoEvent(cmdInfo CommandInfo, err error) Event { + event := createEventFromCommandInfo("info", cmdInfo) + if err != nil { + event.Properties["error"] = err.Error() + event.Properties["has_been_executed"] = true + } + return event +} + +func CreateChangeTelemetryStatusEvent(action string, origin string, err error) Event { + event := Event{ + Object: "cli-telemetry", + Action: action, + Properties: map[string]interface{}{ + "origin": origin, + }, + } + if err != nil { + event.Properties["error"] = err.Error() + } + return event +} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go new file mode 100644 index 000000000..928e59dd8 --- /dev/null +++ b/telemetry/telemetry.go @@ -0,0 +1,197 @@ +package telemetry + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/segmentio/analytics-go" +) + +var ( + // Overwrite this function for tests + CreateActiveTelemetry = newSegmentClient + + SegmentEndpoint = "http://localhost:33457" + segmentKey = "AbgkrgN4cbRhAVEwlzMkHbwvrXnxHh35" +) + +type Approval string + +const ( + Enabled Approval = "enabled" + Disabled Approval = "disabled" + NoStdin Approval = "disabled_default" +) + +type Client interface { + io.Closer + // Send a telemetry event. This method is not to be called directly. Use config.Track instead + Track(event Event) error +} + +// A segment event to be sent to the telemetry +// Important: this is not meant to be constructed directly apart in tests +// If you want to create a new event, add its constructor in ./events.go +type Event struct { + Object string `json:"object"` + Action string `json:"action"` + Properties map[string]interface{} `json:"properties"` +} + +type User struct { + UniqueID string + UserID string + IsSelfHosted bool + OS string + Version string + TeamName string +} + +// Create a telemetry client to be used to send telemetry events +func CreateClient(user User, enabled bool) Client { + if !enabled { + return nullClient{} + } + + return CreateActiveTelemetry(user) +} + +// Sends the user's approval event +func SendTelemetryApproval(user User, approval Approval) error { + client := CreateActiveTelemetry(user) + defer client.Close() + + return client.Track(CreateChangeTelemetryStatusEvent(string(approval), "first-time-prompt", nil)) +} + +// Null client +// Used when telemetry is disabled + +func CreateNullClient() Client { + return nullClient{} +} + +type nullClient struct{} + +func (cli nullClient) Close() error { return nil } + +func (cli nullClient) Track(_ Event) error { return nil } + +// Segment client +// Used when telemetry is enabled + +// Nil segment logger +type nilSegmentEmptyLogger struct{} + +func (nilSegmentEmptyLogger) Logf(format string, args ...interface{}) {} +func (nilSegmentEmptyLogger) Errorf(format string, args ...interface{}) {} + +type segmentClient struct { + analyticsClient analytics.Client + user User +} + +func newSegmentClient(user User) Client { + cli, err := analytics.NewWithConfig(segmentKey, analytics.Config{ + Endpoint: SegmentEndpoint, + Logger: nilSegmentEmptyLogger{}, + }) + + if err != nil { + return CreateNullClient() + } + + if len(user.UniqueID) == 0 { + return CreateNullClient() + } + + err = cli.Enqueue( + analytics.Identify{ + UserId: user.UniqueID, + Traits: analytics.NewTraits().Set("os", user.OS), + }, + ) + if err != nil { + fmt.Printf("Error while identifying with telemetry: %s\n", err) + } + + return &segmentClient{cli, user} +} + +func (segment *segmentClient) Track(event Event) error { + if event.Properties == nil { + event.Properties = make(map[string]interface{}) + } + if event.Action != "" { + event.Properties["action"] = event.Action + } + + if segment.user.UniqueID != "" { + event.Properties["anonymous_id"] = segment.user.UniqueID + } + + if segment.user.UserID != "" { + event.Properties["cci_user_id"] = segment.user.UserID + } + + event.Properties["is_self_hosted"] = segment.user.IsSelfHosted + + if segment.user.OS != "" { + event.Properties["os"] = segment.user.OS + } + + if segment.user.Version != "" { + event.Properties["cli_version"] = segment.user.Version + } + + if segment.user.TeamName != "" { + event.Properties["team_name"] = segment.user.TeamName + } + + return segment.analyticsClient.Enqueue(analytics.Track{ + UserId: segment.user.UniqueID, + Event: event.Object, + Properties: event.Properties, + }) +} + +func (segment *segmentClient) Close() error { + return segment.analyticsClient.Close() +} + +// File telemetry +// Used for E2E tests + +type fileTelemetry struct { + filePath string + events []Event +} + +func CreateFileTelemetry(filePath string) Client { + return &fileTelemetry{filePath, make([]Event, 0)} +} + +func (cli *fileTelemetry) Track(event Event) error { + cli.events = append(cli.events, event) + return nil +} + +func (cli *fileTelemetry) Close() error { + file, err := os.OpenFile(cli.filePath, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return err + } + + content, err := json.Marshal(&cli.events) + if err != nil { + return err + } + + if _, err = file.Write(content); err != nil { + return err + } + + return file.Close() +} diff --git a/telemetry/utils.go b/telemetry/utils.go new file mode 100644 index 000000000..2201887ad --- /dev/null +++ b/telemetry/utils.go @@ -0,0 +1,31 @@ +package telemetry + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Utility function used when creating telemetry events. +// It takes a cobra Command and creates a telemetry.CommandInfo of it +// If getParent is true, puts both the command's args in `LocalArgs` and the parent's args +// Else only put the command's args +// Note: child flags overwrite parent flags with same name +func GetCommandInformation(cmd *cobra.Command, getParent bool) CommandInfo { + localArgs := map[string]string{} + + parent := cmd.Parent() + if getParent && parent != nil { + parent.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + } + + cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) { + localArgs[flag.Name] = flag.Value.String() + }) + + return CommandInfo{ + Name: cmd.Name(), + LocalArgs: localArgs, + } +}