Based on discussion so far it seems that we have consensus about merging
the lang/eval and engine-related commits that precede this one as a
starting point for ongoing work in future (hopefully smaller) PRs.
This commit therefore mostly just acknowledges a small amount of unused
code in a way that it won't cause linter noise, since the future work is
likely to make these be used and so it'll be helpful to have them around as
examples to build on. (Which is, after all, the whole point of merging this
as dead code in the first place!)
This does _actually_ remove an old, now-redundant declaration of the
"compiled module instance" interface though, since that eventually ended
up in a different place and I just forgot to clean up this initial form
of it in later refactoring.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This covers most of the logic required to turn a source graph into a
compiled graph ready for execution. There's currently only support for one
of the opcodes though, so subsequent commits will sketch those out more
and then add some tests and fix any problems that inevitably exist here but
aren't yet visible because there are no tests.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Previously the PlanGlue methods all took PlanningOracle pointers as one
of their arguments, which is annoying since all of them should end up with
pointers to the same object and it makes it hard for the PlanGlue
implementation to do any work outside of and between the PlanGlue method
calls.
Instead then we'll have DrivePlanning take a function for building a
PlanGlue implementation given a PlanningOracle pointer, and then the
planning engine returns an implementation that binds a planContext to a
PlanningOracle it can then use to do all of its work.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This is a sketch of starting and configuring provider instances once they
are needed and then closing them once they are no longer needed. The
stubby implementation of data resource instance planning is there mainly
just to illustrate how this is intended to work, but it's not yet complete
enough to actually function.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This completes a previously-missing piece of the "prepareToPlan" result,
tracking which provider instances are relying on each ephemeral resource
instance.
This is important because the planning engine can "see"
resource-instance-to-provider relationships in the state that the eval
system isn't aware of, and so the planning engine must be able to keep
a provider instance open long enough to deal with both config-driven and
state-driven uses of it, which in turn means keeping open any ephemeral
resource instances that those provider instances depend on.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
The eval system gradually reports the "desired" declarations at various
different levels of granularity, and so the planning engine should compare
that with the instances in the previous run state to notice when any
existing resource instance is no longer in the desired state.
This doesn't yet include a real implementation of planning the deletion of
such resource instances. Real planning behaviors depend on us having some
sort of provider instance and ephemeral resource instance manager to be
able to make requests to configured providers, so that will follow in
subsequent commits before we can implement the actual planning behaviors.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
In a new async recursive tree walk we'll gradually traverse the tree of
module calls and announce the desired state objects at each level, so that
the PlanGlue implementation can then compare that with its own record of
the prior state to determine by omission which resource instances are
"orphans" that should be planned for deletion.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This puts the infrastructure in place to allow us to notify the PlanGlue
implementation about what's declared in the configuration so it can detect
orphaned resource instances by omission, but stops short of actually doing
that because it'll require another concurrent recursive tree walk which
is likely to be complex enough to deserve to live in its own later commit.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Previously this had the idea that the planning engine would work in two
subphases where first it would deal with the desired state (decided by
evaluating the configuration) and then it would do a second pass to plan
to delete any resource instances that didn't show up in the desired state.
Although that would technically work, we need to keep configured provider
instances open for the whole timespan between planning the first
desired resource instance and the final orphan instance using each
provider, and so splitting into two phases effectively means we'd have
provider instances sitting open but idle waiting for the desired state
subphase to complete to learn if they are needed for the orphan subphase.
This commit stubs out (but does not yet implement) a new approach where
the orphan planning can happen concurrently with the desired state
planning. This works by having the evaluator gradually describe to the
planning engine which module calls, module call instances, resources, and
resource instances _are_ present in the desired state -- as soon as that
information becomes available -- so that the planning engine can notice
by omission what subset of the prior state resource instances are no longer
desired and are therefore orphans.
This means that the planning engine should now have all of the information
it needs to recognize when a provider can be closed: the PlanningOracle
reports which _desired_ resource instances (or placeholders thereof) are
expected to need each provider instance, and the prior state tracks
exactly which provider instance each prior resource instance was associated
with, and so a provider's work is finished once all instances in the union
of those two sets have had their action planned.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Although our EvalContext API is primarily focused on what the eval system
needs, it's desirable for higher-level subsystems that wrap the evaluator
to be able to guarantee they are using the same provider and other
dependencies, so we'll expose the EvalContext that each ConfigInstance is
associated with in the public API.
The Providers interface now also grows to include NewConfiguredProvider as
a way to instantiate a configured provider instance. The eval system
doesn't need that itself, but exposing it here means that a caller can be
confident that they are definitely instantiating and configuring the same
provider that the eval system had previously used to validate the
configuration.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
As was already noted in a comment in the PlanningOracle declaration, the
PlanningOracle API is an exported entrypoint usable from outside lang/eval
and so each exported method should establish its own workgraph worker to
make sure it's handling those concerns correctly regardless of how the
caller chooses to use them.
For example, a caller might choose to make concurrent calls into the oracle
from multiple goroutines, which would violate the assumption that each
concurrently-running codepath is associated with a separate workgraph
worker.
Even if a caller is managing their own workgraph workers in the expected
way, this is functionally harmless and relatively cheap.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This is a building block for use by a planning engine implemented elsewhere
in OpenTofu so that it can be notified (by the completion of a call to the
function) when all resource instances that use a particular provider
instance or ephemeral resource instance have completed their plan-time
evaluation work.
Existing methods of PlanningOracle already expose sets of addresses to
wait for, but this method is separate from those because the planning
engine also needs the dependency information as data to know when an
ephemeral resource instance might need to be left open beyond the part of
the planning process driven by lang/eval in order to plan the delete
actions for any "orphan" resource instances that are in the prior state
but no longer declared in the configuration.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Continuing to split up some of the source files that have grown long over
ongoing experimentation, this is the start of splitting the config_plan.go
file into smaller parts.
For now only PlanningOracle gets its own file, but other types related to
planning might follow in later commits.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This was the big known missing piece for making a "real" implementation
of planning using this new package, so that the planning engine can know
what configuration to use when preparing a configured provider instance
for planning requests.
I'm sure there are other missing pieces that I've just not learned about
yet, but this is still a good checkpoint to stop iterating on lang/eval
for a little while and start stubbing out the planning engine.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
The previous approach had the problem that we only had the
CompiledModuleInstance object for a child module briefly in a local
variable while evaluating the child call, which is sufficient for just
evaluation but is not enough to support full config tree traversal for
questions like "what resource instances are declared throughout the
configuration?" and "what configgraph node corresponds to this provider
configuration instance address?"
This moves the separation of concerns around a little to be perhaps more
like how resource instances work, where the ModuleCallInstance.Value
method just wraps a function provided by the "compile" layer which then
takes the config value and compiles the child module instance.
This means that the "compile" layer can then hold on to the
CompiledModuleInstance object for the child module instance as part of
the "glue" in the ModuleCallInstance object and then use it to deal with
the config-tree-traversing methods in its own CompiledModuleInstance
implementation.
The new test of ConfigInstance.PrepareToPlan in lang/eval illustrates that
the system is now finally able to walk the whole configuration tree to find
resource instances and the provider instances they depend on.
(The handling of inheritance of providers between parent and child module
instances is still not working like the current system does, because the
"providers sidechannel" mechanism remains incomplete.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This file was originally in lang/eval directly and so it had all of the
compile-related functions grouped together to separate them from everything
else in that package.
Now that the "compile" process has its own package we have more room to
have many smaller files instead of one huge file, making this a little
easier to navigate now that the logic has grown.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Exposing only "ResourceInstancesDeep" on CompiledModuleInstance seemed
attractive at first when it was only partially implemented anyway, but
trying to finish implementing it revealed that the current design couldn't
actually support that API because we don't currently keep the
CompiledModuleInstance object for a child module instance after we've used
it to evaluate the child module instance's output values.
This commit doesn't actually solve that problem, but it does rework the
CompiledModuleInstance API so that the problem of finding and enumerating
child module instances is separated from the problem of finding resource
instances in just the current module instance, and then I'll try to find
a good way to satisfy this new, slightly-generalized API in future commits.
(The ultimate goal here is to be able to enumerate all of the resource
instances throughout the configuration for dependency-tracking purposes,
and to be able to find a specific ProviderInstance object given its
absolute address so that the plan or apply engine can request the
configuration value to configure the provider with.)
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This introduces the "compiler" changes and the configgraph changes to let
us evaluate the configuration for a particular provider instance.
A future commit will expose this through the public API of lang/eval so
that other systems like the planning engine will be able to ask for the
configuration for whatever provider instance a given resource instance
belongs to.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This is still not completely true to how current OpenTofu behaves, but it's
at least a little closer and avoids hard-coding a specific fixed provider
address in favor of actually following what's declared in the
configuration.
The system doesn't actually support evaluating the configuration for a
provider instance yet, so this remains just a placeholder for the
relatively simple tests in this package. Support for actually evaluating
the provider configurations will follow in later commits.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This is a little too busy/complicated for my liking but it's a place to
start and hopefully we'll be able to cut this down a little after we see
how the planning engine implementation turns out.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
The way this all fits together doesn't feel quite right yet since there's
a bunch of wrapping layers that aren't really adding anything except
indirection between the packages, but this is at least a starting point
for child module support.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This borrows the function table preparation from our previous lang.Scope
API (which this experiment would, if successful, ultimately replace) and
also intentionally implements the exprs.Evalable interface a little
improperly for now in the hope that we'll update our HCL patch for
function inspections to use hcl.StaticCall instead of misusing
hcl.Traversal at some point in the future.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
In other packages we've tried being pragmatic by making the main code
compensate for missing arguments in older tests, so that we don't always
need to update every test across the whole system every time we add a new
feature.
However, that compromise unfortunately tends to lead to non-test code
inadvertently relying on the compensations too, so this is an attempt at
a slightly different compromise where the main code will panic if called
incorrectly but there's an extra helper function for tests to fill in
reasonable "inert" defaults for the fields they leave unpopulated.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Recent changes in cty have included some new functions for working with
marks more efficiently. This particular function allows us to iterate over
the deep marks on a value without creating a new unmarked copy of the
value and without building an intermediate map of marks.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This also further highlighted that the marks-related codepaths in cty have
lots of room for performance improvement. In particular, we're spending
a lot of time in cty.Value.UnmarkDeep rebuilding values that actually
don't need to change and could likely get some benefit from rewriting
that to use the recently-added WrangleMarksDeep that is able to avoid
constructing new values for parts of the tree where no marks are changing.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This stops short of actually implementing the evaluation of a module call,
but gets the compilation behavior in place and the usual boilerplate for
handling multi-instance module calls.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
A big part of the motivation of the design of configgraph vs. package tofu
was to keep each node type self-contained and avoid the need for a big
shared "god object" that everything interacts with, and in turn part of
the reason to be interested in that is because it should makes it actually
practical and useful to write unit tests for these node types.
This commit introduces a few unit tests mainly just as a proof-of-concept
to see if the design has lived up to the ambition. So far that seems to be
true, although I'll reserve judgement until there's at least some coverage
on every on of the node types in this package.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
During early experiments the logic for "compiling" a configs.Module into
a bunch of configgraph node objects was just a bunch of unexported
functions directly in package eval, because it wasn't clear yet what
abstraction would make most sense between those two.
Now that this has solidified a little more this commit is a first attempt
at drawing a line between packages implementing "compilation" for specific
editions of the language (for which there is currently only one, which
I've named "tofu2024" as a placeholder because Terraform's only existing
language was similarly called "TF2021") and the top-level evaluation
package that tries to be as edition-agnostic as possible, although it is
currently still the one responsible for deciding to call into tofu2024.
There doesn't seem much point in abstracting away the decision about what
edition to use until we actually have a plan to introduce a new edition
and therefore have some idea of what the design constraints for that might
turn out to be.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Completing the reorganization of all of the scope-shape-related decisions
into the "compiler" code instead of package configgraph, this now deals
with the scope that contains the each.key/each.value/count.index local
symbols.
Package configgraph now no longer knows anything about the shape of the
scopes except for a few pragmatic mentions of specific conventions in
error messages where it doesn't seem worth the complexity to try to
organize that differently until we know concretely what problem we're
trying to solve.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
The pattern of having the "compiler" logic in lang/exprs always deal with
building suitable exprs.Valuers and configgraph just evaluating whatever
it was given has been working out in other contexts like the resource to
resource instance journey, so we'll follow that pattern for CheckRule too:
If the evaluation of a CheckRule depends on some value that's generated
by logic in configgraph then configgraph has a callback to ask the
"compiler" layer to dynamically construct zero or more CheckRule objects
that are pre-bound to the relevant value. For checks that only rely on
the global scope, such as output value preconditions, they are still
precompiled into a static slice in the parent object.
This also continues the progress towards all of the "expression scopes"
living with the "compiler" code instead of in configgraph, so that in
future different modules can potentially be bound to differently-shaped
scopes as we continue to evolve the language.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
On reflection it seems inconsistent that the "compile" logic in lang/eval
is responsible for deciding which expressions get evaluated in which scopes
but not for deciding what's actually in the top-level scope, since the
scope contents are something quite likely to vary between language
editions and language experiments.
With that in mind, this separates the idea of the "module instance scope"
from the actual ModuleInstance type, making it instead a wrapper type.
This continues the trend toward the grapheval package being mostly agnostic
to how the surface-level language is designed and instead just modeling
the relationships that are implied by the configuration.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This introduces the machinery for representing provider configs, provider
instances, and the relationships between resource instances and provider
instances.
Unfortunately there's a lot of accumulated complexity in the rules for
how OpenTofu currently decides which provider configs are available for
use in a module, including lots of implicit behavior for
backward-compatibility, so right now that is stubbed out with just a
hard-coded implementation that puts a "test/foo" provider with local name
"foo" in every module instance. We'll deal with a real implementation of
all that in later commits.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Previously the way this was dealing with "compiling" resource instances
once we've determined the instance keys was a little inconsistent with the
intended architecture because the configgraph.Resource type needed to know
how to pass various bits of context down into each of the instances, but
everywhere else we've tried to keep the configgraph types pretty agnostic
about such details and let the "compiler" layer worry about that.
Now the entire task of "compiling" a resource instance is delegated to
the "compiler" layer using a callback it provides. This makes things easier
because that layer already has all of the resource-level information it
needs available anyway, and so it can just plug that information directly
into the ResourceInstance fields itself in the same way as it builds
other types like ModuleInstance and Resource.
This also introduces an interface type to represent the callback API from
the ResourceInstance itself back to the compiler layer, which means we can
now split config validation and "get result value" into two methods, which
makes the implementation of each of those methods considerably simpler --
they are both now one-liners, and the data flow from validation to
get result value is encapsulated in the configgraph package where it will
be shared between all phases.
Overall the idea here is to make Resource and ResourceInstance independent
of one another so that, as is the goal for everything in configgraph, it's
more practical to write unit tests that don't need to mock out an entire
world just to test one small unit.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
It doesn't hurt to be liberal in handling this everywhere that we know
there's an error because applying the mark where it was already present
is effectively a no-op, so we'll introduce more of these both for
robustness and to help folks who are reading this code in future to
learn the EvalError patterns by observation.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
There's now enough here to produce a data structure where each ephemeral
resource instance is associated with all of the resource instances that
must complete planning before it can be closed.
This also doubles as the first demonstration of dynamic analysis of
resource instance dependencies using cty marks, with
TestPrepare_ephemeralResourceUsers illustrating the more precise results.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This is my current idea for how to resolve the chicken/egg problem of the
planning phase needing to know who uses ephemeral resource instances and
provider instances but us needing to evaluate the configuration to know
those relationships.
This borrows the evaluation behavior previously used for
ConfigInstance.Validate to produce a conservative evaluation of the
configuration without depending on any configured providers or ephemeral
resource instances, which we (in future commits) will use to discover
the relationships between those objects so that the real walk during the
planning phase can know when to start and stop them.
This relies on the idea that an evaluation with values stubbed out as
unknown should produce _at least_ the relationships that would occur with
fewer unknown values, though it might also report additional dependencies
that would vanish once values become more known. This gives the plan phase
something to start with and then we'll learn a tighter set of dependencies
during the planning phase which will form the basis of the apply-time
execution graph.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
The HCL "dynblock" extension blurs the distinction between static
structure and dynamic expressions by allowing the use of expressions to
generate zero or more nested blocks.
Because this feature is a bit of a layering violation it requires some
different usage patterns to get the correct result, and so this new
EvalableHCLBodyWithDynamicBlocks aims to encapsulate those details so that
the caller can just treat the result like a normal Evalable.
lang/eval now uses this for the body of a resource configuration, so that
"dynamic" blocks can work in there similarly to how they do in the previous
language runtime.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
The module_instance.go file was getting quite long and hard to navigate,
and the Valuer vs. Scope distinction seems like a reasonable place to
draw a line between two different files related to module instances.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
The separation of concerns here still doesn't feel _quite_ right since
a resource instance node needs to know what provider it belongs to even
though that package doesn't otherwise interact with providers directly
at all, but this at least clarifies exactly one location that's responsible
for getting the config value for the result value callback to use.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This also includes an assortment of other plumbing that I did while working
on this, since my current work mode is primarily driven by experimentation
rather than working towards specific milestones.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
In "package tofu" today we try to do everything using a generic acyclic
graph model and generic graph walk, which _works_ but tends to make every
other part of the problem very hard to follow because we rely a lot on
sidecar shared mutable data structures to propagate results between the
isolated operations.
This is the beginning of an experimental new way to do it where the "graph"
is implied by a model that more closely represents how the language itself
works, with explicit modelling of the relationships between different
types of objects and letting results flow directly from one object to
another without any big shared mutable state.
There's still a lot to do before this is actually complete enough to
evaluate whether it's a viable new design, but I'm considering this a good
starting checkpoint since there's enough here to run a simple test of
propagating data all the way from input variables to output values via
intermediate local values.
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>