Skip to content

Commit bebf568

Browse files
committed
feat: add Explorer query export to csv API
1 parent d799685 commit bebf568

File tree

2 files changed

+84
-12
lines changed

2 files changed

+84
-12
lines changed

explorer.go

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
package tfe
55

66
import (
7+
"bytes"
78
"context"
89
"fmt"
910
"net/url"
11+
"strings"
1012
"time"
1113
)
1214

@@ -25,6 +27,8 @@ type Explorer interface {
2527
QueryProviders(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerProviderViewList, error)
2628
// Query information about Terraform version usage within an organization.
2729
QueryTerraformVersions(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerTerraformVersionViewList, error)
30+
// Download a full, unpaged export of query results in CSV format.
31+
ExportToCSV(ctx context.Context, organization string, options ExplorerQueryOptions) ([]byte, error)
2832
}
2933

3034
type explorer struct {
@@ -69,7 +73,7 @@ type ExplorerQueryFilter struct {
6973
Value string
7074
}
7175

72-
func (eqf *ExplorerQueryFilter) toQueryParam() (string, string) {
76+
func (eqf *ExplorerQueryFilter) toKeyValue() (string, string) {
7377
key := fmt.Sprintf("filter[%d][%s][%s][0]", eqf.Index, eqf.Name, eqf.Operator)
7478
return key, eqf.Value
7579
}
@@ -78,13 +82,30 @@ func (eqf *ExplorerQueryFilter) toQueryParam() (string, string) {
7882
type ExplorerQueryOptions struct {
7983
ListOptions
8084

81-
View ExplorerViewType `url:"type"`
82-
Sort string `url:"sort,omitempty"`
83-
Fields string `url:"fields,omitempty"`
85+
View ExplorerViewType `url:"type"`
86+
Sort string `url:"sort,omitempty"`
8487

88+
Fields []string `url:"-"`
8589
Filters []*ExplorerQueryFilter `url:"-"`
8690
}
8791

92+
func (eqo *ExplorerQueryOptions) extractFilters() map[string][]string {
93+
filterParams := make(map[string][]string)
94+
for _, filter := range eqo.Filters {
95+
if filter != nil {
96+
k, v := filter.toKeyValue()
97+
filterParams[k] = []string{v}
98+
}
99+
}
100+
101+
// Append the fields query param, ensuring the correct view type is specified
102+
if len(eqo.Fields) > 0 {
103+
fieldsKey := fmt.Sprintf("fields[%s]", eqo.View)
104+
filterParams[fieldsKey] = []string{strings.Join(eqo.Fields, ",")}
105+
}
106+
return filterParams
107+
}
108+
88109
// WorkspaceView represents information about a workspace in the target
89110
// organization and any current runs associated with that workspace.
90111
type WorkspaceView struct {
@@ -251,16 +272,32 @@ func (e *explorer) QueryTerraformVersions(ctx context.Context, organization stri
251272
return eql, nil
252273
}
253274

254-
func (e *explorer) buildExplorerQueryRequest(organization string, options ExplorerQueryOptions) (*ClientRequest, error) {
255-
filterParams := make(map[string][]string)
256-
u := fmt.Sprintf("organizations/%s/explorer", url.QueryEscape(organization))
275+
// ExportToCSV performs an Explorer query and exports the results to CSV format.
276+
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer#export-data-as-csv
277+
func (e *explorer) ExportToCSV(ctx context.Context, organization string, options ExplorerQueryOptions) ([]byte, error) {
278+
filterParams := options.extractFilters()
257279

258-
for _, filter := range options.Filters {
259-
if filter != nil {
260-
k, v := filter.toQueryParam()
261-
filterParams[k] = []string{v}
262-
}
280+
u := fmt.Sprintf("organizations/%s/explorer/export/csv", url.QueryEscape(organization))
281+
req, err := e.client.NewRequestWithAdditionalQueryParams("GET", u, options, filterParams)
282+
if err != nil {
283+
return nil, err
263284
}
264285

286+
// Override accept header
287+
req.retryableRequest.Header.Set("Accept", "*/*")
288+
289+
buf := &bytes.Buffer{}
290+
err = req.Do(ctx, buf)
291+
if err != nil {
292+
return nil, err
293+
}
294+
295+
return buf.Bytes(), nil
296+
}
297+
298+
func (e *explorer) buildExplorerQueryRequest(organization string, options ExplorerQueryOptions) (*ClientRequest, error) {
299+
filterParams := options.extractFilters()
300+
301+
u := fmt.Sprintf("organizations/%s/explorer", url.QueryEscape(organization))
265302
return e.client.NewRequestWithAdditionalQueryParams("GET", u, options, filterParams)
266303
}

explorer_integration_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package tfe
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/csv"
57
"testing"
68

9+
"github.com/stretchr/testify/assert"
710
"github.com/stretchr/testify/require"
811
)
912

@@ -210,3 +213,35 @@ func TestExplorer_QueryWorkspaces(t *testing.T) {
210213
}
211214
})
212215
}
216+
217+
func TestExplorer_ExportToCSV(t *testing.T) {
218+
client := testClient(t)
219+
ctx := context.Background()
220+
organization := testExplorerOrganization(t)
221+
222+
csvResult, err := client.Explorer.ExportToCSV(ctx, organization, ExplorerQueryOptions{
223+
View: WorkspacesViewType,
224+
Fields: []string{"workspace_name", "current_run_status"},
225+
Filters: []*ExplorerQueryFilter{
226+
{
227+
Index: 0,
228+
Name: "current_run_status",
229+
Operator: OpIs,
230+
Value: "applied",
231+
},
232+
},
233+
})
234+
require.NoError(t, err)
235+
r := csv.NewReader(bytes.NewReader(csvResult))
236+
237+
header, err := r.Read()
238+
require.NoError(t, err)
239+
assert.Equal(t, len(header), 2)
240+
// Fields come in the order specified in the request
241+
assert.Equal(t, header[0], "workspace_name")
242+
assert.Equal(t, header[1], "current_run_status")
243+
244+
rows, err := r.ReadAll()
245+
require.NoError(t, err)
246+
assert.Greater(t, len(rows), 0)
247+
}

0 commit comments

Comments
 (0)