1package tfe 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/url" 9 "time" 10) 11 12// Compile-time proof of interface implementation. 13var _ Plans = (*plans)(nil) 14 15// Plans describes all the plan related methods that the Terraform Enterprise 16// API supports. 17// 18// TFE API docs: https://www.terraform.io/docs/enterprise/api/plan.html 19type Plans interface { 20 // Read a plan by its ID. 21 Read(ctx context.Context, planID string) (*Plan, error) 22 23 // Logs retrieves the logs of a plan. 24 Logs(ctx context.Context, planID string) (io.Reader, error) 25} 26 27// plans implements Plans. 28type plans struct { 29 client *Client 30} 31 32// PlanStatus represents a plan state. 33type PlanStatus string 34 35//List all available plan statuses. 36const ( 37 PlanCanceled PlanStatus = "canceled" 38 PlanCreated PlanStatus = "created" 39 PlanErrored PlanStatus = "errored" 40 PlanFinished PlanStatus = "finished" 41 PlanMFAWaiting PlanStatus = "mfa_waiting" 42 PlanPending PlanStatus = "pending" 43 PlanQueued PlanStatus = "queued" 44 PlanRunning PlanStatus = "running" 45 PlanUnreachable PlanStatus = "unreachable" 46) 47 48// Plan represents a Terraform Enterprise plan. 49type Plan struct { 50 ID string `jsonapi:"primary,plans"` 51 HasChanges bool `jsonapi:"attr,has-changes"` 52 LogReadURL string `jsonapi:"attr,log-read-url"` 53 ResourceAdditions int `jsonapi:"attr,resource-additions"` 54 ResourceChanges int `jsonapi:"attr,resource-changes"` 55 ResourceDestructions int `jsonapi:"attr,resource-destructions"` 56 Status PlanStatus `jsonapi:"attr,status"` 57 StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"` 58 59 // Relations 60 Exports []*PlanExport `jsonapi:"relation,exports"` 61} 62 63// PlanStatusTimestamps holds the timestamps for individual plan statuses. 64type PlanStatusTimestamps struct { 65 CanceledAt time.Time `json:"canceled-at"` 66 ErroredAt time.Time `json:"errored-at"` 67 FinishedAt time.Time `json:"finished-at"` 68 ForceCanceledAt time.Time `json:"force-canceled-at"` 69 QueuedAt time.Time `json:"queued-at"` 70 StartedAt time.Time `json:"started-at"` 71} 72 73// Read a plan by its ID. 74func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) { 75 if !validStringID(&planID) { 76 return nil, errors.New("invalid value for plan ID") 77 } 78 79 u := fmt.Sprintf("plans/%s", url.QueryEscape(planID)) 80 req, err := s.client.newRequest("GET", u, nil) 81 if err != nil { 82 return nil, err 83 } 84 85 p := &Plan{} 86 err = s.client.do(ctx, req, p) 87 if err != nil { 88 return nil, err 89 } 90 91 return p, nil 92} 93 94// Logs retrieves the logs of a plan. 95func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) { 96 if !validStringID(&planID) { 97 return nil, errors.New("invalid value for plan ID") 98 } 99 100 // Get the plan to make sure it exists. 101 p, err := s.Read(ctx, planID) 102 if err != nil { 103 return nil, err 104 } 105 106 // Return an error if the log URL is empty. 107 if p.LogReadURL == "" { 108 return nil, fmt.Errorf("plan %s does not have a log URL", planID) 109 } 110 111 u, err := url.Parse(p.LogReadURL) 112 if err != nil { 113 return nil, fmt.Errorf("invalid log URL: %v", err) 114 } 115 116 done := func() (bool, error) { 117 p, err := s.Read(ctx, p.ID) 118 if err != nil { 119 return false, err 120 } 121 122 switch p.Status { 123 case PlanCanceled, PlanErrored, PlanFinished, PlanUnreachable: 124 return true, nil 125 default: 126 return false, nil 127 } 128 } 129 130 return &LogReader{ 131 client: s.client, 132 ctx: ctx, 133 done: done, 134 logURL: u, 135 }, nil 136} 137