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