From fca9c7928970ad5f785f3e9b210d6c89e5826753 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Mon, 10 Mar 2025 14:11:40 -0400 Subject: [PATCH] refactor: reorganize code structure (#556) * refactor: rename providers to Provider * refactor folders * fix build cmd * fix build cmd * fix build cmd * fix cmd start --- .github/workflows/build.yml | 2 +- Makefile | 8 +- app/http/healthcheck/healthcheck.go | 31 --- app/http/routes/models/blocking_request.go | 10 - app/http/routes/models/dynamic_request.go | 15 -- app/http/routes/strategies.go | 15 -- app/http/routes/version.go | 13 -- app/http/routes/version_test.go | 32 --- app/sablier.go | 184 ----------------- cmd/health.go | 27 --- cmd/healthcheck/healthcheck.go | 52 +++++ cmd/root_test.go | 194 ------------------ cmd/{root.go => sablier/cmd.go} | 41 +++- {app => cmd/sablier}/logger.go | 4 +- cmd/sablier/provider.go | 50 +++++ cmd/sablier/sablier.go | 92 +++++++++ cmd/sablier/theme.go | 28 +++ cmd/start.go | 22 -- cmd/{ => version}/version.go | 6 +- internal/api/api.go | 15 ++ internal/api/api_test.go | 15 +- internal/api/start_blocking.go | 18 +- internal/api/start_dynamic.go | 33 +-- internal/api/theme_list.go | 3 +- internal/server/routes.go | 5 +- internal/server/server.go | 8 +- main.go | 11 - {config => pkg/config}/configuration.go | 0 {config => pkg/config}/logging.go | 0 {config => pkg/config}/provider.go | 8 +- {config => pkg/config}/server.go | 0 {config => pkg/config}/sessions.go | 0 {config => pkg/config}/storage.go | 0 {config => pkg/config}/strategy.go | 0 pkg/provider/docker/container_inspect.go | 2 +- pkg/provider/docker/container_inspect_test.go | 2 +- pkg/provider/docker/container_list.go | 4 +- pkg/provider/docker/container_list_test.go | 4 +- pkg/provider/docker/container_start.go | 2 +- pkg/provider/docker/container_start_test.go | 2 +- pkg/provider/docker/container_stop.go | 2 +- pkg/provider/docker/container_stop_test.go | 2 +- pkg/provider/docker/docker.go | 8 +- pkg/provider/docker/events.go | 2 +- pkg/provider/docker/events_test.go | 2 +- pkg/provider/dockerswarm/docker_swarm.go | 12 +- pkg/provider/dockerswarm/events.go | 2 +- pkg/provider/dockerswarm/events_test.go | 2 +- pkg/provider/dockerswarm/service_inspect.go | 4 +- .../dockerswarm/service_inspect_test.go | 4 +- pkg/provider/dockerswarm/service_list.go | 6 +- pkg/provider/dockerswarm/service_list_test.go | 4 +- pkg/provider/dockerswarm/service_start.go | 2 +- .../dockerswarm/service_start_test.go | 4 +- pkg/provider/dockerswarm/service_stop.go | 2 +- pkg/provider/dockerswarm/service_stop_test.go | 4 +- pkg/provider/kubernetes/deployment_events.go | 2 +- pkg/provider/kubernetes/deployment_inspect.go | 2 +- .../kubernetes/deployment_inspect_test.go | 6 +- pkg/provider/kubernetes/deployment_list.go | 6 +- pkg/provider/kubernetes/instance_events.go | 2 +- .../kubernetes/instance_events_test.go | 4 +- pkg/provider/kubernetes/instance_inspect.go | 2 +- .../kubernetes/instance_inspect_test.go | 6 +- pkg/provider/kubernetes/instance_list.go | 4 +- pkg/provider/kubernetes/instance_list_test.go | 6 +- pkg/provider/kubernetes/instance_start.go | 2 +- .../kubernetes/instance_start_test.go | 4 +- pkg/provider/kubernetes/instance_stop.go | 2 +- pkg/provider/kubernetes/instance_stop_test.go | 4 +- pkg/provider/kubernetes/kubernetes.go | 16 +- pkg/provider/kubernetes/statefulset_events.go | 2 +- .../kubernetes/statefulset_inspect.go | 2 +- .../kubernetes/statefulset_inspect_test.go | 4 +- pkg/provider/kubernetes/statefulset_list.go | 6 +- pkg/provider/kubernetes/workload_scale.go | 2 +- pkg/sablier/group_watch.go | 26 +++ pkg/sablier/instance_expired.go | 18 ++ pkg/sablier/sablier.go | 7 + pkg/sablier/sabliertest/mocks_sablier.go | 12 ++ pkg/theme/render.go | 2 +- pkg/theme/render_test.go | 3 +- {version => pkg/version}/info.go | 0 83 files changed, 474 insertions(+), 698 deletions(-) delete mode 100644 app/http/healthcheck/healthcheck.go delete mode 100644 app/http/routes/models/blocking_request.go delete mode 100644 app/http/routes/models/dynamic_request.go delete mode 100644 app/http/routes/strategies.go delete mode 100644 app/http/routes/version.go delete mode 100644 app/http/routes/version_test.go delete mode 100644 app/sablier.go delete mode 100644 cmd/health.go create mode 100644 cmd/healthcheck/healthcheck.go delete mode 100644 cmd/root_test.go rename cmd/{root.go => sablier/cmd.go} (92%) rename {app => cmd/sablier}/logger.go (93%) create mode 100644 cmd/sablier/provider.go create mode 100644 cmd/sablier/sablier.go create mode 100644 cmd/sablier/theme.go delete mode 100644 cmd/start.go rename cmd/{ => version}/version.go (68%) create mode 100644 internal/api/api.go delete mode 100644 main.go rename {config => pkg/config}/configuration.go (100%) rename {config => pkg/config}/logging.go (100%) rename {config => pkg/config}/provider.go (74%) rename {config => pkg/config}/server.go (100%) rename {config => pkg/config}/sessions.go (100%) rename {config => pkg/config}/storage.go (100%) rename {config => pkg/config}/strategy.go (100%) create mode 100644 pkg/sablier/group_watch.go create mode 100644 pkg/sablier/instance_expired.go rename {version => pkg/version}/info.go (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b244dfe..7dc4b4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: cache-dependency-path: go.sum - name: Build - run: go build -v . + run: go build -v ./cmd/sablier - name: Test run: go test -v -json -race -covermode atomic -coverprofile coverage.txt ./... 2>&1 | go tool go-junit-report -parser gojson > junit.xml diff --git a/Makefile b/Makefile index 5b86af8..e40d5f4 100644 --- a/Makefile +++ b/Makefile @@ -11,20 +11,20 @@ GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) BUILDTIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") BUILDUSER := $(shell whoami)@$(shell hostname) -VPREFIX := github.com/sablierapp/sablier/version +VPREFIX := github.com/sablierapp/sablier/pkg/version GO_LDFLAGS := -s -w -X $(VPREFIX).Branch=$(GIT_BRANCH) -X $(VPREFIX).Version=$(VERSION) -X $(VPREFIX).Revision=$(GIT_REVISION) -X $(VPREFIX).BuildUser=$(BUILDUSER) -X $(VPREFIX).BuildDate=$(BUILDTIME) $(PLATFORMS): - CGO_ENABLED=0 GOOS=$(os) GOARCH=$(arch) go build -trimpath -tags=nomsgpack -v -ldflags="${GO_LDFLAGS}" -o 'sablier_$(VERSION)_$(os)-$(arch)' . + CGO_ENABLED=0 GOOS=$(os) GOARCH=$(arch) go build -trimpath -tags=nomsgpack -v -ldflags="${GO_LDFLAGS}" -o 'sablier_$(VERSION)_$(os)-$(arch)' ./cmd/sablier run: - go run main.go start --storage.file=state.json --logging.level=debug + go run ./cmd/sablier start --storage.file=state.json --logging.level=debug gen: go generate -v ./... build: - go build -v . + go build -v ./cmd/sablier test: go test -v ./... diff --git a/app/http/healthcheck/healthcheck.go b/app/http/healthcheck/healthcheck.go deleted file mode 100644 index 8f8747b..0000000 --- a/app/http/healthcheck/healthcheck.go +++ /dev/null @@ -1,31 +0,0 @@ -package healthcheck - -import ( - "io" - "net/http" -) - -const ( - healthy = true - unhealthy = false -) - -func Health(url string) (string, bool) { - resp, err := http.Get(url) - - if err != nil { - return err.Error(), unhealthy - } - - body, err := io.ReadAll(resp.Body) - - if err != nil { - return err.Error(), unhealthy - } - - if resp.StatusCode >= 400 { - return string(body), unhealthy - } - - return string(body), healthy -} diff --git a/app/http/routes/models/blocking_request.go b/app/http/routes/models/blocking_request.go deleted file mode 100644 index ef7e556..0000000 --- a/app/http/routes/models/blocking_request.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -import "time" - -type BlockingRequest struct { - Names []string `form:"names"` - Group string `form:"group"` - SessionDuration time.Duration `form:"session_duration"` - Timeout time.Duration `form:"timeout"` -} diff --git a/app/http/routes/models/dynamic_request.go b/app/http/routes/models/dynamic_request.go deleted file mode 100644 index 0689fd2..0000000 --- a/app/http/routes/models/dynamic_request.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import ( - "time" -) - -type DynamicRequest struct { - Group string `form:"group"` - Names []string `form:"names"` - ShowDetails bool `form:"show_details"` - DisplayName string `form:"display_name"` - Theme string `form:"theme"` - SessionDuration time.Duration `form:"session_duration"` - RefreshFrequency time.Duration `form:"refresh_frequency"` -} diff --git a/app/http/routes/strategies.go b/app/http/routes/strategies.go deleted file mode 100644 index 1018761..0000000 --- a/app/http/routes/strategies.go +++ /dev/null @@ -1,15 +0,0 @@ -package routes - -import ( - "github.com/sablierapp/sablier/config" - "github.com/sablierapp/sablier/pkg/sablier" - "github.com/sablierapp/sablier/pkg/theme" -) - -type ServeStrategy struct { - Theme *theme.Themes - - SessionsManager sablier.Sablier - StrategyConfig config.Strategy - SessionsConfig config.Sessions -} diff --git a/app/http/routes/version.go b/app/http/routes/version.go deleted file mode 100644 index 35daeef..0000000 --- a/app/http/routes/version.go +++ /dev/null @@ -1,13 +0,0 @@ -package routes - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - "github.com/sablierapp/sablier/version" -) - -func GetVersion(c *gin.Context) { - c.JSON(http.StatusOK, version.Map()) -} diff --git a/app/http/routes/version_test.go b/app/http/routes/version_test.go deleted file mode 100644 index c39efa0..0000000 --- a/app/http/routes/version_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package routes - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/sablierapp/sablier/version" - "gotest.tools/v3/assert" -) - -func TestGetVersion(t *testing.T) { - gin.SetMode(gin.TestMode) - version.Branch = "testing" - version.Revision = "8ffebca" - - recorder := httptest.NewRecorder() - c, _ := gin.CreateTestContext(recorder) - expected, _ := json.Marshal(version.Map()) - - GetVersion(c) - res := recorder.Result() - defer res.Body.Close() - data, _ := io.ReadAll(res.Body) - - assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, string(data), string(expected)) - -} diff --git a/app/sablier.go b/app/sablier.go deleted file mode 100644 index df57214..0000000 --- a/app/sablier.go +++ /dev/null @@ -1,184 +0,0 @@ -package app - -import ( - "context" - "fmt" - "github.com/docker/docker/client" - "github.com/sablierapp/sablier/app/http/routes" - "github.com/sablierapp/sablier/pkg/provider/docker" - "github.com/sablierapp/sablier/pkg/provider/dockerswarm" - "github.com/sablierapp/sablier/pkg/provider/kubernetes" - "github.com/sablierapp/sablier/pkg/sablier" - "github.com/sablierapp/sablier/pkg/store/inmemory" - "github.com/sablierapp/sablier/pkg/theme" - k8s "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - - "log/slog" - "os" - "os/signal" - "syscall" - "time" - - "github.com/sablierapp/sablier/config" - "github.com/sablierapp/sablier/internal/server" - "github.com/sablierapp/sablier/version" -) - -func Start(ctx context.Context, conf config.Config) error { - // Create context that listens for the interrupt signal from the OS. - ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) - defer stop() - logger := setupLogger(conf.Logging) - - logger.Info("running Sablier version " + version.Info()) - - provider, err := NewProvider(ctx, logger, conf.Provider) - if err != nil { - return err - } - - store := inmemory.NewInMemory() - err = store.OnExpire(ctx, onSessionExpires(ctx, provider, logger)) - if err != nil { - return err - } - - s := sablier.New(logger, store, provider) - - groups, err := provider.InstanceGroups(ctx) - if err != nil { - logger.WarnContext(ctx, "initial group scan failed", slog.Any("reason", err)) - } else { - s.SetGroups(groups) - } - - updateGroups := make(chan map[string][]string) - go WatchGroups(ctx, provider, 2*time.Second, updateGroups, logger) - go func() { - for groups := range updateGroups { - s.SetGroups(groups) - } - }() - - instanceStopped := make(chan string) - go provider.NotifyInstanceStopped(ctx, instanceStopped) - go func() { - for stopped := range instanceStopped { - err := s.RemoveInstance(ctx, stopped) - if err != nil { - logger.Warn("could not remove instance", slog.Any("error", err)) - } - } - }() - - if conf.Provider.AutoStopOnStartup { - err := s.StopAllUnregisteredInstances(ctx) - if err != nil { - logger.ErrorContext(ctx, "unable to stop unregistered instances", slog.Any("reason", err)) - } - } - - var t *theme.Themes - - if conf.Strategy.Dynamic.CustomThemesPath != "" { - logger.DebugContext(ctx, "loading themes from custom theme path", slog.String("path", conf.Strategy.Dynamic.CustomThemesPath)) - custom := os.DirFS(conf.Strategy.Dynamic.CustomThemesPath) - t, err = theme.NewWithCustomThemes(custom, logger) - if err != nil { - return err - } - } else { - logger.DebugContext(ctx, "loading themes without custom theme path", slog.String("reason", "--strategy.dynamic.custom-themes-path is empty")) - t, err = theme.New(logger) - if err != nil { - return err - } - } - - strategy := &routes.ServeStrategy{ - Theme: t, - SessionsManager: s, - StrategyConfig: conf.Strategy, - SessionsConfig: conf.Sessions, - } - - go server.Start(ctx, logger, conf.Server, strategy) - - // Listen for the interrupt signal. - <-ctx.Done() - - stop() - logger.InfoContext(ctx, "shutting down gracefully, press Ctrl+C again to force") - - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - logger.InfoContext(ctx, "Server exiting") - - return nil -} - -func onSessionExpires(ctx context.Context, provider sablier.Provider, logger *slog.Logger) func(key string) { - return func(_key string) { - go func(key string) { - logger.InfoContext(ctx, "instance expired", slog.String("instance", key)) - err := provider.InstanceStop(ctx, key) - if err != nil { - logger.ErrorContext(ctx, "instance expired could not be stopped from provider", slog.String("instance", key), slog.Any("error", err)) - } - }(_key) - } -} - -func NewProvider(ctx context.Context, logger *slog.Logger, config config.Provider) (sablier.Provider, error) { - if err := config.IsValid(); err != nil { - return nil, err - } - - switch config.Name { - case "swarm", "docker_swarm": - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return nil, fmt.Errorf("cannot create docker swarm client: %v", err) - } - return dockerswarm.NewDockerSwarmProvider(ctx, cli, logger) - case "docker": - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return nil, fmt.Errorf("cannot create docker client: %v", err) - } - return docker.NewDockerClassicProvider(ctx, cli, logger) - case "kubernetes": - kubeclientConfig, err := rest.InClusterConfig() - if err != nil { - return nil, err - } - kubeclientConfig.QPS = config.Kubernetes.QPS - kubeclientConfig.Burst = config.Kubernetes.Burst - - cli, err := k8s.NewForConfig(kubeclientConfig) - if err != nil { - return nil, err - } - return kubernetes.NewKubernetesProvider(ctx, cli, logger, config.Kubernetes) - } - return nil, fmt.Errorf("unimplemented provider %s", config.Name) -} - -func WatchGroups(ctx context.Context, provider sablier.Provider, frequency time.Duration, send chan<- map[string][]string, logger *slog.Logger) { - ticker := time.NewTicker(frequency) - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - groups, err := provider.InstanceGroups(ctx) - if err != nil { - logger.Error("cannot retrieve group from provider", slog.Any("reason", err)) - } else if groups != nil { - send <- groups - } - } - } -} diff --git a/cmd/health.go b/cmd/health.go deleted file mode 100644 index cae4345..0000000 --- a/cmd/health.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/sablierapp/sablier/app/http/healthcheck" - "github.com/spf13/cobra" -) - -var newHealthCommand = func() *cobra.Command { - return &cobra.Command{ - Use: "health", - Short: "Calls the health endpoint of a Sablier instance", - Run: func(cmd *cobra.Command, args []string) { - details, healthy := healthcheck.Health(cmd.Flag("url").Value.String()) - - if healthy { - fmt.Fprintf(os.Stderr, "healthy: %v\n", details) - os.Exit(0) - } else { - fmt.Fprintf(os.Stderr, "unhealthy: %v\n", details) - os.Exit(1) - } - }, - } -} diff --git a/cmd/healthcheck/healthcheck.go b/cmd/healthcheck/healthcheck.go new file mode 100644 index 0000000..23789e6 --- /dev/null +++ b/cmd/healthcheck/healthcheck.go @@ -0,0 +1,52 @@ +package healthcheck + +import ( + "fmt" + "github.com/spf13/cobra" + "io" + "net/http" + "os" +) + +const ( + healthy = true + unhealthy = false +) + +func Health(url string) (string, bool) { + resp, err := http.Get(url) + + if err != nil { + return err.Error(), unhealthy + } + + body, err := io.ReadAll(resp.Body) + + if err != nil { + return err.Error(), unhealthy + } + + if resp.StatusCode >= 400 { + return string(body), unhealthy + } + + return string(body), healthy +} + +func NewCmd() *cobra.Command { + return &cobra.Command{ + Use: "health", + Short: "Calls the health endpoint of a Sablier instance", + Run: func(cmd *cobra.Command, args []string) { + details, healthy := Health(cmd.Flag("url").Value.String()) + + if healthy { + fmt.Fprintf(os.Stderr, "healthy: %v\n", details) + os.Exit(0) + } else { + fmt.Fprintf(os.Stderr, "unhealthy: %v\n", details) + os.Exit(1) + } + }, + } +} diff --git a/cmd/root_test.go b/cmd/root_test.go deleted file mode 100644 index a0e5fee..0000000 --- a/cmd/root_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package cmd - -import ( - "bufio" - "bytes" - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/sablierapp/sablier/config" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/stretchr/testify/require" - "gotest.tools/v3/assert" -) - -func TestDefault(t *testing.T) { - testDir, err := os.Getwd() - require.NoError(t, err, "error getting the current working directory") - - wantConfig, err := os.ReadFile(filepath.Join(testDir, "testdata", "config_default.json")) - require.NoError(t, err, "error reading test config file") - - // CHANGE `startCmd` behavior to only print the config, this is for testing purposes only - newStartCommand = mockStartCommand - - t.Run("config file", func(t *testing.T) { - conf = config.NewConfig() - cmd := NewRootCommand() - output := &bytes.Buffer{} - cmd.SetOut(output) - cmd.SetArgs([]string{ - "start", - }) - cmd.Execute() - - gotOutput := output.String() - - assert.Equal(t, string(wantConfig), gotOutput) - }) -} - -func TestPrecedence(t *testing.T) { - testDir, err := os.Getwd() - require.NoError(t, err, "error getting the current working directory") - - // CHANGE `startCmd` behavior to only print the config, this is for testing purposes only - newStartCommand = mockStartCommand - - t.Run("config file", func(t *testing.T) { - wantConfig, err := os.ReadFile(filepath.Join(testDir, "testdata", "config_yaml_wanted.json")) - require.NoError(t, err, "error reading test config file") - - conf = config.NewConfig() - cmd := NewRootCommand() - output := &bytes.Buffer{} - cmd.SetOut(output) - cmd.SetArgs([]string{ - "--configFile", filepath.Join(testDir, "testdata", "config.yml"), - "start", - }) - cmd.Execute() - - gotOutput := output.String() - - assert.Equal(t, string(wantConfig), gotOutput) - }) - - t.Run("env var", func(t *testing.T) { - setEnvsFromFile(filepath.Join(testDir, "testdata", "config.env")) - defer unsetEnvsFromFile(filepath.Join(testDir, "testdata", "config.env")) - - wantConfig, err := os.ReadFile(filepath.Join(testDir, "testdata", "config_env_wanted.json")) - require.NoError(t, err, "error reading test config file") - - conf = config.NewConfig() - cmd := NewRootCommand() - output := &bytes.Buffer{} - cmd.SetOut(output) - cmd.SetArgs([]string{ - "--configFile", filepath.Join(testDir, "testdata", "config.yml"), - "start", - }) - cmd.Execute() - - gotOutput := output.String() - - assert.Equal(t, string(wantConfig), gotOutput) - }) - - t.Run("flag", func(t *testing.T) { - setEnvsFromFile(filepath.Join(testDir, "testdata", "config.env")) - defer unsetEnvsFromFile(filepath.Join(testDir, "testdata", "config.env")) - - wantConfig, err := os.ReadFile(filepath.Join(testDir, "testdata", "config_cli_wanted.json")) - require.NoError(t, err, "error reading test config file") - - cmd := NewRootCommand() - output := &bytes.Buffer{} - conf = config.NewConfig() - cmd.SetOut(output) - cmd.SetArgs([]string{ - "--configFile", filepath.Join(testDir, "testdata", "config.yml"), - "start", - "--provider.name", "cli", - "--provider.kubernetes.qps", "256", - "--provider.kubernetes.burst", "512", - "--provider.kubernetes.delimiter", "_", - "--server.port", "3333", - "--server.base-path", "/cli/", - "--storage.file", "/tmp/cli.json", - "--sessions.default-duration", "3h", - "--sessions.expiration-interval", "3h", - "--logging.level", "info", - "--strategy.dynamic.custom-themes-path", "/tmp/cli/themes", - // Must use `=` see https://github.com/spf13/cobra/issues/613 - "--strategy.dynamic.show-details-by-default=false", - "--strategy.dynamic.default-theme", "cli", - "--strategy.dynamic.default-refresh-frequency", "3h", - "--strategy.blocking.default-timeout", "3h", - }) - cmd.Execute() - - gotOutput := output.String() - - assert.Equal(t, string(wantConfig), gotOutput) - }) -} - -func setEnvsFromFile(path string) { - readFile, err := os.Open(path) - - if err != nil { - panic(err) - } - - defer readFile.Close() - - if err != nil { - panic(err) - } - - fileScanner := bufio.NewScanner(readFile) - - fileScanner.Split(bufio.ScanLines) - - for fileScanner.Scan() { - splitted := strings.Split(fileScanner.Text(), "=") - os.Setenv(splitted[0], splitted[1]) - } -} - -func unsetEnvsFromFile(path string) { - readFile, err := os.Open(path) - - if err != nil { - panic(err) - } - - defer readFile.Close() - - if err != nil { - panic(err) - } - - fileScanner := bufio.NewScanner(readFile) - - fileScanner.Split(bufio.ScanLines) - - for fileScanner.Scan() { - splitted := strings.Split(fileScanner.Text(), "=") - os.Unsetenv(splitted[0]) - } -} - -func mockStartCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "start", - Short: "InstanceStart the Sablier server", - Run: func(cmd *cobra.Command, args []string) { - viper.Unmarshal(&conf) - - out := cmd.OutOrStdout() - - encoder := json.NewEncoder(out) - - encoder.SetIndent("", " ") - encoder.Encode(conf) - }, - } - return cmd -} diff --git a/cmd/root.go b/cmd/sablier/cmd.go similarity index 92% rename from cmd/root.go rename to cmd/sablier/cmd.go index 85f7159..450e470 100644 --- a/cmd/root.go +++ b/cmd/sablier/cmd.go @@ -1,16 +1,17 @@ -package cmd +package main import ( "fmt" + "github.com/sablierapp/sablier/cmd/healthcheck" + "github.com/sablierapp/sablier/cmd/version" + "github.com/sablierapp/sablier/pkg/config" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" "log/slog" "os" "strings" "time" - - "github.com/sablierapp/sablier/config" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" ) const ( @@ -21,7 +22,7 @@ const ( var conf = config.NewConfig() var cfgFile string -func Execute() { +func main() { cmd := NewRootCommand() if err := cmd.Execute(); err != nil { os.Exit(1) @@ -42,7 +43,7 @@ It provides an integrations with multiple reverse proxies and different loading rootCmd.PersistentFlags().StringVar(&cfgFile, "configFile", "", "Config file path. If not defined, looks for sablier.(yml|yaml|toml) in /etc/sablier/ > $XDG_CONFIG_HOME > $HOME/.config/ and current directory") - startCmd := newStartCommand() + startCmd := NewCmd() // Provider flags startCmd.Flags().StringVar(&conf.Provider.Name, "provider.name", "docker", fmt.Sprintf("Provider to use to manage containers %v", config.GetProviders())) viper.BindPFlag("provider.name", startCmd.Flags().Lookup("provider.name")) @@ -69,7 +70,7 @@ It provides an integrations with multiple reverse proxies and different loading viper.BindPFlag("sessions.expiration-interval", startCmd.Flags().Lookup("sessions.expiration-interval")) // logging level - rootCmd.PersistentFlags().StringVar(&conf.Logging.Level, "logging.level", strings.ToLower(slog.LevelInfo.String()), "The logging level. Can be one of [panic, fatal, error, warn, info, debug, trace]") + rootCmd.PersistentFlags().StringVar(&conf.Logging.Level, "logging.level", strings.ToLower(slog.LevelInfo.String()), "The logging level. Can be one of [error, warn, info, debug]") viper.BindPFlag("logging.level", rootCmd.PersistentFlags().Lookup("logging.level")) // strategy @@ -85,9 +86,9 @@ It provides an integrations with multiple reverse proxies and different loading viper.BindPFlag("strategy.blocking.default-timeout", startCmd.Flags().Lookup("strategy.blocking.default-timeout")) rootCmd.AddCommand(startCmd) - rootCmd.AddCommand(newVersionCommand()) + rootCmd.AddCommand(version.NewCmd()) - healthCmd := newHealthCommand() + healthCmd := healthcheck.NewCmd() healthCmd.Flags().String("url", "http://localhost:10000/health", "Sablier health endpoint") rootCmd.AddCommand(healthCmd) @@ -147,3 +148,21 @@ func bindFlags(cmd *cobra.Command, v *viper.Viper) { } }) } + +func NewCmd() *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "Start the Sablier server", + Run: func(cmd *cobra.Command, args []string) { + err := viper.Unmarshal(&conf) + if err != nil { + panic(err) + } + + err = Start(cmd.Context(), conf) + if err != nil { + panic(err) + } + }, + } +} diff --git a/app/logger.go b/cmd/sablier/logger.go similarity index 93% rename from app/logger.go rename to cmd/sablier/logger.go index afffda3..b6bba7d 100644 --- a/app/logger.go +++ b/cmd/sablier/logger.go @@ -1,8 +1,8 @@ -package app +package main import ( "github.com/lmittmann/tint" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "log/slog" "os" "strings" diff --git a/cmd/sablier/provider.go b/cmd/sablier/provider.go new file mode 100644 index 0000000..191898d --- /dev/null +++ b/cmd/sablier/provider.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "fmt" + "github.com/docker/docker/client" + "github.com/sablierapp/sablier/pkg/config" + "github.com/sablierapp/sablier/pkg/provider/docker" + "github.com/sablierapp/sablier/pkg/provider/dockerswarm" + "github.com/sablierapp/sablier/pkg/provider/kubernetes" + "github.com/sablierapp/sablier/pkg/sablier" + k8s "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "log/slog" +) + +func setupProvider(ctx context.Context, logger *slog.Logger, config config.Provider) (sablier.Provider, error) { + if err := config.IsValid(); err != nil { + return nil, err + } + + switch config.Name { + case "swarm", "docker_swarm": + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("cannot create docker swarm client: %v", err) + } + return dockerswarm.New(ctx, cli, logger) + case "docker": + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("cannot create docker client: %v", err) + } + return docker.New(ctx, cli, logger) + case "kubernetes": + kubeclientConfig, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + kubeclientConfig.QPS = config.Kubernetes.QPS + kubeclientConfig.Burst = config.Kubernetes.Burst + + cli, err := k8s.NewForConfig(kubeclientConfig) + if err != nil { + return nil, err + } + return kubernetes.New(ctx, cli, logger, config.Kubernetes) + } + return nil, fmt.Errorf("unimplemented provider %s", config.Name) +} diff --git a/cmd/sablier/sablier.go b/cmd/sablier/sablier.go new file mode 100644 index 0000000..ac468a3 --- /dev/null +++ b/cmd/sablier/sablier.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "fmt" + "github.com/sablierapp/sablier/internal/api" + "github.com/sablierapp/sablier/pkg/config" + "github.com/sablierapp/sablier/pkg/sablier" + "github.com/sablierapp/sablier/pkg/store/inmemory" + "github.com/sablierapp/sablier/pkg/version" + "log/slog" + "os/signal" + "syscall" + "time" + + "github.com/sablierapp/sablier/internal/server" +) + +func Start(ctx context.Context, conf config.Config) error { + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer stop() + logger := setupLogger(conf.Logging) + + logger.Info("running Sablier version " + version.Info()) + + provider, err := setupProvider(ctx, logger, conf.Provider) + if err != nil { + return fmt.Errorf("cannot setup provider: %w", err) + } + + store := inmemory.NewInMemory() + err = store.OnExpire(ctx, sablier.OnInstanceExpired(ctx, provider, logger)) + if err != nil { + return err + } + + s := sablier.New(logger, store, provider) + + groups, err := provider.InstanceGroups(ctx) + if err != nil { + logger.WarnContext(ctx, "initial group scan failed", slog.Any("reason", err)) + } else { + s.SetGroups(groups) + } + + go s.GroupWatch(ctx) + instanceStopped := make(chan string) + go provider.NotifyInstanceStopped(ctx, instanceStopped) + go func() { + for stopped := range instanceStopped { + err := s.RemoveInstance(ctx, stopped) + if err != nil { + logger.Warn("could not remove instance", slog.Any("error", err)) + } + } + }() + + if conf.Provider.AutoStopOnStartup { + err := s.StopAllUnregisteredInstances(ctx) + if err != nil { + logger.ErrorContext(ctx, "unable to stop unregistered instances", slog.Any("reason", err)) + } + } + + t, err := setupTheme(ctx, conf, logger) + if err != nil { + return fmt.Errorf("cannot setup theme: %w", err) + } + + strategy := &api.ServeStrategy{ + Theme: t, + Sablier: s, + StrategyConfig: conf.Strategy, + SessionsConfig: conf.Sessions, + } + + go server.Start(ctx, logger, conf.Server, strategy) + + // Listen for the interrupt signal. + <-ctx.Done() + + stop() + logger.InfoContext(ctx, "shutting down gracefully, press Ctrl+C again to force") + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + logger.InfoContext(ctx, "Server exiting") + + return nil +} diff --git a/cmd/sablier/theme.go b/cmd/sablier/theme.go new file mode 100644 index 0000000..80ed760 --- /dev/null +++ b/cmd/sablier/theme.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "github.com/sablierapp/sablier/pkg/config" + "github.com/sablierapp/sablier/pkg/theme" + "log/slog" + "os" +) + +func setupTheme(ctx context.Context, conf config.Config, logger *slog.Logger) (*theme.Themes, error) { + if conf.Strategy.Dynamic.CustomThemesPath != "" { + logger.DebugContext(ctx, "loading themes from custom theme path", slog.String("path", conf.Strategy.Dynamic.CustomThemesPath)) + custom := os.DirFS(conf.Strategy.Dynamic.CustomThemesPath) + t, err := theme.NewWithCustomThemes(custom, logger) + if err != nil { + return nil, err + } + return t, nil + } + logger.DebugContext(ctx, "loading themes without custom theme path", slog.String("reason", "--strategy.dynamic.custom-themes-path is empty")) + t, err := theme.New(logger) + if err != nil { + return nil, err + + } + return t, nil +} diff --git a/cmd/start.go b/cmd/start.go deleted file mode 100644 index 517aa7a..0000000 --- a/cmd/start.go +++ /dev/null @@ -1,22 +0,0 @@ -package cmd - -import ( - "github.com/sablierapp/sablier/app" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var newStartCommand = func() *cobra.Command { - return &cobra.Command{ - Use: "start", - Short: "InstanceStart the Sablier server", - Run: func(cmd *cobra.Command, args []string) { - viper.Unmarshal(&conf) - - err := app.Start(cmd.Context(), conf) - if err != nil { - panic(err) - } - }, - } -} diff --git a/cmd/version.go b/cmd/version/version.go similarity index 68% rename from cmd/version.go rename to cmd/version/version.go index 02dfd99..af94bf3 100644 --- a/cmd/version.go +++ b/cmd/version/version.go @@ -1,13 +1,13 @@ -package cmd +package version import ( "fmt" + "github.com/sablierapp/sablier/pkg/version" - "github.com/sablierapp/sablier/version" "github.com/spf13/cobra" ) -var newVersionCommand = func() *cobra.Command { +func NewCmd() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print the version Sablier", diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..ba5b3e4 --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,15 @@ +package api + +import ( + config2 "github.com/sablierapp/sablier/pkg/config" + "github.com/sablierapp/sablier/pkg/sablier" + "github.com/sablierapp/sablier/pkg/theme" +) + +type ServeStrategy struct { + Theme *theme.Themes + + Sablier sablier.Sablier + StrategyConfig config2.Strategy + SessionsConfig config2.Sessions +} diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 2ca9e47..2d3603f 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -3,8 +3,7 @@ package api import ( "github.com/gin-gonic/gin" "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/app/http/routes" - "github.com/sablierapp/sablier/config" + config2 "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/sablier/sabliertest" "github.com/sablierapp/sablier/pkg/theme" "go.uber.org/mock/gomock" @@ -14,7 +13,7 @@ import ( "testing" ) -func NewApiTest(t *testing.T) (app *gin.Engine, router *gin.RouterGroup, strategy *routes.ServeStrategy, mock *sabliertest.MockSablier) { +func NewApiTest(t *testing.T) (app *gin.Engine, router *gin.RouterGroup, strategy *ServeStrategy, mock *sabliertest.MockSablier) { t.Helper() gin.SetMode(gin.TestMode) ctrl := gomock.NewController(t) @@ -24,11 +23,11 @@ func NewApiTest(t *testing.T) (app *gin.Engine, router *gin.RouterGroup, strateg app = gin.New() router = app.Group("/api") mock = sabliertest.NewMockSablier(ctrl) - strategy = &routes.ServeStrategy{ - Theme: th, - SessionsManager: mock, - StrategyConfig: config.NewStrategyConfig(), - SessionsConfig: config.NewSessionsConfig(), + strategy = &ServeStrategy{ + Theme: th, + Sablier: mock, + StrategyConfig: config2.NewStrategyConfig(), + SessionsConfig: config2.NewSessionsConfig(), } return app, router, strategy, mock diff --git a/internal/api/start_blocking.go b/internal/api/start_blocking.go index c89d708..a8945ce 100644 --- a/internal/api/start_blocking.go +++ b/internal/api/start_blocking.go @@ -3,15 +3,21 @@ package api import ( "errors" "github.com/gin-gonic/gin" - "github.com/sablierapp/sablier/app/http/routes" - "github.com/sablierapp/sablier/app/http/routes/models" "github.com/sablierapp/sablier/pkg/sablier" "net/http" + "time" ) -func StartBlocking(router *gin.RouterGroup, s *routes.ServeStrategy) { +type BlockingRequest struct { + Names []string `form:"names"` + Group string `form:"group"` + SessionDuration time.Duration `form:"session_duration"` + Timeout time.Duration `form:"timeout"` +} + +func StartBlocking(router *gin.RouterGroup, s *ServeStrategy) { router.GET("/strategies/blocking", func(c *gin.Context) { - request := models.BlockingRequest{ + request := BlockingRequest{ SessionDuration: s.SessionsConfig.DefaultDuration, Timeout: s.StrategyConfig.Blocking.DefaultTimeout, } @@ -34,9 +40,9 @@ func StartBlocking(router *gin.RouterGroup, s *routes.ServeStrategy) { var sessionState *sablier.SessionState var err error if len(request.Names) > 0 { - sessionState, err = s.SessionsManager.RequestReadySession(c.Request.Context(), request.Names, request.SessionDuration, request.Timeout) + sessionState, err = s.Sablier.RequestReadySession(c.Request.Context(), request.Names, request.SessionDuration, request.Timeout) } else { - sessionState, err = s.SessionsManager.RequestReadySessionGroup(c.Request.Context(), request.Group, request.SessionDuration, request.Timeout) + sessionState, err = s.Sablier.RequestReadySessionGroup(c.Request.Context(), request.Group, request.SessionDuration, request.Timeout) var groupNotFoundError sablier.ErrGroupNotFound if errors.As(err, &groupNotFoundError) { AbortWithProblemDetail(c, ProblemGroupNotFound(groupNotFoundError)) diff --git a/internal/api/start_dynamic.go b/internal/api/start_dynamic.go index 8c17c81..d1047b6 100644 --- a/internal/api/start_dynamic.go +++ b/internal/api/start_dynamic.go @@ -8,16 +8,25 @@ import ( "sort" "strconv" "strings" + "time" "github.com/gin-gonic/gin" - "github.com/sablierapp/sablier/app/http/routes" - "github.com/sablierapp/sablier/app/http/routes/models" - theme2 "github.com/sablierapp/sablier/pkg/theme" + "github.com/sablierapp/sablier/pkg/theme" ) -func StartDynamic(router *gin.RouterGroup, s *routes.ServeStrategy) { +type DynamicRequest struct { + Group string `form:"group"` + Names []string `form:"names"` + ShowDetails bool `form:"show_details"` + DisplayName string `form:"display_name"` + Theme string `form:"theme"` + SessionDuration time.Duration `form:"session_duration"` + RefreshFrequency time.Duration `form:"refresh_frequency"` +} + +func StartDynamic(router *gin.RouterGroup, s *ServeStrategy) { router.GET("/strategies/dynamic", func(c *gin.Context) { - request := models.DynamicRequest{ + request := DynamicRequest{ Theme: s.StrategyConfig.Dynamic.DefaultTheme, ShowDetails: s.StrategyConfig.Dynamic.ShowDetailsByDefault, RefreshFrequency: s.StrategyConfig.Dynamic.DefaultRefreshFrequency, @@ -42,9 +51,9 @@ func StartDynamic(router *gin.RouterGroup, s *routes.ServeStrategy) { var sessionState *sablier.SessionState var err error if len(request.Names) > 0 { - sessionState, err = s.SessionsManager.RequestSession(c, request.Names, request.SessionDuration) + sessionState, err = s.Sablier.RequestSession(c, request.Names, request.SessionDuration) } else { - sessionState, err = s.SessionsManager.RequestSessionGroup(c, request.Group, request.SessionDuration) + sessionState, err = s.Sablier.RequestSessionGroup(c, request.Group, request.SessionDuration) var groupNotFoundError sablier.ErrGroupNotFound if errors.As(err, &groupNotFoundError) { AbortWithProblemDetail(c, ProblemGroupNotFound(groupNotFoundError)) @@ -64,7 +73,7 @@ func StartDynamic(router *gin.RouterGroup, s *routes.ServeStrategy) { AddSablierHeader(c, sessionState) - renderOptions := theme2.Options{ + renderOptions := theme.Options{ DisplayName: request.DisplayName, ShowDetails: request.ShowDetails, SessionDuration: request.SessionDuration, @@ -75,7 +84,7 @@ func StartDynamic(router *gin.RouterGroup, s *routes.ServeStrategy) { buf := new(bytes.Buffer) writer := bufio.NewWriter(buf) err = s.Theme.Render(request.Theme, renderOptions, writer) - var themeNotFound theme2.ErrThemeNotFound + var themeNotFound theme.ErrThemeNotFound if errors.As(err, &themeNotFound) { AbortWithProblemDetail(c, ProblemThemeNotFound(themeNotFound)) return @@ -89,7 +98,7 @@ func StartDynamic(router *gin.RouterGroup, s *routes.ServeStrategy) { }) } -func sessionStateToRenderOptionsInstanceState(sessionState *sablier.SessionState) (instances []theme2.Instance) { +func sessionStateToRenderOptionsInstanceState(sessionState *sablier.SessionState) (instances []theme.Instance) { if sessionState == nil { return } @@ -105,7 +114,7 @@ func sessionStateToRenderOptionsInstanceState(sessionState *sablier.SessionState return } -func instanceStateToRenderOptionsRequestState(instanceState sablier.InstanceInfo) theme2.Instance { +func instanceStateToRenderOptionsRequestState(instanceState sablier.InstanceInfo) theme.Instance { var err error if instanceState.Message == "" { @@ -114,7 +123,7 @@ func instanceStateToRenderOptionsRequestState(instanceState sablier.InstanceInfo err = errors.New(instanceState.Message) } - return theme2.Instance{ + return theme.Instance{ Name: instanceState.Name, Status: string(instanceState.Status), CurrentReplicas: instanceState.CurrentReplicas, diff --git a/internal/api/theme_list.go b/internal/api/theme_list.go index b2553b9..5977870 100644 --- a/internal/api/theme_list.go +++ b/internal/api/theme_list.go @@ -2,11 +2,10 @@ package api import ( "github.com/gin-gonic/gin" - "github.com/sablierapp/sablier/app/http/routes" "net/http" ) -func ListThemes(router *gin.RouterGroup, s *routes.ServeStrategy) { +func ListThemes(router *gin.RouterGroup, s *ServeStrategy) { handler := func(c *gin.Context) { c.JSON(http.StatusOK, map[string]interface{}{ "themes": s.Theme.List(), diff --git a/internal/server/routes.go b/internal/server/routes.go index 3030f7e..d45347a 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -3,12 +3,11 @@ package server import ( "context" "github.com/gin-gonic/gin" - "github.com/sablierapp/sablier/app/http/routes" - "github.com/sablierapp/sablier/config" "github.com/sablierapp/sablier/internal/api" + "github.com/sablierapp/sablier/pkg/config" ) -func registerRoutes(ctx context.Context, router *gin.Engine, serverConf config.Server, s *routes.ServeStrategy) { +func registerRoutes(ctx context.Context, router *gin.Engine, serverConf config.Server, s *api.ServeStrategy) { // Enables automatic redirection if the current route cannot be matched but a // handler for the path with (without) the trailing slash exists. router.RedirectTrailingSlash = true diff --git a/internal/server/server.go b/internal/server/server.go index deb2300..63c9218 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -5,14 +5,14 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/sablierapp/sablier/app/http/routes" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/internal/api" + "github.com/sablierapp/sablier/pkg/config" "log/slog" "net/http" "time" ) -func setupRouter(ctx context.Context, logger *slog.Logger, serverConf config.Server, s *routes.ServeStrategy) *gin.Engine { +func setupRouter(ctx context.Context, logger *slog.Logger, serverConf config.Server, s *api.ServeStrategy) *gin.Engine { r := gin.New() r.Use(StructuredLogger(logger)) @@ -23,7 +23,7 @@ func setupRouter(ctx context.Context, logger *slog.Logger, serverConf config.Ser return r } -func Start(ctx context.Context, logger *slog.Logger, serverConf config.Server, s *routes.ServeStrategy) { +func Start(ctx context.Context, logger *slog.Logger, serverConf config.Server, s *api.ServeStrategy) { start := time.Now() if logger.Enabled(ctx, slog.LevelDebug) { diff --git a/main.go b/main.go deleted file mode 100644 index 5c26563..0000000 --- a/main.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "github.com/gin-gonic/gin" - "github.com/sablierapp/sablier/cmd" -) - -func main() { - gin.SetMode(gin.ReleaseMode) - cmd.Execute() -} diff --git a/config/configuration.go b/pkg/config/configuration.go similarity index 100% rename from config/configuration.go rename to pkg/config/configuration.go diff --git a/config/logging.go b/pkg/config/logging.go similarity index 100% rename from config/logging.go rename to pkg/config/logging.go diff --git a/config/provider.go b/pkg/config/provider.go similarity index 74% rename from config/provider.go rename to pkg/config/provider.go index 398fc32..f8cc11b 100644 --- a/config/provider.go +++ b/pkg/config/provider.go @@ -14,11 +14,11 @@ type Provider struct { } type Kubernetes struct { - //QPS limit for K8S API access client-side throttle + // QPS limit for K8S API access client-side throttle QPS float32 `mapstructure:"QPS" yaml:"QPS" default:"5"` - //Maximum burst for client-side throttle + // Maximum burst for client-side throttle Burst int `mapstructure:"BURST" yaml:"Burst" default:"10"` - //Delimiter used for namespace/resource type/name resolution. Defaults to "_" for backward compatibility. But you should use "/" or ".". + // Delimiter used for namespace/resource type/name resolution. Defaults to "_" for backward compatibility. But you should use "/" or ".". Delimiter string `mapstructure:"DELIMITER" yaml:"Delimiter" default:"_"` } @@ -31,7 +31,7 @@ func NewProviderConfig() Provider { Kubernetes: Kubernetes{ QPS: 5, Burst: 10, - Delimiter: "_", //Delimiter used for namespace/resource type/name resolution. Defaults to "_" for backward compatibility. But you should use "/" or ".". + Delimiter: "_", }, } } diff --git a/config/server.go b/pkg/config/server.go similarity index 100% rename from config/server.go rename to pkg/config/server.go diff --git a/config/sessions.go b/pkg/config/sessions.go similarity index 100% rename from config/sessions.go rename to pkg/config/sessions.go diff --git a/config/storage.go b/pkg/config/storage.go similarity index 100% rename from config/storage.go rename to pkg/config/storage.go diff --git a/config/strategy.go b/pkg/config/strategy.go similarity index 100% rename from config/strategy.go rename to pkg/config/strategy.go diff --git a/pkg/provider/docker/container_inspect.go b/pkg/provider/docker/container_inspect.go index 26077ee..2c7931b 100644 --- a/pkg/provider/docker/container_inspect.go +++ b/pkg/provider/docker/container_inspect.go @@ -7,7 +7,7 @@ import ( "log/slog" ) -func (p *DockerClassicProvider) InstanceInspect(ctx context.Context, name string) (sablier.InstanceInfo, error) { +func (p *Provider) InstanceInspect(ctx context.Context, name string) (sablier.InstanceInfo, error) { spec, err := p.Client.ContainerInspect(ctx, name) if err != nil { return sablier.InstanceInfo{}, fmt.Errorf("cannot inspect container: %w", err) diff --git a/pkg/provider/docker/container_inspect_test.go b/pkg/provider/docker/container_inspect_test.go index ee5eeb7..a94d7f5 100644 --- a/pkg/provider/docker/container_inspect_test.go +++ b/pkg/provider/docker/container_inspect_test.go @@ -263,7 +263,7 @@ func TestDockerClassicProvider_GetState(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := docker.NewDockerClassicProvider(ctx, c.client, slogt.New(t)) + p, err := docker.New(ctx, c.client, slogt.New(t)) name, err := tt.args.do(c) assert.NilError(t, err) diff --git a/pkg/provider/docker/container_list.go b/pkg/provider/docker/container_list.go index 6955f18..78e3843 100644 --- a/pkg/provider/docker/container_list.go +++ b/pkg/provider/docker/container_list.go @@ -11,7 +11,7 @@ import ( "strings" ) -func (p *DockerClassicProvider) InstanceList(ctx context.Context, options provider.InstanceListOptions) ([]sablier.InstanceConfiguration, error) { +func (p *Provider) InstanceList(ctx context.Context, options provider.InstanceListOptions) ([]sablier.InstanceConfiguration, error) { args := filters.NewArgs() args.Add("label", fmt.Sprintf("%s=true", "sablier.enable")) @@ -49,7 +49,7 @@ func containerToInstance(c dockertypes.Container) sablier.InstanceConfiguration } } -func (p *DockerClassicProvider) InstanceGroups(ctx context.Context) (map[string][]string, error) { +func (p *Provider) InstanceGroups(ctx context.Context) (map[string][]string, error) { args := filters.NewArgs() args.Add("label", fmt.Sprintf("%s=true", "sablier.enable")) diff --git a/pkg/provider/docker/container_list_test.go b/pkg/provider/docker/container_list_test.go index 08620b4..a56fd5b 100644 --- a/pkg/provider/docker/container_list_test.go +++ b/pkg/provider/docker/container_list_test.go @@ -18,7 +18,7 @@ func TestDockerClassicProvider_InstanceList(t *testing.T) { ctx := t.Context() dind := setupDinD(t, ctx) - p, err := docker.NewDockerClassicProvider(ctx, dind.client, slogt.New(t)) + p, err := docker.New(ctx, dind.client, slogt.New(t)) assert.NilError(t, err) c1, err := dind.CreateMimic(ctx, MimicOptions{ @@ -77,7 +77,7 @@ func TestDockerClassicProvider_GetGroups(t *testing.T) { ctx := t.Context() dind := setupDinD(t, ctx) - p, err := docker.NewDockerClassicProvider(ctx, dind.client, slogt.New(t)) + p, err := docker.New(ctx, dind.client, slogt.New(t)) assert.NilError(t, err) c1, err := dind.CreateMimic(ctx, MimicOptions{ diff --git a/pkg/provider/docker/container_start.go b/pkg/provider/docker/container_start.go index 5378002..a68b087 100644 --- a/pkg/provider/docker/container_start.go +++ b/pkg/provider/docker/container_start.go @@ -6,7 +6,7 @@ import ( "github.com/docker/docker/api/types/container" ) -func (p *DockerClassicProvider) InstanceStart(ctx context.Context, name string) error { +func (p *Provider) InstanceStart(ctx context.Context, name string) error { // TODO: InstanceStart should block until the container is ready. err := p.Client.ContainerStart(ctx, name, container.StartOptions{}) if err != nil { diff --git a/pkg/provider/docker/container_start_test.go b/pkg/provider/docker/container_start_test.go index ef8d2fe..d3a9efc 100644 --- a/pkg/provider/docker/container_start_test.go +++ b/pkg/provider/docker/container_start_test.go @@ -47,7 +47,7 @@ func TestDockerClassicProvider_Start(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := docker.NewDockerClassicProvider(ctx, c.client, slogt.New(t)) + p, err := docker.New(ctx, c.client, slogt.New(t)) assert.NilError(t, err) name, err := tt.args.do(c) diff --git a/pkg/provider/docker/container_stop.go b/pkg/provider/docker/container_stop.go index 212c242..0a8a0fe 100644 --- a/pkg/provider/docker/container_stop.go +++ b/pkg/provider/docker/container_stop.go @@ -7,7 +7,7 @@ import ( "log/slog" ) -func (p *DockerClassicProvider) InstanceStop(ctx context.Context, name string) error { +func (p *Provider) InstanceStop(ctx context.Context, name string) error { p.l.DebugContext(ctx, "stopping container", slog.String("name", name)) err := p.Client.ContainerStop(ctx, name, container.StopOptions{}) if err != nil { diff --git a/pkg/provider/docker/container_stop_test.go b/pkg/provider/docker/container_stop_test.go index d5a9374..f1ed33a 100644 --- a/pkg/provider/docker/container_stop_test.go +++ b/pkg/provider/docker/container_stop_test.go @@ -57,7 +57,7 @@ func TestDockerClassicProvider_Stop(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := docker.NewDockerClassicProvider(ctx, c.client, slogt.New(t)) + p, err := docker.New(ctx, c.client, slogt.New(t)) name, err := tt.args.do(c) assert.NilError(t, err) diff --git a/pkg/provider/docker/docker.go b/pkg/provider/docker/docker.go index 82bba63..d16c9f3 100644 --- a/pkg/provider/docker/docker.go +++ b/pkg/provider/docker/docker.go @@ -9,15 +9,15 @@ import ( ) // Interface guard -var _ sablier.Provider = (*DockerClassicProvider)(nil) +var _ sablier.Provider = (*Provider)(nil) -type DockerClassicProvider struct { +type Provider struct { Client client.APIClient desiredReplicas int32 l *slog.Logger } -func NewDockerClassicProvider(ctx context.Context, cli *client.Client, logger *slog.Logger) (*DockerClassicProvider, error) { +func New(ctx context.Context, cli *client.Client, logger *slog.Logger) (*Provider, error) { logger = logger.With(slog.String("provider", "docker")) serverVersion, err := cli.ServerVersion(ctx) @@ -29,7 +29,7 @@ func NewDockerClassicProvider(ctx context.Context, cli *client.Client, logger *s slog.String("version", serverVersion.Version), slog.String("api_version", serverVersion.APIVersion), ) - return &DockerClassicProvider{ + return &Provider{ Client: cli, desiredReplicas: 1, l: logger, diff --git a/pkg/provider/docker/events.go b/pkg/provider/docker/events.go index 1111e6d..1b78b39 100644 --- a/pkg/provider/docker/events.go +++ b/pkg/provider/docker/events.go @@ -10,7 +10,7 @@ import ( "strings" ) -func (p *DockerClassicProvider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { +func (p *Provider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { msgs, errs := p.Client.Events(ctx, events.ListOptions{ Filters: filters.NewArgs( filters.Arg("scope", "local"), diff --git a/pkg/provider/docker/events_test.go b/pkg/provider/docker/events_test.go index 81d53c4..55044f7 100644 --- a/pkg/provider/docker/events_test.go +++ b/pkg/provider/docker/events_test.go @@ -18,7 +18,7 @@ func TestDockerClassicProvider_NotifyInstanceStopped(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() dind := setupDinD(t, ctx) - p, err := docker.NewDockerClassicProvider(ctx, dind.client, slogt.New(t)) + p, err := docker.New(ctx, dind.client, slogt.New(t)) assert.NilError(t, err) c, err := dind.CreateMimic(ctx, MimicOptions{}) diff --git a/pkg/provider/dockerswarm/docker_swarm.go b/pkg/provider/dockerswarm/docker_swarm.go index eaa1150..d1ce998 100644 --- a/pkg/provider/dockerswarm/docker_swarm.go +++ b/pkg/provider/dockerswarm/docker_swarm.go @@ -14,16 +14,16 @@ import ( ) // Interface guard -var _ sablier.Provider = (*DockerSwarmProvider)(nil) +var _ sablier.Provider = (*Provider)(nil) -type DockerSwarmProvider struct { +type Provider struct { Client client.APIClient desiredReplicas int32 l *slog.Logger } -func NewDockerSwarmProvider(ctx context.Context, cli *client.Client, logger *slog.Logger) (*DockerSwarmProvider, error) { +func New(ctx context.Context, cli *client.Client, logger *slog.Logger) (*Provider, error) { logger = logger.With(slog.String("provider", "swarm")) serverVersion, err := cli.ServerVersion(ctx) @@ -36,7 +36,7 @@ func NewDockerSwarmProvider(ctx context.Context, cli *client.Client, logger *slo slog.String("api_version", serverVersion.APIVersion), ) - return &DockerSwarmProvider{ + return &Provider{ Client: cli, desiredReplicas: 1, l: logger, @@ -44,7 +44,7 @@ func NewDockerSwarmProvider(ctx context.Context, cli *client.Client, logger *slo } -func (p *DockerSwarmProvider) ServiceUpdateReplicas(ctx context.Context, name string, replicas uint64) error { +func (p *Provider) ServiceUpdateReplicas(ctx context.Context, name string, replicas uint64) error { service, err := p.getServiceByName(name, ctx) if err != nil { return err @@ -69,7 +69,7 @@ func (p *DockerSwarmProvider) ServiceUpdateReplicas(ctx context.Context, name st return nil } -func (p *DockerSwarmProvider) getInstanceName(name string, service swarm.Service) string { +func (p *Provider) getInstanceName(name string, service swarm.Service) string { if name == service.Spec.Name { return name } diff --git a/pkg/provider/dockerswarm/events.go b/pkg/provider/dockerswarm/events.go index 736fd02..2680be9 100644 --- a/pkg/provider/dockerswarm/events.go +++ b/pkg/provider/dockerswarm/events.go @@ -9,7 +9,7 @@ import ( "log/slog" ) -func (p *DockerSwarmProvider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { +func (p *Provider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { msgs, errs := p.Client.Events(ctx, events.ListOptions{ Filters: filters.NewArgs( filters.Arg("scope", "swarm"), diff --git a/pkg/provider/dockerswarm/events_test.go b/pkg/provider/dockerswarm/events_test.go index 7aebf9d..ed07fc0 100644 --- a/pkg/provider/dockerswarm/events_test.go +++ b/pkg/provider/dockerswarm/events_test.go @@ -18,7 +18,7 @@ func TestDockerSwarmProvider_NotifyInstanceStopped(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() dind := setupDinD(t, ctx) - p, err := dockerswarm.NewDockerSwarmProvider(ctx, dind.client, slogt.New(t)) + p, err := dockerswarm.New(ctx, dind.client, slogt.New(t)) assert.NilError(t, err) c, err := dind.CreateMimic(ctx, MimicOptions{}) diff --git a/pkg/provider/dockerswarm/service_inspect.go b/pkg/provider/dockerswarm/service_inspect.go index f6b99b7..cc038f7 100644 --- a/pkg/provider/dockerswarm/service_inspect.go +++ b/pkg/provider/dockerswarm/service_inspect.go @@ -10,7 +10,7 @@ import ( "github.com/sablierapp/sablier/pkg/sablier" ) -func (p *DockerSwarmProvider) InstanceInspect(ctx context.Context, name string) (sablier.InstanceInfo, error) { +func (p *Provider) InstanceInspect(ctx context.Context, name string) (sablier.InstanceInfo, error) { service, err := p.getServiceByName(name, ctx) if err != nil { return sablier.InstanceInfo{}, err @@ -29,7 +29,7 @@ func (p *DockerSwarmProvider) InstanceInspect(ctx context.Context, name string) return sablier.ReadyInstanceState(foundName, p.desiredReplicas), nil } -func (p *DockerSwarmProvider) getServiceByName(name string, ctx context.Context) (*swarm.Service, error) { +func (p *Provider) getServiceByName(name string, ctx context.Context) (*swarm.Service, error) { opts := types.ServiceListOptions{ Filters: filters.NewArgs(), Status: true, diff --git a/pkg/provider/dockerswarm/service_inspect_test.go b/pkg/provider/dockerswarm/service_inspect_test.go index fd282d5..9f87488 100644 --- a/pkg/provider/dockerswarm/service_inspect_test.go +++ b/pkg/provider/dockerswarm/service_inspect_test.go @@ -127,7 +127,7 @@ func TestDockerSwarmProvider_GetState(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := dockerswarm.NewDockerSwarmProvider(ctx, c.client, slogt.New(t)) + p, err := dockerswarm.New(ctx, c.client, slogt.New(t)) name, err := tt.args.do(c) assert.NilError(t, err) @@ -135,7 +135,7 @@ func TestDockerSwarmProvider_GetState(t *testing.T) { tt.want.Name = name got, err := p.InstanceInspect(ctx, name) if !cmp.Equal(err, tt.wantErr) { - t.Errorf("DockerSwarmProvider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Provider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) return } assert.DeepEqual(t, got, tt.want) diff --git a/pkg/provider/dockerswarm/service_list.go b/pkg/provider/dockerswarm/service_list.go index fff9e56..88445de 100644 --- a/pkg/provider/dockerswarm/service_list.go +++ b/pkg/provider/dockerswarm/service_list.go @@ -10,7 +10,7 @@ import ( "github.com/sablierapp/sablier/pkg/sablier" ) -func (p *DockerSwarmProvider) InstanceList(ctx context.Context, _ provider.InstanceListOptions) ([]sablier.InstanceConfiguration, error) { +func (p *Provider) InstanceList(ctx context.Context, _ provider.InstanceListOptions) ([]sablier.InstanceConfiguration, error) { args := filters.NewArgs() args.Add("label", fmt.Sprintf("%s=true", "sablier.enable")) args.Add("mode", "replicated") @@ -32,7 +32,7 @@ func (p *DockerSwarmProvider) InstanceList(ctx context.Context, _ provider.Insta return instances, nil } -func (p *DockerSwarmProvider) serviceToInstance(s swarm.Service) (i sablier.InstanceConfiguration) { +func (p *Provider) serviceToInstance(s swarm.Service) (i sablier.InstanceConfiguration) { var group string if _, ok := s.Spec.Labels["sablier.enable"]; ok { @@ -49,7 +49,7 @@ func (p *DockerSwarmProvider) serviceToInstance(s swarm.Service) (i sablier.Inst } } -func (p *DockerSwarmProvider) InstanceGroups(ctx context.Context) (map[string][]string, error) { +func (p *Provider) InstanceGroups(ctx context.Context) (map[string][]string, error) { f := filters.NewArgs() f.Add("label", fmt.Sprintf("%s=true", "sablier.enable")) diff --git a/pkg/provider/dockerswarm/service_list_test.go b/pkg/provider/dockerswarm/service_list_test.go index 846840f..5a59386 100644 --- a/pkg/provider/dockerswarm/service_list_test.go +++ b/pkg/provider/dockerswarm/service_list_test.go @@ -20,7 +20,7 @@ func TestDockerClassicProvider_InstanceList(t *testing.T) { ctx := t.Context() dind := setupDinD(t, ctx) - p, err := dockerswarm.NewDockerSwarmProvider(ctx, dind.client, slogt.New(t)) + p, err := dockerswarm.New(ctx, dind.client, slogt.New(t)) assert.NilError(t, err) s1, err := dind.CreateMimic(ctx, MimicOptions{ @@ -77,7 +77,7 @@ func TestDockerClassicProvider_GetGroups(t *testing.T) { ctx := t.Context() dind := setupDinD(t, ctx) - p, err := dockerswarm.NewDockerSwarmProvider(ctx, dind.client, slogt.New(t)) + p, err := dockerswarm.New(ctx, dind.client, slogt.New(t)) assert.NilError(t, err) s1, err := dind.CreateMimic(ctx, MimicOptions{ diff --git a/pkg/provider/dockerswarm/service_start.go b/pkg/provider/dockerswarm/service_start.go index 41451c9..eda8767 100644 --- a/pkg/provider/dockerswarm/service_start.go +++ b/pkg/provider/dockerswarm/service_start.go @@ -2,6 +2,6 @@ package dockerswarm import "context" -func (p *DockerSwarmProvider) InstanceStart(ctx context.Context, name string) error { +func (p *Provider) InstanceStart(ctx context.Context, name string) error { return p.ServiceUpdateReplicas(ctx, name, uint64(p.desiredReplicas)) } diff --git a/pkg/provider/dockerswarm/service_start_test.go b/pkg/provider/dockerswarm/service_start_test.go index e1b41d4..832c4b6 100644 --- a/pkg/provider/dockerswarm/service_start_test.go +++ b/pkg/provider/dockerswarm/service_start_test.go @@ -126,7 +126,7 @@ func TestDockerSwarmProvider_Start(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := dockerswarm.NewDockerSwarmProvider(ctx, c.client, slogt.New(t)) + p, err := dockerswarm.New(ctx, c.client, slogt.New(t)) name, err := tt.args.do(c) assert.NilError(t, err) @@ -134,7 +134,7 @@ func TestDockerSwarmProvider_Start(t *testing.T) { tt.want.Name = name err = p.InstanceStart(ctx, name) if !cmp.Equal(err, tt.wantErr) { - t.Errorf("DockerSwarmProvider.InstanceStop() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Provider.InstanceStop() error = %v, wantErr %v", err, tt.wantErr) return } diff --git a/pkg/provider/dockerswarm/service_stop.go b/pkg/provider/dockerswarm/service_stop.go index 2061255..2eb144b 100644 --- a/pkg/provider/dockerswarm/service_stop.go +++ b/pkg/provider/dockerswarm/service_stop.go @@ -2,6 +2,6 @@ package dockerswarm import "context" -func (p *DockerSwarmProvider) InstanceStop(ctx context.Context, name string) error { +func (p *Provider) InstanceStop(ctx context.Context, name string) error { return p.ServiceUpdateReplicas(ctx, name, 0) } diff --git a/pkg/provider/dockerswarm/service_stop_test.go b/pkg/provider/dockerswarm/service_stop_test.go index ad8cfdf..fbe23b2 100644 --- a/pkg/provider/dockerswarm/service_stop_test.go +++ b/pkg/provider/dockerswarm/service_stop_test.go @@ -94,7 +94,7 @@ func TestDockerSwarmProvider_Stop(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := dockerswarm.NewDockerSwarmProvider(ctx, c.client, slogt.New(t)) + p, err := dockerswarm.New(ctx, c.client, slogt.New(t)) name, err := tt.args.do(c) assert.NilError(t, err) @@ -102,7 +102,7 @@ func TestDockerSwarmProvider_Stop(t *testing.T) { tt.want.Name = name err = p.InstanceStop(ctx, name) if !cmp.Equal(err, tt.wantErr) { - t.Errorf("DockerSwarmProvider.InstanceStop() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Provider.InstanceStop() error = %v, wantErr %v", err, tt.wantErr) return } diff --git a/pkg/provider/kubernetes/deployment_events.go b/pkg/provider/kubernetes/deployment_events.go index 36787b7..d9ea375 100644 --- a/pkg/provider/kubernetes/deployment_events.go +++ b/pkg/provider/kubernetes/deployment_events.go @@ -8,7 +8,7 @@ import ( "time" ) -func (p *KubernetesProvider) watchDeployents(instance chan<- string) cache.SharedIndexInformer { +func (p *Provider) watchDeployents(instance chan<- string) cache.SharedIndexInformer { handler := cache.ResourceEventHandlerFuncs{ UpdateFunc: func(old, new interface{}) { newDeployment := new.(*appsv1.Deployment) diff --git a/pkg/provider/kubernetes/deployment_inspect.go b/pkg/provider/kubernetes/deployment_inspect.go index 393bb8e..9280498 100644 --- a/pkg/provider/kubernetes/deployment_inspect.go +++ b/pkg/provider/kubernetes/deployment_inspect.go @@ -7,7 +7,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (p *KubernetesProvider) DeploymentInspect(ctx context.Context, config ParsedName) (sablier.InstanceInfo, error) { +func (p *Provider) DeploymentInspect(ctx context.Context, config ParsedName) (sablier.InstanceInfo, error) { d, err := p.Client.AppsV1().Deployments(config.Namespace).Get(ctx, config.Name, metav1.GetOptions{}) if err != nil { return sablier.InstanceInfo{}, fmt.Errorf("error getting deployment: %w", err) diff --git a/pkg/provider/kubernetes/deployment_inspect_test.go b/pkg/provider/kubernetes/deployment_inspect_test.go index 22064ee..4162854 100644 --- a/pkg/provider/kubernetes/deployment_inspect_test.go +++ b/pkg/provider/kubernetes/deployment_inspect_test.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/google/go-cmp/cmp" "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/provider/kubernetes" "github.com/sablierapp/sablier/pkg/sablier" "gotest.tools/v3/assert" @@ -118,7 +118,7 @@ func TestKubernetesProvider_DeploymentInspect(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := kubernetes.NewKubernetesProvider(ctx, c.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, c.client, slogt.New(t), config.NewProviderConfig().Kubernetes) name, err := tt.args.do(c) assert.NilError(t, err) @@ -126,7 +126,7 @@ func TestKubernetesProvider_DeploymentInspect(t *testing.T) { tt.want.Name = name got, err := p.InstanceInspect(ctx, name) if !cmp.Equal(err, tt.wantErr) { - t.Errorf("KubernetesProvider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Provider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) return } assert.DeepEqual(t, got, tt.want) diff --git a/pkg/provider/kubernetes/deployment_list.go b/pkg/provider/kubernetes/deployment_list.go index 8b0362c..e06207b 100644 --- a/pkg/provider/kubernetes/deployment_list.go +++ b/pkg/provider/kubernetes/deployment_list.go @@ -8,7 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (p *KubernetesProvider) DeploymentList(ctx context.Context) ([]sablier.InstanceConfiguration, error) { +func (p *Provider) DeploymentList(ctx context.Context) ([]sablier.InstanceConfiguration, error) { labelSelector := metav1.LabelSelector{ MatchLabels: map[string]string{ "sablier.enable": "true", @@ -30,7 +30,7 @@ func (p *KubernetesProvider) DeploymentList(ctx context.Context) ([]sablier.Inst return instances, nil } -func (p *KubernetesProvider) deploymentToInstance(d *v1.Deployment) sablier.InstanceConfiguration { +func (p *Provider) deploymentToInstance(d *v1.Deployment) sablier.InstanceConfiguration { var group string if _, ok := d.Labels["sablier.enable"]; ok { @@ -49,7 +49,7 @@ func (p *KubernetesProvider) deploymentToInstance(d *v1.Deployment) sablier.Inst } } -func (p *KubernetesProvider) DeploymentGroups(ctx context.Context) (map[string][]string, error) { +func (p *Provider) DeploymentGroups(ctx context.Context) (map[string][]string, error) { labelSelector := metav1.LabelSelector{ MatchLabels: map[string]string{ "sablier.enable": "true", diff --git a/pkg/provider/kubernetes/instance_events.go b/pkg/provider/kubernetes/instance_events.go index 0bdb81f..9e27bee 100644 --- a/pkg/provider/kubernetes/instance_events.go +++ b/pkg/provider/kubernetes/instance_events.go @@ -2,7 +2,7 @@ package kubernetes import "context" -func (p *KubernetesProvider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { +func (p *Provider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { informer := p.watchDeployents(instance) go informer.Run(ctx.Done()) informer = p.watchStatefulSets(instance) diff --git a/pkg/provider/kubernetes/instance_events_test.go b/pkg/provider/kubernetes/instance_events_test.go index 1485add..3a082bb 100644 --- a/pkg/provider/kubernetes/instance_events_test.go +++ b/pkg/provider/kubernetes/instance_events_test.go @@ -3,7 +3,7 @@ package kubernetes_test import ( "context" "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/provider/kubernetes" "gotest.tools/v3/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,7 +19,7 @@ func TestKubernetesProvider_NotifyInstanceStopped(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() kind := setupKinD(t, ctx) - p, err := kubernetes.NewKubernetesProvider(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) assert.NilError(t, err) waitC := make(chan string) diff --git a/pkg/provider/kubernetes/instance_inspect.go b/pkg/provider/kubernetes/instance_inspect.go index c41ea94..52c4391 100644 --- a/pkg/provider/kubernetes/instance_inspect.go +++ b/pkg/provider/kubernetes/instance_inspect.go @@ -6,7 +6,7 @@ import ( "github.com/sablierapp/sablier/pkg/sablier" ) -func (p *KubernetesProvider) InstanceInspect(ctx context.Context, name string) (sablier.InstanceInfo, error) { +func (p *Provider) InstanceInspect(ctx context.Context, name string) (sablier.InstanceInfo, error) { parsed, err := ParseName(name, ParseOptions{Delimiter: p.delimiter}) if err != nil { return sablier.InstanceInfo{}, err diff --git a/pkg/provider/kubernetes/instance_inspect_test.go b/pkg/provider/kubernetes/instance_inspect_test.go index 8c70386..d6cc5a9 100644 --- a/pkg/provider/kubernetes/instance_inspect_test.go +++ b/pkg/provider/kubernetes/instance_inspect_test.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/provider/kubernetes" "gotest.tools/v3/assert" "testing" @@ -14,7 +14,7 @@ func TestKubernetesProvider_InstanceInspect(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } - + ctx := context.Background() type args struct { name string @@ -43,7 +43,7 @@ func TestKubernetesProvider_InstanceInspect(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := kubernetes.NewKubernetesProvider(ctx, c.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, c.client, slogt.New(t), config.NewProviderConfig().Kubernetes) _, err = p.InstanceInspect(ctx, tt.args.name) assert.Error(t, err, tt.want.Error()) diff --git a/pkg/provider/kubernetes/instance_list.go b/pkg/provider/kubernetes/instance_list.go index 9ce6421..74d83b0 100644 --- a/pkg/provider/kubernetes/instance_list.go +++ b/pkg/provider/kubernetes/instance_list.go @@ -6,7 +6,7 @@ import ( "github.com/sablierapp/sablier/pkg/sablier" ) -func (p *KubernetesProvider) InstanceList(ctx context.Context, options provider.InstanceListOptions) ([]sablier.InstanceConfiguration, error) { +func (p *Provider) InstanceList(ctx context.Context, options provider.InstanceListOptions) ([]sablier.InstanceConfiguration, error) { deployments, err := p.DeploymentList(ctx) if err != nil { return nil, err @@ -20,7 +20,7 @@ func (p *KubernetesProvider) InstanceList(ctx context.Context, options provider. return append(deployments, statefulSets...), nil } -func (p *KubernetesProvider) InstanceGroups(ctx context.Context) (map[string][]string, error) { +func (p *Provider) InstanceGroups(ctx context.Context) (map[string][]string, error) { deployments, err := p.DeploymentGroups(ctx) if err != nil { return nil, err diff --git a/pkg/provider/kubernetes/instance_list_test.go b/pkg/provider/kubernetes/instance_list_test.go index e7eb24c..1858241 100644 --- a/pkg/provider/kubernetes/instance_list_test.go +++ b/pkg/provider/kubernetes/instance_list_test.go @@ -2,7 +2,7 @@ package kubernetes_test import ( "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/provider" "github.com/sablierapp/sablier/pkg/provider/kubernetes" "github.com/sablierapp/sablier/pkg/sablier" @@ -19,7 +19,7 @@ func TestKubernetesProvider_InstanceList(t *testing.T) { ctx := t.Context() kind := setupKinD(t, ctx) - p, err := kubernetes.NewKubernetesProvider(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) assert.NilError(t, err) d1, err := kind.CreateMimicDeployment(ctx, MimicOptions{ @@ -93,7 +93,7 @@ func TestKubernetesProvider_InstanceGroups(t *testing.T) { ctx := t.Context() kind := setupKinD(t, ctx) - p, err := kubernetes.NewKubernetesProvider(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) assert.NilError(t, err) d1, err := kind.CreateMimicDeployment(ctx, MimicOptions{ diff --git a/pkg/provider/kubernetes/instance_start.go b/pkg/provider/kubernetes/instance_start.go index a621505..d249848 100644 --- a/pkg/provider/kubernetes/instance_start.go +++ b/pkg/provider/kubernetes/instance_start.go @@ -2,7 +2,7 @@ package kubernetes import "context" -func (p *KubernetesProvider) InstanceStart(ctx context.Context, name string) error { +func (p *Provider) InstanceStart(ctx context.Context, name string) error { parsed, err := ParseName(name, ParseOptions{Delimiter: p.delimiter}) if err != nil { return err diff --git a/pkg/provider/kubernetes/instance_start_test.go b/pkg/provider/kubernetes/instance_start_test.go index 69f148e..7daace6 100644 --- a/pkg/provider/kubernetes/instance_start_test.go +++ b/pkg/provider/kubernetes/instance_start_test.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/provider/kubernetes" "gotest.tools/v3/assert" "testing" @@ -92,7 +92,7 @@ func TestKubernetesProvider_InstanceStart(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := kubernetes.NewKubernetesProvider(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) assert.NilError(t, err) name, err := tt.args.do(kind) diff --git a/pkg/provider/kubernetes/instance_stop.go b/pkg/provider/kubernetes/instance_stop.go index b858d83..fc9a34c 100644 --- a/pkg/provider/kubernetes/instance_stop.go +++ b/pkg/provider/kubernetes/instance_stop.go @@ -2,7 +2,7 @@ package kubernetes import "context" -func (p *KubernetesProvider) InstanceStop(ctx context.Context, name string) error { +func (p *Provider) InstanceStop(ctx context.Context, name string) error { parsed, err := ParseName(name, ParseOptions{Delimiter: p.delimiter}) if err != nil { return err diff --git a/pkg/provider/kubernetes/instance_stop_test.go b/pkg/provider/kubernetes/instance_stop_test.go index d0e458f..c748b82 100644 --- a/pkg/provider/kubernetes/instance_stop_test.go +++ b/pkg/provider/kubernetes/instance_stop_test.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/provider/kubernetes" "gotest.tools/v3/assert" "testing" @@ -92,7 +92,7 @@ func TestKubernetesProvider_InstanceStop(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := kubernetes.NewKubernetesProvider(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, kind.client, slogt.New(t), config.NewProviderConfig().Kubernetes) assert.NilError(t, err) name, err := tt.args.do(kind) diff --git a/pkg/provider/kubernetes/kubernetes.go b/pkg/provider/kubernetes/kubernetes.go index 90cb767..ac703d7 100644 --- a/pkg/provider/kubernetes/kubernetes.go +++ b/pkg/provider/kubernetes/kubernetes.go @@ -2,23 +2,23 @@ package kubernetes import ( "context" + providerConfig "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/sablier" "log/slog" - providerConfig "github.com/sablierapp/sablier/config" "k8s.io/client-go/kubernetes" ) // Interface guard -var _ sablier.Provider = (*KubernetesProvider)(nil) +var _ sablier.Provider = (*Provider)(nil) -type KubernetesProvider struct { +type Provider struct { Client kubernetes.Interface delimiter string l *slog.Logger } -func NewKubernetesProvider(ctx context.Context, client *kubernetes.Clientset, logger *slog.Logger, kubeclientConfig providerConfig.Kubernetes) (*KubernetesProvider, error) { +func New(ctx context.Context, client *kubernetes.Clientset, logger *slog.Logger, config providerConfig.Kubernetes) (*Provider, error) { logger = logger.With(slog.String("provider", "kubernetes")) info, err := client.ServerVersion() @@ -28,13 +28,13 @@ func NewKubernetesProvider(ctx context.Context, client *kubernetes.Clientset, lo logger.InfoContext(ctx, "connection established with kubernetes", slog.String("version", info.String()), - slog.Float64("config.qps", float64(kubeclientConfig.QPS)), - slog.Int("config.burst", kubeclientConfig.Burst), + slog.Float64("config.qps", float64(config.QPS)), + slog.Int("config.burst", config.Burst), ) - return &KubernetesProvider{ + return &Provider{ Client: client, - delimiter: kubeclientConfig.Delimiter, + delimiter: config.Delimiter, l: logger, }, nil diff --git a/pkg/provider/kubernetes/statefulset_events.go b/pkg/provider/kubernetes/statefulset_events.go index 050f750..56ed5e0 100644 --- a/pkg/provider/kubernetes/statefulset_events.go +++ b/pkg/provider/kubernetes/statefulset_events.go @@ -8,7 +8,7 @@ import ( "time" ) -func (p *KubernetesProvider) watchStatefulSets(instance chan<- string) cache.SharedIndexInformer { +func (p *Provider) watchStatefulSets(instance chan<- string) cache.SharedIndexInformer { handler := cache.ResourceEventHandlerFuncs{ UpdateFunc: func(old, new interface{}) { newStatefulSet := new.(*appsv1.StatefulSet) diff --git a/pkg/provider/kubernetes/statefulset_inspect.go b/pkg/provider/kubernetes/statefulset_inspect.go index e4d9383..c352d40 100644 --- a/pkg/provider/kubernetes/statefulset_inspect.go +++ b/pkg/provider/kubernetes/statefulset_inspect.go @@ -6,7 +6,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (p *KubernetesProvider) StatefulSetInspect(ctx context.Context, config ParsedName) (sablier.InstanceInfo, error) { +func (p *Provider) StatefulSetInspect(ctx context.Context, config ParsedName) (sablier.InstanceInfo, error) { ss, err := p.Client.AppsV1().StatefulSets(config.Namespace).Get(ctx, config.Name, metav1.GetOptions{}) if err != nil { return sablier.InstanceInfo{}, err diff --git a/pkg/provider/kubernetes/statefulset_inspect_test.go b/pkg/provider/kubernetes/statefulset_inspect_test.go index 5b3e43d..29d50c4 100644 --- a/pkg/provider/kubernetes/statefulset_inspect_test.go +++ b/pkg/provider/kubernetes/statefulset_inspect_test.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/google/go-cmp/cmp" "github.com/neilotoole/slogt" - "github.com/sablierapp/sablier/config" + "github.com/sablierapp/sablier/pkg/config" "github.com/sablierapp/sablier/pkg/provider/kubernetes" "github.com/sablierapp/sablier/pkg/sablier" "gotest.tools/v3/assert" @@ -118,7 +118,7 @@ func TestKubernetesProvider_InspectStatefulSet(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - p, err := kubernetes.NewKubernetesProvider(ctx, c.client, slogt.New(t), config.NewProviderConfig().Kubernetes) + p, err := kubernetes.New(ctx, c.client, slogt.New(t), config.NewProviderConfig().Kubernetes) name, err := tt.args.do(c) assert.NilError(t, err) diff --git a/pkg/provider/kubernetes/statefulset_list.go b/pkg/provider/kubernetes/statefulset_list.go index 801b274..e2b2732 100644 --- a/pkg/provider/kubernetes/statefulset_list.go +++ b/pkg/provider/kubernetes/statefulset_list.go @@ -8,7 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (p *KubernetesProvider) StatefulSetList(ctx context.Context) ([]sablier.InstanceConfiguration, error) { +func (p *Provider) StatefulSetList(ctx context.Context) ([]sablier.InstanceConfiguration, error) { labelSelector := metav1.LabelSelector{ MatchLabels: map[string]string{ "sablier.enable": "true", @@ -30,7 +30,7 @@ func (p *KubernetesProvider) StatefulSetList(ctx context.Context) ([]sablier.Ins return instances, nil } -func (p *KubernetesProvider) statefulSetToInstance(ss *v1.StatefulSet) sablier.InstanceConfiguration { +func (p *Provider) statefulSetToInstance(ss *v1.StatefulSet) sablier.InstanceConfiguration { var group string if _, ok := ss.Labels["sablier.enable"]; ok { @@ -49,7 +49,7 @@ func (p *KubernetesProvider) statefulSetToInstance(ss *v1.StatefulSet) sablier.I } } -func (p *KubernetesProvider) StatefulSetGroups(ctx context.Context) (map[string][]string, error) { +func (p *Provider) StatefulSetGroups(ctx context.Context) (map[string][]string, error) { labelSelector := metav1.LabelSelector{ MatchLabels: map[string]string{ "sablier.enable": "true", diff --git a/pkg/provider/kubernetes/workload_scale.go b/pkg/provider/kubernetes/workload_scale.go index 2243069..aca6e1d 100644 --- a/pkg/provider/kubernetes/workload_scale.go +++ b/pkg/provider/kubernetes/workload_scale.go @@ -12,7 +12,7 @@ type Workload interface { UpdateScale(ctx context.Context, workloadName string, scale *autoscalingv1.Scale, opts metav1.UpdateOptions) (*autoscalingv1.Scale, error) } -func (p *KubernetesProvider) scale(ctx context.Context, config ParsedName, replicas int32) error { +func (p *Provider) scale(ctx context.Context, config ParsedName, replicas int32) error { var workload Workload switch config.Kind { diff --git a/pkg/sablier/group_watch.go b/pkg/sablier/group_watch.go new file mode 100644 index 0000000..3aebecc --- /dev/null +++ b/pkg/sablier/group_watch.go @@ -0,0 +1,26 @@ +package sablier + +import ( + "context" + "log/slog" + "time" +) + +func (s *sablier) GroupWatch(ctx context.Context) { + // This should be changed to event based instead of polling. + ticker := time.NewTicker(2 * time.Second) + for { + select { + case <-ctx.Done(): + s.l.InfoContext(ctx, "stop watching groups", slog.Any("reason", ctx.Err())) + return + case <-ticker.C: + groups, err := s.provider.InstanceGroups(ctx) + if err != nil { + s.l.ErrorContext(ctx, "cannot retrieve group from provider", slog.Any("reason", err)) + } else if groups != nil { + s.SetGroups(groups) + } + } + } +} diff --git a/pkg/sablier/instance_expired.go b/pkg/sablier/instance_expired.go new file mode 100644 index 0000000..e0472fd --- /dev/null +++ b/pkg/sablier/instance_expired.go @@ -0,0 +1,18 @@ +package sablier + +import ( + "context" + "log/slog" +) + +func OnInstanceExpired(ctx context.Context, provider Provider, logger *slog.Logger) func(string) { + return func(_key string) { + go func(key string) { + logger.InfoContext(ctx, "instance expired", slog.String("instance", key)) + err := provider.InstanceStop(ctx, key) + if err != nil { + logger.ErrorContext(ctx, "instance expired could not be stopped from provider", slog.String("instance", key), slog.Any("error", err)) + } + }(_key) + } +} diff --git a/pkg/sablier/sablier.go b/pkg/sablier/sablier.go index 04e866f..c7e06e4 100644 --- a/pkg/sablier/sablier.go +++ b/pkg/sablier/sablier.go @@ -4,6 +4,7 @@ import ( "context" "github.com/google/go-cmp/cmp" "log/slog" + "sync" "time" ) @@ -18,11 +19,14 @@ type Sablier interface { RemoveInstance(ctx context.Context, name string) error SetGroups(groups map[string][]string) StopAllUnregisteredInstances(ctx context.Context) error + GroupWatch(ctx context.Context) } type sablier struct { provider Provider sessions Store + + groupsMu sync.RWMutex groups map[string][]string l *slog.Logger @@ -32,12 +36,15 @@ func New(logger *slog.Logger, store Store, provider Provider) Sablier { return &sablier{ provider: provider, sessions: store, + groupsMu: sync.RWMutex{}, groups: map[string][]string{}, l: logger, } } func (s *sablier) SetGroups(groups map[string][]string) { + s.groupsMu.Lock() + defer s.groupsMu.Unlock() if groups == nil { groups = map[string][]string{} } diff --git a/pkg/sablier/sabliertest/mocks_sablier.go b/pkg/sablier/sabliertest/mocks_sablier.go index ac06a81..5a7063b 100644 --- a/pkg/sablier/sabliertest/mocks_sablier.go +++ b/pkg/sablier/sabliertest/mocks_sablier.go @@ -42,6 +42,18 @@ func (m *MockSablier) EXPECT() *MockSablierMockRecorder { return m.recorder } +// GroupWatch mocks base method. +func (m *MockSablier) GroupWatch(ctx context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GroupWatch", ctx) +} + +// GroupWatch indicates an expected call of GroupWatch. +func (mr *MockSablierMockRecorder) GroupWatch(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupWatch", reflect.TypeOf((*MockSablier)(nil).GroupWatch), ctx) +} + // RemoveInstance mocks base method. func (m *MockSablier) RemoveInstance(ctx context.Context, name string) error { m.ctrl.T.Helper() diff --git a/pkg/theme/render.go b/pkg/theme/render.go index 6adddd4..520f4f5 100644 --- a/pkg/theme/render.go +++ b/pkg/theme/render.go @@ -2,10 +2,10 @@ package theme import ( "fmt" + "github.com/sablierapp/sablier/pkg/version" "io" "github.com/sablierapp/sablier/pkg/durations" - "github.com/sablierapp/sablier/version" ) func (t *Themes) Render(name string, opts Options, writer io.Writer) error { diff --git a/pkg/theme/render_test.go b/pkg/theme/render_test.go index 631017a..85a9ed0 100644 --- a/pkg/theme/render_test.go +++ b/pkg/theme/render_test.go @@ -5,13 +5,12 @@ import ( "fmt" "github.com/neilotoole/slogt" "github.com/sablierapp/sablier/pkg/theme" + "github.com/sablierapp/sablier/pkg/version" "log/slog" "os" "testing" "testing/fstest" "time" - - "github.com/sablierapp/sablier/version" ) var ( diff --git a/version/info.go b/pkg/version/info.go similarity index 100% rename from version/info.go rename to pkg/version/info.go