states: store ephemeral output values in memory (#35676)

Ephemeral root output values must be kept in the in-memory state representation, but not written to the state file. To achieve this, we store ephemeral root outputs separately from non-ephemeral root outputs, so Terraform can access them during a single plan or apply phase.

Ephemeral root outputs always have a value of null in the state file. This means that the "terraform output" command, that reads the state file, reports null values for these outputs. Consumers of 'terraform output -json' should use the presence of '"ephemeral": true' in such output to interpret the value correctly.
pull/35729/head
kmoe 1 year ago committed by GitHub
parent 19d938e82f
commit a2039517a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -237,6 +237,12 @@ type RunningOperation struct {
// this state is managed by the backend. This should only be read
// after the operation completes to avoid read/write races.
State *states.State
// EphemeralOutputValues is populated only after an Apply operation
// completes, and contains the value for each ephemeral output in the root
// module.
// Ephemeral output values are not stored in the state file.
EphemeralOutputValues map[string]*states.OutputValue
}
// OperationResult describes the result status of an operation.

@ -376,6 +376,8 @@ func (b *Local) opApply(
return
}
runningOp.EphemeralOutputValues = applyState.EphemeralRootOutputValues
// Store the final state
runningOp.State = applyState
err := statemgr.WriteAndPersist(opState, applyState, schemas)

@ -266,6 +266,10 @@ func (s *stateStorageThatFailsRefresh) GetRootOutputValues(ctx context.Context)
return nil, fmt.Errorf("unimplemented")
}
func (s *stateStorageThatFailsRefresh) GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) {
return nil, fmt.Errorf("unimplemented")
}
func (s *stateStorageThatFailsRefresh) WriteState(*states.State) error {
return fmt.Errorf("unimplemented")
}

@ -598,6 +598,11 @@ func (s *State) GetRootOutputValues(ctx context.Context) (map[string]*states.Out
return result, nil
}
func (s *State) GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) {
// NOTE Ephemeral output values are not yet supported by the cloud backend.
return nil, nil
}
func clamp(val, min, max int64) int64 {
if val < min {
return min

@ -85,7 +85,6 @@ func (state State) renderHumanStateModule(renderer Renderer, module jsonstate.Mo
}
func (state State) renderHumanStateOutputs(renderer Renderer, opts computed.RenderHumanOpts) {
if len(state.RootModuleOutputs) > 0 {
renderer.Streams.Printf("\n\nOutputs:\n\n")

@ -221,6 +221,12 @@ func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]Output,
ret := make(map[string]Output)
for k, v := range outputs {
if v.Ephemeral {
// should never happen
panic(fmt.Sprintf("Ephemeral output value %s passed to state.MarshalOutputs. This is a bug in Terraform - please report it.", k))
}
ty := v.Value.Type()
ov, err := ctyjson.Marshal(v.Value, ty)
if err != nil {

@ -7,6 +7,8 @@ import (
"fmt"
"strings"
"maps"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/states"
@ -89,12 +91,17 @@ func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValu
return nil, diags
}
output, err := stateStore.GetRootOutputValues(ctx)
outputs, err := stateStore.GetRootOutputValues(ctx)
if err != nil {
return nil, diags.Append(err)
}
ephemeralOutputs, err := stateStore.GetEphemeralRootOutputValues(ctx)
if err != nil {
return nil, diags.Append(err)
}
maps.Copy(outputs, ephemeralOutputs)
return output, diags
return outputs, diags
}
func (c *OutputCommand) Help() string {

@ -80,7 +80,43 @@ func TestOutput_json(t *testing.T) {
}
actual := strings.TrimSpace(output.Stdout())
expected := "{\n \"foo\": {\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": \"bar\"\n }\n}"
expected := "{\n \"foo\": {\n \"ephemeral\": false,\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": \"bar\"\n }\n}"
if actual != expected {
t.Fatalf("wrong output\ngot: %#v\nwant: %#v", actual, expected)
}
}
func TestOutput_jsonEphemeral(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetEphemeralOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
})
statePath := testStateFile(t, originalState)
view, done := testView(t)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
View: view,
},
}
args := []string{
"-state", statePath,
"-json",
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: \n%s", output.Stderr())
}
actual := strings.TrimSpace(output.Stdout())
expected := "{\n \"foo\": {\n \"ephemeral\": true,\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": null\n }\n}"
if actual != expected {
t.Fatalf("wrong output\ngot: %#v\nwant: %#v", actual, expected)
}

@ -559,7 +559,7 @@ func TestRefresh_backup(t *testing.T) {
statePath := testStateFile(t, state)
// Output path
outf, err := ioutil.TempFile(td, "tf")
outf, err := os.CreateTemp(td, "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
@ -574,7 +574,7 @@ func TestRefresh_backup(t *testing.T) {
}
// Backup path
backupf, err := ioutil.TempFile(td, "tf")
backupf, err := os.CreateTemp(td, "tf")
if err != nil {
t.Fatalf("err: %s", err)
}

@ -217,7 +217,11 @@ func (v *OutputJSON) Output(name string, outputs map[string]*states.OutputValue)
// show in the single value case. We must now maintain that behavior
// for compatibility, so this is an emulation of the JSON
// serialization of outputs used in state format version 3.
//
// Note that when running the output command, the value of an ephemeral
// output is always nil and its type is always cty.DynamicPseudoType.
type OutputMeta struct {
Ephemeral bool `json:"ephemeral"`
Sensitive bool `json:"sensitive"`
Type json.RawMessage `json:"type"`
Value json.RawMessage `json:"value"`
@ -236,6 +240,7 @@ func (v *OutputJSON) Output(name string, outputs map[string]*states.OutputValue)
return diags
}
outputMetas[n] = OutputMeta{
Ephemeral: os.Ephemeral,
Sensitive: os.Sensitive,
Type: json.RawMessage(jsonType),
Value: json.RawMessage(jsonVal),

@ -149,6 +149,7 @@ foo = <sensitive>
arguments.ViewJSON,
`{
"bar": {
"ephemeral": false,
"sensitive": false,
"type": [
"list",
@ -161,6 +162,7 @@ foo = <sensitive>
]
},
"baz": {
"ephemeral": false,
"sensitive": false,
"type": [
"object",
@ -175,6 +177,7 @@ foo = <sensitive>
}
},
"foo": {
"ephemeral": false,
"sensitive": true,
"type": "string",
"value": "secret"

@ -16,4 +16,5 @@ type OutputValue struct {
Addr addrs.AbsOutputValue
Value cty.Value
Sensitive bool
Ephemeral bool
}

@ -73,6 +73,19 @@ func (s *State) GetRootOutputValues(ctx context.Context) (map[string]*states.Out
return state.RootOutputValues, nil
}
func (s *State) GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) {
if err := s.RefreshState(); err != nil {
return nil, fmt.Errorf("Failed to load state: %s", err)
}
state := s.State()
if state == nil {
state = states.NewState()
}
return state.EphemeralRootOutputValues, nil
}
// StateForMigration is part of our implementation of statemgr.Migrator.
func (s *State) StateForMigration() *statefile.File {
s.mu.Lock()

@ -29,14 +29,21 @@ type State struct {
// an implementation detail and must not be used by outside callers.
Modules map[string]*Module
// OutputValues contains the state for each output value defined in the
// root module.
// RootOutputValues contains the state for each non-ephemeral output value
// defined in the root module.
//
// Output values in other modules don't persist anywhere between runs,
// so Terraform Core tracks those only internally and does not expose
// them in any artifacts that survive between runs.
RootOutputValues map[string]*OutputValue
// EphemeralRootOutputValues contains the state for each ephemeral output
// value defined in the root module.
//
// Ephemeral outputs are treated separately from non-ephemeral outputs, to
// ensure that their values are never written to the state file.
EphemeralRootOutputValues map[string]*OutputValue
// CheckResults contains a snapshot of the statuses of checks at the
// end of the most recent update to the state. Callers might compare
// checks between runs to see if e.g. a previously-failing check has
@ -56,8 +63,9 @@ func NewState() *State {
modules := map[string]*Module{}
modules[addrs.RootModuleInstance.String()] = NewModule(addrs.RootModuleInstance)
return &State{
Modules: modules,
RootOutputValues: make(map[string]*OutputValue),
Modules: modules,
RootOutputValues: make(map[string]*OutputValue),
EphemeralRootOutputValues: make(map[string]*OutputValue),
}
}
@ -77,7 +85,7 @@ func (s *State) Empty() bool {
if s == nil {
return true
}
if len(s.RootOutputValues) != 0 {
if len(s.RootOutputValues) != 0 || len(s.EphemeralRootOutputValues) != 0 {
return false
}
for _, ms := range s.Modules {
@ -301,9 +309,9 @@ func (s *State) OutputValue(addr addrs.AbsOutputValue) *OutputValue {
// SetOutputValue updates the value stored for the given output value if and
// only if it's a root module output value.
//
// All other output values will just be silently ignored, because we don't
// store those here anymore. (They live in a namedvals.State object hidden
// in the internals of Terraform Core.)
// All child module output values will just be silently ignored, because we
// don't store those here any more. (They live in a namedvals.State object
// hidden in the internals of Terraform Core.)
func (s *State) SetOutputValue(addr addrs.AbsOutputValue, value cty.Value, sensitive bool) {
if !addr.Module.IsRoot() {
return
@ -323,6 +331,41 @@ func (s *State) RemoveOutputValue(addr addrs.AbsOutputValue) {
delete(s.RootOutputValues, addr.OutputValue.Name)
}
// EphemeralOutputValue returns the state for the output value with the given
// address, or nil if no such ephemeral output value is tracked in the state.
//
// Only root module output values are tracked in the state, so this always
// returns nil for output values in any other module.
func (s *State) EphemeralOutputValue(addr addrs.AbsOutputValue) *OutputValue {
if !addr.Module.IsRoot() {
return nil
}
return s.EphemeralRootOutputValues[addr.OutputValue.Name]
}
// SetEphemeralOutputValue updates the value stored for the given ephemeral
// output value if and only if it's a root module output value.
func (s *State) SetEphemeralOutputValue(addr addrs.AbsOutputValue, value cty.Value, sensitive bool) {
if !addr.Module.IsRoot() {
return
}
s.EphemeralRootOutputValues[addr.OutputValue.Name] = &OutputValue{
Addr: addr,
Value: value,
Sensitive: sensitive,
Ephemeral: true,
}
}
// RemoveOutputValue removes the record of a previously-stored ephemeral output
// value.
func (s *State) RemoveEphemeralOutputValue(addr addrs.AbsOutputValue) {
if !addr.Module.IsRoot() {
return
}
delete(s.EphemeralRootOutputValues, addr.OutputValue.Name)
}
// ProviderAddrs returns a list of all of the provider configuration addresses
// referenced throughout the receiving state.
//

@ -35,10 +35,15 @@ func (s *State) DeepCopy() *State {
for k, v := range s.RootOutputValues {
outputValues[k] = v.DeepCopy()
}
ephemeralOutputValues := make(map[string]*OutputValue, len(s.EphemeralRootOutputValues))
for k, v := range s.EphemeralRootOutputValues {
ephemeralOutputValues[k] = v.DeepCopy()
}
return &State{
Modules: modules,
RootOutputValues: outputValues,
CheckResults: s.CheckResults.DeepCopy(),
Modules: modules,
RootOutputValues: outputValues,
EphemeralRootOutputValues: ephemeralOutputValues,
CheckResults: s.CheckResults.DeepCopy(),
}
}
@ -228,5 +233,6 @@ func (os *OutputValue) DeepCopy() *OutputValue {
Addr: os.Addr,
Value: os.Value,
Sensitive: os.Sensitive,
Ephemeral: os.Ephemeral,
}
}

@ -247,7 +247,7 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
ms.SetResourceProvider(rAddr, providerAddr)
}
// The root module is special in that we persist its attributes and thus
// The root module is special in that we persist its outputs and thus
// need to reload them now. (For descendent modules we just re-calculate
// them based on the latest configuration on each run.)
{
@ -260,6 +260,7 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
},
}
os.Sensitive = fos.Sensitive
os.Ephemeral = fos.Ephemeral
ty, err := ctyjson.UnmarshalType([]byte(fos.ValueTypeRaw))
if err != nil {
@ -282,7 +283,11 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
}
os.Value = val
state.RootOutputValues[name] = os
if os.Ephemeral {
state.EphemeralRootOutputValues[name] = os
} else {
state.RootOutputValues[name] = os
}
}
}
@ -356,6 +361,26 @@ func writeStateV4(file *File, w io.Writer) tfdiags.Diagnostics {
}
}
// Ephemeral outputs are always saved to the state with a value of null.
for name, eos := range file.State.EphemeralRootOutputValues {
typeSrc, err := ctyjson.MarshalType(eos.Value.Type())
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize output value in state",
fmt.Sprintf("An error occured while serializing the type of output value %q: %s.", name, err),
))
continue
}
sV4.RootOutputs[name] = outputStateV4{
Ephemeral: true,
Sensitive: eos.Sensitive,
ValueRaw: json.RawMessage("null"),
ValueTypeRaw: json.RawMessage(typeSrc),
}
}
for _, ms := range file.State.Modules {
moduleAddr := ms.Addr
for _, rs := range ms.Resources {
@ -680,6 +705,7 @@ type outputStateV4 struct {
ValueRaw json.RawMessage `json:"value"`
ValueTypeRaw json.RawMessage `json:"type"`
Sensitive bool `json:"sensitive,omitempty"`
Ephemeral bool `json:"ephemeral,omitempty"`
}
type resourceStateV4 struct {

@ -251,6 +251,20 @@ func (s *Filesystem) GetRootOutputValues(ctx context.Context) (map[string]*state
return state.RootOutputValues, nil
}
func (s *Filesystem) GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) {
err := s.RefreshState()
if err != nil {
return nil, err
}
state := s.State()
if state == nil {
state = states.NewState()
}
return state.EphemeralRootOutputValues, nil
}
func (s *Filesystem) refreshState() error {
var reader io.Reader

@ -27,6 +27,10 @@ func (s *LockDisabled) GetRootOutputValues(ctx context.Context) (map[string]*sta
return s.Inner.GetRootOutputValues(ctx)
}
func (s *LockDisabled) GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) {
return s.Inner.GetEphemeralRootOutputValues(ctx)
}
func (s *LockDisabled) WriteState(v *states.State) error {
return s.Inner.WriteState(v)
}

@ -33,8 +33,13 @@ type Persistent interface {
// the output values from it because enhanced backends can apply special permissions
// to differentiate reading the state and reading the outputs within the state.
type OutputReader interface {
// GetRootOutputValues fetches the root module output values from state or another source
// GetRootOutputValues fetches the non-ephemeral root module output values
// from state or another source.
GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error)
// GetEphemeralRootOutputValues fetches the ephemeral root module output values
// from state or another source.
GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error)
}
// Refresher is the interface for managers that can read snapshots from

@ -74,6 +74,10 @@ func (m *fakeFull) GetRootOutputValues(ctx context.Context) (map[string]*states.
return m.State().RootOutputValues, nil
}
func (m *fakeFull) GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) {
return m.State().EphemeralRootOutputValues, nil
}
func (m *fakeFull) Lock(info *LockInfo) (string, error) {
m.lockLock.Lock()
defer m.lockLock.Unlock()
@ -124,6 +128,10 @@ func (m *fakeErrorFull) GetRootOutputValues(ctx context.Context) (map[string]*st
return nil, errors.New("fake state manager error")
}
func (m *fakeErrorFull) GetEphemeralRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) {
return nil, errors.New("fake state manager error")
}
func (m *fakeErrorFull) WriteState(s *states.State) error {
return errors.New("fake state manager error")
}

@ -118,6 +118,47 @@ func (s *SyncState) RemoveOutputValue(addr addrs.AbsOutputValue) {
s.state.RemoveOutputValue(addr)
}
// EphemeralOutputValue returns a snapshot of the state of the ephemeral output
// value with the given address, or nil if no such ephemeral output value is
// tracked.
//
// The return value is a pointer to a copy of the output value state, which the
// caller may then freely access and mutate.
func (s *SyncState) EphemeralOutputValue(addr addrs.AbsOutputValue) *OutputValue {
s.lock.RLock()
ret := s.state.EphemeralOutputValue(addr).DeepCopy()
s.lock.RUnlock()
return ret
}
// SetEphemeralOutputValue writes a given ephemeral output value into the
// state, overwriting any existing value of the same name.
//
// The state only tracks output values for the root module, so attempts to
// write output values for any other module will be silently ignored.
func (s *SyncState) SetEphemeralOutputValue(addr addrs.AbsOutputValue, value cty.Value, sensitive bool) {
if !addr.Module.IsRoot() {
return
}
defer s.beginWrite()()
s.state.SetEphemeralOutputValue(addr, value, sensitive)
}
// RemoveEphemeralOutputValue removes the stored value for the ephemeral output
// value with the given address.
//
// The state only tracks output values for the root module, so attempts to
// remove output values for any other module will be silently ignored.
func (s *SyncState) RemoveEphemeralOutputValue(addr addrs.AbsOutputValue) {
if !addr.Module.IsRoot() {
return
}
defer s.beginWrite()()
s.state.RemoveEphemeralOutputValue(addr)
}
// Resource returns a snapshot of the state of the resource with the given
// address, or nil if no such resource is tracked.
//

@ -12273,7 +12273,6 @@ output "out" {
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
got := state.RootOutputValues["out"].Value
want := cty.ObjectVal(map[string]cty.Value{
"required": cty.StringVal("boop"),

@ -817,6 +817,7 @@ func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.Sour
Addr: addr.Absolute(d.ModulePath),
Value: cty.NilVal,
Sensitive: config.Sensitive,
Ephemeral: config.Ephemeral,
}
} else if output.Value == cty.NilVal || output.Value.IsNull() {
// Then we did get a value but Terraform itself thought it was NilVal
@ -828,6 +829,9 @@ func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.Sour
if output.Sensitive {
val = val.Mark(marks.Sensitive)
}
if output.Ephemeral {
val = val.Mark(marks.Ephemeral)
}
return val, diags
}

@ -10,6 +10,8 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"maps"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
@ -627,6 +629,7 @@ func (n *NodeDestroyableOutput) Execute(ctx EvalContext, op walkOperation) tfdia
if n.Addr.Module.IsRoot() && mod != nil {
s := state.Lock()
rootOutputs := s.RootOutputValues
maps.Copy(rootOutputs, s.EphemeralRootOutputValues)
if o, ok := rootOutputs[n.Addr.OutputValue.Name]; ok {
sensitiveBefore = o.Sensitive
before = o.Value
@ -750,7 +753,10 @@ func (n *NodeApplyableOutput) setValue(namedVals *namedvals.State, state *states
// Null outputs must be saved for modules so that they can still be
// evaluated. Null root outputs are removed entirely, which is always fine
// because they can't be referenced by anything else in the configuration.
if n.Addr.Module.IsRoot() && val.IsNull() {
//
// This does not apply to ephemeral outputs, which always have a value of
// null in the state file.
if n.Addr.Module.IsRoot() && val.IsNull() && !n.Config.Ephemeral {
log.Printf("[TRACE] setValue: Removing %s from state (it is now null)", n.Addr)
state.RemoveOutputValue(n.Addr)
return
@ -770,24 +776,26 @@ func (n *NodeApplyableOutput) setValue(namedVals *namedvals.State, state *states
}
// Non-ephemeral output values get saved in the state too
if !n.Config.Ephemeral {
// The state itself doesn't represent unknown values, so we null them
// out here and then we'll save the real unknown value in the planned
// changeset, if we have one on this graph walk.
log.Printf("[TRACE] setValue: Saving value for %s in state", n.Addr)
// non-root outputs need to keep sensitive marks for evaluation, but are
// not serialized.
if n.Addr.Module.IsRoot() {
val, _ = val.UnmarkDeep()
if deferred.DependenciesDeferred(n.Dependencies) {
// If the output is from deferred resources then we return a
// simple null value representing that the value is really
// unknown as the dependencies were not properly computed.
val = cty.NullVal(val.Type())
} else {
val = cty.UnknownAsNull(val)
}
// The state itself doesn't represent unknown values, so we null them
// out here and then we'll save the real unknown value in the planned
// changeset, if we have one on this graph walk.
log.Printf("[TRACE] setValue: Saving value for %s in state", n.Addr)
// non-root outputs need to keep sensitive marks for evaluation, but are
// not serialized.
if n.Addr.Module.IsRoot() {
val, _ = val.UnmarkDeep()
if deferred.DependenciesDeferred(n.Dependencies) {
// If the output is from deferred resources then we return a
// simple null value representing that the value is really
// unknown as the dependencies were not properly computed.
val = cty.NullVal(val.Type())
} else {
val = cty.UnknownAsNull(val)
}
}
if n.Config.Ephemeral {
state.SetEphemeralOutputValue(n.Addr, val, n.Config.Sensitive)
} else {
state.SetOutputValue(n.Addr, val, n.Config.Sensitive)
}
}

Loading…
Cancel
Save