mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
refactor: flatten watch trigger preparation and initial sync walk
watch (cognitive complexity 52) inlined three passes over each service's triggers; they become prepareRebuildTriggers (validation + always-build marking), watchTriggerPaths (paths to monitor, bind-mount skips, initial sync) and initialSyncRequested (the DEPRECATED x-initialSync fallback). initialSyncFiles (34) delegates its WalkDir closure to initialSyncDirectory, keeping the Info() call order so error behavior on unreadable entries is unchanged. No behavior change: same validations, same warnings, same collected paths and sync mappings. Co-Authored-By: Claude Fable 5 <[email protected]> Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
committed by
Nicolas De loof
co-authored by
Claude Fable 5
parent
f46228a609
commit
68ab763674
+114
-86
@@ -186,9 +186,6 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME(ndeloof) complete migration to gocognit
|
||||
//
|
||||
//nolint:gocognit
|
||||
func (s *composeService) watch(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error) {
|
||||
var err error
|
||||
if project, err = project.WithSelectedServices(options.Services); err != nil {
|
||||
@@ -218,47 +215,16 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
|
||||
continue
|
||||
}
|
||||
|
||||
for _, trigger := range config.Watch {
|
||||
if trigger.Action == types.WatchActionRebuild {
|
||||
if service.Build == nil {
|
||||
return nil, fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
|
||||
}
|
||||
if options.Build == nil {
|
||||
return nil, fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
|
||||
}
|
||||
// set the service to always be built - watch triggers `Up()` when it receives a rebuild event
|
||||
service.PullPolicy = types.PullPolicyBuild
|
||||
project.Services[serviceName] = service
|
||||
}
|
||||
service, err = prepareRebuildTriggers(project, serviceName, service, config, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, trigger := range config.Watch {
|
||||
if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
|
||||
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
|
||||
continue
|
||||
} else {
|
||||
shouldInitialSync := trigger.InitialSync
|
||||
|
||||
// Check legacy extension attribute for backward compatibility
|
||||
if !shouldInitialSync {
|
||||
var legacyInitialSync bool
|
||||
success, err := trigger.Extensions.Get("x-initialSync", &legacyInitialSync)
|
||||
if err == nil && success && legacyInitialSync {
|
||||
shouldInitialSync = true
|
||||
logrus.Warnf("x-initialSync is DEPRECATED, please use the official `initial_sync` attribute\n")
|
||||
}
|
||||
}
|
||||
|
||||
if shouldInitialSync && isSync(trigger) {
|
||||
// Need to check that initial files meant to be synced from the watch action are in the container
|
||||
err := s.initialSync(ctx, project, service, trigger, syncer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
paths = append(paths, trigger.Path)
|
||||
triggerPaths, err := s.watchTriggerPaths(ctx, project, service, config, syncer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, triggerPaths...)
|
||||
|
||||
serviceWatchRules, err := getWatchRules(config, service)
|
||||
if err != nil {
|
||||
@@ -295,6 +261,63 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
|
||||
}, nil
|
||||
}
|
||||
|
||||
// prepareRebuildTriggers validates rebuild watch actions and marks the
|
||||
// service to always be built — watch triggers `Up()` when it receives a
|
||||
// rebuild event. It returns the possibly-updated service config.
|
||||
func prepareRebuildTriggers(project *types.Project, serviceName string, service types.ServiceConfig, config *types.DevelopConfig, options api.WatchOptions) (types.ServiceConfig, error) {
|
||||
for _, trigger := range config.Watch {
|
||||
if trigger.Action != types.WatchActionRebuild {
|
||||
continue
|
||||
}
|
||||
if service.Build == nil {
|
||||
return service, fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
|
||||
}
|
||||
if options.Build == nil {
|
||||
return service, fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
|
||||
}
|
||||
service.PullPolicy = types.PullPolicyBuild
|
||||
project.Services[serviceName] = service
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// watchTriggerPaths collects the paths to monitor for a service, skipping
|
||||
// (with a warning) those already covered by a bind mount volume, and runs the
|
||||
// initial sync for sync triggers requesting one.
|
||||
func (s *composeService) watchTriggerPaths(ctx context.Context, project *types.Project, service types.ServiceConfig, config *types.DevelopConfig, syncer sync.Syncer) ([]string, error) {
|
||||
var paths []string
|
||||
for _, trigger := range config.Watch {
|
||||
if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
|
||||
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
|
||||
continue
|
||||
}
|
||||
if initialSyncRequested(trigger) && isSync(trigger) {
|
||||
// Need to check that initial files meant to be synced from the watch action are in the container
|
||||
err := s.initialSync(ctx, project, service, trigger, syncer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
paths = append(paths, trigger.Path)
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// initialSyncRequested tells whether a sync trigger requests an initial sync,
|
||||
// honoring the DEPRECATED x-initialSync extension attribute
|
||||
func initialSyncRequested(trigger types.Trigger) bool {
|
||||
if trigger.InitialSync {
|
||||
return true
|
||||
}
|
||||
var legacyInitialSync bool
|
||||
success, err := trigger.Extensions.Get("x-initialSync", &legacyInitialSync)
|
||||
if err == nil && success && legacyInitialSync {
|
||||
logrus.Warnf("x-initialSync is DEPRECATED, please use the official `initial_sync` attribute\n")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]watchRule, error) {
|
||||
var rules []watchRule
|
||||
|
||||
@@ -777,10 +800,6 @@ func (s *composeService) initialSync(ctx context.Context, project *types.Project
|
||||
}
|
||||
|
||||
// Syncs files from develop.watch.path if they have been modified after the image has been created
|
||||
//
|
||||
// FIXME(ndeloof) complete migration to gocognit
|
||||
//
|
||||
//nolint:gocognit
|
||||
func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher) ([]*sync.PathMapping, error) {
|
||||
fi, err := os.Stat(trigger.Path)
|
||||
if err != nil {
|
||||
@@ -790,57 +809,66 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pathsToCopy []*sync.PathMapping
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
// process directory
|
||||
err = filepath.WalkDir(trigger.Path, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
// handle possible path err, just in case...
|
||||
return err
|
||||
}
|
||||
if trigger.Path == path {
|
||||
// walk starts at the root directory
|
||||
return nil
|
||||
}
|
||||
if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) {
|
||||
// By definition sync ignores bind mounted paths
|
||||
if d.IsDir() {
|
||||
// skip folder
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil // skip file
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
if info.ModTime().Before(timeImageCreated) {
|
||||
// skip file if it was modified before image creation
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(trigger.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// only copy files (and not full directories)
|
||||
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
|
||||
HostPath: path,
|
||||
ContainerPath: filepath.Join(trigger.Target, rel),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return initialSyncDirectory(trigger, service, ignore, timeImageCreated)
|
||||
case mode.IsRegular():
|
||||
// process file
|
||||
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
|
||||
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
|
||||
return []*sync.PathMapping{{
|
||||
HostPath: trigger.Path,
|
||||
ContainerPath: trigger.Target,
|
||||
})
|
||||
}}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// initialSyncDirectory collects the files of a watched directory which were
|
||||
// modified after the image was created, skipping ignored and bind-mounted
|
||||
// paths
|
||||
func initialSyncDirectory(trigger types.Trigger, service types.ServiceConfig, ignore watch.PathMatcher, timeImageCreated time.Time) ([]*sync.PathMapping, error) {
|
||||
var pathsToCopy []*sync.PathMapping
|
||||
err := filepath.WalkDir(trigger.Path, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
// handle possible path err, just in case...
|
||||
return err
|
||||
}
|
||||
if trigger.Path == path {
|
||||
// walk starts at the root directory
|
||||
return nil
|
||||
}
|
||||
if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) {
|
||||
// By definition sync ignores bind mounted paths
|
||||
if d.IsDir() {
|
||||
// skip folder
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil // skip file
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.ModTime().Before(timeImageCreated) {
|
||||
// skip file if it was modified before image creation
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(trigger.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// only copy files (and not full directories)
|
||||
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
|
||||
HostPath: path,
|
||||
ContainerPath: filepath.Join(trigger.Target, rel),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
return pathsToCopy, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user