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 13 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
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
debounceTime time.Duration
dockerExcludes []string

newRecursiveWatcher func() (recursiveWatcher, error)
buildContainerImages func(mft manifest.DynamicWorkload) (map[string]string, error)
readDockerignoreFile func(fs afero.Fs, contextDir string) ([]string, error)
CaptainCarpensir marked this conversation as resolved.
Show resolved Hide resolved
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),
readDockerignoreFile: dockerfile.ReadDockerignore,
}
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.dockerExcludes, err = o.readDockerignoreFile(afero.NewOsFs(), filepath.Join(ws.Path(), dockerfileDir))
CaptainCarpensir marked this conversation as resolved.
Show resolved Hide resolved
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.dockerExcludes {
CaptainCarpensir marked this conversation as resolved.
Show resolved Hide resolved
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
63 changes: 35 additions & 28 deletions internal/pkg/cli/run_local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,16 +483,17 @@ func TestRunLocalOpts_Execute(t *testing.T) {
}

testCases := map[string]struct {
inputAppName string
inputEnvName string
inputWkldName string
inputEnvOverrides map[string]string
inputPortOverrides []string
inputWatch bool
inputTaskRole bool
inputProxy bool
inputReader io.Reader
buildImagesError error
inputAppName string
inputEnvName string
inputWkldName string
inputEnvOverrides map[string]string
inputPortOverrides []string
inputDockerExcludes []string
inputWatch bool
inputTaskRole bool
inputProxy bool
inputReader io.Reader
buildImagesError error

setupMocks func(t *testing.T, m *runLocalExecuteMocks)
wantedWkldName string
Expand Down Expand Up @@ -948,33 +949,38 @@ ecs exec: all containers failed to retrieve credentials`),
}
},
},
"watch flag receives hidden file update, doesn't restart": {
inputAppName: testAppName,
inputWkldName: testWkldName,
inputEnvName: testEnvName,
inputWatch: true,
"watch flag receives hidden file and ignored file update, doesn't restart": {
inputAppName: testAppName,
inputWkldName: testWkldName,
inputEnvName: testEnvName,
inputWatch: true,
inputDockerExcludes: []string{"ignoredDir/*"},
setupMocks: func(t *testing.T, m *runLocalExecuteMocks) {
m.ecsClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil)
m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil)
m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil)
m.interpolator.EXPECT().Interpolate("").Return("", nil)
m.ws.EXPECT().Path().Return("")

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 Down Expand Up @@ -1192,16 +1198,17 @@ ecs exec: all containers failed to retrieve credentials`),
Credentials: credentials.NewStaticCredentials("myEnvID", "myEnvSecret", "myEnvToken"),
},
},
cmd: m.mockRunner,
dockerEngine: m.dockerEngine,
repository: m.repository,
targetEnv: &mockEnv,
targetApp: &mockApp,
prog: m.prog,
orchestrator: m.orchestrator,
hostFinder: m.hostFinder,
envChecker: m.envChecker,
debounceTime: 0, // disable debounce during testing
cmd: m.mockRunner,
dockerEngine: m.dockerEngine,
repository: m.repository,
targetEnv: &mockEnv,
targetApp: &mockApp,
prog: m.prog,
orchestrator: m.orchestrator,
hostFinder: m.hostFinder,
envChecker: m.envChecker,
debounceTime: 0, // disable debounce during testing
dockerExcludes: tc.inputDockerExcludes,
newRecursiveWatcher: func() (recursiveWatcher, error) {
return m.watcher, nil
},
Expand Down
33 changes: 33 additions & 0 deletions internal/pkg/docker/dockerfile/ignorefile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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"
)

// ReadDockerignore reads the .dockerignore file in the context directory and
// returns the list of paths to exclude.
func ReadDockerignore(fs afero.Fs, contextDir string) ([]string, error) {
f, err := fs.Open(filepath.Join(contextDir, ".dockerignore"))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer f.Close()

patterns, err := ignorefile.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("error reading .dockerignore: %w", err)
CaptainCarpensir marked this conversation as resolved.
Show resolved Hide resolved
}

return patterns, nil
}
67 changes: 67 additions & 0 deletions internal/pkg/docker/dockerfile/ignorefile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package dockerfile

import (
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)

func TestDockerignoreFile(t *testing.T) {
var (
defaultPath = "./"
)

testCases := map[string]struct {
dockerignoreFilePath string
dockerignoreFile []byte
wantedExcludes []string
wantedErr error
}{
"reads file that doesn't exist": {
dockerignoreFilePath: "./falsepath",
wantedExcludes: nil,
},
"dockerignore file is empty": {
dockerignoreFilePath: defaultPath,
wantedExcludes: nil,
},
"parse dockerignore file": {
dockerignoreFilePath: defaultPath,
dockerignoreFile: []byte(`
copilot/*
copilot/*/*
# commenteddir/*
/copilot/*
testdir/
`),
wantedExcludes: []string{
"copilot/*",
"copilot/*/*",
"copilot/*",
"testdir",
},
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
fs := afero.Afero{Fs: afero.NewMemMapFs()}
err := fs.WriteFile("./.dockerignore", tc.dockerignoreFile, 0644)
if err != nil {
t.FailNow()
}

actualExcludes, err := ReadDockerignore(fs.Fs, tc.dockerignoreFilePath)
if tc.wantedErr != nil {
require.EqualError(t, err, tc.wantedErr.Error())
} else {
require.NoError(t, err)
require.Equal(t, tc.wantedExcludes, actualExcludes)
}
})
}
}