watch: only allow a single instance per-project

This is a good place to start introducing (local) exclusivity
to Compose. Now, when `alpha watch` launches, it will check for
the existence of a PID file in the user XDG runtime directory,
and create one if the existing one is stale or does not exist.
If the PID file exists and is valid, an error is returned and
Compose exits.

A slight tweak to the experimental remote Git loader has been
made to use the XDG package for consistency.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
This commit is contained in:
Milas Bowman 2023-08-24 15:40:43 -04:00 committed by Nicolas De loof
parent 186744e034
commit 19f66918cc
6 changed files with 68 additions and 12 deletions

View File

@ -21,6 +21,8 @@ import (
"fmt"
"os"
"github.com/docker/compose/v2/internal/locker"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
@ -57,5 +59,13 @@ func runWatch(ctx context.Context, backend api.Service, opts watchOptions, servi
return err
}
l, err := locker.NewPidfile(project.Name)
if err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
}
if err := l.Lock(); err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
}
return backend.Watch(ctx, project, services, api.WatchOptions{})
}

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.21
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Microsoft/go-winio v0.6.1
github.com/adrg/xdg v0.4.0
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go v1.18.2
github.com/containerd/console v1.0.3

2
go.sum
View File

@ -66,6 +66,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=

View File

@ -0,0 +1,41 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package locker
import (
"fmt"
"os"
"github.com/adrg/xdg"
"github.com/docker/docker/pkg/pidfile"
)
type Pidfile struct {
path string
}
func NewPidfile(projectName string) (*Pidfile, error) {
path, err := xdg.RuntimeFile(fmt.Sprintf("docker-compose.%s.pid", projectName))
if err != nil {
return nil, err
}
return &Pidfile{path: path}, nil
}
func (f *Pidfile) Lock() error {
return pidfile.Write(f.path, os.Getpid())
}

View File

@ -71,6 +71,9 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
CopyFile(t, filepath.Join("fixtures", "watch", "compose.yaml"), composeFilePath)
projName := "e2e-watch-" + svcName
if tarSync {
projName += "-tar"
}
env := []string{
"COMPOSE_FILE=" + composeFilePath,
"COMPOSE_PROJECT_NAME=" + projName,
@ -96,6 +99,7 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
t.Cleanup(func() {
// IMPORTANT: watch doesn't exit on its own, don't leak processes!
if r.Cmd.Process != nil {
t.Logf("Killing watch process: pid[%d]", r.Cmd.Process.Pid)
_ = r.Cmd.Process.Kill()
}
})

View File

@ -25,6 +25,8 @@ import (
"regexp"
"strconv"
"github.com/adrg/xdg"
"github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types"
@ -45,19 +47,15 @@ func GitRemoteLoaderEnabled() (bool, error) {
}
func NewGitRemoteLoader() (loader.ResourceLoader, error) {
var base string
if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" {
base = cacheHome
} else {
home, err := os.UserHomeDir()
// xdg.CacheFile creates the parent directories for the target file path
// and returns the fully qualified path, so use "git" as a filename and
// then chop it off after, i.e. no ~/.cache/docker-compose/git file will
// ever be created
cache, err := xdg.CacheFile(filepath.Join("docker-compose", "git"))
if err != nil {
return nil, err
return nil, fmt.Errorf("initializing git cache: %w", err)
}
base = filepath.Join(home, ".cache")
}
cache := filepath.Join(base, "docker-compose")
err := os.MkdirAll(cache, 0o700)
cache = filepath.Dir(cache)
return gitRemoteLoader{
cache: cache,
}, err