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 }