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 _ Applies = (*applies)(nil)
14
15// Applies describes all the apply related methods that the Terraform
16// Enterprise API supports.
17//
18// TFE API docs: https://www.terraform.io/docs/enterprise/api/apply.html
19type Applies interface {
20	// Read an apply by its ID.
21	Read(ctx context.Context, applyID string) (*Apply, error)
22
23	// Logs retrieves the logs of an apply.
24	Logs(ctx context.Context, applyID string) (io.Reader, error)
25}
26
27// applies implements Applys.
28type applies struct {
29	client *Client
30}
31
32// ApplyStatus represents an apply state.
33type ApplyStatus string
34
35//List all available apply statuses.
36const (
37	ApplyCanceled    ApplyStatus = "canceled"
38	ApplyCreated     ApplyStatus = "created"
39	ApplyErrored     ApplyStatus = "errored"
40	ApplyFinished    ApplyStatus = "finished"
41	ApplyMFAWaiting  ApplyStatus = "mfa_waiting"
42	ApplyPending     ApplyStatus = "pending"
43	ApplyQueued      ApplyStatus = "queued"
44	ApplyRunning     ApplyStatus = "running"
45	ApplyUnreachable ApplyStatus = "unreachable"
46)
47
48// Apply represents a Terraform Enterprise apply.
49type Apply struct {
50	ID                   string                 `jsonapi:"primary,applies"`
51	LogReadURL           string                 `jsonapi:"attr,log-read-url"`
52	ResourceAdditions    int                    `jsonapi:"attr,resource-additions"`
53	ResourceChanges      int                    `jsonapi:"attr,resource-changes"`
54	ResourceDestructions int                    `jsonapi:"attr,resource-destructions"`
55	Status               ApplyStatus            `jsonapi:"attr,status"`
56	StatusTimestamps     *ApplyStatusTimestamps `jsonapi:"attr,status-timestamps"`
57}
58
59// ApplyStatusTimestamps holds the timestamps for individual apply statuses.
60type ApplyStatusTimestamps struct {
61	CanceledAt      time.Time `json:"canceled-at"`
62	ErroredAt       time.Time `json:"errored-at"`
63	FinishedAt      time.Time `json:"finished-at"`
64	ForceCanceledAt time.Time `json:"force-canceled-at"`
65	QueuedAt        time.Time `json:"queued-at"`
66	StartedAt       time.Time `json:"started-at"`
67}
68
69// Read an apply by its ID.
70func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
71	if !validStringID(&applyID) {
72		return nil, errors.New("invalid value for apply ID")
73	}
74
75	u := fmt.Sprintf("applies/%s", url.QueryEscape(applyID))
76	req, err := s.client.newRequest("GET", u, nil)
77	if err != nil {
78		return nil, err
79	}
80
81	a := &Apply{}
82	err = s.client.do(ctx, req, a)
83	if err != nil {
84		return nil, err
85	}
86
87	return a, nil
88}
89
90// Logs retrieves the logs of an apply.
91func (s *applies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
92	if !validStringID(&applyID) {
93		return nil, errors.New("invalid value for apply ID")
94	}
95
96	// Get the apply to make sure it exists.
97	a, err := s.Read(ctx, applyID)
98	if err != nil {
99		return nil, err
100	}
101
102	// Return an error if the log URL is empty.
103	if a.LogReadURL == "" {
104		return nil, fmt.Errorf("apply %s does not have a log URL", applyID)
105	}
106
107	u, err := url.Parse(a.LogReadURL)
108	if err != nil {
109		return nil, fmt.Errorf("invalid log URL: %v", err)
110	}
111
112	done := func() (bool, error) {
113		a, err := s.Read(ctx, a.ID)
114		if err != nil {
115			return false, err
116		}
117
118		switch a.Status {
119		case ApplyCanceled, ApplyErrored, ApplyFinished, ApplyUnreachable:
120			return true, nil
121		default:
122			return false, nil
123		}
124	}
125
126	return &LogReader{
127		client: s.client,
128		ctx:    ctx,
129		done:   done,
130		logURL: u,
131	}, nil
132}
133