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