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 }