Skip to content

Commit 4c2b00c

Browse files
committed
feat: add support for the explorer API
This commit adds support for the two generally available endpoints in the Explorer API: execute a query, export data to CSV. Since the jsonapi decoder does not support unmarshalling polymorphic slices, each view type has been assigned its own query function returning the appropriate data type.
1 parent f9d7888 commit 4c2b00c

File tree

5 files changed

+573
-0
lines changed

5 files changed

+573
-0
lines changed

errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ var (
236236
ErrInvalidAccessToken = errors.New("invalid value for access token")
237237

238238
ErrInvalidTaskResultsCallbackStatus = fmt.Errorf("invalid value for task result status. Must be either `%s`, `%s`, or `%s`", TaskFailed, TaskPassed, TaskRunning)
239+
240+
ErrInvalidExplorerQueryFilterIndex = errors.New("invalid query filter index, must be greater than or equal to 0")
241+
242+
ErrInvalidExplorerViewType = errors.New("invalid explorer query view type, must be one of workspaces, tf_versions, providers, or modules")
239243
)
240244

241245
var (

explorer.go

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfe
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"fmt"
10+
"net/url"
11+
"strings"
12+
"time"
13+
)
14+
15+
// Compile-time proof of interface implementation.
16+
var _ Explorer = (*explorer)(nil)
17+
18+
// Explorer describes all the explorer related methods that the Terraform Enterprise API supports.
19+
//
20+
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer
21+
type Explorer interface {
22+
// Query information about workspaces within an organization.
23+
QueryWorkspaces(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerWorkspaceViewList, error)
24+
// Query information about module version usage within an organization.
25+
QueryModules(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerModuleViewList, error)
26+
// Query information about provider version usage within an organization.
27+
QueryProviders(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerProviderViewList, error)
28+
// Query information about Terraform version usage within an organization.
29+
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)
32+
}
33+
34+
type explorer struct {
35+
client *Client
36+
}
37+
38+
// ExplorerViewType represents the view types the Explorer API supports
39+
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer#view-types
40+
type ExplorerViewType string
41+
42+
const (
43+
WorkspacesViewType ExplorerViewType = "workspaces"
44+
ProvidersViewType ExplorerViewType = "providers"
45+
ModulesViewType ExplorerViewType = "modules"
46+
TerraformVersionsViewType ExplorerViewType = "tf_versions"
47+
)
48+
49+
// ExplorerQueryFilterOperator represents the supported operations for filtering.
50+
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer#filter-operators
51+
type ExplorerQueryFilterOperator string
52+
53+
const (
54+
OpIs ExplorerQueryFilterOperator = "is"
55+
OpIsNot ExplorerQueryFilterOperator = "is_not"
56+
OpContains ExplorerQueryFilterOperator = "contains"
57+
OpDoesNotContain ExplorerQueryFilterOperator = "does_not_contain"
58+
OpIsEmpty ExplorerQueryFilterOperator = "is_empty"
59+
OpIsNotEmpty ExplorerQueryFilterOperator = "is_not_empty"
60+
OpGreaterThan ExplorerQueryFilterOperator = "gt"
61+
OpLessThan ExplorerQueryFilterOperator = "lt"
62+
OpGreaterThanOrEqual ExplorerQueryFilterOperator = "gteq"
63+
OpLessThanOrEqual ExplorerQueryFilterOperator = "lteq"
64+
OpIsBefore ExplorerQueryFilterOperator = "is_before"
65+
OpIsAfter ExplorerQueryFilterOperator = "is_after"
66+
)
67+
68+
// ExplorerQueryFilter represents a filter query parameter for the query endpoint.
69+
type ExplorerQueryFilter struct {
70+
// Unique, sequential index for each filter, starting at 0 and incrementing by 1.
71+
Index int
72+
// Field name to apply the filter, valid for the queried view type.
73+
Name string
74+
// The operator use when filtering, must be supported by the field type.
75+
Operator ExplorerQueryFilterOperator
76+
// The filter value used by the filter during the query.
77+
Value string
78+
}
79+
80+
func (eqf *ExplorerQueryFilter) toKeyValue() (string, string) {
81+
key := fmt.Sprintf("filter[%d][%s][%s][0]", eqf.Index, eqf.Name, eqf.Operator)
82+
return key, eqf.Value
83+
}
84+
85+
// ExplorerQueryOptions represents the parameter options for querying the Explorer API
86+
type ExplorerQueryOptions struct {
87+
ListOptions
88+
89+
// Must be one of the following available views: WorkspacesViewType, ModulesViewType,
90+
// ProvidersViewType or TerraformVersionsViewType. Each query function will
91+
// set this value automatically, except ExportToCSV().
92+
View ExplorerViewType `url:"type"`
93+
// Optional snake_case field to sort data, prefix with '-' for descending, must exist in view type.
94+
Sort string `url:"sort,omitempty"`
95+
96+
// List of fields to limit the data returned by the query.
97+
Fields []string `url:"-"`
98+
// List of filters to limit the data returned by the query.
99+
Filters []*ExplorerQueryFilter `url:"-"`
100+
}
101+
102+
func (eqo *ExplorerQueryOptions) extractFilters() map[string][]string {
103+
filterParams := make(map[string][]string)
104+
for _, filter := range eqo.Filters {
105+
if filter != nil {
106+
k, v := filter.toKeyValue()
107+
filterParams[k] = []string{v}
108+
}
109+
}
110+
111+
// Append the fields query param, ensuring the correct view type is specified
112+
if len(eqo.Fields) > 0 {
113+
fieldsKey := fmt.Sprintf("fields[%s]", eqo.View)
114+
filterParams[fieldsKey] = []string{strings.Join(eqo.Fields, ",")}
115+
}
116+
return filterParams
117+
}
118+
119+
// WorkspaceView represents information about a workspace in the target
120+
// organization and any current runs associated with that workspace.
121+
type WorkspaceView struct {
122+
Type string `jsonapi:"primary,visibility-workspace"`
123+
AllChecksSucceeded bool `jsonapi:"attr,all-checks-succeeded"`
124+
ChecksErrored int `jsonapi:"attr,checks-errored"`
125+
ChecksFailed int `jsonapi:"attr,checks-failed"`
126+
ChecksPassed int `jsonapi:"attr,checks-passed"`
127+
ChecksUnknown int `jsonapi:"attr,checks-unknown"`
128+
CurrentRunAppliedAt time.Time `jsonapi:"attr,current-run-applied-at,rfc3339"`
129+
CurrentRunExternalID string `jsonapi:"attr,current-run-external-id"`
130+
CurrentRunStatus RunStatus `jsonapi:"attr,current-run-status"`
131+
Drifted bool `jsonapi:"attr,drifted"`
132+
ExternalID string `jsonapi:"attr,external-id"`
133+
ModuleCount int `jsonapi:"attr,module-count"`
134+
Modules interface{} `jsonapi:"attr,modules"`
135+
OrganizationName string `jsonapi:"attr,organization-name"`
136+
ProjectExternalID string `jsonapi:"attr,project-external-id"`
137+
ProjectName string `jsonapi:"attr,project-name"`
138+
ProviderCount int `jsonapi:"attr,provider-count"`
139+
Providers interface{} `jsonapi:"attr,providers"`
140+
ResourcesDrifted int `jsonapi:"attr,resources-drifted"`
141+
ResourcesUndrifted int `jsonapi:"attr,resources-undrifted"`
142+
StateVersionTerraformVersion string `jsonapi:"attr,state-version-terraform-version"`
143+
VCSRepoIdentifier *string `jsonapi:"attr,vcs-repo-identifier"`
144+
WorkspaceCreatedAt time.Time `jsonapi:"attr,workspace-created-at,rfc3339"`
145+
WorkspaceName string `jsonapi:"attr,workspace-name"`
146+
WorkspaceTerraformVersion string `jsonapi:"attr,workspace-terraform-version"`
147+
WorkspaceUpdatedAt time.Time `jsonapi:"attr,workspace-updated-at,rfc3339"`
148+
}
149+
150+
// ModuleView represents information about a Terraform module version used by
151+
// an organization.
152+
type ModuleView struct {
153+
Type string `jsonapi:"primary,visibility-module-version"`
154+
Name string `jsonapi:"attr,name"`
155+
Source string `jsonapi:"attr,source"`
156+
Version string `jsonapi:"attr,version"`
157+
WorkspaceCount int `jsonapi:"attr,workspace-count"`
158+
Workspaces string `jsonapi:"attr,workspaces"`
159+
}
160+
161+
// ProviderView represents information about a Terraform provider version used
162+
// by an organization.
163+
type ProviderView struct {
164+
Type string `jsonapi:"primary,visibility-provider-version"`
165+
Name string `jsonapi:"attr,name"`
166+
Source string `jsonapi:"attr,source"`
167+
Version string `jsonapi:"attr,version"`
168+
WorkspaceCount int `jsonapi:"attr,workspace-count"`
169+
Workspaces string `jsonapi:"attr,workspaces"`
170+
}
171+
172+
// TerraformVersionView represents information about a Terraform version used
173+
// by workspaces in an organization.
174+
type TerraformVersionView struct {
175+
Type string `jsonapi:"primary,visibility-tf-version"`
176+
Version string `jsonapi:"attr,version"`
177+
WorkspaceCount int `jsonapi:"attr,workspace-count"`
178+
Workspaces string `jsonapi:"attr,workspaces"`
179+
}
180+
181+
// ExplorerWorkspaceViewList represents a list of workspace views
182+
type ExplorerWorkspaceViewList struct {
183+
*Pagination
184+
Items []*WorkspaceView
185+
}
186+
187+
// ExplorerModuleViewList represents a list of module views
188+
type ExplorerModuleViewList struct {
189+
*Pagination
190+
Items []*ModuleView
191+
}
192+
193+
// ExplorerProviderViewList represents a list of provider views
194+
type ExplorerProviderViewList struct {
195+
*Pagination
196+
Items []*ProviderView
197+
}
198+
199+
// ExplorerTerraformVersionViewList represents a list of Terraform version views
200+
type ExplorerTerraformVersionViewList struct {
201+
*Pagination
202+
Items []*TerraformVersionView
203+
}
204+
205+
// QueryWorkspaces invokes the Explorer's Query endpoint to return information
206+
// about workspaces and their associated runs in the specified organization.
207+
func (e *explorer) QueryWorkspaces(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerWorkspaceViewList, error) {
208+
// Force the correct view type
209+
options.View = WorkspacesViewType
210+
211+
req, err := e.buildExplorerQueryRequest(organization, options)
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
eql := &ExplorerWorkspaceViewList{}
217+
err = req.Do(ctx, eql)
218+
if err != nil {
219+
return nil, err
220+
}
221+
222+
return eql, nil
223+
}
224+
225+
// QueryModules invokes the Explorer's Query endpoint to return information
226+
// about module versions in use across the specified organization.
227+
func (e *explorer) QueryModules(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerModuleViewList, error) {
228+
// Force the correct view type
229+
options.View = ModulesViewType
230+
231+
req, err := e.buildExplorerQueryRequest(organization, options)
232+
if err != nil {
233+
return nil, err
234+
}
235+
236+
eql := &ExplorerModuleViewList{}
237+
err = req.Do(ctx, eql)
238+
if err != nil {
239+
return nil, err
240+
}
241+
242+
return eql, nil
243+
}
244+
245+
// QueryProviders invokes the Explorer's Query endpoint to return information
246+
// about provider versions in use across the specified organization.
247+
func (e *explorer) QueryProviders(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerProviderViewList, error) {
248+
// Force the correct view type
249+
options.View = ProvidersViewType
250+
251+
req, err := e.buildExplorerQueryRequest(organization, options)
252+
if err != nil {
253+
return nil, err
254+
}
255+
256+
eql := &ExplorerProviderViewList{}
257+
err = req.Do(ctx, eql)
258+
if err != nil {
259+
return nil, err
260+
}
261+
262+
return eql, nil
263+
}
264+
265+
// QueryTerraformVersions invokes the Explorer's Query endpoint to return information
266+
// about Terraform versions in use across the specified organization.
267+
func (e *explorer) QueryTerraformVersions(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerTerraformVersionViewList, error) {
268+
// Force the correct view type
269+
options.View = TerraformVersionsViewType
270+
271+
req, err := e.buildExplorerQueryRequest(organization, options)
272+
if err != nil {
273+
return nil, err
274+
}
275+
276+
eql := &ExplorerTerraformVersionViewList{}
277+
err = req.Do(ctx, eql)
278+
if err != nil {
279+
return nil, err
280+
}
281+
282+
return eql, nil
283+
}
284+
285+
// ExportToCSV performs an Explorer query and exports the results to CSV format.
286+
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer#export-data-as-csv
287+
func (e *explorer) ExportToCSV(ctx context.Context, organization string, options ExplorerQueryOptions) ([]byte, error) {
288+
filterParams := options.extractFilters()
289+
290+
u := fmt.Sprintf("organizations/%s/explorer/export/csv", url.QueryEscape(organization))
291+
req, err := e.client.NewRequestWithAdditionalQueryParams("GET", u, options, filterParams)
292+
if err != nil {
293+
return nil, err
294+
}
295+
296+
// Override accept header
297+
req.retryableRequest.Header.Set("Accept", "*/*")
298+
299+
buf := &bytes.Buffer{}
300+
err = req.Do(ctx, buf)
301+
if err != nil {
302+
return nil, err
303+
}
304+
305+
return buf.Bytes(), nil
306+
}
307+
308+
func (e *explorer) buildExplorerQueryRequest(organization string, options ExplorerQueryOptions) (*ClientRequest, error) {
309+
filterParams := options.extractFilters()
310+
311+
u := fmt.Sprintf("organizations/%s/explorer", url.QueryEscape(organization))
312+
return e.client.NewRequestWithAdditionalQueryParams("GET", u, options, filterParams)
313+
}

0 commit comments

Comments
 (0)