Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

BUG FIXES:
* `r/tfe_stack`: Fixed a bug where omitting the `speculative_enabled` attribute caused plans to fail. The attribute is optional and defaults to `false` if not provided. By @arunatibm [#1972](https://github.com/hashicorp/terraform-provider-tfe/pull/1972)
* `r/tfe_registry_module`: Adds support for `name` and `module_provider` alongside `vcs_repo` with `source_directory`, by @jillirami [#1959](https://github.com/hashicorp/terraform-provider-tfe/pull/1959)

## v0.74.0

Expand Down
55 changes: 54 additions & 1 deletion internal/provider/resource_tfe_registry_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func resourceTFERegistryModule() *schema.Resource {
},

CustomizeDiff: func(c context.Context, d *schema.ResourceDiff, meta interface{}) error {
if err := validateNameAndProvider(d); err != nil {
return err
}
if err := validateVcsRepo(d); err != nil {
return err
}
Expand Down Expand Up @@ -87,7 +90,6 @@ func resourceTFERegistryModule() *schema.Resource {
Optional: true,
Computed: true,
ForceNew: true,
ExactlyOneOf: []string{"vcs_repo"},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required to be removed because this prevents having both vcs_repo AND module_provider which we need to support monorepo scenarios with source_directory

RequiredWith: []string{"organization", "name"},
},
"name": {
Expand Down Expand Up @@ -230,6 +232,14 @@ func resourceTFERegistryModuleCreateWithVCS(v interface{}, meta interface{}, d *
log.Printf("[WARN] Error getting organization name: %s", err)
}

if name, ok := d.GetOk("name"); ok {
options.Name = tfe.String(name.(string))
}

if provider, ok := d.GetOk("module_provider"); ok {
options.Provider = tfe.String(provider.(string))
}

options.VCSRepo = &tfe.RegistryModuleVCSRepoOptions{
Identifier: tfe.String(vcsRepo["identifier"].(string)),
GHAInstallationID: tfe.String(vcsRepo["github_app_installation_id"].(string)),
Expand Down Expand Up @@ -567,6 +577,49 @@ func resourceTFERegistryModuleDelete(d *schema.ResourceData, meta interface{}) e
return nil
}

func validateNameAndProvider(d *schema.ResourceDiff) error {
configMap := d.GetRawConfig().AsValueMap()
nameValue, hasName := configMap["name"]
providerValue, hasProvider := configMap["module_provider"]
vcsRepoValue := configMap["vcs_repo"]

nameProvided := hasName && !nameValue.IsNull()
providerProvided := hasProvider && !providerValue.IsNull()
if vcsRepoValue.LengthInt() == 0 {
return nil
}

vcsRepoBlock := vcsRepoValue.AsValueSlice()[0]
sourceDirectory := vcsRepoBlock.GetAttr("source_directory")
if sourceDirectory.IsNull() || (sourceDirectory.IsKnown() && sourceDirectory.AsString() == "") {
return nil
}

// Check if identifier follows terraform-<provider>-<name> convention
displayIdentifier := vcsRepoBlock.GetAttr("display_identifier")
if displayIdentifier.IsNull() || !displayIdentifier.IsKnown() {
return nil
}
// Extract repo name from "org/repo" format
repoName := displayIdentifier.AsString()
if idx := strings.LastIndex(repoName, "/"); idx >= 0 {
repoName = repoName[idx+1:]
}

nameParts := strings.SplitN(repoName, "-", 3)
followsConvention := len(nameParts) == 3
// Standard repos: neither name nor provider is required
if followsConvention {
return nil
}
// Non-standard repos: requires both name and provider fields
if !followsConvention && (!nameProvided || !providerProvided) {
return fmt.Errorf("name and module_provider are required when the repository name does not follow the terraform-<provider>-<name> convention")
}

return nil
}

func resourceTFERegistryModuleImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// First we'll check for an identity
identity, err := d.Identity()
Expand Down
135 changes: 126 additions & 9 deletions internal/provider/resource_tfe_registry_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,52 @@ func TestAccTFERegistryModule_vcsRepoWithTagPrefixMonorepo(t *testing.T) {
})
}

// TestAccTFERegistryModule_monorepoNonStandardName tests using source_directory with
// a repository that doesn't follow the terraform-<provider>-<name> naming convention.
func TestAccTFERegistryModule_monorepoNonStandardName(t *testing.T) {
skipUnlessBeta(t)
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
testAccPreCheckTFERegistryModule(t)
},
ProtoV6ProviderFactories: testAccMuxedProviders,
Steps: []resource.TestStep{
{
Config: testAccTFERegistryModule_monorepoNonStandardName(rInt),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("tfe_registry_module.foobar", "name", "nestedA"),
resource.TestCheckResourceAttr("tfe_registry_module.foobar", "module_provider", "aws"),
resource.TestCheckResourceAttr("tfe_registry_module.foobar", "vcs_repo.0.source_directory", "modules/nestedA"),
resource.TestCheckResourceAttr("tfe_registry_module.foobar", "vcs_repo.0.branch", "main"),
resource.TestCheckResourceAttr("tfe_registry_module.foobar", "vcs_repo.0.tags", "false"),
),
},
},
})
}

func TestAccTFERegistryModule_monorepoNonStandardNameWithoutNameandProvider(t *testing.T) {
skipUnlessBeta(t)
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
testAccPreCheckTFERegistryModule(t)
},
ProtoV6ProviderFactories: testAccMuxedProviders,
Steps: []resource.TestStep{
{
Config: testAccTFERegistryModule_monorepoNonStandardNameWithoutNameandProvider(rInt),
ExpectError: regexp.MustCompile(`name and module_provider are required when the repository name does not follow the terraform-<provider>-<name> convention`),
},
},
})
}

func TestAccTFERegistryModule_noCodeModule(t *testing.T) {
skipIfEnterprise(t)

Expand Down Expand Up @@ -933,16 +979,16 @@ func TestAccTFERegistryModuleImport_publicRM(t *testing.T) {
})
}

func TestAccTFERegistryModule_invalidWithBothVCSRepoAndModuleProvider(t *testing.T) {
func TestAccTFERegistryModule_validWithBothVCSRepoAndModuleProvider(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
ProtoV6ProviderFactories: testAccMuxedProviders,
Steps: []resource.TestStep{
{
Config: testAccTFERegistryModule_invalidWithBothVCSRepoAndModuleProvider(),
ExpectError: regexp.MustCompile("\"module_provider\": only one of `module_provider,vcs_repo` can be specified,\nbut `module_provider,vcs_repo` were specified."),
Config: testAccTFERegistryModule_validWithBothVCSRepoAndModuleProvider(),
ExpectError: regexp.MustCompile("\"module_provider\": all of `module_provider,name,organization` must be\nspecified"),
},
},
})
Expand Down Expand Up @@ -1783,7 +1829,7 @@ resource "tfe_registry_module" "foobar" {
rInt)
}

func testAccTFERegistryModule_invalidWithBothVCSRepoAndModuleProvider() string {
func testAccTFERegistryModule_validWithBothVCSRepoAndModuleProvider() string {
return `
resource "tfe_registry_module" "foobar" {
module_provider = "aws"
Expand Down Expand Up @@ -2142,10 +2188,81 @@ resource "tfe_registry_module" "foobar" {

initial_version = "1.0.0"

test_config {
tests_enabled = true
agent_execution_mode = "agent"
agent_pool_id = "apool-fake-id"
}
test_config {
tests_enabled = true
agent_execution_mode = "agent"
agent_pool_id = "apool-fake-id"
}
}`, rInt, envGithubToken, envGithubRegistryModuleIdentifer, envGithubRegistryModuleIdentifer)
}

func testAccTFERegistryModule_monorepoNonStandardName(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
name = "tst-terraform-%d"
email = "admin@company.com"
}

resource "tfe_oauth_client" "foobar" {
organization = tfe_organization.foobar.name
api_url = "https://api.github.com"
http_url = "https://github.com"
oauth_token = "%s"
service_provider = "github"
}

resource "tfe_registry_module" "foobar" {
organization = tfe_organization.foobar.name

name = "nestedA"
module_provider = "aws"

vcs_repo {
display_identifier = "%s"
identifier = "%s"
oauth_token_id = tfe_oauth_client.foobar.oauth_token_id
branch = "main"
tags = false
source_directory = "modules/nestedA"
}
}`,
rInt,
envGithubToken,
envGithubRegistryModuleIdentifer,
envGithubRegistryModuleIdentifer,
)
}

func testAccTFERegistryModule_monorepoNonStandardNameWithoutNameandProvider(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
name = "tst-terraform-%d"
email = "admin@company.com"
}

resource "tfe_oauth_client" "foobar" {
organization = tfe_organization.foobar.name
api_url = "https://api.github.com"
http_url = "https://github.com"
oauth_token = "%s"
service_provider = "github"
}

resource "tfe_registry_module" "foobar" {
organization = tfe_organization.foobar.name

vcs_repo {
display_identifier = "%s"
identifier = "%s"
oauth_token_id = tfe_oauth_client.foobar.oauth_token_id
branch = "main"
tags = false
source_directory = "modules/nestedA"
}
}`,
rInt,
envGithubToken,
envGithubRegistryModuleIdentifer,
envGithubRegistryModuleIdentifer,
)
}
34 changes: 33 additions & 1 deletion website/docs/r/registry_module.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,38 @@ resource "tfe_registry_module" "petstore" {
}
```

Create private registry module from a monorepo with source_directory (BETA):

```hcl
resource "tfe_organization" "test-organization" {
name = "my-org-name"
email = "admin@company.com"
}

resource "tfe_oauth_client" "test-oauth-client" {
organization = tfe_organization.test-organization.name
api_url = "https://api.github.com"
http_url = "https://github.com"
oauth_token = "my-vcs-provider-token"
service_provider = "github"
}

resource "tfe_registry_module" "monorepo-module" {
organization = tfe_organization.test-organization.name
name = "vpc"
module_provider = "aws"

vcs_repo {
display_identifier = "my-org-name/private-modules"
identifier = "my-org-name/private-modules"
oauth_token_id = tfe_oauth_client.test-oauth-client.oauth_token_id
source_directory = "modules/vpc"
}
}
```

~> **NOTE:** When using `source_directory`, you **must** explicitly specify both `name` and `module_provider`. This is required because monorepos and repositories with non-standard names (not following `terraform-<provider>-<name>` convention) cannot have these values automatically inferred by the API.

Create private registry module without VCS:

```hcl
Expand Down Expand Up @@ -190,7 +222,7 @@ The following arguments are supported:

* `vcs_repo` - (Optional) Settings for the registry module's VCS repository. Forces a
new resource if changed. One of `vcs_repo` or `module_provider` is required.
* `module_provider` - (Optional) Specifies the Terraform provider that this module is used for. For example, "aws"
* `module_provider` - (Optional) Specifies the Terraform provider that this module is used for. For example, "aws".
* `name` - (Optional) The name of registry module. It must be set if `module_provider` is used.
* `organization` - (Optional) The name of the organization associated with the registry module. It must be set if `module_provider` is used, or if `vcs_repo` is used via a GitHub App.
* `namespace` - (Optional) The namespace of a public registry module. It can be used if `module_provider` is set and `registry_name` is public.
Expand Down