initial support for `sync`

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2023-02-01 09:18:43 +01:00 committed by Nicolas De loof
parent e63cbfba0e
commit 1640f155e9
5 changed files with 107 additions and 49 deletions

1
go.mod
View File

@ -16,7 +16,6 @@ require (
github.com/docker/docker v20.10.20+incompatible // replaced; see replace rule for actual version
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.5.0
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/golang/mock v1.6.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.6.0

4
go.sum
View File

@ -206,9 +206,8 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
@ -910,7 +909,6 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

View File

@ -551,7 +551,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
}
// getLinks mimics V1 compose/service.py::Service::_get_links()
func (s composeService) getLinks(ctx context.Context, projectName string, service types.ServiceConfig, number int) ([]string, error) {
func (s *composeService) getLinks(ctx context.Context, projectName string, service types.ServiceConfig, number int) ([]string, error) {
var links []string
format := func(k, v string) string {
return fmt.Sprintf("%s:%s", k, v)

View File

@ -17,7 +17,7 @@ package compose
import (
"context"
"fmt"
"log"
"path/filepath"
"strings"
"time"
@ -32,56 +32,29 @@ import (
)
type DevelopmentConfig struct {
Sync map[string]string `json:"sync,omitempty"`
Excludes []string `json:"excludes,omitempty"`
}
const quietPeriod = 2 * time.Second
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
fmt.Fprintln(s.stderr(), "not implemented yet")
needRebuild := make(chan string)
needSync := make(chan api.CopyOptions, 5)
eg, ctx := errgroup.WithContext(ctx)
needRefresh := make(chan string)
eg.Go(func() error {
clock := clockwork.NewRealClock()
debounce(ctx, clock, quietPeriod, needRefresh, func(services []string) {
fmt.Fprintf(s.stderr(), "Updating %s after changes were detected\n", strings.Join(services, ", "))
imageIds, err := s.build(ctx, project, api.BuildOptions{
Services: services,
})
if err != nil {
fmt.Fprintf(s.stderr(), "Build failed")
}
for i, service := range project.Services {
if id, ok := imageIds[service.Name]; ok {
service.Image = id
}
project.Services[i] = service
}
err = s.Up(ctx, project, api.UpOptions{
Create: api.CreateOptions{
Services: services,
Inherit: true,
},
Start: api.StartOptions{
Services: services,
Project: project,
},
})
if err != nil {
fmt.Fprintf(s.stderr(), "Application failed to start after update")
}
})
debounce(ctx, clock, quietPeriod, needRebuild, s.makeRebuildFn(ctx, project))
return nil
})
eg.Go(s.makeSyncFn(ctx, project, needSync))
err := project.WithServices(services, func(service types.ServiceConfig) error {
var config DevelopmentConfig
if y, ok := service.Extensions["x-develop"]; ok {
err := mapstructure.Decode(y, &config)
if err != nil {
return err
}
config, err := loadDevelopmentConfig(service, project)
if err != nil {
return err
}
if service.Build == nil {
return errors.New("can't watch a service without a build section")
@ -98,7 +71,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
return err
}
fmt.Println("watching " + context)
fmt.Fprintf(s.stderr(), "watching %s\n", context)
err = watcher.Start()
if err != nil {
return err
@ -106,13 +79,32 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
eg.Go(func() error {
defer watcher.Close() //nolint:errcheck
WATCH:
for {
select {
case <-ctx.Done():
return nil
case event := <-watcher.Events():
log.Println("fs event :", event.Path())
needRefresh <- service.Name
fmt.Fprintf(s.stderr(), "change detected on %s\n", event.Path())
for src, dest := range config.Sync {
path := filepath.Clean(event.Path())
src = filepath.Clean(src)
if watch.IsChild(path, src) {
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
dest = filepath.Join(dest, rel)
needSync <- api.CopyOptions{
Source: path,
Destination: fmt.Sprintf("%s:%s", service.Name, dest),
}
continue WATCH
}
}
needRebuild <- service.Name
case err := <-watcher.Errors():
return err
}
@ -127,6 +119,73 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
return eg.Wait()
}
func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (DevelopmentConfig, error) {
var config DevelopmentConfig
if y, ok := service.Extensions["x-develop"]; ok {
err := mapstructure.Decode(y, &config)
if err != nil {
return DevelopmentConfig{}, err
}
for src, dest := range config.Sync {
if !filepath.IsAbs(src) {
delete(config.Sync, src)
src = filepath.Join(project.WorkingDir, src)
config.Sync[src] = dest
}
}
}
return config, nil
}
func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Project) func(services []string) {
return func(services []string) {
fmt.Fprintf(s.stderr(), "Updating %s after changes were detected\n", strings.Join(services, ", "))
imageIds, err := s.build(ctx, project, api.BuildOptions{
Services: services,
})
if err != nil {
fmt.Fprintf(s.stderr(), "Build failed")
}
for i, service := range project.Services {
if id, ok := imageIds[service.Name]; ok {
service.Image = id
}
project.Services[i] = service
}
err = s.Up(ctx, project, api.UpOptions{
Create: api.CreateOptions{
Services: services,
Inherit: true,
},
Start: api.StartOptions{
Services: services,
Project: project,
},
})
if err != nil {
fmt.Fprintf(s.stderr(), "Application failed to start after update")
}
}
}
func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project, needSync chan api.CopyOptions) func() error {
return func() error {
for {
select {
case <-ctx.Done():
return nil
case opt := <-needSync:
err := s.Copy(ctx, project.Name, opt)
if err != nil {
return err
}
fmt.Fprintf(s.stderr(), "%s updated\n", opt.Source)
}
}
}
}
func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input chan string, fn func(services []string)) {
services := utils.Set[string]{}
t := clock.AfterFunc(delay, func() {

View File

@ -23,7 +23,9 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/tilt-dev/fsnotify"
)
var (
@ -86,7 +88,7 @@ func NewWatcher(paths []string, ignore PathMatcher) (Notify, error) {
return newWatcher(paths, ignore)
}
const WindowsBufferSizeEnvVar = "TILT_WATCH_WINDOWS_BUFFER_SIZE"
const WindowsBufferSizeEnvVar = "COMPOSE_WATCH_WINDOWS_BUFFER_SIZE"
const defaultBufferSize int = 65536
@ -102,5 +104,5 @@ func DesiredWindowsBufferSize() int {
}
func IsWindowsShortReadError(err error) bool {
return runtime.GOOS == "windows" && err != nil && strings.Contains(err.Error(), "short read")
return runtime.GOOS == "windows" && !errors.Is(err, fsnotify.ErrEventOverflow)
}