Skip to content

Commit 74613bf

Browse files
brandoncctrombley
authored andcommitted
adds Logs method to QueryRuns
1 parent f93f693 commit 74613bf

File tree

3 files changed

+164
-14
lines changed

3 files changed

+164
-14
lines changed

helper_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,27 @@ func createRunWaitForAnyStatuses(t *testing.T, client *Client, w *Workspace, sta
12521252
}
12531253
}
12541254

1255+
func createQueryRunWaitForAnyStatuses(t *testing.T, client *Client, w *Workspace, statuses []QueryRunStatus) (*QueryRun, func()) {
1256+
ctx := context.Background()
1257+
qr := createQueryRun(t, client, w)
1258+
1259+
timeout := 2 * time.Minute
1260+
1261+
ctxPollQueryRunReady, cancelPollQueryRunReady := context.WithTimeout(ctx, timeout)
1262+
1263+
run := pollQueryRunStatus(
1264+
t,
1265+
client,
1266+
ctxPollQueryRunReady,
1267+
qr,
1268+
append(statuses, QueryRunErrored),
1269+
)
1270+
1271+
return run, func() {
1272+
cancelPollQueryRunReady()
1273+
}
1274+
}
1275+
12551276
func applyableStatuses(r *Run) []RunStatus {
12561277
if len(r.PolicyChecks) > 0 {
12571278
return []RunStatus{
@@ -1298,6 +1319,39 @@ func pollRunStatus(t *testing.T, client *Client, ctx context.Context, r *Run, rs
12981319
return r
12991320
}
13001321

1322+
// pollQueryRunStatus will poll the given query run until its status matches one of the given run statuses or the given context
1323+
// times out.
1324+
func pollQueryRunStatus(t *testing.T, client *Client, ctx context.Context, q *QueryRun, rss []QueryRunStatus) *QueryRun {
1325+
deadline, ok := ctx.Deadline()
1326+
if !ok {
1327+
t.Logf("No deadline was set to poll query run %q which could result in an infinite loop", q.ID)
1328+
}
1329+
1330+
t.Logf("Polling query run %q for status included in %q with deadline of %s", q.ID, rss, deadline)
1331+
1332+
ticker := time.NewTicker(2 * time.Second)
1333+
defer ticker.Stop()
1334+
1335+
for finished := false; !finished; {
1336+
t.Log("...")
1337+
select {
1338+
case <-ctx.Done():
1339+
t.Fatalf("Run %q had status %q at deadline", q.ID, q.Status)
1340+
case <-ticker.C:
1341+
q = readQueryRun(t, client, ctx, q)
1342+
t.Logf("Query Run %q had status %q", q.ID, q.Status)
1343+
for _, rs := range rss {
1344+
if rs == q.Status {
1345+
finished = true
1346+
break
1347+
}
1348+
}
1349+
}
1350+
}
1351+
1352+
return q
1353+
}
1354+
13011355
// pollStateVersionStatus will poll the given state version until its status
13021356
// matches one of the given statuses or the given context times out.
13031357
func pollStateVersionStatus(t *testing.T, client *Client, ctx context.Context, sv *StateVersion, statuses []StateVersionStatus) *StateVersion {
@@ -1347,6 +1401,18 @@ func readRun(t *testing.T, client *Client, ctx context.Context, r *Run) *Run {
13471401
return rr
13481402
}
13491403

1404+
// readQueryRun will re-read the given query run.
1405+
func readQueryRun(t *testing.T, client *Client, ctx context.Context, r *QueryRun) *QueryRun {
1406+
t.Logf("Reading query run %q", r.ID)
1407+
1408+
qr, err := client.QueryRuns.Read(ctx, r.ID)
1409+
if err != nil {
1410+
t.Fatalf("Could not read run %q: %s", r.ID, err)
1411+
}
1412+
1413+
return qr
1414+
}
1415+
13501416
// applyRun will apply the given run.
13511417
func applyRun(t *testing.T, client *Client, ctx context.Context, r *Run) {
13521418
t.Logf("Applying run %q", r.ID)

query_runs.go

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package tfe
66
import (
77
"context"
88
"fmt"
9+
"io"
910
"net/url"
1011
"time"
1112
)
@@ -31,6 +32,9 @@ type QueryRuns interface {
3132
// ReadWithOptions reads a query run by its ID using the options supplied
3233
ReadWithOptions(ctx context.Context, queryRunID string, options *QueryRunReadOptions) (*QueryRun, error)
3334

35+
// Logs retrieves the logs of a query run.
36+
Logs(ctx context.Context, queryRunID string) (io.Reader, error)
37+
3438
// Cancel a query run by its ID.
3539
Cancel(ctx context.Context, runID string) error
3640

@@ -46,18 +50,17 @@ type QueryRunCreateOptions struct {
4650
// https://jsonapi.org/format/#crud-creating
4751
Type string `jsonapi:"primary,queries"`
4852

49-
// TerraformVersion specifies the Terraform version to use in this run.
50-
// Only valid for plan-only runs; must be a valid Terraform version available to the organization.
53+
// TerraformVersion specifies the Terraform version to use in this query run.
5154
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
5255

5356
Source QueryRunSource `jsonapi:"attr,source"`
5457

55-
// Specifies the configuration version to use for this run. If the
58+
// Specifies the configuration version to use for this query run. If the
5659
// configuration version object is omitted, the run will be created using the
5760
// workspace's latest configuration version.
5861
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
5962

60-
// Specifies the workspace where the run will be executed.
63+
// Specifies the workspace where the query run will be executed.
6164
Workspace *Workspace `jsonapi:"relation,workspace"`
6265

6366
// Variables allows you to specify terraform input variables for
@@ -82,6 +85,19 @@ type QueryRunIncludeOpt string
8285
// QueryRunSource represents the available sources for query runs.
8386
type QueryRunSource string
8487

88+
// QueryRunStatus is the query run state
89+
type QueryRunStatus string
90+
91+
// List all available run statuses.
92+
const (
93+
QueryRunCanceled QueryRunStatus = "canceled"
94+
QueryRunErrored QueryRunStatus = "errored"
95+
QueryRunPending QueryRunStatus = "pending"
96+
QueryRunQueuing QueryRunStatus = "queued"
97+
QueryRunRunning QueryRunStatus = "running"
98+
QueryRunFinished QueryRunStatus = "queuing_apply"
99+
)
100+
85101
// List all available run sources.
86102
const (
87103
QueryRunSourceAPI QueryRunSource = "tfe-api"
@@ -116,13 +132,14 @@ type QueryRunReadOptions struct {
116132

117133
// QueryRun represents a Terraform Enterprise query run.
118134
type QueryRun struct {
119-
ID string `jsonapi:"primary,queries"`
120-
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
121-
Source RunSource `jsonapi:"attr,source"`
122-
Status RunStatus `jsonapi:"attr,status"`
123-
StatusTimestamps *RunStatusTimestamps `jsonapi:"attr,status-timestamps"`
124-
TerraformVersion string `jsonapi:"attr,terraform-version"`
125-
Variables []*RunVariableAttr `jsonapi:"attr,variables"`
135+
ID string `jsonapi:"primary,queries"`
136+
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
137+
Source QueryRunSource `jsonapi:"attr,source"`
138+
Status QueryRunStatus `jsonapi:"attr,status"`
139+
StatusTimestamps *QueryRunStatusTimestamps `jsonapi:"attr,status-timestamps"`
140+
TerraformVersion string `jsonapi:"attr,terraform-version"`
141+
Variables []*RunVariableAttr `jsonapi:"attr,variables"`
142+
LogReadURL string `jsonapi:"attr,log-read-url"`
126143

127144
// Relations
128145
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
@@ -206,6 +223,49 @@ func (r *queryRuns) ReadWithOptions(ctx context.Context, queryRunID string, opti
206223
return &run, nil
207224
}
208225

226+
func (r *queryRuns) Logs(ctx context.Context, queryRunID string) (io.Reader, error) {
227+
if !validStringID(&queryRunID) {
228+
return nil, ErrInvalidQueryRunID
229+
}
230+
231+
// Get the query to make sure it exists.
232+
q, err := r.Read(ctx, queryRunID)
233+
if err != nil {
234+
return nil, err
235+
}
236+
237+
// Return an error if the log URL is empty.
238+
if q.LogReadURL == "" {
239+
return nil, fmt.Errorf("query %s does not have a log URL", queryRunID)
240+
}
241+
242+
u, err := url.Parse(q.LogReadURL)
243+
if err != nil {
244+
return nil, fmt.Errorf("invalid log URL: %w", err)
245+
}
246+
247+
done := func() (bool, error) {
248+
p, err := r.Read(ctx, q.ID)
249+
if err != nil {
250+
return false, err
251+
}
252+
253+
switch p.Status {
254+
case QueryRunCanceled, QueryRunErrored, QueryRunFinished:
255+
return true, nil
256+
default:
257+
return false, nil
258+
}
259+
}
260+
261+
return &LogReader{
262+
client: r.client,
263+
ctx: ctx,
264+
done: done,
265+
logURL: u,
266+
}, nil
267+
}
268+
209269
func (r *queryRuns) Cancel(ctx context.Context, queryRunID string) error {
210270
if queryRunID == "" {
211271
return ErrInvalidQueryRunID

query_runs_integration_test.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package tfe
55

66
import (
77
"context"
8+
"io"
89
"testing"
910

1011
"github.com/stretchr/testify/assert"
@@ -166,11 +167,12 @@ func TestQueryRunsCreate(t *testing.T) {
166167
assert.Equal(t, len(vars), len(qr.Variables))
167168

168169
for _, v := range qr.Variables {
169-
if v.Key == "test_foo" {
170+
switch v.Key {
171+
case "test_foo":
170172
assert.Equal(t, v.Value, "Hello, Foo!")
171-
} else if v.Key == "test_variable" {
173+
case "test_variable":
172174
assert.Equal(t, v.Value, "Hello, World!")
173-
} else {
175+
default:
174176
t.Fatalf("Unexpected variable key: %s", v.Key)
175177
}
176178
}
@@ -263,6 +265,28 @@ func TestQueryRunsCancel(t *testing.T) {
263265
})
264266
}
265267

268+
func TestQueryRunsLogs(t *testing.T) {
269+
skipUnlessBeta(t)
270+
client := testClient(t)
271+
ctx := context.Background()
272+
273+
wTest, wTestCleanup := createWorkspace(t, client, nil)
274+
defer wTestCleanup()
275+
276+
qr, cleanup := createQueryRunWaitForAnyStatuses(t, client, wTest, []QueryRunStatus{QueryRunErrored, QueryRunFinished})
277+
t.Cleanup(cleanup)
278+
279+
t.Run("when the query run exists", func(t *testing.T) {
280+
// We assume the second query run is in a state that can be canceled.
281+
reader, err := client.QueryRuns.Logs(ctx, qr.ID)
282+
require.NoError(t, err)
283+
284+
logs, err := io.ReadAll(reader)
285+
require.NoError(t, err)
286+
assert.NotEmpty(t, logs, "some logs should be returned")
287+
})
288+
}
289+
266290
func TestQueryRunsForceCancel(t *testing.T) {
267291
skipUnlessBeta(t)
268292
client := testClient(t)

0 commit comments

Comments
 (0)