diff --git a/tfe/provider.go b/tfe/provider.go index 56aae2795..32343106a 100644 --- a/tfe/provider.go +++ b/tfe/provider.go @@ -93,28 +93,29 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ - "tfe_agent_pool": resourceTFEAgentPool(), - "tfe_agent_token": resourceTFEAgentToken(), - "tfe_notification_configuration": resourceTFENotificationConfiguration(), - "tfe_oauth_client": resourceTFEOAuthClient(), - "tfe_organization": resourceTFEOrganization(), - "tfe_organization_membership": resourceTFEOrganizationMembership(), - "tfe_organization_token": resourceTFEOrganizationToken(), - "tfe_policy_set": resourceTFEPolicySet(), - "tfe_policy_set_parameter": resourceTFEPolicySetParameter(), - "tfe_registry_module": resourceTFERegistryModule(), - "tfe_run_trigger": resourceTFERunTrigger(), - "tfe_sentinel_policy": resourceTFESentinelPolicy(), - "tfe_ssh_key": resourceTFESSHKey(), - "tfe_team": resourceTFETeam(), - "tfe_team_access": resourceTFETeamAccess(), - "tfe_team_organization_member": resourceTFETeamOrganizationMember(), - "tfe_team_member": resourceTFETeamMember(), - "tfe_team_members": resourceTFETeamMembers(), - "tfe_team_token": resourceTFETeamToken(), - "tfe_terraform_version": resourceTFETerraformVersion(), - "tfe_workspace": resourceTFEWorkspace(), - "tfe_variable": resourceTFEVariable(), + "tfe_agent_pool": resourceTFEAgentPool(), + "tfe_agent_token": resourceTFEAgentToken(), + "tfe_notification_configuration": resourceTFENotificationConfiguration(), + "tfe_oauth_client": resourceTFEOAuthClient(), + "tfe_organization": resourceTFEOrganization(), + "tfe_organization_membership": resourceTFEOrganizationMembership(), + "tfe_organization_module_sharing": resourceTFEOrganizationModuleSharing(), + "tfe_organization_token": resourceTFEOrganizationToken(), + "tfe_policy_set": resourceTFEPolicySet(), + "tfe_policy_set_parameter": resourceTFEPolicySetParameter(), + "tfe_registry_module": resourceTFERegistryModule(), + "tfe_run_trigger": resourceTFERunTrigger(), + "tfe_sentinel_policy": resourceTFESentinelPolicy(), + "tfe_ssh_key": resourceTFESSHKey(), + "tfe_team": resourceTFETeam(), + "tfe_team_access": resourceTFETeamAccess(), + "tfe_team_organization_member": resourceTFETeamOrganizationMember(), + "tfe_team_member": resourceTFETeamMember(), + "tfe_team_members": resourceTFETeamMembers(), + "tfe_team_token": resourceTFETeamToken(), + "tfe_terraform_version": resourceTFETerraformVersion(), + "tfe_workspace": resourceTFEWorkspace(), + "tfe_variable": resourceTFEVariable(), }, ConfigureFunc: providerConfigure, diff --git a/tfe/resource_tfe_organization_module_sharing.go b/tfe/resource_tfe_organization_module_sharing.go new file mode 100644 index 000000000..e6b104188 --- /dev/null +++ b/tfe/resource_tfe_organization_module_sharing.go @@ -0,0 +1,107 @@ +package tfe + +import ( + "fmt" + "log" + "strings" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceTFEOrganizationModuleSharing() *schema.Resource { + return &schema.Resource{ + Create: resourceTFEOrganizationModuleSharingCreate, + Read: resourceTFEOrganizationModuleSharingRead, + Update: resourceTFEOrganizationModuleSharingUpdate, + Delete: resourceTFEOrganizationModuleSharingDelete, + Schema: map[string]*schema.Schema{ + "organization": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return strings.EqualFold(old, new) + }, + }, + + "module_consumers": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + }, + } +} + +func resourceTFEOrganizationModuleSharingCreate(d *schema.ResourceData, meta interface{}) error { + // Get the organization name that will share "produce" modules + producer := d.Get("organization").(string) + + log.Printf("[DEBUG] Create %s module consumers", producer) + d.SetId(producer) + + return resourceTFEOrganizationModuleSharingUpdate(d, meta) +} + +func resourceTFEOrganizationModuleSharingUpdate(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + var consumers []string + for _, name := range d.Get("module_consumers").([]interface{}) { + // ignore empty strings + if name == nil { + continue + } + consumers = append(consumers, name.(string)) + } + + log.Printf("[DEBUG] Update %s module consumers", d.Id()) + err := tfeClient.Admin.Organizations.UpdateModuleConsumers(ctx, d.Id(), consumers) + if err != nil { + return fmt.Errorf("error updating module consumers to %s: %w", d.Id(), err) + } + + return resourceTFEOrganizationModuleSharingRead(d, meta) +} + +func resourceTFEOrganizationModuleSharingRead(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + options := tfe.AdminOrganizationListModuleConsumersOptions{} + + log.Printf("[DEBUG] Read configuration of module sharing for organization: %s", d.Id()) + for { + consumerList, err := tfeClient.Admin.Organizations.ListModuleConsumers(ctx, d.Id(), options) + if err != nil { + if err == tfe.ErrResourceNotFound { + log.Printf("[DEBUG] Organization %s does not longer exist", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading organization %s module consumer list: %w", d.Id(), err) + } + + if consumerList.CurrentPage >= consumerList.TotalPages { + break + } + + options.PageNumber = consumerList.NextPage + } + + return nil +} + +func resourceTFEOrganizationModuleSharingDelete(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + log.Printf("[DEBUG] Disable module sharing for organization: %s", d.Id()) + err := tfeClient.Admin.Organizations.UpdateModuleConsumers(ctx, d.Id(), []string{}) + if err != nil { + if err == tfe.ErrResourceNotFound { + return nil + } + return fmt.Errorf("failed to delete module sharing for organization %s: %w", d.Id(), err) + } + + return nil +} diff --git a/tfe/resource_tfe_organization_module_sharing_test.go b/tfe/resource_tfe_organization_module_sharing_test.go new file mode 100644 index 000000000..0068eef05 --- /dev/null +++ b/tfe/resource_tfe_organization_module_sharing_test.go @@ -0,0 +1,163 @@ +package tfe + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccTFEOrganizationModuleSharing_basic(t *testing.T) { + skipIfFreeOnly(t) + skipIfCloud(t) + + rInt1 := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + rInt2 := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + rInt3 := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + // Destroying a module sharing relationship is effectively updating + // the module sharing resource consumers to be an empty array + // We've omitted CheckDestroy since verifying the module sharing + // has been deleted requires the organizations to exist (and they are destroyed + // prior to CheckDestroy being executed) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEOrganizationModuleSharing_basic(rInt1, rInt2, rInt3), + Check: resource.ComposeAggregateTestCheckFunc( + // organization attribute */ + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.producer", "organization", fmt.Sprintf("tst-terraform-%d", rInt1)), + + // module_consumers attribute + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.producer", "module_consumers.#", "2"), + resource.TestCheckResourceAttrSet( + "tfe_organization_module_sharing.producer", "module_consumers.0"), + resource.TestCheckResourceAttrSet( + "tfe_organization_module_sharing.producer", "module_consumers.1"), + ), + }, + }, + }) +} + +func TestAccTFEOrganizationModuleSharing_emptyOrg(t *testing.T) { + skipIfFreeOnly(t) + skipIfCloud(t) + + rInt1 := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + rInt2 := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEOrganizationModuleSharing_emptyOrg(rInt1, rInt2), + Check: resource.ComposeAggregateTestCheckFunc( + // organization attribute */ + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.foo", "organization", fmt.Sprintf("tst-terraform-%d", rInt1)), + + // module_consumers attribute + // even though we've provided an empty string, + // we'll have two entries here since they are ignored + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.foo", "module_consumers.#", "2"), + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.foo", "module_consumers.0", ""), + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.foo", "module_consumers.1", fmt.Sprintf("tst-terraform-%d", rInt2)), + ), + }, + }, + }) +} + +func TestAccTFEOrganizationModuleSharing_stopSharing(t *testing.T) { + skipIfFreeOnly(t) + skipIfCloud(t) + + rInt1 := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + // This test will serve as a proxy for CheckDestroy, since + // setting a module_consumers to an empty array of + // "destroys" the resource + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEOrganizationModuleSharing_stopSharing(rInt1), + Check: resource.ComposeAggregateTestCheckFunc( + // organization attribute */ + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.foo", "organization", fmt.Sprintf("tst-terraform-%d", rInt1)), + + // module_consumers attribute + resource.TestCheckResourceAttr( + "tfe_organization_module_sharing.foo", "module_consumers.#", "0"), + ), + }, + }, + }) +} + +func testAccTFEOrganizationModuleSharing_basic(rInt1 int, rInt2 int, rInt3 int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_organization" "foo" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_organization" "bar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_organization_module_sharing" "producer" { + organization = tfe_organization.foobar.id + module_consumers = [tfe_organization.foo.id, tfe_organization.bar.id] +}`, rInt1, rInt2, rInt3) +} + +func testAccTFEOrganizationModuleSharing_emptyOrg(rInt1 int, rInt2 int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foo" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_organization" "bar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_organization_module_sharing" "foo" { + organization = tfe_organization.foo.id + module_consumers = ["", tfe_organization.bar.id] +}`, rInt1, rInt2) +} + +func testAccTFEOrganizationModuleSharing_stopSharing(rInt1 int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foo" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_organization_module_sharing" "foo" { + organization = tfe_organization.foo.id + module_consumers = [] +}`, rInt1) +} diff --git a/tfe/testing.go b/tfe/testing.go index be1033188..d1c5f6b63 100644 --- a/tfe/testing.go +++ b/tfe/testing.go @@ -38,13 +38,22 @@ func skipIfFreeOnly(t *testing.T) { } } +func skipIfCloud(t *testing.T) { + if !enterpriseEnabled() { + t.Skip("Skipping test for a feature unavailable in Terraform Cloud. Set 'ENABLE_TFE=1' to run.") + } +} + func skipIfEnterprise(t *testing.T) { - skip := os.Getenv("ENABLE_TFE") == "1" - if skip { + if enterpriseEnabled() { t.Skip("Skipping test for a feature unavailable in Terraform Enterprise. Set 'ENABLE_TFE=0' to run.") } } +func enterpriseEnabled() bool { + return os.Getenv("ENABLE_TFE") == "1" +} + func isAcceptanceTest() bool { return os.Getenv("TF_ACC") == "1" } diff --git a/website/docs/r/organization_module_sharing.html.markdown b/website/docs/r/organization_module_sharing.html.markdown new file mode 100644 index 000000000..9736d2fed --- /dev/null +++ b/website/docs/r/organization_module_sharing.html.markdown @@ -0,0 +1,32 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_organization_module_sharing" +sidebar_current: "docs-resource-tfe-organization-module-sharing" +description: |- + Manage module sharing for an organization. +--- + +# tfe_organization_module_sharing + +Manage module sharing for an organization. + +~> **NOTE:** This resource requires using the provider with +an instance of Terraform Enterprise at least as recent as v202004-1. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_organization_module_sharing" "test" { + organization = "my-org-name" + module_consumers = ["my-org-name-2", "my-org-name-3"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `organization` - (Required) Name of the organization. +* `module_consumers` - (Required) Names of the organizations to consume the module registry.