diff --git a/cmd/skaffold/app/cmd/dev.go b/cmd/skaffold/app/cmd/dev.go index aed9a86299b..3e3642e0460 100644 --- a/cmd/skaffold/app/cmd/dev.go +++ b/cmd/skaffold/app/cmd/dev.go @@ -39,6 +39,7 @@ func NewCmdDev(out io.Writer) *cobra.Command { AddRunDevFlags(cmd) AddDevFlags(cmd) cmd.Flags().BoolVar(&opts.TailDev, "tail", true, "Stream logs from deployed objects") + cmd.Flags().StringVar(&opts.Trigger, "trigger", "polling", "How are changes detected? (polling or manual)") return cmd } diff --git a/pkg/skaffold/config/options.go b/pkg/skaffold/config/options.go index a067e279bfb..40b5c2bd482 100644 --- a/pkg/skaffold/config/options.go +++ b/pkg/skaffold/config/options.go @@ -33,6 +33,7 @@ type SkaffoldOptions struct { CustomTag string Namespace string Watch []string + Trigger string WatchPollInterval int } diff --git a/pkg/skaffold/runner/runner.go b/pkg/skaffold/runner/runner.go index 542dee9acf1..ad461cc5cfa 100644 --- a/pkg/skaffold/runner/runner.go +++ b/pkg/skaffold/runner/runner.go @@ -23,7 +23,6 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/bazel" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build" @@ -31,7 +30,6 @@ import ( "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/kaniko" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/local" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/tag" - "github.com/GoogleContainerTools/skaffold/pkg/skaffold/color" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" @@ -54,6 +52,7 @@ type SkaffoldRunner struct { deploy.Deployer test.Tester tag.Tagger + watch.Trigger opts *config.SkaffoldOptions watchFactory watch.Factory @@ -94,11 +93,17 @@ func NewForConfig(opts *config.SkaffoldOptions, cfg *latest.SkaffoldConfig) (*Sk deployer = WithNotification(deployer) } + trigger, err := watch.NewTrigger(opts) + if err != nil { + return nil, errors.Wrap(err, "creating watch trigger") + } + return &SkaffoldRunner{ Builder: builder, Tester: tester, Deployer: deployer, Tagger: tagger, + Trigger: trigger, opts: opts, watchFactory: watch.NewWatcher, }, nil @@ -239,7 +244,7 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la logger.Mute() defer func() { changed.reset() - color.Default.Fprintln(out, "Watching for changes...") + r.Trigger.WatchForChanges(out) if !hasError { logger.Unmute() } @@ -352,8 +357,8 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la } } - pollInterval := time.Duration(r.opts.WatchPollInterval) * time.Millisecond - return nil, watcher.Run(ctx, pollInterval, onChange) + r.Trigger.WatchForChanges(out) + return nil, watcher.Run(ctx, r.Trigger, onChange) } func (r *SkaffoldRunner) shouldWatch(artifact *latest.Artifact) bool { diff --git a/pkg/skaffold/runner/runner_test.go b/pkg/skaffold/runner/runner_test.go index e0bcf6430f6..f0634988d25 100644 --- a/pkg/skaffold/runner/runner_test.go +++ b/pkg/skaffold/runner/runner_test.go @@ -22,7 +22,6 @@ import ( "io" "io/ioutil" "testing" - "time" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/local" @@ -136,7 +135,7 @@ func (t *TestWatcher) Register(deps func() ([]string, error), onChange func(watc return nil } -func (t *TestWatcher) Run(ctx context.Context, pollInterval time.Duration, onChange func() error) error { +func (t *TestWatcher) Run(ctx context.Context, trigger watch.Trigger, onChange func() error) error { evts := watch.Events{} if t.events != nil { evts = t.events[0] @@ -236,7 +235,9 @@ func TestNewForConfig(t *testing.T) { } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - cfg, err := NewForConfig(&config.SkaffoldOptions{}, test.config) + cfg, err := NewForConfig(&config.SkaffoldOptions{ + Trigger: "polling", + }, test.config) testutil.CheckError(t, test.shouldErr, err) if cfg != nil { @@ -404,15 +405,21 @@ func TestDev(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { + opts := &config.SkaffoldOptions{ + WatchPollInterval: 100, + Trigger: "polling", + } + + trigger, _ := watch.NewTrigger(opts) + runner := &SkaffoldRunner{ Builder: test.builder, Tester: test.tester, Deployer: test.deployer, Tagger: &tag.ChecksumTagger{}, + Trigger: trigger, watchFactory: test.watcherFactory, - opts: &config.SkaffoldOptions{ - WatchPollInterval: 100, - }, + opts: opts, } _, err := runner.Dev(context.Background(), ioutil.Discard, nil) @@ -425,9 +432,13 @@ func TestBuildAndDeployAllArtifacts(t *testing.T) { kubernetes.Client = fakeGetClient defer resetClient() + opts := &config.SkaffoldOptions{ + Trigger: "polling", + } builder := &TestBuilder{} tester := &TestTester{} deployer := &TestDeployer{} + trigger, _ := watch.NewTrigger(opts) artifacts := []*latest.Artifact{ {ImageName: "image1"}, {ImageName: "image2"}, @@ -437,7 +448,8 @@ func TestBuildAndDeployAllArtifacts(t *testing.T) { Builder: builder, Tester: tester, Deployer: deployer, - opts: &config.SkaffoldOptions{}, + Trigger: trigger, + opts: opts, } ctx := context.Background() diff --git a/pkg/skaffold/watch/triggers.go b/pkg/skaffold/watch/triggers.go new file mode 100644 index 00000000000..bce847064da --- /dev/null +++ b/pkg/skaffold/watch/triggers.go @@ -0,0 +1,111 @@ +/* +Copyright 2018 The Skaffold 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 watch + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/color" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" + "github.com/sirupsen/logrus" +) + +// Trigger describes a mechanism that triggers the watch. +type Trigger interface { + Start() (<-chan bool, func()) + WatchForChanges(io.Writer) + Debounce() bool +} + +// NewTrigger creates a new trigger. +func NewTrigger(opts *config.SkaffoldOptions) (Trigger, error) { + switch strings.ToLower(opts.Trigger) { + case "polling": + return &pollTrigger{ + Interval: time.Duration(opts.WatchPollInterval) * time.Millisecond, + }, nil + case "manual": + return &manualTrigger{}, nil + default: + return nil, fmt.Errorf("unsupported type of trigger: %s", opts.Trigger) + } +} + +// pollTrigger watches for changes on a given interval of time. +type pollTrigger struct { + Interval time.Duration +} + +// Debounce tells the watcher to debounce rapid sequence of changes. +func (t *pollTrigger) Debounce() bool { + return true +} + +func (t *pollTrigger) WatchForChanges(out io.Writer) { + color.Yellow.Fprintf(out, "Watching for changes every %v...\n", t.Interval) +} + +// Start starts a timer. +func (t *pollTrigger) Start() (<-chan bool, func()) { + trigger := make(chan bool) + + ticker := time.NewTicker(t.Interval) + go func() { + for { + <-ticker.C + trigger <- true + } + }() + + return trigger, ticker.Stop +} + +// manualTrigger watches for changes when the user presses a key. +type manualTrigger struct { +} + +// Debounce tells the watcher to not debounce rapid sequence of changes. +func (t *manualTrigger) Debounce() bool { + return false +} + +func (t *manualTrigger) WatchForChanges(out io.Writer) { + color.Yellow.Fprintln(out, "Press any key to rebuild/redeploy the changes") +} + +// Start starts listening to pressed keys. +func (t *manualTrigger) Start() (<-chan bool, func()) { + trigger := make(chan bool) + + reader := bufio.NewReader(os.Stdin) + go func() { + for { + _, _, err := reader.ReadRune() + if err != nil { + logrus.Debugf("manual trigger error: %s", err) + } + trigger <- true + } + }() + + return trigger, func() {} +} diff --git a/pkg/skaffold/watch/watch.go b/pkg/skaffold/watch/watch.go index 6b2933c8f79..a6c750f683b 100644 --- a/pkg/skaffold/watch/watch.go +++ b/pkg/skaffold/watch/watch.go @@ -18,7 +18,6 @@ package watch import ( "context" - "time" "github.com/pkg/errors" ) @@ -29,7 +28,7 @@ type Factory func() Watcher // Watcher monitors files changes for multiples components. type Watcher interface { Register(deps func() ([]string, error), onChange func(Events)) error - Run(ctx context.Context, pollInterval time.Duration, onChange func() error) error + Run(ctx context.Context, trigger Trigger, onChange func() error) error } type watchList []*component @@ -62,9 +61,9 @@ func (w *watchList) Register(deps func() ([]string, error), onChange func(Events } // Run watches files until the context is cancelled or an error occurs. -func (w *watchList) Run(ctx context.Context, pollInterval time.Duration, onChange func() error) error { - ticker := time.NewTicker(pollInterval) - defer ticker.Stop() +func (w *watchList) Run(ctx context.Context, trigger Trigger, onChange func() error) error { + t, cleanup := trigger.Start() + defer cleanup() changedComponents := map[int]bool{} @@ -72,7 +71,7 @@ func (w *watchList) Run(ctx context.Context, pollInterval time.Duration, onChang select { case <-ctx.Done(): return nil - case <-ticker.C: + case <-t: changed := 0 for i, component := range *w { state, err := stat(component.deps) @@ -94,7 +93,8 @@ func (w *watchList) Run(ctx context.Context, pollInterval time.Duration, onChang // To prevent that, we debounce changes that happen too quickly // by waiting for a full turn where nothing happens and trigger a rebuild for // the accumulated changes. - if changed == 0 && len(changedComponents) > 0 { + debounce := trigger.Debounce() + if (!debounce && changed > 0) || (debounce && changed == 0 && len(changedComponents) > 0) { for i, component := range *w { if changedComponents[i] { component.onChange(component.events) diff --git a/pkg/skaffold/watch/watch_test.go b/pkg/skaffold/watch/watch_test.go index 64a44230508..bf47a32f1ab 100644 --- a/pkg/skaffold/watch/watch_test.go +++ b/pkg/skaffold/watch/watch_test.go @@ -79,7 +79,11 @@ func TestWatch(t *testing.T) { var stopped sync.WaitGroup stopped.Add(1) go func() { - err = watcher.Run(ctx, 10*time.Millisecond, somethingChanged.callNoErr) + trigger := &pollTrigger{ + Interval: 10 * time.Millisecond, + } + + err = watcher.Run(ctx, trigger, somethingChanged.callNoErr) stopped.Done() testutil.CheckError(t, false, err) }()