github.com/leg100/ots@v0.0.7-0.20210919080622-034055ced4bd/plan.go (about)

     1  package ots
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  
     7  	tfe "github.com/leg100/go-tfe"
     8  	"gorm.io/gorm"
     9  )
    10  
    11  const (
    12  	LocalStateFilename  = "terraform.tfstate"
    13  	PlanFilename        = "plan.out"
    14  	JSONPlanFilename    = "plan.out.json"
    15  	ApplyOutputFilename = "apply.out"
    16  )
    17  
    18  // Plan represents a Terraform Enterprise plan.
    19  type Plan struct {
    20  	ID string
    21  
    22  	gorm.Model
    23  
    24  	ResourceAdditions    int
    25  	ResourceChanges      int
    26  	ResourceDestructions int
    27  	Status               tfe.PlanStatus
    28  	StatusTimestamps     *tfe.PlanStatusTimestamps
    29  
    30  	// LogsBlobID is the blob ID for the log output from a terraform plan
    31  	LogsBlobID string
    32  
    33  	// PlanFileBlobID is the blob ID of the execution plan file in binary format
    34  	PlanFileBlobID string
    35  
    36  	// PlanJSONBlobID is the blob ID of the execution plan file in json format
    37  	PlanJSONBlobID string
    38  }
    39  
    40  type PlanService interface {
    41  	Get(id string) (*Plan, error)
    42  	GetPlanJSON(id string) ([]byte, error)
    43  }
    44  
    45  // PlanFinishOptions represents the options for finishing a plan.
    46  type PlanFinishOptions struct {
    47  	// Type is a public field utilized by JSON:API to set the resource type via
    48  	// the field tag.  It is not a user-defined value and does not need to be
    49  	// set.  https://jsonapi.org/format/#crud-creating
    50  	Type string `jsonapi:"primary,plans"`
    51  
    52  	ResourceAdditions    int `jsonapi:"attr,resource-additions"`
    53  	ResourceChanges      int `jsonapi:"attr,resource-changes"`
    54  	ResourceDestructions int `jsonapi:"attr,resource-destructions"`
    55  }
    56  
    57  func newPlan() *Plan {
    58  	return &Plan{
    59  		ID:               GenerateID("plan"),
    60  		StatusTimestamps: &tfe.PlanStatusTimestamps{},
    61  		LogsBlobID:       NewBlobID(),
    62  		PlanFileBlobID:   NewBlobID(),
    63  		PlanJSONBlobID:   NewBlobID(),
    64  	}
    65  }
    66  
    67  // HasChanges determines whether plan has any changes (adds/changes/deletions).
    68  func (p *Plan) HasChanges() bool {
    69  	if p.ResourceAdditions > 0 || p.ResourceChanges > 0 || p.ResourceDestructions > 0 {
    70  		return true
    71  	}
    72  	return false
    73  }
    74  
    75  func (p *Plan) GetLogsBlobID() string {
    76  	return p.LogsBlobID
    77  }
    78  
    79  func (p *Plan) Do(run *Run, exe *Executor) error {
    80  	if err := exe.RunCLI("terraform", "plan", "-no-color", fmt.Sprintf("-out=%s", PlanFilename)); err != nil {
    81  		return err
    82  	}
    83  
    84  	if err := exe.RunCLI("sh", "-c", fmt.Sprintf("terraform show -json %s > %s", PlanFilename, JSONPlanFilename)); err != nil {
    85  		return err
    86  	}
    87  
    88  	if err := exe.RunFunc(run.uploadPlan); err != nil {
    89  		return err
    90  	}
    91  
    92  	if err := exe.RunFunc(run.uploadJSONPlan); err != nil {
    93  		return err
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  // UpdateResources parses the plan file produced from terraform plan to
   100  // determine the number and type of resource changes planned and updates the
   101  // plan object accordingly.
   102  func (p *Plan) UpdateResources(bs BlobStore) error {
   103  	jsonFile, err := bs.Get(p.PlanJSONBlobID)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	planFile := PlanFile{}
   109  	if err := json.Unmarshal(jsonFile, &planFile); err != nil {
   110  		return err
   111  	}
   112  
   113  	// Parse plan output
   114  	adds, updates, deletes := planFile.Changes()
   115  
   116  	// Update status
   117  	p.ResourceAdditions = adds
   118  	p.ResourceChanges = updates
   119  	p.ResourceDestructions = deletes
   120  
   121  	return nil
   122  }
   123  
   124  func (p *Plan) UpdateStatus(status tfe.PlanStatus) {
   125  	p.Status = status
   126  	p.setTimestamp(status)
   127  }
   128  
   129  func (p *Plan) setTimestamp(status tfe.PlanStatus) {
   130  	switch status {
   131  	case tfe.PlanCanceled:
   132  		p.StatusTimestamps.CanceledAt = TimeNow()
   133  	case tfe.PlanErrored:
   134  		p.StatusTimestamps.ErroredAt = TimeNow()
   135  	case tfe.PlanFinished:
   136  		p.StatusTimestamps.FinishedAt = TimeNow()
   137  	case tfe.PlanQueued:
   138  		p.StatusTimestamps.QueuedAt = TimeNow()
   139  	case tfe.PlanRunning:
   140  		p.StatusTimestamps.StartedAt = TimeNow()
   141  	}
   142  }