In the very first implementation of "sensitive values" we were
unfortunately not disciplined about separating the idea of "marked value"
from the idea of "sensitive value" (where the latter is a subset of the
former). The first implementation just assumed that any marking whatsoever
meant "sensitive".
We later improved that by adding the marks package and the marks.Sensitive
value to standardize on the representation of "sensitive value" as being
a value marked with _that specific mark_.
However, we did not perform a thorough review of all of the mark-handling
codepaths to make sure they all agreed on that definition. In particular,
the state and plan models were both designed as if they supported arbitrary
marks but then in practice marks other than marks.Sensitive would be
handled in various inconsistent ways: dropped entirely, or interpreted as
if marks.Sensitive, and possibly do so inconsistently when a value is
used only in memory vs. round-tripped through a wire/file format.
The goal of this commit is to resolve those oddities so that there are now
two possible situations:
- General mark handling: some codepaths genuinely handle marks
generically, by transporting them from input value to output value in
a way consistent with how cty itself deals with marks. This is the
ideal case because it means we can add new marks in future and assume
these codepaths will handle them correctly without any further
modifications.
- Sensitive-only mark preservation: the codepaths that interact with our
wire protocols and file formats typically have only specialized support
for sensitive values in particular, and lack support for any other
marks. Those codepaths are now subject to a new rule where they must
return an error if asked to deal with any other mark, so that if we
introduce new marks in future we'll be forced either to define how we'll
avoid those markings reaching the file/wire formats or extend the
file/wire formats to support the new marks.
Some new helper functions in package marks are intended to standardize how
we deal with the "sensitive values only" situations, in the hope that
this will make it easier to keep things consistent as the codebase evolves
in future.
In practice the modules runtime only ever uses marks.Sensitive as a mark
today, so all of these checks are effectively covering "should never
happen" cases. The only other mark Terraform uses is an implementation
detail of "terraform console" and does not interact with any of the
codepaths that only support sensitive values in particular.
Although we can often use the presence of resource instances in a
component instance as an implication of the component instance's existence,
it's possible (albeit rare) for a component instance to have no resources
in it at all, and thus it would be ambiguous whether it exists or not.
Now we'll track the existence of the component instances themselves as
extra objects in both the raw state and the state description. Along with
their existence we'll also track a snapshot of the output values as they
were at the most recent apply, which for now is primarily for external
consumption but we'll also include them in the raw state in case it ends up
being useful for something down the road.
Add additional fields to the AppliedChange#ResourceInstance
We're adding Resource Mode, Resource Type and Provider Address to the AppliedChange's ResourceInstance
This comment was noting that an earlier stub of this function was only
returning an external facing "state description" object and not including
the raw state for use when creating future plans.
That was subsequently fixed -- this now returns both description and raw
forms -- but the comment stuck around.
Previously our strategy for reporting the "applied change" results for
resource instance objects was to iterate over the final state and emit
an object for each resource instance we found in there.
That's not a sufficient solution though, because if any objects are deleted
as part of applying the plan then they won't be in the final state and so
we won't know to tell the caller that they should drop the relevant
objects from the stack-level state.
Instead, we'll keep track of all of the resource instance objects that
could potentially be affected by applying a plan and then use that set to
decide which "applied change" objects to emit. If one of the affected
objects is not present in the final state then we'll assume it was deleted
and so command the caller to delete its records of that object from both
the raw state and the state description.
With this in place, it's now possible to create a destroy-mode plan and
then apply it to end up with no resource instances at all. The stack
runtime itself is still missing handling of destroying its own objects
like components and embedded stacks, so this isn't a complete solution but
it does at least allow properly cleaning up the records of objects that
exist in remote systems, so it's now easier to clean up a development
environment with real infrastructure backing it.
We need to retain the prior state (in a form that Terraform Core's modules
runtime can accept) between the plan and apply phases because Terraform
uses it to verify that the provider's "final plan" is consistent with
the initial plan.
We previously tried to do this by reusing the "old" value from the planned
change, but that's not really possible in practice because of the
historical implementation detail that Terraform wants state information
in a JSON format and plan information in MessagePack.
This also contains the beginnings of handling the "discarding" of
unrecognized keys from the state data structures, although we'll probably
need to do more in that area later since this is just the bare minimum.
Previously we incorrectly assumed that resource instances only have one
"current" object each. However, resource instances can also have deposed
objects which we must also keep track of.
This is a breaking change to the rpcapi since we're now using a new
message type for a resource instance _object_ address in several places.
That breakage is intentional here because at the time of writing this
commit the rpcapi is not yet in any stable release and we want to force
updating our existing internal client to properly handle deposed objects
too.
This gets us ever closer to being able to preserve resource instance
objects from one run to the next. In subsequent commits we'll make use of
the "LoadFromProto" option to load the prior state during the planning
phase and take it into account when we're deciding what actions to
propose.
Previously we just had a stub that only generated basic external
description objects. Now we'll also emit "raw state" objects for these,
and have some initial support for deposed objects too.
Previously we just had this just stubbed out to always return an empty
object because we didn't have the schema information available to
transcode the JSON-encoded state data into the form our RPC API wants to
produce.
An earlier commit added the schema information we need, so we can now
transcode just in time to produce the protocol buffers serialization of
the applied change description.
This is still otherwise just a stub implementation. We'll still need to
deal with emitting the "raw" representation of this state for use in
future plans, and make sure we're properly handling deposed objects, in
future commits.
Unfortunately for historical reasons the "terraform" package produces
state and plan artifacts that have dynamic data elements pre-encoded in
the formats that Terraform CLI's traditional plan and state formats use.
Since Stacks uses different plan and state models, and exclusively uses
MessagePack encoding for dynamic values in both, we'll sometimes need to
transcode from one format to another when generating and decoding the
stacks-oriented representations.
Transcoding between these serialization formats requires access to the
relevant schema because the formats themselves each have an infoset that
is only a subset of Terraform's type system. Therefore we'll now annotate
the PlannedChange and AppliedChange objects that include
provider-specific data types with the schema required to transcode them.
This commit does not yet actually arrange to make use of those schemas.
That will follow in later commits.
We'd like to start developing some clients for this part of the RPC API
concurrently with remaining work here in Terraform Core, and so this is
a bare-minimum implementation of emitting "applied change" events for
resource instances just so there's something for client developers to test
against.
There are various things wrong with this, including but not limited to:
- It always reports that the new state for a resource instance is an
empty object, rather than including the correct updated object.
- It only supports "current" resource instance objects, and ignores
deposed ones.
- It just uses the resource instance address alone as the key for the
state description map, which is fine for now when we only have one
kind of description object anyway but will not be sufficient for a
real implementation that needs to emit various different kinds of
object.
We'll need to rework most of this in future commits, but this is hopefully
sufficient to start implementing and testing API clients, at least to some
extend, despite the crudity of the results.
This is a first pass at actually driving the apply phase through to
completion, using the ChangeExec function to schedule the real change
operations from the plan and then, just like for planning, a visit to
each participating object to ask it to check itself and report the results
of any changes it has made.
Many of our objects don't have any external side-effects of their own and
so just need to check their results are still valid after the apply phase
has replaced unknown values with known values. For those we can mostly
just share the same logic between plan and apply aside from asking for
ApplyPhase instead of PlanPhase.
ComponentInstance and OutputValue are the two objects whose treatment
differs the most between plan and apply, because both of those need to
emit externally-visible "applied change" objects to explain their
side-effects to the caller.
The apply-walk driver has a lot of behavior in common with the existing
plan-walk driver. For this commit we're just accepting that and having
two very similar pieces of code that differ only in some leaf details.
In a future commit we might try to generalize that so we can share more
logic between the two, but only if we can do that without making the code
significantly harder to read.