Skip to content

Commit f4ff33a

Browse files
qianlongztkimdre
andauthored
fix(docker): fix change detect error (#1137)
* fix(docker): fix change detect error #1132 * chore: golangci-lint fix * fix(docker): fix path check logical error * chore(docker): add test not deps git repo & fix error check path affected * chore: dockerignore ignore docs test git and bin * chore: golangci-lint fix * test(docker): add more test case for checkPathAffected * fix(docker): test setAgeKey * fix(docker): t.Parallel not use with setenv * fix(compose): resolve variable shadowing in volume change detection loop --------- Co-authored-by: Kim Oliver Drechsel <kim@drechsel.xyz> Co-authored-by: Kim Oliver Drechsel <kimoliver.drechsel@adorsys.com>
1 parent ff506ea commit f4ff33a

File tree

5 files changed

+772
-322
lines changed

5 files changed

+772
-322
lines changed

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
.bin/
2+
bin
23
.idea/
34
.vscode/
45
.git/
56
docs/
67
test/
78
test-cases/
89
**/testdata
10+
Dockerfile
11+
*.compose.yaml
12+
docker-compose.yaml
13+
.dockerignore

internal/docker/compose.go

Lines changed: 66 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ func DeployStack(
538538
}
539539
}
540540
} else {
541-
detectedChanges, err := ProjectFilesHaveChanges(changedFiles, project)
541+
detectedChanges, err := ProjectFilesHaveChanges(externalRepoPath, changedFiles, project)
542542
if err != nil {
543543
errMsg := "failed to check for changed project files"
544544
return fmt.Errorf("%s: %w", errMsg, err)
@@ -642,39 +642,54 @@ func getPaths(changedFiles []gitInternal.ChangedFile, basePath string) []string
642642
p = filepath.Join(basePath, p)
643643
}
644644

645-
if !slices.Contains(absPaths, p) {
646-
absPaths = append(absPaths, p)
647-
}
645+
absPaths = append(absPaths, p)
648646
}
649647
}
650648

651-
return absPaths
649+
return slice.Unique(absPaths)
650+
}
651+
652+
// checkPathAffected checks if a changed file is affected by a used file.
653+
func checkPathAffected(changed string, used string) bool {
654+
used = filepath.Clean(used)
655+
changed = filepath.Clean(changed)
656+
657+
rel, err := filepath.Rel(used, changed)
658+
if err != nil {
659+
// It' share same reporoot, so it should not happen
660+
slog.Debug("checkPathAffected ",
661+
slog.String("used", used),
662+
slog.String("changed", changed),
663+
slog.Any("error", err),
664+
)
665+
666+
return false
667+
}
668+
669+
return !strings.HasPrefix(rel, "..")
652670
}
653671

654672
// HasChangedConfigs checks if any files used in docker compose `configs:` definitions have changed using the Git status.
655-
func HasChangedConfigs(changedFiles []gitInternal.ChangedFile, project *types.Project) ([]string, error) {
656-
// We only need the relative part in this case
657-
paths := getPaths(changedFiles, ".")
673+
func HasChangedConfigs(paths []string, project *types.Project) ([]string, error) {
674+
configToServicesMap := make(map[string][]string)
675+
676+
for name, s := range project.Services {
677+
for _, cfg := range s.Configs {
678+
configToServicesMap[cfg.Source] = append(configToServicesMap[cfg.Source], name)
679+
}
680+
}
658681

659682
var changedServices []string
660683

661-
for _, c := range project.Configs {
684+
for name, c := range project.Configs {
662685
// Changes in config.Content are handled in project hash comparison
663686
if c.File == "" {
664687
continue
665688
}
666689

667690
for _, p := range paths {
668-
if strings.HasSuffix(c.File, p) {
669-
// Find services that use if the changed config
670-
for _, s := range project.Services {
671-
for _, cfg := range s.Configs {
672-
if strings.HasSuffix(c.File, cfg.Source) {
673-
changedServices = append(changedServices, s.Name)
674-
break
675-
}
676-
}
677-
}
691+
if checkPathAffected(p, c.File) {
692+
changedServices = append(changedServices, configToServicesMap[name]...)
678693
}
679694
}
680695
}
@@ -683,28 +698,25 @@ func HasChangedConfigs(changedFiles []gitInternal.ChangedFile, project *types.Pr
683698
}
684699

685700
// HasChangedSecrets checks if any files used in docker compose `secrets:` definitions have changed using the Git status.
686-
func HasChangedSecrets(changedFiles []gitInternal.ChangedFile, project *types.Project) ([]string, error) {
687-
// We only need the relative part in this case
688-
paths := getPaths(changedFiles, ".")
701+
func HasChangedSecrets(paths []string, project *types.Project) ([]string, error) {
702+
secretsToServicesMap := make(map[string][]string)
703+
704+
for name, s := range project.Services {
705+
for _, secret := range s.Secrets {
706+
secretsToServicesMap[secret.Source] = append(secretsToServicesMap[secret.Source], name)
707+
}
708+
}
689709

690710
var changedServices []string
691711

692-
for _, s := range project.Secrets {
712+
for name, s := range project.Secrets {
693713
if s.File == "" {
694714
continue
695715
}
696716

697717
for _, p := range paths {
698-
if strings.HasSuffix(s.File, p) {
699-
// Find services that use if the changed config
700-
for _, svc := range project.Services {
701-
for _, secret := range svc.Secrets {
702-
if strings.HasSuffix(s.File, secret.Source) {
703-
changedServices = append(changedServices, svc.Name)
704-
break
705-
}
706-
}
707-
}
718+
if checkPathAffected(p, s.File) {
719+
changedServices = append(changedServices, secretsToServicesMap[name]...)
708720
}
709721
}
710722
}
@@ -713,24 +725,17 @@ func HasChangedSecrets(changedFiles []gitInternal.ChangedFile, project *types.Pr
713725
}
714726

715727
// HasChangedBindMounts checks if any files used in docker compose `volumes:` definitions with type `bind` have changed using the Git status.
716-
func HasChangedBindMounts(changedFiles []gitInternal.ChangedFile, project *types.Project) ([]string, error) {
717-
paths := getPaths(changedFiles, ".")
718-
728+
func HasChangedBindMounts(paths []string, project *types.Project) ([]string, error) {
719729
var changedServices []string
720730

721731
for _, s := range project.Services {
732+
out:
722733
for _, v := range s.Volumes {
723734
if v.Type == "bind" && v.Source != "" {
724-
bindSourceAbs := v.Source
725-
726-
if filesInPath(paths, bindSourceAbs) {
727-
for _, svc := range project.Services {
728-
for _, vol := range svc.Volumes {
729-
if vol.Type == "bind" && vol.Source == v.Source {
730-
changedServices = append(changedServices, svc.Name)
731-
break
732-
}
733-
}
735+
for _, p := range paths {
736+
if checkPathAffected(p, v.Source) {
737+
changedServices = append(changedServices, s.Name)
738+
break out
734739
}
735740
}
736741
}
@@ -741,25 +746,16 @@ func HasChangedBindMounts(changedFiles []gitInternal.ChangedFile, project *types
741746
}
742747

743748
// HasChangedEnvFiles checks if any files used in docker compose `env_file:` definitions have changed using the Git status.
744-
func HasChangedEnvFiles(changedFiles []gitInternal.ChangedFile, project *types.Project) ([]string, error) {
745-
// We only need the relative part in this case
746-
paths := getPaths(changedFiles, ".")
747-
749+
func HasChangedEnvFiles(paths []string, project *types.Project) ([]string, error) {
748750
var changedServices []string
749751

750752
for _, s := range project.Services {
753+
out:
751754
for _, envFile := range s.EnvFiles {
752755
for _, p := range paths {
753-
if strings.HasSuffix(envFile.Path, p) {
754-
// Find services that use if the changed env file
755-
for _, svc := range project.Services {
756-
for _, ef := range svc.EnvFiles {
757-
if strings.HasSuffix(envFile.Path, ef.Path) {
758-
changedServices = append(changedServices, svc.Name)
759-
break
760-
}
761-
}
762-
}
756+
if checkPathAffected(p, envFile.Path) {
757+
changedServices = append(changedServices, s.Name)
758+
break out
763759
}
764760
}
765761
}
@@ -770,9 +766,7 @@ func HasChangedEnvFiles(changedFiles []gitInternal.ChangedFile, project *types.P
770766

771767
// HasChangedBuildFiles checks if any files used as build context in docker compose `build:` definitions have changed using the Git status.
772768
// This includes any file within the build context directory for each service. If a changed file is within a build context, it returns true.
773-
func HasChangedBuildFiles(changedFiles []gitInternal.ChangedFile, project *types.Project) ([]string, error) {
774-
paths := getPaths(changedFiles, ".")
775-
769+
func HasChangedBuildFiles(paths []string, project *types.Project) ([]string, error) {
776770
var changedServices []string
777771

778772
for _, s := range project.Services {
@@ -811,15 +805,17 @@ func HasChangedBuildFiles(changedFiles []gitInternal.ChangedFile, project *types
811805
contexts = append(contexts, dockerFile)
812806
}
813807

808+
out:
809+
814810
for _, ctxFile := range contexts {
815811
if !path.IsAbs(ctxFile) {
816812
ctxFile = filepath.Join(project.WorkingDir, ctxFile)
817813
}
818814

819815
for _, p := range paths {
820-
if strings.HasSuffix(ctxFile, p) {
816+
if checkPathAffected(p, ctxFile) {
821817
changedServices = append(changedServices, s.Name)
822-
break
818+
break out
823819
}
824820
}
825821
}
@@ -845,10 +841,10 @@ func sortChanges(changes []Change) {
845841
}
846842

847843
// ProjectFilesHaveChanges checks if any files related to the compose project have changed.
848-
func ProjectFilesHaveChanges(changedFiles []gitInternal.ChangedFile, project *types.Project) ([]Change, error) {
844+
func ProjectFilesHaveChanges(repoRootExternal string, changedFiles []gitInternal.ChangedFile, project *types.Project) ([]Change, error) {
849845
checks := []struct {
850846
name string
851-
fn func([]gitInternal.ChangedFile, *types.Project) ([]string, error)
847+
fn func([]string, *types.Project) ([]string, error)
852848
}{
853849
{"configs", HasChangedConfigs},
854850
{"secrets", HasChangedSecrets},
@@ -857,10 +853,12 @@ func ProjectFilesHaveChanges(changedFiles []gitInternal.ChangedFile, project *ty
857853
{"buildFiles", HasChangedBuildFiles},
858854
}
859855

856+
paths := getPaths(changedFiles, repoRootExternal)
857+
860858
var changes []Change
861859

862860
for _, check := range checks {
863-
changedServices, err := check.fn(changedFiles, project)
861+
changedServices, err := check.fn(paths, project)
864862
if err != nil {
865863
return nil, fmt.Errorf("failed to check '%s' for changes: %w", check.name, err)
866864
}
@@ -1074,58 +1072,6 @@ func CheckDefaultComposeFiles(composeFiles []string, workingDir string) ([]strin
10741072
return composeFiles, nil
10751073
}
10761074

1077-
// filesInPath checks if at least one of the given filePaths is inside the searchPath.
1078-
func filesInPath(filePaths []string, searchPath string) bool {
1079-
for _, p := range filePaths {
1080-
if strings.HasSuffix(p, filepath.Base(searchPath)) {
1081-
p = joinPathsWithoutDuplicates(searchPath, p)
1082-
}
1083-
1084-
rel, err := filepath.Rel(searchPath, p)
1085-
if err != nil {
1086-
continue
1087-
}
1088-
1089-
if !strings.HasPrefix(rel, "..") {
1090-
return true
1091-
}
1092-
}
1093-
1094-
return false
1095-
}
1096-
1097-
// joinPathsWithoutDuplicates joins multiple paths, removing duplicate overlapping segments between each pair.
1098-
func joinPathsWithoutDuplicates(paths ...string) string {
1099-
if len(paths) == 0 {
1100-
return ""
1101-
}
1102-
1103-
isAbs := filepath.IsAbs(paths[0])
1104-
1105-
resultElems := strings.Split(filepath.Clean(paths[0]), string(filepath.Separator))
1106-
for _, next := range paths[1:] {
1107-
nextElems := strings.Split(filepath.Clean(next), string(filepath.Separator))
1108-
maxOverlap := 0
1109-
1110-
m := min(len(resultElems), len(nextElems))
1111-
for k := m; k > 0; k-- {
1112-
if reflect.DeepEqual(resultElems[len(resultElems)-k:], nextElems[:k]) {
1113-
maxOverlap = k
1114-
break
1115-
}
1116-
}
1117-
1118-
resultElems = append(resultElems, nextElems[maxOverlap:]...)
1119-
}
1120-
1121-
joined := filepath.Join(resultElems...)
1122-
if isAbs && !strings.HasPrefix(joined, string(filepath.Separator)) {
1123-
joined = string(filepath.Separator) + joined
1124-
}
1125-
1126-
return joined
1127-
}
1128-
11291075
// DecryptProjectFiles decrypts all files used in the compose project that are encrypted using doco-cd's encryption mechanism.
11301076
// This includes configs, secrets, bind mounts, env files and build contexts.
11311077
// Since absolute file paths in types.Project are paths on the docker host, repoPath also needs to be the external path to the repository.

0 commit comments

Comments
 (0)