Skip to content
This repository was archived by the owner on Sep 9, 2025. It is now read-only.

Commit 353bd32

Browse files
committed
feat: compose deployments
1 parent 928cc8a commit 353bd32

File tree

11 files changed

+1239
-93
lines changed

11 files changed

+1239
-93
lines changed

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.0
1+
1.1.0

internal/agent/agent.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,20 @@ type Agent struct {
2828
func New(cfg *config.Config) *Agent {
2929
ctx, cancel := context.WithCancel(context.Background())
3030

31-
agent := &Agent{
32-
config: cfg,
33-
ctx: ctx,
34-
cancel: cancel,
35-
shutdown: make(chan struct{}),
36-
startTime: time.Now(),
37-
}
38-
39-
// Initialize components in correct order
40-
agent.dockerClient = docker.NewClient()
41-
agent.taskManager = tasks.NewManager(agent.dockerClient)
42-
agent.httpClient = NewHTTPClient(cfg, agent.taskManager)
31+
dockerClient := docker.NewClient()
32+
taskManager := tasks.NewManager(dockerClient, cfg)
33+
httpClient := NewHTTPClient(cfg, taskManager)
4334

44-
return agent
35+
return &Agent{
36+
config: cfg,
37+
httpClient: httpClient,
38+
dockerClient: dockerClient,
39+
taskManager: taskManager,
40+
ctx: ctx,
41+
cancel: cancel,
42+
shutdown: make(chan struct{}),
43+
startTime: time.Now(),
44+
}
4545
}
4646

4747
func (a *Agent) Start() error {

internal/agent/http_client_test.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import (
1616

1717
func TestNewHTTPClient(t *testing.T) {
1818
cfg := &config.Config{
19-
ArcaneHost: "localhost",
20-
ArcanePort: 3000,
21-
AgentID: "test-agent",
22-
TLSEnabled: false,
19+
ArcaneHost: "localhost",
20+
ArcanePort: 3000,
21+
AgentID: "test-agent",
22+
TLSEnabled: false,
23+
ComposeBasePath: "/opt/compose-projects", // Add this
2324
}
2425

2526
dockerClient := docker.NewClient()
26-
taskManager := tasks.NewManager(dockerClient)
27+
taskManager := tasks.NewManager(dockerClient, cfg) // Pass config
2728
httpClient := NewHTTPClient(cfg, taskManager)
2829

2930
if httpClient == nil {
@@ -46,14 +47,15 @@ func TestNewHTTPClient(t *testing.T) {
4647

4748
func TestNewHTTPClientWithTLS(t *testing.T) {
4849
cfg := &config.Config{
49-
ArcaneHost: "example.com",
50-
ArcanePort: 443,
51-
AgentID: "test-agent",
52-
TLSEnabled: true,
50+
ArcaneHost: "example.com",
51+
ArcanePort: 443,
52+
AgentID: "test-agent",
53+
TLSEnabled: true,
54+
ComposeBasePath: "/opt/compose-projects", // Add this
5355
}
5456

5557
dockerClient := docker.NewClient()
56-
taskManager := tasks.NewManager(dockerClient)
58+
taskManager := tasks.NewManager(dockerClient, cfg) // Pass config
5759
httpClient := NewHTTPClient(cfg, taskManager)
5860

5961
expectedURL := "https://example.com:443"
@@ -78,14 +80,15 @@ func TestHTTPClientMakeRequest(t *testing.T) {
7880
defer server.Close()
7981

8082
cfg := &config.Config{
81-
ArcaneHost: "localhost",
82-
ArcanePort: 3000,
83-
AgentID: "test-agent",
84-
TLSEnabled: false,
83+
ArcaneHost: "localhost",
84+
ArcanePort: 3000,
85+
AgentID: "test-agent",
86+
TLSEnabled: false,
87+
ComposeBasePath: "/opt/compose-projects", // Add this
8588
}
8689

8790
dockerClient := docker.NewClient()
88-
taskManager := tasks.NewManager(dockerClient)
91+
taskManager := tasks.NewManager(dockerClient, cfg) // Pass config
8992
httpClient := NewHTTPClient(cfg, taskManager)
9093

9194
// Override baseURL to use test server
@@ -154,7 +157,7 @@ func TestHTTPClientStart(t *testing.T) {
154157
}
155158

156159
dockerClient := docker.NewClient()
157-
taskManager := tasks.NewManager(dockerClient)
160+
taskManager := tasks.NewManager(dockerClient, cfg) // Pass config
158161
httpClient := NewHTTPClient(cfg, taskManager)
159162

160163
// Override baseURL to use test server
@@ -211,7 +214,7 @@ func TestHTTPClientStartIntegration(t *testing.T) {
211214
}
212215

213216
dockerClient := docker.NewClient()
214-
taskManager := tasks.NewManager(dockerClient)
217+
taskManager := tasks.NewManager(dockerClient, cfg) // Pass config
215218
httpClient := NewHTTPClient(cfg, taskManager)
216219
httpClient.baseURL = server.URL
217220

@@ -245,7 +248,7 @@ func TestExecuteTask(t *testing.T) {
245248
}
246249

247250
dockerClient := docker.NewClient()
248-
taskManager := tasks.NewManager(dockerClient)
251+
taskManager := tasks.NewManager(dockerClient, cfg) // Pass config
249252
httpClient := NewHTTPClient(cfg, taskManager)
250253

251254
// Override baseURL to use test server

internal/compose/manager.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package compose
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
type Manager struct {
10+
basePath string
11+
}
12+
13+
type ProjectConfig struct {
14+
Name string `json:"name"`
15+
ComposeFile string `json:"compose_file,omitempty"` // Optional, defaults to docker-compose.yml
16+
Content string `json:"content"` // Docker compose YAML content
17+
EnvVars map[string]string `json:"env_vars,omitempty"` // Environment variables for .env file
18+
Override bool `json:"override,omitempty"` // Whether to override existing files
19+
}
20+
21+
func NewManager(basePath string) *Manager {
22+
return &Manager{
23+
basePath: basePath,
24+
}
25+
}
26+
27+
// EnsureBaseDirectory creates the base compose directory if it doesn't exist
28+
func (m *Manager) EnsureBaseDirectory() error {
29+
if err := os.MkdirAll(m.basePath, 0755); err != nil {
30+
return fmt.Errorf("failed to create base directory %s: %w", m.basePath, err)
31+
}
32+
return nil
33+
}
34+
35+
// CreateProject creates a new compose project directory with files
36+
func (m *Manager) CreateProject(config ProjectConfig) error {
37+
if config.Name == "" {
38+
return fmt.Errorf("project name is required")
39+
}
40+
41+
if config.Content == "" {
42+
return fmt.Errorf("compose content is required")
43+
}
44+
45+
// Set default compose file name
46+
if config.ComposeFile == "" {
47+
config.ComposeFile = "docker-compose.yml"
48+
}
49+
50+
projectPath := filepath.Join(m.basePath, config.Name)
51+
52+
// Create project directory
53+
if err := os.MkdirAll(projectPath, 0755); err != nil {
54+
return fmt.Errorf("failed to create project directory %s: %w", projectPath, err)
55+
}
56+
57+
// Create compose file
58+
composeFilePath := filepath.Join(projectPath, config.ComposeFile)
59+
if err := m.writeFileIfNotExists(composeFilePath, config.Content, config.Override); err != nil {
60+
return fmt.Errorf("failed to create compose file: %w", err)
61+
}
62+
63+
// Create .env file if env vars provided
64+
if len(config.EnvVars) > 0 {
65+
envFilePath := filepath.Join(projectPath, ".env")
66+
envContent := m.generateEnvContent(config.EnvVars)
67+
if err := m.writeFileIfNotExists(envFilePath, envContent, config.Override); err != nil {
68+
return fmt.Errorf("failed to create .env file: %w", err)
69+
}
70+
}
71+
72+
return nil
73+
}
74+
75+
// UpdateProject updates an existing project's files
76+
func (m *Manager) UpdateProject(config ProjectConfig) error {
77+
config.Override = true // Force override for updates
78+
return m.CreateProject(config)
79+
}
80+
81+
// DeleteProject removes a project directory
82+
func (m *Manager) DeleteProject(projectName string) error {
83+
if projectName == "" {
84+
return fmt.Errorf("project name is required")
85+
}
86+
87+
projectPath := filepath.Join(m.basePath, projectName)
88+
89+
// Check if project exists
90+
if _, err := os.Stat(projectPath); os.IsNotExist(err) {
91+
return fmt.Errorf("project %s does not exist", projectName)
92+
}
93+
94+
// Remove project directory
95+
if err := os.RemoveAll(projectPath); err != nil {
96+
return fmt.Errorf("failed to delete project %s: %w", projectName, err)
97+
}
98+
99+
return nil
100+
}
101+
102+
// ListProjects returns a list of existing project names
103+
func (m *Manager) ListProjects() ([]string, error) {
104+
entries, err := os.ReadDir(m.basePath)
105+
if err != nil {
106+
if os.IsNotExist(err) {
107+
return []string{}, nil
108+
}
109+
return nil, fmt.Errorf("failed to read base directory: %w", err)
110+
}
111+
112+
var projects []string
113+
for _, entry := range entries {
114+
if entry.IsDir() {
115+
projects = append(projects, entry.Name())
116+
}
117+
}
118+
119+
return projects, nil
120+
}
121+
122+
// ProjectExists checks if a project directory exists
123+
func (m *Manager) ProjectExists(projectName string) bool {
124+
projectPath := filepath.Join(m.basePath, projectName)
125+
_, err := os.Stat(projectPath)
126+
return !os.IsNotExist(err)
127+
}
128+
129+
// GetProjectPath returns the full path to a project directory
130+
func (m *Manager) GetProjectPath(projectName string) string {
131+
return filepath.Join(m.basePath, projectName)
132+
}
133+
134+
// GetComposePath returns the full path to a project's compose file
135+
func (m *Manager) GetComposePath(projectName, composeFile string) string {
136+
if composeFile == "" {
137+
composeFile = "docker-compose.yml"
138+
}
139+
return filepath.Join(m.basePath, projectName, composeFile)
140+
}
141+
142+
// writeFileIfNotExists writes content to a file, optionally overriding existing files
143+
func (m *Manager) writeFileIfNotExists(filePath, content string, override bool) error {
144+
// Check if file exists
145+
if _, err := os.Stat(filePath); err == nil && !override {
146+
return fmt.Errorf("file %s already exists and override is false", filePath)
147+
}
148+
149+
// Write file
150+
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
151+
return fmt.Errorf("failed to write file %s: %w", filePath, err)
152+
}
153+
154+
return nil
155+
}
156+
157+
// generateEnvContent creates .env file content from environment variables
158+
func (m *Manager) generateEnvContent(envVars map[string]string) string {
159+
content := "# Environment variables for Docker Compose\n"
160+
content += "# Generated by Arcane Agent\n\n"
161+
162+
for key, value := range envVars {
163+
content += fmt.Sprintf("%s=%s\n", key, value)
164+
}
165+
166+
return content
167+
}

0 commit comments

Comments
 (0)