forked from Nixius/authelia
1
0
Fork 0
ATLAS/docker/ss-atlas/internal/swarm/client.go

159 lines
4.6 KiB
Go

package swarm
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"text/template"
"git.nixc.us/a250/ss-atlas/internal/config"
)
type Client struct {
cfg *config.Config
}
func New(cfg *config.Config) *Client {
return &Client{cfg: cfg}
}
func (c *Client) DeployStack(stackName, username, domain string) error {
tmplPath := filepath.Join(c.cfg.TemplatePath, "stack-template.yml")
tmplBytes, err := os.ReadFile(tmplPath)
if err != nil {
return fmt.Errorf("read stack template: %w", err)
}
t, err := template.New("stack").Parse(string(tmplBytes))
if err != nil {
return fmt.Errorf("parse stack template: %w", err)
}
data := map[string]any{
"ID": username,
"Subdomain": username,
"Domain": domain,
"TraefikNetwork": c.cfg.TraefikNetwork,
}
var rendered bytes.Buffer
if err := t.Execute(&rendered, data); err != nil {
return fmt.Errorf("render stack template: %w", err)
}
tmpFile := filepath.Join(os.TempDir(), stackName+".yml")
if err := os.WriteFile(tmpFile, rendered.Bytes(), 0600); err != nil {
return fmt.Errorf("write temp stack file: %w", err)
}
defer os.Remove(tmpFile)
cmd := exec.Command("docker", "stack", "deploy",
"-c", tmpFile,
stackName,
)
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("docker stack deploy: %s: %w", strings.TrimSpace(string(output)), err)
}
log.Printf("deployed stack %s: %s", stackName, strings.TrimSpace(string(output)))
// Force-restart the web service so Traefik picks up label changes immediately.
// Traefik reads labels from running task containers, not service specs, so a
// task restart is required for routing changes to take effect.
forceCmd := exec.Command("docker", "service", "update", "--force", stackName+"_web")
forceCmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
if forceOut, forceErr := forceCmd.CombinedOutput(); forceErr != nil {
log.Printf("warn: force-update %s_web: %s", stackName, strings.TrimSpace(string(forceOut)))
}
return nil
}
func (c *Client) RemoveStack(stackName string) error {
cmd := exec.Command("docker", "stack", "rm", stackName)
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("docker stack rm: %s: %w", strings.TrimSpace(string(output)), err)
}
log.Printf("removed stack %s: %s", stackName, strings.TrimSpace(string(output)))
return nil
}
func (c *Client) StackExists(stackName string) (bool, error) {
cmd := exec.Command("docker", "stack", "ls", "--format", "{{.Name}}")
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
output, err := cmd.CombinedOutput()
if err != nil {
return false, fmt.Errorf("docker stack ls: %w", err)
}
for _, line := range strings.Split(string(output), "\n") {
if strings.TrimSpace(line) == stackName {
return true, nil
}
}
return false, nil
}
// GetWebReplicas returns the desired replica count for <stackName>_web.
// Returns 0 if the service does not exist.
func (c *Client) GetWebReplicas(stackName string) (int, error) {
cmd := exec.Command("docker", "service", "inspect",
"--format", "{{.Spec.Mode.Replicated.Replicas}}",
stackName+"_web",
)
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
output, err := cmd.CombinedOutput()
if err != nil {
return 0, nil
}
n, err := strconv.Atoi(strings.TrimSpace(string(output)))
if err != nil {
return 0, nil
}
return n, nil
}
// ScaleStack sets the desired replica count for <stackName>_web.
func (c *Client) ScaleStack(stackName string, replicas int) error {
svc := fmt.Sprintf("%s_web=%d", stackName, replicas)
cmd := exec.Command("docker", "service", "scale", svc)
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("scale stack %s: %s: %w", stackName, strings.TrimSpace(string(output)), err)
}
log.Printf("scaled %s_web to %d: %s", stackName, replicas, strings.TrimSpace(string(output)))
return nil
}
// RestartStack force-restarts the web service, triggering a fresh container.
func (c *Client) RestartStack(stackName string) error {
cmd := exec.Command("docker", "service", "update", "--force", stackName+"_web")
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("restart stack %s: %s: %w", stackName, strings.TrimSpace(string(output)), err)
}
log.Printf("restarted %s_web: %s", stackName, strings.TrimSpace(string(output)))
return nil
}