Skip to content

Commit c66297d

Browse files
feat: add interactive email configuration wizard for Authentik
- Added structured email configuration wizard with validation and user prompts - Implemented backup and restore functionality for .env file modifications - Added container restart handling to apply email configuration changes - Enhanced error handling with detailed user feedback and recovery options - Added documentation for email configuration constants and rationale - Improved security by requiring root access for email configuration changes
1 parent d1933af commit c66297d

7 files changed

Lines changed: 1068 additions & 99 deletions

File tree

pkg/docker/compose_restart.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// pkg/docker/compose_restart.go
2+
// Docker Compose service restart operations using Docker SDK
3+
// RATIONALE: Restart specific services in a Compose project with health checks
4+
// ARCHITECTURE: Uses Docker SDK (NOT shell commands) for better error handling
5+
6+
package docker
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"time"
12+
13+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
14+
containertypes "github.com/docker/docker/api/types/container"
15+
"github.com/docker/docker/api/types/filters"
16+
"github.com/docker/docker/client"
17+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
18+
"go.uber.org/zap"
19+
)
20+
21+
// RestartComposeServicesConfig controls compose service restart behavior
22+
type RestartComposeServicesConfig struct {
23+
ProjectName string // Docker Compose project name (e.g., "hecate")
24+
ServiceNames []string // Services to restart (e.g., ["server", "worker"])
25+
Timeout time.Duration // Stop timeout before forceful kill
26+
HealthCheck bool // Wait for containers to reach "running" state
27+
}
28+
29+
// RestartComposeServices restarts specific services in a Docker Compose project
30+
// ASSESS: Find containers by project and service labels
31+
// INTERVENE: Restart containers with timeout
32+
// EVALUATE: Wait for running state, verify health
33+
func RestartComposeServices(rc *eos_io.RuntimeContext, cfg *RestartComposeServicesConfig) error {
34+
logger := otelzap.Ctx(rc.Ctx)
35+
36+
if cfg == nil {
37+
return fmt.Errorf("config is required")
38+
}
39+
40+
if cfg.ProjectName == "" {
41+
return fmt.Errorf("project name is required")
42+
}
43+
44+
if len(cfg.ServiceNames) == 0 {
45+
return fmt.Errorf("at least one service name is required")
46+
}
47+
48+
if cfg.Timeout == 0 {
49+
cfg.Timeout = 30 * time.Second
50+
}
51+
52+
logger.Info("Restarting Docker Compose services",
53+
zap.String("project", cfg.ProjectName),
54+
zap.Strings("services", cfg.ServiceNames),
55+
zap.Duration("timeout", cfg.Timeout),
56+
zap.Bool("health_check", cfg.HealthCheck))
57+
58+
// Create Docker client
59+
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
60+
if err != nil {
61+
return fmt.Errorf("failed to create Docker client: %w", err)
62+
}
63+
defer cli.Close()
64+
65+
// ASSESS: Find containers by compose project label
66+
projectFilter := filters.NewArgs(
67+
filters.Arg("label", fmt.Sprintf("com.docker.compose.project=%s", cfg.ProjectName)),
68+
)
69+
70+
containers, err := cli.ContainerList(rc.Ctx, containertypes.ListOptions{
71+
All: true, // Include stopped containers
72+
Filters: projectFilter,
73+
})
74+
if err != nil {
75+
return fmt.Errorf("failed to list containers for project %s: %w", cfg.ProjectName, err)
76+
}
77+
78+
if len(containers) == 0 {
79+
return fmt.Errorf("no containers found for project %s", cfg.ProjectName)
80+
}
81+
82+
logger.Debug("Found containers in compose project",
83+
zap.String("project", cfg.ProjectName),
84+
zap.Int("total_containers", len(containers)))
85+
86+
// ASSESS: Filter containers by service name
87+
var toRestart []containertypes.Summary
88+
for _, c := range containers {
89+
serviceName := c.Labels["com.docker.compose.service"]
90+
for _, targetService := range cfg.ServiceNames {
91+
if serviceName == targetService {
92+
toRestart = append(toRestart, c)
93+
logger.Debug("Service container matched for restart",
94+
zap.String("service", serviceName),
95+
zap.String("container_id", c.ID[:12]),
96+
zap.String("container_name", c.Names[0]))
97+
break
98+
}
99+
}
100+
}
101+
102+
if len(toRestart) == 0 {
103+
return fmt.Errorf("no containers found for services %v in project %s", cfg.ServiceNames, cfg.ProjectName)
104+
}
105+
106+
logger.Info("Found containers to restart",
107+
zap.Int("count", len(toRestart)),
108+
zap.String("project", cfg.ProjectName))
109+
110+
// INTERVENE: Restart each container
111+
timeoutSeconds := int(cfg.Timeout.Seconds())
112+
restartedIDs := make([]string, 0, len(toRestart))
113+
114+
for _, c := range toRestart {
115+
serviceName := c.Labels["com.docker.compose.service"]
116+
containerName := c.Names[0] // First name is primary
117+
118+
logger.Info("Restarting container",
119+
zap.String("service", serviceName),
120+
zap.String("container", containerName),
121+
zap.Int("timeout_seconds", timeoutSeconds))
122+
123+
stopOptions := containertypes.StopOptions{
124+
Timeout: &timeoutSeconds,
125+
}
126+
127+
if err := cli.ContainerRestart(rc.Ctx, c.ID, stopOptions); err != nil {
128+
return fmt.Errorf("failed to restart container %s (service: %s): %w", containerName, serviceName, err)
129+
}
130+
131+
logger.Info("Container restarted successfully",
132+
zap.String("service", serviceName),
133+
zap.String("container", containerName))
134+
135+
restartedIDs = append(restartedIDs, c.ID)
136+
}
137+
138+
// EVALUATE: Wait for containers to be healthy
139+
if cfg.HealthCheck {
140+
logger.Info("Waiting for containers to be healthy",
141+
zap.Int("count", len(restartedIDs)))
142+
143+
// Wait a few seconds for containers to start
144+
time.Sleep(5 * time.Second)
145+
146+
for _, containerID := range restartedIDs {
147+
if err := waitForContainerRunning(rc.Ctx, cli, containerID, 30*time.Second); err != nil {
148+
// Find original container info for better error message
149+
var serviceName, containerName string
150+
for _, c := range toRestart {
151+
if c.ID == containerID {
152+
serviceName = c.Labels["com.docker.compose.service"]
153+
containerName = c.Names[0]
154+
break
155+
}
156+
}
157+
158+
return fmt.Errorf("container %s (service: %s) failed to start: %w", containerName, serviceName, err)
159+
}
160+
}
161+
162+
logger.Info("All containers are running and healthy",
163+
zap.Int("count", len(restartedIDs)))
164+
}
165+
166+
logger.Info("Docker Compose services restarted successfully",
167+
zap.String("project", cfg.ProjectName),
168+
zap.Strings("services", cfg.ServiceNames))
169+
170+
return nil
171+
}
172+
173+
// waitForContainerRunning polls container state until running or timeout
174+
// EVALUATE: Verify container reached healthy state
175+
func waitForContainerRunning(ctx context.Context, cli *client.Client, containerID string, timeout time.Duration) error {
176+
deadline := time.Now().Add(timeout)
177+
178+
for time.Now().Before(deadline) {
179+
inspect, err := cli.ContainerInspect(ctx, containerID)
180+
if err != nil {
181+
return fmt.Errorf("failed to inspect container: %w", err)
182+
}
183+
184+
if inspect.State.Running {
185+
return nil
186+
}
187+
188+
// If container is in a terminal state (not running and not starting), fail fast
189+
if inspect.State.Status != "created" && inspect.State.Status != "restarting" {
190+
return fmt.Errorf("container in unexpected state: %s", inspect.State.Status)
191+
}
192+
193+
// Wait before retrying
194+
time.Sleep(1 * time.Second)
195+
}
196+
197+
return fmt.Errorf("timeout waiting for container to start after %s", timeout)
198+
}

0 commit comments

Comments
 (0)