github.com/kevinklinger/open_terraform@v1.3.6/noninternal/configs/provider_validation.go (about)

     1  package configs
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/kevinklinger/open_terraform/noninternal/addrs"
    10  )
    11  
    12  // validateProviderConfigs walks the full configuration tree from the root
    13  // module outward, static validation rules to the various combinations of
    14  // provider configuration, required_providers values, and module call providers
    15  // mappings.
    16  //
    17  // To retain compatibility with previous terraform versions, empty "proxy
    18  // provider blocks" are still allowed within modules, though they will
    19  // generate warnings when the configuration is loaded. The new validation
    20  // however will generate an error if a suitable provider configuration is not
    21  // passed in through the module call.
    22  //
    23  // The call argument is the ModuleCall for the provided Config cfg. The
    24  // noProviderConfigRange argument is passed down the call stack, indicating
    25  // that the module call, or a parent module call, has used a feature (at the
    26  // specified source location) that precludes providers from being configured at
    27  // all within the module.
    28  func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConfigRange *hcl.Range) (diags hcl.Diagnostics) {
    29  	mod := cfg.Module
    30  
    31  	for name, child := range cfg.Children {
    32  		mc := mod.ModuleCalls[name]
    33  		childNoProviderConfigRange := noProviderConfigRange
    34  		// if the module call has any of count, for_each or depends_on,
    35  		// providers are prohibited from being configured in this module, or
    36  		// any module beneath this module.
    37  		switch {
    38  		case mc.Count != nil:
    39  			childNoProviderConfigRange = mc.Count.Range().Ptr()
    40  		case mc.ForEach != nil:
    41  			childNoProviderConfigRange = mc.ForEach.Range().Ptr()
    42  		case mc.DependsOn != nil:
    43  			if len(mc.DependsOn) > 0 {
    44  				childNoProviderConfigRange = mc.DependsOn[0].SourceRange().Ptr()
    45  			} else {
    46  				// Weird! We'll just use the call itself, then.
    47  				childNoProviderConfigRange = mc.DeclRange.Ptr()
    48  			}
    49  		}
    50  		diags = append(diags, validateProviderConfigs(mc, child, childNoProviderConfigRange)...)
    51  	}
    52  
    53  	// the set of provider configuration names passed into the module, with the
    54  	// source range of the provider assignment in the module call.
    55  	passedIn := map[string]PassedProviderConfig{}
    56  
    57  	// the set of empty configurations that could be proxy configurations, with
    58  	// the source range of the empty configuration block.
    59  	emptyConfigs := map[string]hcl.Range{}
    60  
    61  	// the set of provider with a defined configuration, with the source range
    62  	// of the configuration block declaration.
    63  	configured := map[string]hcl.Range{}
    64  
    65  	// the set of configuration_aliases defined in the required_providers
    66  	// block, with the fully qualified provider type.
    67  	configAliases := map[string]addrs.AbsProviderConfig{}
    68  
    69  	// the set of provider names defined in the required_providers block, and
    70  	// their provider types.
    71  	localNames := map[string]addrs.Provider{}
    72  
    73  	for _, pc := range mod.ProviderConfigs {
    74  		name := providerName(pc.Name, pc.Alias)
    75  		// Validate the config against an empty schema to see if it's empty.
    76  		_, pcConfigDiags := pc.Config.Content(&hcl.BodySchema{})
    77  		if pcConfigDiags.HasErrors() || pc.Version.Required != nil {
    78  			configured[name] = pc.DeclRange
    79  		} else {
    80  			emptyConfigs[name] = pc.DeclRange
    81  		}
    82  	}
    83  
    84  	if mod.ProviderRequirements != nil {
    85  		// Track all known local types too to ensure we don't have duplicated
    86  		// with different local names.
    87  		localTypes := map[string]bool{}
    88  
    89  		// check for duplicate requirements of the same type
    90  		for _, req := range mod.ProviderRequirements.RequiredProviders {
    91  			if localTypes[req.Type.String()] {
    92  				// find the last declaration to give a better error
    93  				prevDecl := ""
    94  				for localName, typ := range localNames {
    95  					if typ.Equals(req.Type) {
    96  						prevDecl = localName
    97  					}
    98  				}
    99  
   100  				diags = append(diags, &hcl.Diagnostic{
   101  					Severity: hcl.DiagWarning,
   102  					Summary:  "Duplicate required provider",
   103  					Detail: fmt.Sprintf(
   104  						"Provider %s with the local name %q was previously required as %q. A provider can only be required once within required_providers.",
   105  						req.Type.ForDisplay(), req.Name, prevDecl,
   106  					),
   107  					Subject: &req.DeclRange,
   108  				})
   109  			} else if addrs.IsDefaultProvider(req.Type) {
   110  				// Now check for possible implied duplicates, where a provider
   111  				// block uses a default namespaced provider, but that provider
   112  				// was required via a different name.
   113  				impliedLocalName := req.Type.Type
   114  				// We have to search through the configs for a match, since the keys contains any aliases.
   115  				for _, pc := range mod.ProviderConfigs {
   116  					if pc.Name == impliedLocalName && req.Name != impliedLocalName {
   117  						diags = append(diags, &hcl.Diagnostic{
   118  							Severity: hcl.DiagWarning,
   119  							Summary:  "Duplicate required provider",
   120  							Detail: fmt.Sprintf(
   121  								"Provider %s with the local name %q was implicitly required via a configuration block as %q. The provider configuration block name must match the name used in required_providers.",
   122  								req.Type.ForDisplay(), req.Name, req.Type.Type,
   123  							),
   124  							Subject: &req.DeclRange,
   125  						})
   126  						break
   127  					}
   128  				}
   129  			}
   130  
   131  			localTypes[req.Type.String()] = true
   132  
   133  			localNames[req.Name] = req.Type
   134  			for _, alias := range req.Aliases {
   135  				addr := addrs.AbsProviderConfig{
   136  					Module:   cfg.Path,
   137  					Provider: req.Type,
   138  					Alias:    alias.Alias,
   139  				}
   140  				configAliases[providerName(alias.LocalName, alias.Alias)] = addr
   141  			}
   142  		}
   143  	}
   144  
   145  	checkImpliedProviderNames := func(resourceConfigs map[string]*Resource) {
   146  		// Now that we have all the provider configs and requirements validated,
   147  		// check for any resources which use an implied localname which doesn't
   148  		// match that of required_providers
   149  		for _, r := range resourceConfigs {
   150  			// We're looking for resources with no specific provider reference
   151  			if r.ProviderConfigRef != nil {
   152  				continue
   153  			}
   154  
   155  			localName := r.Addr().ImpliedProvider()
   156  
   157  			_, err := addrs.ParseProviderPart(localName)
   158  			if err != nil {
   159  				diags = append(diags, &hcl.Diagnostic{
   160  					Severity: hcl.DiagError,
   161  					Summary:  "Invalid provider local name",
   162  					Detail:   fmt.Sprintf("%q is an invalid implied provider local name: %s", localName, err),
   163  					Subject:  r.DeclRange.Ptr(),
   164  				})
   165  				continue
   166  			}
   167  
   168  			if _, ok := localNames[localName]; ok {
   169  				// OK, this was listed directly in the required_providers
   170  				continue
   171  			}
   172  
   173  			defAddr := addrs.ImpliedProviderForUnqualifiedType(localName)
   174  
   175  			// Now make sure we don't have the same provider required under a
   176  			// different name.
   177  			for prevLocalName, addr := range localNames {
   178  				if addr.Equals(defAddr) {
   179  					diags = append(diags, &hcl.Diagnostic{
   180  						Severity: hcl.DiagWarning,
   181  						Summary:  "Duplicate required provider",
   182  						Detail: fmt.Sprintf(
   183  							"Provider %q was implicitly required via resource %q, but listed in required_providers as %q. Either the local name in required_providers must match the resource name, or the %q provider must be assigned within the resource block.",
   184  							defAddr, r.Addr(), prevLocalName, prevLocalName,
   185  						),
   186  						Subject: &r.DeclRange,
   187  					})
   188  				}
   189  			}
   190  		}
   191  	}
   192  	checkImpliedProviderNames(mod.ManagedResources)
   193  	checkImpliedProviderNames(mod.DataResources)
   194  
   195  	// collect providers passed from the parent
   196  	if parentCall != nil {
   197  		for _, passed := range parentCall.Providers {
   198  			name := providerName(passed.InChild.Name, passed.InChild.Alias)
   199  			passedIn[name] = passed
   200  		}
   201  	}
   202  
   203  	parentModuleText := "the root module"
   204  	moduleText := "the root module"
   205  	if !cfg.Path.IsRoot() {
   206  		moduleText = cfg.Path.String()
   207  		if parent := cfg.Path.Parent(); !parent.IsRoot() {
   208  			// module address are prefixed with `module.`
   209  			parentModuleText = parent.String()
   210  		}
   211  	}
   212  
   213  	// Verify that any module calls only refer to named providers, and that
   214  	// those providers will have a configuration at runtime. This way we can
   215  	// direct users where to add the missing configuration, because the runtime
   216  	// error is only "missing provider X".
   217  	for _, modCall := range mod.ModuleCalls {
   218  		for _, passed := range modCall.Providers {
   219  			// aliased providers are handled more strictly, and are never
   220  			// inherited, so they are validated within modules further down.
   221  			// Skip these checks to prevent redundant diagnostics.
   222  			if passed.InParent.Alias != "" {
   223  				continue
   224  			}
   225  
   226  			name := passed.InParent.String()
   227  			_, confOK := configured[name]
   228  			_, localOK := localNames[name]
   229  			_, passedOK := passedIn[name]
   230  
   231  			// This name was not declared somewhere within in the
   232  			// configuration. We ignore empty configs, because they will
   233  			// already produce a warning.
   234  			if !(confOK || localOK) {
   235  				defAddr := addrs.NewDefaultProvider(name)
   236  				diags = append(diags, &hcl.Diagnostic{
   237  					Severity: hcl.DiagWarning,
   238  					Summary:  "Reference to undefined provider",
   239  					Detail: fmt.Sprintf(
   240  						"There is no explicit declaration for local provider name %q in %s, so Terraform is assuming you mean to pass a configuration for provider %q.\n\nTo clarify your intent and silence this warning, add to %s a required_providers entry named %q with source = %q, or a different source address if appropriate.",
   241  						name, moduleText, defAddr.ForDisplay(),
   242  						parentModuleText, name, defAddr.ForDisplay(),
   243  					),
   244  					Subject: &passed.InParent.NameRange,
   245  				})
   246  				continue
   247  			}
   248  
   249  			// Now we may have named this provider within the module, but
   250  			// there won't be a configuration available at runtime if the
   251  			// parent module did not pass one in.
   252  			if !cfg.Path.IsRoot() && !(confOK || passedOK) {
   253  				defAddr := addrs.NewDefaultProvider(name)
   254  				diags = append(diags, &hcl.Diagnostic{
   255  					Severity: hcl.DiagWarning,
   256  					Summary:  "Missing required provider configuration",
   257  					Detail: fmt.Sprintf(
   258  						"The configuration for %s expects to inherit a configuration for provider %s with local name %q, but %s doesn't pass a configuration under that name.\n\nTo satisfy this requirement, add an entry for %q to the \"providers\" argument in the module %q block.",
   259  						moduleText, defAddr.ForDisplay(), name, parentModuleText,
   260  						name, parentCall.Name,
   261  					),
   262  					Subject: parentCall.DeclRange.Ptr(),
   263  				})
   264  			}
   265  		}
   266  	}
   267  
   268  	if cfg.Path.IsRoot() {
   269  		// nothing else to do in the root module
   270  		return diags
   271  	}
   272  
   273  	// there cannot be any configurations if no provider config is allowed
   274  	if len(configured) > 0 && noProviderConfigRange != nil {
   275  		// We report this from the perspective of the use of count, for_each,
   276  		// or depends_on rather than from inside the module, because the
   277  		// recipient of this message is more likely to be the author of the
   278  		// calling module (trying to use an older module that hasn't been
   279  		// updated yet) than of the called module.
   280  		diags = append(diags, &hcl.Diagnostic{
   281  			Severity: hcl.DiagError,
   282  			Summary:  "Module is incompatible with count, for_each, and depends_on",
   283  			Detail: fmt.Sprintf(
   284  				"The module at %s is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, or depends_on arguments.\n\nIf you also control the module %q, consider updating this module to instead expect provider configurations to be passed by its caller.",
   285  				cfg.Path, cfg.SourceAddr,
   286  			),
   287  			Subject: noProviderConfigRange,
   288  		})
   289  	}
   290  
   291  	// now check that the user is not attempting to override a config
   292  	for name := range configured {
   293  		if passed, ok := passedIn[name]; ok {
   294  			diags = append(diags, &hcl.Diagnostic{
   295  				Severity: hcl.DiagError,
   296  				Summary:  "Cannot override provider configuration",
   297  				Detail: fmt.Sprintf(
   298  					"The configuration of %s has its own local configuration for %s, and so it cannot accept an overridden configuration provided by %s.",
   299  					moduleText, name, parentModuleText,
   300  				),
   301  				Subject: &passed.InChild.NameRange,
   302  			})
   303  		}
   304  	}
   305  
   306  	// A declared alias requires either a matching configuration within the
   307  	// module, or one must be passed in.
   308  	for name, providerAddr := range configAliases {
   309  		_, confOk := configured[name]
   310  		_, passedOk := passedIn[name]
   311  
   312  		if confOk || passedOk {
   313  			continue
   314  		}
   315  
   316  		diags = append(diags, &hcl.Diagnostic{
   317  			Severity: hcl.DiagError,
   318  			Summary:  "Missing required provider configuration",
   319  			Detail: fmt.Sprintf(
   320  				"The child module requires an additional configuration for provider %s, with the local name %q.\n\nRefer to the module's documentation to understand the intended purpose of this additional provider configuration, and then add an entry for %s in the \"providers\" meta-argument in the module block to choose which provider configuration the module should use for that purpose.",
   321  				providerAddr.Provider.ForDisplay(), name,
   322  				name,
   323  			),
   324  			Subject: &parentCall.DeclRange,
   325  		})
   326  	}
   327  
   328  	// You cannot pass in a provider that cannot be used
   329  	for name, passed := range passedIn {
   330  		childTy := passed.InChild.providerType
   331  		// get a default type if there was none set
   332  		if childTy.IsZero() {
   333  			// This means the child module is only using an inferred
   334  			// provider type. We allow this but will generate a warning to
   335  			// declare provider_requirements below.
   336  			childTy = addrs.NewDefaultProvider(passed.InChild.Name)
   337  		}
   338  
   339  		providerAddr := addrs.AbsProviderConfig{
   340  			Module:   cfg.Path,
   341  			Provider: childTy,
   342  			Alias:    passed.InChild.Alias,
   343  		}
   344  
   345  		localAddr, localName := localNames[name]
   346  		if localName {
   347  			providerAddr.Provider = localAddr
   348  		}
   349  
   350  		aliasAddr, configAlias := configAliases[name]
   351  		if configAlias {
   352  			providerAddr = aliasAddr
   353  		}
   354  
   355  		_, emptyConfig := emptyConfigs[name]
   356  
   357  		if !(localName || configAlias || emptyConfig) {
   358  
   359  			// we still allow default configs, so switch to a warning if the incoming provider is a default
   360  			if addrs.IsDefaultProvider(providerAddr.Provider) {
   361  				diags = append(diags, &hcl.Diagnostic{
   362  					Severity: hcl.DiagWarning,
   363  					Summary:  "Reference to undefined provider",
   364  					Detail: fmt.Sprintf(
   365  						"There is no explicit declaration for local provider name %q in %s, so Terraform is assuming you mean to pass a configuration for %q.\n\nIf you also control the child module, add a required_providers entry named %q with the source address %q.",
   366  						name, moduleText, providerAddr.Provider.ForDisplay(),
   367  						name, providerAddr.Provider.ForDisplay(),
   368  					),
   369  					Subject: &passed.InChild.NameRange,
   370  				})
   371  			} else {
   372  				diags = append(diags, &hcl.Diagnostic{
   373  					Severity: hcl.DiagError,
   374  					Summary:  "Reference to undefined provider",
   375  					Detail: fmt.Sprintf(
   376  						"The child module does not declare any provider requirement with the local name %q.\n\nIf you also control the child module, you can add a required_providers entry named %q with the source address %q to accept this provider configuration.",
   377  						name, name, providerAddr.Provider.ForDisplay(),
   378  					),
   379  					Subject: &passed.InChild.NameRange,
   380  				})
   381  			}
   382  		}
   383  
   384  		// The provider being passed in must also be of the correct type.
   385  		pTy := passed.InParent.providerType
   386  		if pTy.IsZero() {
   387  			// While we would like to ensure required_providers exists here,
   388  			// implied default configuration is still allowed.
   389  			pTy = addrs.NewDefaultProvider(passed.InParent.Name)
   390  		}
   391  
   392  		// use the full address for a nice diagnostic output
   393  		parentAddr := addrs.AbsProviderConfig{
   394  			Module:   cfg.Parent.Path,
   395  			Provider: pTy,
   396  			Alias:    passed.InParent.Alias,
   397  		}
   398  
   399  		if cfg.Parent.Module.ProviderRequirements != nil {
   400  			req, defined := cfg.Parent.Module.ProviderRequirements.RequiredProviders[name]
   401  			if defined {
   402  				parentAddr.Provider = req.Type
   403  			}
   404  		}
   405  
   406  		if !providerAddr.Provider.Equals(parentAddr.Provider) {
   407  			// If this module declares the same source address for a different
   408  			// local name then we'll prefer to suggest changing to match
   409  			// the child module's chosen name, assuming that it was the local
   410  			// name that was wrong rather than the source address.
   411  			var otherLocalName string
   412  			for localName, sourceAddr := range localNames {
   413  				if sourceAddr.Equals(parentAddr.Provider) {
   414  					otherLocalName = localName
   415  					break
   416  				}
   417  			}
   418  
   419  			const errSummary = "Provider type mismatch"
   420  			if otherLocalName != "" {
   421  				diags = append(diags, &hcl.Diagnostic{
   422  					Severity: hcl.DiagError,
   423  					Summary:  errSummary,
   424  					Detail: fmt.Sprintf(
   425  						"The assigned configuration is for provider %q, but local name %q in %s represents %q.\n\nTo pass this configuration to the child module, use the local name %q instead.",
   426  						parentAddr.Provider.ForDisplay(), passed.InChild.Name,
   427  						parentModuleText, providerAddr.Provider.ForDisplay(),
   428  						otherLocalName,
   429  					),
   430  					Subject: &passed.InChild.NameRange,
   431  				})
   432  			} else {
   433  				// If there is no declared requirement for the provider the
   434  				// caller is trying to pass under any name then we'll instead
   435  				// report it as an unsuitable configuration to pass into the
   436  				// child module's provider configuration slot.
   437  				diags = append(diags, &hcl.Diagnostic{
   438  					Severity: hcl.DiagError,
   439  					Summary:  errSummary,
   440  					Detail: fmt.Sprintf(
   441  						"The local name %q in %s represents provider %q, but %q in %s represents %q.\n\nEach provider has its own distinct configuration schema and provider types, so this module's %q can be assigned only a configuration for %s, which is not required by %s.",
   442  						passed.InParent, parentModuleText, parentAddr.Provider.ForDisplay(),
   443  						passed.InChild, moduleText, providerAddr.Provider.ForDisplay(),
   444  						passed.InChild, providerAddr.Provider.ForDisplay(),
   445  						moduleText,
   446  					),
   447  					Subject: passed.InParent.NameRange.Ptr(),
   448  				})
   449  			}
   450  		}
   451  	}
   452  
   453  	// Empty configurations are no longer needed. Since the replacement for
   454  	// this calls for one entry per provider rather than one entry per
   455  	// provider _configuration_, we'll first gather them up by provider
   456  	// and then report a single warning for each, whereby we can show a direct
   457  	// example of what the replacement should look like.
   458  	type ProviderReqSuggestion struct {
   459  		SourceAddr      addrs.Provider
   460  		SourceRanges    []hcl.Range
   461  		RequiredConfigs []string
   462  		AliasCount      int
   463  	}
   464  	providerReqSuggestions := make(map[string]*ProviderReqSuggestion)
   465  	for name, src := range emptyConfigs {
   466  		providerLocalName := name
   467  		if idx := strings.IndexByte(providerLocalName, '.'); idx >= 0 {
   468  			providerLocalName = providerLocalName[:idx]
   469  		}
   470  
   471  		sourceAddr, ok := localNames[name]
   472  		if !ok {
   473  			sourceAddr = addrs.NewDefaultProvider(providerLocalName)
   474  		}
   475  
   476  		suggestion := providerReqSuggestions[providerLocalName]
   477  		if suggestion == nil {
   478  			providerReqSuggestions[providerLocalName] = &ProviderReqSuggestion{
   479  				SourceAddr: sourceAddr,
   480  			}
   481  			suggestion = providerReqSuggestions[providerLocalName]
   482  		}
   483  
   484  		if providerLocalName != name {
   485  			// It's an aliased provider config, then.
   486  			suggestion.AliasCount++
   487  		}
   488  
   489  		suggestion.RequiredConfigs = append(suggestion.RequiredConfigs, name)
   490  		suggestion.SourceRanges = append(suggestion.SourceRanges, src)
   491  	}
   492  	for name, suggestion := range providerReqSuggestions {
   493  		var buf strings.Builder
   494  
   495  		fmt.Fprintf(
   496  			&buf,
   497  			"Earlier versions of Terraform used empty provider blocks (\"proxy provider configurations\") for child modules to declare their need to be passed a provider configuration by their callers. That approach was ambiguous and is now deprecated.\n\nIf you control this module, you can migrate to the new declaration syntax by removing all of the empty provider %q blocks and then adding or updating an entry like the following to the required_providers block of %s:\n",
   498  			name, moduleText,
   499  		)
   500  		fmt.Fprintf(&buf, "    %s = {\n", name)
   501  		fmt.Fprintf(&buf, "      source = %q\n", suggestion.SourceAddr.ForDisplay())
   502  		if suggestion.AliasCount > 0 {
   503  			// A lexical sort is fine because all of these strings are
   504  			// guaranteed to start with the same provider local name, and
   505  			// so we're only really sorting by the alias part.
   506  			sort.Strings(suggestion.RequiredConfigs)
   507  			fmt.Fprintln(&buf, "      configuration_aliases = [")
   508  			for _, addrStr := range suggestion.RequiredConfigs {
   509  				fmt.Fprintf(&buf, "        %s,\n", addrStr)
   510  			}
   511  			fmt.Fprintln(&buf, "      ]")
   512  
   513  		}
   514  		fmt.Fprint(&buf, "    }")
   515  
   516  		// We're arbitrarily going to just take the one source range that
   517  		// sorts earliest here. Multiple should be rare, so this is only to
   518  		// ensure that we produce a deterministic result in the edge case.
   519  		sort.Slice(suggestion.SourceRanges, func(i, j int) bool {
   520  			return suggestion.SourceRanges[i].String() < suggestion.SourceRanges[j].String()
   521  		})
   522  		diags = append(diags, &hcl.Diagnostic{
   523  			Severity: hcl.DiagWarning,
   524  			Summary:  "Redundant empty provider block",
   525  			Detail:   buf.String(),
   526  			Subject:  suggestion.SourceRanges[0].Ptr(),
   527  		})
   528  	}
   529  
   530  	return diags
   531  }
   532  
   533  func providerName(name, alias string) string {
   534  	if alias != "" {
   535  		name = name + "." + alias
   536  	}
   537  	return name
   538  }