@@ -33,6 +33,9 @@ func resourceTFERegistryModule() *schema.Resource {
3333 },
3434
3535 CustomizeDiff : func (c context.Context , d * schema.ResourceDiff , meta interface {}) error {
36+ if err := validateNameAndProvider (d ); err != nil {
37+ return err
38+ }
3639 if err := validateVcsRepo (d ); err != nil {
3740 return err
3841 }
@@ -46,12 +49,10 @@ func resourceTFERegistryModule() *schema.Resource {
4649 ForceNew : true ,
4750 },
4851 "module_provider" : {
49- Type : schema .TypeString ,
50- Optional : true ,
51- Computed : true ,
52- ForceNew : true ,
53- ExactlyOneOf : []string {"vcs_repo" },
54- RequiredWith : []string {"organization" , "name" },
52+ Type : schema .TypeString ,
53+ Optional : true ,
54+ Computed : true ,
55+ ForceNew : true ,
5556 },
5657 "name" : {
5758 Type : schema .TypeString ,
@@ -193,6 +194,18 @@ func resourceTFERegistryModuleCreateWithVCS(v interface{}, meta interface{}, d *
193194 log .Printf ("[WARN] Error getting organization name: %s" , err )
194195 }
195196
197+ // Support for explicitly specifying Name and Provider with VCS repos.
198+ // This is particularly useful for monorepos with source_directory where the repository name
199+ // doesn't follow the terraform-<provider>-<name> convention (e.g., "private-modules", "monorepo").
200+ // When these fields are not provided, the API will infer them from the repository identifier.
201+ if name , ok := d .GetOk ("name" ); ok {
202+ options .Name = tfe .String (name .(string ))
203+ }
204+
205+ if provider , ok := d .GetOk ("module_provider" ); ok {
206+ options .Provider = tfe .String (provider .(string ))
207+ }
208+
196209 options .VCSRepo = & tfe.RegistryModuleVCSRepoOptions {
197210 Identifier : tfe .String (vcsRepo ["identifier" ].(string )),
198211 GHAInstallationID : tfe .String (vcsRepo ["github_app_installation_id" ].(string )),
@@ -519,6 +532,76 @@ func resourceTFERegistryModuleDelete(d *schema.ResourceData, meta interface{}) e
519532 return nil
520533}
521534
535+ func validateNameAndProvider (d * schema.ResourceDiff ) error {
536+ configMap := d .GetRawConfig ().AsValueMap ()
537+ nameValue , hasName := configMap ["name" ]
538+ providerValue , hasProvider := configMap ["module_provider" ]
539+ vcsRepoValue , hasVcsRepo := configMap ["vcs_repo" ]
540+
541+ nameProvided := hasName && ! nameValue .IsNull ()
542+ providerProvided := hasProvider && ! providerValue .IsNull ()
543+ vcsRepoProvided := hasVcsRepo && ! vcsRepoValue .IsNull ()
544+
545+ // Either vcs_repo OR module_provider must be provided
546+ if ! vcsRepoProvided && ! providerProvided {
547+ return fmt .Errorf ("one of vcs_repo or module_provider is required" )
548+ }
549+
550+ // Without vcs_repo, both name and module_provider are required
551+ if ! vcsRepoProvided {
552+ if ! nameProvided || ! providerProvided {
553+ return fmt .Errorf ("name and module_provider are required when not using vcs_repo" )
554+ }
555+ return nil
556+ }
557+
558+ // With vcs_repo: check source_directory and repo naming convention
559+ if vcsRepoValue .LengthInt () == 0 {
560+ return nil
561+ }
562+
563+ vcsRepoBlock := vcsRepoValue .AsValueSlice ()[0 ]
564+
565+ // When using source_directory, both fields are required
566+ sourceDirectory := vcsRepoBlock .GetAttr ("source_directory" )
567+ if ! sourceDirectory .IsNull () && sourceDirectory .AsString () != "" {
568+ if ! nameProvided || ! providerProvided {
569+ return fmt .Errorf ("name and module_provider are required when using source_directory" )
570+ }
571+ return nil
572+ }
573+
574+ // Check if repo follows terraform-<provider>-<name> convention
575+ displayIdentifier := vcsRepoBlock .GetAttr ("display_identifier" )
576+ if displayIdentifier .IsNull () || ! displayIdentifier .IsKnown () {
577+ return nil
578+ }
579+
580+ // Extract repo name from "org/repo" format and check convention
581+ repoName := displayIdentifier .AsString ()
582+ if idx := strings .LastIndex (repoName , "/" ); idx >= 0 {
583+ repoName = repoName [idx + 1 :]
584+ }
585+
586+ nameParts := strings .Split (repoName , "-" )
587+ followsConvention := len (nameParts ) == 3
588+
589+ // Standard repos allow both provided or both omitted, but not partial
590+ if followsConvention && nameProvided != providerProvided {
591+ if nameProvided {
592+ return fmt .Errorf ("module_provider must be provided when name is specified" )
593+ }
594+ return fmt .Errorf ("name must be provided when module_provider is specified" )
595+ }
596+
597+ // Non-standard repos require both fields
598+ if ! followsConvention && (! nameProvided || ! providerProvided ) {
599+ return fmt .Errorf ("name and module_provider are required when the repository name does not follow the terraform-<provider>-<name> convention" )
600+ }
601+
602+ return nil
603+ }
604+
522605func resourceTFERegistryModuleImporter (ctx context.Context , d * schema.ResourceData , meta interface {}) ([]* schema.ResourceData , error ) {
523606 registryModuleInfo := strings .SplitN (d .Id (), "/" , 6 )
524607 if len (registryModuleInfo ) == 4 {
0 commit comments