diff --git a/cmd/sablier/cmd.go b/cmd/sablier/cmd.go index 583fe8da..44930382 100644 --- a/cmd/sablier/cmd.go +++ b/cmd/sablier/cmd.go @@ -3,16 +3,17 @@ package main import ( "errors" "fmt" + "log/slog" + "os" + "strings" + "time" + "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" ) const ( @@ -56,6 +57,8 @@ It provides an integrations with multiple reverse proxies and different loading viper.BindPFlag("provider.kubernetes.burst", startCmd.Flags().Lookup("provider.kubernetes.burst")) startCmd.Flags().StringVar(&conf.Provider.Kubernetes.Delimiter, "provider.kubernetes.delimiter", "_", "Delimiter used for namespace/resource type/name resolution. Defaults to \"_\" for backward compatibility. But you should use \"/\" or \".\"") viper.BindPFlag("provider.kubernetes.delimiter", startCmd.Flags().Lookup("provider.kubernetes.delimiter")) + startCmd.Flags().BoolVar(&conf.Provider.Systemd.UserInstance, "provider.systemd.userinstance", false, "Whether to use systemd user instance. Defaults to false, which means it will use systemd system instance.") + viper.BindPFlag("provider.systemd.userinstance", startCmd.Flags().Lookup("provider.systemd.userinstance")) // Server flags startCmd.Flags().IntVar(&conf.Server.Port, "server.port", 10000, "The server port to use") viper.BindPFlag("server.port", startCmd.Flags().Lookup("server.port")) diff --git a/cmd/sablier/provider.go b/cmd/sablier/provider.go index 191898d0..f4fcbbc4 100644 --- a/cmd/sablier/provider.go +++ b/cmd/sablier/provider.go @@ -3,15 +3,18 @@ package main import ( "context" "fmt" + "log/slog" + + "github.com/coreos/go-systemd/v22/dbus" "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/provider/systemd" "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) { @@ -45,6 +48,19 @@ func setupProvider(ctx context.Context, logger *slog.Logger, config config.Provi return nil, err } return kubernetes.New(ctx, cli, logger, config.Kubernetes) + case "systemd": + var con *dbus.Conn + var err error + if config.Systemd.UserInstance { + con, err = dbus.NewUserConnectionContext(ctx) + } else { + con, err = dbus.NewSystemConnectionContext(ctx) + } + if err != nil { + return nil, err + } + + return systemd.New(ctx, con, logger) } return nil, fmt.Errorf("unimplemented provider %s", config.Name) } diff --git a/go.mod b/go.mod index 06cae059..7ec74dd5 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,8 @@ require ( k8s.io/client-go v0.33.2 ) +require github.com/coreos/go-systemd/v22 v22.5.0 + require ( dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect @@ -87,6 +89,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -189,6 +192,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/go.sum b/go.sum index 55a6c9ae..2d958400 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -149,6 +151,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= @@ -491,6 +495,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/go.work.sum b/go.work.sum index f83d98f0..d0ea5c4d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -508,6 +508,7 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -541,6 +542,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -557,6 +559,7 @@ github.com/google/go-dap v0.12.0/go.mod h1:tNjCASCm5cqePi/RVXXWEVqtnNLV1KTWtYOqu github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= @@ -712,6 +715,9 @@ github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8 github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= @@ -1047,6 +1053,7 @@ golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1057,6 +1064,7 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1065,6 +1073,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/config/provider.go b/pkg/config/provider.go index f8cc11bb..f837ac05 100644 --- a/pkg/config/provider.go +++ b/pkg/config/provider.go @@ -11,6 +11,7 @@ type Provider struct { Name string `mapstructure:"NAME" yaml:"name,omitempty" default:"docker"` AutoStopOnStartup bool `yaml:"auto-stop-on-startup,omitempty" default:"true"` Kubernetes Kubernetes + Systemd Systemd } type Kubernetes struct { @@ -22,7 +23,12 @@ type Kubernetes struct { Delimiter string `mapstructure:"DELIMITER" yaml:"Delimiter" default:"_"` } -var providers = []string{"docker", "docker_swarm", "swarm", "kubernetes"} +type Systemd struct { + // Use systemd user instance + UserInstance bool `mapstructure:"userInstance" yaml:"userInstance" default:"false"` +} + +var providers = []string{"docker", "docker_swarm", "swarm", "kubernetes", "systemd"} func NewProviderConfig() Provider { return Provider{ diff --git a/pkg/provider/systemd/events.go b/pkg/provider/systemd/events.go new file mode 100644 index 00000000..02fbf833 --- /dev/null +++ b/pkg/provider/systemd/events.go @@ -0,0 +1,49 @@ +package systemd + +import ( + "context" + "errors" + "io" + "log/slog" +) + +func (p *Provider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { + + err := p.Con.Subscribe() + if err != nil { + p.l.ErrorContext(ctx, "failed setting up systemd subscription", slog.Any("error", err)) + } + + msgs, errs := p.Con.SubscribeUnits(0) + + for { + select { + case msg, ok := <-msgs: + if !ok { + p.l.ErrorContext(ctx, "event stream closed") + close(instance) + return + } + for name, status := range msg { + if status == nil || status.ActiveState != "active" { + instance <- name + } + } + case err, ok := <-errs: + if !ok { + p.l.ErrorContext(ctx, "event stream closed") + close(instance) + return + } + if errors.Is(err, io.EOF) { + p.l.ErrorContext(ctx, "event stream closed") + close(instance) + return + } + p.l.ErrorContext(ctx, "event stream error", slog.Any("error", err)) + case <-ctx.Done(): + close(instance) + return + } + } +} diff --git a/pkg/provider/systemd/systemd.go b/pkg/provider/systemd/systemd.go new file mode 100644 index 00000000..fc034276 --- /dev/null +++ b/pkg/provider/systemd/systemd.go @@ -0,0 +1,66 @@ +package systemd + +import ( + "context" + "fmt" + "log/slog" + + "github.com/coreos/go-systemd/v22/dbus" + "github.com/sablierapp/sablier/pkg/sablier" + "gopkg.in/ini.v1" +) + +// Interface guard +var _ sablier.Provider = (*Provider)(nil) + +type Provider struct { + Con *dbus.Conn + desiredReplicas int32 + l *slog.Logger +} + +func New(ctx context.Context, con *dbus.Conn, logger *slog.Logger) (*Provider, error) { + logger = logger.With(slog.String("provider", "systemd")) + + connected := con.Connected() + if !connected { + return nil, fmt.Errorf("no connection to systemd dbus") + } + + logger.InfoContext(ctx, "connection established with systemd dbus") + return &Provider{ + Con: con, + l: logger, + }, nil +} + +func (p *Provider) parseSablierProperties(unitStatus dbus.UnitStatus) (map[string]string, error) { + dbusProps, err := p.Con.GetUnitPropertiesContext(context.Background(), unitStatus.Name) + if err != nil { + return nil, err + } + + sourcePath, ok := dbusProps["SourcePath"].(string) + if !ok || sourcePath == "" { + // Not a unit we could start or stop + return nil, nil + } + + cfg, err := ini.Load(sourcePath) + if err != nil { + return nil, err + } + + section, err := cfg.GetSection("X-Sablier") + if err != nil { + // No sablier props found + return nil, nil + } + + props := make(map[string]string) + for _, key := range section.Keys() { + props[key.Name()] = key.Value() + } + + return props, nil +} diff --git a/pkg/provider/systemd/unit_inspect.go b/pkg/provider/systemd/unit_inspect.go new file mode 100644 index 00000000..60d357bc --- /dev/null +++ b/pkg/provider/systemd/unit_inspect.go @@ -0,0 +1,49 @@ +package systemd + +import ( + "context" + "fmt" + + "github.com/sablierapp/sablier/pkg/sablier" +) + +func (p *Provider) InstanceInspect(ctx context.Context, name string) (sablier.InstanceInfo, error) { + unit, err := p.getUnit(ctx, name) + if err != nil { + return sablier.InstanceInfo{}, fmt.Errorf("cannot inspect systemd unit: %w", err) + } + + // "active", "inactive", "failed", "activating", "deactivating", "maintenance", "reloading" or "refreshing" + switch unit.status.ActiveState { + case "inactive", "deactivating", "maintenance", "reloading", "refreshing": + return sablier.NotReadyInstanceState(name, 0, p.desiredReplicas), nil + case "active": + return sablier.ReadyInstanceState(name, p.desiredReplicas), nil + case "failed": + return sablier.UnrecoverableInstanceState(name, fmt.Sprintf("system unit failed"), p.desiredReplicas), nil + default: + return sablier.UnrecoverableInstanceState(name, fmt.Sprintf("systemd unit status \"%s\" not handled", unit.status.ActiveState), p.desiredReplicas), nil + } +} + +func (p *Provider) getUnit(ctx context.Context, name string) (Unit, error) { + unitStatuses, err := p.Con.ListUnitsByNamesContext(ctx, []string{name}) + if err != nil { + return Unit{}, err + } + + if len(unitStatuses) == 0 { + return Unit{}, fmt.Errorf("unit %s not found", name) + } + + unitStatus := unitStatuses[0] + props, err := p.parseSablierProperties(unitStatus) + if err != nil { + return Unit{}, err + } + + return Unit{ + status: unitStatus, + props: props, + }, nil +} diff --git a/pkg/provider/systemd/unit_list.go b/pkg/provider/systemd/unit_list.go new file mode 100644 index 00000000..23bbeb4e --- /dev/null +++ b/pkg/provider/systemd/unit_list.go @@ -0,0 +1,97 @@ +package systemd + +import ( + "context" + + "github.com/coreos/go-systemd/v22/dbus" + "github.com/sablierapp/sablier/pkg/provider" + "github.com/sablierapp/sablier/pkg/sablier" +) + +func (p *Provider) InstanceList(ctx context.Context, options provider.InstanceListOptions) ([]sablier.InstanceConfiguration, error) { + units, err := p.listUnits(ctx, options) + if err != nil { + return nil, err + } + + instances := make([]sablier.InstanceConfiguration, 0, len(units)) + for _, u := range units { + instances = append(instances, unitToInstance(u)) + } + + return instances, nil +} + +func unitToInstance(u Unit) sablier.InstanceConfiguration { + var group string + + if _, ok := u.props["Enable"]; ok { + if g, ok := u.props["Group"]; ok { + group = g + } else { + group = "default" + } + } + + return sablier.InstanceConfiguration{ + Name: u.status.Name, + Group: group, + } +} + +func (p *Provider) InstanceGroups(ctx context.Context) (map[string][]string, error) { + units, err := p.listUnits(ctx, provider.InstanceListOptions{ + All: true, + }) + + if err != nil { + return nil, err + } + + groups := make(map[string][]string) + for _, u := range units { + groupName := u.props["Group"] + if len(groupName) == 0 { + groupName = "default" + } + group := groups[groupName] + group = append(group, u.status.Name) + groups[groupName] = group + } + + return groups, nil +} + +func (p *Provider) listUnits(ctx context.Context, options provider.InstanceListOptions) ([]Unit, error) { + var unitStatuses []dbus.UnitStatus + var err error + if options.All { + unitStatuses, err = p.Con.ListUnitsContext(ctx) + } else { + unitStatuses, err = p.Con.ListUnitsFilteredContext(ctx, []string{"active"}) + } + if err != nil { + return nil, err + } + + units := make([]Unit, 0) + for _, unitStatus := range unitStatuses { + sablierProps, err := p.parseSablierProperties(unitStatus) + if err != nil { + return nil, err + } + if sablierProps["Enable"] == "true" { + units = append(units, Unit{ + status: unitStatus, + props: sablierProps, + }) + } + } + + return units, nil +} + +type Unit struct { + status dbus.UnitStatus + props map[string]string +} diff --git a/pkg/provider/systemd/unit_start.go b/pkg/provider/systemd/unit_start.go new file mode 100644 index 00000000..e2008837 --- /dev/null +++ b/pkg/provider/systemd/unit_start.go @@ -0,0 +1,33 @@ +package systemd + +import ( + "context" + "fmt" + "log/slog" +) + +func (p *Provider) InstanceStart(ctx context.Context, name string) error { + p.l.DebugContext(ctx, "starting systemd unit", slog.String("name", name)) + + ch := make(chan string, 1) + defer close(ch) + + _, err := p.Con.StartUnitContext(ctx, name, "replace", ch) + if err != nil { + p.l.ErrorContext(ctx, "cannot start systemd unit", slog.String("name", name), slog.Any("error", err)) + return fmt.Errorf("cannot start systemd unit %s: %w", name, err) + } + select { + case <-ctx.Done(): + p.l.ErrorContext(ctx, "context cancelled while waiting for systemd unit to start", slog.String("name", name)) + return ctx.Err() + case status := <-ch: + if status == "done" { + p.l.DebugContext(ctx, "systemd unit started", slog.String("name", name)) + return nil + } + p.l.ErrorContext(ctx, "systemd unit failed to start", slog.String("name", name), slog.String("status", status)) + return fmt.Errorf("systemd unit %s failed to start: %s", name, status) + } + +} diff --git a/pkg/provider/systemd/unit_stop.go b/pkg/provider/systemd/unit_stop.go new file mode 100644 index 00000000..fd67317a --- /dev/null +++ b/pkg/provider/systemd/unit_stop.go @@ -0,0 +1,33 @@ +package systemd + +import ( + "context" + "fmt" + "log/slog" +) + +func (p *Provider) InstanceStop(ctx context.Context, name string) error { + p.l.DebugContext(ctx, "stopping systemd unit", slog.String("name", name)) + + ch := make(chan string, 1) + defer close(ch) + + _, err := p.Con.StopUnitContext(ctx, name, "replace", ch) + if err != nil { + p.l.ErrorContext(ctx, "cannot stop systemd unit", slog.String("name", name), slog.Any("error", err)) + return fmt.Errorf("cannot stop systemd unit %s: %w", name, err) + } + select { + case <-ctx.Done(): + p.l.ErrorContext(ctx, "context cancelled while waiting for systemd unit to stop", slog.String("name", name)) + return ctx.Err() + case status := <-ch: + if status == "done" { + p.l.DebugContext(ctx, "systemd unit stopped", slog.String("name", name)) + return nil + } + p.l.ErrorContext(ctx, "systemd unit failed to stop", slog.String("name", name), slog.String("status", status)) + return fmt.Errorf("systemd unit %s failed to stop: %s", name, status) + } + +}