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  }