Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
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)

ENHANCEMENTS:
* `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

FEATURES:
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