mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-21 13:23:03 +01:00
refactor: reorganize code structure (#556)
* refactor: rename providers to Provider * refactor folders * fix build cmd * fix build cmd * fix build cmd * fix cmd start
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
8
Makefile
8
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 ./...
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
184
app/sablier.go
184
app/sablier.go
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
52
cmd/healthcheck/healthcheck.go
Normal file
52
cmd/healthcheck/healthcheck.go
Normal file
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
194
cmd/root_test.go
194
cmd/root_test.go
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
50
cmd/sablier/provider.go
Normal file
50
cmd/sablier/provider.go
Normal file
@@ -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)
|
||||
}
|
||||
92
cmd/sablier/sablier.go
Normal file
92
cmd/sablier/sablier.go
Normal file
@@ -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
|
||||
}
|
||||
28
cmd/sablier/theme.go
Normal file
28
cmd/sablier/theme.go
Normal file
@@ -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
|
||||
}
|
||||
22
cmd/start.go
22
cmd/start.go
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
15
internal/api/api.go
Normal file
15
internal/api/api.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
11
main.go
11
main.go
@@ -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()
|
||||
}
|
||||
@@ -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: "_",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
26
pkg/sablier/group_watch.go
Normal file
26
pkg/sablier/group_watch.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
pkg/sablier/instance_expired.go
Normal file
18
pkg/sablier/instance_expired.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user