From 7dad938fdb6bfeaf7cc082ce50bcf99f7889a2a9 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 9 Feb 2024 18:12:52 -0800 Subject: [PATCH] rpcapi+stacks: Stacks runtime can see whether experiments are allowed We allow experiments only in alpha builds, and so we propagate the flag for whether that's allowed in from "package main". We previously had that plumbed in only as far as the rpcapi startup. This plumbs the flag all the way into package stackeval so that we can in turn propagate it to Terraform's module config loader, which is ultimately the one responsible for ensuring that language experiments can be enabled only when the flag is set. Therefore it will now be possible to opt in to language experiments in modules that are used in stack components. --- internal/rpcapi/internal_client.go | 2 +- internal/rpcapi/plugin.go | 22 ++++++++--- internal/rpcapi/stacks.go | 38 +++++++++++-------- internal/rpcapi/stacks_inspector.go | 20 +++++----- internal/rpcapi/stacks_test.go | 6 +-- internal/stacks/stackruntime/apply.go | 2 + internal/stacks/stackruntime/eval_expr.go | 3 ++ .../internal/stackeval/applying.go | 2 + .../internal/stackeval/component_config.go | 1 + .../stackruntime/internal/stackeval/main.go | 21 ++++++++++ .../internal/stackeval/main_apply.go | 1 + internal/stacks/stackruntime/plan.go | 3 ++ internal/stacks/stackruntime/validate.go | 3 ++ 13 files changed, 90 insertions(+), 34 deletions(-) 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 }