github.com/leg100/ots@v0.0.7-0.20210919080622-034055ced4bd/run.go (about) 1 package ots 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/md5" 7 "encoding/base64" 8 "errors" 9 "fmt" 10 "os" 11 "path/filepath" 12 "time" 13 14 tfe "github.com/leg100/go-tfe" 15 "gorm.io/gorm" 16 ) 17 18 const ( 19 // DefaultRefresh specifies that the state be refreshed prior to running a 20 // plan 21 DefaultRefresh = true 22 ) 23 24 var ( 25 ErrRunDiscardNotAllowed = errors.New("run was not paused for confirmation or priority; discard not allowed") 26 ErrRunCancelNotAllowed = errors.New("run was not planning or applying; cancel not allowed") 27 ErrRunForceCancelNotAllowed = errors.New("run was not planning or applying, has not been canceled non-forcefully, or the cool-off period has not yet passed") 28 29 ErrInvalidRunGetOptions = errors.New("invalid run get options") 30 31 // ActiveRunStatuses are those run statuses that deem a run to be active. 32 // There can only be at most one active run for a workspace. 33 ActiveRunStatuses = []tfe.RunStatus{ 34 tfe.RunApplyQueued, 35 tfe.RunApplying, 36 tfe.RunConfirmed, 37 tfe.RunPlanQueued, 38 tfe.RunPlanned, 39 tfe.RunPlanning, 40 } 41 ) 42 43 type Run struct { 44 ID string 45 46 gorm.Model 47 48 ForceCancelAvailableAt time.Time 49 IsDestroy bool 50 Message string 51 Permissions *tfe.RunPermissions 52 PositionInQueue int 53 Refresh bool 54 RefreshOnly bool 55 Status tfe.RunStatus 56 StatusTimestamps *tfe.RunStatusTimestamps 57 ReplaceAddrs []string 58 TargetAddrs []string 59 60 // Relations 61 Plan *Plan 62 Apply *Apply 63 Workspace *Workspace 64 ConfigurationVersion *ConfigurationVersion 65 } 66 67 // Phase implementations represent the phases that make up a run: a plan and an 68 // apply. 69 type Phase interface { 70 GetLogsBlobID() string 71 Do(*Run, *Executor) error 72 } 73 74 // RunFactory is a factory for constructing Run objects. 75 type RunFactory struct { 76 ConfigurationVersionService ConfigurationVersionService 77 WorkspaceService WorkspaceService 78 } 79 80 // RunService implementations allow interactions with runs 81 type RunService interface { 82 Create(opts *tfe.RunCreateOptions) (*Run, error) 83 Get(id string) (*Run, error) 84 List(opts RunListOptions) (*RunList, error) 85 Apply(id string, opts *tfe.RunApplyOptions) error 86 Discard(id string, opts *tfe.RunDiscardOptions) error 87 Cancel(id string, opts *tfe.RunCancelOptions) error 88 ForceCancel(id string, opts *tfe.RunForceCancelOptions) error 89 EnqueuePlan(id string) error 90 GetPlanLogs(id string, opts GetChunkOptions) ([]byte, error) 91 GetApplyLogs(id string, opts GetChunkOptions) ([]byte, error) 92 GetPlanJSON(id string) ([]byte, error) 93 GetPlanFile(id string) ([]byte, error) 94 UploadPlan(runID string, plan []byte, json bool) error 95 96 JobService 97 } 98 99 // RunStore implementations persist Run objects. 100 type RunStore interface { 101 Create(run *Run) (*Run, error) 102 Get(opts RunGetOptions) (*Run, error) 103 List(opts RunListOptions) (*RunList, error) 104 // TODO: add support for a special error type that tells update to skip 105 // updates - useful when fn checks current fields and decides not to update 106 Update(id string, fn func(*Run) error) (*Run, error) 107 } 108 109 // RunList represents a list of runs. 110 type RunList struct { 111 *tfe.Pagination 112 Items []*Run 113 } 114 115 // RunGetOptions are options for retrieving a single Run. Either ID or ApplyID 116 // or PlanID must be specfiied. 117 type RunGetOptions struct { 118 // ID of run to retrieve 119 ID *string 120 121 // Get run via apply ID 122 ApplyID *string 123 124 // Get run via plan ID 125 PlanID *string 126 } 127 128 // RunListOptions are options for paginating and filtering a list of runs 129 type RunListOptions struct { 130 tfe.RunListOptions 131 132 // Filter by run statuses (with an implicit OR condition) 133 Statuses []tfe.RunStatus 134 135 // Filter by workspace ID 136 WorkspaceID *string 137 } 138 139 func (r *Run) GetID() string { 140 return r.ID 141 } 142 143 func (r *Run) GetStatus() string { 144 return string(r.Status) 145 } 146 147 // Discard updates the state of a run to reflect it having been discarded. 148 func (r *Run) Discard() error { 149 if !r.IsDiscardable() { 150 return ErrRunDiscardNotAllowed 151 } 152 153 r.UpdateStatus(tfe.RunDiscarded) 154 155 return nil 156 } 157 158 // Cancel run. 159 func (r *Run) Cancel() error { 160 if !r.IsCancelable() { 161 return ErrRunCancelNotAllowed 162 } 163 164 // Run can be forcefully cancelled after a cool-off period of ten seconds 165 r.ForceCancelAvailableAt = time.Now().Add(10 * time.Second) 166 167 r.UpdateStatus(tfe.RunCanceled) 168 169 return nil 170 } 171 172 // ForceCancel updates the state of a run to reflect it having been forcefully 173 // cancelled. 174 func (r *Run) ForceCancel() error { 175 if !r.IsForceCancelable() { 176 return ErrRunForceCancelNotAllowed 177 } 178 179 r.StatusTimestamps.ForceCanceledAt = TimeNow() 180 181 return nil 182 } 183 184 // Actions lists which actions are currently invokable. 185 func (r *Run) Actions() *tfe.RunActions { 186 return &tfe.RunActions{ 187 IsCancelable: r.IsCancelable(), 188 IsConfirmable: r.IsConfirmable(), 189 IsForceCancelable: r.IsForceCancelable(), 190 IsDiscardable: r.IsDiscardable(), 191 } 192 } 193 194 // IsCancelable determines whether run can be cancelled. 195 func (r *Run) IsCancelable() bool { 196 switch r.Status { 197 case tfe.RunPending, tfe.RunPlanQueued, tfe.RunPlanning, tfe.RunApplyQueued, tfe.RunApplying: 198 return true 199 default: 200 return false 201 } 202 } 203 204 // IsConfirmable determines whether run can be confirmed. 205 func (r *Run) IsConfirmable() bool { 206 switch r.Status { 207 case tfe.RunPlanned: 208 return true 209 default: 210 return false 211 } 212 } 213 214 // IsDiscardable determines whether run can be discarded. 215 func (r *Run) IsDiscardable() bool { 216 switch r.Status { 217 case tfe.RunPending, tfe.RunPolicyChecked, tfe.RunPolicyOverride, tfe.RunPlanned: 218 return true 219 default: 220 return false 221 } 222 } 223 224 // IsForceCancelable determines whether a run can be forcibly cancelled. 225 func (r *Run) IsForceCancelable() bool { 226 return r.IsCancelable() && !r.ForceCancelAvailableAt.IsZero() && time.Now().After(r.ForceCancelAvailableAt) 227 } 228 229 // IsActive determines whether run is currently the active run on a workspace, 230 // i.e. it is neither finished nor pending 231 func (r *Run) IsActive() bool { 232 if r.IsDone() || r.Status == tfe.RunPending { 233 return false 234 } 235 return true 236 } 237 238 // IsDone determines whether run has reached an end state, e.g. applied, 239 // discarded, etc. 240 func (r *Run) IsDone() bool { 241 switch r.Status { 242 case tfe.RunApplied, tfe.RunPlannedAndFinished, tfe.RunDiscarded, tfe.RunCanceled, tfe.RunErrored: 243 return true 244 default: 245 return false 246 } 247 } 248 249 type ErrInvalidRunStatusTransition struct { 250 From tfe.RunStatus 251 To tfe.RunStatus 252 } 253 254 func (e ErrInvalidRunStatusTransition) Error() string { 255 return fmt.Sprintf("invalid run status transition from %s to %s", e.From, e.To) 256 } 257 258 func (r *Run) IsSpeculative() bool { 259 return r.ConfigurationVersion.Speculative 260 } 261 262 // ActivePhase retrieves the currently active phase 263 func (r *Run) ActivePhase() (Phase, error) { 264 switch r.Status { 265 case tfe.RunPlanning: 266 return r.Plan, nil 267 case tfe.RunApplying: 268 return r.Apply, nil 269 default: 270 return nil, fmt.Errorf("invalid run status: %s", r.Status) 271 } 272 } 273 274 // Start starts a run phase. 275 func (r *Run) Start() error { 276 switch r.Status { 277 case tfe.RunPlanQueued: 278 r.UpdateStatus(tfe.RunPlanning) 279 case tfe.RunApplyQueued: 280 r.UpdateStatus(tfe.RunApplying) 281 default: 282 return fmt.Errorf("run cannot be started: invalid status: %s", r.Status) 283 } 284 285 return nil 286 } 287 288 // Finish updates the run to reflect the current phase having finished. An event 289 // is emitted reflecting the run's new status. 290 func (r *Run) Finish(bs BlobStore) (*Event, error) { 291 if r.Status == tfe.RunApplying { 292 r.UpdateStatus(tfe.RunApplied) 293 294 if err := r.Apply.UpdateResources(bs); err != nil { 295 return nil, err 296 } 297 298 return &Event{Payload: r, Type: RunApplied}, nil 299 } 300 301 // Only remaining valid status is planning 302 if r.Status != tfe.RunPlanning { 303 return nil, fmt.Errorf("run cannot be finished: invalid status: %s", r.Status) 304 } 305 306 if err := r.Plan.UpdateResources(bs); err != nil { 307 return nil, err 308 } 309 310 // Speculative plan, proceed no further 311 if r.ConfigurationVersion.Speculative { 312 r.UpdateStatus(tfe.RunPlannedAndFinished) 313 return &Event{Payload: r, Type: RunPlannedAndFinished}, nil 314 } 315 316 r.UpdateStatus(tfe.RunPlanned) 317 318 if r.Workspace.AutoApply { 319 r.UpdateStatus(tfe.RunApplyQueued) 320 return &Event{Type: ApplyQueued, Payload: r}, nil 321 } 322 323 return &Event{Payload: r, Type: RunPlanned}, nil 324 } 325 326 // UpdateStatus updates the status of the run as well as its plan and apply 327 func (r *Run) UpdateStatus(status tfe.RunStatus) { 328 switch status { 329 case tfe.RunPending: 330 r.Plan.UpdateStatus(tfe.PlanPending) 331 case tfe.RunPlanQueued: 332 r.Plan.UpdateStatus(tfe.PlanQueued) 333 case tfe.RunPlanning: 334 r.Plan.UpdateStatus(tfe.PlanRunning) 335 case tfe.RunPlanned, tfe.RunPlannedAndFinished: 336 r.Plan.UpdateStatus(tfe.PlanFinished) 337 case tfe.RunApplyQueued: 338 r.Apply.UpdateStatus(tfe.ApplyQueued) 339 case tfe.RunApplying: 340 r.Apply.UpdateStatus(tfe.ApplyRunning) 341 case tfe.RunApplied: 342 r.Apply.UpdateStatus(tfe.ApplyFinished) 343 case tfe.RunErrored: 344 switch r.Status { 345 case tfe.RunPlanning: 346 r.Plan.UpdateStatus(tfe.PlanErrored) 347 case tfe.RunApplying: 348 r.Apply.UpdateStatus(tfe.ApplyErrored) 349 } 350 case tfe.RunCanceled: 351 switch r.Status { 352 case tfe.RunPlanQueued, tfe.RunPlanning: 353 r.Plan.UpdateStatus(tfe.PlanCanceled) 354 case tfe.RunApplyQueued, tfe.RunApplying: 355 r.Apply.UpdateStatus(tfe.ApplyCanceled) 356 } 357 } 358 359 r.Status = status 360 r.setTimestamp(status) 361 362 // TODO: determine when tfe.ApplyUnreachable is applicable and set 363 // accordingly 364 } 365 366 func (r *Run) setTimestamp(status tfe.RunStatus) { 367 switch status { 368 case tfe.RunPending: 369 r.StatusTimestamps.PlanQueueableAt = TimeNow() 370 case tfe.RunPlanQueued: 371 r.StatusTimestamps.PlanQueuedAt = TimeNow() 372 case tfe.RunPlanning: 373 r.StatusTimestamps.PlanningAt = TimeNow() 374 case tfe.RunPlanned: 375 r.StatusTimestamps.PlannedAt = TimeNow() 376 case tfe.RunPlannedAndFinished: 377 r.StatusTimestamps.PlannedAndFinishedAt = TimeNow() 378 case tfe.RunApplyQueued: 379 r.StatusTimestamps.ApplyQueuedAt = TimeNow() 380 case tfe.RunApplying: 381 r.StatusTimestamps.ApplyingAt = TimeNow() 382 case tfe.RunApplied: 383 r.StatusTimestamps.AppliedAt = TimeNow() 384 case tfe.RunErrored: 385 r.StatusTimestamps.ErroredAt = TimeNow() 386 case tfe.RunCanceled: 387 r.StatusTimestamps.CanceledAt = TimeNow() 388 case tfe.RunDiscarded: 389 r.StatusTimestamps.DiscardedAt = TimeNow() 390 } 391 } 392 393 func (r *Run) Do(exe *Executor) error { 394 if err := exe.RunFunc(r.downloadConfig); err != nil { 395 return err 396 } 397 398 if err := exe.RunFunc(deleteBackendConfigFromDirectory); err != nil { 399 return err 400 } 401 402 if err := exe.RunFunc(r.downloadState); err != nil { 403 return err 404 } 405 406 if err := exe.RunCLI("terraform", "init", "-no-color"); err != nil { 407 return err 408 } 409 410 phase, err := r.ActivePhase() 411 if err != nil { 412 return err 413 } 414 415 if err := phase.Do(r, exe); err != nil { 416 return err 417 } 418 419 return nil 420 } 421 422 func (r *Run) downloadConfig(ctx context.Context, exe *Executor) error { 423 // Download config 424 cv, err := exe.ConfigurationVersionService.Download(r.ConfigurationVersion.ID) 425 if err != nil { 426 return fmt.Errorf("unable to download config: %w", err) 427 } 428 429 // Decompress and untar config 430 if err := Unpack(bytes.NewBuffer(cv), exe.Path); err != nil { 431 return fmt.Errorf("unable to unpack config: %w", err) 432 } 433 434 return nil 435 } 436 437 // downloadState downloads current state to disk. If there is no state yet 438 // nothing will be downloaded and no error will be reported. 439 func (r *Run) downloadState(ctx context.Context, exe *Executor) error { 440 state, err := exe.StateVersionService.Current(r.Workspace.ID) 441 if IsNotFound(err) { 442 return nil 443 } else if err != nil { 444 return err 445 } 446 447 statefile, err := exe.StateVersionService.Download(state.ID) 448 if err != nil { 449 return err 450 } 451 452 if err := os.WriteFile(filepath.Join(exe.Path, LocalStateFilename), statefile, 0644); err != nil { 453 return err 454 } 455 456 return nil 457 } 458 459 func (r *Run) uploadPlan(ctx context.Context, exe *Executor) error { 460 file, err := os.ReadFile(filepath.Join(exe.Path, PlanFilename)) 461 if err != nil { 462 return err 463 } 464 465 if err := exe.RunService.UploadPlan(r.ID, file, false); err != nil { 466 return fmt.Errorf("unable to upload plan: %w", err) 467 } 468 469 return nil 470 } 471 472 func (r *Run) uploadJSONPlan(ctx context.Context, exe *Executor) error { 473 jsonFile, err := os.ReadFile(filepath.Join(exe.Path, JSONPlanFilename)) 474 if err != nil { 475 return err 476 } 477 478 if err := exe.RunService.UploadPlan(r.ID, jsonFile, true); err != nil { 479 return fmt.Errorf("unable to upload JSON plan: %w", err) 480 } 481 482 return nil 483 } 484 485 func (r *Run) downloadPlanFile(ctx context.Context, exe *Executor) error { 486 plan, err := exe.RunService.GetPlanFile(r.ID) 487 if err != nil { 488 return err 489 } 490 491 return os.WriteFile(filepath.Join(exe.Path, PlanFilename), plan, 0644) 492 } 493 494 // uploadState reads, parses, and uploads state 495 func (r *Run) uploadState(ctx context.Context, exe *Executor) error { 496 stateFile, err := os.ReadFile(filepath.Join(exe.Path, LocalStateFilename)) 497 if err != nil { 498 return err 499 } 500 501 state, err := Parse(stateFile) 502 if err != nil { 503 return err 504 } 505 506 _, err = exe.StateVersionService.Create(r.Workspace.ID, tfe.StateVersionCreateOptions{ 507 State: String(base64.StdEncoding.EncodeToString(stateFile)), 508 MD5: String(fmt.Sprintf("%x", md5.Sum(stateFile))), 509 Lineage: &state.Lineage, 510 Serial: Int64(state.Serial), 511 }) 512 if err != nil { 513 return err 514 } 515 516 return nil 517 } 518 519 // NewRun constructs a run object. 520 func (f *RunFactory) NewRun(opts *tfe.RunCreateOptions) (*Run, error) { 521 if opts.Workspace == nil { 522 return nil, errors.New("workspace is required") 523 } 524 525 run := Run{ 526 ID: GenerateID("run"), 527 Permissions: &tfe.RunPermissions{ 528 CanForceCancel: true, 529 CanApply: true, 530 CanCancel: true, 531 CanDiscard: true, 532 CanForceExecute: true, 533 }, 534 Refresh: DefaultRefresh, 535 ReplaceAddrs: opts.ReplaceAddrs, 536 TargetAddrs: opts.TargetAddrs, 537 StatusTimestamps: &tfe.RunStatusTimestamps{}, 538 Plan: newPlan(), 539 Apply: newApply(), 540 } 541 542 run.UpdateStatus(tfe.RunPending) 543 544 ws, err := f.WorkspaceService.Get(WorkspaceSpecifier{ID: &opts.Workspace.ID}) 545 if err != nil { 546 return nil, err 547 } 548 run.Workspace = ws 549 550 cv, err := f.getConfigurationVersion(opts) 551 if err != nil { 552 return nil, err 553 } 554 run.ConfigurationVersion = cv 555 556 if opts.IsDestroy != nil { 557 run.IsDestroy = *opts.IsDestroy 558 } 559 560 if opts.Message != nil { 561 run.Message = *opts.Message 562 } 563 564 if opts.Refresh != nil { 565 run.Refresh = *opts.Refresh 566 } 567 568 return &run, nil 569 } 570 571 func (f *RunFactory) getConfigurationVersion(opts *tfe.RunCreateOptions) (*ConfigurationVersion, error) { 572 // Unless CV ID provided, get workspace's latest CV 573 if opts.ConfigurationVersion != nil { 574 return f.ConfigurationVersionService.Get(opts.ConfigurationVersion.ID) 575 } 576 return f.ConfigurationVersionService.GetLatest(opts.Workspace.ID) 577 }