diff --git a/internal/rpcapi/internal_client.go b/internal/rpcapi/internal_client.go index c3e86c5d8e..ae11a50525 100644 --- a/internal/rpcapi/internal_client.go +++ b/internal/rpcapi/internal_client.go @@ -42,7 +42,7 @@ type Client struct { func NewInternalClient(ctx context.Context, clientCaps *terraform1.ClientCapabilities) (*Client, error) { fakeListener := bufconn.Listen(4 * 1024 * 1024 /* buffer size */) srv := grpc.NewServer() - registerGRPCServices(srv) + registerGRPCServices(srv, &serviceOpts{}) go func() { if err := srv.Serve(fakeListener); err != nil { diff --git a/internal/rpcapi/plugin.go b/internal/rpcapi/plugin.go index 14e651d8d5..6d57920022 100644 --- a/internal/rpcapi/plugin.go +++ b/internal/rpcapi/plugin.go @@ -28,19 +28,22 @@ func (p *corePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, } func (p *corePlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { - registerGRPCServices(s) + generalOpts := &serviceOpts{ + experimentsAllowed: p.experimentsAllowed, + } + registerGRPCServices(s, generalOpts) return nil } -func registerGRPCServices(s *grpc.Server) { +func registerGRPCServices(s *grpc.Server, opts *serviceOpts) { // We initially only register the setup server, because the registration // of other services can vary depending on the capabilities negotiated // during handshake. - setup := newSetupServer(serverHandshake(s)) + setup := newSetupServer(serverHandshake(s, opts)) terraform1.RegisterSetupServer(s, setup) } -func serverHandshake(s *grpc.Server) func(context.Context, *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error) { +func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error) { dependencies := dynrpcserver.NewDependenciesStub() terraform1.RegisterDependenciesServer(s, dependencies) stacks := dynrpcserver.NewStacksStub() @@ -71,7 +74,7 @@ func serverHandshake(s *grpc.Server) func(context.Context, *terraform1.ClientCap // doing real work. In future the details of what we register here // might vary based on the negotiated capabilities. dependencies.ActivateRPCServer(newDependenciesServer(handles, services)) - stacks.ActivateRPCServer(newStacksServer(handles)) + stacks.ActivateRPCServer(newStacksServer(handles, opts)) packages.ActivateRPCServer(newPackagesServer(services)) // If the client requested any extra capabililties that we're going @@ -79,3 +82,12 @@ func serverHandshake(s *grpc.Server) func(context.Context, *terraform1.ClientCap return &terraform1.ServerCapabilities{}, nil } } + +// serviceOpts are options that could potentially apply to all of our +// individual RPC services. +// +// This could potentially be embedded inside a service-specific options +// structure, if needed. +type serviceOpts struct { + experimentsAllowed bool +} diff --git a/internal/rpcapi/stacks.go b/internal/rpcapi/stacks.go index 3779f3876f..76218531f6 100644 --- a/internal/rpcapi/stacks.go +++ b/internal/rpcapi/stacks.go @@ -31,14 +31,16 @@ import ( type stacksServer struct { terraform1.UnimplementedStacksServer - handles *handleTable + handles *handleTable + experimentsAllowed bool } var _ terraform1.StacksServer = (*stacksServer)(nil) -func newStacksServer(handles *handleTable) *stacksServer { +func newStacksServer(handles *handleTable, opts *serviceOpts) *stacksServer { return &stacksServer{ - handles: handles, + handles: handles, + experimentsAllowed: opts.experimentsAllowed, } } @@ -104,7 +106,8 @@ func (s *stacksServer) ValidateStackConfiguration(ctx context.Context, req *terr } diags := stackruntime.Validate(ctx, &stackruntime.ValidateRequest{ - Config: cfg, + Config: cfg, + ExperimentsAllowed: s.experimentsAllowed, }) return &terraform1.ValidateStackConfiguration_Response{ Diagnostics: diagnosticsToProto(diags), @@ -230,11 +233,12 @@ func (s *stacksServer) PlanStackChanges(req *terraform1.PlanStackChanges_Request changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) rtReq := stackruntime.PlanRequest{ - PlanMode: planMode, - Config: cfg, - PrevState: prevState, - ProviderFactories: providerFactories, - InputValues: inputValues, + PlanMode: planMode, + Config: cfg, + PrevState: prevState, + ProviderFactories: providerFactories, + InputValues: inputValues, + ExperimentsAllowed: s.experimentsAllowed, } rtResp := stackruntime.PlanResponse{ PlannedChanges: changesCh, @@ -368,9 +372,10 @@ func (s *stacksServer) ApplyStackChanges(req *terraform1.ApplyStackChanges_Reque changesCh := make(chan stackstate.AppliedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) rtReq := stackruntime.ApplyRequest{ - Config: cfg, - ProviderFactories: providerFactories, - RawPlan: req.PlannedChanges, + Config: cfg, + ProviderFactories: providerFactories, + RawPlan: req.PlannedChanges, + ExperimentsAllowed: s.experimentsAllowed, } rtResp := stackruntime.ApplyResponse{ AppliedChanges: changesCh, @@ -503,10 +508,11 @@ func (s *stacksServer) OpenStackInspector(ctx context.Context, req *terraform1.O } hnd := s.handles.NewStackInspector(&stacksInspector{ - Config: cfg, - State: state, - ProviderFactories: providerFactories, - InputValues: inputValues, + Config: cfg, + State: state, + ProviderFactories: providerFactories, + InputValues: inputValues, + ExperimentsAllowed: s.experimentsAllowed, }) return &terraform1.OpenStackInspector_Response{ diff --git a/internal/rpcapi/stacks_inspector.go b/internal/rpcapi/stacks_inspector.go index 0b7843781e..4969b1cd43 100644 --- a/internal/rpcapi/stacks_inspector.go +++ b/internal/rpcapi/stacks_inspector.go @@ -29,10 +29,11 @@ import ( // provide what they want to inspect just once and then perform any number // of subsequent inspection actions against it. type stacksInspector struct { - Config *stackconfig.Config - State *stackstate.State - ProviderFactories map[addrs.Provider]providers.Factory - InputValues map[stackaddrs.InputVariable]stackruntime.ExternalInputValue + Config *stackconfig.Config + State *stackstate.State + ProviderFactories map[addrs.Provider]providers.Factory + InputValues map[stackaddrs.InputVariable]stackruntime.ExternalInputValue + ExperimentsAllowed bool } // InspectExpressionResult evaluates a given expression string in the @@ -57,11 +58,12 @@ func (i *stacksInspector) InspectExpressionResult(ctx context.Context, req *terr } val, moreDiags := stackruntime.EvalExpr(ctx, expr, &stackruntime.EvalExprRequest{ - Config: i.Config, - State: i.State, - EvalStackInstance: stackAddr, - InputValues: i.InputValues, - ProviderFactories: i.ProviderFactories, + Config: i.Config, + State: i.State, + EvalStackInstance: stackAddr, + InputValues: i.InputValues, + ProviderFactories: i.ProviderFactories, + ExperimentsAllowed: i.ExperimentsAllowed, }) diags = diags.Append(moreDiags) if val == cty.NilVal { diff --git a/internal/rpcapi/stacks_test.go b/internal/rpcapi/stacks_test.go index db56e4b598..f8955f2956 100644 --- a/internal/rpcapi/stacks_test.go +++ b/internal/rpcapi/stacks_test.go @@ -28,7 +28,7 @@ func TestStacksOpenCloseStackConfiguration(t *testing.T) { ctx := context.Background() handles := newHandleTable() - stacksServer := newStacksServer(handles) + stacksServer := newStacksServer(handles, &serviceOpts{}) // In normal use a client would have previously opened a source bundle // using Dependencies.OpenSourceBundle, so we'll simulate the effect @@ -110,7 +110,7 @@ func TestStacksFindStackConfigurationComponents(t *testing.T) { ctx := context.Background() handles := newHandleTable() - stacksServer := newStacksServer(handles) + stacksServer := newStacksServer(handles, &serviceOpts{}) // In normal use a client would have previously opened a source bundle // using Dependencies.OpenSourceBundle, so we'll simulate the effect @@ -226,7 +226,7 @@ func TestStacksPlanStackChanges(t *testing.T) { ctx := context.Background() handles := newHandleTable() - stacksServer := newStacksServer(handles) + stacksServer := newStacksServer(handles, &serviceOpts{}) fakeSourceBundle := &sourcebundle.Bundle{} bundleHnd := handles.NewSourceBundle(fakeSourceBundle) diff --git a/internal/stacks/stackruntime/apply.go b/internal/stacks/stackruntime/apply.go index a7f6fa813e..1af2733b15 100644 --- a/internal/stacks/stackruntime/apply.go +++ b/internal/stacks/stackruntime/apply.go @@ -96,6 +96,8 @@ type ApplyRequest struct { RawPlan []*anypb.Any ProviderFactories map[addrs.Provider]providers.Factory + + ExperimentsAllowed bool } // ApplyResponse is used by [Apply] to describe the results of applying. diff --git a/internal/stacks/stackruntime/eval_expr.go b/internal/stacks/stackruntime/eval_expr.go index 9deb0b3dbc..84925d1834 100644 --- a/internal/stacks/stackruntime/eval_expr.go +++ b/internal/stacks/stackruntime/eval_expr.go @@ -28,6 +28,7 @@ func EvalExpr(ctx context.Context, expr hcl.Expression, req *EvalExprRequest) (c InputVariableValues: req.InputValues, ProviderFactories: req.ProviderFactories, }) + main.AllowLanguageExperiments(req.ExperimentsAllowed) return main.EvalExpr(ctx, expr, req.EvalStackInstance, stackeval.InspectPhase) } @@ -50,4 +51,6 @@ type EvalExprRequest struct { // configurations corresponding to these. InputValues map[stackaddrs.InputVariable]ExternalInputValue ProviderFactories map[addrs.Provider]providers.Factory + + ExperimentsAllowed bool } diff --git a/internal/stacks/stackruntime/internal/stackeval/applying.go b/internal/stacks/stackruntime/internal/stackeval/applying.go index 642ec3aba3..facd06e3e5 100644 --- a/internal/stacks/stackruntime/internal/stackeval/applying.go +++ b/internal/stacks/stackruntime/internal/stackeval/applying.go @@ -26,6 +26,8 @@ type ApplyOpts struct { // unrecognized then the apply phase will use this to emit the necessary // "discard" events to keep the state consistent. PrevStateDescKeys collections.Set[statekeys.Key] + + ExperimentsAllowed bool } // Applyable is an interface implemented by types which represent objects diff --git a/internal/stacks/stackruntime/internal/stackeval/component_config.go b/internal/stacks/stackruntime/internal/stackeval/component_config.go index ea5e68c13e..403d5c6aa9 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_config.go @@ -126,6 +126,7 @@ func (c *ComponentConfig) CheckModuleTree(ctx context.Context) (*configs.Config, // source files on disk (an implementation detail) rather than // preserving the source address abstraction. parser := configs.NewParser(afero.NewOsFs()) + parser.AllowLanguageExperiments(c.main.LanguageExperimentsAllowed()) if !parser.IsConfigDir(rootModuleDir) { diags = diags.Append(&hcl.Diagnostic{ diff --git a/internal/stacks/stackruntime/internal/stackeval/main.go b/internal/stacks/stackruntime/internal/stackeval/main.go index 1a2cbad9c8..f9665fb3ad 100644 --- a/internal/stacks/stackruntime/internal/stackeval/main.go +++ b/internal/stacks/stackruntime/internal/stackeval/main.go @@ -65,6 +65,11 @@ type Main struct { // This must never be used outside of test code in this package. testOnlyGlobals map[string]cty.Value + // languageExperimentsAllowed gets set if our caller enables the use + // of language experiments by calling [Main.AllowLanguageExperiments] + // shortly after creating this object. + languageExperimentsAllowed bool + // The remaining fields memoize other objects we might create in response // to method calls. Must lock "mu" before interacting with them. mu sync.Mutex @@ -156,6 +161,22 @@ func NewForInspecting(config *stackconfig.Config, state *stackstate.State, opts } } +// AllowLanguageExperiments changes the flag for whether language experiments +// are allowed during evaluation. +// +// Call this very shortly after creating a [Main], before performing any other +// actions on it. Changing this setting after other methods have been called +// will produce unpredictable results. +func (m *Main) AllowLanguageExperiments(allow bool) { + m.languageExperimentsAllowed = allow +} + +// LanguageExperimentsAllowed returns true if language experiments are allowed +// to be used during evaluation. +func (m *Main) LanguageExperimentsAllowed() bool { + return m.languageExperimentsAllowed +} + // Validating returns true if the receiving [Main] is configured for validating. // // If this returns false then validation methods may panic or return strange diff --git a/internal/stacks/stackruntime/internal/stackeval/main_apply.go b/internal/stacks/stackruntime/internal/stackeval/main_apply.go index 3345c411e8..b940e40706 100644 --- a/internal/stacks/stackruntime/internal/stackeval/main_apply.go +++ b/internal/stacks/stackruntime/internal/stackeval/main_apply.go @@ -199,6 +199,7 @@ func ApplyPlan(ctx context.Context, config *stackconfig.Config, rawPlan []*anypb }) main := NewForApplying(config, plan.RootInputValues, plan, results, opts) + main.AllowLanguageExperiments(opts.ExperimentsAllowed) begin(ctx, main) // the change tasks registered above become runnable // With the planned changes now in progress, we'll visit everything and diff --git a/internal/stacks/stackruntime/plan.go b/internal/stacks/stackruntime/plan.go index c44546774f..701dd5a2e9 100644 --- a/internal/stacks/stackruntime/plan.go +++ b/internal/stacks/stackruntime/plan.go @@ -49,6 +49,7 @@ func Plan(ctx context.Context, req *PlanRequest, resp *PlanResponse) { ForcePlanTimestamp: req.ForcePlanTimestamp, }) + main.AllowLanguageExperiments(req.ExperimentsAllowed) main.PlanAll(ctx, stackeval.PlanOutput{ AnnouncePlannedChange: func(ctx context.Context, change stackplan.PlannedChange) { resp.PlannedChanges <- change @@ -97,6 +98,8 @@ type PlanRequest struct { // to return the given value instead of whatever real time the plan // operation started. This is for testing purposes only. ForcePlanTimestamp *time.Time + + ExperimentsAllowed bool } // PlanResponse is used by [Plan] to describe the results of planning. diff --git a/internal/stacks/stackruntime/validate.go b/internal/stacks/stackruntime/validate.go index c6a428f585..197bad1962 100644 --- a/internal/stacks/stackruntime/validate.go +++ b/internal/stacks/stackruntime/validate.go @@ -19,6 +19,7 @@ func Validate(ctx context.Context, req *ValidateRequest) tfdiags.Diagnostics { defer span.End() main := stackeval.NewForValidating(req.Config, stackeval.ValidateOpts{}) + main.AllowLanguageExperiments(req.ExperimentsAllowed) diags := main.ValidateAll(ctx) diags = diags.Append( main.DoCleanup(ctx), @@ -32,5 +33,7 @@ func Validate(ctx context.Context, req *ValidateRequest) tfdiags.Diagnostics { type ValidateRequest struct { Config *stackconfig.Config + ExperimentsAllowed bool + // TODO: Provider factories and other similar such things }