Skip to content

Commit bbd76f3

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 8c2c92d + 3bc3299 commit bbd76f3

21 files changed

+1166
-413
lines changed

.github/workflows/test.yaml

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ jobs:
3333
version: latest
3434
args: --timeout=5m
3535

36-
test:
37-
name: Acceptance Tests
36+
test_compose:
37+
name: Test Docker Compose
3838
runs-on: ubuntu-latest
3939
timeout-minutes: 10
4040
services:
@@ -56,7 +56,40 @@ jobs:
5656
WEBHOOK_SECRET: test_Secret1
5757
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
5858
HTTP_PROXY: http://localhost:3128
59+
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
5960
- name: Upload coverage reports to Codecov
6061
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
6162
with:
62-
token: ${{ secrets.CODECOV_TOKEN }}
63+
token: ${{ secrets.CODECOV_TOKEN }}
64+
files: coverage.out
65+
66+
test_swarm:
67+
name: Tests Docker Swarm Mode
68+
runs-on: ubuntu-latest
69+
timeout-minutes: 10
70+
services:
71+
squid-proxy:
72+
image: ubuntu/squid:latest@sha256:98f98aaa024e4a58433450f8adde48ef5748f1ae05133b2367d64317cc881a3a
73+
ports:
74+
- 3128:3128
75+
76+
steps:
77+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
78+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
79+
with:
80+
go-version-file: 'go.mod'
81+
cache: true
82+
- run: go mod download
83+
- run: docker swarm init
84+
- run: go test -p 1 -v -coverprofile=coverage-swarm.out -covermode=atomic ./... -timeout 5m
85+
timeout-minutes: 5
86+
env:
87+
WEBHOOK_SECRET: test_Secret1
88+
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
89+
HTTP_PROXY: http://localhost:3128
90+
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
91+
- name: Upload coverage reports to Codecov
92+
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
93+
with:
94+
token: ${{ secrets.CODECOV_TOKEN }}
95+
files: coverage-swarm.out

cmd/doco-cd/http_handler.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,16 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
214214
return
215215
}
216216

217+
filterLabel := api.ProjectLabel
218+
if docker.SwarmModeEnabled {
219+
filterLabel = docker.StackNamespaceLabel
220+
}
221+
217222
if deployConfig.Destroy {
218223
subJobLog.Debug("destroying stack")
219224

220225
// Check if doco-cd manages the project before destroying the stack
221-
containers, err := docker.GetLabeledContainers(ctx, dockerClient, api.ProjectLabel, deployConfig.Name)
226+
containers, err := docker.GetLabeledContainers(ctx, dockerClient, filterLabel, deployConfig.Name)
222227
if err != nil {
223228
onError(repoName, w, subJobLog.With(logger.ErrAttr(err)), "failed to retrieve containers", err.Error(), jobID, http.StatusInternalServerError)
224229

@@ -269,6 +274,17 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
269274
return
270275
}
271276

277+
if docker.SwarmModeEnabled && deployConfig.DestroyOpts.RemoveVolumes {
278+
err = docker.RemoveLabeledVolumes(ctx, dockerClient, deployConfig.Name, filterLabel)
279+
if err != nil {
280+
onError(repoName, w, subJobLog.With(logger.ErrAttr(err)), "failed to remove volumes", err.Error(), jobID, http.StatusInternalServerError)
281+
282+
return
283+
}
284+
285+
subJobLog.Debug("failed to remove volumes", slog.String("stack", deployConfig.Name))
286+
}
287+
272288
if deployConfig.DestroyOpts.RemoveRepoDir {
273289
// Remove the repository directory after destroying the stack
274290
subJobLog.Debug("removing deployment directory", slog.String("path", externalRepoPath))
@@ -307,7 +323,7 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
307323
}
308324
} else {
309325
// Skip deployment if another project with the same name already exists
310-
containers, err := docker.GetLabeledContainers(ctx, dockerClient, api.ProjectLabel, deployConfig.Name)
326+
containers, err := docker.GetLabeledContainers(ctx, dockerClient, filterLabel, deployConfig.Name)
311327
if err != nil {
312328
onError(repoName, w, subJobLog.With(logger.ErrAttr(err)), "failed to retrieve containers", err.Error(), jobID, http.StatusInternalServerError)
313329

@@ -368,8 +384,8 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
368384
slog.String("deployed_commit", deployedCommit))
369385
}
370386

371-
err = docker.DeployStack(subJobLog, internalRepoPath, externalRepoPath, &ctx, &dockerCli, &payload,
372-
deployConfig, changedFiles, latestCommit, Version, false)
387+
err = docker.DeployStack(subJobLog, internalRepoPath, externalRepoPath, &ctx, &dockerCli, dockerClient,
388+
&payload, deployConfig, changedFiles, latestCommit, Version, false)
373389
if err != nil {
374390
onError(repoName, w, subJobLog.With(logger.ErrAttr(err)), "deployment failed", err.Error(), jobID, http.StatusInternalServerError)
375391

cmd/doco-cd/http_handler_test.go

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import (
1111
"os"
1212
"path"
1313
"regexp"
14+
"strconv"
1415
"testing"
16+
"time"
17+
18+
"github.com/docker/docker/api/types/swarm"
1519

1620
"github.com/docker/compose/v2/pkg/api"
1721
"github.com/docker/compose/v2/pkg/compose"
@@ -25,7 +29,8 @@ import (
2529
)
2630

2731
const (
28-
githubPayloadFile = "testdata/github_payload.json"
32+
githubPayloadFile = "testdata/github_payload.json"
33+
githubPayloadFileSwarmMode = "testdata/github_payload_swarm_mode.json"
2934
)
3035

3136
// Make http call to HealthCheckHandler.
@@ -82,9 +87,19 @@ func TestHandlerData_WebhookHandler(t *testing.T) {
8287
expectedStatusCode := http.StatusCreated
8388
tmpDir := t.TempDir()
8489

85-
repoDir := path.Join(tmpDir, getRepoName("https://github.com/kimdre/doco-cd.git"))
90+
payloadFile := githubPayloadFile
91+
cloneUrl := "https://github.com/kimdre/doco-cd.git"
92+
indexPath := path.Join("test", "index.html")
93+
94+
if docker.SwarmModeEnabled {
95+
payloadFile = githubPayloadFileSwarmMode
96+
cloneUrl = "https://github.com/kimdre/doco-cd_tests.git"
97+
indexPath = path.Join("html", "index.html")
98+
}
8699

87-
payload, err := os.ReadFile(githubPayloadFile)
100+
indexPath = path.Join(tmpDir, getRepoName(cloneUrl), indexPath)
101+
102+
payload, err := os.ReadFile(payloadFile)
88103
if err != nil {
89104
t.Fatal(err)
90105
}
@@ -192,22 +207,74 @@ func TestHandlerData_WebhookHandler(t *testing.T) {
192207
}
193208
})
194209

195-
testContainer, err := dockerCli.Client().ContainerInspect(ctx, testContainerID)
196-
if err != nil {
197-
t.Fatal(err)
198-
}
210+
testContainerPort := ""
211+
212+
if docker.SwarmModeEnabled {
213+
t.Log("Testing in Swarm mode, using service inspect")
214+
215+
svc, _, err := dockerCli.Client().ServiceInspectWithRaw(ctx, "test-deploy_test", swarm.ServiceInspectOptions{
216+
InsertDefaults: true,
217+
})
218+
if err != nil {
219+
t.Fatalf("Failed to inspect test container: %v", err)
220+
}
221+
222+
if len(svc.Endpoint.Ports) == 0 {
223+
t.Fatal("Test container has no published ports")
224+
}
225+
226+
testContainerPort = strconv.FormatUint(uint64(svc.Endpoint.Ports[0].PublishedPort), 10)
227+
228+
defer func() {
229+
err = dockerCli.Client().ServiceRemove(ctx, "test-deploy_test")
230+
if err != nil {
231+
t.Fatalf("Failed to remove test container service: %v", err)
232+
}
233+
}()
234+
} else {
235+
testContainer, err := dockerCli.Client().ContainerInspect(ctx, testContainerID)
236+
if err != nil {
237+
t.Fatal(err)
238+
}
239+
240+
if testContainer.State.Running != true {
241+
t.Errorf("Test container is not running: %v", testContainer.State)
242+
}
243+
244+
// Check if test container returns the expected response on its published port
245+
networkPort := testContainer.NetworkSettings.Ports["80/tcp"]
199246

200-
if testContainer.State.Running != true {
201-
t.Errorf("Test container is not running: %v", testContainer.State)
247+
testContainerPort = networkPort[0].HostPort
202248
}
203249

204-
// Check if test container returns the expected response on its published port
205-
testContainerPort := testContainer.NetworkSettings.Ports["80/tcp"][0].HostPort
206-
testURL := "http://localhost:" + testContainerPort
250+
testURL := "http://127.0.0.1:" + testContainerPort
251+
t.Logf("Test URL: %s", testURL)
207252

208-
resp, err := http.Get(testURL) // #nosec G107
209-
if err != nil {
210-
t.Fatalf("Failed to make GET request to test container: %v", err)
253+
httpClient := &http.Client{Timeout: 3 * time.Second}
254+
255+
resp := &http.Response{}
256+
for i := 0; i < 10; i++ {
257+
resp, err = httpClient.Get(testURL) // #nosec G107
258+
if err != nil {
259+
t.Logf("Failed to make GET request to test container (attempt %d): %v", i+1, err)
260+
time.Sleep(3 * time.Second) // Wait before retrying
261+
262+
continue
263+
}
264+
265+
if resp.StatusCode == http.StatusOK {
266+
t.Logf("Successfully connected to test container on attempt %d", i+1)
267+
break
268+
}
269+
270+
t.Logf("Test container returned status code %d on attempt %d", resp.StatusCode, i+1)
271+
272+
err = resp.Body.Close()
273+
if err != nil {
274+
t.Fatal(err)
275+
}
276+
277+
time.Sleep(3 * time.Second) // Wait before retrying
211278
}
212279

213280
t.Cleanup(
@@ -229,9 +296,7 @@ func TestHandlerData_WebhookHandler(t *testing.T) {
229296

230297
bodyString := string(bodyBytes)
231298

232-
indexFile := path.Join(repoDir, "test", "index.html")
233-
234-
fileContent, err := os.ReadFile(indexFile) // #nosec G304
299+
fileContent, err := os.ReadFile(indexPath) // #nosec G304
235300
if err != nil {
236301
t.Fatalf("Failed to read index.html file: %v", err)
237302
}

cmd/doco-cd/main.go

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -186,17 +186,31 @@ func main() {
186186
}
187187
}(dockerCli.Client())
188188

189-
log.Debug("docker client created")
190-
191-
dockerClient, _ := client.NewClientWithOpts(
189+
dockerClient, err := client.NewClientWithOpts(
192190
client.FromEnv,
193191
client.WithAPIVersionNegotiation(),
194192
)
193+
if err != nil {
194+
log.Critical("failed to create docker client", logger.ErrAttr(err))
195+
196+
return
197+
}
198+
199+
if c.DockerSwarmFeatures {
200+
// Check if docker host is running in swarm mode
201+
docker.SwarmModeEnabled, err = docker.CheckDaemonIsSwarmManager(context.Background(), dockerCli)
202+
if err != nil {
203+
log.Critical("failed to check if docker host is running in swarm mode", logger.ErrAttr(err))
204+
205+
return
206+
}
207+
}
195208

196209
log.Debug("negotiated docker versions to use",
197210
slog.Group("versions",
198211
slog.String("docker_client", dockerClient.ClientVersion()),
199212
slog.String("docker_api", dockerCli.CurrentVersion()),
213+
slog.Bool("swarm_mode", docker.SwarmModeEnabled),
200214
))
201215

202216
// Get container id of this application
@@ -268,50 +282,6 @@ func main() {
268282
}
269283
}
270284

271-
log.Debug("retrieving containers that are managed by doco-cd")
272-
273-
containers, err := docker.GetLabeledContainers(context.TODO(), dockerClient, docker.DocoCDLabels.Metadata.Manager, config.AppName)
274-
if err != nil {
275-
log.Error("failed to retrieve doco-cd containers", logger.ErrAttr(err))
276-
}
277-
278-
if len(containers) == 0 {
279-
log.Debug("no containers found that are managed by doco-cd", slog.Int("count", len(containers)))
280-
} else {
281-
log.Debug("retrieved containers successfully", slog.Int("count", len(containers)))
282-
}
283-
284-
for _, cont := range containers {
285-
log.Debug("inspecting container", slog.Group("container",
286-
slog.String("id", cont.ID),
287-
slog.String("name", cont.Names[0]),
288-
))
289-
290-
dir := cont.Labels[docker.DocoCDLabels.Deployment.WorkingDir]
291-
if len(dir) == 0 {
292-
log.Error(fmt.Sprintf("failed to retrieve container %v working directory", cont.ID))
293-
294-
continue
295-
}
296-
297-
wg.Add(1)
298-
299-
go func() {
300-
defer wg.Done()
301-
// docker.OnCrash(
302-
//
303-
// dockerCli.Client(),
304-
// cont.ID,
305-
// func() {
306-
// log.Info("cleaning up", slog.String("path", dir))
307-
// _ = os.RemoveAll(dir)
308-
// },
309-
// func(err error) { log.Error("failed to clean up path: "+dir, logger.ErrAttr(err)) },
310-
//
311-
// )
312-
}()
313-
}
314-
315285
go func() {
316286
log.Info("serving prometheus metrics", slog.Int("http_port", int(c.MetricsPort)), slog.String("path", prometheus.MetricsPath))
317287

0 commit comments

Comments
 (0)