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  }