diff --git a/.golangci.yaml b/.golangci.yaml index 3fd336f2..1995214e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -73,6 +73,7 @@ linters-settings: - "github.com/prometheus/client_model/go" - "github.com/prometheus/common/expfmt" - "github.com/panjf2000/gnet/v2" + - "github.com/spf13/cobra" tagalign: align: false sort: false diff --git a/cmd/cmd_helpers_test.go b/cmd/cmd_helpers_test.go new file mode 100644 index 00000000..3f1699ef --- /dev/null +++ b/cmd/cmd_helpers_test.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "bytes" + + "github.com/spf13/cobra" +) + +var ( + globalTestConfigFile = "./test_global.yaml" + pluginTestConfigFile = "./test_plugins.yaml" +) + +// executeCommandC executes a cobra command and returns the command, output, and error. +// Taken from https://github.com/spf13/cobra/blob/0c72800b8dba637092b57a955ecee75949e79a73/command_test.go#L48. +func executeCommandC(root *cobra.Command, args ...string) (string, error) { + buf := new(bytes.Buffer) + root.SetOut(buf) + root.SetErr(buf) + root.SetArgs(args) + + _, err := root.ExecuteC() + + return buf.String(), err +} diff --git a/cmd/config_init_test.go b/cmd/config_init_test.go new file mode 100644 index 00000000..8c220cb2 --- /dev/null +++ b/cmd/config_init_test.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_configInitCmd(t *testing.T) { + // Test configInitCmd. + output, err := executeCommandC(rootCmd, "config", "init", "-c", globalTestConfigFile) + assert.NoError(t, err, "configInitCmd should not return an error") + assert.Equal(t, + fmt.Sprintf("Config file '%s' was created successfully.", globalTestConfigFile), + output, + "configInitCmd should print the correct output") + // Check that the config file was created. + assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file") + + // Test configInitCmd with the --force flag to overwrite the config file. + output, err = executeCommandC(rootCmd, "config", "init", "--force") + assert.NoError(t, err, "configInitCmd should not return an error") + assert.Equal(t, + fmt.Sprintf("Config file '%s' was overwritten successfully.", globalTestConfigFile), + output, + "configInitCmd should print the correct output") + + // Clean up. + err = os.Remove(globalTestConfigFile) + assert.NoError(t, err) +} diff --git a/cmd/config_lint_test.go b/cmd/config_lint_test.go new file mode 100644 index 00000000..82cf9a1a --- /dev/null +++ b/cmd/config_lint_test.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_configLintCmd(t *testing.T) { + // Test configInitCmd. + output, err := executeCommandC(rootCmd, "config", "init", "-c", globalTestConfigFile) + assert.NoError(t, err, "configInitCmd should not return an error") + assert.Equal(t, + fmt.Sprintf("Config file '%s' was created successfully.", globalTestConfigFile), + output, + "configInitCmd should print the correct output") + // Check that the config file was created. + assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file") + + // Test configLintCmd. + output, err = executeCommandC(rootCmd, "config", "lint", "-c", globalTestConfigFile) + assert.NoError(t, err, "configLintCmd should not return an error") + assert.Equal(t, + "global config is valid\n", + output, + "configLintCmd should print the correct output") + + // Clean up. + err = os.Remove(globalTestConfigFile) + assert.NoError(t, err) +} diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 00000000..62870b55 --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_configCmd(t *testing.T) { + // Test configCmd with no arguments. + output, err := executeCommandC(rootCmd, "config") + assert.NoError(t, err, "configCmd should not return an error") + assert.Equal(t, + `Manage GatewayD global configuration + +Usage: + gatewayd config [flags] + gatewayd config [command] + +Available Commands: + init Create or overwrite the GatewayD global config + lint Lint the GatewayD global config + +Flags: + -h, --help help for config + +Use "gatewayd config [command] --help" for more information about a command. +`, + output, + "configCmd should print the correct output") +} diff --git a/cmd/plugin_init_test.go b/cmd/plugin_init_test.go new file mode 100644 index 00000000..9d06df61 --- /dev/null +++ b/cmd/plugin_init_test.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_pluginInitCmd(t *testing.T) { + // Test plugin init command. + output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin init command should not have returned an error") + assert.Equal(t, + fmt.Sprintf("Config file '%s' was created successfully.", pluginTestConfigFile), + output, + "plugin init command should have returned the correct output") + assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file") + + // Clean up. + err = os.Remove(pluginTestConfigFile) + assert.NoError(t, err) +} diff --git a/cmd/plugin_install.go b/cmd/plugin_install.go index 548289e3..9ec7dac4 100644 --- a/cmd/plugin_install.go +++ b/cmd/plugin_install.go @@ -85,7 +85,7 @@ var pluginInstallCmd = &cobra.Command{ splittedURL := strings.Split(args[0], "@") // If the version is not specified, use the latest version. if len(splittedURL) < NumParts { - log.Println("Version not specified. Using latest version") + cmd.Println("Version not specified. Using latest version") } if len(splittedURL) >= NumParts { pluginVersion = splittedURL[1] @@ -138,7 +138,9 @@ var pluginInstallCmd = &cobra.Command{ strings.Contains(name, archiveExt) }) if downloadURL != "" && releaseID != 0 { - downloadFile(client, account, pluginName, downloadURL, releaseID, pluginFilename) + cmd.Println("Downloading", downloadURL) + downloadFile(client, account, pluginName, releaseID, pluginFilename) + cmd.Println("Download completed successfully") } else { log.Panic("The plugin file could not be found in the release assets") } @@ -148,7 +150,9 @@ var pluginInstallCmd = &cobra.Command{ return strings.Contains(name, "checksums.txt") }) if checksumsFilename != "" && downloadURL != "" && releaseID != 0 { - downloadFile(client, account, pluginName, downloadURL, releaseID, checksumsFilename) + cmd.Println("Downloading", downloadURL) + downloadFile(client, account, pluginName, releaseID, checksumsFilename) + cmd.Println("Download completed successfully") } else { log.Panic("The checksum file could not be found in the release assets") } @@ -174,13 +178,13 @@ var pluginInstallCmd = &cobra.Command{ log.Panic("Checksum verification failed") } - log.Println("Checksum verification passed") + cmd.Println("Checksum verification passed") break } } if pullOnly { - log.Println("Plugin binary downloaded to", pluginFilename) + cmd.Println("Plugin binary downloaded to", pluginFilename) return } } else { @@ -204,7 +208,7 @@ var pluginInstallCmd = &cobra.Command{ pluginFileSum := "" for _, filename := range filenames { if strings.Contains(filename, pluginName) { - log.Println("Plugin binary extracted to", filename) + cmd.Println("Plugin binary extracted to", filename) localPath = filename // Get the checksum for the extracted plugin binary. // TODO: Should we verify the checksum using the checksum.txt file instead? @@ -303,7 +307,7 @@ var pluginInstallCmd = &cobra.Command{ } // TODO: Add a rollback mechanism. - log.Println("Plugin installed successfully") + cmd.Println("Plugin installed successfully") }, } diff --git a/cmd/plugin_install_test.go b/cmd/plugin_install_test.go new file mode 100644 index 00000000..0f931aa0 --- /dev/null +++ b/cmd/plugin_install_test.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_pluginInstallCmd(t *testing.T) { + // Create a test plugin config file. + output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin init should not return an error") + assert.Equal(t, + fmt.Sprintf("Config file '%s' was created successfully.", pluginTestConfigFile), + output, + "plugin init command should have returned the correct output") + assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file") + + // Test plugin install command. + output, err = executeCommandC( + rootCmd, "plugin", "install", + "github.com/gatewayd-io/gatewayd-plugin-cache@v0.2.4", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin install should not return an error") + assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz") //nolint:lll + assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/checksums.txt") //nolint:lll + assert.Contains(t, output, "Download completed successfully") + assert.Contains(t, output, "Checksum verification passed") + assert.Contains(t, output, "Plugin binary extracted to plugins/gatewayd-plugin-cache") + assert.Contains(t, output, "Plugin installed successfully") + + // See if the plugin was actually installed. + output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin list should not return an error") + assert.Contains(t, output, "Name: gatewayd-plugin-cache") + + // Clean up. + assert.NoError(t, os.RemoveAll("plugins/")) + assert.NoError(t, os.Remove("checksums.txt")) + assert.NoError(t, os.Remove("gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz")) + assert.NoError(t, os.Remove(pluginTestConfigFile)) +} diff --git a/cmd/plugin_lint_test.go b/cmd/plugin_lint_test.go new file mode 100644 index 00000000..07dd57cf --- /dev/null +++ b/cmd/plugin_lint_test.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_pluginLintCmd(t *testing.T) { + // Test plugin lint command. + output, err := executeCommandC(rootCmd, "plugin", "lint", "-p", "../gatewayd_plugins.yaml") + assert.NoError(t, err, "plugin lint command should not have returned an error") + assert.Equal(t, + "plugins config is valid\n", + output, + "plugin lint command should have returned the correct output") +} diff --git a/cmd/plugin_list_test.go b/cmd/plugin_list_test.go new file mode 100644 index 00000000..8b53dc53 --- /dev/null +++ b/cmd/plugin_list_test.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_pluginListCmd(t *testing.T) { + // Test plugin list command. + output, err := executeCommandC(rootCmd, "plugin", "init", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin init command should not have returned an error") + assert.Equal(t, + fmt.Sprintf("Config file '%s' was created successfully.", pluginTestConfigFile), + output, + "plugin init command should have returned the correct output") + assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file") + + output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin list command should not have returned an error") + assert.Equal(t, + "No plugins found\n", + output, + "plugin list command should have returned empty output") + + // Clean up. + err = os.Remove(pluginTestConfigFile) + assert.NoError(t, err) +} + +func Test_pluginListCmdWithPlugins(t *testing.T) { + // Test plugin list command. + // Read the plugin config file from the root directory. + pluginTestConfigFile := "../gatewayd_plugins.yaml" + output, err := executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin list command should not have returned an error") + assert.Equal(t, `Total plugins: 1 +Plugins: + Name: gatewayd-plugin-cache + Enabled: true + Path: ../gatewayd-plugin-cache/gatewayd-plugin-cache + Args: --log-level debug + Env: + MAGIC_COOKIE_KEY=GATEWAYD_PLUGIN + MAGIC_COOKIE_VALUE=5712b87aa5d7e9f9e9ab643e6603181c5b796015cb1c09d6f5ada882bf2a1872 + REDIS_URL=redis://localhost:6379/0 + EXPIRY=1h + METRICS_ENABLED=True + METRICS_UNIX_DOMAIN_SOCKET=/tmp/gatewayd-plugin-cache.sock + METRICS_PATH=/metrics + PERIODIC_INVALIDATOR_ENABLED=True + PERIODIC_INVALIDATOR_INTERVAL=1m + PERIODIC_INVALIDATOR_START_DELAY=1m + API_ADDRESS=localhost:18080 + EXIT_ON_STARTUP_ERROR=False + SENTRY_DSN=https://70eb1abcd32e41acbdfc17bc3407a543@o4504550475038720.ingest.sentry.io/4505342961123328 + Checksum: 054e7dba9c1e3e3910f4928a000d35c8a6199719fad505c66527f3e9b1993833 +`, + output, + "plugin list command should have returned the correct output") +} diff --git a/cmd/plugin_test.go b/cmd/plugin_test.go new file mode 100644 index 00000000..4f8233a3 --- /dev/null +++ b/cmd/plugin_test.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_pluginCmd(t *testing.T) { + // Test pluginCmd with no arguments. + output, err := executeCommandC(rootCmd, "plugin") + assert.NoError(t, err, "pluginCmd should not return an error") + assert.Equal(t, `Manage plugins and their configuration + +Usage: + gatewayd plugin [flags] + gatewayd plugin [command] + +Available Commands: + init Create or overwrite the GatewayD plugins config + install Install a plugin from a local archive or a GitHub repository + lint Lint the GatewayD plugins config + list List the GatewayD plugins + +Flags: + -h, --help help for plugin + +Use "gatewayd plugin [command] --help" for more information about a command. +`, + output, + "pluginCmd should print the correct output") +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 00000000..ea2ee268 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_rootCmd(t *testing.T) { + output, err := executeCommandC(rootCmd) + assert.NoError(t, err, "rootCmd should not return an error") + //nolint:lll + assert.Equal(t, + `GatewayD is a cloud-native database gateway and framework for building data-driven applications. It sits between your database servers and clients and proxies all their communication. + +Usage: + gatewayd [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + config Manage GatewayD global configuration + help Help about any command + plugin Manage plugins and their configuration + run Run a GatewayD instance + version Show version information + +Flags: + -h, --help help for gatewayd + +Use "gatewayd [command] --help" for more information about a command. +`, + output, + "rootCmd should print the correct output") +} diff --git a/cmd/run.go b/cmd/run.go index 188b19d5..42de63da 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -41,6 +41,8 @@ import ( "google.golang.org/grpc/credentials" ) +// TODO: Get rid of the global variables. +// https://github.com/gatewayd-io/gatewayd/issues/324 var ( enableTracing bool collectorURL string @@ -60,8 +62,71 @@ var ( proxies = make(map[string]*network.Proxy) servers = make(map[string]*network.Server) healthCheckScheduler = gocron.NewScheduler(time.UTC) + + stopChan = make(chan struct{}) ) +func StopGracefully( + runCtx context.Context, + pluginTimeoutCtx context.Context, + sig os.Signal, + metricsMerger *metrics.Merger, + pluginRegistry *plugin.Registry, + logger zerolog.Logger, + servers map[string]*network.Server, + stopChan chan struct{}, +) { + _, span := otel.Tracer(config.TracerName).Start(runCtx, "Shutdown server") + signal := "unknown" + if sig != nil { + signal = sig.String() + } + + logger.Info().Msg("Notifying the plugins that the server is shutting down") + if pluginRegistry != nil { + _, err := pluginRegistry.Run( + pluginTimeoutCtx, + map[string]interface{}{"signal": signal}, + v1.HookName_HOOK_NAME_ON_SIGNAL, + ) + if err != nil { + logger.Error().Err(err).Msg("Failed to run OnSignal hooks") + span.RecordError(err) + } + } + + logger.Info().Msg("Stopping GatewayD") + span.AddEvent("Stopping GatewayD", trace.WithAttributes( + attribute.String("signal", signal), + )) + if healthCheckScheduler != nil { + healthCheckScheduler.Clear() + logger.Info().Msg("Stopped health check scheduler") + span.AddEvent("Stopped health check scheduler") + } + if metricsMerger != nil { + metricsMerger.Stop() + logger.Info().Msg("Stopped metrics merger") + span.AddEvent("Stopped metrics merger") + } + for name, server := range servers { + logger.Info().Str("name", name).Msg("Stopping server") + server.Shutdown() //nolint:contextcheck + span.AddEvent("Stopped server") + } + logger.Info().Msg("Stopped all servers") + if pluginRegistry != nil { + pluginRegistry.Shutdown() + logger.Info().Msg("Stopped plugin registry") + span.AddEvent("Stopped plugin registry") + } + span.End() + + // Close the stop channel to notify the other goroutines to stop. + stopChan <- struct{}{} + close(stopChan) +} + // runCmd represents the run command. var runCmd = &cobra.Command{ Use: "run", @@ -289,7 +354,13 @@ var runCmd = &cobra.Command{ if conf.Plugin.EnableMetricsMerger && metricsMerger != nil { handler = mergedMetricsHandler(handler) } - http.Handle(metricsConfig.Path, gziphandler.GzipHandler(handler)) + // Check if the metrics server is already running before registering the handler. + if _, err = http.Get(address); err != nil { //nolint:gosec + http.Handle(metricsConfig.Path, gziphandler.GzipHandler(handler)) + } else { + logger.Warn().Msg("Metrics server is already running, consider changing the port") + span.RecordError(err) + } //nolint:gosec if err = http.ListenAndServe( @@ -632,42 +703,16 @@ var runCmd = &cobra.Command{ for sig := range signalsCh { for _, s := range signals { if sig != s { - _, span := otel.Tracer(config.TracerName).Start(runCtx, "Shutdown server") - - logger.Info().Msg("Notifying the plugins that the server is shutting down") - _, err := pluginRegistry.Run( + StopGracefully( + runCtx, pluginTimeoutCtx, - map[string]interface{}{"signal": sig.String()}, - v1.HookName_HOOK_NAME_ON_SIGNAL, + sig, + metricsMerger, + pluginRegistry, + logger, + servers, + stopChan, ) - if err != nil { - logger.Error().Err(err).Msg("Failed to run OnSignal hooks") - span.RecordError(err) - } - - logger.Info().Msg("Stopping GatewayD") - span.AddEvent("Stopping GatewayD", trace.WithAttributes( - attribute.String("signal", sig.String()), - )) - healthCheckScheduler.Clear() - logger.Info().Msg("Stopped health check scheduler") - span.AddEvent("Stopped health check scheduler") - if metricsMerger != nil { - metricsMerger.Stop() - logger.Info().Msg("Stopped metrics merger") - span.AddEvent("Stopped metrics merger") - } - for name, server := range servers { - logger.Info().Str("name", name).Msg("Stopping server") - server.Shutdown() - span.AddEvent("Stopped server") - } - logger.Info().Msg("Stopped all servers") - pluginRegistry.Shutdown() - logger.Info().Msg("Stopped plugin registry") - span.AddEvent("Stopped plugin registry") - - span.End() os.Exit(0) } } @@ -703,7 +748,7 @@ var runCmd = &cobra.Command{ span.End() // Wait for the server to shutdown. - <-make(chan struct{}) + <-stopChan }, } diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 00000000..fd7ef8f0 --- /dev/null +++ b/cmd/run_test.go @@ -0,0 +1,156 @@ +package cmd + +import ( + "os" + "sync" + "testing" + "time" + + "github.com/gatewayd-io/gatewayd/config" + "github.com/stretchr/testify/assert" + "github.com/zenizh/go-capturer" +) + +func Test_runCmd(t *testing.T) { + // Create a test plugins config file. + _, err := executeCommandC(rootCmd, "plugin", "init", "--force", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin init command should not have returned an error") + assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file") + + // Create a test config file. + _, err = executeCommandC(rootCmd, "config", "init", "--force", "-c", globalTestConfigFile) + assert.NoError(t, err, "configInitCmd should not return an error") + // Check that the config file was created. + assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file") + + var waitGroup sync.WaitGroup + waitGroup.Add(1) + go func(waitGroup *sync.WaitGroup) { + time.Sleep(100 * time.Millisecond) + + StopGracefully( + runCmd.Context(), + runCmd.Context(), + nil, + nil, + nil, + loggers[config.Default], + servers, + stopChan, + ) + + waitGroup.Done() + }(&waitGroup) + + waitGroup.Add(1) + go func(waitGroup *sync.WaitGroup) { + // Test run command. + output := capturer.CaptureOutput(func() { + _, err := executeCommandC(rootCmd, "run", "-c", globalTestConfigFile, "-p", pluginTestConfigFile) + assert.NoError(t, err, "run command should not have returned an error") + }) + // Print the output for debugging purposes. + runCmd.Print(output) + // Check if GatewayD started and stopped correctly. + assert.Contains(t, + output, + "GatewayD is running", + "run command should have returned the correct output") + assert.Contains(t, + output, + "Stopped all servers\n", + "run command should have returned the correct output") + + waitGroup.Done() + }(&waitGroup) + + waitGroup.Wait() + + // Clean up. + assert.NoError(t, os.Remove(pluginTestConfigFile)) + assert.NoError(t, os.Remove(globalTestConfigFile)) +} + +func Test_runCmdWithCachePlugin(t *testing.T) { + // TODO: Remove this once these global variables are removed from cmd/run.go. + // https://github.com/gatewayd-io/gatewayd/issues/324 + stopChan = make(chan struct{}) + + // Create a test plugins config file. + _, err := executeCommandC(rootCmd, "plugin", "init", "--force", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin init command should not have returned an error") + assert.FileExists(t, pluginTestConfigFile, "plugin init command should have created a config file") + + // Create a test config file. + _, err = executeCommandC(rootCmd, "config", "init", "--force", "-c", globalTestConfigFile) + assert.NoError(t, err, "configInitCmd should not return an error") + // Check that the config file was created. + assert.FileExists(t, globalTestConfigFile, "configInitCmd should create a config file") + + // Test plugin install command. + output, err := executeCommandC( + rootCmd, "plugin", "install", + "github.com/gatewayd-io/gatewayd-plugin-cache@v0.2.4", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin install should not return an error") + assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz") //nolint:lll + assert.Contains(t, output, "Downloading https://github.com/gatewayd-io/gatewayd-plugin-cache/releases/download/v0.2.4/checksums.txt") //nolint:lll + assert.Contains(t, output, "Download completed successfully") + assert.Contains(t, output, "Checksum verification passed") + assert.Contains(t, output, "Plugin binary extracted to plugins/gatewayd-plugin-cache") + assert.Contains(t, output, "Plugin installed successfully") + + // See if the plugin was actually installed. + output, err = executeCommandC(rootCmd, "plugin", "list", "-p", pluginTestConfigFile) + assert.NoError(t, err, "plugin list should not return an error") + assert.Contains(t, output, "Name: gatewayd-plugin-cache") + + var waitGroup sync.WaitGroup + waitGroup.Add(1) + go func(waitGroup *sync.WaitGroup) { + time.Sleep(500 * time.Millisecond) + + StopGracefully( + runCmd.Context(), + runCmd.Context(), + nil, + nil, + nil, + loggers[config.Default], + servers, + stopChan, + ) + + waitGroup.Done() + }(&waitGroup) + + waitGroup.Add(1) + go func(waitGroup *sync.WaitGroup) { + // Test run command. + output := capturer.CaptureOutput(func() { + _, err := executeCommandC(rootCmd, "run", "-c", globalTestConfigFile, "-p", pluginTestConfigFile) + assert.NoError(t, err, "run command should not have returned an error") + }) + // Print the output for debugging purposes. + runCmd.Print(output) + // Check if GatewayD started and stopped correctly. + assert.Contains(t, + output, + "GatewayD is running", + "run command should have returned the correct output") + assert.Contains(t, + output, + "Stopped all servers\n", + "run command should have returned the correct output") + + waitGroup.Done() + }(&waitGroup) + + waitGroup.Wait() + + // Clean up. + assert.NoError(t, os.RemoveAll("plugins/")) + assert.NoError(t, os.Remove("checksums.txt")) + assert.NoError(t, os.Remove("gatewayd-plugin-cache-linux-amd64-v0.2.4.tar.gz")) + assert.NoError(t, os.Remove(pluginTestConfigFile)) + assert.NoError(t, os.Remove(globalTestConfigFile)) +} diff --git a/cmd/utils.go b/cmd/utils.go index bc8115d1..05e459b8 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -44,7 +44,9 @@ var ( ) // generateConfig generates a config file of the given type. -func generateConfig(cmd *cobra.Command, fileType configFileType, configFile string, force bool) { +func generateConfig( + cmd *cobra.Command, fileType configFileType, configFile string, forceRewriteFile bool, +) { logger := log.New(cmd.OutOrStdout(), "", 0) // Create a new config object and load the defaults. @@ -71,7 +73,7 @@ func generateConfig(cmd *cobra.Command, fileType configFileType, configFile stri // Check if the config file already exists and if we should overwrite it. exists := false - if _, err := os.Stat(configFile); err == nil && !force { + if _, err := os.Stat(configFile); err == nil && !forceRewriteFile { logger.Fatal( "Config file already exists. Use --force to overwrite or choose a different filename.") } else if err == nil { @@ -84,10 +86,10 @@ func generateConfig(cmd *cobra.Command, fileType configFileType, configFile stri } verb := "created" - if exists && force { + if exists && forceRewriteFile { verb = "overwritten" } - logger.Printf("Config file '%s' was %s successfully.", configFile, verb) + cmd.Printf("Config file '%s' was %s successfully.", configFile, verb) } func lintConfig(cmd *cobra.Command, fileType configFileType, configFile string) { @@ -161,12 +163,10 @@ func lintConfig(cmd *cobra.Command, fileType configFileType, configFile string) logger.Fatalf("Error validating %s config: %s\n", string(fileType), err) } - logger.Printf("%s config is valid\n", fileType) + cmd.Printf("%s config is valid\n", fileType) } func listPlugins(cmd *cobra.Command, pluginConfigFile string, onlyEnabled bool) { - logger := log.New(cmd.OutOrStdout(), "", 0) - // Load the plugin config file. conf := config.NewConfig(context.TODO(), "", pluginConfigFile) conf.LoadDefaults(context.TODO()) @@ -174,8 +174,10 @@ func listPlugins(cmd *cobra.Command, pluginConfigFile string, onlyEnabled bool) conf.UnmarshalPluginConfig(context.TODO()) if len(conf.Plugin.Plugins) != 0 { - logger.Printf("Total plugins: %d\n", len(conf.Plugin.Plugins)) - logger.Println("Plugins:") + cmd.Printf("Total plugins: %d\n", len(conf.Plugin.Plugins)) + cmd.Println("Plugins:") + } else { + cmd.Println("No plugins found") } // Print the list of plugins. @@ -183,15 +185,15 @@ func listPlugins(cmd *cobra.Command, pluginConfigFile string, onlyEnabled bool) if onlyEnabled && !plugin.Enabled { continue } - logger.Printf(" Name: %s\n", plugin.Name) - logger.Printf(" Enabled: %t\n", plugin.Enabled) - logger.Printf(" Path: %s\n", plugin.LocalPath) - logger.Printf(" Args: %s\n", strings.Join(plugin.Args, " ")) - logger.Println(" Env:") + cmd.Printf(" Name: %s\n", plugin.Name) + cmd.Printf(" Enabled: %t\n", plugin.Enabled) + cmd.Printf(" Path: %s\n", plugin.LocalPath) + cmd.Printf(" Args: %s\n", strings.Join(plugin.Args, " ")) + cmd.Println(" Env:") for _, env := range plugin.Env { - logger.Printf(" %s\n", env) + cmd.Printf(" %s\n", env) } - logger.Printf(" Checksum: %s\n", plugin.Checksum) + cmd.Printf(" Checksum: %s\n", plugin.Checksum) } } @@ -388,11 +390,8 @@ func findAsset(release *github.RepositoryRelease, match func(string) bool) (stri } func downloadFile( - client *github.Client, account, pluginName, downloadURL string, - releaseID int64, filename string, + client *github.Client, account, pluginName string, releaseID int64, filename string, ) { - log.Println("Downloading", downloadURL) - // Download the plugin. readCloser, redirectURL, err := client.Repositories.DownloadReleaseAsset( context.Background(), account, pluginName, releaseID, http.DefaultClient) @@ -445,6 +444,4 @@ func downloadFile( if err != nil { log.Panic("There was an error downloading the plugin: ", err) } - - log.Println("Download completed successfully") } diff --git a/cmd/version.go b/cmd/version.go index 7331f6c1..94c44cba 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,8 +1,6 @@ package cmd import ( - "fmt" - "github.com/gatewayd-io/gatewayd/config" "github.com/spf13/cobra" ) @@ -11,8 +9,8 @@ import ( var versionCmd = &cobra.Command{ Use: "version", Short: "Show version information", - Run: func(_ *cobra.Command, _ []string) { - fmt.Println(config.VersionInfo()) //nolint:forbidigo + Run: func(cmd *cobra.Command, _ []string) { + cmd.Println(config.VersionInfo()) }, } diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 00000000..c648cad2 --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "regexp" + "testing" + + "github.com/gatewayd-io/gatewayd/config" + "github.com/stretchr/testify/assert" +) + +func Test_versionCmd(t *testing.T) { + // Test versionCmd with no arguments. + config.Version = "SEMVER" + config.VersionDetails = "COMMIT-HASH" + output, err := executeCommandC(rootCmd, "version") + assert.NoError(t, err, "versionCmd should not return an error") + assert.Regexp(t, + // The regexp matches something like the following output: + // GatewayD v0.7.7 (2023-09-16T19:27:38+0000/038f75b, go1.21.0, linux/amd64) + regexp.MustCompile(`^GatewayD SEMVER \(COMMIT-HASH, go\d+\.\d+\.\d+, \w+/\w+\)\n$`), + output, + "versionCmd should print the correct output") +} diff --git a/go.mod b/go.mod index 3498d55d..a0f702e8 100644 --- a/go.mod +++ b/go.mod @@ -8,17 +8,17 @@ require ( github.com/codingsince1985/checksum v1.3.0 github.com/envoyproxy/protoc-gen-validate v1.0.2 github.com/gatewayd-io/gatewayd-plugin-sdk v0.1.1 - github.com/getsentry/sentry-go v0.24.0 + github.com/getsentry/sentry-go v0.24.1 github.com/go-co-op/gocron v1.33.1 github.com/google/go-cmp v0.5.9 github.com/google/go-github/v53 v53.2.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-plugin v1.5.1 github.com/invopop/jsonschema v0.8.0 github.com/knadh/koanf v1.5.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/panjf2000/gnet/v2 v2.3.1 + github.com/panjf2000/gnet/v2 v2.3.2 github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_model v0.4.0 github.com/prometheus/common v0.44.0 @@ -27,14 +27,14 @@ require ( github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 - go.opentelemetry.io/otel v1.17.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 - go.opentelemetry.io/otel/sdk v1.17.0 - go.opentelemetry.io/otel/trace v1.17.0 + go.opentelemetry.io/otel v1.18.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0 + go.opentelemetry.io/otel/sdk v1.18.0 + go.opentelemetry.io/otel/trace v1.18.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d - google.golang.org/grpc v1.58.0 + google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb + google.golang.org/grpc v1.58.1 google.golang.org/protobuf v1.31.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -71,11 +71,11 @@ require ( github.com/robfig/cron/v3 v3.0.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.17.0 // indirect + go.opentelemetry.io/otel/metric v1.18.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.25.0 // indirect + go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.13.0 // indirect golang.org/x/net v0.15.0 // indirect golang.org/x/oauth2 v0.12.0 // indirect @@ -83,6 +83,6 @@ require ( golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect ) diff --git a/go.sum b/go.sum index a65e999f..a731b08c 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,6 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72H github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -78,8 +76,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gatewayd-io/gatewayd-plugin-sdk v0.1.1 h1:ujlh6TSDFmG2e9VtY6hZk8BW0p7i0AEQL3BpMMLzxwc= github.com/gatewayd-io/gatewayd-plugin-sdk v0.1.1/go.mod h1:B4oWVHf7NeSCs7szN8nrlIO6tkznV1F3ZMqE9VxDtKY= -github.com/getsentry/sentry-go v0.24.0 h1:02b7qEmJ56EHGe9KFgjArjU/vG/aywm7Efgu+iPc01Y= -github.com/getsentry/sentry-go v0.24.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/getsentry/sentry-go v0.24.1 h1:W6/0GyTy8J6ge6lVCc94WB6Gx2ZuLrgopnn9w8Hiwuk= +github.com/getsentry/sentry-go v0.24.1/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-co-op/gocron v1.33.1 h1:wjX+Dg6Ae29a/f9BSQjY1Rl+jflTpW9aDyMqseCj78c= github.com/go-co-op/gocron v1.33.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= @@ -146,8 +144,8 @@ github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1 h1:LSsiG61v9IzzxMkqEr6nrix4miJI62xlRjwT7BYD2SM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1/go.mod h1:Hbb13e3/WtqQ8U5hLGkek9gJvBLasHuPFI0UEGfnQ10= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -278,8 +276,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/panjf2000/ants/v2 v2.8.1 h1:C+n/f++aiW8kHCExKlpX6X+okmxKXP7DWLutxuAPuwQ= github.com/panjf2000/ants/v2 v2.8.1/go.mod h1:KIBmYG9QQX5U2qzFP/yQJaq/nSb6rahS9iEHkrCMgM8= -github.com/panjf2000/gnet/v2 v2.3.1 h1:J7vHkNxwsevVIw3u/6LCXgcnpGBk5iKqhQ2RMblGodc= -github.com/panjf2000/gnet/v2 v2.3.1/go.mod h1:Ik5lTy2nmBg9Uvjfcf2KRYs+EXVNOLyxPHpFOFlqu+M= +github.com/panjf2000/gnet/v2 v2.3.2 h1:cwzq4S2fZbHvBaGriMlZTDtiFL/EzaaVny2V03wOuj0= +github.com/panjf2000/gnet/v2 v2.3.2/go.mod h1:jQ0+i/ZSs4wxUKl06sgjWE0bL/yWI1d5LkNdqulZXFc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= @@ -372,18 +370,18 @@ github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQ go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= -go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 h1:U5GYackKpVKlPrd/5gKMlrTlP2dCESAAFU682VCpieY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0/go.mod h1:aFsJfCEnLzEu9vRRAcUiB/cpRTbVsNdF3OHSPpdjxZQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 h1:iGeIsSYwpYSvh5UGzWrJfTDJvPjrXtxl3GUppj6IXQU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0/go.mod h1:1j3H3G1SBYpZFti6OI4P0uRQCW20MXkG5v4UWXppLLE= -go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= -go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= -go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE= -go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= -go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= -go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= +go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0 h1:yE32ay7mJG2leczfREEhoW3VfSZIvHaB+gvVo1o8DQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0/go.mod h1:G17FHPDLt74bCI7tJ4CMitEk4BXTYG4FW6XUpkPBXa4= +go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= +go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= +go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= +go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= +go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= +go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -396,8 +394,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= @@ -552,12 +550,12 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb h1:Isk1sSH7bovx8Rti2wZK0UZF6oraBDK74uoyLEEVFN0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -566,8 +564,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o= -google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.1 h1:OL+Vz23DTtrrldqHK49FUOPHyY75rvFqJfXC84NYW58= +google.golang.org/grpc v1.58.1/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= 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= diff --git a/logging/hclog_adapter_test.go b/logging/hclog_adapter_test.go index 7ac2c78d..b34235ea 100644 --- a/logging/hclog_adapter_test.go +++ b/logging/hclog_adapter_test.go @@ -3,6 +3,7 @@ package logging import ( "context" "testing" + "time" "github.com/gatewayd-io/gatewayd/config" "github.com/hashicorp/go-hclog" @@ -20,7 +21,7 @@ func TestNewHcLogAdapter(t *testing.T) { Output: []config.LogOutput{config.Console}, Level: zerolog.TraceLevel, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, NoColor: true, }, ) @@ -83,3 +84,58 @@ func TestNewHcLogAdapter_LogLevel_Difference(t *testing.T) { assert.Contains(t, consoleOutput, "ERR This is an error message") assert.NotContains(t, consoleOutput, "DBG This is a log message, but it should not be logged") } + +// TestNewHcLogAdapter_Log tests the HcLogAdapter.Log method. +func TestNewHcLogAdapter_Log(t *testing.T) { + consoleOutput := capturer.CaptureStdout(func() { + logger := NewLogger( + context.Background(), + LoggerConfig{ + Output: []config.LogOutput{config.Console}, + Level: zerolog.TraceLevel, + TimeFormat: zerolog.TimeFormatUnix, + NoColor: true, + }, + ) + + hcLogAdapter := NewHcLogAdapter(&logger, "test") + hcLogAdapter.SetLevel(hclog.Trace) + + hcLogAdapter.Log(hclog.Off, "This is a message") + hcLogAdapter.Log(hclog.NoLevel, "This is yet another message") + hcLogAdapter.Log(hclog.Trace, "This is a trace message") + hcLogAdapter.Log(hclog.Debug, "This is a debug message") + hcLogAdapter.Log(hclog.Info, "This is an info message") + hcLogAdapter.Log(hclog.Warn, "This is a warn message") + hcLogAdapter.Log(hclog.Error, "This is an error message") + }) + + assert.NotContains(t, consoleOutput, "This is a message") + assert.NotContains(t, consoleOutput, "This is yet another message") + assert.Contains(t, consoleOutput, "TRC This is a trace message") + assert.Contains(t, consoleOutput, "DBG This is a debug message") + assert.Contains(t, consoleOutput, "INF This is an info message") + assert.Contains(t, consoleOutput, "WRN This is a warn message") + assert.Contains(t, consoleOutput, "ERR This is an error message") +} + +func TestNewHcLogAdapter_GetLevel(t *testing.T) { + logger := NewLogger( + context.Background(), + LoggerConfig{ + Output: []config.LogOutput{config.Console}, + Level: zerolog.TraceLevel, + TimeFormat: zerolog.TimeFormatUnix, + NoColor: true, + }, + ) + + hcLogAdapter := NewHcLogAdapter(&logger, "test") + hcLogAdapter.SetLevel(hclog.Trace) + assert.Equal(t, hclog.Trace, hcLogAdapter.GetLevel()) + + hcLogAdapter.SetLevel(hclog.Debug) + assert.Equal(t, hclog.Debug, hcLogAdapter.GetLevel()) + assert.NotEqual(t, zerolog.DebugLevel, logger.GetLevel(), + "The logger should not be affected by the hclog adapter's level") +} diff --git a/logging/logger_test.go b/logging/logger_test.go index ea9444ca..1e6ab381 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "testing" + "time" "github.com/gatewayd-io/gatewayd/config" "github.com/rs/zerolog" @@ -40,7 +41,7 @@ func TestNewLogger_File(t *testing.T) { LoggerConfig{ Output: []config.LogOutput{config.File}, FileName: "gatewayd.log", - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, MaxSize: config.DefaultMaxSize, MaxBackups: config.DefaultMaxBackups, MaxAge: config.DefaultMaxAge, diff --git a/metrics/merger_test.go b/metrics/merger_test.go index 8edb17d8..a792ecdf 100644 --- a/metrics/merger_test.go +++ b/metrics/merger_test.go @@ -22,7 +22,7 @@ func TestMerger(t *testing.T) { logging.LoggerConfig{ Output: []config.LogOutput{config.Console}, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, Level: zerolog.InfoLevel, NoColor: true, }, diff --git a/network/client_test.go b/network/client_test.go index 2ae1d18a..e82917ba 100644 --- a/network/client_test.go +++ b/network/client_test.go @@ -3,6 +3,7 @@ package network import ( "context" "testing" + "time" "github.com/gatewayd-io/gatewayd/config" "github.com/gatewayd-io/gatewayd/logging" @@ -17,7 +18,7 @@ func CreateNewClient(t *testing.T) *Client { logger := logging.NewLogger(context.Background(), logging.LoggerConfig{ Output: []config.LogOutput{config.Console}, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, Level: zerolog.DebugLevel, NoColor: true, }) diff --git a/network/network_helpers_test.go b/network/network_helpers_test.go index 6cc09526..766c0315 100644 --- a/network/network_helpers_test.go +++ b/network/network_helpers_test.go @@ -112,7 +112,7 @@ func CollectAndComparePrometheusMetrics(t *testing.T) { gatewayd_bytes_sent_to_server_sum 282 gatewayd_bytes_sent_to_server_count 5 gatewayd_client_connections 1 - gatewayd_plugin_hooks_executed_total 10 + gatewayd_plugin_hooks_executed_total 11 gatewayd_plugin_hooks_registered_total 0 gatewayd_plugins_loaded_total 0 gatewayd_proxied_connections 1 @@ -122,6 +122,7 @@ func CollectAndComparePrometheusMetrics(t *testing.T) { gatewayd_server_connections 5 gatewayd_traffic_bytes_sum 182 gatewayd_traffic_bytes_count 4 + gatewayd_server_ticks_fired_total 1 ` metrics = []string{ @@ -139,6 +140,7 @@ func CollectAndComparePrometheusMetrics(t *testing.T) { "gatewayd_proxy_passthroughs_total", "gatewayd_server_connections", "gatewayd_traffic_bytes", + "gatewayd_server_ticks_fired_total", } ) assert.NoError(t, diff --git a/network/proxy_test.go b/network/proxy_test.go index e1603e83..b7a50c29 100644 --- a/network/proxy_test.go +++ b/network/proxy_test.go @@ -3,6 +3,7 @@ package network import ( "context" "testing" + "time" "github.com/gatewayd-io/gatewayd/config" "github.com/gatewayd-io/gatewayd/logging" @@ -17,7 +18,7 @@ func TestNewProxy(t *testing.T) { logger := logging.NewLogger(context.Background(), logging.LoggerConfig{ Output: []config.LogOutput{config.Console}, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, Level: zerolog.DebugLevel, NoColor: true, }) @@ -80,7 +81,7 @@ func TestNewProxyElastic(t *testing.T) { logger := logging.NewLogger(context.Background(), logging.LoggerConfig{ Output: []config.LogOutput{config.Console}, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, Level: zerolog.DebugLevel, NoColor: true, }) diff --git a/network/server_test.go b/network/server_test.go index 3c2f62b4..c0c28055 100644 --- a/network/server_test.go +++ b/network/server_test.go @@ -1,9 +1,13 @@ package network import ( + "bufio" "context" "errors" + "io" + "os" "testing" + "time" v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1" "github.com/gatewayd-io/gatewayd/config" @@ -21,11 +25,15 @@ func TestRunServer(t *testing.T) { errs := make(chan error) logger := logging.NewLogger(context.Background(), logging.LoggerConfig{ - Output: []config.LogOutput{config.Console}, + Output: []config.LogOutput{ + config.Console, + config.File, + }, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, - Level: zerolog.ErrorLevel, + ConsoleTimeFormat: time.RFC3339, + Level: zerolog.DebugLevel, NoColor: true, + FileName: "server_test.log", }) pluginRegistry := plugin.NewRegistry( @@ -54,7 +62,8 @@ func TestRunServer(t *testing.T) { } else { errs <- errors.New("request is not a []byte") //nolint:goerr113 } - assert.Empty(t, paramsMap["error"]) + assert.Empty(t, paramsMap["error"], "The error MUST be empty.") + return params, nil } pluginRegistry.AddHook(v1.HookName_HOOK_NAME_ON_TRAFFIC_FROM_CLIENT, 1, onTrafficFromClient) @@ -170,6 +179,7 @@ func TestRunServer(t *testing.T) { gnet.WithMulticore(false), gnet.WithReuseAddr(true), gnet.WithReusePort(true), + gnet.WithTicker(true), // Enable ticker. }, proxy, logger, @@ -183,12 +193,36 @@ func TestRunServer(t *testing.T) { errs <- err } close(errs) + + // Read the log file and check if the log file contains the expected log messages. + if _, err := os.Stat("server_test.log"); err == nil { + logFile, err := os.Open("server_test.log") + assert.NoError(t, err) + defer logFile.Close() + + reader := bufio.NewReader(logFile) + assert.NotNil(t, reader) + + buffer, err := io.ReadAll(reader) + assert.NoError(t, err) + assert.Greater(t, len(buffer), 0) // The log file should not be empty. + + logLines := string(buffer) + assert.Contains(t, logLines, "GatewayD is running", "GatewayD should be running") + assert.Contains(t, logLines, "GatewayD is ticking...", "GatewayD should be ticking") + assert.Contains(t, logLines, "Ingress traffic", "Ingress traffic should be logged") + assert.Contains(t, logLines, "Egress traffic", "Egress traffic should be logged") + assert.Contains(t, logLines, "GatewayD is shutting down...", "GatewayD should be shutting down") + } }(server, errs) //nolint:thelper go func(t *testing.T, server *Server, proxy *Proxy) { for { if server.IsRunning() { + // Pause for a while to allow the server to start. + time.Sleep(500 * time.Millisecond) + client := NewClient( context.Background(), &config.Client{ @@ -231,8 +265,10 @@ func TestRunServer(t *testing.T) { CollectAndComparePrometheusMetrics(t) // Clean up. - server.Shutdown() client.Close() + // Pause for a while to allow the server to disconnect and shutdown. + time.Sleep(500 * time.Millisecond) + server.Shutdown() break } } diff --git a/network/utils_test.go b/network/utils_test.go index 4fb8b374..6fe5e595 100644 --- a/network/utils_test.go +++ b/network/utils_test.go @@ -3,6 +3,7 @@ package network import ( "context" "testing" + "time" "github.com/gatewayd-io/gatewayd/config" "github.com/gatewayd-io/gatewayd/logging" @@ -15,7 +16,7 @@ func TestGetID(t *testing.T) { cfg := logging.LoggerConfig{ Output: []config.LogOutput{config.Console}, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, Level: zerolog.DebugLevel, NoColor: true, } @@ -30,7 +31,7 @@ func TestResolve(t *testing.T) { cfg := logging.LoggerConfig{ Output: []config.LogOutput{config.Console}, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, Level: zerolog.DebugLevel, NoColor: true, } diff --git a/plugin/plugin_registry_test.go b/plugin/plugin_registry_test.go index 19c78ec5..ab73342b 100644 --- a/plugin/plugin_registry_test.go +++ b/plugin/plugin_registry_test.go @@ -3,6 +3,7 @@ package plugin import ( "context" "testing" + "time" sdkPlugin "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin" v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1" @@ -19,7 +20,7 @@ func NewPluginRegistry(t *testing.T) *Registry { cfg := logging.LoggerConfig{ Output: []config.LogOutput{config.Console}, TimeFormat: zerolog.TimeFormatUnix, - ConsoleTimeFormat: config.DefaultConsoleTimeFormat, + ConsoleTimeFormat: time.RFC3339, Level: zerolog.DebugLevel, NoColor: true, } diff --git a/plugin/utils.go b/plugin/utils.go index e7771f4d..52e9e667 100644 --- a/plugin/utils.go +++ b/plugin/utils.go @@ -34,10 +34,24 @@ func CastToPrimitiveTypes(args map[string]interface{}) map[string]interface{} { for key, value := range args { switch value := value.(type) { case time.Duration: + // Cast time.Duration to string. args[key] = value.String() case map[string]interface{}: // Recursively cast nested maps. args[key] = CastToPrimitiveTypes(value) + case []interface{}: + // Recursively cast nested arrays. + array := make([]interface{}, len(value)) + for idx, v := range value { + result := v + if v, ok := v.(time.Duration); ok { + // Cast time.Duration to string. + array[idx] = v.String() + } else { + array[idx] = result + } + } + args[key] = array // TODO: Add more types here as needed. default: args[key] = value diff --git a/plugin/utils_test.go b/plugin/utils_test.go index 0e3e695a..5c0d12ea 100644 --- a/plugin/utils_test.go +++ b/plugin/utils_test.go @@ -2,6 +2,7 @@ package plugin import ( "testing" + "time" v1 "github.com/gatewayd-io/gatewayd-plugin-sdk/plugin/v1" "github.com/stretchr/testify/assert" @@ -73,3 +74,51 @@ func Test_Verify_fail(t *testing.T) { func Test_Verify_nil(t *testing.T) { assert.True(t, Verify(nil, nil)) } + +func Test_NewCommand(t *testing.T) { + cmd := NewCommand("/test", []string{"--test"}, []string{"test=123"}) + assert.NotNil(t, cmd) + assert.Equal(t, "/test", cmd.Path) + // Command.Args[0] is always set to the command name itself. + assert.Equal(t, []string{"/test", "--test"}, cmd.Args) + assert.Equal(t, []string{"test=123"}, cmd.Env) +} + +// Test_CastToPrimitiveTypes tests the CastToPrimitiveTypes function. +func Test_CastToPrimitiveTypes(t *testing.T) { + actual := map[string]interface{}{ + "string": "test", + "int": 123, + "bool": true, + "map": map[string]interface{}{"test": "test"}, + "duration": time.Duration(123), + "array": []interface{}{ + "test", + 123, + true, + map[string]interface{}{ + "test": "test", + }, + time.Duration(123), + }, + } + expected := map[string]interface{}{ + "string": "test", + "int": 123, + "bool": true, + "map": map[string]interface{}{"test": "test"}, + "duration": "123ns", // time.Duration is casted to string. + "array": []interface{}{ + "test", + 123, + true, + map[string]interface{}{ + "test": "test", + }, + "123ns", // time.Duration is casted to string. + }, + } + + casted := CastToPrimitiveTypes(actual) + assert.Equal(t, expected, casted) +} diff --git a/pool/pool_test.go b/pool/pool_test.go index b36efc4d..e406d11e 100644 --- a/pool/pool_test.go +++ b/pool/pool_test.go @@ -206,3 +206,21 @@ func TestPool_GetClientIDs(t *testing.T) { assert.Contains(t, ids, "client2.ID") pool.Clear() } + +func TestPool_Cap(t *testing.T) { + pool := NewPool(context.Background(), 1) + assert.NotNil(t, pool) + assert.NotNil(t, pool.Pool()) + assert.Equal(t, 0, pool.Size()) + assert.Equal(t, 1, pool.Cap()) + err := pool.Put("client1.ID", "client1") + assert.Nil(t, err) + assert.Equal(t, 1, pool.Size()) + err = pool.Put("client2.ID", "client2") + assert.NotNil(t, err) + assert.Equal(t, 1, pool.Size()) + assert.Equal(t, 1, pool.Cap()) + pool.Clear() + assert.Equal(t, 0, pool.Size()) + assert.Equal(t, 1, pool.Cap()) +}