forked from Nixius/authelia
159 lines
4.6 KiB
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
|
|
}
|