1package tfe
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"fmt"
8	"io"
9	"net/url"
10	"time"
11)
12
13// Compile-time proof of interface implementation.
14var _ CostEstimates = (*costEstimates)(nil)
15
16// CostEstimates describes all the costEstimate related methods that
17// the Terraform Enterprise API supports.
18//
19// TFE API docs: https://www.terraform.io/docs/enterprise/api/ (TBD)
20type CostEstimates interface {
21	// Read a costEstimate by its ID.
22	Read(ctx context.Context, costEstimateID string) (*CostEstimate, error)
23
24	// Logs retrieves the logs of a costEstimate.
25	Logs(ctx context.Context, costEstimateID string) (io.Reader, error)
26}
27
28// costEstimates implements CostEstimates.
29type costEstimates struct {
30	client *Client
31}
32
33// CostEstimateStatus represents a costEstimate state.
34type CostEstimateStatus string
35
36// List all available costEstimate statuses.
37const (
38	CostEstimateCanceled              CostEstimateStatus = "canceled"
39	CostEstimateErrored               CostEstimateStatus = "errored"
40	CostEstimateFinished              CostEstimateStatus = "finished"
41	CostEstimatePending               CostEstimateStatus = "pending"
42	CostEstimateQueued                CostEstimateStatus = "queued"
43	CostEstimateSkippedDueToTargeting CostEstimateStatus = "skipped_due_to_targeting"
44)
45
46// CostEstimate represents a Terraform Enterprise costEstimate.
47type CostEstimate struct {
48	ID                      string                        `jsonapi:"primary,cost-estimates"`
49	DeltaMonthlyCost        string                        `jsonapi:"attr,delta-monthly-cost"`
50	ErrorMessage            string                        `jsonapi:"attr,error-message"`
51	MatchedResourcesCount   int                           `jsonapi:"attr,matched-resources-count"`
52	PriorMonthlyCost        string                        `jsonapi:"attr,prior-monthly-cost"`
53	ProposedMonthlyCost     string                        `jsonapi:"attr,proposed-monthly-cost"`
54	ResourcesCount          int                           `jsonapi:"attr,resources-count"`
55	Status                  CostEstimateStatus            `jsonapi:"attr,status"`
56	StatusTimestamps        *CostEstimateStatusTimestamps `jsonapi:"attr,status-timestamps"`
57	UnmatchedResourcesCount int                           `jsonapi:"attr,unmatched-resources-count"`
58}
59
60// CostEstimateStatusTimestamps holds the timestamps for individual costEstimate statuses.
61type CostEstimateStatusTimestamps struct {
62	CanceledAt              time.Time `json:"canceled-at"`
63	ErroredAt               time.Time `json:"errored-at"`
64	FinishedAt              time.Time `json:"finished-at"`
65	PendingAt               time.Time `json:"pending-at"`
66	QueuedAt                time.Time `json:"queued-at"`
67	SkippedDueToTargetingAt time.Time `json:"skipped-due-to-targeting-at"`
68}
69
70// Read a costEstimate by its ID.
71func (s *costEstimates) Read(ctx context.Context, costEstimateID string) (*CostEstimate, error) {
72	if !validStringID(&costEstimateID) {
73		return nil, errors.New("invalid value for cost estimate ID")
74	}
75
76	u := fmt.Sprintf("cost-estimates/%s", url.QueryEscape(costEstimateID))
77	req, err := s.client.newRequest("GET", u, nil)
78	if err != nil {
79		return nil, err
80	}
81
82	ce := &CostEstimate{}
83	err = s.client.do(ctx, req, ce)
84	if err != nil {
85		return nil, err
86	}
87
88	return ce, nil
89}
90
91// Logs retrieves the logs of a costEstimate.
92func (s *costEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
93	if !validStringID(&costEstimateID) {
94		return nil, errors.New("invalid value for cost estimate ID")
95	}
96
97	// Loop until the context is canceled or the cost estimate is finished
98	// running. The cost estimate logs are not streamed and so only available
99	// once the estimate is finished.
100	for {
101		// Get the costEstimate to make sure it exists.
102		ce, err := s.Read(ctx, costEstimateID)
103		if err != nil {
104			return nil, err
105		}
106
107		switch ce.Status {
108		case CostEstimateQueued:
109			select {
110			case <-ctx.Done():
111				return nil, ctx.Err()
112			case <-time.After(1000 * time.Millisecond):
113				continue
114			}
115		}
116
117		u := fmt.Sprintf("cost-estimates/%s/output", url.QueryEscape(costEstimateID))
118		req, err := s.client.newRequest("GET", u, nil)
119		if err != nil {
120			return nil, err
121		}
122
123		logs := bytes.NewBuffer(nil)
124		err = s.client.do(ctx, req, logs)
125		if err != nil {
126			return nil, err
127		}
128
129		return logs, nil
130	}
131}
132