github.com/opentofu/opentofu@v1.7.1/internal/command/views/json/hook.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package json 7 8 import ( 9 "fmt" 10 "time" 11 12 "github.com/opentofu/opentofu/internal/addrs" 13 "github.com/opentofu/opentofu/internal/plans" 14 ) 15 16 type Hook interface { 17 HookType() MessageType 18 String() string 19 } 20 21 // ApplyStart: triggered by PreApply hook 22 type applyStart struct { 23 Resource ResourceAddr `json:"resource"` 24 Action ChangeAction `json:"action"` 25 IDKey string `json:"id_key,omitempty"` 26 IDValue string `json:"id_value,omitempty"` 27 actionVerb string 28 } 29 30 var _ Hook = (*applyStart)(nil) 31 32 func (h *applyStart) HookType() MessageType { 33 return MessageApplyStart 34 } 35 36 func (h *applyStart) String() string { 37 var id string 38 if h.IDKey != "" && h.IDValue != "" { 39 id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) 40 } 41 return fmt.Sprintf("%s: %s...%s", h.Resource.Addr, h.actionVerb, id) 42 } 43 44 func NewApplyStart(addr addrs.AbsResourceInstance, action plans.Action, idKey string, idValue string) Hook { 45 hook := &applyStart{ 46 Resource: newResourceAddr(addr), 47 Action: changeAction(action), 48 IDKey: idKey, 49 IDValue: idValue, 50 actionVerb: startActionVerb(action), 51 } 52 53 return hook 54 } 55 56 // ApplyProgress: currently triggered by a timer started on PreApply. In 57 // future, this might also be triggered by provider progress reporting. 58 type applyProgress struct { 59 Resource ResourceAddr `json:"resource"` 60 Action ChangeAction `json:"action"` 61 Elapsed float64 `json:"elapsed_seconds"` 62 actionVerb string 63 elapsed time.Duration 64 } 65 66 var _ Hook = (*applyProgress)(nil) 67 68 func (h *applyProgress) HookType() MessageType { 69 return MessageApplyProgress 70 } 71 72 func (h *applyProgress) String() string { 73 return fmt.Sprintf("%s: Still %s... [%s elapsed]", h.Resource.Addr, h.actionVerb, h.elapsed) 74 } 75 76 func NewApplyProgress(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { 77 return &applyProgress{ 78 Resource: newResourceAddr(addr), 79 Action: changeAction(action), 80 Elapsed: elapsed.Seconds(), 81 actionVerb: progressActionVerb(action), 82 elapsed: elapsed, 83 } 84 } 85 86 // ApplyComplete: triggered by PostApply hook 87 type applyComplete struct { 88 Resource ResourceAddr `json:"resource"` 89 Action ChangeAction `json:"action"` 90 IDKey string `json:"id_key,omitempty"` 91 IDValue string `json:"id_value,omitempty"` 92 Elapsed float64 `json:"elapsed_seconds"` 93 actionNoun string 94 elapsed time.Duration 95 } 96 97 var _ Hook = (*applyComplete)(nil) 98 99 func (h *applyComplete) HookType() MessageType { 100 return MessageApplyComplete 101 } 102 103 func (h *applyComplete) String() string { 104 var id string 105 if h.IDKey != "" && h.IDValue != "" { 106 id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) 107 } 108 return fmt.Sprintf("%s: %s complete after %s%s", h.Resource.Addr, h.actionNoun, h.elapsed, id) 109 } 110 111 func NewApplyComplete(addr addrs.AbsResourceInstance, action plans.Action, idKey, idValue string, elapsed time.Duration) Hook { 112 return &applyComplete{ 113 Resource: newResourceAddr(addr), 114 Action: changeAction(action), 115 IDKey: idKey, 116 IDValue: idValue, 117 Elapsed: elapsed.Seconds(), 118 actionNoun: actionNoun(action), 119 elapsed: elapsed, 120 } 121 } 122 123 // ApplyErrored: triggered by PostApply hook on failure. This will be followed 124 // by diagnostics when the apply finishes. 125 type applyErrored struct { 126 Resource ResourceAddr `json:"resource"` 127 Action ChangeAction `json:"action"` 128 Elapsed float64 `json:"elapsed_seconds"` 129 actionNoun string 130 elapsed time.Duration 131 } 132 133 var _ Hook = (*applyErrored)(nil) 134 135 func (h *applyErrored) HookType() MessageType { 136 return MessageApplyErrored 137 } 138 139 func (h *applyErrored) String() string { 140 return fmt.Sprintf("%s: %s errored after %s", h.Resource.Addr, h.actionNoun, h.elapsed) 141 } 142 143 func NewApplyErrored(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { 144 return &applyErrored{ 145 Resource: newResourceAddr(addr), 146 Action: changeAction(action), 147 Elapsed: elapsed.Seconds(), 148 actionNoun: actionNoun(action), 149 elapsed: elapsed, 150 } 151 } 152 153 // ProvisionStart: triggered by PreProvisionInstanceStep hook 154 type provisionStart struct { 155 Resource ResourceAddr `json:"resource"` 156 Provisioner string `json:"provisioner"` 157 } 158 159 var _ Hook = (*provisionStart)(nil) 160 161 func (h *provisionStart) HookType() MessageType { 162 return MessageProvisionStart 163 } 164 165 func (h *provisionStart) String() string { 166 return fmt.Sprintf("%s: Provisioning with '%s'...", h.Resource.Addr, h.Provisioner) 167 } 168 169 func NewProvisionStart(addr addrs.AbsResourceInstance, provisioner string) Hook { 170 return &provisionStart{ 171 Resource: newResourceAddr(addr), 172 Provisioner: provisioner, 173 } 174 } 175 176 // ProvisionProgress: triggered by ProvisionOutput hook 177 type provisionProgress struct { 178 Resource ResourceAddr `json:"resource"` 179 Provisioner string `json:"provisioner"` 180 Output string `json:"output"` 181 } 182 183 var _ Hook = (*provisionProgress)(nil) 184 185 func (h *provisionProgress) HookType() MessageType { 186 return MessageProvisionProgress 187 } 188 189 func (h *provisionProgress) String() string { 190 return fmt.Sprintf("%s: (%s): %s", h.Resource.Addr, h.Provisioner, h.Output) 191 } 192 193 func NewProvisionProgress(addr addrs.AbsResourceInstance, provisioner string, output string) Hook { 194 return &provisionProgress{ 195 Resource: newResourceAddr(addr), 196 Provisioner: provisioner, 197 Output: output, 198 } 199 } 200 201 // ProvisionComplete: triggered by PostProvisionInstanceStep hook 202 type provisionComplete struct { 203 Resource ResourceAddr `json:"resource"` 204 Provisioner string `json:"provisioner"` 205 } 206 207 var _ Hook = (*provisionComplete)(nil) 208 209 func (h *provisionComplete) HookType() MessageType { 210 return MessageProvisionComplete 211 } 212 213 func (h *provisionComplete) String() string { 214 return fmt.Sprintf("%s: (%s) Provisioning complete", h.Resource.Addr, h.Provisioner) 215 } 216 217 func NewProvisionComplete(addr addrs.AbsResourceInstance, provisioner string) Hook { 218 return &provisionComplete{ 219 Resource: newResourceAddr(addr), 220 Provisioner: provisioner, 221 } 222 } 223 224 // ProvisionErrored: triggered by PostProvisionInstanceStep hook on failure. 225 // This will be followed by diagnostics when the apply finishes. 226 type provisionErrored struct { 227 Resource ResourceAddr `json:"resource"` 228 Provisioner string `json:"provisioner"` 229 } 230 231 var _ Hook = (*provisionErrored)(nil) 232 233 func (h *provisionErrored) HookType() MessageType { 234 return MessageProvisionErrored 235 } 236 237 func (h *provisionErrored) String() string { 238 return fmt.Sprintf("%s: (%s) Provisioning errored", h.Resource.Addr, h.Provisioner) 239 } 240 241 func NewProvisionErrored(addr addrs.AbsResourceInstance, provisioner string) Hook { 242 return &provisionErrored{ 243 Resource: newResourceAddr(addr), 244 Provisioner: provisioner, 245 } 246 } 247 248 // RefreshStart: triggered by PreRefresh hook 249 type refreshStart struct { 250 Resource ResourceAddr `json:"resource"` 251 IDKey string `json:"id_key,omitempty"` 252 IDValue string `json:"id_value,omitempty"` 253 } 254 255 var _ Hook = (*refreshStart)(nil) 256 257 func (h *refreshStart) HookType() MessageType { 258 return MessageRefreshStart 259 } 260 261 func (h *refreshStart) String() string { 262 var id string 263 if h.IDKey != "" && h.IDValue != "" { 264 id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) 265 } 266 return fmt.Sprintf("%s: Refreshing state...%s", h.Resource.Addr, id) 267 } 268 269 func NewRefreshStart(addr addrs.AbsResourceInstance, idKey, idValue string) Hook { 270 return &refreshStart{ 271 Resource: newResourceAddr(addr), 272 IDKey: idKey, 273 IDValue: idValue, 274 } 275 } 276 277 // RefreshComplete: triggered by PostRefresh hook 278 type refreshComplete struct { 279 Resource ResourceAddr `json:"resource"` 280 IDKey string `json:"id_key,omitempty"` 281 IDValue string `json:"id_value,omitempty"` 282 } 283 284 var _ Hook = (*refreshComplete)(nil) 285 286 func (h *refreshComplete) HookType() MessageType { 287 return MessageRefreshComplete 288 } 289 290 func (h *refreshComplete) String() string { 291 var id string 292 if h.IDKey != "" && h.IDValue != "" { 293 id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) 294 } 295 return fmt.Sprintf("%s: Refresh complete%s", h.Resource.Addr, id) 296 } 297 298 func NewRefreshComplete(addr addrs.AbsResourceInstance, idKey, idValue string) Hook { 299 return &refreshComplete{ 300 Resource: newResourceAddr(addr), 301 IDKey: idKey, 302 IDValue: idValue, 303 } 304 } 305 306 // Convert the subset of plans.Action values we expect to receive into a 307 // present-tense verb for the applyStart hook message. 308 func startActionVerb(action plans.Action) string { 309 switch action { 310 case plans.Create: 311 return "Creating" 312 case plans.Update: 313 return "Modifying" 314 case plans.Delete: 315 return "Destroying" 316 case plans.Read: 317 return "Refreshing" 318 case plans.CreateThenDelete, plans.DeleteThenCreate: 319 // This is not currently possible to reach, as we receive separate 320 // passes for create and delete 321 return "Replacing" 322 case plans.Forget: 323 return "Removing" 324 case plans.NoOp: 325 // This should never be possible: a no-op planned change should not 326 // be applied. We'll fall back to "Applying". 327 fallthrough 328 default: 329 return "Applying" 330 } 331 } 332 333 // Convert the subset of plans.Action values we expect to receive into a 334 // present-tense verb for the applyProgress hook message. This will be 335 // prefixed with "Still ", so it is lower-case. 336 func progressActionVerb(action plans.Action) string { 337 switch action { 338 case plans.Create: 339 return "creating" 340 case plans.Update: 341 return "modifying" 342 case plans.Delete: 343 return "destroying" 344 case plans.Read: 345 return "refreshing" 346 case plans.CreateThenDelete, plans.DeleteThenCreate: 347 // This is not currently possible to reach, as we receive separate 348 // passes for create and delete 349 return "replacing" 350 case plans.Forget: 351 return "removing" 352 case plans.NoOp: 353 // This should never be possible: a no-op planned change should not 354 // be applied. We'll fall back to "applying". 355 fallthrough 356 default: 357 return "applying" 358 } 359 } 360 361 // Convert the subset of plans.Action values we expect to receive into a 362 // noun for the applyComplete and applyErrored hook messages. This will be 363 // combined into a phrase like "Creation complete after 1m4s". 364 func actionNoun(action plans.Action) string { 365 switch action { 366 case plans.Create: 367 return "Creation" 368 case plans.Update: 369 return "Modifications" 370 case plans.Delete: 371 return "Destruction" 372 case plans.Read: 373 return "Refresh" 374 case plans.CreateThenDelete, plans.DeleteThenCreate: 375 // This is not currently possible to reach, as we receive separate 376 // passes for create and delete 377 return "Replacement" 378 case plans.Forget: 379 return "Removal" 380 case plans.NoOp: 381 // This should never be possible: a no-op planned change should not 382 // be applied. We'll fall back to "Apply". 383 fallthrough 384 default: 385 return "Apply" 386 } 387 }