diff --git a/CHANGELOG.md b/CHANGELOG.md index bee152721..8bbd8d839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +## Features + +* Add support for the Explorer API by @sebasslash [#1018](https://github.com/hashicorp/go-tfe/pull/1018) + ## Enhancements * Add BETA support for adding custom project permission for variable sets `ProjectVariableSetsPermission` by @netramali [21879](https://github.com/hashicorp/atlas/pull/21879) @@ -25,6 +29,7 @@ * Add BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022) * Adds support to delete all tag bindings on either a project or workspace by @sebasslash [#1023](https://github.com/hashicorp/go-tfe/pull/1023) + # v1.71.0 ## Enhancements diff --git a/errors.go b/errors.go index fdb68e53f..e6475d802 100644 --- a/errors.go +++ b/errors.go @@ -236,6 +236,10 @@ var ( ErrInvalidAccessToken = errors.New("invalid value for access token") ErrInvalidTaskResultsCallbackStatus = fmt.Errorf("invalid value for task result status. Must be either `%s`, `%s`, or `%s`", TaskFailed, TaskPassed, TaskRunning) + + ErrInvalidExplorerQueryFilterIndex = errors.New("invalid query filter index, must be greater than or equal to 0") + + ErrInvalidExplorerViewType = errors.New("invalid explorer query view type, must be one of workspaces, tf_versions, providers, or modules") ) var ( diff --git a/examples/explorer/main.go b/examples/explorer/main.go new file mode 100644 index 000000000..63624de28 --- /dev/null +++ b/examples/explorer/main.go @@ -0,0 +1,148 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "bytes" + "context" + "encoding/csv" + "fmt" + "log" + "time" + + tfe "github.com/hashicorp/go-tfe" +) + +func main() { + config := &tfe.Config{ + Token: "insert-your-token-here", + RetryServerErrors: true, + } + + client, err := tfe.NewClient(config) + if err != nil { + log.Fatal(err) + } + + // Create a context + ctx := context.Background() + + organization := "insert-your-organization-name" + + // Note: The following queries may not yield any results as the data available to query is dependent on your organization. Also results are paginated so the initial response may not reflect the full query result. + + // (#1) Workspaces Example: Give me all the workspace names that have a + // current run status of "errored" AND are in project "foo" + wql, err := client.Explorer.QueryWorkspaces(ctx, organization, tfe.ExplorerQueryOptions{ + Fields: []string{"workspace_name"}, + Filters: []*tfe.ExplorerQueryFilter{ + { + Index: 0, + Name: "current_run_status", + Operator: tfe.OpIs, + Value: "errored", + }, + { + Index: 1, + Name: "project_name", + Operator: tfe.OpIs, + Value: "foo", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println(wql.Items[0].WorkspaceName) + + // (#2) Modules Example: Give me all the modules that are being used by more than one workspace and sort them by number of workspaces DESC. + mql, err := client.Explorer.QueryModules(ctx, organization, tfe.ExplorerQueryOptions{ + Sort: "-workspace_count", + Filters: []*tfe.ExplorerQueryFilter{ + { + Index: 0, + Name: "workspace_count", + Operator: tfe.OpGreaterThan, + Value: "1", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println(mql.Items[0].Name) + fmt.Println(mql.Items[0].WorkspaceCount) + + // (#3) Providers Example: Give me all the providers that are being used by the workspace "staging-us-east-1" + pql, err := client.Explorer.QueryProviders(ctx, organization, tfe.ExplorerQueryOptions{ + Filters: []*tfe.ExplorerQueryFilter{ + { + Index: 0, + Name: "workspaces", + Operator: tfe.OpContains, + Value: "staging-us-east-1", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println(len(pql.Items)) + + // (#4) Terraform Versions Example: Give me all of the workspaces + // that are not using Terraform version 1.10.0 + tfql, err := client.Explorer.QueryTerraformVersions(ctx, organization, tfe.ExplorerQueryOptions{ + Filters: []*tfe.ExplorerQueryFilter{ + { + Index: 0, + Name: "version", + Operator: tfe.OpIsNot, + Value: "1.10.0", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println(tfql.Items[0].Version) + fmt.Println(tfql.Items[0].WorkspaceCount) + + // (#5) Export to CSV: Give me all the workspaces where health checks have + // succeeded and have been updated since 2 days ago. Note: This method can also + // be used for modules, providers and terraform version queries. + since := time.Now().AddDate(0, 0, -2).Format(time.RFC3339) + data, err := client.Explorer.ExportToCSV(ctx, organization, tfe.ExplorerQueryOptions{ + Filters: []*tfe.ExplorerQueryFilter{ + { + Index: 0, + Name: "all_checks_succeeded", + Operator: tfe.OpIs, + Value: "true", + }, + { + Index: 1, + Name: "updated_at", + Operator: tfe.OpIsAfter, + Value: since, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + reader := csv.NewReader(bytes.NewReader(data)) + rows, err := reader.ReadAll() + if err != nil { + log.Fatal(err) + } + + for _, r := range rows { + // Do something with each row in the CSV + fmt.Println(r) + } +} diff --git a/explorer.go b/explorer.go new file mode 100644 index 000000000..c926b5199 --- /dev/null +++ b/explorer.go @@ -0,0 +1,313 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "bytes" + "context" + "fmt" + "net/url" + "strings" + "time" +) + +// Compile-time proof of interface implementation. +var _ Explorer = (*explorer)(nil) + +// Explorer describes all the explorer related methods that the Terraform Enterprise API supports. +// +// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer +type Explorer interface { + // Query information about workspaces within an organization. + QueryWorkspaces(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerWorkspaceViewList, error) + // Query information about module version usage within an organization. + QueryModules(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerModuleViewList, error) + // Query information about provider version usage within an organization. + QueryProviders(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerProviderViewList, error) + // Query information about Terraform version usage within an organization. + QueryTerraformVersions(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerTerraformVersionViewList, error) + // Download a full, unpaged export of query results in CSV format. + ExportToCSV(ctx context.Context, organization string, options ExplorerQueryOptions) ([]byte, error) +} + +type explorer struct { + client *Client +} + +// ExplorerViewType represents the view types the Explorer API supports +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer#view-types +type ExplorerViewType string + +const ( + WorkspacesViewType ExplorerViewType = "workspaces" + ProvidersViewType ExplorerViewType = "providers" + ModulesViewType ExplorerViewType = "modules" + TerraformVersionsViewType ExplorerViewType = "tf_versions" +) + +// ExplorerQueryFilterOperator represents the supported operations for filtering. +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer#filter-operators +type ExplorerQueryFilterOperator string + +const ( + OpIs ExplorerQueryFilterOperator = "is" + OpIsNot ExplorerQueryFilterOperator = "is_not" + OpContains ExplorerQueryFilterOperator = "contains" + OpDoesNotContain ExplorerQueryFilterOperator = "does_not_contain" + OpIsEmpty ExplorerQueryFilterOperator = "is_empty" + OpIsNotEmpty ExplorerQueryFilterOperator = "is_not_empty" + OpGreaterThan ExplorerQueryFilterOperator = "gt" + OpLessThan ExplorerQueryFilterOperator = "lt" + OpGreaterThanOrEqual ExplorerQueryFilterOperator = "gteq" + OpLessThanOrEqual ExplorerQueryFilterOperator = "lteq" + OpIsBefore ExplorerQueryFilterOperator = "is_before" + OpIsAfter ExplorerQueryFilterOperator = "is_after" +) + +// ExplorerQueryFilter represents a filter query parameter for the query endpoint. +type ExplorerQueryFilter struct { + // Unique, sequential index for each filter, starting at 0 and incrementing by 1. + Index int + // Field name to apply the filter, valid for the queried view type. + Name string + // The operator use when filtering, must be supported by the field type. + Operator ExplorerQueryFilterOperator + // The filter value used by the filter during the query. + Value string +} + +func (eqf *ExplorerQueryFilter) toKeyValue() (string, string) { + key := fmt.Sprintf("filter[%d][%s][%s][0]", eqf.Index, eqf.Name, eqf.Operator) + return key, eqf.Value +} + +// ExplorerQueryOptions represents the parameter options for querying the Explorer API +type ExplorerQueryOptions struct { + ListOptions + + // Must be one of the following available views: WorkspacesViewType, ModulesViewType, + // ProvidersViewType or TerraformVersionsViewType. Each query function will + // set this value automatically, except ExportToCSV(). + View ExplorerViewType `url:"type"` + // Optional snake_case field to sort data, prefix with '-' for descending, must exist in view type. + Sort string `url:"sort,omitempty"` + + // List of fields to limit the data returned by the query. + Fields []string `url:"-"` + // List of filters to limit the data returned by the query. + Filters []*ExplorerQueryFilter `url:"-"` +} + +func (eqo *ExplorerQueryOptions) extractFilters() map[string][]string { + filterParams := make(map[string][]string) + for _, filter := range eqo.Filters { + if filter != nil { + k, v := filter.toKeyValue() + filterParams[k] = []string{v} + } + } + + // Append the fields query param, ensuring the correct view type is specified + if len(eqo.Fields) > 0 { + fieldsKey := fmt.Sprintf("fields[%s]", eqo.View) + filterParams[fieldsKey] = []string{strings.Join(eqo.Fields, ",")} + } + return filterParams +} + +// WorkspaceView represents information about a workspace in the target +// organization and any current runs associated with that workspace. +type WorkspaceView struct { + Type string `jsonapi:"primary,visibility-workspace"` + AllChecksSucceeded bool `jsonapi:"attr,all-checks-succeeded"` + ChecksErrored int `jsonapi:"attr,checks-errored"` + ChecksFailed int `jsonapi:"attr,checks-failed"` + ChecksPassed int `jsonapi:"attr,checks-passed"` + ChecksUnknown int `jsonapi:"attr,checks-unknown"` + CurrentRunAppliedAt time.Time `jsonapi:"attr,current-run-applied-at,rfc3339"` + CurrentRunExternalID string `jsonapi:"attr,current-run-external-id"` + CurrentRunStatus RunStatus `jsonapi:"attr,current-run-status"` + Drifted bool `jsonapi:"attr,drifted"` + ExternalID string `jsonapi:"attr,external-id"` + ModuleCount int `jsonapi:"attr,module-count"` + Modules interface{} `jsonapi:"attr,modules"` + OrganizationName string `jsonapi:"attr,organization-name"` + ProjectExternalID string `jsonapi:"attr,project-external-id"` + ProjectName string `jsonapi:"attr,project-name"` + ProviderCount int `jsonapi:"attr,provider-count"` + Providers interface{} `jsonapi:"attr,providers"` + ResourcesDrifted int `jsonapi:"attr,resources-drifted"` + ResourcesUndrifted int `jsonapi:"attr,resources-undrifted"` + StateVersionTerraformVersion string `jsonapi:"attr,state-version-terraform-version"` + VCSRepoIdentifier *string `jsonapi:"attr,vcs-repo-identifier"` + WorkspaceCreatedAt time.Time `jsonapi:"attr,workspace-created-at,rfc3339"` + WorkspaceName string `jsonapi:"attr,workspace-name"` + WorkspaceTerraformVersion string `jsonapi:"attr,workspace-terraform-version"` + WorkspaceUpdatedAt time.Time `jsonapi:"attr,workspace-updated-at,rfc3339"` +} + +// ModuleView represents information about a Terraform module version used by +// an organization. +type ModuleView struct { + Type string `jsonapi:"primary,visibility-module-version"` + Name string `jsonapi:"attr,name"` + Source string `jsonapi:"attr,source"` + Version string `jsonapi:"attr,version"` + WorkspaceCount int `jsonapi:"attr,workspace-count"` + Workspaces string `jsonapi:"attr,workspaces"` +} + +// ProviderView represents information about a Terraform provider version used +// by an organization. +type ProviderView struct { + Type string `jsonapi:"primary,visibility-provider-version"` + Name string `jsonapi:"attr,name"` + Source string `jsonapi:"attr,source"` + Version string `jsonapi:"attr,version"` + WorkspaceCount int `jsonapi:"attr,workspace-count"` + Workspaces string `jsonapi:"attr,workspaces"` +} + +// TerraformVersionView represents information about a Terraform version used +// by workspaces in an organization. +type TerraformVersionView struct { + Type string `jsonapi:"primary,visibility-tf-version"` + Version string `jsonapi:"attr,version"` + WorkspaceCount int `jsonapi:"attr,workspace-count"` + Workspaces string `jsonapi:"attr,workspaces"` +} + +// ExplorerWorkspaceViewList represents a list of workspace views +type ExplorerWorkspaceViewList struct { + *Pagination + Items []*WorkspaceView +} + +// ExplorerModuleViewList represents a list of module views +type ExplorerModuleViewList struct { + *Pagination + Items []*ModuleView +} + +// ExplorerProviderViewList represents a list of provider views +type ExplorerProviderViewList struct { + *Pagination + Items []*ProviderView +} + +// ExplorerTerraformVersionViewList represents a list of Terraform version views +type ExplorerTerraformVersionViewList struct { + *Pagination + Items []*TerraformVersionView +} + +// QueryWorkspaces invokes the Explorer's Query endpoint to return information +// about workspaces and their associated runs in the specified organization. +func (e *explorer) QueryWorkspaces(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerWorkspaceViewList, error) { + // Force the correct view type + options.View = WorkspacesViewType + + req, err := e.buildExplorerQueryRequest(organization, options) + if err != nil { + return nil, err + } + + eql := &ExplorerWorkspaceViewList{} + err = req.Do(ctx, eql) + if err != nil { + return nil, err + } + + return eql, nil +} + +// QueryModules invokes the Explorer's Query endpoint to return information +// about module versions in use across the specified organization. +func (e *explorer) QueryModules(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerModuleViewList, error) { + // Force the correct view type + options.View = ModulesViewType + + req, err := e.buildExplorerQueryRequest(organization, options) + if err != nil { + return nil, err + } + + eql := &ExplorerModuleViewList{} + err = req.Do(ctx, eql) + if err != nil { + return nil, err + } + + return eql, nil +} + +// QueryProviders invokes the Explorer's Query endpoint to return information +// about provider versions in use across the specified organization. +func (e *explorer) QueryProviders(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerProviderViewList, error) { + // Force the correct view type + options.View = ProvidersViewType + + req, err := e.buildExplorerQueryRequest(organization, options) + if err != nil { + return nil, err + } + + eql := &ExplorerProviderViewList{} + err = req.Do(ctx, eql) + if err != nil { + return nil, err + } + + return eql, nil +} + +// QueryTerraformVersions invokes the Explorer's Query endpoint to return information +// about Terraform versions in use across the specified organization. +func (e *explorer) QueryTerraformVersions(ctx context.Context, organization string, options ExplorerQueryOptions) (*ExplorerTerraformVersionViewList, error) { + // Force the correct view type + options.View = TerraformVersionsViewType + + req, err := e.buildExplorerQueryRequest(organization, options) + if err != nil { + return nil, err + } + + eql := &ExplorerTerraformVersionViewList{} + err = req.Do(ctx, eql) + if err != nil { + return nil, err + } + + return eql, nil +} + +// ExportToCSV performs an Explorer query and exports the results to CSV format. +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/explorer#export-data-as-csv +func (e *explorer) ExportToCSV(ctx context.Context, organization string, options ExplorerQueryOptions) ([]byte, error) { + filterParams := options.extractFilters() + + u := fmt.Sprintf("organizations/%s/explorer/export/csv", url.QueryEscape(organization)) + req, err := e.client.NewRequestWithAdditionalQueryParams("GET", u, options, filterParams) + if err != nil { + return nil, err + } + + // Override accept header + req.retryableRequest.Header.Set("Accept", "*/*") + + buf := &bytes.Buffer{} + err = req.Do(ctx, buf) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (e *explorer) buildExplorerQueryRequest(organization string, options ExplorerQueryOptions) (*ClientRequest, error) { + filterParams := options.extractFilters() + + u := fmt.Sprintf("organizations/%s/explorer", url.QueryEscape(organization)) + return e.client.NewRequestWithAdditionalQueryParams("GET", u, options, filterParams) +} diff --git a/explorer_integration_test.go b/explorer_integration_test.go new file mode 100644 index 000000000..4d2425e6b --- /dev/null +++ b/explorer_integration_test.go @@ -0,0 +1,246 @@ +package tfe + +import ( + "bytes" + "context" + "encoding/csv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExplorer_QueryModules(t *testing.T) { + client := testClient(t) + ctx := context.Background() + organization := testExplorerOrganization(t) + + t.Run("without any filter, sort, field query params", func(t *testing.T) { + wql, err := client.Explorer.QueryModules(ctx, organization, ExplorerQueryOptions{}) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + for _, view := range wql.Items { + require.NotEmpty(t, view.Name) + require.NotEmpty(t, view.Source) + require.NotEmpty(t, view.Version) + } + }) + + t.Run("with a sort query param", func(t *testing.T) { + wql, err := client.Explorer.QueryModules(ctx, organization, ExplorerQueryOptions{ + Sort: "workspace_count", + }) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + }) + + t.Run("with a filter query param", func(t *testing.T) { + wql, err := client.Explorer.QueryModules(ctx, organization, ExplorerQueryOptions{ + Filters: []*ExplorerQueryFilter{ + { + Index: 0, + Name: "workspace_count", + Operator: OpGreaterThan, + Value: "0", + }, + { + Index: 1, + Name: "workspaces", + Operator: OpContains, + Value: "tflocal", + }, + }, + }) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + for _, view := range wql.Items { + require.Contains(t, view.Workspaces, "tflocal") + require.Greater(t, view.WorkspaceCount, 0) + } + }) +} + +func TestExplorer_QueryProviders(t *testing.T) { + client := testClient(t) + ctx := context.Background() + organization := testExplorerOrganization(t) + + t.Run("without any filter, sort, field query params", func(t *testing.T) { + pql, err := client.Explorer.QueryProviders(ctx, organization, ExplorerQueryOptions{}) + require.NoError(t, err) + require.Greater(t, len(pql.Items), 0) + + for _, view := range pql.Items { + require.NotEmpty(t, view.Name) + require.NotEmpty(t, view.Version) + require.NotEmpty(t, view.Source) + } + }) + + t.Run("with a sort query param", func(t *testing.T) { + wql, err := client.Explorer.QueryWorkspaces(ctx, organization, ExplorerQueryOptions{ + Sort: "module_count", + }) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + prev := wql.Items[0] + for _, view := range wql.Items { + if view.ModuleCount > prev.ModuleCount { + t.Fatalf("entry not sorted: %d > %d", view.ModuleCount, prev.ModuleCount) + } + prev = view + } + }) + + t.Run("with a filter query param", func(t *testing.T) { + wql, err := client.Explorer.QueryWorkspaces(ctx, organization, ExplorerQueryOptions{ + Filters: []*ExplorerQueryFilter{ + { + Index: 0, + Name: "provider_count", + Operator: OpGreaterThan, + Value: "0", + }, + { + Index: 1, + Name: "current_run_status", + Operator: OpIs, + Value: "errored", + }, + }, + }) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + for _, view := range wql.Items { + require.Greater(t, view.ProviderCount, 0) + require.Equal(t, view.CurrentRunStatus, RunErrored) + } + }) +} + +func TestExplorer_QueryTerraformVersions(t *testing.T) { + client := testClient(t) + ctx := context.Background() + organization := testExplorerOrganization(t) + + t.Run("without any filter, sort, field query params", func(t *testing.T) { + wql, err := client.Explorer.QueryTerraformVersions(ctx, organization, ExplorerQueryOptions{}) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + for _, view := range wql.Items { + require.NotEmpty(t, view.Version) + } + }) + + t.Run("with a filter query param", func(t *testing.T) { + wql, err := client.Explorer.QueryTerraformVersions(ctx, organization, ExplorerQueryOptions{ + Filters: []*ExplorerQueryFilter{ + { + Index: 0, + Name: "version", + Operator: OpIs, + Value: "0.12.0", + }, + }, + }) + require.NoError(t, err) + require.Equal(t, len(wql.Items), 0) + }) +} + +func TestExplorer_QueryWorkspaces(t *testing.T) { + client := testClient(t) + ctx := context.Background() + organization := testExplorerOrganization(t) + + t.Run("without any filter, sort, field query params", func(t *testing.T) { + wql, err := client.Explorer.QueryWorkspaces(ctx, organization, ExplorerQueryOptions{}) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + for _, view := range wql.Items { + require.NotEmpty(t, view.WorkspaceName) + require.NotEmpty(t, view.ExternalID) + require.NotEmpty(t, view.WorkspaceCreatedAt) + } + }) + + t.Run("with a sort query param", func(t *testing.T) { + wql, err := client.Explorer.QueryWorkspaces(ctx, organization, ExplorerQueryOptions{ + Sort: "module_count", + }) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + prev := wql.Items[0] + for _, view := range wql.Items { + if view.ModuleCount > prev.ModuleCount { + t.Fatalf("entry not sorted: %d > %d", view.ModuleCount, prev.ModuleCount) + } + prev = view + } + }) + + t.Run("with a filter query param", func(t *testing.T) { + wql, err := client.Explorer.QueryWorkspaces(ctx, organization, ExplorerQueryOptions{ + Filters: []*ExplorerQueryFilter{ + { + Index: 0, + Name: "provider_count", + Operator: OpGreaterThan, + Value: "0", + }, + { + Index: 1, + Name: "current_run_status", + Operator: OpIs, + Value: "errored", + }, + }, + }) + require.NoError(t, err) + require.Greater(t, len(wql.Items), 0) + + for _, view := range wql.Items { + require.Greater(t, view.ProviderCount, 0) + require.Equal(t, view.CurrentRunStatus, RunErrored) + } + }) +} + +func TestExplorer_ExportToCSV(t *testing.T) { + client := testClient(t) + ctx := context.Background() + organization := testExplorerOrganization(t) + + csvResult, err := client.Explorer.ExportToCSV(ctx, organization, ExplorerQueryOptions{ + View: WorkspacesViewType, + Fields: []string{"workspace_name", "current_run_status"}, + Filters: []*ExplorerQueryFilter{ + { + Index: 0, + Name: "current_run_status", + Operator: OpIs, + Value: "applied", + }, + }, + }) + require.NoError(t, err) + r := csv.NewReader(bytes.NewReader(csvResult)) + + header, err := r.Read() + require.NoError(t, err) + assert.Equal(t, len(header), 2) + // Fields come in the order specified in the request + assert.Equal(t, header[0], "workspace_name") + assert.Equal(t, header[1], "current_run_status") + + rows, err := r.ReadAll() + require.NoError(t, err) + assert.Greater(t, len(rows), 0) +} diff --git a/github_app_installation.go b/github_app_installation.go index 0af570112..855467c89 100644 --- a/github_app_installation.go +++ b/github_app_installation.go @@ -51,7 +51,6 @@ type GHAInstallationListOptions struct { func (s *gHAInstallations) List(ctx context.Context, options *GHAInstallationListOptions) (*GHAInstallationList, error) { u := "github-app/installations" req, err := s.client.NewRequest("GET", u, options) - fmt.Println(u) if err != nil { return nil, err } diff --git a/helper_test.go b/helper_test.go index 6d7062426..d19c5d095 100644 --- a/helper_test.go +++ b/helper_test.go @@ -2917,6 +2917,14 @@ func requireExactlyOneNotEmpty(t *testing.T, v ...any) { } } +func testExplorerOrganization(t *testing.T) string { + organization, ok := os.LookupEnv("EXPLORER_TEST_ORGANIZATION") + if !ok { + t.Skip("Skipping Explorer Integration tests, organization var is not set") + } + return organization +} + func runTaskCallbackMockServer(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { diff --git a/tfe.go b/tfe.go index a4f40be3e..4e1838c97 100644 --- a/tfe.go +++ b/tfe.go @@ -132,6 +132,7 @@ type Client struct { Comments Comments ConfigurationVersions ConfigurationVersions CostEstimates CostEstimates + Explorer Explorer GHAInstallations GHAInstallations GPGKeys GPGKeys NotificationConfigurations NotificationConfigurations @@ -460,6 +461,7 @@ func NewClient(cfg *Config) (*Client, error) { client.ConfigurationVersions = &configurationVersions{client: client} client.GHAInstallations = &gHAInstallations{client: client} client.CostEstimates = &costEstimates{client: client} + client.Explorer = &explorer{client: client} client.GPGKeys = &gpgKeys{client: client} client.RegistryNoCodeModules = ®istryNoCodeModules{client: client} client.NotificationConfigurations = ¬ificationConfigurations{client: client}