diff --git a/.local-dev/config/ns.yaml b/.local-dev/config/ns.yaml index db470d0d..f3ab1008 100644 --- a/.local-dev/config/ns.yaml +++ b/.local-dev/config/ns.yaml @@ -75,6 +75,15 @@ components: type: traefik traefik: priorityOffset: 0 + middleware: + sablier: + enable: true + url: http://sablier:10000 + sessionDuration: 5m + dynamic: + theme: ghost + blocking: + timeout: 1m tls: certResolver: nsresolver wildcard: diff --git a/cmd/config.go b/cmd/config.go index 64d43bfc..8b41afae 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -167,6 +167,13 @@ func init() { viper.SetDefault("components.controller.docker.ss.url", "") viper.SetDefault("components.controller.docker.routing.type", "traefik") viper.SetDefault("components.controller.docker.routing.traefik.priorityOffset", 0) + + viper.SetDefault("components.controller.docker.middleware.sablier.enable", true) + viper.SetDefault("components.controller.docker.middleware.sablier.url", "http://sablier:10000") + viper.SetDefault("components.controller.docker.middleware.sablier.sessionDuration", "1h") + viper.SetDefault("components.controller.docker.middleware.sablier.dynamic.theme", "ghost") + viper.SetDefault("components.controller.docker.middleware.sablier.blocking.timeout", "1m") + viper.SetDefault("components.controller.docker.tls.certResolver", "nsresolver") viper.SetDefault("components.controller.docker.tls.wildcard.domains", nil) diff --git a/compose.yaml b/compose.yaml index b392befd..21d5eb20 100644 --- a/compose.yaml +++ b/compose.yaml @@ -389,6 +389,14 @@ services: - /var/lib/docker/:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro + sablier: + image: sablierapp/sablier:1.8.1 + command: + - start + - --provider.name=docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + traefik: image: traefik:3.1 restart: always @@ -409,6 +417,8 @@ services: - --ping=true - --metrics.prometheus=true - --metrics.prometheus.entrypoint=metrics + - --experimental.plugins.sablier.modulename=github.com/sablierapp/sablier + - --experimental.plugins.sablier.version=v1.8.1 ports: - "80:80" - "443:443" diff --git a/pkg/infrastructure/backend/dockerimpl/backend.go b/pkg/infrastructure/backend/dockerimpl/backend.go index fb28dfce..00a135cd 100644 --- a/pkg/infrastructure/backend/dockerimpl/backend.go +++ b/pkg/infrastructure/backend/dockerimpl/backend.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/samber/lo" "github.com/traPtitech/neoshowcase/pkg/util/retry" clitypes "github.com/docker/cli/cli/config/types" @@ -180,9 +181,17 @@ func (b *Backend) containerLabels(app *domain.Application) map[string]string { appLabel: "true", appIDLabel: app.ID, appRestartedAtLabel: app.UpdatedAt.Format(time.RFC3339Nano), + "sablier.enable": lo.Ternary(b.useSablier(app), "true", "false"), + "sablier.group": sablierGroupName(app.ID), }) } +func (b *Backend) useSablier(app *domain.Application) bool { + return b.config.Middleware.Sablier.Enable && + app.DeployType == domain.DeployTypeRuntime && + app.Config.BuildConfig.GetRuntimeConfig().AutoShutdown.Enabled +} + func containerName(appID string) string { return fmt.Sprintf("nsapp-%s", appID) } @@ -202,3 +211,11 @@ func stripMiddlewareName(website *domain.Website) string { func ssHeaderMiddlewareName(ss *domain.StaticSite) string { return fmt.Sprintf("nsapp-ss-header-%s", ss.Application.ID) } + +func sablierMiddlewareName(app *domain.Application) string { + return app.ID + "-sablier" +} + +func sablierGroupName(appID string) string { + return fmt.Sprintf("nsapp-%s", appID) +} diff --git a/pkg/infrastructure/backend/dockerimpl/config.go b/pkg/infrastructure/backend/dockerimpl/config.go index 633afb6c..93ded893 100644 --- a/pkg/infrastructure/backend/dockerimpl/config.go +++ b/pkg/infrastructure/backend/dockerimpl/config.go @@ -84,6 +84,25 @@ type Config struct { PriorityOffset int `mapstructure:"priorityOffset" yaml:"priorityOffset"` } `mapstructure:"traefik" yaml:"traefik"` } `mapstructure:"routing" yaml:"routing"` + // Middleware section defines middleware settings. + Middleware struct { + // Sablier (https://github.com/acouvreur/sablier) starts user apps on demand and shuts them down after a certain time. + Sablier struct { + Enable bool `mapstructure:"enable" yaml:"enable"` + SablierURL string `mapstructure:"url" yaml:"url"` + // SessionDuration defines how long the session should last. + // + // Example: "10m" + SessionDuration string `mapstructure:"sessionDuration" yaml:"sessionDuration"` + Dynamic struct { + Theme string `mapstructure:"theme" yaml:"theme"` + } `mapstructure:"dynamic" yaml:"dynamic"` + Blocking struct { + // Timeout defines how long the blocking should last. + Timeout string `mapstructure:"timeout" yaml:"timeout"` + } `mapstructure:"blocking" yaml:"blocking"` + } `mapstructure:"sablier" yaml:"sablier"` + } // TLS section defines tls setting for user app ingress. TLS struct { CertResolver string `mapstructure:"certResolver" yaml:"certResolver"` diff --git a/pkg/infrastructure/backend/dockerimpl/ingress.go b/pkg/infrastructure/backend/dockerimpl/ingress.go index 69fb876e..fe981009 100644 --- a/pkg/infrastructure/backend/dockerimpl/ingress.go +++ b/pkg/infrastructure/backend/dockerimpl/ingress.go @@ -19,7 +19,7 @@ type ( a []any ) -func (b *Backend) routerBase(website *domain.Website, svcName string) (router m, middlewares m) { +func (b *Backend) routerBase(app *domain.Application, website *domain.Website, svcName string) (router m, middlewares m) { middlewares = make(m) var entrypoints []string @@ -42,6 +42,12 @@ func (b *Backend) routerBase(website *domain.Website, svcName string) (router m, log.Warnf("auth config not available for %s", website.FQDN) } + if b.useSablier(app) { + middlewareName := sablierMiddlewareName(app) + middlewareNames = append(middlewareNames, middlewareName) + middlewares[middlewareName] = b.sablierMiddleware(app) + } + var rule string if website.PathPrefix == "/" { rule = fmt.Sprintf("Host(`%s`)", website.FQDN) @@ -99,7 +105,7 @@ func newRuntimeConfigBuilder() *runtimeConfigBuilder { func (b *runtimeConfigBuilder) addWebsite(backend *Backend, app *domain.Application, website *domain.Website) { svcName := traefikName(website) - router, middlewares := backend.routerBase(website, svcName) + router, middlewares := backend.routerBase(app, website, svcName) netName := networkName(app.ID) svc := m{ @@ -153,3 +159,31 @@ func (b *Backend) writeConfig(filename string, config any) error { defer enc.Close() return enc.Encode(config) } + +func (b *Backend) sablierMiddleware(app *domain.Application) m { + // ref: https://sablierapp.dev/#/plugins/traefik?id=configure-the-plugin-using-the-dynamic-configuration + var config = m{ + "sablierUrl": b.config.Middleware.Sablier.SablierURL, + "group": sablierGroupName(app.ID), + "sessionDuration": b.config.Middleware.Sablier.SessionDuration, + } + + switch app.Config.BuildConfig.GetRuntimeConfig().AutoShutdown.Startup { + case domain.StartupBehaviorLoadingPage: + config["dynamic"] = m{ + "displayName": app.Name, + "showDetails": "true", + "theme": b.config.Middleware.Sablier.Dynamic.Theme, + } + case domain.StartupBehaviorBlocking: + config["blocking"] = m{ + "timeout": b.config.Middleware.Sablier.Blocking.Timeout, + } + } + + return m{ + "plugin": m{ + "sablier": config, + }, + } +} diff --git a/pkg/infrastructure/backend/dockerimpl/synchronize_runtime.go b/pkg/infrastructure/backend/dockerimpl/synchronize_runtime.go index fcd6dd97..3d64ea83 100644 --- a/pkg/infrastructure/backend/dockerimpl/synchronize_runtime.go +++ b/pkg/infrastructure/backend/dockerimpl/synchronize_runtime.go @@ -85,8 +85,9 @@ func (b *Backend) syncAppContainer(ctx context.Context, app *domain.RuntimeDesir hostConfig := &container.HostConfig{ PortBindings: make(map[nat.Port][]nat.PortBinding), RestartPolicy: container.RestartPolicy{ - Name: "on-failure", - MaximumRetryCount: 5, + Name: "on-failure", + // sablier stops the container, so we don't need to restart it + MaximumRetryCount: lo.Ternary(b.useSablier(app.App), 0, 5), }, } for _, p := range app.App.PortPublications { diff --git a/pkg/infrastructure/backend/dockerimpl/synchronize_ss.go b/pkg/infrastructure/backend/dockerimpl/synchronize_ss.go index 7d51cf5a..d2350b5c 100644 --- a/pkg/infrastructure/backend/dockerimpl/synchronize_ss.go +++ b/pkg/infrastructure/backend/dockerimpl/synchronize_ss.go @@ -20,7 +20,7 @@ func newSSConfigBuilder() *ssConfigBuilder { } func (b *ssConfigBuilder) addStaticSite(backend *Backend, site *domain.StaticSite) { - router, newMiddlewares := backend.routerBase(site.Website, traefikSSServiceName) + router, newMiddlewares := backend.routerBase(site.Application, site.Website, traefikSSServiceName) for name, mw := range newMiddlewares { b.middlewares[name] = mw }