From f0163064e109bdfda36b8ae6a8fb01c6cd7661ee Mon Sep 17 00:00:00 2001 From: Aleksandr Rybolovlev Date: Tue, 18 Feb 2025 10:21:30 +0100 Subject: [PATCH 1/5] Add support for listing Runs in an organization --- run.go | 77 +++++++++++++++++++++++++ run_integration_test.go | 125 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/run.go b/run.go index 4ec0c7823..b31417ee2 100644 --- a/run.go +++ b/run.go @@ -21,6 +21,9 @@ type Runs interface { // List all the runs of the given workspace. List(ctx context.Context, workspaceID string, options *RunListOptions) (*RunList, error) + // List all the runs of the given organization. + ListForOrganization(ctx context.Context, organisation string, options *RunListForOrganizationOptions) (*RunList, error) + // Create a new run with the given options. Create(ctx context.Context, options RunCreateOptions) (*Run, error) @@ -250,6 +253,52 @@ type RunListOptions struct { Include []RunIncludeOpt `url:"include,omitempty"` } +// RunListForOrganizationOptions represents the options for listing runs for an organization. +type RunListForOrganizationOptions struct { + ListOptions + + // Optional: Searches runs that matches the supplied VCS username. + User string `url:"search[user],omitempty"` + + // Optional: Searches runs that matches the supplied commit sha. + Commit string `url:"search[commit],omitempty"` + + // Optional: Searches for runs that match the VCS username, commit sha, run_id, or run message your specify. + // The presence of search[commit] or search[user] takes priority over this parameter and will be omitted. + Basic string `url:"search[basic],omitempty"` + + // Optional: Comma-separated list of acceptable run statuses. + // Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-states, + // or as constants with the RunStatus string type. + Status string `url:"filter[status],omitempty"` + + // Optional: Comma-separated list of acceptable run sources. + // Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-sources, + // or as constants with the RunSource string type. + Source string `url:"filter[source],omitempty"` + + // Optional: Comma-separated list of acceptable run operation types. + // Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-operations, + // or as constants with the RunOperation string type. + Operation string `url:"filter[operation],omitempty"` + + // Optional: Comma-separated list of agent pool names. + AgentPoolNames string `url:"filter[agent_pool_names],omitempty"` + + // Optional: Comma-separated list of run status groups. + StatusGroup string `url:"filter[status_group],omitempty"` + + // Optional: Comma-separated list of run timeframe. + Timeframe string `url:"filter[timeframe],omitempty"` + + // Optional: Comma-separated list of workspace names. The result lists runs that belong to one of the workspaces your specify. + WorkspaceNames string `url:"filter[workspace_names],omitempty"` + + // Optional: A list of relations to include. See available resources: + // https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#available-related-resources + Include []RunIncludeOpt `url:"include,omitempty"` +} + // RunReadOptions represents the options for reading a run. type RunReadOptions struct { // Optional: A list of relations to include. See available resources: @@ -398,6 +447,30 @@ func (s *runs) List(ctx context.Context, workspaceID string, options *RunListOpt return rl, nil } +// List all the runs of the given workspace. +func (s *runs) ListForOrganization(ctx context.Context, organization string, options *RunListForOrganizationOptions) (*RunList, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("organizations/%s/runs", url.PathEscape(organization)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + rl := &RunList{} + err = req.Do(ctx, rl) + if err != nil { + return nil, err + } + + return rl, nil +} + // Create a new run with the given options. func (s *runs) Create(ctx context.Context, options RunCreateOptions) (*Run, error) { if err := options.valid(); err != nil { @@ -546,3 +619,7 @@ func (o *RunReadOptions) valid() error { func (o *RunListOptions) valid() error { return nil } + +func (o *RunListForOrganizationOptions) valid() error { + return nil +} diff --git a/run_integration_test.go b/run_integration_test.go index d0f05d293..45ac41b0a 100644 --- a/run_integration_test.go +++ b/run_integration_test.go @@ -720,3 +720,128 @@ func TestRunCreateOptions_Marshal(t *testing.T) { assert.Equal(t, string(bodyBytes), expectedBody) } + +func TestRunsListForOrganization(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + apTest, _ := createAgentPool(t, client, orgTest) + + wTest, _ := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ + Name: String(randomString(t)), + ExecutionMode: String("agent"), + AgentPoolID: &apTest.ID, + }) + rTest1, _ := createRun(t, client, wTest) + rTest2, _ := createRun(t, client, wTest) + + t.Run("without list options", func(t *testing.T) { + rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, nil) + require.NoError(t, err) + + found := []string{} + for _, r := range rl.Items { + found = append(found, r.ID) + } + + assert.Contains(t, found, rTest1.ID) + assert.Contains(t, found, rTest2.ID) + assert.Equal(t, 1, rl.CurrentPage) + assert.Equal(t, 2, rl.TotalCount) + }) + + t.Run("without list options and include as nil", func(t *testing.T) { + rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{ + Include: []RunIncludeOpt{}, + }) + require.NoError(t, err) + require.NotEmpty(t, rl.Items) + + found := []string{} + for _, r := range rl.Items { + found = append(found, r.ID) + } + + assert.Contains(t, found, rTest1.ID) + assert.Contains(t, found, rTest2.ID) + assert.Equal(t, 1, rl.CurrentPage) + assert.Equal(t, 2, rl.TotalCount) + }) + + t.Run("with list options", func(t *testing.T) { + t.Skip("paging not supported yet in API") + + // Request a page number which is out of range. The result should + // be successful, but return no results if the paging options are + // properly passed along. + rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{ + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + }) + require.NoError(t, err) + assert.Empty(t, rl.Items) + assert.Equal(t, 999, rl.CurrentPage) + assert.Equal(t, 2, rl.TotalCount) + }) + + t.Run("with workspace included", func(t *testing.T) { + rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{ + Include: []RunIncludeOpt{RunWorkspace}, + }) + require.NoError(t, err) + + require.NotEmpty(t, rl.Items) + require.NotNil(t, rl.Items[0].Workspace) + assert.NotEmpty(t, rl.Items[0].Workspace.Name) + }) + + t.Run("without a valid organization name", func(t *testing.T) { + rl, err := client.Runs.ListForOrganization(ctx, badIdentifier, nil) + assert.Nil(t, rl) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) + + t.Run("with filter by agent pool", func(t *testing.T) { + rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{ + AgentPoolNames: apTest.Name, + }) + require.NoError(t, err) + + found := make([]string, len(rl.Items)) + for i, r := range rl.Items { + found[i] = r.ID + } + + assert.Contains(t, found, rTest1.ID) + assert.Contains(t, found, rTest2.ID) + assert.Equal(t, 1, rl.CurrentPage) + assert.Equal(t, 2, rl.TotalCount) + }) + + t.Run("with filter by workspace", func(t *testing.T) { + rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{ + WorkspaceNames: wTest.Name, + Include: []RunIncludeOpt{RunWorkspace}, + }) + require.NoError(t, err) + + found := make([]string, len(rl.Items)) + for i, r := range rl.Items { + found[i] = r.ID + } + + assert.Contains(t, found, rTest1.ID) + assert.Contains(t, found, rTest2.ID) + require.NotNil(t, rl.Items[0].Workspace) + assert.NotEmpty(t, rl.Items[0].Workspace.Name) + require.NotNil(t, rl.Items[1].Workspace) + assert.NotEmpty(t, rl.Items[1].Workspace.Name) + assert.Equal(t, 1, rl.CurrentPage) + assert.Equal(t, 2, rl.TotalCount) + }) +} From b8ce4690f2bd5ca98e0c0f37a71b51d1d910bf7d Mon Sep 17 00:00:00 2001 From: Aleksandr Rybolovlev Date: Tue, 18 Feb 2025 11:24:19 +0100 Subject: [PATCH 2/5] Add mocks --- mocks/run_mocks.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mocks/run_mocks.go b/mocks/run_mocks.go index da01c808d..23f457c3c 100644 --- a/mocks/run_mocks.go +++ b/mocks/run_mocks.go @@ -140,6 +140,21 @@ func (mr *MockRunsMockRecorder) List(ctx, workspaceID, options any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRuns)(nil).List), ctx, workspaceID, options) } +// ListForOrganization mocks base method. +func (m *MockRuns) ListForOrganization(ctx context.Context, organisation string, options *tfe.RunListForOrganizationOptions) (*tfe.RunList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListForOrganization", ctx, organisation, options) + ret0, _ := ret[0].(*tfe.RunList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListForOrganization indicates an expected call of ListForOrganization. +func (mr *MockRunsMockRecorder) ListForOrganization(ctx, organisation, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListForOrganization", reflect.TypeOf((*MockRuns)(nil).ListForOrganization), ctx, organisation, options) +} + // Read mocks base method. func (m *MockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { m.ctrl.T.Helper() From 907ac1542c1cc5b207ec3ac6d2ac2b8b2a286212 Mon Sep 17 00:00:00 2001 From: Aleksandr Rybolovlev Date: Wed, 19 Feb 2025 09:41:28 +0100 Subject: [PATCH 3/5] Address review comments: organisation to organization --- mocks/run_mocks.go | 8 ++++---- run.go | 2 +- test_run_integration_test.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mocks/run_mocks.go b/mocks/run_mocks.go index 23f457c3c..00414fe9c 100644 --- a/mocks/run_mocks.go +++ b/mocks/run_mocks.go @@ -141,18 +141,18 @@ func (mr *MockRunsMockRecorder) List(ctx, workspaceID, options any) *gomock.Call } // ListForOrganization mocks base method. -func (m *MockRuns) ListForOrganization(ctx context.Context, organisation string, options *tfe.RunListForOrganizationOptions) (*tfe.RunList, error) { +func (m *MockRuns) ListForOrganization(ctx context.Context, organization string, options *tfe.RunListForOrganizationOptions) (*tfe.RunList, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListForOrganization", ctx, organisation, options) + ret := m.ctrl.Call(m, "ListForOrganization", ctx, organization, options) ret0, _ := ret[0].(*tfe.RunList) ret1, _ := ret[1].(error) return ret0, ret1 } // ListForOrganization indicates an expected call of ListForOrganization. -func (mr *MockRunsMockRecorder) ListForOrganization(ctx, organisation, options any) *gomock.Call { +func (mr *MockRunsMockRecorder) ListForOrganization(ctx, organization, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListForOrganization", reflect.TypeOf((*MockRuns)(nil).ListForOrganization), ctx, organisation, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListForOrganization", reflect.TypeOf((*MockRuns)(nil).ListForOrganization), ctx, organization, options) } // Read mocks base method. diff --git a/run.go b/run.go index b31417ee2..e87b6919a 100644 --- a/run.go +++ b/run.go @@ -22,7 +22,7 @@ type Runs interface { List(ctx context.Context, workspaceID string, options *RunListOptions) (*RunList, error) // List all the runs of the given organization. - ListForOrganization(ctx context.Context, organisation string, options *RunListForOrganizationOptions) (*RunList, error) + ListForOrganization(ctx context.Context, organization string, options *RunListForOrganizationOptions) (*RunList, error) // Create a new run with the given options. Create(ctx context.Context, options RunCreateOptions) (*Run, error) diff --git a/test_run_integration_test.go b/test_run_integration_test.go index bd205c776..0f1ecdcd3 100644 --- a/test_run_integration_test.go +++ b/test_run_integration_test.go @@ -168,7 +168,7 @@ func TestTestRunsCreate(t *testing.T) { _, err := client.TestRuns.Create(ctx, options) require.Equal(t, ErrRequiredRegistryModule, err) }) - t.Run("without an organisation", func(t *testing.T) { + t.Run("without an organization", func(t *testing.T) { rm := &RegistryModule{ ID: rmTest.ID, Name: rmTest.Name, From 5451615f83d48c48e99a172f20d8d0e2725ab61f Mon Sep 17 00:00:00 2001 From: Aleksandr Rybolovlev Date: Wed, 19 Feb 2025 09:48:43 +0100 Subject: [PATCH 4/5] Address review comments: fix tests --- run_integration_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/run_integration_test.go b/run_integration_test.go index 45ac41b0a..a4784acbb 100644 --- a/run_integration_test.go +++ b/run_integration_test.go @@ -772,9 +772,7 @@ func TestRunsListForOrganization(t *testing.T) { }) t.Run("with list options", func(t *testing.T) { - t.Skip("paging not supported yet in API") - - // Request a page number which is out of range. The result should + // Request a page number that is out of range. The result should // be successful, but return no results if the paging options are // properly passed along. rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{ From 84ddafa5d4e73468e69fd7c089c71ffc5d801031 Mon Sep 17 00:00:00 2001 From: Aleksandr Rybolovlev Date: Wed, 19 Feb 2025 09:53:29 +0100 Subject: [PATCH 5/5] Add a changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a076205d..058cec6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Enhancements * Adds `DefaultProject` to `OrganizationUpdateOptions` to support updating an organization's default project. This provides BETA support, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users, by @mkam [#1056](https://github.com/hashicorp/go-tfe/pull/1056) +* Adds a new method `ListForOrganization` to list Runs in an organization by @arybolovlev [#1059](https://github.com/hashicorp/go-tfe/pull/1059) ## Bug fixes