From 33de91d9ffef1490e72dde4a4334f0101f7ceeac Mon Sep 17 00:00:00 2001 From: Matej Risek Date: Fri, 8 Jul 2022 18:12:46 +0200 Subject: [PATCH] Feature/add trigger patterns (#502) * Add trigger-patterns to data source * Use client to create workspace in order to test datasource * Update go-tfe version * Update documentation * Add Changelog entry --- CHANGELOG.md | 1 + tfe/data_source_workspace.go | 7 + tfe/data_source_workspace_test.go | 80 +++++++++- tfe/resource_tfe_workspace.go | 56 +++++-- tfe/resource_tfe_workspace_test.go | 194 ++++++++++++++++++++++++- website/docs/d/workspace.html.markdown | 4 +- website/docs/r/workspace.html.markdown | 3 +- 7 files changed, 328 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e0a6f00..c3181de0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ FEATURES: * **New Resource**: `tfe_workspace_variable_set` ([#537](https://github.com/hashicorp/terraform-provider-tfe/pull/537)) adds the ability to assign a variable set to a workspace in a single, flexible resource. +* r/tfe_workspace, d/tfe_workspace: `trigger-patterns` ([#502](https://github.com/hashicorp/terraform-provider-tfe/pull/502)) attribute is introduced to support specifying a set of [glob patterns](https://www.terraform.io/cloud-docs/workspaces/settings/vcs#glob-patterns-for-automatic-run-triggering) for automatic VCS run triggering. DEPRECATION NOTICE: The `workspace_ids` argument on `tfe_variable_set` has been labelled as deprecated and should not be used in conjunction with `tfe_workspace_variable_set`. diff --git a/tfe/data_source_workspace.go b/tfe/data_source_workspace.go index bf4e89bf7..80f5fc7a9 100644 --- a/tfe/data_source_workspace.go +++ b/tfe/data_source_workspace.go @@ -117,6 +117,12 @@ func dataSourceTFEWorkspace() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, + "trigger_patterns": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "working_directory": { Type: schema.TypeString, Computed: true, @@ -183,6 +189,7 @@ func dataSourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error d.Set("structured_run_output_enabled", workspace.StructuredRunOutputEnabled) d.Set("terraform_version", workspace.TerraformVersion) d.Set("trigger_prefixes", workspace.TriggerPrefixes) + d.Set("trigger_patterns", workspace.TriggerPatterns) d.Set("working_directory", workspace.WorkingDirectory) // Set remote_state_consumer_ids if global_remote_state is false diff --git a/tfe/data_source_workspace_test.go b/tfe/data_source_workspace_test.go index 6ee2422fa..f816fdeff 100644 --- a/tfe/data_source_workspace_test.go +++ b/tfe/data_source_workspace_test.go @@ -2,6 +2,7 @@ package tfe import ( "fmt" + "github.com/hashicorp/go-tfe" "math/rand" "strconv" "testing" @@ -112,6 +113,51 @@ func TestAccTFEWorkspaceDataSource_basic(t *testing.T) { }) } +func TestAccTFEWorkspaceDataSourceWithTriggerPatterns(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatalf("error getting client %v", err) + } + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + organization, orgCleanup := givenOrganization(t, tfeClient, fmt.Sprintf("tst-terraform-%d-ff-on", rInt)) + defer orgCleanup() + + workspaceName := fmt.Sprintf("workspace-%d", rInt) + _, err = tfeClient.Workspaces.Create(ctx, organization.Name, tfe.WorkspaceCreateOptions{ + Name: &workspaceName, + FileTriggersEnabled: tfe.Bool(true), + TriggerPatterns: []string{"/modules/**/*", "/**/networking/*"}, + }) + if err != nil { + t.Fatal(err) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspaceDataSourceConfigWithTriggerPatterns(workspaceName, organization.Name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.tfe_workspace.foobar", "id"), + resource.TestCheckResourceAttr( + "data.tfe_workspace.foobar", "name", workspaceName), + resource.TestCheckResourceAttr( + "data.tfe_workspace.foobar", "organization", organization.Name), + resource.TestCheckResourceAttr( + "data.tfe_workspace.foobar", "file_triggers_enabled", "true"), + resource.TestCheckResourceAttr( + "data.tfe_workspace.foobar", "trigger_patterns.#", "2"), + resource.TestCheckResourceAttr( + "data.tfe_workspace.foobar", "trigger_patterns.0", "/modules/**/*"), + resource.TestCheckResourceAttr( + "data.tfe_workspace.foobar", "trigger_patterns.1", "/**/networking/*"), + ), + }, + }, + }) +} + func testAccTFEWorkspaceDataSourceConfig(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { @@ -132,16 +178,24 @@ resource "tfe_workspace" "foobar" { terraform_version = "0.11.1" trigger_prefixes = ["/modules", "/shared"] working_directory = "terraform/test" - global_remote_state = true + global_remote_state = true } data "tfe_workspace" "foobar" { name = tfe_workspace.foobar.name organization = tfe_workspace.foobar.organization - depends_on = [tfe_workspace.foobar] + depends_on = [tfe_workspace.foobar] }`, rInt, rInt) } +func testAccTFEWorkspaceDataSourceConfigWithTriggerPatterns(workspaceName string, organizationName string) string { + return fmt.Sprintf(` +data "tfe_workspace" "foobar" { + name = "%s" + organization = "%s" +}`, workspaceName, organizationName) +} + func testAccTFEWorkspaceDataSourceConfig_remoteStateConsumers(rInt1, rInt2 int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { @@ -167,3 +221,25 @@ data "tfe_workspace" "foobar" { depends_on = [tfe_workspace.foobar] }`, rInt1, rInt2, rInt1) } + +func givenOrganization(t *testing.T, tfeClient *tfe.Client, organizationName string) (*tfe.Organization, func()) { + var orgCleanup func() + + dummyEmail := "test@test.test" + org, err := tfeClient.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ + Name: tfe.String(organizationName), + Email: &dummyEmail, + }) + if err != nil { + t.Fatal(err) + } + orgCleanup = func() { + if err := tfeClient.Organizations.Delete(ctx, org.Name); err != nil { + t.Errorf("Error destroying organization! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "Organization: %s\nError: %s", org.Name, err) + } + } + + return org, orgCleanup +} diff --git a/tfe/resource_tfe_workspace.go b/tfe/resource_tfe_workspace.go index 15da56dce..fdc956168 100644 --- a/tfe/resource_tfe_workspace.go +++ b/tfe/resource_tfe_workspace.go @@ -44,6 +44,8 @@ func resourceTFEWorkspace() *schema.Resource { return err } + validateVcsTriggers(d) + return nil }, @@ -161,10 +163,19 @@ func resourceTFEWorkspace() *schema.Resource { }, "trigger_prefixes": { - Type: schema.TypeList, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"trigger_patterns"}, + }, + + "trigger_patterns": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"trigger_prefixes"}, }, "working_directory": { @@ -252,10 +263,18 @@ func resourceTFEWorkspaceCreate(d *schema.ResourceData, meta interface{}) error if tps, ok := d.GetOk("trigger_prefixes"); ok { for _, tp := range tps.([]interface{}) { - if t, ok := tp.(string); ok { - options.TriggerPrefixes = append(options.TriggerPrefixes, t) - } + options.TriggerPrefixes = append(options.TriggerPrefixes, tp.(string)) + } + } else { + options.TriggerPrefixes = []string{} + } + + if tps, ok := d.GetOk("trigger_patterns"); ok { + for _, tp := range tps.([]interface{}) { + options.TriggerPatterns = append(options.TriggerPatterns, tp.(string)) } + } else { + options.TriggerPatterns = []string{} } // Get and assert the VCS repo configuration block. @@ -345,6 +364,7 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error { d.Set("structured_run_output_enabled", workspace.StructuredRunOutputEnabled) d.Set("terraform_version", workspace.TerraformVersion) d.Set("trigger_prefixes", workspace.TriggerPrefixes) + d.Set("trigger_patterns", workspace.TriggerPatterns) d.Set("working_directory", workspace.WorkingDirectory) d.Set("organization", workspace.Organization.Name) @@ -400,8 +420,9 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error id := d.Id() if d.HasChange("name") || d.HasChange("auto_apply") || d.HasChange("queue_all_runs") || - d.HasChange("terraform_version") || d.HasChange("working_directory") || d.HasChange("vcs_repo") || - d.HasChange("file_triggers_enabled") || d.HasChange("trigger_prefixes") || + d.HasChange("terraform_version") || d.HasChange("working_directory") || + d.HasChange("vcs_repo") || d.HasChange("file_triggers_enabled") || + d.HasChange("trigger_prefixes") || d.HasChange("trigger_patterns") || d.HasChange("allow_destroy_plan") || d.HasChange("speculative_enabled") || d.HasChange("operations") || d.HasChange("execution_mode") || d.HasChange("description") || d.HasChange("agent_pool_id") || @@ -450,10 +471,17 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error } } } else { - // Reset trigger prefixes when none are present in the config. options.TriggerPrefixes = []string{} } + if tps, ok := d.GetOk("trigger_patterns"); ok { + for _, tp := range tps.([]interface{}) { + options.TriggerPatterns = append(options.TriggerPatterns, tp.(string)) + } + } else { + options.TriggerPatterns = []string{} + } + if workingDir, ok := d.GetOk("working_directory"); ok { options.WorkingDirectory = tfe.String(workingDir.(string)) } @@ -655,6 +683,14 @@ func validateRemoteState(_ context.Context, d *schema.ResourceDiff) error { return nil } +func validateVcsTriggers(d *schema.ResourceDiff) { + if d.HasChange("trigger_patterns") { + d.SetNewComputed("trigger_prefixes") + } else if d.HasChange("trigger_prefixes") { + d.SetNewComputed("trigger_patterns") + } +} + func resourceTFEWorkspaceImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { tfeClient := meta.(*tfe.Client) diff --git a/tfe/resource_tfe_workspace_test.go b/tfe/resource_tfe_workspace_test.go index 34378840f..826fb73b2 100644 --- a/tfe/resource_tfe_workspace_test.go +++ b/tfe/resource_tfe_workspace_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "math/rand" + "regexp" "strconv" "testing" "time" @@ -366,6 +367,137 @@ func TestAccTFEWorkspace_updateTriggerPrefixes(t *testing.T) { }) } +func TestAccTFEWorkspace_overwriteTriggerPatternsWithPrefixes(t *testing.T) { + workspace := &tfe.Workspace{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspace_triggerPatterns(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists( + "tfe_workspace.foobar", workspace), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.#", "2"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.#", "0"), + ), + }, + { + Config: testAccTFEWorkspace_triggerPrefixes(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists( + "tfe_workspace.foobar", workspace), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.#", "2"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.0", "/modules"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.1", "/shared"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.#", "0"), + ), + }, + { + Config: testAccTFEWorkspace_updateEmptyTriggerPrefixes(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists( + "tfe_workspace.foobar", workspace), + testAccCheckTFEWorkspaceAttributes(workspace), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.#", "0"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.#", "0"), + ), + }, + }, + }) +} + +func TestAccTFEWorkspace_updateTriggerPatterns(t *testing.T) { + workspace := &tfe.Workspace{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEWorkspaceDestroy, + Steps: []resource.TestStep{ + // Create trigger prefixes first so we can verify they are being removed if we introduce trigger patterns + { + Config: testAccTFEWorkspace_triggerPrefixes(rInt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.#", "2"), + ), + }, + // Overwrite prefixes with patterns + { + Config: testAccTFEWorkspace_triggerPatterns(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists( + "tfe_workspace.foobar", workspace), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.#", "2"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.0", "/modules/**/*"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.1", "/**/networking/*"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.#", "0"), + ), + }, + // Second update + { + Config: testAccTFEWorkspace_updateTriggerPatterns(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists( + "tfe_workspace.foobar", workspace), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.#", "3"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.0", "/**/networking/*"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.1", "/another_module/*/test/*"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_patterns.2", "/**/resources/**/*"), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "trigger_prefixes.#", "0"), + ), + }, + { + Config: testAccTFEWorkspace_updateEmptyTriggerPatterns(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists("tfe_workspace.foobar", workspace), + testAccCheckTFEWorkspaceAttributes(workspace), + resource.TestCheckResourceAttr("tfe_workspace.foobar", "trigger_patterns.#", "0"), + resource.TestCheckResourceAttr("tfe_workspace.foobar", "trigger_prefixes.#", "0"), + ), + }, + }, + }) +} + +func TestAccTFEWorkspace_patternsAndPrefixesConflicting(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspace_prefixesAndPatternsConflicting(rInt), + ExpectError: regexp.MustCompile(`Conflicting configuration`), + }, + }, + }) +} + func TestAccTFEWorkspace_changeTags(t *testing.T) { workspace := &tfe.Workspace{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -1612,7 +1744,7 @@ resource "tfe_workspace" "foobar" { func testAccTFEWorkspace_triggerPrefixes(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { - name = "tst-terraform-%d" + name = "tst-terraform-%d-ff-on" email = "admin@company.com" } @@ -1626,14 +1758,70 @@ resource "tfe_workspace" "foobar" { func testAccTFEWorkspace_updateEmptyTriggerPrefixes(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { - name = "tst-terraform-%d" + name = "tst-terraform-%d-ff-on" + email = "admin@company.com" +} +resource "tfe_workspace" "foobar" { + name = "workspace-test" + organization = tfe_organization.foobar.id + auto_apply = true + trigger_prefixes = [] +}`, rInt) +} + +func testAccTFEWorkspace_triggerPatterns(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d-ff-on" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace" + organization = tfe_organization.foobar.id + trigger_patterns = ["/modules/**/*", "/**/networking/*"] +}`, rInt) +} + +func testAccTFEWorkspace_updateTriggerPatterns(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d-ff-on" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace" + organization = tfe_organization.foobar.id + trigger_patterns = ["/**/networking/*", "/another_module/*/test/*", "/**/resources/**/*"] +}`, rInt) +} + +func testAccTFEWorkspace_updateEmptyTriggerPatterns(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d-ff-on" email = "admin@company.com" } resource "tfe_workspace" "foobar" { name = "workspace-test" organization = tfe_organization.foobar.id auto_apply = true - trigger_prefixes = [] + trigger_patterns = [] +}`, rInt) +} + +func testAccTFEWorkspace_prefixesAndPatternsConflicting(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d-ff-on" + email = "admin@company.com" +} +resource "tfe_workspace" "foobar" { + name = "workspace-test" + organization = tfe_organization.foobar.id + trigger_prefixes = [] + trigger_patterns = [] }`, rInt) } diff --git a/website/docs/d/workspace.html.markdown b/website/docs/d/workspace.html.markdown index d907d40e1..57dbf98ef 100644 --- a/website/docs/d/workspace.html.markdown +++ b/website/docs/d/workspace.html.markdown @@ -51,7 +51,9 @@ In addition to all arguments above, the following attributes are exported: * `structured_run_output_enabled` - Indicates whether runs in this workspace use the enhanced apply UI. * `tag_names` - The names of tags added to this workspace. * `terraform_version` - The version (or version constraint) of Terraform used for this workspace. -* `trigger_prefixes` - List of repository-root-relative paths which describe all locations to be tracked for changes. +* `trigger_prefixes` - List of trigger prefixes that describe the paths Terraform Cloud monitors for changes, in addition to the working directory. Trigger prefixes are always appended to the root directory of the repository. + Terraform Cloud or Terraform Enterprise will start a run when files are changed in any directory path matching the provided set of prefixes. +* `trigger_patterns` - List of [glob patterns](https://www.terraform.io/cloud-docs/workspaces/settings/vcs#glob-patterns-for-automatic-run-triggering) that describe the files Terraform Cloud monitors for changes. Trigger patterns are always appended to the root directory of the repository. Only available for Terraform Cloud. * `vcs_repo` - Settings for the workspace's VCS repository. * `working_directory` - A relative path that Terraform will execute within. diff --git a/website/docs/r/workspace.html.markdown b/website/docs/r/workspace.html.markdown index e2895e8cb..f0d59c4fb 100644 --- a/website/docs/r/workspace.html.markdown +++ b/website/docs/r/workspace.html.markdown @@ -97,6 +97,7 @@ The following arguments are supported: Defaults to `true`. Setting this to `false` ensures that all runs in this workspace will display their output as text logs. * `ssh_key_id` - (Optional) The ID of an SSH key to assign to the workspace. +* `tag_names` - (Optional) A list of tag names for this workspace. Note that tags must only contain lowercase letters, numbers, colons, or hyphens. * `terraform_version` - (Optional) The version of Terraform to use for this workspace. This can be either an exact version or a [version constraint](https://www.terraform.io/docs/language/expressions/version-constraints.html) @@ -105,7 +106,7 @@ The following arguments are supported: available version. * `trigger_prefixes` - (Optional) List of repository-root-relative paths which describe all locations to be tracked for changes. -* `tag_names` - (Optional) A list of tag names for this workspace. Note that tags must only contain lowercase letters, numbers, colons, or hyphens. +* `trigger_patterns` - (Optional) List of [glob patterns](https://www.terraform.io/cloud-docs/workspaces/settings/vcs#glob-patterns-for-automatic-run-triggering) that describe the files Terraform Cloud monitors for changes. Trigger patterns are always appended to the root directory of the repository. Mutually exclusive with `trigger-prefixes`. Only available for Terraform Cloud. * `working_directory` - (Optional) A relative path that Terraform will execute within. Defaults to the root of your repository. * `vcs_repo` - (Optional) Settings for the workspace's VCS repository, enabling the [UI/VCS-driven run workflow](https://www.terraform.io/docs/cloud/run/ui.html).