github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/trigger/triggers.go (about)

     1  /*
     2  Copyright 2019 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package trigger
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"strings"
    26  	"sync/atomic"
    27  	"time"
    28  
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    32  	fsNotify "github.com/GoogleContainerTools/skaffold/pkg/skaffold/trigger/fsnotify"
    33  )
    34  
    35  // Trigger describes a mechanism that triggers the watch.
    36  type Trigger interface {
    37  	Start(context.Context) (<-chan bool, error)
    38  	LogWatchToUser(io.Writer)
    39  	Debounce() bool
    40  }
    41  
    42  type Config interface {
    43  	Trigger() string
    44  	Artifacts() []*latest.Artifact
    45  	WatchPollInterval() int
    46  }
    47  
    48  // NewTrigger creates a new trigger.
    49  func NewTrigger(cfg Config, isActive func() bool) (Trigger, error) {
    50  	switch strings.ToLower(cfg.Trigger()) {
    51  	case "polling":
    52  		return &pollTrigger{
    53  			Interval: time.Duration(cfg.WatchPollInterval()) * time.Millisecond,
    54  			isActive: isActive,
    55  		}, nil
    56  	case "notify":
    57  		return newFSNotifyTrigger(cfg, isActive), nil
    58  	case "manual":
    59  		return &manualTrigger{
    60  			isActive: isActive,
    61  		}, nil
    62  	default:
    63  		return nil, fmt.Errorf("unsupported trigger: %s", cfg.Trigger())
    64  	}
    65  }
    66  
    67  func newFSNotifyTrigger(cfg Config, isActive func() bool) Trigger {
    68  	workspaces := map[string]struct{}{}
    69  	for _, a := range cfg.Artifacts() {
    70  		workspaces[a.Workspace] = struct{}{}
    71  	}
    72  	return fsNotify.New(workspaces, isActive, cfg.WatchPollInterval())
    73  }
    74  
    75  // pollTrigger watches for changes on a given interval of time.
    76  type pollTrigger struct {
    77  	Interval time.Duration
    78  	isActive func() bool
    79  }
    80  
    81  // Debounce tells the watcher to debounce rapid sequence of changes.
    82  func (t *pollTrigger) Debounce() bool {
    83  	return true
    84  }
    85  
    86  func (t *pollTrigger) LogWatchToUser(out io.Writer) {
    87  	if t.isActive() {
    88  		output.Yellow.Fprintf(out, "Watching for changes every %v...\n", t.Interval)
    89  	} else {
    90  		output.Yellow.Fprintln(out, "Not watching for changes...")
    91  	}
    92  }
    93  
    94  // Start starts a timer.
    95  func (t *pollTrigger) Start(ctx context.Context) (<-chan bool, error) {
    96  	trigger := make(chan bool)
    97  
    98  	ticker := time.NewTicker(t.Interval)
    99  	go func() {
   100  		for {
   101  			select {
   102  			case <-ticker.C:
   103  
   104  				// Ignore if trigger is inactive
   105  				if !t.isActive() {
   106  					continue
   107  				}
   108  				trigger <- true
   109  			case <-ctx.Done():
   110  				ticker.Stop()
   111  				return
   112  			}
   113  		}
   114  	}()
   115  
   116  	return trigger, nil
   117  }
   118  
   119  // manualTrigger watches for changes when the user presses a key.
   120  type manualTrigger struct {
   121  	isActive func() bool
   122  }
   123  
   124  // Debounce tells the watcher to not debounce rapid sequence of changes.
   125  func (t *manualTrigger) Debounce() bool {
   126  	return false
   127  }
   128  
   129  func (t *manualTrigger) LogWatchToUser(out io.Writer) {
   130  	if t.isActive() {
   131  		output.Yellow.Fprintln(out, "Press any key to rebuild/redeploy the changes")
   132  	} else {
   133  		output.Yellow.Fprintln(out, "Not watching for changes...")
   134  	}
   135  }
   136  
   137  // Start starts listening to pressed keys.
   138  func (t *manualTrigger) Start(ctx context.Context) (<-chan bool, error) {
   139  	trigger := make(chan bool)
   140  
   141  	var stopped int32
   142  	go func() {
   143  		<-ctx.Done()
   144  		atomic.StoreInt32(&stopped, 1)
   145  	}()
   146  
   147  	reader := bufio.NewReader(os.Stdin)
   148  	go func() {
   149  		for {
   150  			_, _, err := reader.ReadRune()
   151  			if err != nil {
   152  				log.Entry(ctx).Debugf("manual trigger error: %s", err)
   153  			}
   154  
   155  			// Wait until the context is cancelled.
   156  			if atomic.LoadInt32(&stopped) == 1 {
   157  				return
   158  			}
   159  
   160  			// Ignore if trigger is inactive
   161  			if !t.isActive() {
   162  				continue
   163  			}
   164  			trigger <- true
   165  		}
   166  	}()
   167  
   168  	return trigger, nil
   169  }
   170  
   171  // StartTrigger attempts to start a trigger.
   172  // It will attempt to start as a polling trigger if it tried unsuccessfully to start a notify trigger.
   173  func StartTrigger(ctx context.Context, t Trigger) (<-chan bool, error) {
   174  	ret, err := t.Start(ctx)
   175  	if err == nil {
   176  		return ret, err
   177  	}
   178  	if fsnotify, ok := t.(*fsNotify.Trigger); ok {
   179  		log.Entry(ctx).Debug("Couldn't start notify trigger. Falling back to a polling trigger")
   180  
   181  		t = &pollTrigger{
   182  			Interval: fsnotify.Interval,
   183  			isActive: fsnotify.IsActive(),
   184  		}
   185  		ret, err = t.Start(ctx)
   186  	}
   187  
   188  	return ret, err
   189  }