github.com/splunk/dan1-qbec@v0.7.3/internal/rollout/rollout.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 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 rollout implements waiting for rollout completion of a set of objects. 18 package rollout 19 20 import ( 21 "fmt" 22 "reflect" 23 "sync" 24 "time" 25 26 "github.com/pkg/errors" 27 "github.com/splunk/qbec/internal/model" 28 "github.com/splunk/qbec/internal/types" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/watch" 31 ) 32 33 type statusTracker struct { 34 obj model.K8sMeta 35 fn types.RolloutStatusFunc 36 wp WatchProvider 37 listener StatusListener 38 } 39 40 func (s *statusTracker) wait() (finalErr error) { 41 defer func() { 42 if finalErr != nil { 43 s.listener.OnError(s.obj, finalErr) 44 } 45 }() 46 watcher, err := s.wp(s.obj) 47 if err != nil { 48 return errors.Wrap(err, "get watch interface") 49 } 50 var prevStatus types.RolloutStatus 51 _, err = watch.Until(0, watcher, func(e watch.Event) (bool, error) { 52 switch e.Type { 53 case watch.Deleted: 54 return false, fmt.Errorf("object was deleted") 55 case watch.Error: 56 return false, fmt.Errorf("watch error: %v", e.Object) 57 default: 58 un, ok := e.Object.(*unstructured.Unstructured) 59 if !ok { 60 return false, fmt.Errorf("unexpected watch object type: want *unstructured.Unstructured, got %v", reflect.TypeOf(e.Object)) 61 } 62 status, err := s.fn(un, 0) 63 if err != nil { 64 return false, err 65 } 66 if prevStatus != *status { 67 prevStatus = *status 68 s.listener.OnStatusChange(s.obj, prevStatus) 69 } 70 return status.Done, nil 71 } 72 }) 73 74 return err 75 } 76 77 // StatusListener receives status update callbacks. 78 type StatusListener interface { 79 OnInit(objects []model.K8sMeta) // the set of objects that are being monitored 80 OnStatusChange(object model.K8sMeta, rs types.RolloutStatus) // status for specified object 81 OnError(object model.K8sMeta, err error) // watch error of some kind for specified object 82 OnEnd(err error) // end of status updates with final error 83 } 84 85 // nopListener is the sentinel used when caller doesn't provide a listener. 86 type nopListener struct{} 87 88 func (n nopListener) OnInit(objects []model.K8sMeta) {} 89 func (n nopListener) OnStatusChange(object model.K8sMeta, rs types.RolloutStatus) {} 90 func (n nopListener) OnError(object model.K8sMeta, err error) {} 91 func (n nopListener) OnEnd(err error) {} 92 93 // WatchProvider provides a resource interface for a specific object type and namespace. 94 type WatchProvider func(obj model.K8sMeta) (watch.Interface, error) 95 96 // WaitOptions are options to the wait function. 97 type WaitOptions struct { 98 Listener StatusListener 99 Timeout time.Duration 100 } 101 102 func (w *WaitOptions) setupDefaults() { 103 if w.Listener == nil { 104 w.Listener = nopListener{} 105 } 106 if w.Timeout == 0 { 107 w.Timeout = 5 * time.Minute 108 } 109 } 110 111 // errCounter tracks a count of seen errors. 112 type errCounter struct { 113 l sync.Mutex 114 count int 115 } 116 117 func (ec *errCounter) add(err error) { 118 if err == nil { 119 return 120 } 121 ec.l.Lock() 122 ec.count++ 123 ec.l.Unlock() 124 } 125 126 func (ec *errCounter) toSummaryError() error { 127 ec.l.Lock() 128 defer ec.l.Unlock() 129 if ec.count == 0 { 130 return nil 131 } 132 return fmt.Errorf("%d wait errors", ec.count) 133 } 134 135 // allow standard status function map to be overridden for tests. 136 var statusMapper = types.StatusFuncFor 137 138 // WaitUntilComplete waits for the supplied objects to be ready and returns when they are. An error is returned 139 // if the function times out before all objects are ready. Any status listener provider is notified of 140 // individual status changes and errors during the wait. Individual watches having errors are turned into a 141 // aggregate error. 142 func WaitUntilComplete(objects []model.K8sMeta, wp WatchProvider, opts WaitOptions) (finalErr error) { 143 opts.setupDefaults() 144 145 var watchObjects []model.K8sMeta // the subset of objects we will actually watch 146 var trackers []*statusTracker // the list of trackers that we will run 147 148 // extract objects to wait for 149 for _, obj := range objects { 150 fn := statusMapper(obj) 151 if fn != nil { 152 watchObjects = append(watchObjects, obj) 153 trackers = append(trackers, &statusTracker{obj: obj, fn: fn, wp: wp, listener: opts.Listener}) 154 } 155 } 156 // notify listeners 157 opts.Listener.OnInit(watchObjects) 158 defer func() { 159 opts.Listener.OnEnd(finalErr) 160 }() 161 162 if len(trackers) == 0 { 163 return nil 164 } 165 var wg sync.WaitGroup 166 var counter errCounter 167 168 wg.Add(len(trackers)) 169 for _, so := range trackers { 170 go func(s *statusTracker) { 171 defer wg.Done() 172 counter.add(s.wait()) 173 }(so) 174 } 175 176 done := make(chan struct{}) 177 go func() { 178 wg.Wait() 179 close(done) 180 }() 181 182 timeout := make(chan struct{}) 183 go func() { 184 time.Sleep(opts.Timeout) 185 close(timeout) 186 }() 187 188 select { 189 case <-done: 190 return counter.toSummaryError() 191 case <-timeout: 192 return fmt.Errorf("wait timed out after %v", opts.Timeout) 193 } 194 }