diff --git a/cmd/common.go b/cmd/common.go index abebee1ab..b482dca8c 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -82,10 +82,23 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, } // load Kong version after workspace - kongVersion, err := kongVersion(ctx, wsConfig) + kongVersion, err := fetchKongVersion(ctx, wsConfig) if err != nil { return errors.Wrap(err, "reading Kong version") } + parsedKongVersion, err := parseKongVersion(kongVersion) + if err != nil { + return errors.Wrap(err, "parsing Kong version") + } + + // TODO: instead of guessing the cobra command here, move the sendAnalytics + // call to the RunE function. That is not trivial because it requires the + // workspace name and kong client to be present on that level. + cmd := "sync" + if dry { + cmd = "diff" + } + _ = sendAnalytics(cmd, kongVersion) workspaceExists, err := workspaceExists(ctx, wsConfig) if err != nil { @@ -135,7 +148,7 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, // read the target state rawState, err := file.Get(targetContent, file.RenderConfig{ CurrentState: currentState, - KongVersion: kongVersion, + KongVersion: parsedKongVersion, }) if err != nil { return err @@ -166,8 +179,7 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, return nil } -func kongVersion(ctx context.Context, - config utils.KongClientConfig) (semver.Version, error) { +func fetchKongVersion(ctx context.Context, config utils.KongClientConfig) (string, error) { var version string @@ -177,30 +189,33 @@ func kongVersion(ctx context.Context, config.Workspace = "" client, err := utils.GetKongClient(config) if err != nil { - return semver.Version{}, err + return "", err } root, err := client.Root(ctx) if err != nil { if workspace == "" { - return semver.Version{}, err + return "", err } // try with workspace path req, err := http.NewRequest("GET", utils.CleanAddress(config.Address)+"/"+workspace+"/kong", nil) if err != nil { - return semver.Version{}, err + return "", err } var resp map[string]interface{} - _, err = client.Do(nil, req, &resp) + _, err = client.Do(ctx, req, &resp) if err != nil { - return semver.Version{}, err + return "", err } version = resp["version"].(string) } else { version = root["version"].(string) } + return version, nil +} +func parseKongVersion(version string) (semver.Version, error) { v, err := utils.CleanKongVersion(version) if err != nil { return semver.Version{}, err @@ -246,3 +261,10 @@ func containsProxyConfiguration(content utils.KongRawState) bool { func containsRBACConfiguration(content utils.KongRawState) bool { return len(content.RBACRoles) != 0 } + +func sendAnalytics(cmd, kongVersion string) error { + if disableAnalytics { + return nil + } + return utils.SendAnalytics(cmd, VERSION, kongVersion) +} diff --git a/cmd/dump.go b/cmd/dump.go index 98dbe6e32..83373c638 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -54,6 +54,12 @@ configure Kong.`, format := file.Format(strings.ToUpper(dumpCmdStateFormat)) + kongVersion, err := fetchKongVersion(ctx, rootConfig.ForWorkspace(dumpWorkspace)) + if err != nil { + return errors.Wrap(err, "reading Kong version") + } + _ = sendAnalytics("dump", kongVersion) + // Kong Enterprise dump all workspace if dumpAllWorkspaces { if dumpWorkspace != "" { diff --git a/cmd/konnect_diff.go b/cmd/konnect_diff.go index f48bfd681..77694e04d 100644 --- a/cmd/konnect_diff.go +++ b/cmd/konnect_diff.go @@ -21,6 +21,7 @@ the entities present in files locally. This allows you to see the entities that will be created or updated or deleted.` + konnectAlphaState, Args: validateNoArgs, RunE: func(cmd *cobra.Command, args []string) error { + _ = sendAnalytics("konnect-diff", "") return syncKonnect(cmd.Context(), konnectDiffCmdKongStateFile, true, konnectDiffCmdParallelism) }, diff --git a/cmd/konnect_dump.go b/cmd/konnect_dump.go index df50af277..ac692a1c5 100644 --- a/cmd/konnect_dump.go +++ b/cmd/konnect_dump.go @@ -29,6 +29,7 @@ configure Konnect.` + konnectAlphaState, Args: validateNoArgs, RunE: func(cmd *cobra.Command, args []string) error { httpClient := utils.HTTPClient() + _ = sendAnalytics("konnect-dump", "") if yes, err := utils.ConfirmFileOverwrite(konnectDumpCmdKongStateFile, dumpCmdStateFormat, assumeYes); err != nil { return err diff --git a/cmd/konnect_ping.go b/cmd/konnect_ping.go index b3f61a28a..aa84c264d 100644 --- a/cmd/konnect_ping.go +++ b/cmd/konnect_ping.go @@ -16,6 +16,7 @@ can connect to Konnect's API endpoint. It also validates the supplied credentials.` + konnectAlphaState, Args: validateNoArgs, RunE: func(cmd *cobra.Command, args []string) error { + _ = utils.SendAnalytics("konnect-ping", VERSION, "") client, err := utils.GetKonnectClient(nil, konnectConfig.Debug) if err != nil { return err diff --git a/cmd/konnect_sync.go b/cmd/konnect_sync.go index cd7c96d9a..88221cabd 100644 --- a/cmd/konnect_sync.go +++ b/cmd/konnect_sync.go @@ -13,6 +13,7 @@ var konnectSyncCmd = &cobra.Command{ to get Konnect's state in sync with the input state.` + konnectAlphaState, Args: validateNoArgs, RunE: func(cmd *cobra.Command, args []string) error { + _ = sendAnalytics("konnect-sync", "") return syncKonnect(cmd.Context(), konnectDiffCmdKongStateFile, false, konnectDiffCmdParallelism) }, diff --git a/cmd/ping.go b/cmd/ping.go index 225310580..72732c8c8 100644 --- a/cmd/ping.go +++ b/cmd/ping.go @@ -22,10 +22,11 @@ can connect to Kong's Admin API or not.`, ctx := cmd.Context() wsConfig := rootConfig.ForWorkspace(pingWorkspace) - version, err := kongVersion(ctx, wsConfig) + version, err := fetchKongVersion(ctx, wsConfig) if err != nil { return errors.Wrap(err, "reading Kong version") } + _ = sendAnalytics("ping", version) fmt.Println("Successfully connected to Kong!") fmt.Println("Kong version: ", version) return nil diff --git a/cmd/reset.go b/cmd/reset.go index cda736c52..8fc755582 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -43,6 +43,13 @@ By default, this command will ask for a confirmation prompt.`, if err != nil { return err } + + kongVersion, err := fetchKongVersion(ctx, rootConfig.ForWorkspace(resetWorkspace)) + if err != nil { + return errors.Wrap(err, "reading Kong version") + } + _ = sendAnalytics("reset", kongVersion) + // Kong OSS or default workspace if !resetAllWorkspaces && resetWorkspace == "" { state, err := dump.Get(ctx, rootClient, dumpConfig) diff --git a/cmd/root.go b/cmd/root.go index 3a839342d..700baebb8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,6 +20,8 @@ var ( cfgFile string rootConfig utils.KongClientConfig konnectConfig utils.KonnectConfig + + disableAnalytics bool ) // rootCmd represents the base command when called without any subcommands @@ -126,6 +128,11 @@ func init() { "File containing password to your Konnect account") viper.BindPFlag("konnect-password-file", rootCmd.PersistentFlags().Lookup("konnect-password-file")) + + rootCmd.PersistentFlags().Bool("analytics", true, + "share anonymized data to help improve decK") + viper.BindPFlag("analytics", + rootCmd.PersistentFlags().Lookup("analytics")) } // initConfig reads in config file and ENV variables if set. @@ -189,6 +196,7 @@ func initKonnectConfig() error { password = strings.TrimRight(password, "\n") } + disableAnalytics = !viper.GetBool("analytics") konnectConfig.Email = viper.GetString("konnect-email") konnectConfig.Password = password konnectConfig.Debug = (viper.GetInt("verbose") >= 1) diff --git a/cmd/validate.go b/cmd/validate.go index 371dd8579..83effd7f3 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -26,6 +26,7 @@ this command. `, Args: validateNoArgs, RunE: func(cmd *cobra.Command, args []string) error { + _ = sendAnalytics("validate", "") // read target file // this does json schema validation as well targetContent, err := file.GetContentFromFiles(validateCmdKongStateFile) diff --git a/go.mod b/go.mod index fcff71627..3f9d9a8f3 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/pelletier/go-toml v1.7.0 // indirect github.com/pkg/errors v0.9.1 github.com/sergi/go-diff v1.1.0 // indirect + github.com/shirou/gopsutil/v3 v3.21.3 github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v0.0.7 diff --git a/go.sum b/go.sum index 96fd47e3d..35f6cf7fa 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/alecthomas/jsonschema v0.0.0-20191017121752-4bb6e3fae4f2 h1:swGeCLPiUQ647AIRnFxnAHdzlg6IPpmU6QdkOPZINt8= github.com/alecthomas/jsonschema v0.0.0-20191017121752-4bb6e3fae4f2/go.mod h1:Juc2PrI3wtNfUwptSvAIeNx+HrETwHQs6nf+TkOJlOA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -76,6 +78,8 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -266,6 +270,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 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/shirou/gopsutil/v3 v3.21.3 h1:wgcdAHZS2H6qy4JFewVTtqfiYxFzCeEJod/mLztdPG8= +github.com/shirou/gopsutil/v3 v3.21.3/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -304,6 +310,10 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tklauser/go-sysconf v0.3.4 h1:HT8SVixZd3IzLdfs/xlpq0jeSfTX57g1v6wB1EuzV7M= +github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek= +github.com/tklauser/numcpus v0.2.1 h1:ct88eFm+Q7m2ZfXJdan1xYoXKlmwsfP+k88q05KvlZc= +github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -415,6 +425,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w 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-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/utils/analytics.go b/utils/analytics.go new file mode 100644 index 000000000..982a827ff --- /dev/null +++ b/utils/analytics.go @@ -0,0 +1,72 @@ +package utils + +import ( + "bytes" + "fmt" + "net" + "os" + "runtime" + "strings" + + "github.com/shirou/gopsutil/v3/host" +) + +const ( + reportsHost = "kong-hf.konghq.com" + reportsPort = 61829 +) + +func SendAnalytics(cmd, deckVersion, kongVersion string) error { + if strings.ToLower(os.Getenv("DECK_ANALYTICS")) == "off" { + return nil + } + if cmd == "" { + return fmt.Errorf("invalid argument, 'cmd' cannot be empty") + } + + stats := collectStats(cmd, deckVersion, kongVersion) + body := formatStats(stats) + + addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", reportsHost, reportsPort)) + if err != nil { + return err + } + conn, err := net.DialUDP("udp", nil, addr) + if err != nil { + return err + } + defer conn.Close() + + _, err = conn.Write([]byte(body)) + if err != nil { + return err + } + return nil +} + +func formatStats(stats map[string]string) string { + var buffer bytes.Buffer + buffer.WriteString("<14>") + for k, v := range stats { + buffer.WriteString(fmt.Sprintf("%s=%s;", k, v)) + } + return buffer.String() +} + +func collectStats(cmd, deckVersion, kongVersion string) map[string]string { + result := map[string]string{ + "signal": "decK", + "v": deckVersion, + "cmd": cmd, + "os": runtime.GOOS, + "arch": runtime.GOARCH, + } + if kongVersion != "" { + result["kv"] = kongVersion + } + info, err := host.Info() + if err == nil { + result["osv"] = info.Platform + " " + info.PlatformVersion + } + return result +}