Skip to content

Commit

Permalink
stacks: call terraform.Validate on stack component configs
Browse files Browse the repository at this point in the history
  • Loading branch information
liamcervante committed Feb 21, 2024
1 parent c46515c commit 68ab945
Show file tree
Hide file tree
Showing 13 changed files with 1,330 additions and 1,031 deletions.
25 changes: 25 additions & 0 deletions internal/rpcapi/stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,35 @@ func (s *stacksServer) ValidateStackConfiguration(ctx context.Context, req *terr
if cfg == nil {
return nil, status.Error(codes.InvalidArgument, "the given stack configuration handle is invalid")
}
depsHnd := handle[*depsfile.Locks](req.DependencyLocksHandle)
var deps *depsfile.Locks
if !depsHnd.IsNil() {
deps = s.handles.DependencyLocks(depsHnd)
if deps == nil {
return nil, status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid")
}
} else {
deps = depsfile.NewLocks()
}
providerCacheHnd := handle[*providercache.Dir](req.ProviderCacheHandle)
var providerCache *providercache.Dir
if !providerCacheHnd.IsNil() {
providerCache = s.handles.ProviderPluginCache(providerCacheHnd)
if providerCache == nil {
return nil, status.Error(codes.InvalidArgument, "the given provider cache handle is invalid")
}
}

// (providerFactoriesForLocks explicitly supports a nil providerCache)
providerFactories, err := providerFactoriesForLocks(deps, providerCache)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err)
}

diags := stackruntime.Validate(ctx, &stackruntime.ValidateRequest{
Config: cfg,
ExperimentsAllowed: s.experimentsAllowed,
ProviderFactories: providerFactories,
})
return &terraform1.ValidateStackConfiguration_Response{
Diagnostics: diagnosticsToProto(diags),
Expand Down
1,935 changes: 979 additions & 956 deletions internal/rpcapi/terraform1/terraform1.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions internal/rpcapi/terraform1/terraform1.proto
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ message CloseStackConfiguration {
message ValidateStackConfiguration {
message Request {
int64 stack_config_handle = 1;
int64 dependency_locks_handle = 2;
int64 provider_cache_handle = 3;
}
message Response {
repeated Diagnostic diagnostics = 1;
Expand Down
6 changes: 6 additions & 0 deletions internal/stacks/stackruntime/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ func mainBundleSourceAddrStr(dirName string) string {
return "git::https://example.com/test.git//" + dirName
}

func mainBundleLocalAddrStr(dirName string) string {
// For now, the internal Terraform graph doesn't know about source bundles
// so diagnostics returned from there use the relative path.
return "testdata/mainbundle/test/" + dirName
}

// loadMainBundleConfigForTest is a convenience wrapper around
// loadConfigForTest that knows the location and package address of our
// "main" source bundle, in ./testdata/mainbundle, so that we can use that
Expand Down
168 changes: 165 additions & 3 deletions internal/stacks/stackruntime/internal/stackeval/component_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)

Expand All @@ -33,6 +35,7 @@ type ComponentConfig struct {

main *Main

validate promising.Once[tfdiags.Diagnostics]
moduleTree promising.Once[withDiagnostics[*configs.Config]]
}

Expand Down Expand Up @@ -279,6 +282,105 @@ func (c *ComponentConfig) RequiredProviderInstances(ctx context.Context) addrs.S
return moduleTree.Root.EffectiveRequiredProviderConfigs()
}

func (c *ComponentConfig) CheckProviders(ctx context.Context, phase EvalPhase) (addrs.Set[addrs.RootProviderConfig], tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

stackConfig := c.main.StackConfig(ctx, c.Addr().Stack)
declConfigs := c.Declaration(ctx).ProviderConfigs
neededProviders := c.RequiredProviderInstances(ctx)

ret := addrs.MakeSet[addrs.RootProviderConfig]()
for _, inCalleeAddr := range neededProviders {
typeAddr := inCalleeAddr.Provider
localName, ok := stackConfig.ProviderLocalName(ctx, typeAddr)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Component requires undeclared provider",
Detail: fmt.Sprintf(
"The root module for %s requires a configuration for provider %q, which isn't declared as a dependency of this stack configuration.\n\nDeclare this provider in the stack's required_providers block, and then assign a configuration for that provider in this component's \"providers\" argument.",
c.Addr(), typeAddr.ForDisplay(),
),
Subject: c.Declaration(ctx).DeclRange.ToHCL().Ptr(),
})
continue
}

localAddr := addrs.LocalProviderConfig{
LocalName: localName,
Alias: inCalleeAddr.Alias,
}
if _, exists := declConfigs[localAddr]; !exists {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required provider configuration",
Detail: fmt.Sprintf(
"The root module for %s requires a provider configuration named %q for provider %q, which is not assigned in the component's \"providers\" argument.",
c.Addr(), localAddr.StringCompact(), typeAddr.ForDisplay(),
),
Subject: c.Declaration(ctx).DeclRange.ToHCL().Ptr(),
})
continue
}

// TODO: Also validate the provider types are the same.

ret.Add(inCalleeAddr)
}
return ret, diags
}

func (c *ComponentConfig) neededProviderClients(ctx context.Context, phase EvalPhase) (map[addrs.RootProviderConfig]providers.Interface, bool) {
insts := make(map[addrs.RootProviderConfig]providers.Interface)
valid := true

providers, _ := c.CheckProviders(ctx, phase)
for _, provider := range providers {
pTy := c.main.ProviderType(ctx, provider.Provider)
if pTy == nil {
valid = false
continue // not our job to report a missing provider
}

// We don't need to configure the client for validate functionality.
inst, err := pTy.UnconfiguredClient(ctx)
if err != nil {
valid = false
continue
}
insts[provider] = inst
}

return insts, valid
}

func (c *ComponentConfig) neededProviderSchemas(ctx context.Context, phase EvalPhase) (map[addrs.Provider]providers.ProviderSchema, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

config := c.ModuleTree(ctx)
decl := c.Declaration(ctx)

providerSchemas := make(map[addrs.Provider]providers.ProviderSchema)
for _, sourceAddr := range config.ProviderTypes() {
pTy := c.main.ProviderType(ctx, sourceAddr)
if pTy == nil {
continue // not our job to report a missing provider
}
schema, err := pTy.Schema(ctx)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider initialization error",
Detail: fmt.Sprintf("Failed to fetch the provider schema for %s: %s.", sourceAddr, err),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
continue
}
providerSchemas[sourceAddr] = schema
}
return providerSchemas, diags
}

// ExprReferenceValue implements Referenceable.
func (c *ComponentConfig) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value {
// Currently we don't say anything at all about component results during
Expand All @@ -291,10 +393,70 @@ func (c *ComponentConfig) ExprReferenceValue(ctx context.Context, phase EvalPhas
}

func (c *ComponentConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
diags, err := c.validate.Do(ctx, func(ctx context.Context) (tfdiags.Diagnostics, error) {
var diags tfdiags.Diagnostics

_, moreDiags := c.CheckModuleTree(ctx)
diags = diags.Append(moreDiags)
moduleTree, moreDiags := c.CheckModuleTree(ctx)
diags = diags.Append(moreDiags)
if moduleTree == nil {
return diags, nil
}
decl := c.Declaration(ctx)

// TODO: Also check if the providers are valid.
// TODO: Also check if the input variables are valid.

providerSchemas, moreDiags := c.neededProviderSchemas(ctx, phase)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags, nil
}

// TODO: Manually validate the provider configs.

tfCtx, err := terraform.NewContext(&terraform.ContextOpts{
PreloadedProviderSchemas: providerSchemas,
Provisioners: c.main.availableProvisioners(),
})
if err != nil {
// Should not get here because we should always pass a valid
// ContextOpts above.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to instantiate Terraform modules runtime",
fmt.Sprintf("Could not load the main Terraform language runtime: %s.\n\nThis is a bug in Terraform; please report it!", err),
))
return diags, nil
}

providerClients, valid := c.neededProviderClients(ctx, phase)
if !valid {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Cannot validate component",
Detail: fmt.Sprintf("Cannot validate %s because its provider configuration assignments are invalid.", c.Addr()),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
return diags, nil
}
defer func() {
// Close the unconfigured provider clients that we opened in
// neededProviderClients.
for _, client := range providerClients {
client.Close()
}
}()

diags = diags.Append(tfCtx.Validate(moduleTree, &terraform.ValidateOpts{
ExternalProviders: providerClients,
}))
return diags, nil
})
if err != nil {
// this is crazy, we never return an error from the inner function so
// this really shouldn't happen.
panic(fmt.Sprintf("unexpected error from validate.Do: %s", err))
}

return diags
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@ import (
"github.com/zclconf/go-cty/cty/convert"

"github.com/hashicorp/terraform/internal/addrs"
fileProvisioner "github.com/hashicorp/terraform/internal/builtin/provisioners/file"
remoteExecProvisioner "github.com/hashicorp/terraform/internal/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
Expand Down Expand Up @@ -189,7 +186,7 @@ func (c *ComponentInstance) inputValuesForModulesRuntime(ctx context.Context, ph
return ret
}

// CheckProviders evaluates the "providers" argument from the component
// Providers evaluates the "providers" argument from the component
// configuration and returns a mapping from the provider configuration
// addresses that the component's root module expect to have populated
// to the address of the [ProviderInstance] from the stack configuration
Expand All @@ -200,7 +197,7 @@ func (c *ComponentInstance) inputValuesForModulesRuntime(ctx context.Context, ph
// then there are some problems with the providers argument and so the
// map might be incomplete, and so callers should use it only with a great
// deal of care.
func (c *ComponentInstance) Providers(ctx context.Context, phase EvalPhase) (selections map[addrs.RootProviderConfig]stackaddrs.AbsProviderConfigInstance, valid bool) {
func (c *ComponentInstance) Providers(ctx context.Context, phase EvalPhase) (map[addrs.RootProviderConfig]stackaddrs.AbsProviderConfigInstance, bool) {
ret, diags := c.CheckProviders(ctx, phase)
return ret, !diags.HasErrors()
}
Expand Down Expand Up @@ -250,11 +247,13 @@ func (c *ComponentInstance) CheckProviders(ctx context.Context, phase EvalPhase)
typeAddr := inCalleeAddr.Provider
localName, ok := stackConfig.ProviderLocalName(ctx, typeAddr)
if !ok {
// TODO: We should probably catch this as a one-time error during
// validation of the component config block, rather than raising
// it separately for each instance, since the set of required
// providers for both this stack and the root module of the
// component are statically-declared.
// We perform a similar check within component_config.go during
// the validation. At the validation stage we are only verifying the
// configuration, while this check is also checking the state. We
// can't check the state during validation, so we perform this check
// again. In reality, we will not even reach this if there are
// problems with the configuration as the validation will have
// failed before the plan/apply was even started.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Component requires undeclared provider",
Expand Down Expand Up @@ -515,7 +514,7 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
},
},
PreloadedProviderSchemas: providerSchemas,
Provisioners: c.availableProvisioners(),
Provisioners: c.main.availableProvisioners(),
})
if err != nil {
// Should not get here because we should always pass a valid
Expand Down Expand Up @@ -717,7 +716,7 @@ func (c *ComponentInstance) ApplyModuleTreePlan(ctx context.Context, plan *plans
tfHook,
},
PreloadedProviderSchemas: providerSchemas,
Provisioners: c.availableProvisioners(),
Provisioners: c.main.availableProvisioners(),
})
if err != nil {
// Should not get here because we should always pass a valid
Expand Down Expand Up @@ -1474,35 +1473,6 @@ func (c *ComponentInstance) resourceTypeSchema(ctx context.Context, providerType
return ret, nil
}

// availableProvisioners returns the table of provisioner factories that should
// be made available to modules in this component.
func (c *ComponentInstance) availableProvisioners() map[string]provisioners.Factory {
return map[string]provisioners.Factory{
"remote-exec": func() (provisioners.Interface, error) {
return remoteExecProvisioner.New(), nil
},
"file": func() (provisioners.Interface, error) {
return fileProvisioner.New(), nil
},
"local-exec": func() (provisioners.Interface, error) {
// We don't yet have any way to ensure a consistent execution
// environment for local-exec, which means that use of this
// provisioner is very likely to hurt portability between
// local and remote usage of stacks. Existing use of local-exec
// also tends to assume a writable module directory, whereas
// stack components execute from a read-only directory.
//
// Therefore we'll leave this unavailable for now with an explicit
// error message, although we might revisit this later if there's
// a strong reason to allow it and if we can find a suitable
// way to avoid the portability pitfalls that might inhibit
// moving execution of a stack from one execution environment to
// another.
return nil, fmt.Errorf("local-exec provisioners are not supported in stack components; use provider functionality or remote provisioners instead")
},
}
}

func (c *ComponentInstance) tracingName() string {
return c.Addr().String()
}
Loading

0 comments on commit 68ab945

Please sign in to comment.