Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: watch flag skips files specified by dockerignore #5565

Merged
merged 22 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/imdario/mergo v0.3.16
github.com/lnquy/cron v1.1.1
github.com/moby/buildkit v0.11.6
github.com/moby/patternmatcher v0.6.0
github.com/onsi/ginkgo/v2 v2.13.2
github.com/onsi/gomega v1.30.0
github.com/robfig/cron/v3 v3.0.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/moby/buildkit v0.11.6 h1:VYNdoKk5TVxN7k4RvZgdeM4GOyRvIi4Z8MXOY7xvyUs=
github.com/moby/buildkit v0.11.6/go.mod h1:GCqKfHhz+pddzfgaR7WmHVEE3nKKZMMDPpK8mh3ZLv4=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
Expand Down
4 changes: 4 additions & 0 deletions internal/pkg/cli/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,10 @@ type templateDiffer interface {
DeployDiff(inTmpl string) (string, error)
}

type dockerignoreFile interface {
Excludes() []string
}

type dockerEngineRunner interface {
CheckDockerEngineRunning() error
Run(context.Context, *dockerengine.RunOptions) error
Expand Down
37 changes: 37 additions & 0 deletions internal/pkg/cli/mocks/mock_interfaces.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 78 additions & 38 deletions internal/pkg/cli/run_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation"
"github.com/aws/copilot-cli/internal/pkg/describe"
"github.com/aws/copilot-cli/internal/pkg/docker/dockerengine"
"github.com/aws/copilot-cli/internal/pkg/docker/dockerfile"
"github.com/aws/copilot-cli/internal/pkg/docker/orchestrator"
"github.com/aws/copilot-cli/internal/pkg/ecs"
"github.com/aws/copilot-cli/internal/pkg/exec"
Expand Down Expand Up @@ -116,29 +117,31 @@ type runLocalVars struct {
type runLocalOpts struct {
runLocalVars

sel deploySelector
ecsClient ecsClient
ecsExecutor ecsCommandExecutor
ssm secretGetter
secretsManager secretGetter
sessProvider sessionProvider
sess *session.Session
envManagerSess *session.Session
targetEnv *config.Environment
targetApp *config.Application
store store
ws wsWlDirReader
cmd execRunner
dockerEngine dockerEngineRunner
repository repositoryService
prog progress
orchestrator containerOrchestrator
hostFinder hostFinder
envChecker versionCompatibilityChecker
debounceTime time.Duration
newRecursiveWatcher func() (recursiveWatcher, error)

sel deploySelector
ecsClient ecsClient
ecsExecutor ecsCommandExecutor
ssm secretGetter
secretsManager secretGetter
sessProvider sessionProvider
sess *session.Session
envManagerSess *session.Session
targetEnv *config.Environment
targetApp *config.Application
store store
ws wsWlDirReader
cmd execRunner
dockerEngine dockerEngineRunner
repository repositoryService
prog progress
orchestrator containerOrchestrator
hostFinder hostFinder
envChecker versionCompatibilityChecker
dockerignore dockerignoreFile
debounceTime time.Duration

newRecursiveWatcher func() (recursiveWatcher, error)
buildContainerImages func(mft manifest.DynamicWorkload) (map[string]string, error)
newDockerignoreFile func(fs afero.Fs, contextDir string) (*dockerfile.DockerignoreFile, error)
configureClients func() error
labeledTermPrinter func(fw syncbuffer.FileWriter, bufs []*syncbuffer.LabeledSyncBuffer, opts ...syncbuffer.LabeledTermPrinterOption) clideploy.LabeledTermPrinter
unmarshal func([]byte) (manifest.DynamicWorkload, error)
Expand Down Expand Up @@ -169,18 +172,19 @@ func newRunLocalOpts(vars runLocalVars) (*runLocalOpts, error) {
return syncbuffer.NewLabeledTermPrinter(fw, bufs, opts...)
}
o := &runLocalOpts{
runLocalVars: vars,
sel: selector.NewDeploySelect(prompt.New(), store, deployStore),
store: store,
ws: ws,
newInterpolator: newManifestInterpolator,
sessProvider: sessProvider,
unmarshal: manifest.UnmarshalWorkload,
sess: defaultSess,
cmd: exec.NewCmd(),
dockerEngine: dockerengine.New(exec.NewCmd()),
labeledTermPrinter: labeledTermPrinter,
prog: termprogress.NewSpinner(log.DiagnosticWriter),
runLocalVars: vars,
sel: selector.NewDeploySelect(prompt.New(), store, deployStore),
store: store,
ws: ws,
newInterpolator: newManifestInterpolator,
sessProvider: sessProvider,
unmarshal: manifest.UnmarshalWorkload,
sess: defaultSess,
cmd: exec.NewCmd(),
dockerEngine: dockerengine.New(exec.NewCmd()),
labeledTermPrinter: labeledTermPrinter,
prog: termprogress.NewSpinner(log.DiagnosticWriter),
newDockerignoreFile: dockerfile.NewDockerignoreFile,
}
o.configureClients = func() error {
defaultSessEnvRegion, err := o.sessProvider.DefaultWithRegion(o.targetEnv.Region)
Expand Down Expand Up @@ -237,6 +241,24 @@ func newRunLocalOpts(vars runLocalVars) (*runLocalOpts, error) {
return nil
}
o.buildContainerImages = func(mft manifest.DynamicWorkload) (map[string]string, error) {
var dockerfileDir string
switch v := mft.Manifest().(type) {
case *manifest.LoadBalancedWebService:
dockerfileDir, _ = filepath.Split(aws.StringValue(v.ImageConfig.ImageWithPort.Image.Build.BuildString))
case *manifest.BackendService:
dockerfileDir, _ = filepath.Split(aws.StringValue(v.ImageConfig.ImageWithOptionalPort.Image.Build.BuildString))
case *manifest.ScheduledJob:
dockerfileDir, _ = filepath.Split(aws.StringValue(v.ImageConfig.Image.Build.BuildString))
case *manifest.RequestDrivenWebService:
dockerfileDir, _ = filepath.Split(aws.StringValue(v.ImageConfig.Image.Build.BuildString))
case *manifest.WorkerService:
dockerfileDir, _ = filepath.Split(aws.StringValue(v.ImageConfig.Image.Build.BuildString))
}
o.dockerignore, err = o.newDockerignoreFile(afero.NewOsFs(), filepath.Join(ws.Path(), dockerfileDir))
if err != nil {
return nil, err
}

gitShortCommit := imageTagFromGit(o.cmd)
image := clideploy.ContainerImageIdentifier{
GitShortCommitTag: gitShortCommit,
Expand Down Expand Up @@ -612,6 +634,7 @@ func (o *runLocalOpts) watchLocalFiles(stopCh <-chan struct{}) (<-chan interface
watcherErrors := watcher.Errors()

debounceTimer := time.NewTimer(o.debounceTime)
debounceTimerRunning := false
if !debounceTimer.Stop() {
// flush the timer in case stop is called after the timer finishes
<-debounceTimer.C
Expand Down Expand Up @@ -640,11 +663,12 @@ func (o *runLocalOpts) watchLocalFiles(stopCh <-chan struct{}) (<-chan interface
break
}

// check if any subdirectories within copilot directory are hidden
isHidden := false
parent := workspacePath
suffix, _ := strings.CutPrefix(event.Name, parent+"/")

// check if any subdirectories within copilot directory are hidden
// fsnotify events are always of form /a/b/c, don't use filepath.Split as that's OS dependent
isHidden := false
for _, child := range strings.Split(suffix, "/") {
parent = filepath.Join(parent, child)
subdirHidden, err := file.IsHiddenFile(child)
Expand All @@ -656,11 +680,27 @@ func (o *runLocalOpts) watchLocalFiles(stopCh <-chan struct{}) (<-chan interface
}
}

// TODO(Aiden): implement dockerignore blacklist for update
if !isHidden {
// skip updates from files matching .dockerignore patterns
isExcluded := false
for _, pattern := range o.dockerignore.Excludes() {
matches, err := filepath.Match(pattern, suffix)
if err != nil {
break
}
if matches {
isExcluded = true
}
}

if !isHidden && !isExcluded {
if !debounceTimerRunning {
fmt.Println("Restarting task...")
debounceTimerRunning = true
}
debounceTimer.Reset(o.debounceTime)
}
case <-debounceTimer.C:
debounceTimerRunning = false
watchCh <- nil
}
}
Expand Down
18 changes: 14 additions & 4 deletions internal/pkg/cli/run_local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ type runLocalExecuteMocks struct {
watcher *filetest.Double
hostFinder *hostFinderDouble
envChecker *mocks.MockversionCompatibilityChecker
dockerignore *mocks.MockdockerignoreFile
}

type mockProvider struct {
Expand Down Expand Up @@ -948,7 +949,7 @@ ecs exec: all containers failed to retrieve credentials`),
}
},
},
"watch flag receives hidden file update, doesn't restart": {
"watch flag receives hidden file and ignored file update, doesn't restart": {
inputAppName: testAppName,
inputWkldName: testWkldName,
inputEnvName: testEnvName,
Expand All @@ -959,22 +960,27 @@ ecs exec: all containers failed to retrieve credentials`),
m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil)
m.interpolator.EXPECT().Interpolate("").Return("", nil)
m.ws.EXPECT().Path().Return("")
m.dockerignore.EXPECT().Excludes().Return([]string{"ignoredDir/*"}).AnyTimes()

eventCh := make(chan fsnotify.Event, 1)
eventCh := make(chan fsnotify.Event, 2)
m.watcher.EventsFn = func() <-chan fsnotify.Event {
eventCh <- fsnotify.Event{
Name: ".hiddensubdir/mockFilename",
Op: fsnotify.Write,
}
eventCh <- fsnotify.Event{
Name: "ignoredDir/mockFilename",
Op: fsnotify.Write,
}
return eventCh
}

watcherErrCh := make(chan error, 1)
watcherErrCh := make(chan error, 2)
m.watcher.ErrorsFn = func() <-chan error {
return watcherErrCh
}

errCh := make(chan error, 1)
errCh := make(chan error, 2)
m.orchestrator.StartFn = func() <-chan error {
return errCh
}
Expand All @@ -999,6 +1005,7 @@ ecs exec: all containers failed to retrieve credentials`),
m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil).Times(2)
m.interpolator.EXPECT().Interpolate("").Return("", nil).Times(2)
m.ws.EXPECT().Path().Return("")
m.dockerignore.EXPECT().Excludes().Return(nil)
m.ecsClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(alteredTaskDef, nil)

eventCh := make(chan fsnotify.Event, 1)
Expand Down Expand Up @@ -1085,6 +1092,7 @@ ecs exec: all containers failed to retrieve credentials`),
m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil).Times(2)
m.interpolator.EXPECT().Interpolate("").Return("", nil).Times(2)
m.ws.EXPECT().Path().Return("")
m.dockerignore.EXPECT().Excludes().Return(nil)

eventCh := make(chan fsnotify.Event, 1)
m.watcher.EventsFn = func() <-chan fsnotify.Event {
Expand Down Expand Up @@ -1141,6 +1149,7 @@ ecs exec: all containers failed to retrieve credentials`),
watcher: &filetest.Double{},
hostFinder: &hostFinderDouble{},
envChecker: mocks.NewMockversionCompatibilityChecker(ctrl),
dockerignore: mocks.NewMockdockerignoreFile(ctrl),
}
tc.setupMocks(t, m)
opts := runLocalOpts{
Expand Down Expand Up @@ -1201,6 +1210,7 @@ ecs exec: all containers failed to retrieve credentials`),
orchestrator: m.orchestrator,
hostFinder: m.hostFinder,
envChecker: m.envChecker,
dockerignore: m.dockerignore,
debounceTime: 0, // disable debounce during testing
newRecursiveWatcher: func() (recursiveWatcher, error) {
return m.watcher, nil
Expand Down
57 changes: 57 additions & 0 deletions internal/pkg/docker/dockerfile/ignorefile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package dockerfile

import (
"fmt"
"os"
"path/filepath"

"github.com/moby/patternmatcher/ignorefile"
"github.com/spf13/afero"
)

// DockerignoreFile represents a .dockerignore file holding information of excluded files.
type DockerignoreFile struct {
CaptainCarpensir marked this conversation as resolved.
Show resolved Hide resolved
excludes []string

fs afero.Fs
}

// NewDockerignoreFile returns a DockerignoreFile read from a given filesystem.
func NewDockerignoreFile(fs afero.Fs, contextDir string) (*DockerignoreFile, error) {
df := &DockerignoreFile{
fs: fs,
excludes: []string{},
}
return df, df.readDockerignore(contextDir)
}

// ReadDockerignore reads the .dockerignore file in the context directory and
// returns the list of paths to exclude.
func (df *DockerignoreFile) readDockerignore(contextDir string) error {
f, err := df.fs.Open(filepath.Join(contextDir, ".dockerignore"))
switch {
case os.IsNotExist(err):
return nil
case err != nil:
return err
}
CaptainCarpensir marked this conversation as resolved.
Show resolved Hide resolved
defer f.Close()

patterns, err := ignorefile.ReadAll(f)
if err != nil {
return fmt.Errorf("error reading .dockerignore: %w", err)
}

if patterns != nil {
df.excludes = patterns
}
return nil
}

// Excludes returns the exclude patterns of a .dockerignore file.
func (df *DockerignoreFile) Excludes() []string {
return df.excludes
}