From 8c7958b6941583d7989811d463b61579763a43eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Thu, 12 May 2022 18:10:48 +0200 Subject: [PATCH 1/9] feat: add config set command --- cmd/meroxa/root/config/config.go | 1 + cmd/meroxa/root/config/set.go | 123 +++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 cmd/meroxa/root/config/set.go diff --git a/cmd/meroxa/root/config/config.go b/cmd/meroxa/root/config/config.go index ba2067d29..f11638dcc 100644 --- a/cmd/meroxa/root/config/config.go +++ b/cmd/meroxa/root/config/config.go @@ -48,5 +48,6 @@ func (*Config) Aliases() []string { func (*Config) SubCommands() []*cobra.Command { return []*cobra.Command{ builder.BuildCobraCommand(&Describe{}), + builder.BuildCobraCommand(&Set{}), } } diff --git a/cmd/meroxa/root/config/set.go b/cmd/meroxa/root/config/set.go new file mode 100644 index 000000000..a5fa882c8 --- /dev/null +++ b/cmd/meroxa/root/config/set.go @@ -0,0 +1,123 @@ +/* +Copyright Β© 2022 Meroxa Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliee. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "context" + "errors" + "fmt" + "github.com/meroxa/cli/cmd/meroxa/builder" + "github.com/meroxa/cli/config" + "github.com/meroxa/cli/log" + "regexp" + "strings" +) + +var ( + _ builder.CommandWithDocs = (*Set)(nil) + _ builder.CommandWithLogger = (*Set)(nil) + _ builder.CommandWithExecute = (*Set)(nil) + _ builder.CommandWithArgs = (*Set)(nil) + _ builder.CommandWithConfig = (*Set)(nil) +) + +type Set struct { + logger log.Logger + config config.Config + args struct { + keys map[string]string + } +} + +func (s *Set) Usage() string { + return "set" +} + +func (s *Set) Docs() builder.Docs { + return builder.Docs{ + Short: "Update your Meroxa CLI configuration file with a specific key=value", + Example: "" + + "$ meroxa config set DisableUpdateNotification=true\n" + + "$ meroxa config set DISABLE_UPDATE_NOTIFICATION=true\n" + + "$ meroxa config set OneKey=true AnotherKey=false\n" + + "$ meroxa config set ApiUrl=https://staging.meroxa.com\n", + Long: "This command will let you update your Meroxa configuration file to customize your CLI experience." + + "You can check the presence of this file by running `meroxa config describe`, or even provide your own using `--config my-other-cfg-file`" + + "A key with a format such as MyKey will be converted automatically to as MY_KEY.", + } +} + +func (s *Set) Execute(ctx context.Context) error { + for k, v := range s.args.keys { + s.logger.Infof(ctx, "Updating your Meroxa configuration file with %s=%s...", k, v) + s.config.Set(k, v) + + } + s.logger.Info(ctx, "Done!") + // run over each key and write them to a file + // show confirmation message + return nil +} + +func (s *Set) Logger(logger log.Logger) { + s.logger = logger +} + +func (s *Set) Config(cfg config.Config) { + s.config = cfg +} + +// TODO: Implement (and Test!) +func (s *Set) normalizeKey(key string) string { + var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") + var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + + snake := matchFirstCap.ReplaceAllString(key, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + return strings.ToUpper(snake) +} + +func (s *Set) validateAndAssignKeyValue(kv string) error { + nkv := strings.Split(kv, "=") + + if len(nkv) != 2 { + return fmt.Errorf("a key=value needs to contain at least and only one `=` sign") + } + + k := s.normalizeKey(nkv[0]) + v := nkv[1] + + s.args.keys[k] = v + return nil +} + +func (s *Set) ParseArgs(args []string) error { + s.args.keys = make(map[string]string) + + var err error + if len(args) < 1 { + return errors.New("requires at least one KEY=VALUE pair (example: meroxa config set KEY=VALUE)") + } + + for _, a := range args { + err = s.validateAndAssignKeyValue(a) + if err != nil { + return err + } + } + return nil +} From 03a03431d8e5d9284c6fc3c5d7d81e0d5c9f22b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Thu, 12 May 2022 18:10:57 +0200 Subject: [PATCH 2/9] feat: autoupdate CLI --- .goreleaser.yml | 2 - cmd/meroxa/builder/autoupdate.go | 84 ++++++++++++++++++++++++++++++++ cmd/meroxa/builder/builder.go | 66 ++++++++++++++++++++----- cmd/meroxa/global/global.go | 25 +++++----- cmd/meroxa/main.go | 5 ++ 5 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 cmd/meroxa/builder/autoupdate.go diff --git a/.goreleaser.yml b/.goreleaser.yml index d50cfe5e3..e8cfba892 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,3 @@ -# This is an example goreleaser.yaml file with some sane defaults. -# Make sure to check the documentation at http://goreleaser.com project_name: meroxa builds: - env: diff --git a/cmd/meroxa/builder/autoupdate.go b/cmd/meroxa/builder/autoupdate.go new file mode 100644 index 000000000..bbb84b36c --- /dev/null +++ b/cmd/meroxa/builder/autoupdate.go @@ -0,0 +1,84 @@ +/* +Copyright Β© 2022 Meroxa Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + "encoding/json" + "io" + "log" + "net/http" + "time" + + "github.com/meroxa/cli/cmd/meroxa/global" +) + +// needToCheckNewerCLIVersion checks if CLI within a week +func needToCheckNewerCLIVersion() bool { + disabledNotificationsUpdate := global.Config.GetBool(global.DisableNotificationsUpdate) + if disabledNotificationsUpdate { + return false + } + + latestUpdatedAt := global.Config.GetTime(global.LatestCLIVersionUpdatedAtEnv) + if latestUpdatedAt.IsZero() { + return true + } + + duration := time.Now().UTC().Sub(latestUpdatedAt) + return duration.Hours() > 24*7 // nolint:gomnd +} + +// getCurrentCLIVersion returns current CLI tag +func getCurrentCLIVersion() string { + return global.CurrentTag +} + +type TagResponse struct { + Name string `json:"name"` +} + +// getLatestCLIVersion returns latest CLI available tag +func getLatestCLIVersion() string { + client := &http.Client{} + + // Fetches tags in GitHub + req, err := http.NewRequest("GET", "https://api.github.com/repos/meroxa/cli/tags", nil) + if err != nil { + return "" + } + + req.Header.Add("Accept", "application/vnd.github.v3+json") + resp, err := client.Do(req) + if err != nil { + return "" + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + b, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalln(err) + } + + var result []TagResponse + if err := json.Unmarshal(b, &result); err != nil { + return "" + } + + return result[0].Name +} diff --git a/cmd/meroxa/builder/builder.go b/cmd/meroxa/builder/builder.go index 785eb9e59..3a09d12db 100644 --- a/cmd/meroxa/builder/builder.go +++ b/cmd/meroxa/builder/builder.go @@ -214,6 +214,9 @@ func BuildCobraCommand(c Command) *cobra.Command { buildCommandWithNoHeaders(cmd, c) buildCommandWithSubCommands(cmd, c) + // this will run for all commands using PostRun hook + buildCommandAutoUpdate(cmd, c) + // this has to be the last function, so it captures all errors from RunE buildCommandEvent(cmd, c) @@ -300,18 +303,7 @@ func buildCommandWithConfig(cmd *cobra.Command, c Command) { } } - err := global.Config.WriteConfig() - - if err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - err = global.Config.SafeWriteConfig() - } - if err != nil { - return fmt.Errorf("meroxa: could not write config file: %v", err) - } - } - - return nil + return writeConfigFile() } } @@ -482,6 +474,42 @@ func buildCommandEvent(cmd *cobra.Command, c Command) { } } +// This runs for all commands. +func buildCommandAutoUpdate(cmd *cobra.Command, c Command) { + oldPostRunE := cmd.PostRunE + cmd.PostRunE = func(cmd *cobra.Command, args []string) error { + if oldPostRunE != nil { + err := oldPostRunE(cmd, args) + if err != nil { + return err + } + } + + // Do not check and show warning to update when using --json + if cmd.Flags().Changed("json") { + return nil + } + + if needToCheckNewerCLIVersion() { + global.Config.Set(global.LatestCLIVersionUpdatedAtEnv, time.Now().UTC()) + + err := writeConfigFile() + if err != nil { + return err + } + + latestCLIVersion := getLatestCLIVersion() + + if getCurrentCLIVersion() != latestCLIVersion { + fmt.Printf("\n\n\t✨ meroxa %s is available! Update it by doing: `brew tap meroxa/taps && brew install meroxa`", latestCLIVersion) + fmt.Printf("\n\t🧐 You can check what changed in https://github.com/meroxa/cli/releases/tag/%s", latestCLIVersion) + fmt.Printf("\n\tπŸ’‘ To disable these warnings, run `meroxa config set %s=true`\n", global.DisableNotificationsUpdate) + } + } + return nil + } +} + func buildCommandWithExecute(cmd *cobra.Command, c Command) { v, ok := c.(CommandWithExecute) if !ok { @@ -752,3 +780,17 @@ func CheckFeatureFlag(exec Command, cmd CommandWithFeatureFlag) error { } return nil } + +func writeConfigFile() error { + err := global.Config.WriteConfig() + + if err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + err = global.Config.SafeWriteConfig() + } + if err != nil { + return fmt.Errorf("meroxa: could not write config file: %v", err) + } + } + return nil +} diff --git a/cmd/meroxa/global/global.go b/cmd/meroxa/global/global.go index 790adcc5e..066552ff2 100644 --- a/cmd/meroxa/global/global.go +++ b/cmd/meroxa/global/global.go @@ -26,8 +26,9 @@ import ( ) var ( - Version string - Config *viper.Viper + Version string + CurrentTag string + Config *viper.Viper ) var ( @@ -39,15 +40,17 @@ var ( ) const ( - AccessTokenEnv = "ACCESS_TOKEN" - ActorEnv = "ACTOR" - ActorUUIDEnv = "ACTOR_UUID" - CasedDebugEnv = "CASED_DEBUG" - CasedPublishKeyEnv = "CASED_PUBLISH_KEY" - PublishMetricsEnv = "PUBLISH_METRICS" - RefreshTokenEnv = "REFRESH_TOKEN" - UserFeatureFlagsEnv = "USER_FEATURE_FLAGS" - UserInfoUpdatedAtEnv = "USER_INFO_UPDATED_AT" + AccessTokenEnv = "ACCESS_TOKEN" + ActorEnv = "ACTOR" + ActorUUIDEnv = "ACTOR_UUID" + CasedDebugEnv = "CASED_DEBUG" + CasedPublishKeyEnv = "CASED_PUBLISH_KEY" + LatestCLIVersionUpdatedAtEnv = "LATEST_CLI_VERSION_UPDATED_AT" + DisableNotificationsUpdate = "DISABLE_NOTIFICATIONS_UPDATE" + PublishMetricsEnv = "PUBLISH_METRICS" + RefreshTokenEnv = "REFRESH_TOKEN" + UserFeatureFlagsEnv = "USER_FEATURE_FLAGS" + UserInfoUpdatedAtEnv = "USER_INFO_UPDATED_AT" ) func RegisterGlobalFlags(cmd *cobra.Command) { diff --git a/cmd/meroxa/main.go b/cmd/meroxa/main.go index 72aadc8c9..a92e39baa 100644 --- a/cmd/meroxa/main.go +++ b/cmd/meroxa/main.go @@ -34,6 +34,10 @@ func gitInfoNotEmpty() bool { } func main() { + // In production this will be the updated version by `goreleaser` + // TODO: Indicate where this is happening + global.CurrentTag = version + if gitInfoNotEmpty() { if GitCommit != "" { version += fmt.Sprintf(":%s", GitCommit) @@ -41,6 +45,7 @@ func main() { if GitLatestTag != "" { version += fmt.Sprintf(" %s", GitLatestTag) + global.CurrentTag = GitLatestTag } if GitUntracked != "" { From fb45a0fcd53c3e21870cc16aac939aaffb779209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Fri, 13 May 2022 11:14:52 +0200 Subject: [PATCH 3/9] doc: Add documentation for config set --- docs/cmd/md/meroxa_config.md | 1 + docs/cmd/md/meroxa_config_set.md | 40 +++++++++++++++++++ docs/cmd/www/meroxa-config-set.md | 47 +++++++++++++++++++++++ docs/cmd/www/meroxa-config.md | 1 + etc/completion/meroxa.completion.sh | 29 ++++++++++++++ etc/man/man1/meroxa-config-set.1 | 59 +++++++++++++++++++++++++++++ etc/man/man1/meroxa-config.1 | 2 +- 7 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 docs/cmd/md/meroxa_config_set.md create mode 100644 docs/cmd/www/meroxa-config-set.md create mode 100644 etc/man/man1/meroxa-config-set.1 diff --git a/docs/cmd/md/meroxa_config.md b/docs/cmd/md/meroxa_config.md index 11436b2bb..11020cde5 100644 --- a/docs/cmd/md/meroxa_config.md +++ b/docs/cmd/md/meroxa_config.md @@ -25,4 +25,5 @@ meroxa config [flags] * [meroxa](meroxa.md) - The Meroxa CLI * [meroxa config describe](meroxa_config_describe.md) - Show Meroxa CLI configuration details +* [meroxa config set](meroxa_config_set.md) - Update your Meroxa CLI configuration file with a specific key=value diff --git a/docs/cmd/md/meroxa_config_set.md b/docs/cmd/md/meroxa_config_set.md new file mode 100644 index 000000000..88f84c8a5 --- /dev/null +++ b/docs/cmd/md/meroxa_config_set.md @@ -0,0 +1,40 @@ +## meroxa config set + +Update your Meroxa CLI configuration file with a specific key=value + +### Synopsis + +This command will let you update your Meroxa configuration file to customize your CLI experience.You can check the presence of this file by running `meroxa config describe`, or even provide your own using `--config my-other-cfg-file`A key with a format such as MyKey will be converted automatically to as MY_KEY. + +``` +meroxa config set [flags] +``` + +### Examples + +``` +$ meroxa config set DisableUpdateNotification=true +$ meroxa config set DISABLE_UPDATE_NOTIFICATION=true +$ meroxa config set OneKey=true AnotherKey=false +$ meroxa config set ApiUrl=https://staging.meroxa.com +``` + +### Options + +``` + -h, --help help for set +``` + +### Options inherited from parent commands + +``` + --cli-config-file string meroxa configuration file + --debug display any debugging information + --json output json + --timeout duration set the duration of the client timeout in seconds (default 10s) +``` + +### SEE ALSO + +* [meroxa config](meroxa_config.md) - Manage your Meroxa CLI configuration + diff --git a/docs/cmd/www/meroxa-config-set.md b/docs/cmd/www/meroxa-config-set.md new file mode 100644 index 000000000..3e918300a --- /dev/null +++ b/docs/cmd/www/meroxa-config-set.md @@ -0,0 +1,47 @@ +--- +createdAt: +updatedAt: +title: "meroxa config set" +slug: meroxa-config-set +url: /cli/cmd/meroxa-config-set/ +--- +## meroxa config set + +Update your Meroxa CLI configuration file with a specific key=value + +### Synopsis + +This command will let you update your Meroxa configuration file to customize your CLI experience.You can check the presence of this file by running `meroxa config describe`, or even provide your own using `--config my-other-cfg-file`A key with a format such as MyKey will be converted automatically to as MY_KEY. + +``` +meroxa config set [flags] +``` + +### Examples + +``` +$ meroxa config set DisableUpdateNotification=true +$ meroxa config set DISABLE_UPDATE_NOTIFICATION=true +$ meroxa config set OneKey=true AnotherKey=false +$ meroxa config set ApiUrl=https://staging.meroxa.com +``` + +### Options + +``` + -h, --help help for set +``` + +### Options inherited from parent commands + +``` + --cli-config-file string meroxa configuration file + --debug display any debugging information + --json output json + --timeout duration set the duration of the client timeout in seconds (default 10s) +``` + +### SEE ALSO + +* [meroxa config](/cli/cmd/meroxa-config/) - Manage your Meroxa CLI configuration + diff --git a/docs/cmd/www/meroxa-config.md b/docs/cmd/www/meroxa-config.md index d0d3e3c37..a2b80bfc9 100644 --- a/docs/cmd/www/meroxa-config.md +++ b/docs/cmd/www/meroxa-config.md @@ -32,4 +32,5 @@ meroxa config [flags] * [meroxa](/cli/cmd/meroxa/) - The Meroxa CLI * [meroxa config describe](/cli/cmd/meroxa-config-describe/) - Show Meroxa CLI configuration details +* [meroxa config set](/cli/cmd/meroxa-config-set/) - Update your Meroxa CLI configuration file with a specific key=value diff --git a/etc/completion/meroxa.completion.sh b/etc/completion/meroxa.completion.sh index 82bd4d783..66d9e19d2 100644 --- a/etc/completion/meroxa.completion.sh +++ b/etc/completion/meroxa.completion.sh @@ -1017,6 +1017,34 @@ _meroxa_config_help() noun_aliases=() } +_meroxa_config_set() +{ + last_command="meroxa_config_set" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--help") + flags+=("-h") + flags+=("--cli-config-file=") + two_word_flags+=("--cli-config-file") + flags+=("--debug") + flags+=("--json") + flags+=("--timeout=") + two_word_flags+=("--timeout") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _meroxa_config() { last_command="meroxa_config" @@ -1026,6 +1054,7 @@ _meroxa_config() commands=() commands+=("describe") commands+=("help") + commands+=("set") flags=() two_word_flags=() diff --git a/etc/man/man1/meroxa-config-set.1 b/etc/man/man1/meroxa-config-set.1 new file mode 100644 index 000000000..9e4074a73 --- /dev/null +++ b/etc/man/man1/meroxa-config-set.1 @@ -0,0 +1,59 @@ +.nh +.TH "Meroxa" "1" "May 2022" "Meroxa CLI " "Meroxa Manual" + +.SH NAME +.PP +meroxa\-config\-set \- Update your Meroxa CLI configuration file with a specific key=value + + +.SH SYNOPSIS +.PP +\fBmeroxa config set [flags]\fP + + +.SH DESCRIPTION +.PP +This command will let you update your Meroxa configuration file to customize your CLI experience.You can check the presence of this file by running \fB\fCmeroxa config describe\fR, or even provide your own using \fB\fC\-\-config my\-other\-cfg\-file\fRA key with a format such as MyKey will be converted automatically to as MY\_KEY. + + +.SH OPTIONS +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for set + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +.PP +\fB\-\-cli\-config\-file\fP="" + meroxa configuration file + +.PP +\fB\-\-debug\fP[=false] + display any debugging information + +.PP +\fB\-\-json\fP[=false] + output json + +.PP +\fB\-\-timeout\fP=10s + set the duration of the client timeout in seconds + + +.SH EXAMPLE +.PP +.RS + +.nf +$ meroxa config set DisableUpdateNotification=true +$ meroxa config set DISABLE\_UPDATE\_NOTIFICATION=true +$ meroxa config set OneKey=true AnotherKey=false +$ meroxa config set ApiUrl=https://staging.meroxa.com + +.fi +.RE + + +.SH SEE ALSO +.PP +\fBmeroxa\-config(1)\fP diff --git a/etc/man/man1/meroxa-config.1 b/etc/man/man1/meroxa-config.1 index 522cd98ed..be8d5b79e 100644 --- a/etc/man/man1/meroxa-config.1 +++ b/etc/man/man1/meroxa-config.1 @@ -42,4 +42,4 @@ Manage your Meroxa CLI configuration .SH SEE ALSO .PP -\fBmeroxa(1)\fP, \fBmeroxa\-config\-describe(1)\fP +\fBmeroxa(1)\fP, \fBmeroxa\-config\-describe(1)\fP, \fBmeroxa\-config\-set(1)\fP From ad740f1311102ae8773676784d222d60acf1d8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Fri, 13 May 2022 11:15:15 +0200 Subject: [PATCH 4/9] test: Add tests and comments --- cmd/meroxa/builder/autoupdate.go | 43 ++++---- cmd/meroxa/builder/builder.go | 15 +-- cmd/meroxa/builder/builder_test.go | 3 + cmd/meroxa/main.go | 6 +- cmd/meroxa/root/apps/deploy.go | 10 +- cmd/meroxa/root/apps/describe.go | 2 +- cmd/meroxa/root/config/set.go | 33 +++--- cmd/meroxa/root/config/set_test.go | 158 +++++++++++++++++++++++++++++ 8 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 cmd/meroxa/root/config/set_test.go diff --git a/cmd/meroxa/builder/autoupdate.go b/cmd/meroxa/builder/autoupdate.go index bbb84b36c..f8a1e8e4c 100644 --- a/cmd/meroxa/builder/autoupdate.go +++ b/cmd/meroxa/builder/autoupdate.go @@ -17,16 +17,18 @@ limitations under the License. package builder import ( + "context" "encoding/json" "io" - "log" "net/http" "time" "github.com/meroxa/cli/cmd/meroxa/global" ) -// needToCheckNewerCLIVersion checks if CLI within a week +// needToCheckNewerCLIVersion checks different scenarios to determine whether to check or not +// 1. If user disabled auto-updating warning +// 2. If it checked within a week. func needToCheckNewerCLIVersion() bool { disabledNotificationsUpdate := global.Config.GetBool(global.DisableNotificationsUpdate) if disabledNotificationsUpdate { @@ -39,46 +41,47 @@ func needToCheckNewerCLIVersion() bool { } duration := time.Now().UTC().Sub(latestUpdatedAt) - return duration.Hours() > 24*7 // nolint:gomnd + return duration.Hours() > 24*7 } -// getCurrentCLIVersion returns current CLI tag +// getCurrentCLIVersion returns current CLI tag (example: `v2.0.0`) +// version, set by GoReleaser + `v` at the beginning. func getCurrentCLIVersion() string { return global.CurrentTag } -type TagResponse struct { - Name string `json:"name"` -} - -// getLatestCLIVersion returns latest CLI available tag -func getLatestCLIVersion() string { +// getLatestCLIVersion returns latest CLI available tag. +func getLatestCLIVersion(ctx context.Context) (string, error) { client := &http.Client{} // Fetches tags in GitHub - req, err := http.NewRequest("GET", "https://api.github.com/repos/meroxa/cli/tags", nil) + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/repos/meroxa/cli/tags", http.NoBody) if err != nil { - return "" + return "", err } req.Header.Add("Accept", "application/vnd.github.v3+json") resp, err := client.Do(req) if err != nil { - return "" + return "", err + } + + if resp.Body != nil { + defer resp.Body.Close() } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(resp.Body) b, err := io.ReadAll(resp.Body) if err != nil { - log.Fatalln(err) + return "", err + } + + var result []struct { + Name string `json:"name"` } - var result []TagResponse if err := json.Unmarshal(b, &result); err != nil { - return "" + return "", nil } - return result[0].Name + return result[0].Name, nil } diff --git a/cmd/meroxa/builder/builder.go b/cmd/meroxa/builder/builder.go index 3a09d12db..38a0def03 100644 --- a/cmd/meroxa/builder/builder.go +++ b/cmd/meroxa/builder/builder.go @@ -215,7 +215,7 @@ func BuildCobraCommand(c Command) *cobra.Command { buildCommandWithSubCommands(cmd, c) // this will run for all commands using PostRun hook - buildCommandAutoUpdate(cmd, c) + buildCommandAutoUpdate(cmd) // this has to be the last function, so it captures all errors from RunE buildCommandEvent(cmd, c) @@ -475,7 +475,7 @@ func buildCommandEvent(cmd *cobra.Command, c Command) { } // This runs for all commands. -func buildCommandAutoUpdate(cmd *cobra.Command, c Command) { +func buildCommandAutoUpdate(cmd *cobra.Command) { oldPostRunE := cmd.PostRunE cmd.PostRunE = func(cmd *cobra.Command, args []string) error { if oldPostRunE != nil { @@ -498,12 +498,15 @@ func buildCommandAutoUpdate(cmd *cobra.Command, c Command) { return err } - latestCLIVersion := getLatestCLIVersion() + latestCLIVersion, err := getLatestCLIVersion(cmd.Context()) + if err != nil { + return err + } if getCurrentCLIVersion() != latestCLIVersion { - fmt.Printf("\n\n\t✨ meroxa %s is available! Update it by doing: `brew tap meroxa/taps && brew install meroxa`", latestCLIVersion) - fmt.Printf("\n\t🧐 You can check what changed in https://github.com/meroxa/cli/releases/tag/%s", latestCLIVersion) - fmt.Printf("\n\tπŸ’‘ To disable these warnings, run `meroxa config set %s=true`\n", global.DisableNotificationsUpdate) + fmt.Printf("\n\n 🎁 meroxa %s is available! Update it running: `brew tap meroxa/taps && brew install meroxa`", latestCLIVersion) + fmt.Printf("\n 🧐 Check out latest changes in https://github.com/meroxa/cli/releases/tag/%s", latestCLIVersion) + fmt.Printf("\n πŸ’‘ To disable these warnings, run `meroxa config set %s=true`\n", global.DisableNotificationsUpdate) } } return nil diff --git a/cmd/meroxa/builder/builder_test.go b/cmd/meroxa/builder/builder_test.go index 1f26bf7ce..95f7781ef 100644 --- a/cmd/meroxa/builder/builder_test.go +++ b/cmd/meroxa/builder/builder_test.go @@ -284,6 +284,9 @@ func TestBuildCommandWithFlags(t *testing.T) { // Since we can't compare functions, we ignore RunE (coming from `buildCommandEvent`) got.RunE = nil + // Since we can't compare functions, we ignore PostRunE (coming from `buildCommandAutoUpdate`) + got.PostRunE = nil + if v := cmp.Diff(got, want, cmpopts.IgnoreUnexported(cobra.Command{})); v != "" { t.Fatalf(v) } diff --git a/cmd/meroxa/main.go b/cmd/meroxa/main.go index a92e39baa..c6e91f5ae 100644 --- a/cmd/meroxa/main.go +++ b/cmd/meroxa/main.go @@ -35,8 +35,9 @@ func gitInfoNotEmpty() bool { func main() { // In production this will be the updated version by `goreleaser` - // TODO: Indicate where this is happening - global.CurrentTag = version + // We need to include `v` since we'll compare with the actual tag in GitHub + // For more information see https://goreleaser.com/cookbooks/using-main.version + global.CurrentTag = fmt.Sprintf("v%s", version) if gitInfoNotEmpty() { if GitCommit != "" { @@ -45,7 +46,6 @@ func main() { if GitLatestTag != "" { version += fmt.Sprintf(" %s", GitLatestTag) - global.CurrentTag = GitLatestTag } if GitUntracked != "" { diff --git a/cmd/meroxa/root/apps/deploy.go b/cmd/meroxa/root/apps/deploy.go index ded8ac610..35950c679 100644 --- a/cmd/meroxa/root/apps/deploy.go +++ b/cmd/meroxa/root/apps/deploy.go @@ -324,12 +324,10 @@ func (d *Deploy) uploadFile(ctx context.Context, filePath, url string) error { d.logger.StopSpinnerWithStatus("\t", log.Failed) return err } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - d.logger.Error(ctx, err.Error()) - } - }(res.Body) + + if res.Body != nil { + defer res.Body.Close() + } d.logger.StopSpinnerWithStatus("Source uploaded!", log.Successful) return nil diff --git a/cmd/meroxa/root/apps/describe.go b/cmd/meroxa/root/apps/describe.go index ceae8d53e..c12e60d06 100644 --- a/cmd/meroxa/root/apps/describe.go +++ b/cmd/meroxa/root/apps/describe.go @@ -111,7 +111,7 @@ func (d *Describe) Execute(ctx context.Context) error { if err != nil { return err } - defer resp.Body.Close() //nolint:gocritic + defer resp.Body.Close() buf := new(bytes.Buffer) _, err = buf.ReadFrom(resp.Body) diff --git a/cmd/meroxa/root/config/set.go b/cmd/meroxa/root/config/set.go index a5fa882c8..9b5619f30 100644 --- a/cmd/meroxa/root/config/set.go +++ b/cmd/meroxa/root/config/set.go @@ -20,11 +20,12 @@ import ( "context" "errors" "fmt" + "regexp" + "strings" + "github.com/meroxa/cli/cmd/meroxa/builder" "github.com/meroxa/cli/config" "github.com/meroxa/cli/log" - "regexp" - "strings" ) var ( @@ -50,13 +51,13 @@ func (s *Set) Usage() string { func (s *Set) Docs() builder.Docs { return builder.Docs{ Short: "Update your Meroxa CLI configuration file with a specific key=value", - Example: "" + - "$ meroxa config set DisableUpdateNotification=true\n" + + Example: "$ meroxa config set DisableUpdateNotification=true\n" + "$ meroxa config set DISABLE_UPDATE_NOTIFICATION=true\n" + "$ meroxa config set OneKey=true AnotherKey=false\n" + - "$ meroxa config set ApiUrl=https://staging.meroxa.com\n", + "$ meroxa config set ApiUrl=https://staging.meroxa.com", Long: "This command will let you update your Meroxa configuration file to customize your CLI experience." + - "You can check the presence of this file by running `meroxa config describe`, or even provide your own using `--config my-other-cfg-file`" + + "You can check the presence of this file by running `meroxa config describe`, " + + "or even provide your own using `--config my-other-cfg-file`" + "A key with a format such as MyKey will be converted automatically to as MY_KEY.", } } @@ -65,11 +66,8 @@ func (s *Set) Execute(ctx context.Context) error { for k, v := range s.args.keys { s.logger.Infof(ctx, "Updating your Meroxa configuration file with %s=%s...", k, v) s.config.Set(k, v) - } s.logger.Info(ctx, "Done!") - // run over each key and write them to a file - // show confirmation message return nil } @@ -81,23 +79,28 @@ func (s *Set) Config(cfg config.Config) { s.config = cfg } -// TODO: Implement (and Test!) +// normalizeKey converts a key such as `myKey` to `MY_KEY`. func (s *Set) normalizeKey(key string) string { var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") - var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + k := matchFirstCap.ReplaceAllString(key, "${1}_${2}") - snake := matchFirstCap.ReplaceAllString(key, "${1}_${2}") - snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") - return strings.ToUpper(snake) + var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + k = matchAllCap.ReplaceAllString(k, "${1}_${2}") + return strings.ToUpper(k) } +// validateAndAssignKeyValue makes sure keys are provided with one (and only one) `=` sign. func (s *Set) validateAndAssignKeyValue(kv string) error { nkv := strings.Split(kv, "=") - if len(nkv) != 2 { + if len(nkv) != 2 { // nolint:gomnd return fmt.Errorf("a key=value needs to contain at least and only one `=` sign") } + if nkv[0] == "" { + return fmt.Errorf("there has to be at least a key before the `=` sign") + } + k := s.normalizeKey(nkv[0]) v := nkv[1] diff --git a/cmd/meroxa/root/config/set_test.go b/cmd/meroxa/root/config/set_test.go new file mode 100644 index 000000000..fd93e0b19 --- /dev/null +++ b/cmd/meroxa/root/config/set_test.go @@ -0,0 +1,158 @@ +package config + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/meroxa/cli/config" + "github.com/meroxa/cli/log" + + "testing" +) + +var ( + errRequiredMinArgs = errors.New("requires at least one KEY=VALUE pair (example: meroxa config set KEY=VALUE)") + errRequiredBothKeyAndValue = errors.New("there has to be at least a key before the `=` sign") + errRequiredOnlyOneEqualSign = errors.New("a key=value needs to contain at least and only one `=` sign") +) + +func TestSetConfigArgs(t *testing.T) { + tests := []struct { + args []string + err error + keys map[string]string + }{ + {args: nil, err: errRequiredMinArgs, keys: nil}, + {args: []string{"key=value"}, err: nil, keys: map[string]string{ + "KEY": "value", + }}, + {args: []string{"key=value", "key2=value2"}, err: nil, keys: map[string]string{ + "KEY": "value", + "KEY2": "value2", + }}, + {args: []string{"myGreatKey=value", "MY_EVEN_BETTER_KEY=value2"}, err: nil, keys: map[string]string{ + "MY_GREAT_KEY": "value", + "MY_EVEN_BETTER_KEY": "value2", + }}, + {args: []string{"key="}, err: nil, keys: map[string]string{ + "KEY": "", + }}, + {args: []string{"=value"}, err: errRequiredBothKeyAndValue, keys: nil}, + {args: []string{"key==value"}, err: errRequiredOnlyOneEqualSign, keys: nil}, + {args: []string{"key=value=value2", "MY_EVEN_BETTER_KEY=value2"}, err: errRequiredOnlyOneEqualSign, keys: nil}, + } + + for _, tt := range tests { + s := &Set{} + err := s.ParseArgs(tt.args) + + if err != nil && tt.err.Error() != err.Error() { + t.Fatalf("expected %q got %q", tt.err, err) + } + + if err == nil && !reflect.DeepEqual(tt.keys, s.args.keys) { + t.Fatalf("expected \"%v\" got \"%v\"", tt.keys, s.args.keys) + } + } +} + +func TestNormalizeKey(t *testing.T) { + tests := []struct { + key string + expected string + }{ + {key: "key", expected: "KEY"}, + {key: "myKey", expected: "MY_KEY"}, + {key: "my_key", expected: "MY_KEY"}, + {key: "myAnotherKey", expected: "MY_ANOTHER_KEY"}, + {key: "KEY", expected: "KEY"}, + {key: "1KEY", expected: "1_KEY"}, + {key: "MY_KEY", expected: "MY_KEY"}, + } + + for _, tt := range tests { + s := &Set{} + got := s.normalizeKey(tt.key) + + if got != tt.expected { + t.Fatalf("expected %q got %q", tt.expected, got) + } + } +} + +func TestValidateAndAssignKeyValue(t *testing.T) { + tests := []struct { + keyValue string + err error + expectedKey string + expectedValue string + }{ + {keyValue: "key==value", err: errRequiredOnlyOneEqualSign, expectedKey: "", expectedValue: ""}, + {keyValue: "key=", err: nil, expectedKey: "KEY", expectedValue: ""}, + {keyValue: "=value", err: errRequiredBothKeyAndValue, expectedKey: "", expectedValue: ""}, + {keyValue: "key=value", err: nil, expectedKey: "KEY", expectedValue: "value"}, + {keyValue: "KEY=value", err: nil, expectedKey: "KEY", expectedValue: "value"}, + {keyValue: "myKey=value", err: nil, expectedKey: "MY_KEY", expectedValue: "value"}, + } + + for _, tt := range tests { + s := &Set{} + s.args.keys = make(map[string]string) + err := s.validateAndAssignKeyValue(tt.keyValue) + + if err != nil && tt.err.Error() != err.Error() { + t.Fatalf("expected %q got %q", tt.err, err) + } + + if err == nil && s.args.keys[tt.expectedKey] != tt.expectedValue { + t.Fatalf("expected value %q for key %q, got %q", tt.expectedValue, tt.expectedKey, s.args.keys[tt.expectedKey]) + } + } +} + +func TestSetConfigExecution(t *testing.T) { + ctx := context.Background() + logger := log.NewTestLogger() + + k1 := "MY_KEY" + k2 := "ANOTHER_KEY" + + setKeys := map[string]string{ + k1: "value", + k2: "anotherValue", + } + + cfg := config.NewInMemoryConfig() + s := &Set{ + logger: logger, + config: cfg, + } + + s.args.keys = setKeys + + err := s.Execute(ctx) + + if err != nil { + t.Fatalf("not expected error, got %q", err.Error()) + } + + gotLeveledOutput := logger.LeveledOutput() + wantLeveledOutput := fmt.Sprintf("Updating your Meroxa configuration file with %s=%s...\n"+ + "Updating your Meroxa configuration file with %s=%s...\n"+ + "Done!", k1, setKeys[k1], k2, setKeys[k2]) + + if !strings.Contains(gotLeveledOutput, wantLeveledOutput) { + t.Fatalf("expected output:\n%s\ngot:\n%s", wantLeveledOutput, gotLeveledOutput) + } + + keys := []string{k1, k2} + + for _, k := range keys { + if s.config.GetString(k) != setKeys[k] { + t.Fatalf("expected value for key %q to be %q, got %q", k, setKeys[k], s.config.GetString(k)) + } + } +} From be9bc931011e4cc0bb9fc5203ad884a47c94c369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Wed, 25 May 2022 15:36:22 +0200 Subject: [PATCH 5/9] chore: upgrade golangci-lint-action --- .github/workflows/golangci-lint.yml | 7 ++++--- cmd/meroxa/builder/autoupdate.go | 4 ++++ cmd/meroxa/builder/builder.go | 2 +- cmd/meroxa/builder/builder_test.go | 3 +++ cmd/meroxa/root/apps/describe.go | 1 - 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index bb3c29883..b4ce45314 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -6,8 +6,9 @@ jobs: name: lint runs-on: ubuntu-latest steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.18 - uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 - with: - version: v1.45.2 + uses: golangci/golangci-lint-action@v3 diff --git a/cmd/meroxa/builder/autoupdate.go b/cmd/meroxa/builder/autoupdate.go index f8a1e8e4c..c67003beb 100644 --- a/cmd/meroxa/builder/autoupdate.go +++ b/cmd/meroxa/builder/autoupdate.go @@ -30,6 +30,10 @@ import ( // 1. If user disabled auto-updating warning // 2. If it checked within a week. func needToCheckNewerCLIVersion() bool { + if global.Config == nil { + return false + } + disabledNotificationsUpdate := global.Config.GetBool(global.DisableNotificationsUpdate) if disabledNotificationsUpdate { return false diff --git a/cmd/meroxa/builder/builder.go b/cmd/meroxa/builder/builder.go index 38a0def03..3e7c64435 100644 --- a/cmd/meroxa/builder/builder.go +++ b/cmd/meroxa/builder/builder.go @@ -504,7 +504,7 @@ func buildCommandAutoUpdate(cmd *cobra.Command) { } if getCurrentCLIVersion() != latestCLIVersion { - fmt.Printf("\n\n 🎁 meroxa %s is available! Update it running: `brew tap meroxa/taps && brew install meroxa`", latestCLIVersion) + fmt.Printf("\n\n 🎁 meroxa %s is available! To update it run: `brew upgrade meroxa`", latestCLIVersion) fmt.Printf("\n 🧐 Check out latest changes in https://github.com/meroxa/cli/releases/tag/%s", latestCLIVersion) fmt.Printf("\n πŸ’‘ To disable these warnings, run `meroxa config set %s=true`\n", global.DisableNotificationsUpdate) } diff --git a/cmd/meroxa/builder/builder_test.go b/cmd/meroxa/builder/builder_test.go index 95f7781ef..37d82a167 100644 --- a/cmd/meroxa/builder/builder_test.go +++ b/cmd/meroxa/builder/builder_test.go @@ -75,6 +75,9 @@ func TestBuildCobraCommand_Structural(t *testing.T) { // Since we can't compare functions, we ignore RunE (coming from `buildCommandEvent`) got.RunE = nil + // Since we can't compare functions, we ignore PostRunE (coming from `buildCommandAutoUpdate`) + got.PostRunE = nil + if v := cmp.Diff(got, want, cmpopts.IgnoreUnexported(cobra.Command{})); v != "" { t.Fatalf(v) } diff --git a/cmd/meroxa/root/apps/describe.go b/cmd/meroxa/root/apps/describe.go index c12e60d06..57acc2b97 100644 --- a/cmd/meroxa/root/apps/describe.go +++ b/cmd/meroxa/root/apps/describe.go @@ -111,7 +111,6 @@ func (d *Describe) Execute(ctx context.Context) error { if err != nil { return err } - defer resp.Body.Close() buf := new(bytes.Buffer) _, err = buf.ReadFrom(resp.Body) From 82246790109ded6f18a3817001886239c236ecc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Thu, 26 May 2022 13:55:52 +0200 Subject: [PATCH 6/9] test: Add tests for autoupdate and GitHub API --- cmd/meroxa/builder/autoupdate.go | 40 ------------ cmd/meroxa/builder/autoupdate_test.go | 76 +++++++++++++++++++++++ cmd/meroxa/builder/builder.go | 8 ++- cmd/meroxa/github/github.go | 88 +++++++++++++++++++++++++++ cmd/meroxa/github/github_test.go | 71 +++++++++++++++++++++ cmd/meroxa/root/config/set.go | 2 +- cmd/meroxa/root/config/set_test.go | 9 +-- 7 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 cmd/meroxa/builder/autoupdate_test.go create mode 100644 cmd/meroxa/github/github.go create mode 100644 cmd/meroxa/github/github_test.go diff --git a/cmd/meroxa/builder/autoupdate.go b/cmd/meroxa/builder/autoupdate.go index c67003beb..987e38681 100644 --- a/cmd/meroxa/builder/autoupdate.go +++ b/cmd/meroxa/builder/autoupdate.go @@ -17,10 +17,6 @@ limitations under the License. package builder import ( - "context" - "encoding/json" - "io" - "net/http" "time" "github.com/meroxa/cli/cmd/meroxa/global" @@ -53,39 +49,3 @@ func needToCheckNewerCLIVersion() bool { func getCurrentCLIVersion() string { return global.CurrentTag } - -// getLatestCLIVersion returns latest CLI available tag. -func getLatestCLIVersion(ctx context.Context) (string, error) { - client := &http.Client{} - - // Fetches tags in GitHub - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/repos/meroxa/cli/tags", http.NoBody) - if err != nil { - return "", err - } - - req.Header.Add("Accept", "application/vnd.github.v3+json") - resp, err := client.Do(req) - if err != nil { - return "", err - } - - if resp.Body != nil { - defer resp.Body.Close() - } - - b, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var result []struct { - Name string `json:"name"` - } - - if err := json.Unmarshal(b, &result); err != nil { - return "", nil - } - - return result[0].Name, nil -} diff --git a/cmd/meroxa/builder/autoupdate_test.go b/cmd/meroxa/builder/autoupdate_test.go new file mode 100644 index 000000000..5e28a0c75 --- /dev/null +++ b/cmd/meroxa/builder/autoupdate_test.go @@ -0,0 +1,76 @@ +package builder + +import ( + "testing" + "time" + + "github.com/meroxa/cli/cmd/meroxa/global" + "github.com/spf13/viper" +) + +func TestNeedToCheckNewerCLIVersion(t *testing.T) { + oldConfig := global.Config + + tests := []struct { + desc string + config func() *viper.Viper + expected bool + }{ + { + desc: "Global config is nil", + config: func() *viper.Viper { + return nil + }, + expected: false, + }, + { + desc: "Notifications are disabled by the user", + config: func() *viper.Viper { + cfg := viper.New() + cfg.Set(global.DisableNotificationsUpdate, "true") + return cfg + }, + expected: false, + }, + { + desc: "Version has never been checked", + config: viper.New, + expected: true, + }, + { + desc: "Version was recently checked", + config: func() *viper.Viper { + cfg := viper.New() + // Checked last time less than a week before + lastTimeChecked := time.Now().UTC().AddDate(0, 0, -7).Add(time.Minute * 1) + cfg.Set(global.LatestCLIVersionUpdatedAtEnv, lastTimeChecked) + return cfg + }, + expected: false, + }, + { + desc: "Version was checked more than a week before", + config: func() *viper.Viper { + cfg := viper.New() + // Checked last time more than a week before + lastTimeChecked := time.Now().UTC().AddDate(0, 0, -7).Add(-time.Minute * 1) + cfg.Set(global.LatestCLIVersionUpdatedAtEnv, lastTimeChecked) + return cfg + }, + expected: true, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + global.Config = tc.config() + got := needToCheckNewerCLIVersion() + + if tc.expected != got { + t.Fatalf("expected needToCheckNewerCLIVersion to be %v, got %v", tc.expected, got) + } + }) + } + + global.Config = oldConfig +} diff --git a/cmd/meroxa/builder/builder.go b/cmd/meroxa/builder/builder.go index 3e7c64435..0535ee859 100644 --- a/cmd/meroxa/builder/builder.go +++ b/cmd/meroxa/builder/builder.go @@ -21,10 +21,13 @@ import ( "context" "errors" "fmt" + "net/http" "os" "strings" "time" + "github.com/meroxa/cli/cmd/meroxa/github" + "github.com/cased/cased-go" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -498,14 +501,15 @@ func buildCommandAutoUpdate(cmd *cobra.Command) { return err } - latestCLIVersion, err := getLatestCLIVersion(cmd.Context()) + github.Client = &http.Client{} + latestCLIVersion, err := github.GetLatestCLITag(cmd.Context()) if err != nil { return err } if getCurrentCLIVersion() != latestCLIVersion { fmt.Printf("\n\n 🎁 meroxa %s is available! To update it run: `brew upgrade meroxa`", latestCLIVersion) - fmt.Printf("\n 🧐 Check out latest changes in https://github.com/meroxa/cli/releases/tag/%s", latestCLIVersion) + fmt.Printf("\n 🧐 Check out latest changes in https://github.com/meroxa/cli/releases/tag/v%s", latestCLIVersion) fmt.Printf("\n πŸ’‘ To disable these warnings, run `meroxa config set %s=true`\n", global.DisableNotificationsUpdate) } } diff --git a/cmd/meroxa/github/github.go b/cmd/meroxa/github/github.go new file mode 100644 index 000000000..3fe209d3f --- /dev/null +++ b/cmd/meroxa/github/github.go @@ -0,0 +1,88 @@ +/* +Copyright Β© 2022 Meroxa Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "regexp" +) + +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +var ( + Client HTTPClient +) + +// getContentHomebrewFormula returns from GitHub the content of the formula file for Meroxa CLI. +func getContentHomebrewFormula(ctx context.Context) (string, error) { + url := "https://api.github.com/repos/meroxa/homebrew-taps/contents/Formula/meroxa.rb" + req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) + if err != nil { + return "", err + } + + req.Header.Add("Accept", "application/vnd.github.v3+json") + resp, err := Client.Do(req) + if err != nil { + return "", err + } + + if resp.Body != nil { + defer resp.Body.Close() + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + type FormulaDefinition struct { + Content []byte `json:"content,omitempty"` + Encoding string `json:"encoding,omitempty"` + } + + var f FormulaDefinition + if err := json.Unmarshal(b, &f); err != nil { + return "", err + } + + return bytes.NewBuffer(f.Content).String(), nil +} + +// parseVersionFromFormulaFile receives the content of a formula file such as the one in +// "https://api.github.com/repos/meroxa/homebrew-taps/contents/Formula/meroxa.rb" +// and extracts its version number. +func parseVersionFromFormulaFile(content string) string { + r := regexp.MustCompile(`version "(\d+.\d+.\d+)"`) + return r.FindStringSubmatch(content)[1] +} + +// GetLatestCLITag fetches the content formula file from GitHub and then parses its version. +func GetLatestCLITag(ctx context.Context) (string, error) { + brewFormulaFile, err := getContentHomebrewFormula(ctx) + if err != nil { + return brewFormulaFile, err + } + + return parseVersionFromFormulaFile(brewFormulaFile), nil +} diff --git a/cmd/meroxa/github/github_test.go b/cmd/meroxa/github/github_test.go new file mode 100644 index 000000000..ebd790f79 --- /dev/null +++ b/cmd/meroxa/github/github_test.go @@ -0,0 +1,71 @@ +package github + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "reflect" + "testing" +) + +type MockDoType func(req *http.Request) (*http.Response, error) + +type MockClient struct { + MockDo MockDoType +} + +func (m *MockClient) Do(req *http.Request) (*http.Response, error) { + return m.MockDo(req) +} + +func TestGetLatestCLITag(t *testing.T) { + version := "2.2.0" + + f := fmt.Sprintf(` +class Meroxa < Formula + desc "The Meroxa CLI" + homepage "https://meroxa.io" + version "%s" +end +`, version) + + sEnc := base64.StdEncoding.EncodeToString([]byte(f)) + + jsonResponse := fmt.Sprintf(` + { + "content": "%s", + "encoding": "base64" + } + `, sEnc) + + r := io.NopCloser(bytes.NewReader([]byte(jsonResponse))) + Client = &MockClient{ + MockDo: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: r, + }, nil + }, + } + + ctx := context.Background() + gotContent, err := getContentHomebrewFormula(ctx) + + if err != nil { + t.Error("unexpected error, got: ", err) + return + } + + if !reflect.DeepEqual(gotContent, f) { + t.Errorf("expected %v, got %v", f, gotContent) + } + + gotVersion := parseVersionFromFormulaFile(gotContent) + + if gotVersion != version { + t.Errorf("expected version %q, got %q", version, gotVersion) + } +} diff --git a/cmd/meroxa/root/config/set.go b/cmd/meroxa/root/config/set.go index 9b5619f30..672fe6275 100644 --- a/cmd/meroxa/root/config/set.go +++ b/cmd/meroxa/root/config/set.go @@ -64,7 +64,7 @@ func (s *Set) Docs() builder.Docs { func (s *Set) Execute(ctx context.Context) error { for k, v := range s.args.keys { - s.logger.Infof(ctx, "Updating your Meroxa configuration file with %s=%s...", k, v) + s.logger.Infof(ctx, "Updating your Meroxa configuration file with \"%s=%s\"...", k, v) s.config.Set(k, v) } s.logger.Info(ctx, "Done!") diff --git a/cmd/meroxa/root/config/set_test.go b/cmd/meroxa/root/config/set_test.go index fd93e0b19..26464deca 100644 --- a/cmd/meroxa/root/config/set_test.go +++ b/cmd/meroxa/root/config/set_test.go @@ -118,11 +118,9 @@ func TestSetConfigExecution(t *testing.T) { logger := log.NewTestLogger() k1 := "MY_KEY" - k2 := "ANOTHER_KEY" setKeys := map[string]string{ k1: "value", - k2: "anotherValue", } cfg := config.NewInMemoryConfig() @@ -140,15 +138,14 @@ func TestSetConfigExecution(t *testing.T) { } gotLeveledOutput := logger.LeveledOutput() - wantLeveledOutput := fmt.Sprintf("Updating your Meroxa configuration file with %s=%s...\n"+ - "Updating your Meroxa configuration file with %s=%s...\n"+ - "Done!", k1, setKeys[k1], k2, setKeys[k2]) + wantLeveledOutput := fmt.Sprintf("Updating your Meroxa configuration file with \"%s=%s\"...\n"+ + "Done!", k1, setKeys[k1]) if !strings.Contains(gotLeveledOutput, wantLeveledOutput) { t.Fatalf("expected output:\n%s\ngot:\n%s", wantLeveledOutput, gotLeveledOutput) } - keys := []string{k1, k2} + keys := []string{k1} for _, k := range keys { if s.config.GetString(k) != setKeys[k] { From d64dded5fd0e1b8ca0d6783c0051c873bc9c8ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Thu, 26 May 2022 23:19:52 +0200 Subject: [PATCH 7/9] chore: clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that we’re fetching from brew’s formula, we can compare directly with version defined by GoReleaser --- cmd/meroxa/builder/autoupdate.go | 6 ------ cmd/meroxa/builder/builder.go | 2 +- cmd/meroxa/github/github.go | 4 ++-- cmd/meroxa/github/github_test.go | 2 -- cmd/meroxa/global/global.go | 5 ++--- cmd/meroxa/main.go | 7 ++----- 6 files changed, 7 insertions(+), 19 deletions(-) diff --git a/cmd/meroxa/builder/autoupdate.go b/cmd/meroxa/builder/autoupdate.go index 987e38681..514974487 100644 --- a/cmd/meroxa/builder/autoupdate.go +++ b/cmd/meroxa/builder/autoupdate.go @@ -43,9 +43,3 @@ func needToCheckNewerCLIVersion() bool { duration := time.Now().UTC().Sub(latestUpdatedAt) return duration.Hours() > 24*7 } - -// getCurrentCLIVersion returns current CLI tag (example: `v2.0.0`) -// version, set by GoReleaser + `v` at the beginning. -func getCurrentCLIVersion() string { - return global.CurrentTag -} diff --git a/cmd/meroxa/builder/builder.go b/cmd/meroxa/builder/builder.go index 0535ee859..9ad6d88d2 100644 --- a/cmd/meroxa/builder/builder.go +++ b/cmd/meroxa/builder/builder.go @@ -507,7 +507,7 @@ func buildCommandAutoUpdate(cmd *cobra.Command) { return err } - if getCurrentCLIVersion() != latestCLIVersion { + if global.Version != latestCLIVersion { fmt.Printf("\n\n 🎁 meroxa %s is available! To update it run: `brew upgrade meroxa`", latestCLIVersion) fmt.Printf("\n 🧐 Check out latest changes in https://github.com/meroxa/cli/releases/tag/v%s", latestCLIVersion) fmt.Printf("\n πŸ’‘ To disable these warnings, run `meroxa config set %s=true`\n", global.DisableNotificationsUpdate) diff --git a/cmd/meroxa/github/github.go b/cmd/meroxa/github/github.go index 3fe209d3f..46be5f5a7 100644 --- a/cmd/meroxa/github/github.go +++ b/cmd/meroxa/github/github.go @@ -57,8 +57,7 @@ func getContentHomebrewFormula(ctx context.Context) (string, error) { } type FormulaDefinition struct { - Content []byte `json:"content,omitempty"` - Encoding string `json:"encoding,omitempty"` + Content []byte `json:"content,omitempty"` } var f FormulaDefinition @@ -78,6 +77,7 @@ func parseVersionFromFormulaFile(content string) string { } // GetLatestCLITag fetches the content formula file from GitHub and then parses its version. +// example: 2.0.0 func GetLatestCLITag(ctx context.Context) (string, error) { brewFormulaFile, err := getContentHomebrewFormula(ctx) if err != nil { diff --git a/cmd/meroxa/github/github_test.go b/cmd/meroxa/github/github_test.go index ebd790f79..100200c84 100644 --- a/cmd/meroxa/github/github_test.go +++ b/cmd/meroxa/github/github_test.go @@ -33,7 +33,6 @@ end `, version) sEnc := base64.StdEncoding.EncodeToString([]byte(f)) - jsonResponse := fmt.Sprintf(` { "content": "%s", @@ -64,7 +63,6 @@ end } gotVersion := parseVersionFromFormulaFile(gotContent) - if gotVersion != version { t.Errorf("expected version %q, got %q", version, gotVersion) } diff --git a/cmd/meroxa/global/global.go b/cmd/meroxa/global/global.go index 066552ff2..68493a9db 100644 --- a/cmd/meroxa/global/global.go +++ b/cmd/meroxa/global/global.go @@ -26,9 +26,8 @@ import ( ) var ( - Version string - CurrentTag string - Config *viper.Viper + Version string + Config *viper.Viper ) var ( diff --git a/cmd/meroxa/main.go b/cmd/meroxa/main.go index c6e91f5ae..9e8d0ecb8 100644 --- a/cmd/meroxa/main.go +++ b/cmd/meroxa/main.go @@ -23,6 +23,8 @@ import ( ) var ( + // In production this will be the updated version by `goreleaser` which strips `v` from the tag + // For more information see https://goreleaser.com/cookbooks/using-main.version version = "dev" GitCommit string GitUntracked string @@ -34,11 +36,6 @@ func gitInfoNotEmpty() bool { } func main() { - // In production this will be the updated version by `goreleaser` - // We need to include `v` since we'll compare with the actual tag in GitHub - // For more information see https://goreleaser.com/cookbooks/using-main.version - global.CurrentTag = fmt.Sprintf("v%s", version) - if gitInfoNotEmpty() { if GitCommit != "" { version += fmt.Sprintf(":%s", GitCommit) From f3f4605091958b31e2cd24b91a8f6146da128312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Thu, 26 May 2022 23:23:07 +0200 Subject: [PATCH 8/9] feat: Make docs consistent --- cmd/meroxa/root/config/set.go | 8 ++++---- docs/cmd/md/meroxa_config_set.md | 8 ++++---- docs/cmd/www/meroxa-config-set.md | 8 ++++---- etc/man/man1/meroxa-config-set.1 | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cmd/meroxa/root/config/set.go b/cmd/meroxa/root/config/set.go index 672fe6275..b7f24c380 100644 --- a/cmd/meroxa/root/config/set.go +++ b/cmd/meroxa/root/config/set.go @@ -51,10 +51,10 @@ func (s *Set) Usage() string { func (s *Set) Docs() builder.Docs { return builder.Docs{ Short: "Update your Meroxa CLI configuration file with a specific key=value", - Example: "$ meroxa config set DisableUpdateNotification=true\n" + - "$ meroxa config set DISABLE_UPDATE_NOTIFICATION=true\n" + - "$ meroxa config set OneKey=true AnotherKey=false\n" + - "$ meroxa config set ApiUrl=https://staging.meroxa.com", + Example: "meroxa config set DisableUpdateNotification=true\n" + + "meroxa config set DISABLE_UPDATE_NOTIFICATION=true\n" + + "meroxa config set OneKey=true AnotherKey=false\n" + + "meroxa config set ApiUrl=https://staging.meroxa.com", Long: "This command will let you update your Meroxa configuration file to customize your CLI experience." + "You can check the presence of this file by running `meroxa config describe`, " + "or even provide your own using `--config my-other-cfg-file`" + diff --git a/docs/cmd/md/meroxa_config_set.md b/docs/cmd/md/meroxa_config_set.md index 88f84c8a5..0a2a31345 100644 --- a/docs/cmd/md/meroxa_config_set.md +++ b/docs/cmd/md/meroxa_config_set.md @@ -13,10 +13,10 @@ meroxa config set [flags] ### Examples ``` -$ meroxa config set DisableUpdateNotification=true -$ meroxa config set DISABLE_UPDATE_NOTIFICATION=true -$ meroxa config set OneKey=true AnotherKey=false -$ meroxa config set ApiUrl=https://staging.meroxa.com +meroxa config set DisableUpdateNotification=true +meroxa config set DISABLE_UPDATE_NOTIFICATION=true +meroxa config set OneKey=true AnotherKey=false +meroxa config set ApiUrl=https://staging.meroxa.com ``` ### Options diff --git a/docs/cmd/www/meroxa-config-set.md b/docs/cmd/www/meroxa-config-set.md index 3e918300a..8ef4ca056 100644 --- a/docs/cmd/www/meroxa-config-set.md +++ b/docs/cmd/www/meroxa-config-set.md @@ -20,10 +20,10 @@ meroxa config set [flags] ### Examples ``` -$ meroxa config set DisableUpdateNotification=true -$ meroxa config set DISABLE_UPDATE_NOTIFICATION=true -$ meroxa config set OneKey=true AnotherKey=false -$ meroxa config set ApiUrl=https://staging.meroxa.com +meroxa config set DisableUpdateNotification=true +meroxa config set DISABLE_UPDATE_NOTIFICATION=true +meroxa config set OneKey=true AnotherKey=false +meroxa config set ApiUrl=https://staging.meroxa.com ``` ### Options diff --git a/etc/man/man1/meroxa-config-set.1 b/etc/man/man1/meroxa-config-set.1 index 9e4074a73..de81a9109 100644 --- a/etc/man/man1/meroxa-config-set.1 +++ b/etc/man/man1/meroxa-config-set.1 @@ -45,10 +45,10 @@ This command will let you update your Meroxa configuration file to customize you .RS .nf -$ meroxa config set DisableUpdateNotification=true -$ meroxa config set DISABLE\_UPDATE\_NOTIFICATION=true -$ meroxa config set OneKey=true AnotherKey=false -$ meroxa config set ApiUrl=https://staging.meroxa.com +meroxa config set DisableUpdateNotification=true +meroxa config set DISABLE\_UPDATE\_NOTIFICATION=true +meroxa config set OneKey=true AnotherKey=false +meroxa config set ApiUrl=https://staging.meroxa.com .fi .RE From 770124b2aa8b6a9289867ba47f29411d4031ba1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Fri, 27 May 2022 13:46:56 +0200 Subject: [PATCH 9/9] refactor: No need to include this key --- cmd/meroxa/github/github_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/meroxa/github/github_test.go b/cmd/meroxa/github/github_test.go index 100200c84..44e304e77 100644 --- a/cmd/meroxa/github/github_test.go +++ b/cmd/meroxa/github/github_test.go @@ -35,8 +35,7 @@ end sEnc := base64.StdEncoding.EncodeToString([]byte(f)) jsonResponse := fmt.Sprintf(` { - "content": "%s", - "encoding": "base64" + "content": "%s" } `, sEnc)