Skip to content

Commit c5a5f6a

Browse files
tobimadehinclaude
andcommitted
feat(system): replace sudo-exec systemctl with D-Bus API and add cluster setup endpoint
Replaces all `sudo systemctl` shell invocations in proxy, service restart, and reboot paths with direct systemd D-Bus calls via coreos/go-systemd, eliminating runtime sudo dependency. Extracts cgroup v2 slice management into cgroup.go (linux) / cgroup_stub.go (non-linux), wires a POST /clusters/setup HTTP endpoint to provision per-cluster resource slices on demand, and restores existing cluster slices on daemon startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3040673 commit c5a5f6a

13 files changed

Lines changed: 520 additions & 118 deletions

File tree

cmd/dployrd/main.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"syscall"
1515

1616
"github.com/dployr-io/dployr/pkg/auth"
17+
"github.com/dployr-io/dployr/pkg/core/cluster"
1718
"github.com/dployr-io/dployr/pkg/core/deploy"
1819
"github.com/dployr-io/dployr/pkg/core/proxy"
1920
"github.com/dployr-io/dployr/pkg/core/service"
@@ -127,6 +128,9 @@ func main() {
127128
storageMounter := _storage.NewMounter(logger)
128129
storageH := pkgstorage.NewHandler(storageMounter, logger)
129130

131+
clusterSetup := _system.NewClusterSetup()
132+
clusterH := cluster.NewHandler(clusterSetup, logger)
133+
130134
wh := web.WebHandler{
131135
DepsH: dh,
132136
SvcH: sh,
@@ -138,10 +142,33 @@ func main() {
138142
AuthM: am,
139143
MetricsH: mh,
140144
StorageH: storageH,
145+
ClusterH: clusterH,
141146
}
142147

143148
mux := wh.BuildMux(cfg)
144149

150+
// Restore cgroup slices for any clusters that survived a node reprovision.
151+
// On a blank-slate node this is a no-op; slices are created proactively
152+
// via the setup_cluster task sent at cluster assignment time.
153+
if cfg.ClusterMemory > 0 {
154+
if deployments, err := ds.ListDeployments(ctx, 1000, 0); err == nil {
155+
seen := make(map[string]struct{})
156+
for _, d := range deployments {
157+
id := d.Blueprint.ClusterID
158+
if id == "" {
159+
continue
160+
}
161+
if _, ok := seen[id]; ok {
162+
continue
163+
}
164+
seen[id] = struct{}{}
165+
if err := _system.EnsureClusterSlice(id, cfg.ClusterMemory, cfg.ClusterCPU); err != nil {
166+
logger.Warn("failed to ensure cluster slice at startup", "cluster_id", id, "error", err)
167+
}
168+
}
169+
}
170+
}
171+
145172
syncer := _system.NewSyncer(cfg, logger, is, trs, ds, ss, ps, mux, as, fs, workerMaxConcurrent, w.ActiveJobs)
146173
w.SetCompletionHandler(func(id string) {
147174
syncer.RequestFullSync()

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
1313
github.com/Microsoft/go-winio v0.4.14 // indirect
1414
github.com/cespare/xxhash/v2 v2.3.0 // indirect
15+
github.com/cilium/ebpf v0.16.0 // indirect
1516
github.com/containerd/log v0.1.0 // indirect
1617
github.com/distribution/reference v0.6.0 // indirect
1718
github.com/docker/go-units v0.5.0 // indirect
@@ -20,6 +21,7 @@ require (
2021
github.com/go-logr/logr v1.4.3 // indirect
2122
github.com/go-logr/stdr v1.2.2 // indirect
2223
github.com/go-ole/go-ole v1.2.6 // indirect
24+
github.com/godbus/dbus/v5 v5.1.0 // indirect
2325
github.com/gogo/protobuf v1.3.2 // indirect
2426
github.com/google/uuid v1.6.0 // indirect
2527
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -35,6 +37,7 @@ require (
3537
github.com/morikuni/aec v1.1.0 // indirect
3638
github.com/ncruces/go-strftime v0.1.9 // indirect
3739
github.com/opencontainers/go-digest v1.0.0 // indirect
40+
github.com/opencontainers/runtime-spec v1.3.0 // indirect
3841
github.com/pkg/errors v0.9.1 // indirect
3942
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
4043
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -53,13 +56,16 @@ require (
5356
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
5457
golang.org/x/sys v0.42.0 // indirect
5558
golang.org/x/time v0.15.0 // indirect
59+
google.golang.org/protobuf v1.36.11 // indirect
5660
gotest.tools/v3 v3.5.2 // indirect
5761
modernc.org/libc v1.66.10 // indirect
5862
modernc.org/mathutil v1.7.1 // indirect
5963
modernc.org/memory v1.11.0 // indirect
6064
)
6165

6266
require (
67+
github.com/containerd/cgroups/v3 v3.1.3
68+
github.com/coreos/go-systemd/v22 v22.7.0
6369
github.com/creack/pty v1.1.24
6470
github.com/docker/docker v27.5.1+incompatible
6571
github.com/docker/go-connections v0.5.0

go.sum

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
1010
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
1111
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
1212
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
13+
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
14+
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
1315
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
1416
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
17+
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ=
18+
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
1519
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
1620
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
21+
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
22+
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
1723
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
1824
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
1925
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -41,6 +47,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
4147
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
4248
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
4349
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
50+
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
51+
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
52+
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
53+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
4454
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
4555
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
4656
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
@@ -56,15 +66,27 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz
5666
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
5767
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
5868
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
69+
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
70+
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
71+
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
72+
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
5973
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
6074
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
6175
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
6276
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
6377
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
78+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
79+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
80+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
81+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
6482
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
6583
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
6684
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
6785
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
86+
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
87+
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
88+
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
89+
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
6890
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
6991
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
7092
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
@@ -87,6 +109,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
87109
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
88110
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
89111
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
112+
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
113+
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
90114
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
91115
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
92116
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -97,6 +121,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
97121
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
98122
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
99123
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
124+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
125+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
100126
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
101127
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
102128
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
@@ -145,6 +171,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
145171
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
146172
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
147173
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
174+
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
175+
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
148176
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
149177
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
150178
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

internal/proxy/caddy.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
package proxy
55

66
import (
7+
"context"
78
"embed"
89
"encoding/json"
910
"fmt"
1011
"net/http"
1112
"os"
1213
"os/exec"
14+
15+
systemddbus "github.com/coreos/go-systemd/v22/dbus"
1316
"path/filepath"
1417
"strings"
1518
"text/template"
@@ -130,11 +133,17 @@ func (c *CaddyHandler) Setup(apps map[string]proxy.App) error {
130133

131134
func (c *CaddyHandler) Stop() error {
132135
c.logger.Info("stopping caddy service")
133-
stop := exec.Command("sudo", "systemctl", "stop", "caddy")
134-
if output, err := stop.CombinedOutput(); err != nil {
135-
c.logger.Warn("failed to stop caddy service", "output", string(output), "error", err)
136+
conn, err := systemddbus.NewSystemConnectionContext(context.TODO())
137+
if err != nil {
138+
return fmt.Errorf("connect to systemd dbus: %w", err)
139+
}
140+
defer conn.Close()
141+
142+
ch := make(chan string, 1)
143+
if _, err := conn.StopUnitContext(context.TODO(), "caddy.service", "replace", ch); err != nil {
136144
return fmt.Errorf("failed to stop caddy service: %w", err)
137145
}
146+
<-ch
138147
c.logger.Info("caddy service stopped")
139148
return nil
140149
}
@@ -194,11 +203,17 @@ func (c *CaddyHandler) Restart() error {
194203
c.logger.Debug("caddy config validation successful")
195204

196205
c.logger.Info("restarting caddy service")
197-
restart := exec.Command("sudo", "systemctl", "restart", "caddy")
198-
if output, err := restart.CombinedOutput(); err != nil {
199-
c.logger.Error("failed to restart caddy service", "output", string(output), "error", err)
206+
conn, err := systemddbus.NewSystemConnectionContext(context.TODO())
207+
if err != nil {
208+
return fmt.Errorf("connect to systemd dbus: %w", err)
209+
}
210+
defer conn.Close()
211+
212+
ch := make(chan string, 1)
213+
if _, err := conn.RestartUnitContext(context.TODO(), "caddy.service", "replace", ch); err != nil {
200214
return fmt.Errorf("failed to restart caddy service: %w", err)
201215
}
216+
<-ch
202217

203218
// verify it's actually running by checking admin API
204219
maxRetries := 10

internal/scripts/deploy_app.sh

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,33 +63,35 @@ ensure_cluster_slice() {
6363
local slice_name="dployr-cluster-${CLUSTER_ID}.slice"
6464
local slice_file="/etc/systemd/system/${slice_name}"
6565

66-
if [ ! -f "$slice_file" ]; then
67-
local cluster_memory cluster_cpu
68-
cluster_memory=$(sysget 'cluster_memory')
69-
cluster_cpu=$(sysget 'cluster_cpu')
70-
71-
local mem_line="" cpu_line=""
72-
if [ "${cluster_memory:-0}" -gt 0 ] 2>/dev/null; then
73-
mem_line="MemoryMax=${cluster_memory}M"
74-
fi
75-
if [ "${cluster_cpu:-0}" -gt 0 ] 2>/dev/null; then
76-
# millicores → CPUQuota% (100m = 10%, 250m = 25%)
77-
local cpu_pct=$(( cluster_cpu / 10 ))
78-
cpu_line="CPUQuota=${cpu_pct}%"
79-
fi
66+
local cluster_memory cluster_cpu
67+
cluster_memory=$(sysget 'cluster_memory')
68+
cluster_cpu=$(sysget 'cluster_cpu')
8069

81-
sudo -n tee "$slice_file" > /dev/null << EOF
82-
[Unit]
83-
Description=dployr cluster ${CLUSTER_ID}
84-
Before=slices.target
70+
local mem_line="" cpu_line=""
71+
if [ "${cluster_memory:-0}" -gt 0 ] 2>/dev/null; then
72+
mem_line="MemoryMax=${cluster_memory}M"
73+
fi
74+
if [ "${cluster_cpu:-0}" -gt 0 ] 2>/dev/null; then
75+
# millicores → CPUQuota% (100m = 10%, 250m = 25%)
76+
local cpu_pct=$(( cluster_cpu / 10 ))
77+
cpu_line="CPUQuota=${cpu_pct}%"
78+
fi
8579

86-
[Slice]
87-
${mem_line}
88-
${cpu_line}
89-
EOF
80+
# Build desired unit content and only reload systemd when it has changed.
81+
# This makes the function idempotent across repeated deploys and also picks
82+
# up limit changes from tier upgrades/downgrades automatically.
83+
local desired
84+
desired=$(printf '[Unit]\nDescription=dployr cluster %s\nBefore=slices.target\n\n[Slice]\n%s\n%s\n' \
85+
"$CLUSTER_ID" "$mem_line" "$cpu_line")
86+
87+
local current
88+
current=$(cat "$slice_file" 2>/dev/null || echo "")
89+
90+
if [ "$current" != "$desired" ]; then
91+
printf '%s\n' "$desired" | sudo -n tee "$slice_file" > /dev/null
9092
sudo -n systemctl daemon-reload 2>/dev/null || true
9193
sudo -n systemctl start "${slice_name}" 2>/dev/null || true
92-
log "Cluster slice created: ${slice_name} (${mem_line} ${cpu_line})"
94+
log "Cluster slice updated: ${slice_name} (${mem_line:-no-mem-limit} ${cpu_line:-no-cpu-limit})"
9395
fi
9496

9597
echo "$slice_name"

0 commit comments

Comments
 (0)