From 76df95cb33cf241f5d301849ed53f321b5786d20 Mon Sep 17 00:00:00 2001 From: uturunku1 Date: Tue, 1 Mar 2022 17:15:51 -0800 Subject: [PATCH] add validation check for "include" typed string values revert two changes made on PR #339 --- admin_organization.go | 32 +++- admin_run.go | 61 ++++--- admin_run_integration_test.go | 8 + admin_user.go | 32 +++- admin_workspace.go | 29 ++++ agent_pool.go | 44 ++++- agent_pool_integration_test.go | 23 ++- configuration_version.go | 55 +++++- configuration_version_integration_test.go | 2 +- docs/CONTRIBUTING.md | 178 ++++++++++++-------- errors.go | 2 +- mocks/agent_pool_mocks.go | 15 ++ oauth_client.go | 28 +++ organization_membership.go | 51 +++++- organization_membership_integration_test.go | 18 +- policy_check.go | 28 +++ policy_set.go | 28 +++ run.go | 41 +++++ run_task.go | 43 +++++ run_trigger.go | 47 ++++-- state_version.go | 40 +++++ task_stages.go | 32 +++- team.go | 29 +++- workspace.go | 43 +++++ 24 files changed, 763 insertions(+), 146 deletions(-) diff --git a/admin_organization.go b/admin_organization.go index 9f95c5d5e..c43013743 100644 --- a/admin_organization.go +++ b/admin_organization.go @@ -76,9 +76,7 @@ type AdminOrganizationList struct { // https://www.terraform.io/docs/cloud/api/admin/organizations.html#available-related-resources type AdminOrgIncludeOpt string -const ( - AdminOrgOwners AdminOrgIncludeOpt = "owners" -) +const AdminOrgOwners AdminOrgIncludeOpt = "owners" // AdminOrganizationListOptions represents the options for listing organizations via Admin API. type AdminOrganizationListOptions struct { @@ -103,6 +101,9 @@ type AdminOrganizationID struct { // List all the organizations visible to the current user. func (s *adminOrganizations) List(ctx context.Context, options *AdminOrganizationListOptions) (*AdminOrganizationList, error) { + if err := options.valid(); err != nil { + return nil, err + } u := "admin/organizations" req, err := s.client.newRequest("GET", u, options) if err != nil { @@ -225,3 +226,28 @@ func (s *adminOrganizations) Delete(ctx context.Context, organization string) er return s.client.do(ctx, req, nil) } + +func (o *AdminOrganizationListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateAdminOrgIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateAdminOrgIncludeParams(params []AdminOrgIncludeOpt) error { + for _, p := range params { + switch p { + case AdminOrgOwners: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/admin_run.go b/admin_run.go index 78e5a5be9..36e756037 100644 --- a/admin_run.go +++ b/admin_run.go @@ -116,39 +116,48 @@ func (s *adminRuns) ForceCancel(ctx context.Context, runID string, options Admin } func (o *AdminRunsListOptions) valid() error { - if o == nil { // no need to validate fields + if o == nil { // nothing to validate return nil } - if validString(&o.RunStatus) { - validRunStatus := map[string]int{ - string(RunApplied): 1, - string(RunApplyQueued): 1, - string(RunApplying): 1, - string(RunCanceled): 1, - string(RunConfirmed): 1, - string(RunCostEstimated): 1, - string(RunCostEstimating): 1, - string(RunDiscarded): 1, - string(RunErrored): 1, - string(RunPending): 1, - string(RunPlanQueued): 1, - string(RunPlanned): 1, - string(RunPlannedAndFinished): 1, - string(RunPlanning): 1, - string(RunPolicyChecked): 1, - string(RunPolicyChecking): 1, - string(RunPolicyOverride): 1, - string(RunPolicySoftFailed): 1, - } - runStatus := strings.Split(o.RunStatus, ",") + if err := validateAdminRunFilterParams(o.RunStatus); err != nil { + return err + } + + if err := validateAdminRunIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateAdminRunFilterParams(runStatus string) error { + // For the platform, an invalid filter value is a semantically understood query that returns an empty set, no error, no warning. But for go-tfe, an invalid value is good enough reason to error prior to a network call to the platform: + if validString(&runStatus) { + runStatuses := strings.Split(runStatus, ",") // iterate over our statuses, and ensure it is valid. - for _, status := range runStatus { - if _, present := validRunStatus[status]; !present { - return fmt.Errorf("invalid value %s for run status", status) + for _, status := range runStatuses { + switch status { + case string(RunApplied), string(RunApplyQueued), string(RunApplying), string(RunCanceled), string(RunConfirmed), string(RunCostEstimate), string(RunCostEstimating), string(RunDiscarded), string(RunErrored), string(RunPending), string(RunPlanQueued), string(RunPlanned), string(RunPlannedAndFinished), string(RunPlanning), string(RunPolicyChecked), string(RunPolicyChecking), string(RunPolicyOverride), string(RunPolicySoftFailed): + // do nothing + default: + return fmt.Errorf(`invalid value "%s" for run status`, status) } } } return nil } + +func validateAdminRunIncludeParams(params []AdminRunIncludeOpt) error { + for _, p := range params { + switch p { + case AdminRunWorkspace, AdminRunWorkspaceOrg, AdminRunWorkspaceOrgOwners: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/admin_run_integration_test.go b/admin_run_integration_test.go index 6477f94d2..20d092aca 100644 --- a/admin_run_integration_test.go +++ b/admin_run_integration_test.go @@ -92,6 +92,14 @@ func TestAdminRuns_List(t *testing.T) { assert.NotEmpty(t, rl.Items[0].Workspace.Organization.Name) }) + t.Run("with invalid Include option", func(t *testing.T) { + _, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{ + Include: []AdminRunIncludeOpt{"workpsace"}, + }) + + assert.Equal(t, err, ErrInvalidIncludeValue) + }) + t.Run("with RunStatus.pending filter", func(t *testing.T) { r1, err := client.Runs.Read(ctx, rTest1.ID) assert.NoError(t, err) diff --git a/admin_user.go b/admin_user.go index e4ca116e1..80fc6ab71 100644 --- a/admin_user.go +++ b/admin_user.go @@ -68,9 +68,7 @@ type AdminUserList struct { // https://www.terraform.io/docs/cloud/api/admin/users.html#available-related-resources type AdminUserIncludeOpt string -const ( - AdminUserOrgs AdminUserIncludeOpt = "organizations" -) +const AdminUserOrgs AdminUserIncludeOpt = "organizations" // AdminUserListOptions represents the options for listing users. // https://www.terraform.io/docs/cloud/api/admin/users.html#query-parameters @@ -93,6 +91,10 @@ type AdminUserListOptions struct { // List all user accounts in the Terraform Enterprise installation func (a *adminUsers) List(ctx context.Context, options *AdminUserListOptions) (*AdminUserList, error) { + if err := options.valid(); err != nil { + return nil, err + } + u := "admin/users" req, err := a.client.newRequest("GET", u, options) if err != nil { @@ -228,3 +230,27 @@ func (a *adminUsers) Disable2FA(ctx context.Context, userID string) (*AdminUser, return au, nil } + +func (o *AdminUserListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateAdminUserIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateAdminUserIncludeParams(params []AdminUserIncludeOpt) error { + for _, p := range params { + switch p { + case AdminUserOrgs: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + return nil +} diff --git a/admin_workspace.go b/admin_workspace.go index a2957b1a6..1fcfc90d1 100644 --- a/admin_workspace.go +++ b/admin_workspace.go @@ -76,6 +76,10 @@ type AdminWorkspaceList struct { // List all the workspaces within a workspace. func (s *adminWorkspaces) List(ctx context.Context, options *AdminWorkspaceListOptions) (*AdminWorkspaceList, error) { + if err := options.valid(); err != nil { + return nil, err + } + u := "admin/workspaces" req, err := s.client.newRequest("GET", u, options) if err != nil { @@ -126,3 +130,28 @@ func (s *adminWorkspaces) Delete(ctx context.Context, workspaceID string) error return s.client.do(ctx, req, nil) } + +func (o *AdminWorkspaceListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateAdminWSIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateAdminWSIncludeParams(params []AdminWorkspaceIncludeOpt) error { + for _, p := range params { + switch p { + case AdminWorkspaceOrg, AdminWorkspaceCurrentRun, AdminWorkspaceOrgOwners: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/agent_pool.go b/agent_pool.go index f5608debf..c97a8d98c 100644 --- a/agent_pool.go +++ b/agent_pool.go @@ -23,6 +23,9 @@ type AgentPools interface { // Read a agent pool by its ID. Read(ctx context.Context, agentPoolID string) (*AgentPool, error) + // Read a agent pool by its ID with the given options. + ReadWithOptions(ctx context.Context, agentPoolID string, options *AgentPoolReadOptions) (*AgentPool, error) + // Update an agent pool by its ID. Update(ctx context.Context, agentPool string, options AgentPoolUpdateOptions) (*AgentPool, error) @@ -48,6 +51,7 @@ type AgentPool struct { // Relations Organization *Organization `jsonapi:"relation,organization"` + Workspaces []*Workspace `jsonapi:"relation,workspaces"` } // A list of relations to include @@ -55,11 +59,13 @@ type AgentPoolIncludeOpt string const AgentPoolWorkspaces AgentPoolIncludeOpt = "workspaces" +type AgentPoolReadOptions struct { + Include []AgentPoolIncludeOpt `url:"include,omitempty"` +} + // AgentPoolListOptions represents the options for listing agent pools. type AgentPoolListOptions struct { ListOptions - - Include []AgentPoolIncludeOpt `url:"include,omitempty"` } // AgentPoolCreateOptions represents the options for creating an agent pool. @@ -120,11 +126,19 @@ func (s *agentPools) Create(ctx context.Context, organization string, options Ag return pool, nil } -// Read a single agent pool by its ID. +// Read a single agent pool by its ID func (s *agentPools) Read(ctx context.Context, agentpoolID string) (*AgentPool, error) { + return s.ReadWithOptions(ctx, agentpoolID, nil) +} + +// Read a single agent pool by its ID with options. +func (s *agentPools) ReadWithOptions(ctx context.Context, agentpoolID string, options *AgentPoolReadOptions) (*AgentPool, error) { if !validStringID(&agentpoolID) { return nil, ErrInvalidAgentPoolID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentpoolID)) req, err := s.client.newRequest("GET", u, nil) @@ -209,3 +223,27 @@ func (o AgentPoolUpdateOptions) valid() error { } return nil } + +func (o *AgentPoolReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + if err := validateAgentPoolIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateAgentPoolIncludeParams(params []AgentPoolIncludeOpt) error { + for _, p := range params { + switch p { + case AgentPoolWorkspaces: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/agent_pool_integration_test.go b/agent_pool_integration_test.go index d56046218..73107445b 100644 --- a/agent_pool_integration_test.go +++ b/agent_pool_integration_test.go @@ -49,14 +49,6 @@ func TestAgentPoolsList(t *testing.T) { assert.Equal(t, 1, pools.TotalCount) }) - t.Run("with Include options", func(t *testing.T) { - pools, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{ - Include: []AgentPoolIncludeOpt{AgentPoolWorkspaces}, - }) - require.NoError(t, err) - assert.NotEmpty(t, pools.Items[0].Organization.Name) - }) - t.Run("without a valid organization", func(t *testing.T) { pools, err := client.AgentPools.List(ctx, badIdentifier, nil) assert.Nil(t, pools) @@ -139,6 +131,21 @@ func TestAgentPoolsRead(t *testing.T) { assert.Nil(t, k) assert.EqualError(t, err, ErrInvalidAgentPoolID.Error()) }) + + t.Run("with Include option", func(t *testing.T) { + _, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{ + Name: String("foo"), + ExecutionMode: String("agent"), + AgentPoolID: String(pool.ID), + }) + defer wTestCleanup() + + k, err := client.AgentPools.ReadWithOptions(ctx, pool.ID, &AgentPoolReadOptions{ + Include: []AgentPoolIncludeOpt{AgentPoolWorkspaces}, + }) + require.NoError(t, err) + assert.NotEmpty(t, k.Workspaces[0]) + }) } func TestAgentPoolsUpdate(t *testing.T) { diff --git a/configuration_version.go b/configuration_version.go index d7370b299..e88de3afb 100644 --- a/configuration_version.go +++ b/configuration_version.go @@ -98,20 +98,20 @@ type CVStatusTimestamps struct { StartedAt time.Time `jsonapi:"attr,started-at,rfc3339"` } -// ConfigurationVersionIncludeOpt represents the available options for include query params. +// ConfigVerIncludeOpt represents the available options for include query params. // https://www.terraform.io/docs/cloud/api/configuration-versions.html#available-related-resources -type ConfigurationVersionIncludeOpt string +type ConfigVerIncludeOpt string const ( - ConfigurationVerIngressAttributes ConfigurationVersionIncludeOpt = "ingress_attributes" - ConfigurationRun ConfigurationVersionIncludeOpt = "run" + ConfigVerIngressAttributes ConfigVerIncludeOpt = "ingress_attributes" + ConfigVerRun ConfigVerIncludeOpt = "run" ) // ConfigurationVersionReadOptions represents the options for reading a configuration version. type ConfigurationVersionReadOptions struct { // Optional: A list of relations to include. See available resources: // https://www.terraform.io/docs/cloud/api/configuration-versions.html#available-related-resources - Include []ConfigurationVersionIncludeOpt `url:"include,omitempty"` + Include []ConfigVerIncludeOpt `url:"include,omitempty"` } // ConfigurationVersionListOptions represents the options for listing @@ -120,7 +120,7 @@ type ConfigurationVersionListOptions struct { ListOptions // Optional: A list of relations to include. See available resources: // https://www.terraform.io/docs/cloud/api/configuration-versions.html#available-related-resources - Include []ConfigurationVersionIncludeOpt `url:"include,omitempty"` + Include []ConfigVerIncludeOpt `url:"include,omitempty"` } // ConfigurationVersionCreateOptions represents the options for creating a @@ -170,6 +170,9 @@ func (s *configurationVersions) List(ctx context.Context, workspaceID string, op if !validStringID(&workspaceID) { return nil, ErrInvalidWorkspaceID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID)) req, err := s.client.newRequest("GET", u, options) @@ -218,6 +221,9 @@ func (s *configurationVersions) ReadWithOptions(ctx context.Context, cvID string if !validStringID(&cvID) { return nil, ErrInvalidConfigVersionID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("configuration-versions/%s", url.QueryEscape(cvID)) req, err := s.client.newRequest("GET", u, options) @@ -260,3 +266,40 @@ func (s *configurationVersions) Upload(ctx context.Context, u, path string) erro return s.client.do(ctx, req, nil) } + +func (o *ConfigurationVersionReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateConfigVerIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func (o *ConfigurationVersionListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateConfigVerIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateConfigVerIncludeParams(params []ConfigVerIncludeOpt) error { + for _, p := range params { + switch p { + case ConfigVerIngressAttributes, ConfigVerRun: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/configuration_version_integration_test.go b/configuration_version_integration_test.go index 603fc26f1..2bd192e43 100644 --- a/configuration_version_integration_test.go +++ b/configuration_version_integration_test.go @@ -173,7 +173,7 @@ func TestConfigurationVersionsReadWithOptions(t *testing.T) { t.Run("when the configuration version exists", func(t *testing.T) { options := &ConfigurationVersionReadOptions{ - Include: []ConfigurationVersionIncludeOpt{ConfigurationVerIngressAttributes}, + Include: []ConfigVerIncludeOpt{ConfigVerIngressAttributes}, } cv, err := client.ConfigurationVersions.ReadWithOptions(ctx, cv.ID, options) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2595070e5..a78eae1f5 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -69,12 +69,6 @@ type Example struct { Organization *Organization `jsonapi:"relation,organization"` } -// ExampleList represents a list of examples -type ExampleList struct { - *Pagination - Items []*Example -} - // ExampleCreateOptions represents the set of options for creating an example type ExampleCreateOptions struct { // Type is a public field utilized by JSON:API to @@ -93,16 +87,53 @@ type ExampleCreateOptions struct { OptionalValue *string `jsonapi:"attr,optional-value,omitempty"` } -func (o *ExampleCreateOptions) valid() error { - if !validString(&o.Name) { - return ErrRequiredName - } +// ExampleIncludeOpt represents the available options for include query params. +// https://www.terraform.io/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL) +type ExampleIncludeOpt string - if !validString(&o.URL) { - return ErrInvalidRunTaskURL - } +const ( + ExampleOrganization ExampleIncludeOpt = "organization" + ExampleRun ExampleIncludeOpt = "run" +) - return nil +// ExampleListOptions represents the set of options for listing examples +type ExampleListOptions struct { + ListOptions + + // Optional: A list of relations to include with an example. See available resources: + // https://www.terraform.io/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL) + Include []ExampleIncludeOpt `url:"include,omitempty"` +} + +// ExampleList represents a list of examples +type ExampleList struct { + *Pagination + Items []*Example +} + +// ExampleReadOptions represents the set of options for reading an example +type ExampleReadOptions struct { + // Optional: A list of relations to include with an example. See available resources: + // https://www.terraform.io/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL) + Include []RunTaskIncludeOpt `url:"include,omitempty"` +} + +// ExampleUpdateOptions represents the set of options for updating an organization's examples +type ExampleUpdateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,tasks"` + + // Optional: The name of the example, defaults to previous value + Name *string `jsonapi:"attr,name,omitempty"` + + // Optional: The URL to send a example payload, defaults to previous value + URL *string `jsonapi:"attr,url,omitempty"` + + // Optional: An optional value + OptionalValue *string `jsonapi:"attr,optional-value,omitempty"` } // Create is used to create a new example for an organization @@ -130,28 +161,14 @@ func (s *example) Create(ctx context.Context, organization string, options Examp return r, nil } -// ExampleIncludeOpt represents the available options for include query params. -// https://www.terraform.io/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL) -type ExampleIncludeOpt string - -const ( - ExampleOrganization ExampleIncludeOpt = "organization" -) - -// ExampleListOptions represents the set of options for listing examples -type ExampleListOptions struct { - ListOptions - - // Optional: A list of relations to include with an example. See available resources: - // https://www.terraform.io/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL) - Include []ExampleIncludeOpt `url:"include,omitempty"` -} - // List all the examples for an organization func (s *example) List(ctx context.Context, organization string, options *ExampleListOptions) (*ExampleList, error) { if !validStringID(&organization) { return nil, ErrInvalidOrg } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("organizations/%s/examples", url.QueryEscape(organization)) req, err := s.client.newRequest("GET", u, options) @@ -173,18 +190,14 @@ func (s *example) Read(ctx context.Context, exampleID string) (*Example, error) return s.ReadWithOptions(ctx, exampleID, nil) } -// ExampleReadOptions represents the set of options for reading an example -type ExampleReadOptions struct { - // Optional: A list of relations to include with an example. See available resources: - // https://www.terraform.io/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL) - Include []RunTaskIncludeOpt `url:"include,omitempty"` -} - // Read is used to read an organization's example by ID with options func (s *example) ReadWithOptions(ctx context.Context, exampleID string, options *ExampleReadOptions) (*Example, error) { if !validStringID(&exampleID) { return nil, ErrInvalidExampleID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("examples/%s", url.QueryEscape(exampleID)) req, err := s.client.newRequest("GET", u, options) @@ -201,36 +214,6 @@ func (s *example) ReadWithOptions(ctx context.Context, exampleID string, options return e, nil } -// ExampleUpdateOptions represents the set of options for updating an organization's examples -type ExampleUpdateOptions struct { - // Type is a public field utilized by JSON:API to - // set the resource type via the field tag. - // It is not a user-defined value and does not need to be set. - // https://jsonapi.org/format/#crud-creating - Type string `jsonapi:"primary,tasks"` - - // Optional: The name of the example, defaults to previous value - Name *string `jsonapi:"attr,name,omitempty"` - - // Optional: The URL to send a example payload, defaults to previous value - URL *string `jsonapi:"attr,url,omitempty"` - - // Optional: An optional value - OptionalValue *string `jsonapi:"attr,optional-value,omitempty"` -} - -func (o *ExampleUpdateOptions) valid() error { - if o.Name != nil && !validString(o.Name) { - return ErrRequiredName - } - - if o.URL != nil && !validString(o.URL) { - return ErrInvalidRunTaskURL - } - - return nil -} - // Update an existing example for an organization by ID func (s *example) Update(ctx context.Context, exampleID string, options ExampleUpdateOptions) (*Example, error) { if !validStringID(&exampleID) { @@ -270,6 +253,65 @@ func (s *example) Delete(ctx context.Context, exampleID string) error { return s.client.do(ctx, req, nil) } + +func (o *ExampleUpdateOptions) valid() error { + if o.Name != nil && !validString(o.Name) { + return ErrRequiredName + } + + if o.URL != nil && !validString(o.URL) { + return ErrInvalidRunTaskURL + } + + return nil +} + +func (o *ExampleCreateOptions) valid() error { + if !validString(&o.Name) { + return ErrRequiredName + } + + if !validString(&o.URL) { + return ErrInvalidRunTaskURL + } + + return nil +} + +func (o *ExampleListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + if err := validateExampleIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func (o *ExampleReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + if err := validateExampleIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateExampleIncludeParams(params []ExampleIncludeOpt) error { + for _, p := range params { + switch p { + case ExampleOrganization, ExampleRun: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} ``` ## Generating Mocks diff --git a/errors.go b/errors.go index 791836874..2bb63c128 100644 --- a/errors.go +++ b/errors.go @@ -120,7 +120,7 @@ var ( ErrInvalidRunTriggerType = errors.New(`invalid value or no value for RunTriggerType. It must be either "inbound" or "outbound"`) - ErrInvalidRunTriggerInclude = errors.New(`invalid value for "include" field`) + ErrInvalidIncludeValue = errors.New(`invalid value for "include" field`) ErrInvalidSHHKeyID = errors.New("invalid value for SSH key ID") diff --git a/mocks/agent_pool_mocks.go b/mocks/agent_pool_mocks.go index 5d9ddda0f..8295693b6 100644 --- a/mocks/agent_pool_mocks.go +++ b/mocks/agent_pool_mocks.go @@ -94,6 +94,21 @@ func (mr *MockAgentPoolsMockRecorder) Read(ctx, agentPoolID interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAgentPools)(nil).Read), ctx, agentPoolID) } +// ReadWithOptions mocks base method. +func (m *MockAgentPools) ReadWithOptions(ctx context.Context, agentPoolID string, options *tfe.AgentPoolReadOptions) (*tfe.AgentPool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadWithOptions", ctx, agentPoolID, options) + ret0, _ := ret[0].(*tfe.AgentPool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadWithOptions indicates an expected call of ReadWithOptions. +func (mr *MockAgentPoolsMockRecorder) ReadWithOptions(ctx, agentPoolID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockAgentPools)(nil).ReadWithOptions), ctx, agentPoolID, options) +} + // Update mocks base method. func (m *MockAgentPools) Update(ctx context.Context, agentPool string, options tfe.AgentPoolUpdateOptions) (*tfe.AgentPool, error) { m.ctrl.T.Helper() diff --git a/oauth_client.go b/oauth_client.go index 3f81631da..4be2321af 100644 --- a/oauth_client.go +++ b/oauth_client.go @@ -162,6 +162,9 @@ func (s *oAuthClients) List(ctx context.Context, organization string, options *O if !validStringID(&organization) { return nil, ErrInvalidOrg } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization)) req, err := s.client.newRequest("GET", u, options) @@ -277,3 +280,28 @@ func (o OAuthClientCreateOptions) valid() error { } return nil } + +func (o *OAuthClientListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateOauthClientIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateOauthClientIncludeParams(params []OAuthClientIncludeOpt) error { + for _, p := range params { + switch p { + case OauthClientOauthTokens: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/organization_membership.go b/organization_membership.go index c569ea83f..0c4d3340e 100644 --- a/organization_membership.go +++ b/organization_membership.go @@ -62,13 +62,13 @@ type OrganizationMembership struct { Teams []*Team `jsonapi:"relation,teams"` } -// OrganizationMembershipIncludeOpt represents the available options for include query params. +// OrgMembershipIncludeOpt represents the available options for include query params. // https://www.terraform.io/cloud-docs/api-docs/organization-memberships#available-related-resources -type OrganizationMembershipIncludeOpt string +type OrgMembershipIncludeOpt string const ( - OrganizationMembershipUser OrganizationMembershipIncludeOpt = "user" - OrganizationMembershipTeam OrganizationMembershipIncludeOpt = "teams" + OrgMembershipUser OrgMembershipIncludeOpt = "user" + OrgMembershipTeam OrgMembershipIncludeOpt = "teams" ) // OrganizationMembershipListOptions represents the options for listing organization memberships. @@ -76,7 +76,7 @@ type OrganizationMembershipListOptions struct { ListOptions // Optional: A list of relations to include. See available resources // https://www.terraform.io/cloud-docs/api-docs/organization-memberships#available-related-resources - Include []OrganizationMembershipIncludeOpt `url:"include,omitempty"` + Include []OrgMembershipIncludeOpt `url:"include,omitempty"` } // OrganizationMembershipCreateOptions represents the options for creating an organization membership. @@ -95,7 +95,7 @@ type OrganizationMembershipCreateOptions struct { type OrganizationMembershipReadOptions struct { // Optional: A list of relations to include. See available resources // https://www.terraform.io/cloud-docs/api-docs/organization-memberships#available-related-resources - Include []OrganizationMembershipIncludeOpt `url:"include,omitempty"` + Include []OrgMembershipIncludeOpt `url:"include,omitempty"` } // List all the organization memberships of the given organization. @@ -103,6 +103,9 @@ func (s *organizationMemberships) List(ctx context.Context, organization string, if !validStringID(&organization) { return nil, ErrInvalidOrg } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("organizations/%s/organization-memberships", url.QueryEscape(organization)) req, err := s.client.newRequest("GET", u, options) @@ -153,6 +156,9 @@ func (s *organizationMemberships) ReadWithOptions(ctx context.Context, organizat if !validStringID(&organizationMembershipID) { return nil, ErrInvalidMembership } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("organization-memberships/%s", url.QueryEscape(organizationMembershipID)) req, err := s.client.newRequest("GET", u, &options) @@ -190,3 +196,36 @@ func (o OrganizationMembershipCreateOptions) valid() error { } return nil } + +func (o *OrganizationMembershipListOptions) valid() error { + if o == nil { + return nil + } + + if err := validateOrgMembershipIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func (o OrganizationMembershipReadOptions) valid() error { + if err := validateOrgMembershipIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateOrgMembershipIncludeParams(params []OrgMembershipIncludeOpt) error { + for _, p := range params { + switch p { + case OrgMembershipUser, OrgMembershipTeam: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/organization_membership_integration_test.go b/organization_membership_integration_test.go index 358e15768..19b799380 100644 --- a/organization_membership_integration_test.go +++ b/organization_membership_integration_test.go @@ -64,7 +64,7 @@ func TestOrganizationMembershipsList(t *testing.T) { defer memTest2Cleanup() ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{ - Include: []OrganizationMembershipIncludeOpt{OrganizationMembershipUser}, + Include: []OrgMembershipIncludeOpt{OrgMembershipUser}, }) require.NoError(t, err) @@ -96,7 +96,7 @@ func TestOrganizationMembershipsCreate(t *testing.T) { // Get a refreshed view from the API. refreshed, err := client.OrganizationMemberships.ReadWithOptions(ctx, mem.ID, OrganizationMembershipReadOptions{ - Include: []OrganizationMembershipIncludeOpt{OrganizationMembershipUser}, + Include: []OrgMembershipIncludeOpt{OrgMembershipUser}, }) require.NoError(t, err) assert.Equal(t, refreshed, mem) @@ -169,7 +169,7 @@ func TestOrganizationMembershipsReadWithOptions(t *testing.T) { defer memTestCleanup() options := OrganizationMembershipReadOptions{ - Include: []OrganizationMembershipIncludeOpt{OrganizationMembershipUser}, + Include: []OrgMembershipIncludeOpt{OrgMembershipUser}, } t.Run("when the membership exists", func(t *testing.T) { @@ -179,6 +179,18 @@ func TestOrganizationMembershipsReadWithOptions(t *testing.T) { assert.Equal(t, memTest, mem) }) + t.Run("without options", func(t *testing.T) { + _, err := client.OrganizationMemberships.ReadWithOptions(ctx, memTest.ID, OrganizationMembershipReadOptions{}) + require.NoError(t, err) + }) + + t.Run("without invalid include option", func(t *testing.T) { + _, err := client.OrganizationMemberships.ReadWithOptions(ctx, memTest.ID, OrganizationMembershipReadOptions{ + Include: []OrgMembershipIncludeOpt{"users"}, + }) + assert.Equal(t, err, ErrInvalidIncludeValue) + }) + t.Run("when the membership does not exist", func(t *testing.T) { mem, err := client.OrganizationMemberships.ReadWithOptions(ctx, "nonexisting", options) assert.Nil(t, mem) diff --git a/policy_check.go b/policy_check.go index 21c247efd..00198aecd 100644 --- a/policy_check.go +++ b/policy_check.go @@ -130,6 +130,9 @@ func (s *policyChecks) List(ctx context.Context, runID string, options *PolicyCh if !validStringID(&runID) { return nil, ErrInvalidRunID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("runs/%s/policy-checks", url.QueryEscape(runID)) req, err := s.client.newRequest("GET", u, options) @@ -228,3 +231,28 @@ func (s *policyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reade return logs, nil } } + +func (o *PolicyCheckListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validatePolicyCheckIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validatePolicyCheckIncludeParams(params []PolicyCheckIncludeOpt) error { + for _, p := range params { + switch p { + case PolicyCheckRunWorkspace, PolicyCheckRun: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/policy_set.go b/policy_set.go index 5ff610174..26157fbc8 100644 --- a/policy_set.go +++ b/policy_set.go @@ -264,6 +264,9 @@ func (s *policySets) ReadWithOptions(ctx context.Context, policySetID string, op if !validStringID(&policySetID) { return nil, ErrInvalidPolicySetID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID)) req, err := s.client.newRequest("GET", u, options) @@ -447,3 +450,28 @@ func (o PolicySetAddWorkspacesOptions) valid() error { } return nil } + +func (o *PolicySetReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validatePolicySetIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validatePolicySetIncludeParams(params []PolicySetIncludeOpt) error { + for _, p := range params { + switch p { + case PolicySetPolicies, PolicySetWorkspaces, PolicySetCurrentVersion, PolicySetNewestVersion: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/run.go b/run.go index 54d314801..9a069efd0 100644 --- a/run.go +++ b/run.go @@ -283,6 +283,9 @@ func (s *runs) List(ctx context.Context, workspaceID string, options *RunListOpt if !validStringID(&workspaceID) { return nil, ErrInvalidWorkspaceID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("workspaces/%s/runs", url.QueryEscape(workspaceID)) req, err := s.client.newRequest("GET", u, options) @@ -329,6 +332,9 @@ func (s *runs) ReadWithOptions(ctx context.Context, runID string, options *RunRe if !validStringID(&runID) { return nil, ErrInvalidRunID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("runs/%s", url.QueryEscape(runID)) req, err := s.client.newRequest("GET", u, options) @@ -411,3 +417,38 @@ func (o RunCreateOptions) valid() error { } return nil } + +func (o *RunReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateRunIncludeParam(o.Include); err != nil { + return err + } + return nil +} + +func (o *RunListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateRunIncludeParam(o.Include); err != nil { + return err + } + return nil +} + +func validateRunIncludeParam(params []RunIncludeOpt) error { + for _, p := range params { + switch p { + case RunPlan, RunApply, RunCreatedBy, RunCostEstimate, RunConfigVer, RunConfigVerIngress, RunWorkspace, RunTaskStages: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/run_task.go b/run_task.go index b7c5806c5..c38c09a98 100644 --- a/run_task.go +++ b/run_task.go @@ -155,6 +155,9 @@ func (s *runTasks) List(ctx context.Context, organization string, options *RunTa if !validStringID(&organization) { return nil, ErrInvalidOrg } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) req, err := s.client.newRequest("GET", u, options) @@ -181,6 +184,9 @@ func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, option if !validStringID(&runTaskID) { return nil, ErrInvalidRunTaskID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) req, err := s.client.newRequest("GET", u, options) @@ -276,3 +282,40 @@ func (o *RunTaskUpdateOptions) valid() error { return nil } + +func (o *RunTaskListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateRunTaskIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func (o *RunTaskReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateRunTaskIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateRunTaskIncludeParams(params []RunTaskIncludeOpt) error { + for _, p := range params { + switch p { + case RunTaskWorkspaceTasks, RunTaskWorkspace: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/run_trigger.go b/run_trigger.go index 51323194f..f0c77730c 100644 --- a/run_trigger.go +++ b/run_trigger.go @@ -175,36 +175,55 @@ func (s *runTriggers) Delete(ctx context.Context, runTriggerID string) error { return s.client.do(ctx, req, nil) } +func (o RunTriggerCreateOptions) valid() error { + if o.Sourceable == nil { + return ErrRequiredSourceable + } + return nil +} + func (o *RunTriggerListOptions) valid() error { if o == nil { return ErrRequiredRunTriggerListOps } - switch o.RunTriggerType { + if err := validateRunTriggerFilterParam(o.RunTriggerType, o.Include); err != nil { + return err + } + + if err := validateRunTriggerIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateRunTriggerFilterParam(filterParam RunTriggerFilterOp, includeParams []RunTriggerIncludeOpt) error { + switch filterParam { case RunTriggerOutbound, RunTriggerInbound: // Do nothing default: - return ErrInvalidRunTriggerType + return ErrInvalidRunTriggerType // return an error even if string is empty because this a required field } - for _, i := range o.Include { - switch i { - case RunTriggerWorkspace, RunTriggerSourceable: - if o.RunTriggerType != RunTriggerInbound { - return ErrUnsupportedRunTriggerType - } - // Do nothing - default: - return ErrUnsupportedOperations + if len(includeParams) > 0 { + if filterParam != RunTriggerInbound { + return ErrUnsupportedRunTriggerType // if user passes RunTriggerOutbound the platform will not return any "include" data } } return nil } -func (o RunTriggerCreateOptions) valid() error { - if o.Sourceable == nil { - return ErrRequiredSourceable +func validateRunTriggerIncludeParams(params []RunTriggerIncludeOpt) error { + for _, p := range params { + switch p { + case RunTriggerWorkspace, RunTriggerSourceable: + // Do nothing + default: + return ErrInvalidIncludeValue + } } + return nil } diff --git a/state_version.go b/state_version.go index b5107e6bf..3ca13ba71 100644 --- a/state_version.go +++ b/state_version.go @@ -189,6 +189,9 @@ func (s *stateVersions) ReadWithOptions(ctx context.Context, svID string, option if !validStringID(&svID) { return nil, ErrInvalidStateVerID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("state-versions/%s", url.QueryEscape(svID)) req, err := s.client.newRequest("GET", u, options) @@ -215,6 +218,9 @@ func (s *stateVersions) ReadCurrentWithOptions(ctx context.Context, workspaceID if !validStringID(&workspaceID) { return nil, ErrInvalidWorkspaceID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("workspaces/%s/current-state-version", url.QueryEscape(workspaceID)) req, err := s.client.newRequest("GET", u, options) @@ -300,3 +306,37 @@ func (o StateVersionCreateOptions) valid() error { } return nil } + +func (o *StateVersionReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateStateVerIncludeParams(o.Include); err != nil { + return err + } + return nil +} +func (o *StateVersionCurrentOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateStateVerIncludeParams(o.Include); err != nil { + return err + } + return nil +} + +func validateStateVerIncludeParams(params []StateVersionIncludeOpt) error { + for _, p := range params { + switch p { + case SVcreatedby, SVrun, SVrunCreatedBy, SVrunConfigurationVersion, SVoutputs: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/task_stages.go b/task_stages.go index 4adaf5cd4..d6699a452 100644 --- a/task_stages.go +++ b/task_stages.go @@ -62,9 +62,7 @@ type TaskStageStatusTimestamps struct { // TaskStageIncludeOpt represents the available options for include query params. type TaskStageIncludeOpt string -const ( - TaskStageTaskResults TaskStageIncludeOpt = "task_results" -) +const TaskStageTaskResults TaskStageIncludeOpt = "task_results" // TaskStageReadOptions represents the set of options when reading a task stage type TaskStageReadOptions struct { @@ -82,6 +80,9 @@ func (s *taskStages) Read(ctx context.Context, taskStageID string, options *Task if !validStringID(&taskStageID) { return nil, ErrInvalidTaskStageID } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("task-stages/%s", taskStageID) req, err := s.client.newRequest("GET", u, options) @@ -119,3 +120,28 @@ func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageL return tlist, nil } + +func (o *TaskStageReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateTaskStageIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateTaskStageIncludeParams(params []TaskStageIncludeOpt) error { + for _, p := range params { + switch p { + case TaskStageTaskResults: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/team.go b/team.go index 8d360d1b2..4ba72801f 100644 --- a/team.go +++ b/team.go @@ -139,7 +139,9 @@ func (s *teams) List(ctx context.Context, organization string, options *TeamList if !validStringID(&organization) { return nil, ErrInvalidOrg } - + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization)) req, err := s.client.newRequest("GET", u, options) if err != nil { @@ -242,3 +244,28 @@ func (o TeamCreateOptions) valid() error { } return nil } + +func (o *TeamListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateTeamIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateTeamIncludeParams(params []TeamIncludeOpt) error { + for _, p := range params { + switch p { + case TeamUsers, TeamOrganizationMemberships: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/workspace.go b/workspace.go index 340ea5637..0f58e4ab5 100644 --- a/workspace.go +++ b/workspace.go @@ -517,6 +517,9 @@ func (s *workspaces) List(ctx context.Context, organization string, options *Wor if !validStringID(&organization) { return nil, ErrInvalidOrg } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization)) req, err := s.client.newRequest("GET", u, options) @@ -570,6 +573,9 @@ func (s *workspaces) ReadWithOptions(ctx context.Context, organization, workspac if !validStringID(&workspace) { return nil, ErrInvalidWorkspaceValue } + if err := options.valid(); err != nil { + return nil, err + } u := fmt.Sprintf( "organizations/%s/workspaces/%s", @@ -1129,3 +1135,40 @@ func (o WorkspaceRemoveTagsOptions) valid() error { return nil } + +func (o *WorkspaceListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateWorkspaceIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func (o *WorkspaceReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateWorkspaceIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateWorkspaceIncludeParams(params []WSIncludeOpt) error { + for _, p := range params { + switch p { + case WSOrganization, WSCurrentConfigVer, WSCurrentConfigVerIngress, WSCurrentRun, WSCurrentRunPlan, WSCurrentRunConfigVer, WSCurrentrunConfigVerIngress, WSLockedBy, WSReadme, WSOutputs, WSCurrentStateVer: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +}