Skip to content

Commit c14d0ae

Browse files
qianlongztkimdre
andauthored
feat(docker): add recreate ignore label to skip recreate (#1158)
* feat(docker): add recreate ignore label to skip recreate #1157 * chore: remove unless comment * feat(utils): set add Difference method * feat(docker): add `cd.doco.deployment.recreate.ignore.signal` label to send a signal when service recreation is skipped #1157 * chore: make lint * fix(test): t.Parallel called multiple times * chore: make lint * chore(docker): make `getIgnoreRecrateCfgFromProject` more strict * test(docker): update test name * fix(docker): typo on recreate * refactor(docker): switch ignore config format to yaml * fix(docker): fix test ignore label * test: update test function names to use CamelCase --------- Co-authored-by: Kim Oliver Drechsel <kim@drechsel.xyz>
1 parent d2f6589 commit c14d0ae

File tree

14 files changed

+1307
-86
lines changed

14 files changed

+1307
-86
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Dockerfile
1111
*.compose.yaml
1212
docker-compose.yaml
1313
.dockerignore
14+
Makefile

internal/docker/compose.go

Lines changed: 118 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ func LoadCompose(ctx context.Context, repoPath, workingDir, projectName string,
301301
// deployCompose deploys a project as specified by the Docker Compose specification (LoadCompose).
302302
func deployCompose(ctx context.Context, dockerCli command.Cli, project *types.Project,
303303
deployConfig *config.DeployConfig, recreateMode string, services []string,
304+
needSignal []SignalService,
304305
) error {
305306
var (
306307
err error
@@ -313,6 +314,12 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, project *types.Pr
313314
return err
314315
}
315316

317+
if len(needSignal) > 0 {
318+
if err := ComposeSignal(ctx, dockerCli, project, needSignal); err != nil {
319+
return err
320+
}
321+
}
322+
316323
if deployConfig.PruneImages {
317324
beforeImages, err = service.Images(ctx, project.Name, api.ImagesOptions{})
318325
if err != nil {
@@ -416,7 +423,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, project *types.Pr
416423
func DeployStack(
417424
jobLog *slog.Logger, externalRepoPath string, ctx *context.Context,
418425
dockerCli *command.Cli, dockerClient *client.Client, payload *webhook.ParsedPayload, deployConfig *config.DeployConfig,
419-
changedFiles []gitInternal.ChangedFile, latestCommit, appVersion string, forceDeploy bool,
426+
detectedChanges []Change, needSignal []SignalService, latestCommit, appVersion string, forceDeploy bool,
420427
) error {
421428
startTime := time.Now()
422429

@@ -520,12 +527,6 @@ func DeployStack(
520527
}
521528
}
522529
} else {
523-
detectedChanges, err := ProjectFilesHaveChanges(externalRepoPath, changedFiles, project)
524-
if err != nil {
525-
errMsg := "failed to check for changed project files"
526-
return fmt.Errorf("%s: %w", errMsg, err)
527-
}
528-
529530
addComposeServiceLabels(project, deployConfig, payload, externalWorkingDir, appVersion, timestamp, ComposeVersion, latestCommit, projectHash)
530531
addComposeVolumeLabels(project, deployConfig, payload, appVersion, timestamp, ComposeVersion, latestCommit, projectHash)
531532

@@ -545,11 +546,21 @@ func DeployStack(
545546
}
546547

547548
stackLog.Debug("changed project files detected, forcing recreate", slog.Any("changes", detectedChanges))
549+
case len(needSignal) > 0:
550+
stackLog.Debug("changed project files detected, sending signal to service",
551+
slog.Any("need_signal", needSignal))
548552
}
549553

550-
stackLog.Info("deploying stack", slog.Group("recreate", slog.String("mode", recreateMode), slog.Any("forced_services", forcedServices.ToSlice())))
554+
stackLog.Info("deploying stack",
555+
slog.Group("recreate",
556+
slog.String("mode", recreateMode),
557+
slog.Any("forced_services", forcedServices.ToSlice()),
558+
),
559+
slog.Any("need_signal", needSignal),
560+
)
551561

552-
err = deployCompose(*ctx, *dockerCli, project, deployConfig, recreateMode, forcedServices.ToSlice())
562+
err = deployCompose(*ctx, *dockerCli, project, deployConfig, recreateMode,
563+
forcedServices.ToSlice(), needSignal)
553564
if err != nil {
554565
prometheus.DeploymentErrorsTotal.WithLabelValues(deployConfig.Name).Inc()
555566
return fmt.Errorf("failed to deploy stack: %w", err)
@@ -605,7 +616,7 @@ func DestroyStack(
605616
return nil
606617
}
607618

608-
func getPaths(changedFiles []gitInternal.ChangedFile, basePath string) []string {
619+
func GetPathsFromGitChangedFiles(changedFiles []gitInternal.ChangedFile, basePath string) []string {
609620
var absPaths []string
610621

611622
basePath = filepath.Clean(basePath)
@@ -632,7 +643,7 @@ func getPaths(changedFiles []gitInternal.ChangedFile, basePath string) []string
632643
}
633644

634645
// HasChangedConfigs checks if any files used in docker compose `configs:` definitions have changed using the Git status.
635-
func HasChangedConfigs(paths []string, project *types.Project) ([]string, error) {
646+
func HasChangedConfigs(paths []string, project *types.Project, ignoreCfg projectIgnoreCfg) ([]string, []string) {
636647
configToServicesMap := make(map[string][]string)
637648

638649
for name, s := range project.Services {
@@ -641,26 +652,35 @@ func HasChangedConfigs(paths []string, project *types.Project) ([]string, error)
641652
}
642653
}
643654

644-
var changedServices []string
655+
var (
656+
changedServices []string
657+
ignoredServices []string
658+
)
645659

646-
for name, c := range project.Configs {
660+
for cfgName, c := range project.Configs {
647661
// Changes in config.Content are handled in project hash comparison
648662
if c.File == "" {
649663
continue
650664
}
651665

652666
for _, p := range paths {
653667
if filesystem.InBasePath(c.File, p) {
654-
changedServices = append(changedServices, configToServicesMap[name]...)
668+
for _, svcName := range configToServicesMap[cfgName] {
669+
if !checkIsIgnoreByCfg(ignoreCfg, svcName, changeScopeConfigs, cfgName) {
670+
changedServices = append(changedServices, svcName)
671+
} else {
672+
ignoredServices = append(ignoredServices, svcName)
673+
}
674+
}
655675
}
656676
}
657677
}
658678

659-
return slice.Unique(changedServices), nil
679+
return getChangeAndIgnore(changedServices, ignoredServices)
660680
}
661681

662682
// HasChangedSecrets checks if any files used in docker compose `secrets:` definitions have changed using the Git status.
663-
func HasChangedSecrets(paths []string, project *types.Project) ([]string, error) {
683+
func HasChangedSecrets(paths []string, project *types.Project, ignoreCfg projectIgnoreCfg) ([]string, []string) {
664684
secretsToServicesMap := make(map[string][]string)
665685

666686
for name, s := range project.Services {
@@ -669,46 +689,63 @@ func HasChangedSecrets(paths []string, project *types.Project) ([]string, error)
669689
}
670690
}
671691

672-
var changedServices []string
692+
var (
693+
changedServices []string
694+
ignoredServices []string
695+
)
673696

674-
for name, s := range project.Secrets {
697+
for secretName, s := range project.Secrets {
675698
if s.File == "" {
676699
continue
677700
}
678701

679702
for _, p := range paths {
680703
if filesystem.InBasePath(s.File, p) {
681-
changedServices = append(changedServices, secretsToServicesMap[name]...)
704+
for _, svcName := range secretsToServicesMap[secretName] {
705+
if !checkIsIgnoreByCfg(ignoreCfg, svcName, changeScopeSecrets, secretName) {
706+
changedServices = append(changedServices, svcName)
707+
} else {
708+
ignoredServices = append(ignoredServices, svcName)
709+
}
710+
}
682711
}
683712
}
684713
}
685714

686-
return slice.Unique(changedServices), nil
715+
return getChangeAndIgnore(changedServices, ignoredServices)
687716
}
688717

689718
// HasChangedBindMounts checks if any files used in docker compose `volumes:` definitions with type `bind` have changed using the Git status.
690-
func HasChangedBindMounts(paths []string, project *types.Project) ([]string, error) {
691-
var changedServices []string
719+
func HasChangedBindMounts(paths []string, project *types.Project, ignoreCfg projectIgnoreCfg) ([]string, []string) {
720+
var (
721+
changedServices []string
722+
ignoredServices []string
723+
)
692724

693725
for _, s := range project.Services {
694726
out:
695727
for _, v := range s.Volumes {
696728
if v.Type == "bind" && v.Source != "" {
697729
for _, p := range paths {
698730
if filesystem.InBasePath(v.Source, p) {
699-
changedServices = append(changedServices, s.Name)
731+
if !checkIsIgnoreByCfg(ignoreCfg, s.Name, changeScopeBindMounts, v.Target) {
732+
changedServices = append(changedServices, s.Name)
733+
} else {
734+
ignoredServices = append(ignoredServices, s.Name)
735+
}
736+
700737
break out
701738
}
702739
}
703740
}
704741
}
705742
}
706743

707-
return slice.Unique(changedServices), nil
744+
return getChangeAndIgnore(changedServices, ignoredServices)
708745
}
709746

710747
// HasChangedEnvFiles checks if any files used in docker compose `env_file:` definitions have changed using the Git status.
711-
func HasChangedEnvFiles(paths []string, project *types.Project) ([]string, error) {
748+
func HasChangedEnvFiles(paths []string, project *types.Project, _ projectIgnoreCfg) ([]string, []string) {
712749
var changedServices []string
713750

714751
for _, s := range project.Services {
@@ -728,7 +765,7 @@ func HasChangedEnvFiles(paths []string, project *types.Project) ([]string, error
728765

729766
// HasChangedBuildFiles checks if any files used as build context in docker compose `build:` definitions have changed using the Git status.
730767
// This includes any file within the build context directory for each service. If a changed file is within a build context, it returns true.
731-
func HasChangedBuildFiles(paths []string, project *types.Project) ([]string, error) {
768+
func HasChangedBuildFiles(paths []string, project *types.Project, _ projectIgnoreCfg) ([]string, []string) {
732769
var changedServices []string
733770

734771
for _, s := range project.Services {
@@ -802,42 +839,81 @@ func sortChanges(changes []Change) {
802839
}
803840
}
804841

842+
type IgnoredInfo struct {
843+
// Ignored services name
844+
Ignored []string `json:"ignored"`
845+
// Ignored services need to send signal
846+
NeedSendSignal []SignalService `json:"need_signal"`
847+
}
848+
849+
func (i IgnoredInfo) IsEmpty() bool {
850+
return len(i.Ignored) == 0 && len(i.NeedSendSignal) == 0
851+
}
852+
853+
type SignalService struct {
854+
ServiceName string `json:"service_name"`
855+
Signal string `json:"signal"`
856+
}
857+
805858
// ProjectFilesHaveChanges checks if any files related to the compose project have changed.
806-
func ProjectFilesHaveChanges(repoRootExternal string, changedFiles []gitInternal.ChangedFile, project *types.Project) ([]Change, error) {
859+
func ProjectFilesHaveChanges(changePaths []string, project *types.Project) ([]Change, IgnoredInfo, error) {
807860
checks := []struct {
808-
name string
809-
fn func([]string, *types.Project) ([]string, error)
861+
name changeScope
862+
fn func([]string, *types.Project, projectIgnoreCfg) ([]string, []string)
810863
}{
811-
{"configs", HasChangedConfigs},
812-
{"secrets", HasChangedSecrets},
813-
{"bindMounts", HasChangedBindMounts},
814-
{"envFiles", HasChangedEnvFiles},
815-
{"buildFiles", HasChangedBuildFiles},
864+
{changeScopeConfigs, HasChangedConfigs},
865+
{changeScopeSecrets, HasChangedSecrets},
866+
{changeScopeBindMounts, HasChangedBindMounts},
867+
{changeScopeEnvFiles, HasChangedEnvFiles},
868+
{changeScopeBuildFiles, HasChangedBuildFiles},
816869
}
817870

818-
paths := getPaths(changedFiles, repoRootExternal)
871+
ignoreCfg, err := getIgnoreRecreateCfgFromProject(project)
872+
if err != nil {
873+
return nil, IgnoredInfo{}, err
874+
}
819875

820-
var changes []Change
876+
var (
877+
changes []Change
878+
allChangedServices, allIgnoredServices []string
879+
)
821880

822881
for _, check := range checks {
823-
changedServices, err := check.fn(paths, project)
824-
if err != nil {
825-
return nil, fmt.Errorf("failed to check '%s' for changes: %w", check.name, err)
826-
}
882+
changedServices, ignoredServices := check.fn(changePaths, project, ignoreCfg)
883+
884+
allChangedServices = append(allChangedServices, changedServices...)
885+
allIgnoredServices = append(allIgnoredServices, ignoredServices...)
827886

828887
if len(changedServices) > 0 {
829888
slices.Sort(changedServices)
830889

831890
changes = append(changes, Change{
832-
Type: check.name,
891+
Type: string(check.name),
833892
Services: changedServices,
834893
})
835894
}
836895
}
837896

838897
sortChanges(changes)
839898

840-
return changes, nil
899+
_, ignores := getChangeAndIgnore(allChangedServices, allIgnoredServices)
900+
slices.Sort(ignores)
901+
902+
retIgnored := IgnoredInfo{}
903+
904+
for _, svcName := range ignores {
905+
sig := ignoreCfg[svcName].signal
906+
if sig != "" {
907+
retIgnored.NeedSendSignal = append(retIgnored.NeedSendSignal, SignalService{
908+
ServiceName: svcName,
909+
Signal: sig,
910+
})
911+
} else {
912+
retIgnored.Ignored = append(retIgnored.Ignored, svcName)
913+
}
914+
}
915+
916+
return changes, retIgnored, nil
841917
}
842918

843919
// RestartProject restarts all services in the specified project.

0 commit comments

Comments
 (0)