From 8c97b9efbb2e35eda1980d469c526dd8a3209450 Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Fri, 22 Mar 2024 16:47:01 -0500 Subject: [PATCH] Fixed regression, gubernator should start without a config file --- Dockerfile | 3 + cmd/gubernator/main.go | 68 ++++++++++++--------- cmd/gubernator/main_test.go | 117 ++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 cmd/gubernator/main_test.go diff --git a/Dockerfile b/Dockerfile index 42f8951..e69e4e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ ARG TARGETPLATFORM ENV BUILDPLATFORM=${BUILDPLATFORM:-linux/amd64} ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} +LABEL org.opencontainers.image.source = "https://github.com/gubernator-io/gubernator" + WORKDIR /go/src # This should create cached layer of our dependencies for subsequent builds to use @@ -38,6 +40,7 @@ COPY --from=build /healthcheck /healthcheck # Healtcheck HEALTHCHECK --interval=3s --timeout=1s --start-period=2s --retries=2 CMD [ "/healthcheck" ] + # Run the server ENTRYPOINT ["/gubernator"] diff --git a/cmd/gubernator/main.go b/cmd/gubernator/main.go index a1aec81..8b54023 100644 --- a/cmd/gubernator/main.go +++ b/cmd/gubernator/main.go @@ -19,14 +19,15 @@ package main import ( "context" "flag" + "fmt" "io" "os" "os/signal" "runtime" + "strings" "syscall" "github.com/gubernator-io/gubernator/v2" - "github.com/mailgun/holster/v4/clock" "github.com/mailgun/holster/v4/tracing" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/sdk/resource" @@ -39,13 +40,26 @@ var Version = "dev-build" var tracerCloser io.Closer func main() { + err := Main(context.Background()) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } +} + +func Main(ctx context.Context) error { var configFile string logrus.Infof("Gubernator %s (%s/%s)", Version, runtime.GOARCH, runtime.GOOS) flags := flag.NewFlagSet("gubernator", flag.ContinueOnError) + flags.SetOutput(io.Discard) flags.StringVar(&configFile, "config", "", "environment config file") flags.BoolVar(&gubernator.DebugEnabled, "debug", false, "enable debug") - checkErr(flags.Parse(os.Args[1:]), "while parsing flags") + if err := flags.Parse(os.Args[1:]); err != nil { + if !strings.Contains(err.Error(), "flag provided but not defined") { + return fmt.Errorf("while parsing flags: %w", err) + } + } // in order to prevent logging to /tmp by k8s.io/client-go // and other kubernetes related dependencies which are using @@ -61,9 +75,13 @@ func main() { if err != nil { log.WithError(err).Fatal("during tracing.NewResource()") } + defer func() { + if tracerCloser != nil { + _ = tracerCloser.Close() + } + }() // Initialize tracing. - ctx := context.Background() err = tracing.InitTracing(ctx, "github.com/gubernator-io/gubernator/v2", tracing.WithLevel(gubernator.GetTracingLevel()), @@ -73,42 +91,36 @@ func main() { log.WithError(err).Fatal("during tracing.InitTracing()") } + var configFileReader io.Reader // Read our config from the environment or optional environment config file - configFileReader, err := os.Open(configFile) - if err != nil { - log.WithError(err).Fatal("while opening config file") + if configFile != "" { + configFileReader, err = os.Open(configFile) + if err != nil { + log.WithError(err).Fatal("while opening config file") + } } - conf, err := gubernator.SetupDaemonConfig(logrus.StandardLogger(), configFileReader) - checkErr(err, "while getting config") - ctx, cancel := context.WithTimeout(ctx, clock.Second*10) + conf, err := gubernator.SetupDaemonConfig(logrus.StandardLogger(), configFileReader) + if err != nil { + return fmt.Errorf("while collecting daemon config: %w", err) + } // Start the daemon daemon, err := gubernator.SpawnDaemon(ctx, conf) - checkErr(err, "while spawning daemon") - cancel() + if err != nil { + return fmt.Errorf("while spawning daemon: %w", err) + } // Wait here for signals to clean up our mess c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - for range c { + signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + select { + case <-c: log.Info("caught signal; shutting down") daemon.Close() _ = tracing.CloseTracing(context.Background()) - exit(0) - } -} - -func checkErr(err error, msg string) { - if err != nil { - log.WithError(err).Error(msg) - exit(1) - } -} - -func exit(code int) { - if tracerCloser != nil { - tracerCloser.Close() + return nil + case <-ctx.Done(): + return ctx.Err() } - os.Exit(code) } diff --git a/cmd/gubernator/main_test.go b/cmd/gubernator/main_test.go new file mode 100644 index 0000000..8c4e10e --- /dev/null +++ b/cmd/gubernator/main_test.go @@ -0,0 +1,117 @@ +package main_test + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "flag" + "fmt" + "net" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + cli "github.com/gubernator-io/gubernator/v2/cmd/gubernator" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/proxy" +) + +var cliRunning = flag.Bool("test_cli_running", false, "True if running as a child process; used by TestCLI") + +func TestCLI(t *testing.T) { + if *cliRunning { + if err := cli.Main(context.Background()); err != nil { + //if !strings.Contains(err.Error(), "context deadline exceeded") { + // log.Print(err.Error()) + // os.Exit(1) + //} + fmt.Print(err.Error()) + os.Exit(1) + } + os.Exit(0) + } + + tests := []struct { + args []string + env []string + name string + contains string + }{ + { + name: "Should start with no config provided", + env: []string{ + "GUBER_GRPC_ADDRESS=localhost:8080", + "GUBER_HTTP_ADDRESS=localhost:8081", + }, + args: []string{}, + contains: "HTTP Gateway Listening on", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := exec.Command(os.Args[0], append([]string{"--test.run=TestCLI", "--test_cli_running"}, tt.args...)...) + var out bytes.Buffer + c.Stdout = &out + c.Stderr = &out + c.Env = tt.env + + if err := c.Start(); err != nil { + t.Fatal("failed to start child process: ", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + waitCh := make(chan struct{}) + go func() { + _ = c.Wait() + close(waitCh) + }() + + err := waitForConnect(ctx, "localhost:8080", nil) + assert.NoError(t, err) + time.Sleep(time.Second * 1) + + err = c.Process.Signal(syscall.SIGTERM) + require.NoError(t, err) + + <-waitCh + assert.Contains(t, out.String(), tt.contains) + }) + } +} + +// waitForConnect waits until the passed address is accepting connections. +// It will continue to attempt a connection until context is canceled. +func waitForConnect(ctx context.Context, address string, cfg *tls.Config) error { + if address == "" { + return fmt.Errorf("waitForConnect() requires a valid address") + } + + var errs []string + for { + var d proxy.ContextDialer + if cfg != nil { + d = &tls.Dialer{Config: cfg} + } else { + d = &net.Dialer{} + } + conn, err := d.DialContext(ctx, "tcp", address) + if err == nil { + _ = conn.Close() + return nil + } + errs = append(errs, err.Error()) + if ctx.Err() != nil { + errs = append(errs, ctx.Err().Error()) + return errors.New(strings.Join(errs, "\n")) + } + time.Sleep(time.Millisecond * 100) + continue + } +}