diff --git a/README.md b/README.md index 6ed1cf3..306d021 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This project helps you to remove containers/networks/volumes/images by given fil $ RYUK_PORT=8080 ./bin/moby-ryuk $ # You can also run it with Docker - $ docker run -v /var/run/docker.sock:/var/run/docker.sock -e RYUK_PORT=8080 -p 8080:8080 testcontainers/ryuk:0.6.0 + $ docker run -v /var/run/docker.sock:/var/run/docker.sock -e RYUK_PORT=8080 -p 8080:8080 testcontainers/ryuk:0.9.0 1. Connect via TCP: diff --git a/config.go b/config.go new file mode 100644 index 0000000..2ae1556 --- /dev/null +++ b/config.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "log/slog" + "time" + + "github.com/caarlos0/env/v11" +) + +// config represents the configuration for the reaper. +type config struct { + // ConnectionTimeout is the duration without receiving any connections which will trigger a shutdown. + ConnectionTimeout time.Duration `env:"RYUK_CONNECTION_TIMEOUT" envDefault:"60s"` + + // ReconnectionTimeout is the duration after the last connection closes which will trigger + // resource clean up and shutdown. + ReconnectionTimeout time.Duration `env:"RYUK_RECONNECTION_TIMEOUT" envDefault:"10s"` + + // ShutdownTimeout is the maximum amount of time the reaper will wait + // for once signalled to shutdown before it terminates even if connections + // are still established. + ShutdownTimeout time.Duration `env:"RYUK_SHUTDOWN_TIMEOUT" envDefault:"10m"` + + // Port is the port to listen on for connections. + Port uint16 `env:"RYUK_PORT" envDefault:"8080"` + + // Verbose is whether to enable verbose aka debug logging. + Verbose bool `env:"RYUK_VERBOSE" envDefault:"false"` +} + +// LogAttrs returns the configuration as a slice of attributes. +func (c config) LogAttrs() []slog.Attr { + return []slog.Attr{ + slog.Duration("connection_timeout", c.ConnectionTimeout), + slog.Duration("reconnection_timeout", c.ReconnectionTimeout), + slog.Duration("shutdown_timeout", c.ShutdownTimeout), + slog.Int("port", int(c.Port)), + slog.Bool("verbose", c.Verbose), + } +} + +// loadConfig loads the configuration from the environment +// applying defaults where necessary. +func loadConfig() (*config, error) { + var cfg config + if err := env.Parse(&cfg); err != nil { + return nil, fmt.Errorf("parse env: %w", err) + } + + return &cfg, nil +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..e0ef6b4 --- /dev/null +++ b/config_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "os" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// clearConfigEnv clears the environment variables for the config fields. +func clearConfigEnv(t *testing.T) { + t.Helper() + + var cfg config + typ := reflect.TypeOf(cfg) + for i := range typ.NumField() { + field := typ.Field(i) + if name := field.Tag.Get("env"); name != "" { + if os.Getenv(name) != "" { + t.Setenv(name, "") + } + } + } +} + +func Test_loadConfig(t *testing.T) { + clearConfigEnv(t) + + t.Run("defaults", func(t *testing.T) { + expected := config{ + Port: 8080, + ConnectionTimeout: time.Minute, + ReconnectionTimeout: time.Second * 10, + ShutdownTimeout: time.Minute * 10, + } + + cfg, err := loadConfig() + require.NoError(t, err) + require.Equal(t, expected, *cfg) + }) + + t.Run("custom", func(t *testing.T) { + t.Setenv("RYUK_PORT", "1234") + t.Setenv("RYUK_CONNECTION_TIMEOUT", "2s") + t.Setenv("RYUK_RECONNECTION_TIMEOUT", "3s") + t.Setenv("RYUK_SHUTDOWN_TIMEOUT", "7s") + t.Setenv("RYUK_VERBOSE", "true") + + expected := config{ + Port: 1234, + ConnectionTimeout: time.Second * 2, + ReconnectionTimeout: time.Second * 3, + ShutdownTimeout: time.Second * 7, + Verbose: true, + } + + cfg, err := loadConfig() + require.NoError(t, err) + require.Equal(t, expected, *cfg) + }) + + for _, name := range []string{ + "RYUK_PORT", + "RYUK_CONNECTION_TIMEOUT", + "RYUK_RECONNECTION_TIMEOUT", + "RYUK_SHUTDOWN_TIMEOUT", + "RYUK_VERBOSE", + } { + t.Run("invalid-"+name, func(t *testing.T) { + t.Setenv(name, "invalid") + _, err := loadConfig() + require.Error(t, err) + }) + } +} diff --git a/go.mod b/go.mod index 37cb344..4fa4551 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/testcontainers/moby-ryuk go 1.23 require ( + github.com/caarlos0/env/v11 v11.2.2 github.com/docker/docker v27.2.0+incompatible github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.33.0 diff --git a/go.sum b/go.sum index 70ba5cd..5cd15f0 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg= +github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= diff --git a/main.go b/main.go index 673e2d5..bcb433f 100644 --- a/main.go +++ b/main.go @@ -4,15 +4,12 @@ import ( "bufio" "context" "errors" - "flag" "fmt" "io" "log" "net" "net/url" - "os" "os/signal" - "strconv" "strings" "sync" "syscall" @@ -25,89 +22,18 @@ import ( ) const ( - connectionTimeoutEnv string = "RYUK_CONNECTION_TIMEOUT" - portEnv string = "RYUK_PORT" - reconnectionTimeoutEnv string = "RYUK_RECONNECTION_TIMEOUT" - ryukLabel string = "org.testcontainers.ryuk" - verboseEnv string = "RYUK_VERBOSE" + ryukLabel string = "org.testcontainers.ryuk" ) var ( - port int + port uint16 connectionTimeout time.Duration reconnectionTimeout time.Duration verbose bool ) -type config struct { - Port int - ConnectionTimeout time.Duration - ReconnectionTimeout time.Duration - Verbose bool -} - -// newConfig parses command line flags and returns a parsed config. config.timeout -// can be set by environment variable, RYUK_CONNECTION_TIMEOUT. If an error occurs -// while parsing RYUK_CONNECTION_TIMEOUT the error is returned. -func newConfig(args []string) (*config, error) { - cfg := config{ - Port: 8080, - ConnectionTimeout: 60 * time.Second, - ReconnectionTimeout: 10 * time.Second, - Verbose: false, - } - - fs := flag.NewFlagSet("ryuk", flag.ExitOnError) - fs.SetOutput(os.Stdout) - - fs.IntVar(&cfg.Port, "p", 8080, "Deprecated: please use the "+portEnv+" environment variable to set the port to bind at") - - err := fs.Parse(args) - if err != nil { - return nil, err - } - - if timeout, ok := os.LookupEnv(connectionTimeoutEnv); ok { - parsedTimeout, err := time.ParseDuration(timeout) - if err != nil { - return nil, fmt.Errorf("failed to parse \"%s\": %s", connectionTimeoutEnv, err) - } - - cfg.ConnectionTimeout = parsedTimeout - } - - if port, ok := os.LookupEnv(portEnv); ok { - parsedPort, err := strconv.Atoi(port) - if err != nil { - return nil, fmt.Errorf("failed to parse \"%s\": %s", portEnv, err) - } - - cfg.Port = parsedPort - } - - if timeout, ok := os.LookupEnv(reconnectionTimeoutEnv); ok { - parsedTimeout, err := time.ParseDuration(timeout) - if err != nil { - return nil, fmt.Errorf("failed to parse \"%s\": %s", reconnectionTimeoutEnv, err) - } - - cfg.ReconnectionTimeout = parsedTimeout - } - - if verbose, ok := os.LookupEnv(verboseEnv); ok { - v, err := strconv.ParseBool(verbose) - if err != nil { - return nil, fmt.Errorf("failed to parse \"%s\": %s", verboseEnv, err) - } - - cfg.Verbose = v - } - - return &cfg, nil -} - func main() { - cfg, err := newConfig(os.Args[1:]) + cfg, err := loadConfig() if err != nil { panic(err) } diff --git a/main_test.go b/main_test.go index f5a7a9e..8de2ca2 100644 --- a/main_test.go +++ b/main_test.go @@ -312,89 +312,3 @@ func TestPrune(t *testing.T) { assert.Equal(t, maxLength, di) }) } - -func Test_newConfig(t *testing.T) { - t.Run("should return an error when failing to parse RYUK_CONNECTION_TIMEOUT environment variable", func(t *testing.T) { - t.Setenv(connectionTimeoutEnv, "bad_value") - - config, err := newConfig([]string{}) - require.NotNil(t, err) - require.Nil(t, config) - }) - - t.Run("should set connectionTimeout with RYUK_CONNECTION_TIMEOUT environment variable", func(t *testing.T) { - t.Setenv(connectionTimeoutEnv, "10s") - - config, err := newConfig([]string{}) - require.Nil(t, err) - assert.Equal(t, 10*time.Second, config.ConnectionTimeout) - }) - - t.Run("should return an error when failing to parse RYUK_PORT environment variable", func(t *testing.T) { - t.Setenv(portEnv, "bad_value") - - config, err := newConfig([]string{}) - require.NotNil(t, err) - require.Nil(t, config) - }) - - t.Run("should set connectionTimeout with RYUK_PORT environment variable", func(t *testing.T) { - t.Setenv(portEnv, "8081") - - config, err := newConfig([]string{}) - require.Nil(t, err) - assert.Equal(t, 8081, config.Port) - }) - - t.Run("should return an error when failing to parse RYUK_RECONNECTION_TIMEOUT environment variable", func(t *testing.T) { - t.Setenv(reconnectionTimeoutEnv, "bad_value") - - config, err := newConfig([]string{}) - require.NotNil(t, err) - require.Nil(t, config) - }) - - t.Run("should set connectionTimeout with RYUK_RECONNECTION_TIMEOUT environment variable", func(t *testing.T) { - t.Setenv(reconnectionTimeoutEnv, "100s") - - config, err := newConfig([]string{}) - require.Nil(t, err) - assert.Equal(t, 100*time.Second, config.ReconnectionTimeout) - }) - - t.Run("should return an error when failing to parse RYUK_VERBOSE environment variable", func(t *testing.T) { - t.Setenv(verboseEnv, "bad_value") - - config, err := newConfig([]string{}) - require.NotNil(t, err) - require.Nil(t, config) - }) - - t.Run("should set verbose with RYUK_VERBOSE environment variable", func(t *testing.T) { - t.Setenv(verboseEnv, "true") - - config, err := newConfig([]string{}) - require.Nil(t, err) - assert.True(t, config.Verbose) - - t.Setenv(verboseEnv, "false") - - config, err = newConfig([]string{}) - require.Nil(t, err) - assert.False(t, config.Verbose) - }) - - t.Run("should set port with port flag", func(t *testing.T) { - config, err := newConfig([]string{"-p", "3000"}) - require.Nil(t, err) - assert.Equal(t, 3000, config.Port) - }) - - t.Run("should set port from env with port flag and RYUK_PORT environment variable", func(t *testing.T) { - t.Setenv(portEnv, "8081") - - config, err := newConfig([]string{"-p", "3000"}) - require.Nil(t, err) - assert.Equal(t, 8081, config.Port) - }) -}