Skip to content

Commit 1ccd2b4

Browse files
authored
feat: implement changed file detection for stack deployment (#491)
* feat: implement changed file detection for stack deployment * refactor: rename HasSubdirChangedBetweenCommits to HasChangesInSubdir for clarity * test: add unit test for changed file detection between commits * refactor: rename variable for clarity in changed file detection * fix: resolve issue with empty changedFiles return in changed file detection * refactor: rename functions for clarity in file change detection * feat: add configuration and secret files for deployment * feat: add tests for detecting changes in configs, secrets, and bind mounts * refactor: streamline error logging in compose file loading and change detection * chore: update import statement for go-git in compose_test.go * feat: enhance config change detection tests with parameterized test cases
1 parent 8880f44 commit 1ccd2b4

File tree

11 files changed

+543
-64
lines changed

11 files changed

+543
-64
lines changed

cmd/doco-cd/http_handler.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,16 +336,24 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
336336
return
337337
}
338338

339+
var changedFiles []git.ChangedFile
339340
if deployedCommit != "" {
340-
changed, err := git.CompareCommitsInSubdir(repo, plumbing.NewHash(deployedCommit), plumbing.NewHash(latestCommit), deployConfig.WorkingDirectory)
341+
changedFiles, err = git.GetChangedFilesBetweenCommits(repo, plumbing.NewHash(deployedCommit), plumbing.NewHash(latestCommit))
342+
if err != nil {
343+
onError(repoName, w, subJobLog.With(logger.ErrAttr(err)), "failed to get changed files between commits", err.Error(), jobID, http.StatusInternalServerError)
344+
345+
return
346+
}
347+
348+
hasChanged, err := git.HasChangesInSubdir(changedFiles, deployConfig.WorkingDirectory)
341349
if err != nil {
342350
onError(repoName, w, subJobLog, fmt.Errorf("failed to compare commits in subdirectory: %w", err).Error(),
343351
map[string]string{"stack": deployConfig.Name}, jobID, http.StatusInternalServerError)
344352

345353
return
346354
}
347355

348-
if !changed {
356+
if !hasChanged {
349357
jobLog.Debug("no changes detected in subdirectory, skipping deployment",
350358
slog.String("directory", deployConfig.WorkingDirectory),
351359
slog.String("last_commit", latestCommit),
@@ -360,7 +368,8 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
360368
}
361369
}
362370

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

cmd/doco-cd/poll_handler.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,15 +382,23 @@ func RunPoll(ctx context.Context, pollConfig config.PollConfig, appConfig *confi
382382
continue
383383
}
384384

385+
var changedFiles []git.ChangedFile
385386
if deployedCommit != "" {
386-
changed, err := git.CompareCommitsInSubdir(repo, plumbing.NewHash(deployedCommit), plumbing.NewHash(latestCommit), deployConfig.WorkingDirectory)
387+
changedFiles, err = git.GetChangedFilesBetweenCommits(repo, plumbing.NewHash(deployedCommit), plumbing.NewHash(latestCommit))
388+
if err != nil {
389+
subJobLog.Error("failed to get changed files between commits", log.ErrAttr(err))
390+
391+
return fmt.Errorf("failed to get changed files between commits: %w", err)
392+
}
393+
394+
hasChanged, err := git.HasChangesInSubdir(changedFiles, deployConfig.WorkingDirectory)
387395
if err != nil {
388396
subJobLog.Error("failed to compare commits in subdirectory", log.ErrAttr(err))
389397

390398
return fmt.Errorf("failed to compare commits in subdirectory: %w", err)
391399
}
392400

393-
if !changed {
401+
if !hasChanged {
394402
jobLog.Debug("no changes detected in subdirectory, skipping deployment",
395403
slog.String("directory", deployConfig.WorkingDirectory),
396404
slog.String("last_commit", latestCommit),
@@ -415,7 +423,8 @@ func RunPoll(ctx context.Context, pollConfig config.PollConfig, appConfig *confi
415423
Private: pollConfig.Private,
416424
}
417425

418-
err = docker.DeployStack(subJobLog, internalRepoPath, externalRepoPath, &ctx, &dockerCli, &payload, deployConfig, latestCommit, Version, true)
426+
err = docker.DeployStack(subJobLog, internalRepoPath, externalRepoPath, &ctx, &dockerCli, &payload,
427+
deployConfig, changedFiles, latestCommit, Version, false)
419428
if err != nil {
420429
subJobLog.Error("failed to deploy stack", log.ErrAttr(err))
421430

dev.compose.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ services:
2323
environment:
2424
TZ: Europe/Berlin
2525
HTTP_PORT: 80
26-
LOG_LEVEL: info
26+
LOG_LEVEL: debug
2727
# DOCKER_API_VERSION: 1.47
2828
<<: *poll-config
2929
#POLL_CONFIG_FILE: /poll.yml
@@ -61,7 +61,7 @@ configs:
6161
tinyproxy.conf:
6262
content: |
6363
# https://tinyproxy.github.io/
64-
LogLevel Connect
64+
LogLevel Warning
6565
Port 8888
6666
Timeout 600
6767
BasicAuth username password

internal/docker/compose.go

Lines changed: 183 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"strings"
2020
"time"
2121

22+
gitInternal "github.com/kimdre/doco-cd/internal/git"
23+
2224
"github.com/compose-spec/compose-go/v2/cli"
2325
"github.com/compose-spec/compose-go/v2/types"
2426
"github.com/docker/cli/cli/command"
@@ -210,7 +212,6 @@ func LoadCompose(ctx context.Context, workingDir, projectName string, composeFil
210212
cli.WithWorkingDirectory(workingDir),
211213
cli.WithInterpolation(true),
212214
cli.WithResolvedPaths(true),
213-
cli.WithDotEnv,
214215
)
215216
if err != nil {
216217
return nil, fmt.Errorf("failed to create project options: %w", err)
@@ -323,7 +324,7 @@ func DeployCompose(ctx context.Context, dockerCli command.Cli, project *types.Pr
323324
func DeployStack(
324325
jobLog *slog.Logger, internalRepoPath, externalRepoPath string, ctx *context.Context,
325326
dockerCli *command.Cli, payload *webhook.ParsedPayload, deployConfig *config.DeployConfig,
326-
latestCommit, appVersion string, forceDeploy bool,
327+
changedFiles []gitInternal.ChangedFile, latestCommit, appVersion string, forceDeploy bool,
327328
) error {
328329
startTime := time.Now()
329330

@@ -437,8 +438,6 @@ func DeployStack(
437438
}
438439

439440
stackLog.Debug("file decrypted successfully", slog.String("file", path))
440-
} else {
441-
stackLog.Debug("file is not encrypted", slog.String("file", path))
442441
}
443442

444443
return nil
@@ -450,13 +449,25 @@ func DeployStack(
450449
project, err := LoadCompose(*ctx, externalWorkingDir, deployConfig.Name, deployConfig.ComposeFiles)
451450
if err != nil {
452451
errMsg := "failed to load compose config"
453-
stackLog.Error(errMsg,
454-
logger.ErrAttr(err),
455-
slog.Group("compose_files", slog.Any("files", deployConfig.ComposeFiles)))
452+
stackLog.Error(errMsg, logger.ErrAttr(err), slog.Group("compose_files", slog.Any("files", deployConfig.ComposeFiles)))
453+
454+
return fmt.Errorf("%s: %w", errMsg, err)
455+
}
456+
457+
hasChanges, err := MountedFilesHaveChanges(changedFiles, project)
458+
if err != nil {
459+
errMsg := "failed to check for changed project files"
460+
stackLog.Error(errMsg, logger.ErrAttr(err), slog.Group("compose_files", slog.Any("files", deployConfig.ComposeFiles)))
456461

457462
return fmt.Errorf("%s: %w", errMsg, err)
458463
}
459464

465+
if hasChanges {
466+
stackLog.Info("mounted files have changed, forcing recreation of the stack")
467+
468+
deployConfig.ForceRecreate = true
469+
}
470+
460471
stackLog.Info("deploying stack")
461472

462473
done := make(chan struct{})
@@ -527,3 +538,168 @@ func DestroyStack(
527538

528539
return nil
529540
}
541+
542+
// HasChangedConfigs checks if any files used in docker compose `configs:` definitions have changed using the Git status.
543+
func HasChangedConfigs(changedFiles []gitInternal.ChangedFile, project *types.Project) (bool, error) {
544+
for _, c := range project.Configs {
545+
configPath := c.File
546+
if configPath == "" {
547+
continue
548+
}
549+
550+
if !path.IsAbs(configPath) {
551+
configPath = filepath.Join(project.WorkingDir, configPath)
552+
}
553+
554+
for _, f := range changedFiles {
555+
var paths []string
556+
557+
if f.From != nil {
558+
fromPath := f.From.Path()
559+
if !path.IsAbs(fromPath) {
560+
fromPath = filepath.Join(project.WorkingDir, fromPath)
561+
}
562+
563+
paths = append(paths, fromPath)
564+
}
565+
566+
if f.To != nil {
567+
toPath := f.To.Path()
568+
if !path.IsAbs(toPath) {
569+
toPath = filepath.Join(project.WorkingDir, toPath)
570+
}
571+
572+
paths = append(paths, toPath)
573+
}
574+
575+
for _, p := range paths {
576+
if p == configPath {
577+
return true, nil
578+
}
579+
}
580+
}
581+
}
582+
583+
return false, nil
584+
}
585+
586+
// HasChangedSecrets checks if any files used in docker compose `secrets:` definitions have changed using the Git status.
587+
func HasChangedSecrets(changedFiles []gitInternal.ChangedFile, project *types.Project) (bool, error) {
588+
for _, s := range project.Secrets {
589+
secretPath := s.File
590+
if secretPath == "" {
591+
continue
592+
}
593+
594+
if !path.IsAbs(secretPath) {
595+
secretPath = filepath.Join(project.WorkingDir, secretPath)
596+
}
597+
598+
for _, f := range changedFiles {
599+
var paths []string
600+
601+
if f.From != nil {
602+
fromPath := f.From.Path()
603+
if !path.IsAbs(fromPath) {
604+
fromPath = filepath.Join(project.WorkingDir, fromPath)
605+
}
606+
607+
paths = append(paths, fromPath)
608+
}
609+
610+
if f.To != nil {
611+
toPath := f.To.Path()
612+
if !path.IsAbs(toPath) {
613+
toPath = filepath.Join(project.WorkingDir, toPath)
614+
}
615+
616+
paths = append(paths, toPath)
617+
}
618+
619+
for _, p := range paths {
620+
if p == secretPath {
621+
return true, nil
622+
}
623+
}
624+
}
625+
}
626+
627+
return false, nil
628+
}
629+
630+
// HasChangedBindMounts checks if any files used in docker compose `volumes:` definitions with type `bind` have changed using the Git status.
631+
func HasChangedBindMounts(changedFiles []gitInternal.ChangedFile, project *types.Project) (bool, error) {
632+
for _, s := range project.Services {
633+
for _, v := range s.Volumes {
634+
if v.Type == "bind" && v.Source != "" {
635+
bindPath := v.Source
636+
if !path.IsAbs(bindPath) {
637+
bindPath = filepath.Join(project.WorkingDir, bindPath)
638+
}
639+
640+
for _, f := range changedFiles {
641+
var paths []string
642+
643+
if f.From != nil {
644+
fromPath := f.From.Path()
645+
if !path.IsAbs(fromPath) {
646+
fromPath = filepath.Join(project.WorkingDir, fromPath)
647+
}
648+
649+
paths = append(paths, fromPath)
650+
}
651+
652+
if f.To != nil {
653+
toPath := f.To.Path()
654+
if !path.IsAbs(toPath) {
655+
toPath = filepath.Join(project.WorkingDir, toPath)
656+
}
657+
658+
paths = append(paths, toPath)
659+
}
660+
661+
for _, p := range paths {
662+
// Check if bindPath is in the changed file path
663+
if strings.HasPrefix(p, bindPath) {
664+
return true, nil
665+
}
666+
}
667+
}
668+
}
669+
}
670+
}
671+
672+
return false, nil
673+
}
674+
675+
// MountedFilesHaveChanges checks if any files from config, secret or bind mounts have changed in the project.
676+
func MountedFilesHaveChanges(changedFiles []gitInternal.ChangedFile, project *types.Project) (bool, error) {
677+
changedConfigs, err := HasChangedConfigs(changedFiles, project)
678+
if err != nil {
679+
return false, fmt.Errorf("failed to check changed configs: %w", err)
680+
}
681+
682+
if changedConfigs {
683+
return true, nil
684+
}
685+
686+
changedSecrets, err := HasChangedSecrets(changedFiles, project)
687+
if err != nil {
688+
return false, fmt.Errorf("failed to check changed secrets: %w", err)
689+
}
690+
691+
if changedSecrets {
692+
return true, nil
693+
}
694+
695+
changedBindMounts, err := HasChangedBindMounts(changedFiles, project)
696+
if err != nil {
697+
return false, fmt.Errorf("failed to check changed bind mounts: %w", err)
698+
}
699+
700+
if changedBindMounts {
701+
return true, nil
702+
}
703+
704+
return false, nil
705+
}

0 commit comments

Comments
 (0)