state.go raw

   1  // Copyright 2022-2025 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  }
 260