| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package planfile |
| |
| import ( |
| "fmt" |
| "io" |
| "slices" |
| "time" |
| |
| "github.com/zclconf/go-cty/cty" |
| "google.golang.org/protobuf/proto" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/checks" |
| "github.com/hashicorp/terraform/internal/collections" |
| "github.com/hashicorp/terraform/internal/lang" |
| "github.com/hashicorp/terraform/internal/lang/globalref" |
| "github.com/hashicorp/terraform/internal/plans" |
| "github.com/hashicorp/terraform/internal/plans/planproto" |
| "github.com/hashicorp/terraform/internal/providers" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/hashicorp/terraform/version" |
| ) |
| |
| const tfplanFormatVersion = 3 |
| const tfplanFilename = "tfplan" |
| |
| // --------------------------------------------------------------------------- |
| // This file deals with the internal structure of the "tfplan" sub-file within |
| // the plan file format. It's all private API, wrapped by methods defined |
| // elsewhere. This is the only file that should import the |
| // ../internal/planproto package, which contains the ugly stubs generated |
| // by the protobuf compiler. |
| // --------------------------------------------------------------------------- |
| |
| // readTfplan reads a protobuf-encoded description from the plan portion of |
| // a plan file, which is stored in a special file in the archive called |
| // "tfplan". |
| func readTfplan(r io.Reader) (*plans.Plan, error) { |
| src, err := io.ReadAll(r) |
| if err != nil { |
| return nil, err |
| } |
| |
| var rawPlan planproto.Plan |
| err = proto.Unmarshal(src, &rawPlan) |
| if err != nil { |
| return nil, fmt.Errorf("parse error: %s", err) |
| } |
| |
| if rawPlan.Version != tfplanFormatVersion { |
| return nil, fmt.Errorf("unsupported plan file format version %d; only version %d is supported", rawPlan.Version, tfplanFormatVersion) |
| } |
| |
| if rawPlan.TerraformVersion != version.String() { |
| return nil, fmt.Errorf("plan file was created by Terraform %s, but this is %s; plan files cannot be transferred between different Terraform versions", rawPlan.TerraformVersion, version.String()) |
| } |
| |
| plan := &plans.Plan{ |
| VariableValues: map[string]plans.DynamicValue{}, |
| Changes: &plans.ChangesSrc{ |
| Outputs: []*plans.OutputChangeSrc{}, |
| Resources: []*plans.ResourceInstanceChangeSrc{}, |
| }, |
| DriftedResources: []*plans.ResourceInstanceChangeSrc{}, |
| DeferredResources: []*plans.DeferredResourceInstanceChangeSrc{}, |
| Checks: &states.CheckResults{}, |
| } |
| |
| plan.Applyable = rawPlan.Applyable |
| plan.Complete = rawPlan.Complete |
| plan.Errored = rawPlan.Errored |
| |
| plan.UIMode, err = planproto.FromMode(rawPlan.UiMode) |
| if err != nil { |
| return nil, err |
| } |
| |
| for _, rawOC := range rawPlan.OutputChanges { |
| name := rawOC.Name |
| change, err := changeFromTfplan(rawOC.Change) |
| if err != nil { |
| return nil, fmt.Errorf("invalid plan for output %q: %s", name, err) |
| } |
| |
| plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{ |
| // All output values saved in the plan file are root module outputs, |
| // since we don't retain others. (They can be easily recomputed |
| // during apply). |
| Addr: addrs.OutputValue{Name: name}.Absolute(addrs.RootModuleInstance), |
| ChangeSrc: *change, |
| Sensitive: rawOC.Sensitive, |
| }) |
| } |
| |
| checkResults, err := CheckResultsFromPlanProto(rawPlan.CheckResults) |
| if err != nil { |
| return nil, fmt.Errorf("failed to decode check results: %s", err) |
| } |
| plan.Checks = checkResults |
| |
| for _, rawRC := range rawPlan.ResourceChanges { |
| change, err := resourceChangeFromTfplan(rawRC, addrs.ParseAbsResourceInstanceStr) |
| if err != nil { |
| // errors from resourceChangeFromTfplan already include context |
| return nil, err |
| } |
| |
| plan.Changes.Resources = append(plan.Changes.Resources, change) |
| } |
| |
| for _, rawRC := range rawPlan.ResourceDrift { |
| change, err := resourceChangeFromTfplan(rawRC, addrs.ParseAbsResourceInstanceStr) |
| if err != nil { |
| // errors from resourceChangeFromTfplan already include context |
| return nil, err |
| } |
| |
| plan.DriftedResources = append(plan.DriftedResources, change) |
| } |
| |
| for _, rawDC := range rawPlan.DeferredChanges { |
| change, err := deferredChangeFromTfplan(rawDC) |
| if err != nil { |
| return nil, err |
| } |
| |
| plan.DeferredResources = append(plan.DeferredResources, change) |
| } |
| |
| for _, rawRA := range rawPlan.RelevantAttributes { |
| ra, err := resourceAttrFromTfplan(rawRA) |
| if err != nil { |
| return nil, err |
| } |
| plan.RelevantAttributes = append(plan.RelevantAttributes, ra) |
| } |
| |
| for _, rawTargetAddr := range rawPlan.TargetAddrs { |
| target, diags := addrs.ParseTargetStr(rawTargetAddr) |
| if diags.HasErrors() { |
| return nil, fmt.Errorf("plan contains invalid target address %q: %s", target, diags.Err()) |
| } |
| plan.TargetAddrs = append(plan.TargetAddrs, target.Subject) |
| } |
| |
| for _, rawReplaceAddr := range rawPlan.ForceReplaceAddrs { |
| addr, diags := addrs.ParseAbsResourceInstanceStr(rawReplaceAddr) |
| if diags.HasErrors() { |
| return nil, fmt.Errorf("plan contains invalid force-replace address %q: %s", addr, diags.Err()) |
| } |
| plan.ForceReplaceAddrs = append(plan.ForceReplaceAddrs, addr) |
| } |
| |
| for name, rawVal := range rawPlan.Variables { |
| val, err := valueFromTfplan(rawVal) |
| if err != nil { |
| return nil, fmt.Errorf("invalid value for input variable %q: %s", name, err) |
| } |
| plan.VariableValues[name] = val |
| } |
| |
| if len(rawPlan.ApplyTimeVariables) != 0 { |
| plan.ApplyTimeVariables = collections.NewSetCmp[string]() |
| for _, name := range rawPlan.ApplyTimeVariables { |
| plan.ApplyTimeVariables.Add(name) |
| } |
| } |
| |
| for _, hash := range rawPlan.FunctionResults { |
| plan.FunctionResults = append(plan.FunctionResults, |
| lang.FunctionResultHash{ |
| Key: hash.Key, |
| Result: hash.Result, |
| }, |
| ) |
| } |
| |
| switch { |
| case rawPlan.Backend == nil && rawPlan.StateStore == nil: |
| // Similar validation in writeTfPlan should prevent this occurring |
| return nil, |
| fmt.Errorf("plan file has neither backend nor state_store settings; one of these settings is required. This is a bug in Terraform and should be reported.") |
| case rawPlan.Backend != nil && rawPlan.StateStore != nil: |
| // Similar validation in writeTfPlan should prevent this occurring |
| return nil, |
| fmt.Errorf("plan file contains both backend and state_store settings when only one of these settings should be set. This is a bug in Terraform and should be reported.") |
| case rawPlan.Backend != nil: |
| rawBackend := rawPlan.Backend |
| config, err := valueFromTfplan(rawBackend.Config) |
| if err != nil { |
| return nil, fmt.Errorf("plan file has invalid backend configuration: %s", err) |
| } |
| plan.Backend = plans.Backend{ |
| Type: rawBackend.Type, |
| Config: config, |
| Workspace: rawBackend.Workspace, |
| } |
| case rawPlan.StateStore != nil: |
| rawStateStore := rawPlan.StateStore |
| config, err := valueFromTfplan(rawStateStore.Config) |
| if err != nil { |
| return nil, fmt.Errorf("plan file has invalid state_store configuration: %s", err) |
| } |
| provider := &plans.Provider{} |
| err = provider.SetSource(rawStateStore.Provider.Source) |
| if err != nil { |
| return nil, fmt.Errorf("plan file has invalid state_store provider source: %s", err) |
| } |
| err = provider.SetVersion(rawStateStore.Provider.Version) |
| if err != nil { |
| return nil, fmt.Errorf("plan file has invalid state_store provider version: %s", err) |
| } |
| |
| plan.StateStore = plans.StateStore{ |
| Type: rawStateStore.Type, |
| Provider: provider, |
| Config: config, |
| Workspace: rawStateStore.Workspace, |
| } |
| } |
| |
| if plan.Timestamp, err = time.Parse(time.RFC3339, rawPlan.Timestamp); err != nil { |
| return nil, fmt.Errorf("invalid value for timestamp %s: %s", rawPlan.Timestamp, err) |
| } |
| |
| return plan, nil |
| } |
| |
| // ResourceChangeFromProto decodes an isolated resource instance change from |
| // its representation as a protocol buffers message. |
| // |
| // This is used by the stackplan package, which includes planproto messages |
| // in its own wire format while using a different overall container. |
| func ResourceChangeFromProto(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) { |
| return resourceChangeFromTfplan(rawChange, addrs.ParseAbsResourceInstanceStr) |
| } |
| |
| // DeferredResourceChangeFromProto decodes an isolated deferred resource |
| // instance change from its representation as a protocol buffers message. |
| // |
| // This the same as ResourceChangeFromProto but internally allows for splat |
| // addresses, which are not allowed outside deferred changes. |
| func DeferredResourceChangeFromProto(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) { |
| return resourceChangeFromTfplan(rawChange, addrs.ParsePartialResourceInstanceStr) |
| } |
| |
| func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange, parseAddr func(str string) (addrs.AbsResourceInstance, tfdiags.Diagnostics)) (*plans.ResourceInstanceChangeSrc, error) { |
| if rawChange == nil { |
| // Should never happen in practice, since protobuf can't represent |
| // a nil value in a list. |
| return nil, fmt.Errorf("resource change object is absent") |
| } |
| |
| ret := &plans.ResourceInstanceChangeSrc{} |
| |
| if rawChange.Addr == "" { |
| // If "Addr" isn't populated then seems likely that this is a plan |
| // file created by an earlier version of Terraform, which had the |
| // same information spread over various other fields: |
| // ModulePath, Mode, Name, Type, and InstanceKey. |
| return nil, fmt.Errorf("no instance address for resource instance change; perhaps this plan was created by a different version of Terraform?") |
| } |
| |
| instAddr, diags := parseAddr(rawChange.Addr) |
| if diags.HasErrors() { |
| return nil, fmt.Errorf("invalid resource instance address %q: %w", rawChange.Addr, diags.Err()) |
| } |
| prevRunAddr := instAddr |
| if rawChange.PrevRunAddr != "" { |
| prevRunAddr, diags = parseAddr(rawChange.PrevRunAddr) |
| if diags.HasErrors() { |
| return nil, fmt.Errorf("invalid resource instance previous run address %q: %w", rawChange.PrevRunAddr, diags.Err()) |
| } |
| } |
| |
| providerAddr, diags := addrs.ParseAbsProviderConfigStr(rawChange.Provider) |
| if diags.HasErrors() { |
| return nil, diags.Err() |
| } |
| ret.ProviderAddr = providerAddr |
| |
| ret.Addr = instAddr |
| ret.PrevRunAddr = prevRunAddr |
| |
| if rawChange.DeposedKey != "" { |
| if len(rawChange.DeposedKey) != 8 { |
| return nil, fmt.Errorf("deposed object for %s has invalid deposed key %q", ret.Addr, rawChange.DeposedKey) |
| } |
| ret.DeposedKey = states.DeposedKey(rawChange.DeposedKey) |
| } |
| |
| ret.RequiredReplace = cty.NewPathSet() |
| for _, p := range rawChange.RequiredReplace { |
| path, err := pathFromTfplan(p) |
| if err != nil { |
| return nil, fmt.Errorf("invalid path in required replace: %s", err) |
| } |
| ret.RequiredReplace.Add(path) |
| } |
| |
| change, err := changeFromTfplan(rawChange.Change) |
| if err != nil { |
| return nil, fmt.Errorf("invalid plan for resource %s: %s", ret.Addr, err) |
| } |
| |
| ret.ChangeSrc = *change |
| |
| switch rawChange.ActionReason { |
| case planproto.ResourceInstanceActionReason_NONE: |
| ret.ActionReason = plans.ResourceInstanceChangeNoReason |
| case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE: |
| ret.ActionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate |
| case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED: |
| ret.ActionReason = plans.ResourceInstanceReplaceBecauseTainted |
| case planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST: |
| ret.ActionReason = plans.ResourceInstanceReplaceByRequest |
| case planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS: |
| ret.ActionReason = plans.ResourceInstanceReplaceByTriggers |
| case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG: |
| ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoResourceConfig |
| case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION: |
| ret.ActionReason = plans.ResourceInstanceDeleteBecauseWrongRepetition |
| case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX: |
| ret.ActionReason = plans.ResourceInstanceDeleteBecauseCountIndex |
| case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY: |
| ret.ActionReason = plans.ResourceInstanceDeleteBecauseEachKey |
| case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE: |
| ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoModule |
| case planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN: |
| ret.ActionReason = plans.ResourceInstanceReadBecauseConfigUnknown |
| case planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING: |
| ret.ActionReason = plans.ResourceInstanceReadBecauseDependencyPending |
| case planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED: |
| ret.ActionReason = plans.ResourceInstanceReadBecauseCheckNested |
| case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET: |
| ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoMoveTarget |
| default: |
| return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason) |
| } |
| |
| if len(rawChange.Private) != 0 { |
| ret.Private = rawChange.Private |
| } |
| |
| return ret, nil |
| } |
| |
| // ActionFromProto translates from the protobuf representation of change actions |
| // into the "plans" package's representation, or returns an error if the |
| // given action is unrecognized. |
| func ActionFromProto(rawAction planproto.Action) (plans.Action, error) { |
| switch rawAction { |
| case planproto.Action_NOOP: |
| return plans.NoOp, nil |
| case planproto.Action_CREATE: |
| return plans.Create, nil |
| case planproto.Action_READ: |
| return plans.Read, nil |
| case planproto.Action_UPDATE: |
| return plans.Update, nil |
| case planproto.Action_DELETE: |
| return plans.Delete, nil |
| case planproto.Action_CREATE_THEN_DELETE: |
| return plans.CreateThenDelete, nil |
| case planproto.Action_DELETE_THEN_CREATE: |
| return plans.DeleteThenCreate, nil |
| case planproto.Action_FORGET: |
| return plans.Forget, nil |
| case planproto.Action_CREATE_THEN_FORGET: |
| return plans.CreateThenForget, nil |
| default: |
| return plans.NoOp, fmt.Errorf("invalid change action %s", rawAction) |
| } |
| |
| } |
| |
| func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) { |
| if rawChange == nil { |
| return nil, fmt.Errorf("change object is absent") |
| } |
| |
| ret := &plans.ChangeSrc{} |
| |
| // -1 indicates that there is no index. We'll customize these below |
| // depending on the change action, and then decode. |
| beforeIdx, afterIdx := -1, -1 |
| |
| var err error |
| ret.Action, err = ActionFromProto(rawChange.Action) |
| if err != nil { |
| return nil, err |
| } |
| |
| switch ret.Action { |
| case plans.NoOp: |
| beforeIdx = 0 |
| afterIdx = 0 |
| case plans.Create: |
| afterIdx = 0 |
| case plans.Read: |
| beforeIdx = 0 |
| afterIdx = 1 |
| case plans.Update: |
| beforeIdx = 0 |
| afterIdx = 1 |
| case plans.Delete: |
| beforeIdx = 0 |
| case plans.CreateThenDelete: |
| beforeIdx = 0 |
| afterIdx = 1 |
| case plans.DeleteThenCreate: |
| beforeIdx = 0 |
| afterIdx = 1 |
| case plans.Forget: |
| beforeIdx = 0 |
| case plans.CreateThenForget: |
| beforeIdx = 0 |
| afterIdx = 1 |
| default: |
| return nil, fmt.Errorf("invalid change action %s", rawChange.Action) |
| } |
| |
| if beforeIdx != -1 { |
| if l := len(rawChange.Values); l <= beforeIdx { |
| return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action) |
| } |
| var err error |
| ret.Before, err = valueFromTfplan(rawChange.Values[beforeIdx]) |
| if err != nil { |
| return nil, fmt.Errorf("invalid \"before\" value: %s", err) |
| } |
| if ret.Before == nil { |
| return nil, fmt.Errorf("missing \"before\" value: %s", err) |
| } |
| } |
| if afterIdx != -1 { |
| if l := len(rawChange.Values); l <= afterIdx { |
| return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action) |
| } |
| var err error |
| ret.After, err = valueFromTfplan(rawChange.Values[afterIdx]) |
| if err != nil { |
| return nil, fmt.Errorf("invalid \"after\" value: %s", err) |
| } |
| if ret.After == nil { |
| return nil, fmt.Errorf("missing \"after\" value: %s", err) |
| } |
| } |
| |
| if rawChange.Importing != nil { |
| var identity plans.DynamicValue |
| if rawChange.Importing.Identity != nil { |
| var err error |
| identity, err = valueFromTfplan(rawChange.Importing.Identity) |
| if err != nil { |
| return nil, fmt.Errorf("invalid \"identity\" value: %s", err) |
| } |
| } |
| ret.Importing = &plans.ImportingSrc{ |
| ID: rawChange.Importing.Id, |
| Unknown: rawChange.Importing.Unknown, |
| Identity: identity, |
| } |
| } |
| ret.GeneratedConfig = rawChange.GeneratedConfig |
| |
| beforeValSensitiveAttrs, err := pathsFromTfplan(rawChange.BeforeSensitivePaths) |
| if err != nil { |
| return nil, fmt.Errorf("failed to decode before sensitive paths: %s", err) |
| } |
| afterValSensitiveAttrs, err := pathsFromTfplan(rawChange.AfterSensitivePaths) |
| if err != nil { |
| return nil, fmt.Errorf("failed to decode after sensitive paths: %s", err) |
| } |
| if len(beforeValSensitiveAttrs) > 0 { |
| ret.BeforeSensitivePaths = beforeValSensitiveAttrs |
| } |
| if len(afterValSensitiveAttrs) > 0 { |
| ret.AfterSensitivePaths = afterValSensitiveAttrs |
| } |
| |
| if rawChange.BeforeIdentity != nil { |
| beforeIdentity, err := valueFromTfplan(rawChange.BeforeIdentity) |
| if err != nil { |
| return nil, fmt.Errorf("failed to decode before identity: %s", err) |
| } |
| ret.BeforeIdentity = beforeIdentity |
| } |
| if rawChange.AfterIdentity != nil { |
| afterIdentity, err := valueFromTfplan(rawChange.AfterIdentity) |
| if err != nil { |
| return nil, fmt.Errorf("failed to decode after identity: %s", err) |
| } |
| ret.AfterIdentity = afterIdentity |
| } |
| |
| return ret, nil |
| } |
| |
| func valueFromTfplan(rawV *planproto.DynamicValue) (plans.DynamicValue, error) { |
| if len(rawV.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf |
| return nil, fmt.Errorf("dynamic value does not have msgpack serialization") |
| } |
| |
| return plans.DynamicValue(rawV.Msgpack), nil |
| } |
| |
| func deferredChangeFromTfplan(dc *planproto.DeferredResourceInstanceChange) (*plans.DeferredResourceInstanceChangeSrc, error) { |
| if dc == nil { |
| return nil, fmt.Errorf("deferred change object is absent") |
| } |
| |
| change, err := resourceChangeFromTfplan(dc.Change, addrs.ParsePartialResourceInstanceStr) |
| if err != nil { |
| return nil, err |
| } |
| |
| reason, err := DeferredReasonFromProto(dc.Deferred.Reason) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &plans.DeferredResourceInstanceChangeSrc{ |
| DeferredReason: reason, |
| ChangeSrc: change, |
| }, nil |
| } |
| |
| func DeferredReasonFromProto(reason planproto.DeferredReason) (providers.DeferredReason, error) { |
| switch reason { |
| case planproto.DeferredReason_INSTANCE_COUNT_UNKNOWN: |
| return providers.DeferredReasonInstanceCountUnknown, nil |
| case planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN: |
| return providers.DeferredReasonResourceConfigUnknown, nil |
| case planproto.DeferredReason_PROVIDER_CONFIG_UNKNOWN: |
| return providers.DeferredReasonProviderConfigUnknown, nil |
| case planproto.DeferredReason_ABSENT_PREREQ: |
| return providers.DeferredReasonAbsentPrereq, nil |
| case planproto.DeferredReason_DEFERRED_PREREQ: |
| return providers.DeferredReasonDeferredPrereq, nil |
| default: |
| return providers.DeferredReasonInvalid, fmt.Errorf("invalid deferred reason %s", reason) |
| } |
| } |
| |
| // writeTfplan serializes the given plan into the protobuf-based format used |
| // for the "tfplan" portion of a plan file. |
| func writeTfplan(plan *plans.Plan, w io.Writer) error { |
| if plan == nil { |
| return fmt.Errorf("cannot write plan file for nil plan") |
| } |
| if plan.Changes == nil { |
| return fmt.Errorf("cannot write plan file with nil changeset") |
| } |
| |
| rawPlan := &planproto.Plan{ |
| Version: tfplanFormatVersion, |
| TerraformVersion: version.String(), |
| |
| Variables: map[string]*planproto.DynamicValue{}, |
| OutputChanges: []*planproto.OutputChange{}, |
| CheckResults: []*planproto.CheckResults{}, |
| ResourceChanges: []*planproto.ResourceInstanceChange{}, |
| ResourceDrift: []*planproto.ResourceInstanceChange{}, |
| DeferredChanges: []*planproto.DeferredResourceInstanceChange{}, |
| } |
| |
| rawPlan.Applyable = plan.Applyable |
| rawPlan.Complete = plan.Complete |
| rawPlan.Errored = plan.Errored |
| |
| var err error |
| rawPlan.UiMode, err = planproto.NewMode(plan.UIMode) |
| if err != nil { |
| return err |
| } |
| |
| for _, oc := range plan.Changes.Outputs { |
| // When serializing a plan we only retain the root outputs, since |
| // changes to these are externally-visible side effects (e.g. via |
| // terraform_remote_state). |
| if !oc.Addr.Module.IsRoot() { |
| continue |
| } |
| |
| name := oc.Addr.OutputValue.Name |
| |
| // Writing outputs as cty.DynamicPseudoType forces the stored values |
| // to also contain dynamic type information, so we can recover the |
| // original type when we read the values back in readTFPlan. |
| protoChange, err := changeToTfplan(&oc.ChangeSrc) |
| if err != nil { |
| return fmt.Errorf("cannot write output value %q: %s", name, err) |
| } |
| |
| rawPlan.OutputChanges = append(rawPlan.OutputChanges, &planproto.OutputChange{ |
| Name: name, |
| Change: protoChange, |
| Sensitive: oc.Sensitive, |
| }) |
| } |
| |
| checkResults, err := CheckResultsToPlanProto(plan.Checks) |
| if err != nil { |
| return fmt.Errorf("failed to encode check results: %s", err) |
| } |
| rawPlan.CheckResults = checkResults |
| |
| for _, rc := range plan.Changes.Resources { |
| rawRC, err := resourceChangeToTfplan(rc) |
| if err != nil { |
| return err |
| } |
| rawPlan.ResourceChanges = append(rawPlan.ResourceChanges, rawRC) |
| } |
| |
| for _, rc := range plan.DriftedResources { |
| rawRC, err := resourceChangeToTfplan(rc) |
| if err != nil { |
| return err |
| } |
| rawPlan.ResourceDrift = append(rawPlan.ResourceDrift, rawRC) |
| } |
| |
| for _, dc := range plan.DeferredResources { |
| rawDC, err := deferredChangeToTfplan(dc) |
| if err != nil { |
| return err |
| } |
| rawPlan.DeferredChanges = append(rawPlan.DeferredChanges, rawDC) |
| } |
| |
| for _, ra := range plan.RelevantAttributes { |
| rawRA, err := resourceAttrToTfplan(ra) |
| if err != nil { |
| return err |
| } |
| rawPlan.RelevantAttributes = append(rawPlan.RelevantAttributes, rawRA) |
| } |
| |
| for _, targetAddr := range plan.TargetAddrs { |
| rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String()) |
| } |
| |
| for _, replaceAddr := range plan.ForceReplaceAddrs { |
| rawPlan.ForceReplaceAddrs = append(rawPlan.ForceReplaceAddrs, replaceAddr.String()) |
| } |
| |
| for name, val := range plan.VariableValues { |
| rawPlan.Variables[name] = valueToTfplan(val) |
| } |
| if plan.ApplyTimeVariables.Len() != 0 { |
| rawPlan.ApplyTimeVariables = slices.Collect(plan.ApplyTimeVariables.All()) |
| } |
| |
| for _, hash := range plan.FunctionResults { |
| rawPlan.FunctionResults = append(rawPlan.FunctionResults, |
| &planproto.FunctionCallHash{ |
| Key: hash.Key, |
| Result: hash.Result, |
| }, |
| ) |
| } |
| |
| // Store details about accessing state |
| backendInUse := plan.Backend.Type != "" && plan.Backend.Config != nil |
| stateStoreInUse := plan.StateStore.Type != "" && plan.StateStore.Config != nil |
| switch { |
| case !backendInUse && !stateStoreInUse: |
| // This suggests a bug in the code that created the plan, since it |
| // ought to always have either a backend or state_store populated, even if it's the default |
| // "local" backend with a local state file. |
| return fmt.Errorf("plan does not have a backend or state_store configuration") |
| case backendInUse && stateStoreInUse: |
| // This suggests a bug in the code that created the plan, since it |
| // should never have both a backend and state_store populated. |
| return fmt.Errorf("plan contains both backend and state_store configurations, only one is expected") |
| case backendInUse: |
| rawPlan.Backend = &planproto.Backend{ |
| Type: plan.Backend.Type, |
| Config: valueToTfplan(plan.Backend.Config), |
| Workspace: plan.Backend.Workspace, |
| } |
| case stateStoreInUse: |
| rawPlan.StateStore = &planproto.StateStore{ |
| Type: plan.StateStore.Type, |
| Provider: &planproto.Provider{ |
| Version: plan.StateStore.Provider.Version.String(), |
| Source: plan.StateStore.Provider.Source.String(), |
| }, |
| Config: valueToTfplan(plan.StateStore.Config), |
| Workspace: plan.StateStore.Workspace, |
| } |
| } |
| |
| rawPlan.Timestamp = plan.Timestamp.Format(time.RFC3339) |
| |
| src, err := proto.Marshal(rawPlan) |
| if err != nil { |
| return fmt.Errorf("serialization error: %s", err) |
| } |
| |
| _, err = w.Write(src) |
| if err != nil { |
| return fmt.Errorf("failed to write plan to plan file: %s", err) |
| } |
| |
| return nil |
| } |
| |
| func resourceAttrToTfplan(ra globalref.ResourceAttr) (*planproto.PlanResourceAttr, error) { |
| res := &planproto.PlanResourceAttr{} |
| |
| res.Resource = ra.Resource.String() |
| attr, err := pathToTfplan(ra.Attr) |
| if err != nil { |
| return res, err |
| } |
| res.Attr = attr |
| return res, nil |
| } |
| |
| func resourceAttrFromTfplan(ra *planproto.PlanResourceAttr) (globalref.ResourceAttr, error) { |
| var res globalref.ResourceAttr |
| if ra.Resource == "" { |
| return res, fmt.Errorf("missing resource address from relevant attribute") |
| } |
| |
| instAddr, diags := addrs.ParseAbsResourceInstanceStr(ra.Resource) |
| if diags.HasErrors() { |
| return res, fmt.Errorf("invalid resource instance address %q in relevant attributes: %w", ra.Resource, diags.Err()) |
| } |
| |
| res.Resource = instAddr |
| path, err := pathFromTfplan(ra.Attr) |
| if err != nil { |
| return res, fmt.Errorf("invalid path in %q relevant attribute: %s", res.Resource, err) |
| } |
| |
| res.Attr = path |
| return res, nil |
| } |
| |
| // ResourceChangeToProto encodes an isolated resource instance change into |
| // its representation as a protocol buffers message. |
| // |
| // This is used by the stackplan package, which includes planproto messages |
| // in its own wire format while using a different overall container. |
| func ResourceChangeToProto(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) { |
| if change == nil { |
| // We assume this represents the absense of a change, then. |
| return nil, nil |
| } |
| return resourceChangeToTfplan(change) |
| } |
| |
| func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) { |
| ret := &planproto.ResourceInstanceChange{} |
| |
| if change.PrevRunAddr.Resource.Resource.Type == "" { |
| // Suggests that an old caller wasn't yet updated to populate this |
| // properly. All code that generates plans should populate this field, |
| // even if it's just to write in the same value as in change.Addr. |
| change.PrevRunAddr = change.Addr |
| } |
| |
| ret.Addr = change.Addr.String() |
| ret.PrevRunAddr = change.PrevRunAddr.String() |
| if ret.PrevRunAddr == ret.Addr { |
| // In the on-disk format we leave PrevRunAddr unpopulated in the common |
| // case where it's the same as Addr, and then fill it back in again on |
| // read. |
| ret.PrevRunAddr = "" |
| } |
| |
| ret.DeposedKey = string(change.DeposedKey) |
| ret.Provider = change.ProviderAddr.String() |
| |
| requiredReplace := change.RequiredReplace.List() |
| ret.RequiredReplace = make([]*planproto.Path, 0, len(requiredReplace)) |
| for _, p := range requiredReplace { |
| path, err := pathToTfplan(p) |
| if err != nil { |
| return nil, fmt.Errorf("invalid path in required replace: %s", err) |
| } |
| ret.RequiredReplace = append(ret.RequiredReplace, path) |
| } |
| |
| valChange, err := changeToTfplan(&change.ChangeSrc) |
| if err != nil { |
| return nil, fmt.Errorf("failed to serialize resource %s change: %s", change.Addr, err) |
| } |
| ret.Change = valChange |
| |
| switch change.ActionReason { |
| case plans.ResourceInstanceChangeNoReason: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_NONE |
| case plans.ResourceInstanceReplaceBecauseCannotUpdate: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE |
| case plans.ResourceInstanceReplaceBecauseTainted: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED |
| case plans.ResourceInstanceReplaceByRequest: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST |
| case plans.ResourceInstanceReplaceByTriggers: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS |
| case plans.ResourceInstanceDeleteBecauseNoResourceConfig: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG |
| case plans.ResourceInstanceDeleteBecauseWrongRepetition: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION |
| case plans.ResourceInstanceDeleteBecauseCountIndex: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX |
| case plans.ResourceInstanceDeleteBecauseEachKey: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY |
| case plans.ResourceInstanceDeleteBecauseNoModule: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE |
| case plans.ResourceInstanceReadBecauseConfigUnknown: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN |
| case plans.ResourceInstanceReadBecauseDependencyPending: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING |
| case plans.ResourceInstanceReadBecauseCheckNested: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED |
| case plans.ResourceInstanceDeleteBecauseNoMoveTarget: |
| ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET |
| default: |
| return nil, fmt.Errorf("resource %s has unsupported action reason %s", change.Addr, change.ActionReason) |
| } |
| |
| if len(change.Private) > 0 { |
| ret.Private = change.Private |
| } |
| |
| return ret, nil |
| } |
| |
| // ActionToProto translates from the "plans" package's representation of change |
| // actions into the protobuf representation, or returns an error if the |
| // given action is unrecognized. |
| func ActionToProto(action plans.Action) (planproto.Action, error) { |
| switch action { |
| case plans.NoOp: |
| return planproto.Action_NOOP, nil |
| case plans.Create: |
| return planproto.Action_CREATE, nil |
| case plans.Read: |
| return planproto.Action_READ, nil |
| case plans.Update: |
| return planproto.Action_UPDATE, nil |
| case plans.Delete: |
| return planproto.Action_DELETE, nil |
| case plans.DeleteThenCreate: |
| return planproto.Action_DELETE_THEN_CREATE, nil |
| case plans.CreateThenDelete: |
| return planproto.Action_CREATE_THEN_DELETE, nil |
| case plans.Forget: |
| return planproto.Action_FORGET, nil |
| case plans.CreateThenForget: |
| return planproto.Action_CREATE_THEN_FORGET, nil |
| default: |
| return planproto.Action_NOOP, fmt.Errorf("invalid change action %s", action) |
| } |
| } |
| |
| func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) { |
| ret := &planproto.Change{} |
| |
| before := valueToTfplan(change.Before) |
| after := valueToTfplan(change.After) |
| |
| beforeSensitivePaths, err := pathsToTfplan(change.BeforeSensitivePaths) |
| if err != nil { |
| return nil, err |
| } |
| afterSensitivePaths, err := pathsToTfplan(change.AfterSensitivePaths) |
| if err != nil { |
| return nil, err |
| } |
| ret.BeforeSensitivePaths = beforeSensitivePaths |
| ret.AfterSensitivePaths = afterSensitivePaths |
| |
| if change.Importing != nil { |
| var identity *planproto.DynamicValue |
| if change.Importing.Identity != nil { |
| identity = planproto.NewPlanDynamicValue(change.Importing.Identity) |
| } |
| ret.Importing = &planproto.Importing{ |
| Id: change.Importing.ID, |
| Unknown: change.Importing.Unknown, |
| Identity: identity, |
| } |
| |
| } |
| ret.GeneratedConfig = change.GeneratedConfig |
| |
| if change.BeforeIdentity != nil { |
| ret.BeforeIdentity = planproto.NewPlanDynamicValue(change.BeforeIdentity) |
| } |
| if change.AfterIdentity != nil { |
| ret.AfterIdentity = planproto.NewPlanDynamicValue(change.AfterIdentity) |
| } |
| |
| ret.Action, err = ActionToProto(change.Action) |
| if err != nil { |
| return nil, err |
| } |
| |
| switch ret.Action { |
| case planproto.Action_NOOP: |
| ret.Values = []*planproto.DynamicValue{before} // before and after should be identical |
| case planproto.Action_CREATE: |
| ret.Values = []*planproto.DynamicValue{after} |
| case planproto.Action_READ: |
| ret.Values = []*planproto.DynamicValue{before, after} |
| case planproto.Action_UPDATE: |
| ret.Values = []*planproto.DynamicValue{before, after} |
| case planproto.Action_DELETE: |
| ret.Values = []*planproto.DynamicValue{before} |
| case planproto.Action_DELETE_THEN_CREATE: |
| ret.Values = []*planproto.DynamicValue{before, after} |
| case planproto.Action_CREATE_THEN_DELETE: |
| ret.Values = []*planproto.DynamicValue{before, after} |
| case planproto.Action_FORGET: |
| ret.Values = []*planproto.DynamicValue{before} |
| case planproto.Action_CREATE_THEN_FORGET: |
| ret.Values = []*planproto.DynamicValue{before, after} |
| default: |
| return nil, fmt.Errorf("invalid change action %s", change.Action) |
| } |
| |
| return ret, nil |
| } |
| |
| func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue { |
| return planproto.NewPlanDynamicValue(val) |
| } |
| |
| func pathsFromTfplan(paths []*planproto.Path) ([]cty.Path, error) { |
| if len(paths) == 0 { |
| return nil, nil |
| } |
| ret := make([]cty.Path, 0, len(paths)) |
| for _, p := range paths { |
| path, err := pathFromTfplan(p) |
| if err != nil { |
| return nil, err |
| } |
| ret = append(ret, path) |
| } |
| return ret, nil |
| } |
| |
| func pathsToTfplan(paths []cty.Path) ([]*planproto.Path, error) { |
| if len(paths) == 0 { |
| return nil, nil |
| } |
| ret := make([]*planproto.Path, 0, len(paths)) |
| for _, p := range paths { |
| path, err := pathToTfplan(p) |
| if err != nil { |
| return nil, err |
| } |
| ret = append(ret, path) |
| } |
| return ret, nil |
| } |
| |
| // PathFromProto decodes a path to a nested attribute into a cty.Path for |
| // use in tracking marked values. |
| // |
| // This is used by the stackstate package, which uses planproto.Path messages |
| // while using a different overall container. |
| func PathFromProto(path *planproto.Path) (cty.Path, error) { |
| if path == nil { |
| return nil, nil |
| } |
| return pathFromTfplan(path) |
| } |
| |
| func pathFromTfplan(path *planproto.Path) (cty.Path, error) { |
| ret := make([]cty.PathStep, 0, len(path.Steps)) |
| for _, step := range path.Steps { |
| switch s := step.Selector.(type) { |
| case *planproto.Path_Step_ElementKey: |
| dynamicVal, err := valueFromTfplan(s.ElementKey) |
| if err != nil { |
| return nil, fmt.Errorf("error decoding path index step: %s", err) |
| } |
| ty, err := dynamicVal.ImpliedType() |
| if err != nil { |
| return nil, fmt.Errorf("error determining path index type: %s", err) |
| } |
| val, err := dynamicVal.Decode(ty) |
| if err != nil { |
| return nil, fmt.Errorf("error decoding path index value: %s", err) |
| } |
| ret = append(ret, cty.IndexStep{Key: val}) |
| case *planproto.Path_Step_AttributeName: |
| ret = append(ret, cty.GetAttrStep{Name: s.AttributeName}) |
| default: |
| return nil, fmt.Errorf("Unsupported path step %t", step.Selector) |
| } |
| } |
| return ret, nil |
| } |
| |
| func pathToTfplan(path cty.Path) (*planproto.Path, error) { |
| return planproto.NewPath(path) |
| } |
| |
| func deferredChangeToTfplan(dc *plans.DeferredResourceInstanceChangeSrc) (*planproto.DeferredResourceInstanceChange, error) { |
| change, err := resourceChangeToTfplan(dc.ChangeSrc) |
| if err != nil { |
| return nil, err |
| } |
| |
| reason, err := DeferredReasonToProto(dc.DeferredReason) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &planproto.DeferredResourceInstanceChange{ |
| Change: change, |
| Deferred: &planproto.Deferred{ |
| Reason: reason, |
| }, |
| }, nil |
| } |
| |
| func DeferredReasonToProto(reason providers.DeferredReason) (planproto.DeferredReason, error) { |
| switch reason { |
| case providers.DeferredReasonInstanceCountUnknown: |
| return planproto.DeferredReason_INSTANCE_COUNT_UNKNOWN, nil |
| case providers.DeferredReasonResourceConfigUnknown: |
| return planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN, nil |
| case providers.DeferredReasonProviderConfigUnknown: |
| return planproto.DeferredReason_PROVIDER_CONFIG_UNKNOWN, nil |
| case providers.DeferredReasonAbsentPrereq: |
| return planproto.DeferredReason_ABSENT_PREREQ, nil |
| case providers.DeferredReasonDeferredPrereq: |
| return planproto.DeferredReason_DEFERRED_PREREQ, nil |
| default: |
| return planproto.DeferredReason_INVALID, fmt.Errorf("invalid deferred reason %s", reason) |
| } |
| } |
| |
| // CheckResultsFromPlanProto decodes a slice of check results from their protobuf |
| // representation into the "states" package's representation. |
| // |
| // It's used by the stackplan package, which includes an identical representation |
| // of check results within a different overall container. |
| func CheckResultsFromPlanProto(proto []*planproto.CheckResults) (*states.CheckResults, error) { |
| configResults := addrs.MakeMap[addrs.ConfigCheckable, *states.CheckResultAggregate]() |
| |
| for _, rawCheckResults := range proto { |
| aggr := &states.CheckResultAggregate{} |
| switch rawCheckResults.Status { |
| case planproto.CheckResults_UNKNOWN: |
| aggr.Status = checks.StatusUnknown |
| case planproto.CheckResults_PASS: |
| aggr.Status = checks.StatusPass |
| case planproto.CheckResults_FAIL: |
| aggr.Status = checks.StatusFail |
| case planproto.CheckResults_ERROR: |
| aggr.Status = checks.StatusError |
| default: |
| return nil, |
| fmt.Errorf("aggregate check results for %s have unsupported status %#v", |
| rawCheckResults.ConfigAddr, rawCheckResults.Status) |
| } |
| |
| var objKind addrs.CheckableKind |
| switch rawCheckResults.Kind { |
| case planproto.CheckResults_RESOURCE: |
| objKind = addrs.CheckableResource |
| case planproto.CheckResults_OUTPUT_VALUE: |
| objKind = addrs.CheckableOutputValue |
| case planproto.CheckResults_CHECK: |
| objKind = addrs.CheckableCheck |
| case planproto.CheckResults_INPUT_VARIABLE: |
| objKind = addrs.CheckableInputVariable |
| default: |
| return nil, fmt.Errorf("aggregate check results for %s have unsupported object kind %s", |
| rawCheckResults.ConfigAddr, objKind) |
| } |
| |
| // Some trickiness here: we only have an address parser for |
| // addrs.Checkable and not for addrs.ConfigCheckable, but that's okay |
| // because once we have an addrs.Checkable we can always derive an |
| // addrs.ConfigCheckable from it, and a ConfigCheckable should always |
| // be the same syntax as a Checkable with no index information and |
| // thus we can reuse the same parser for both here. |
| configAddrProxy, diags := addrs.ParseCheckableStr(objKind, rawCheckResults.ConfigAddr) |
| if diags.HasErrors() { |
| return nil, diags.Err() |
| } |
| configAddr := configAddrProxy.ConfigCheckable() |
| if configAddr.String() != configAddrProxy.String() { |
| // This is how we catch if the config address included index |
| // information that would be allowed in a Checkable but not |
| // in a ConfigCheckable. |
| return nil, fmt.Errorf("invalid checkable config address %s", rawCheckResults.ConfigAddr) |
| } |
| |
| aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *states.CheckResultObject]() |
| for _, rawCheckResult := range rawCheckResults.Objects { |
| objectAddr, diags := addrs.ParseCheckableStr(objKind, rawCheckResult.ObjectAddr) |
| if diags.HasErrors() { |
| return nil, diags.Err() |
| } |
| if !addrs.Equivalent(objectAddr.ConfigCheckable(), configAddr) { |
| return nil, fmt.Errorf("checkable object %s should not be grouped under %s", objectAddr, configAddr) |
| } |
| |
| obj := &states.CheckResultObject{ |
| FailureMessages: rawCheckResult.FailureMessages, |
| } |
| switch rawCheckResult.Status { |
| case planproto.CheckResults_UNKNOWN: |
| obj.Status = checks.StatusUnknown |
| case planproto.CheckResults_PASS: |
| obj.Status = checks.StatusPass |
| case planproto.CheckResults_FAIL: |
| obj.Status = checks.StatusFail |
| case planproto.CheckResults_ERROR: |
| obj.Status = checks.StatusError |
| default: |
| return nil, fmt.Errorf("object check results for %s has unsupported status %#v", |
| rawCheckResult.ObjectAddr, rawCheckResult.Status) |
| } |
| |
| aggr.ObjectResults.Put(objectAddr, obj) |
| } |
| |
| // If we ended up with no elements in the map then we'll just nil it, |
| // primarily just to make life easier for our round-trip tests. |
| if aggr.ObjectResults.Len() == 0 { |
| aggr.ObjectResults.Elems = nil |
| } |
| |
| configResults.Put(configAddr, aggr) |
| } |
| |
| // If we ended up with no elements in the map then we'll just nil it, |
| // primarily just to make life easier for our round-trip tests. |
| if configResults.Len() == 0 { |
| configResults.Elems = nil |
| } |
| |
| return &states.CheckResults{ |
| ConfigResults: configResults, |
| }, nil |
| } |
| |
| // CheckResultsToPlanProto encodes a slice of check results from the "states" |
| // package's representation into their protobuf representation. |
| // |
| // It's used by the stackplan package, which includes identical representation |
| // of check results within a different overall container. |
| func CheckResultsToPlanProto(checkResults *states.CheckResults) ([]*planproto.CheckResults, error) { |
| if checkResults != nil { |
| protoResults := make([]*planproto.CheckResults, 0) |
| for _, configElem := range checkResults.ConfigResults.Elems { |
| crs := configElem.Value |
| pcrs := &planproto.CheckResults{ |
| ConfigAddr: configElem.Key.String(), |
| } |
| switch crs.Status { |
| case checks.StatusUnknown: |
| pcrs.Status = planproto.CheckResults_UNKNOWN |
| case checks.StatusPass: |
| pcrs.Status = planproto.CheckResults_PASS |
| case checks.StatusFail: |
| pcrs.Status = planproto.CheckResults_FAIL |
| case checks.StatusError: |
| pcrs.Status = planproto.CheckResults_ERROR |
| default: |
| return nil, |
| fmt.Errorf("checkable configuration %s has unsupported aggregate status %s", configElem.Key, crs.Status) |
| } |
| switch kind := configElem.Key.CheckableKind(); kind { |
| case addrs.CheckableResource: |
| pcrs.Kind = planproto.CheckResults_RESOURCE |
| case addrs.CheckableOutputValue: |
| pcrs.Kind = planproto.CheckResults_OUTPUT_VALUE |
| case addrs.CheckableCheck: |
| pcrs.Kind = planproto.CheckResults_CHECK |
| case addrs.CheckableInputVariable: |
| pcrs.Kind = planproto.CheckResults_INPUT_VARIABLE |
| default: |
| return nil, |
| fmt.Errorf("checkable configuration %s has unsupported object type kind %s", configElem.Key, kind) |
| } |
| |
| for _, objectElem := range configElem.Value.ObjectResults.Elems { |
| cr := objectElem.Value |
| pcr := &planproto.CheckResults_ObjectResult{ |
| ObjectAddr: objectElem.Key.String(), |
| FailureMessages: objectElem.Value.FailureMessages, |
| } |
| switch cr.Status { |
| case checks.StatusUnknown: |
| pcr.Status = planproto.CheckResults_UNKNOWN |
| case checks.StatusPass: |
| pcr.Status = planproto.CheckResults_PASS |
| case checks.StatusFail: |
| pcr.Status = planproto.CheckResults_FAIL |
| case checks.StatusError: |
| pcr.Status = planproto.CheckResults_ERROR |
| default: |
| return nil, |
| fmt.Errorf("checkable object %s has unsupported status %s", objectElem.Key, crs.Status) |
| } |
| pcrs.Objects = append(pcrs.Objects, pcr) |
| } |
| |
| protoResults = append(protoResults, pcrs) |
| } |
| |
| return protoResults, nil |
| } else { |
| return nil, nil |
| } |
| } |