Skip to content

Commit 5b4195a

Browse files
author
asher
committed
feat: merged upstream main into branch and resolved conflicts, integrate Docker Compose and Swarm into a unified package.
1 parent 4d123ab commit 5b4195a

15 files changed

+336
-39
lines changed

Diff for: api/agent.go

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type DockerAgentDeployment struct {
2929
AgentDeployment
3030
ComposeFile []byte `json:"composeFile"`
3131
EnvFile []byte `json:"envFile"`
32+
types.DockerType
3233
}
3334

3435
type KubernetesAgentResource struct {

Diff for: cmd/agent/docker/agent_deployment.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import (
88
"path"
99

1010
"github.com/glasskube/distr/api"
11+
"github.com/glasskube/distr/internal/types"
1112
"github.com/google/uuid"
1213
)
1314

1415
type AgentDeployment struct {
15-
ID uuid.UUID `json:"id"`
16-
RevisionID uuid.UUID `json:"revisionId"`
17-
ProjectName string `json:"projectName"`
16+
ID uuid.UUID `json:"id"`
17+
RevisionID uuid.UUID `json:"revisionId"`
18+
ProjectName string `json:"projectName"`
19+
DockerType types.DockerType `json:"docker_type"`
1820
}
1921

2022
func (d *AgentDeployment) FileName() string {
@@ -29,7 +31,7 @@ func NewAgentDeployment(deployment api.DockerAgentDeployment) (*AgentDeployment,
2931
if name, err := getProjectName(deployment.ComposeFile); err != nil {
3032
return nil, err
3133
} else {
32-
return &AgentDeployment{ID: deployment.ID, RevisionID: deployment.RevisionID, ProjectName: name}, nil
34+
return &AgentDeployment{ID: deployment.ID, RevisionID: deployment.RevisionID, ProjectName: name, DockerType: deployment.DockerType}, nil
3335
}
3436
}
3537

@@ -42,8 +44,7 @@ func getProjectName(data []byte) (string, error) {
4244
return name, nil
4345
}
4446
}
45-
46-
func GetExistingDeployments() ([]AgentDeployment, error) {
47+
func GetExistingDeployments() (map[uuid.UUID]AgentDeployment, error) {
4748
if entries, err := os.ReadDir(agentDeploymentDir()); err != nil {
4849
if errors.Is(err, os.ErrNotExist) {
4950
return nil, nil
@@ -62,13 +63,13 @@ func GetExistingDeployments() ([]AgentDeployment, error) {
6263
return &d, nil
6364
}
6465
}
65-
result := make([]AgentDeployment, 0, len(entries))
66+
result := make(map[uuid.UUID]AgentDeployment, len(entries))
6667
for _, entry := range entries {
6768
if !entry.IsDir() {
6869
if d, err := fn(entry.Name()); err != nil {
6970
return nil, err
7071
} else {
71-
result = append(result, *d)
72+
result[d.RevisionID] = *d
7273
}
7374
}
7475
}

Diff for: cmd/agent/docker/docker_actions.go

+213-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,68 @@
11
package main
22

33
import (
4+
"bufio"
45
"bytes"
56
"context"
67
"errors"
78
"fmt"
89
"os"
910
"os/exec"
11+
"strings"
1012

1113
"github.com/glasskube/distr/api"
1214
"github.com/glasskube/distr/internal/agentauth"
15+
"github.com/glasskube/distr/internal/types"
1316
"go.uber.org/zap"
17+
"gopkg.in/yaml.v3"
1418
)
1519

16-
func ApplyComposeFile(ctx context.Context, deployment api.DockerAgentDeployment) (*AgentDeployment, string, error) {
20+
func DockerEngineApply(
21+
ctx context.Context,
22+
deployment api.DockerAgentDeployment,
23+
) (*AgentDeployment, string, error) {
24+
25+
if deployment.DockerType == types.DockerTypeSwarm {
26+
27+
fmt.Println(deployment.RevisionID)
28+
29+
// Step 1 Ensure Docker Swarm is initialized
30+
initCmd := exec.CommandContext(ctx, "docker", "info", "--format", "{{.Swarm.LocalNodeState}}")
31+
initOutput, err := initCmd.CombinedOutput()
32+
if err != nil {
33+
logger.Error("Failed to check Docker Swarm state", zap.Error(err))
34+
return nil, "", fmt.Errorf("failed to check Docker Swarm state: %w", err)
35+
}
36+
37+
if !strings.Contains(strings.TrimSpace(string(initOutput)), "active") {
38+
logger.Error("Docker Swarm not initialized", zap.String("output", string(initOutput)))
39+
return nil, "", fmt.Errorf("docker Swarm not initialized: %s", string(initOutput))
40+
}
41+
// Step 2: Pull images before deployment
42+
_, err = PullSwarmMode(ctx, deployment)
43+
if err != nil {
44+
logger.Error("Failed to Pull", zap.Error(err))
45+
return nil, "", err
46+
}
47+
return ApplyComposeFileSwarm(ctx, deployment)
48+
49+
}
50+
return ApplyComposeFile(ctx, deployment)
51+
52+
}
53+
func DockerEngineUninstall(
54+
ctx context.Context, deployment AgentDeployment,
55+
) error {
56+
if deployment.DockerType == types.DockerTypeSwarm {
57+
return UninstallDockerSwarm(ctx, deployment)
58+
}
59+
return UninstallDockerCompose(ctx, deployment)
60+
}
61+
func ApplyComposeFile(
62+
ctx context.Context,
63+
deployment api.DockerAgentDeployment,
64+
) (*AgentDeployment, string, error) {
65+
1766
agentDeploymet, err := NewAgentDeployment(deployment)
1867
if err != nil {
1968
return nil, "", err
@@ -60,11 +109,173 @@ func ApplyComposeFile(ctx context.Context, deployment api.DockerAgentDeployment)
60109
}
61110
}
62111

63-
func UninstallDockerCompose(ctx context.Context, deployment AgentDeployment) error {
112+
func ApplyComposeFileSwarm(
113+
ctx context.Context,
114+
deployment api.DockerAgentDeployment,
115+
) (*AgentDeployment, string, error) {
116+
117+
agentDeployment, err := NewAgentDeployment(deployment)
118+
if err != nil {
119+
return nil, "", err
120+
}
121+
122+
// Read the Compose file without replacing environment variables
123+
cleanedCompose := cleanComposeFile(deployment.ComposeFile)
124+
125+
// Construct environment variables
126+
envVars := os.Environ()
127+
envVars = append(envVars, agentauth.DockerConfigEnv(deployment.AgentDeployment)...)
128+
129+
// // If an env file is provided, load its values
130+
if deployment.EnvFile != nil {
131+
parsedEnv, err := parseEnvFile(deployment.EnvFile)
132+
if err != nil {
133+
logger.Error("Failed to parse env file", zap.Error(err))
134+
return nil, "", fmt.Errorf("failed to parse env file: %w", err)
135+
}
136+
for key, value := range parsedEnv {
137+
envVars = append(envVars, fmt.Sprintf("%s=%s", key, value))
138+
}
139+
}
140+
141+
// Deploy the stack
142+
composeArgs := []string{
143+
"stack", "deploy",
144+
"--compose-file", "-",
145+
"--with-registry-auth",
146+
"--detach=true",
147+
agentDeployment.ProjectName,
148+
}
149+
cmd := exec.CommandContext(ctx, "docker", composeArgs...)
150+
cmd.Stdin = bytes.NewReader(cleanedCompose)
151+
cmd.Env = envVars // Ensure the same env variables are used
152+
153+
// Execute the command and capture output
154+
cmdOut, err := cmd.CombinedOutput()
155+
statusStr := string(cmdOut)
156+
157+
if err != nil {
158+
logger.Error("Docker stack deploy failed", zap.String("output", statusStr))
159+
return nil, "", errors.New(statusStr)
160+
}
161+
162+
return agentDeployment, statusStr, nil
163+
}
164+
165+
func UninstallDockerCompose(
166+
ctx context.Context, deployment AgentDeployment,
167+
) error {
64168
cmd := exec.CommandContext(ctx, "docker", "compose", "--project-name", deployment.ProjectName, "down", "--volumes")
65169
out, err := cmd.CombinedOutput()
66170
if err != nil {
67171
return fmt.Errorf("%w: %v", err, string(out))
68172
}
69173
return nil
70174
}
175+
176+
func UninstallDockerSwarm(
177+
ctx context.Context, deployment AgentDeployment,
178+
) error {
179+
180+
cmd := exec.CommandContext(ctx, "docker", "stack", "rm", deployment.ProjectName)
181+
out, err := cmd.CombinedOutput()
182+
if err != nil {
183+
return fmt.Errorf("failed to remove Docker Swarm stack: %w: %v", err, string(out))
184+
}
185+
186+
// Optional: Prune unused networks created by Swarm
187+
pruneCmd := exec.CommandContext(ctx, "docker", "network", "prune", "-f")
188+
pruneOut, pruneErr := pruneCmd.CombinedOutput()
189+
if pruneErr != nil {
190+
logger.Warn("Failed to prune networks", zap.String("output", string(pruneOut)), zap.Error(pruneErr))
191+
}
192+
193+
return nil
194+
}
195+
func cleanComposeFile(composeData []byte) []byte {
196+
lines := strings.Split(string(composeData), "\n")
197+
cleanedLines := make([]string, 0, 50)
198+
199+
for _, line := range lines {
200+
// Skip lines that define `name:`
201+
if strings.HasPrefix(strings.TrimSpace(line), "name:") {
202+
continue
203+
}
204+
cleanedLines = append(cleanedLines, line)
205+
}
206+
return []byte(strings.Join(cleanedLines, "\n"))
207+
}
208+
func parseEnvFile(envData []byte) (map[string]string, error) {
209+
envVars := make(map[string]string)
210+
scanner := bufio.NewScanner(bytes.NewReader(envData))
211+
for scanner.Scan() {
212+
line := scanner.Text()
213+
if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") {
214+
continue // Skip empty lines and comments
215+
}
216+
parts := strings.SplitN(line, "=", 2)
217+
if len(parts) != 2 {
218+
return nil, fmt.Errorf("invalid environment variable: %s", line)
219+
}
220+
envVars[parts[0]] = parts[1]
221+
}
222+
return envVars, scanner.Err()
223+
}
224+
225+
type ComposeService struct {
226+
Image string `yaml:"image"`
227+
}
228+
229+
// ComposeFile represents the structure of docker-compose.yml
230+
type ComposeFile struct {
231+
Services map[string]ComposeService `yaml:"services"`
232+
}
233+
234+
func PullSwarmMode(
235+
ctx context.Context, deployment api.DockerAgentDeployment,
236+
) (string, error) {
237+
238+
// Parse the compose YAML file
239+
var compose ComposeFile
240+
err := yaml.Unmarshal(deployment.ComposeFile, &compose)
241+
if err != nil {
242+
return "", fmt.Errorf("failed to parse docker-compose.yml: %w", err)
243+
}
244+
245+
// Extract image names
246+
var images []string
247+
for _, service := range compose.Services {
248+
if service.Image != "" {
249+
images = append(images, service.Image)
250+
}
251+
}
252+
253+
if len(images) == 0 {
254+
return "", fmt.Errorf("no images found in the compose file")
255+
}
256+
257+
// Pull images using Docker CLI
258+
var pullLogs bytes.Buffer
259+
for _, image := range images {
260+
fmt.Println("Pulling image:", image)
261+
logger.Info("Pulling image:", zap.String("id", image))
262+
// Run `docker pull IMAGE_NAME`
263+
cmd := exec.CommandContext(ctx, "docker", "pull", image)
264+
var out bytes.Buffer
265+
cmd.Stdout = &out
266+
cmd.Stderr = &out
267+
268+
err := cmd.Run()
269+
if err != nil {
270+
logger.Error("failed to pull image", zap.Error(err))
271+
return "", fmt.Errorf("failed to pull image %s: %w\nOutput: %s", image, err, out.String())
272+
}
273+
274+
// Append logs
275+
pullLogs.WriteString(out.String() + "\n")
276+
fmt.Println(out.String())
277+
}
278+
279+
fmt.Println("Image pulling complete.")
280+
return pullLogs.String(), nil
281+
}

Diff for: cmd/agent/docker/main.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ loop:
5151
if resource, err := client.DockerResource(ctx); err != nil {
5252
logger.Error("failed to get resource", zap.Error(err))
5353
} else {
54+
5455
if agentVersionID != "" {
5556
if agentVersionID != resource.Version.ID.String() {
5657
logger.Info("agent version has changed. starting self-update")
@@ -71,13 +72,14 @@ loop:
7172
}
7273
}
7374

74-
if deployments, err := GetExistingDeployments(); err != nil {
75+
deployments, err := GetExistingDeployments()
76+
if err != nil {
7577
logger.Error("could not get existing deployments", zap.Error(err))
7678
} else {
7779
for _, deployment := range deployments {
7880
if resource.Deployment == nil || resource.Deployment.ID != deployment.ID {
7981
logger.Info("uninstalling old deployment", zap.String("id", deployment.ID.String()))
80-
if err := UninstallDockerCompose(ctx, deployment); err != nil {
82+
if err := DockerEngineUninstall(ctx, deployment); err != nil {
8183
logger.Error("could not uninstall deployment", zap.Error(err))
8284
} else if err := DeleteDeployment(deployment); err != nil {
8385
logger.Error("could not delete deployment", zap.Error(err))
@@ -96,8 +98,15 @@ loop:
9698
_, err = agentauth.EnsureAuth(ctx, resource.Deployment.AgentDeployment)
9799
if err != nil {
98100
logger.Error("docker auth error", zap.Error(err))
99-
} else if agentDeployment, status, err = ApplyComposeFile(ctx, *resource.Deployment); err == nil {
100-
multierr.AppendInto(&err, SaveDeployment(*agentDeployment))
101+
}
102+
if _, exists := deployments[resource.Deployment.RevisionID]; !exists {
103+
104+
agentDeployment, status, err = DockerEngineApply(ctx, *resource.Deployment)
105+
106+
if err == nil {
107+
multierr.AppendInto(&err, SaveDeployment(*agentDeployment))
108+
}
109+
101110
}
102111

103112
if statusErr := client.Status(ctx, resource.Deployment.RevisionID, status, err); statusErr != nil {

Diff for: frontend/ui/src/app/applications/application-detail.component.html

+18
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,24 @@ <h3 class="block mb-2 text-lg font-medium text-gray-900 dark:text-white">New Ver
244244
</div>
245245
</div>
246246

247+
<div class="w-full">
248+
<label for="dockerTypeSelect" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
249+
>Docker Type *</label
250+
>
251+
<select
252+
[formControl]="newVersionForm.controls.docker.controls.dockerType"
253+
id="dockerTypeSelect"
254+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
255+
<option value="compose">compose</option>
256+
<option value="swarm">swarm</option>
257+
</select>
258+
@if (
259+
newVersionForm.controls.docker.controls.dockerType.invalid &&
260+
newVersionForm.controls.docker.controls.dockerType.touched
261+
) {
262+
<p class="mt-1 text-sm text-red-600 dark:text-red-500">Field is required.</p>
263+
}
264+
</div>
247265
@if (application.type === 'kubernetes') {
248266
<div class="space-y-4 mt-4">
249267
<div class="grid grid-cols-2 md:grid-cols-2 space-y-4 sm:flex sm:space-x-4 sm:space-y-0">

0 commit comments

Comments
 (0)