github.com/sacloud/iaas-api-go@v1.12.0/state.go (about)

     1  // Copyright 2022-2023 The sacloud/iaas-api-go Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package iaas
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"time"
    22  
    23  	"github.com/sacloud/iaas-api-go/accessor"
    24  	"github.com/sacloud/iaas-api-go/defaults"
    25  	"github.com/sacloud/iaas-api-go/types"
    26  	"github.com/sacloud/packages-go/wait"
    27  )
    28  
    29  // UnexpectedAvailabilityError 予期しないAvailabilityとなった場合のerror
    30  type UnexpectedAvailabilityError struct {
    31  	// Err エラー詳細
    32  	Err error
    33  }
    34  
    35  // Error errorインターフェース実装
    36  func (e *UnexpectedAvailabilityError) Error() string {
    37  	return fmt.Sprintf("resource returns unexpected availability value: %s", e.Err.Error())
    38  }
    39  
    40  // UnexpectedInstanceStatusError 予期しないInstanceStatusとなった場合のerror
    41  type UnexpectedInstanceStatusError struct {
    42  	// Err エラー詳細
    43  	Err error
    44  }
    45  
    46  // Error errorインターフェース実装
    47  func (e *UnexpectedInstanceStatusError) Error() string {
    48  	return fmt.Sprintf("resource returns unexpected instance status value: %s", e.Err.Error())
    49  }
    50  
    51  var _ wait.StateWaiter = (*StatePollingWaiter)(nil) // StatePollingWaiterでIaaS固有の事情を考慮したwait.StateWaiterを実装する
    52  
    53  // StatePollingWaiter ポーリングによりリソースの状態が変わるまで待機する
    54  type StatePollingWaiter struct {
    55  	// ReadFunc 対象リソースの状態を取得するためのfunc
    56  	ReadFunc wait.StateReadFunc
    57  
    58  	// StateCheckFunc ReadFuncで得たリソースの情報を元に待ちを継続するかの判定を行うためのfunc
    59  	StateCheckFunc wait.StateCheckFunc
    60  
    61  	// Timeout タイムアウト
    62  	Timeout time.Duration // タイムアウト
    63  
    64  	// Interval ポーリング間隔
    65  	Interval time.Duration
    66  
    67  	// NotFoundRetry Readで404が返ってきた場合のリトライ回数
    68  	//
    69  	// アプライアンスなどの一部のリソースでは作成~起動完了までの間に404を返すことがある。
    70  	// これに対応するためこのフィールドにて404発生の許容回数を指定可能にする。
    71  	NotFoundRetry int
    72  
    73  	// TargetAvailability 対象リソースのAvailabilityがこの状態になった場合になるまで待つ
    74  	//
    75  	// この値を指定する場合、ReadFuncにてAvailabilityHolderを返す必要がある。
    76  	// AvailabilityがTargetAvailabilityとPendingAvailabilityで指定されていない状態になった場合はUnexpectedAvailabilityErrorを返す
    77  	//
    78  	// TargetAvailability(Pending)とTargetInstanceState(Pending)の両方が指定された場合は両方を満たすまで待つ
    79  	// StateCheckFuncとの併用は不可。併用した場合はpanicする。
    80  	TargetAvailability []types.EAvailability
    81  
    82  	// PendingAvailability 対象リソースのAvailabilityがこの状態になった場合は待ちを継続する。
    83  	//
    84  	// 詳細はTargetAvailabilityのコメントを参照
    85  	PendingAvailability []types.EAvailability
    86  
    87  	// TargetInstanceStatus 対象リソースのInstanceStatusがこの状態になった場合になるまで待つ
    88  	//
    89  	// この値を指定する場合、ReadFuncにてInstanceStatusHolderを返す必要がある。
    90  	// InstanceStatusがTargetInstanceStatusとPendingInstanceStatusで指定されていない状態になった場合はUnexpectedInstanceStatusErrorを返す
    91  	//
    92  	// TargetAvailabilityとTargetInstanceStateの両方が指定された場合は両方を満たすまで待つ
    93  	//
    94  	// StateCheckFuncとの併用は不可。併用した場合はpanicする。
    95  	TargetInstanceStatus []types.EServerInstanceStatus
    96  
    97  	// PendingInstanceStatus 対象リソースのInstanceStatusがこの状態になった場合は待ちを継続する。
    98  	//
    99  	// 詳細はTargetInstanceStatusのコメントを参照
   100  	PendingInstanceStatus []types.EServerInstanceStatus
   101  
   102  	// RaiseErrorWithUnknownState State(AvailabilityとInstanceStatus)が予期しない値だった場合にエラーとするか
   103  	RaiseErrorWithUnknownState bool
   104  }
   105  
   106  // WaitForState リソースが指定の状態になるまで待つ
   107  func (w *StatePollingWaiter) WaitForState(ctx context.Context) (interface{}, error) {
   108  	c, p, e := w.WaitForStateAsync(ctx)
   109  	for {
   110  		select {
   111  		case <-ctx.Done():
   112  			return nil, ctx.Err()
   113  		case lastState := <-c:
   114  			return lastState, nil
   115  		case <-p:
   116  			// noop
   117  		case err := <-e:
   118  			return nil, err
   119  		}
   120  	}
   121  }
   122  
   123  // WaitForStateAsync リソースが指定の状態になるまで待つ
   124  func (w *StatePollingWaiter) WaitForStateAsync(ctx context.Context) (<-chan interface{}, <-chan interface{}, <-chan error) {
   125  	w.validateFields()
   126  	if w.Timeout == time.Duration(0) {
   127  		w.Timeout = defaults.DefaultStatePollingTimeout
   128  	}
   129  	if w.Interval == time.Duration(0) {
   130  		w.Interval = defaults.DefaultStatePollingInterval
   131  	}
   132  
   133  	waiter := wait.PollingWaiter{
   134  		ReadFunc:       w.readFunc(),
   135  		StateCheckFunc: w.stateCheckFunc,
   136  		Timeout:        w.Timeout,
   137  		Interval:       w.Interval,
   138  	}
   139  
   140  	return waiter.WaitForStateAsync(ctx)
   141  }
   142  
   143  func (w *StatePollingWaiter) readFunc() func() (interface{}, error) {
   144  	notFoundCounter := w.NotFoundRetry
   145  	return func() (interface{}, error) {
   146  		read, err := w.ReadFunc()
   147  		if err != nil {
   148  			if IsNotFoundError(err) {
   149  				notFoundCounter--
   150  				if notFoundCounter >= 0 {
   151  					return nil, nil
   152  				}
   153  			}
   154  			return nil, err
   155  		}
   156  		return read, err
   157  	}
   158  }
   159  
   160  func (w *StatePollingWaiter) validateFields() {
   161  	if w.ReadFunc == nil {
   162  		panic(errors.New("StatePollingWaiter has invalid setting: ReadFunc is required"))
   163  	}
   164  
   165  	if w.StateCheckFunc != nil && (len(w.TargetAvailability) > 0 || len(w.TargetInstanceStatus) > 0) {
   166  		panic(errors.New("StatePollingWaiter has invalid setting: StateCheckFunc and TargetAvailability/TargetInstanceStatus can not use together"))
   167  	}
   168  
   169  	if w.StateCheckFunc == nil && len(w.TargetAvailability) == 0 && len(w.TargetInstanceStatus) == 0 {
   170  		panic(errors.New("StatePollingWaiter has invalid setting: TargetAvailability or TargetInstanceState must have least 1 items when StateCheckFunc is not set"))
   171  	}
   172  }
   173  
   174  func (w *StatePollingWaiter) stateCheckFunc(state interface{}) (bool, error) {
   175  	if w.StateCheckFunc != nil {
   176  		return w.StateCheckFunc(state)
   177  	}
   178  
   179  	availabilityHolder, hasAvailability := state.(accessor.Availability)
   180  	instanceStateHolder, hasInstanceState := state.(accessor.InstanceStatus)
   181  
   182  	switch {
   183  	case hasAvailability && hasInstanceState:
   184  
   185  		res1, err := w.handleAvailability(availabilityHolder)
   186  		if err != nil {
   187  			return false, err
   188  		}
   189  		res2, err := w.handleInstanceState(instanceStateHolder)
   190  		if err != nil {
   191  			return false, err
   192  		}
   193  		return res1 && res2, nil
   194  
   195  	case hasAvailability:
   196  		return w.handleAvailability(availabilityHolder)
   197  	case hasInstanceState:
   198  		return w.handleInstanceState(instanceStateHolder)
   199  	default:
   200  		// どちらのインターフェースも実装していない場合、stateが存在するだけでtrueとする
   201  		return true, nil
   202  	}
   203  }
   204  
   205  func (w *StatePollingWaiter) handleAvailability(state accessor.Availability) (bool, error) {
   206  	if len(w.TargetAvailability) == 0 {
   207  		return true, nil
   208  	}
   209  	v := state.GetAvailability()
   210  	switch {
   211  	case w.isInAvailability(v, w.TargetAvailability):
   212  		return true, nil
   213  	case w.isInAvailability(v, w.PendingAvailability):
   214  		return false, nil
   215  	default:
   216  		var err error
   217  		if w.RaiseErrorWithUnknownState {
   218  			err = fmt.Errorf("got unexpected value of Availability: got %q", v)
   219  		}
   220  		return false, err
   221  	}
   222  }
   223  
   224  func (w *StatePollingWaiter) handleInstanceState(state accessor.InstanceStatus) (bool, error) {
   225  	if len(w.TargetInstanceStatus) == 0 {
   226  		return true, nil
   227  	}
   228  	v := state.GetInstanceStatus()
   229  	switch {
   230  	case w.isInInstanceStatus(v, w.TargetInstanceStatus):
   231  		return true, nil
   232  	case w.isInInstanceStatus(v, w.PendingInstanceStatus):
   233  		return false, nil
   234  	default:
   235  		var err error
   236  		if w.RaiseErrorWithUnknownState {
   237  			err = fmt.Errorf("got unexpected value of InstanceState: got %q", v)
   238  		}
   239  		return false, err
   240  	}
   241  }
   242  
   243  func (w *StatePollingWaiter) isInAvailability(v types.EAvailability, conditions []types.EAvailability) bool {
   244  	for _, cond := range conditions {
   245  		if v == cond {
   246  			return true
   247  		}
   248  	}
   249  	return false
   250  }
   251  
   252  func (w *StatePollingWaiter) isInInstanceStatus(v types.EServerInstanceStatus, conditions []types.EServerInstanceStatus) bool {
   253  	for _, cond := range conditions {
   254  		if v == cond {
   255  			return true
   256  		}
   257  	}
   258  	return false
   259  }