diff --git a/errors.go b/errors.go index f4ed4d766..6698b2a22 100644 --- a/errors.go +++ b/errors.go @@ -23,6 +23,8 @@ var ( ErrUnsupportedPrivateKey = errors.New("private Key can only be present with Azure DevOps Server service provider") ErrUnsupportedRunTriggerType = errors.New(`"RunTriggerType" must be "inbound" when requesting "include" query params`) + + ErrUnsupportedBothTriggerPatternsAndPrefixes = errors.New(`"TriggerPatterns" and "TriggerPrefixes"" cannot be used in conjunction`) ) // Library errors that usually indicate a bug in the implementation of go-tfe diff --git a/helper_test.go b/helper_test.go index 54749f70c..b772d39a2 100644 --- a/helper_test.go +++ b/helper_test.go @@ -390,11 +390,15 @@ func createOAuthToken(t *testing.T, client *Client, org *Organization) (*OAuthTo } func createOrganization(t *testing.T, client *Client) (*Organization, func()) { - ctx := context.Background() - org, err := client.Organizations.Create(ctx, OrganizationCreateOptions{ + return createOrganizationWithOptions(t, client, OrganizationCreateOptions{ Name: String("tst-" + randomString(t)), Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))), }) +} + +func createOrganizationWithOptions(t *testing.T, client *Client, options OrganizationCreateOptions) (*Organization, func()) { + ctx := context.Background() + org, err := client.Organizations.Create(ctx, options) if err != nil { t.Fatal(err) } @@ -957,6 +961,12 @@ func createVariable(t *testing.T, client *Client, w *Workspace) (*Variable, func } func createWorkspace(t *testing.T, client *Client, org *Organization) (*Workspace, func()) { + return createWorkspaceWithOptions(t, client, org, WorkspaceCreateOptions{ + Name: String(randomString(t)), + }) +} + +func createWorkspaceWithOptions(t *testing.T, client *Client, org *Organization, options WorkspaceCreateOptions) (*Workspace, func()) { var orgCleanup func() if org == nil { @@ -964,9 +974,7 @@ func createWorkspace(t *testing.T, client *Client, org *Organization) (*Workspac } ctx := context.Background() - w, err := client.Workspaces.Create(ctx, org.Name, WorkspaceCreateOptions{ - Name: String(randomString(t)), - }) + w, err := client.Workspaces.Create(ctx, org.Name, options) if err != nil { t.Fatal(err) } diff --git a/workspace.go b/workspace.go index e4200eb85..77983afed 100644 --- a/workspace.go +++ b/workspace.go @@ -131,6 +131,7 @@ type Workspace struct { StructuredRunOutputEnabled bool `jsonapi:"attr,structured-run-output-enabled"` TerraformVersion string `jsonapi:"attr,terraform-version"` TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes"` + TriggerPatterns []string `jsonapi:"attr,trigger-patterns"` VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"` WorkingDirectory string `jsonapi:"attr,working-directory"` UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` @@ -326,6 +327,10 @@ type WorkspaceCreateOptions struct { // tracked for changes. See FileTriggersEnabled above for more details. TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"` + // Optional: List of patterns used to match against changed files in order + // to decide whether to trigger a run or not. + TriggerPatterns []string `jsonapi:"attr,trigger-patterns,omitempty"` + // Settings for the workspace's VCS repository. If omitted, the workspace is // created without a VCS repo. If included, you must specify at least the // oauth-token-id and identifier keys below. @@ -420,6 +425,10 @@ type WorkspaceUpdateOptions struct { // tracked for changes. See FileTriggersEnabled above for more details. TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"` + // Optional: List of patterns used to match against changed files in order + // to decide whether to trigger a run or not. + TriggerPatterns []string `jsonapi:"attr,trigger-patterns,omitempty"` + // Optional: To delete a workspace's existing VCS repo, specify null instead of an // object. To modify a workspace's existing VCS repo, include whichever of // the keys below you wish to modify. To add a new VCS repo to a workspace @@ -1053,6 +1062,9 @@ func (o WorkspaceCreateOptions) valid() error { if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") { return ErrRequiredAgentPoolID } + if o.TriggerPrefixes != nil && o.TriggerPatterns != nil { + return ErrUnsupportedBothTriggerPatternsAndPrefixes + } return nil } @@ -1067,6 +1079,9 @@ func (o WorkspaceUpdateOptions) valid() error { if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") { return ErrRequiredAgentPoolID } + if o.TriggerPrefixes != nil && o.TriggerPatterns != nil { + return ErrUnsupportedBothTriggerPatternsAndPrefixes + } return nil } diff --git a/workspace_integration_test.go b/workspace_integration_test.go index fdf03a512..b63ff3101 100644 --- a/workspace_integration_test.go +++ b/workspace_integration_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io/ioutil" "sort" "strings" @@ -277,6 +278,49 @@ func TestWorkspacesCreate(t *testing.T) { assert.Nil(t, w) assert.Error(t, err) }) + + t.Run("when options include trigger-patterns (behind a feature flag)", func(t *testing.T) { + // Remove the below organization creation and use the one from the outer scope once the feature flag is removed + orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{ + Name: String("tst-" + randomString(t)[0:20] + "-ff-on"), + Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))), + }) + defer orgTestCleanup() + + options := WorkspaceCreateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(true), + TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"}, + } + w, err := client.Workspaces.Create(ctx, orgTest.Name, options) + + require.NoError(t, err) + assert.Equal(t, options.TriggerPatterns, w.TriggerPatterns) + + // Get a refreshed view from the API. + refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name) + require.NoError(t, err) + + for _, item := range []*Workspace{ + w, + refreshed, + } { + assert.Equal(t, options.TriggerPatterns, item.TriggerPatterns) + } + }) + + t.Run("when options include both trigger-patterns and trigger-paths error is returned", func(t *testing.T) { + options := WorkspaceCreateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(true), + TriggerPrefixes: []string{"/module-1", "/module-2"}, + TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"}, + } + w, err := client.Workspaces.Create(ctx, orgTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTriggerPatternsAndPrefixes.Error()) + }) } func TestWorkspacesRead(t *testing.T) { @@ -579,6 +623,54 @@ func TestWorkspacesUpdate(t *testing.T) { assert.Nil(t, w) assert.EqualError(t, err, ErrInvalidOrg.Error()) }) + + t.Run("when options include trigger-patterns (behind a feature flag)", func(t *testing.T) { + // Remove the below organization and workspace creation and use the one from the outer scope once the feature flag is removed + orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{ + Name: String("tst-" + randomString(t)[0:20] + "-ff-on"), + Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))), + }) + defer orgTestCleanup() + + wTest, _ := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ + Name: String(randomString(t)), + TriggerPrefixes: []string{"/prefix-1/", "/prefix-2/"}, + }) + assert.Equal(t, wTest.TriggerPrefixes, []string{"/prefix-1/", "/prefix-2/"}) // Sanity test + + options := WorkspaceUpdateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(true), + TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"}, + } + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options) + require.NoError(t, err) + + // Get a refreshed view from the API. + refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name) + require.NoError(t, err) + + for _, item := range []*Workspace{ + w, + refreshed, + } { + assert.Empty(t, options.TriggerPrefixes) + assert.Equal(t, options.TriggerPatterns, item.TriggerPatterns) + } + }) + + t.Run("when options include both trigger-patterns and trigger-paths error is returned", func(t *testing.T) { + options := WorkspaceUpdateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(true), + TriggerPrefixes: []string{"/module-1", "/module-2"}, + TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"}, + } + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTriggerPatternsAndPrefixes.Error()) + }) } func TestWorkspacesUpdateByID(t *testing.T) { @@ -1359,6 +1451,7 @@ func TestWorkspace_Unmarshal(t *testing.T) { "is-destroyable": true, }, "trigger-prefixes": []string{"prefix-"}, + "trigger-patterns": []string{"pattern1/**/*", "pattern2/**/submodule/*"}, }, }, } @@ -1390,6 +1483,7 @@ func TestWorkspace_Unmarshal(t *testing.T) { assert.Equal(t, ws.VCSRepo.ServiceProvider, "github") assert.Equal(t, ws.Actions.IsDestroyable, true) assert.Equal(t, ws.TriggerPrefixes, []string{"prefix-"}) + assert.Equal(t, ws.TriggerPatterns, []string{"pattern1/**/*", "pattern2/**/submodule/*"}) } func TestWorkspaceCreateOptions_Marshal(t *testing.T) { @@ -1397,6 +1491,7 @@ func TestWorkspaceCreateOptions_Marshal(t *testing.T) { AllowDestroyPlan: Bool(true), Name: String("my-workspace"), TriggerPrefixes: []string{"prefix-"}, + TriggerPatterns: []string{"pattern1/**/*", "pattern2/**/*"}, VCSRepo: &VCSRepoOptions{ Identifier: String("id"), OAuthTokenID: String("token"), @@ -1410,7 +1505,7 @@ func TestWorkspaceCreateOptions_Marshal(t *testing.T) { bodyBytes, err := req.BodyBytes() require.NoError(t, err) - expectedBody := `{"data":{"type":"workspaces","attributes":{"allow-destroy-plan":true,"name":"my-workspace","trigger-prefixes":["prefix-"],"vcs-repo":{"identifier":"id","oauth-token-id":"token"}}}} + expectedBody := `{"data":{"type":"workspaces","attributes":{"allow-destroy-plan":true,"name":"my-workspace","trigger-patterns":["pattern1/**/*","pattern2/**/*"],"trigger-prefixes":["prefix-"],"vcs-repo":{"identifier":"id","oauth-token-id":"token"}}}} ` assert.Equal(t, expectedBody, string(bodyBytes)) }