plans/planfile: Reading and writing the new plan format
The new format is radically different in than the old in physical structure, but still has the same logical parts: the plan itself, a snapshot of the input configuration, and a snapshot of the state as it existed when the plan was created. Rather than creating plan-specific serializations of state and config, the new format instead leans on the existing file formats implemented elsewhere, wrapping the result up in a zip archive with some internal file naming conventions. The plan portion of the file is serialized with protobuf, consistent with our general strategy of replacing all use of encoding/gob with protobuf moving forward.
This commit is contained in:
parent
7357e7f734
commit
a2eb462f5d
|
@ -0,0 +1,7 @@
|
||||||
|
// Package planproto is home to the Go stubs generated from the tfplan protobuf
|
||||||
|
// schema.
|
||||||
|
//
|
||||||
|
// This is an internal package to be used only by Terraform's planfile package.
|
||||||
|
// From elsewhere in Terraform, use the API exported by the planfile package
|
||||||
|
// itself.
|
||||||
|
package planproto
|
|
@ -0,0 +1,3 @@
|
||||||
|
package planproto
|
||||||
|
|
||||||
|
//go:generate protoc --go_out=paths=source_relative:. planfile.proto
|
|
@ -0,0 +1,868 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: planfile.proto
|
||||||
|
|
||||||
|
package planproto // import "github.com/hashicorp/terraform/plans/internal/planproto"
|
||||||
|
|
||||||
|
import proto "github.com/golang/protobuf/proto"
|
||||||
|
import fmt "fmt"
|
||||||
|
import math "math"
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||||
|
|
||||||
|
// Action describes the type of action planned for an object.
|
||||||
|
// Not all action values are valid for all object types.
|
||||||
|
type Action int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
Action_NOOP Action = 0
|
||||||
|
Action_CREATE Action = 1
|
||||||
|
Action_READ Action = 2
|
||||||
|
Action_UPDATE Action = 3
|
||||||
|
Action_REPLACE Action = 4
|
||||||
|
Action_DELETE Action = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
var Action_name = map[int32]string{
|
||||||
|
0: "NOOP",
|
||||||
|
1: "CREATE",
|
||||||
|
2: "READ",
|
||||||
|
3: "UPDATE",
|
||||||
|
4: "REPLACE",
|
||||||
|
5: "DELETE",
|
||||||
|
}
|
||||||
|
var Action_value = map[string]int32{
|
||||||
|
"NOOP": 0,
|
||||||
|
"CREATE": 1,
|
||||||
|
"READ": 2,
|
||||||
|
"UPDATE": 3,
|
||||||
|
"REPLACE": 4,
|
||||||
|
"DELETE": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Action) String() string {
|
||||||
|
return proto.EnumName(Action_name, int32(x))
|
||||||
|
}
|
||||||
|
func (Action) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceInstanceChange_ResourceMode int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResourceInstanceChange_managed ResourceInstanceChange_ResourceMode = 0
|
||||||
|
ResourceInstanceChange_data ResourceInstanceChange_ResourceMode = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var ResourceInstanceChange_ResourceMode_name = map[int32]string{
|
||||||
|
0: "managed",
|
||||||
|
1: "data",
|
||||||
|
}
|
||||||
|
var ResourceInstanceChange_ResourceMode_value = map[string]int32{
|
||||||
|
"managed": 0,
|
||||||
|
"data": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ResourceInstanceChange_ResourceMode) String() string {
|
||||||
|
return proto.EnumName(ResourceInstanceChange_ResourceMode_name, int32(x))
|
||||||
|
}
|
||||||
|
func (ResourceInstanceChange_ResourceMode) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{2, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan is the root message type for the tfplan file
|
||||||
|
type Plan struct {
|
||||||
|
// Version is incremented whenever there is a breaking change to
|
||||||
|
// the serialization format. Programs reading serialized plans should
|
||||||
|
// verify that version is set to the expected value and abort processing
|
||||||
|
// if not. A breaking change is any change that may cause an older
|
||||||
|
// consumer to interpret the structure incorrectly. This number will
|
||||||
|
// not be incremented if an existing consumer can either safely ignore
|
||||||
|
// changes to the format or if an existing consumer would fail to process
|
||||||
|
// the file for another message- or field-specific reason.
|
||||||
|
Version uint64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
|
||||||
|
// The variables that were set when creating the plan. Each value is
|
||||||
|
// a msgpack serialization of an HCL value.
|
||||||
|
Variables map[string]*DynamicValue `protobuf:"bytes,2,rep,name=variables,proto3" json:"variables,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||||
|
// An unordered set of proposed changes to resources throughout the
|
||||||
|
// configuration, including any nested modules. Use the address of
|
||||||
|
// each resource to determine which module it belongs to.
|
||||||
|
ResourceChanges []*ResourceInstanceChange `protobuf:"bytes,3,rep,name=resource_changes,json=resourceChanges,proto3" json:"resource_changes,omitempty"`
|
||||||
|
// An unordered set of proposed changes to outputs in the root module
|
||||||
|
// of the configuration. This set also includes "no action" changes for
|
||||||
|
// outputs that are not changing, as context for detecting inconsistencies
|
||||||
|
// at apply time.
|
||||||
|
OutputChanges []*OutputChange `protobuf:"bytes,4,rep,name=output_changes,json=outputChanges,proto3" json:"output_changes,omitempty"`
|
||||||
|
// The version string for the Terraform binary that created this plan.
|
||||||
|
TerraformVersion string `protobuf:"bytes,14,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"`
|
||||||
|
// SHA256 digests of all of the provider plugin binaries that were used
|
||||||
|
// in the creation of this plan.
|
||||||
|
ProviderHashes map[string]*Hash `protobuf:"bytes,15,rep,name=provider_hashes,json=providerHashes,proto3" json:"provider_hashes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Plan) Reset() { *m = Plan{} }
|
||||||
|
func (m *Plan) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Plan) ProtoMessage() {}
|
||||||
|
func (*Plan) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{0}
|
||||||
|
}
|
||||||
|
func (m *Plan) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_Plan.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *Plan) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_Plan.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *Plan) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_Plan.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *Plan) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_Plan.Size(m)
|
||||||
|
}
|
||||||
|
func (m *Plan) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_Plan.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_Plan proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *Plan) GetVersion() uint64 {
|
||||||
|
if m != nil {
|
||||||
|
return m.Version
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Plan) GetVariables() map[string]*DynamicValue {
|
||||||
|
if m != nil {
|
||||||
|
return m.Variables
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Plan) GetResourceChanges() []*ResourceInstanceChange {
|
||||||
|
if m != nil {
|
||||||
|
return m.ResourceChanges
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Plan) GetOutputChanges() []*OutputChange {
|
||||||
|
if m != nil {
|
||||||
|
return m.OutputChanges
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Plan) GetTerraformVersion() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.TerraformVersion
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Plan) GetProviderHashes() map[string]*Hash {
|
||||||
|
if m != nil {
|
||||||
|
return m.ProviderHashes
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change represents a change made to some object, transforming it from an old
|
||||||
|
// state to a new state.
|
||||||
|
type Change struct {
|
||||||
|
// Not all action values are valid for all object types. Consult
|
||||||
|
// the documentation for any message that embeds Change.
|
||||||
|
Action Action `protobuf:"varint,1,opt,name=action,proto3,enum=tfplan.Action" json:"action,omitempty"`
|
||||||
|
// msgpack-encoded HCL values involved in the change.
|
||||||
|
// - For update and replace, two values are provided that give the old and new values,
|
||||||
|
// respectively.
|
||||||
|
// - For create, one value is provided that gives the new value to be created
|
||||||
|
// - For delete, one value is provided that describes the value being deleted
|
||||||
|
// - For read, two values are provided that give the prior value for this object
|
||||||
|
// (or null, if no prior value exists) and the value that was or will be read,
|
||||||
|
// respectively.
|
||||||
|
// - For no-op, one value is provided that is left unmodified by this non-change.
|
||||||
|
Values []*DynamicValue `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Change) Reset() { *m = Change{} }
|
||||||
|
func (m *Change) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Change) ProtoMessage() {}
|
||||||
|
func (*Change) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{1}
|
||||||
|
}
|
||||||
|
func (m *Change) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_Change.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *Change) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_Change.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *Change) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_Change.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *Change) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_Change.Size(m)
|
||||||
|
}
|
||||||
|
func (m *Change) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_Change.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_Change proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *Change) GetAction() Action {
|
||||||
|
if m != nil {
|
||||||
|
return m.Action
|
||||||
|
}
|
||||||
|
return Action_NOOP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Change) GetValues() []*DynamicValue {
|
||||||
|
if m != nil {
|
||||||
|
return m.Values
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceInstanceChange struct {
|
||||||
|
// module_path is an address to the module that defined this resource.
|
||||||
|
// module_path is omitted for resources in the root module. For descendent modules
|
||||||
|
// it is a string like module.foo.module.bar as would be seen at the beginning of a
|
||||||
|
// resource address. The format of this string is not yet frozen and so external
|
||||||
|
// callers should treat it as an opaque key for filtering purposes.
|
||||||
|
ModulePath string `protobuf:"bytes,1,opt,name=module_path,json=modulePath,proto3" json:"module_path,omitempty"`
|
||||||
|
// mode is the resource mode.
|
||||||
|
Mode ResourceInstanceChange_ResourceMode `protobuf:"varint,2,opt,name=mode,proto3,enum=tfplan.ResourceInstanceChange_ResourceMode" json:"mode,omitempty"`
|
||||||
|
// type is the resource type name, like "aws_instance".
|
||||||
|
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
|
// name is the logical name of the resource as defined in configuration.
|
||||||
|
// For example, in aws_instance.foo this would be "foo".
|
||||||
|
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// instance_key is either an integer index or a string key, depending on which iteration
|
||||||
|
// attributes ("count" or "for_each") are being used for this resource. If none
|
||||||
|
// are in use, this field is omitted.
|
||||||
|
//
|
||||||
|
// Types that are valid to be assigned to InstanceKey:
|
||||||
|
// *ResourceInstanceChange_Str
|
||||||
|
// *ResourceInstanceChange_Int
|
||||||
|
InstanceKey isResourceInstanceChange_InstanceKey `protobuf_oneof:"instance_key"`
|
||||||
|
// deposed_key, if set, indicates that this change applies to a deposed
|
||||||
|
// object for the indicated instance with the given deposed key. If not
|
||||||
|
// set, the change applies to the instance's current object.
|
||||||
|
DeposedKey string `protobuf:"bytes,7,opt,name=deposed_key,json=deposedKey,proto3" json:"deposed_key,omitempty"`
|
||||||
|
// Description of the proposed change. May use "create", "read", "update",
|
||||||
|
// "replace" and "delete" actions. "no-op" changes are not currently used here
|
||||||
|
// but consumers must accept and discard them to allow for future expansion.
|
||||||
|
Change *Change `protobuf:"bytes,8,opt,name=change,proto3" json:"change,omitempty"`
|
||||||
|
// msgpack representation of an arbitrary object value provided by
|
||||||
|
// the resource provider as additional context for the change. Must
|
||||||
|
// be considered an opaque value for any consumer other than the
|
||||||
|
// provider that generated it.
|
||||||
|
Private *DynamicValue `protobuf:"bytes,9,opt,name=private,proto3" json:"private,omitempty"`
|
||||||
|
// An unordered set of paths that prompted the change action to be
|
||||||
|
// "replace" rather than "update". Empty for any action other than
|
||||||
|
// "replace".
|
||||||
|
RequiredReplace []*Path `protobuf:"bytes,10,rep,name=required_replace,json=requiredReplace,proto3" json:"required_replace,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) Reset() { *m = ResourceInstanceChange{} }
|
||||||
|
func (m *ResourceInstanceChange) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*ResourceInstanceChange) ProtoMessage() {}
|
||||||
|
func (*ResourceInstanceChange) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{2}
|
||||||
|
}
|
||||||
|
func (m *ResourceInstanceChange) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_ResourceInstanceChange.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *ResourceInstanceChange) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_ResourceInstanceChange.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *ResourceInstanceChange) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_ResourceInstanceChange.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *ResourceInstanceChange) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_ResourceInstanceChange.Size(m)
|
||||||
|
}
|
||||||
|
func (m *ResourceInstanceChange) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_ResourceInstanceChange.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_ResourceInstanceChange proto.InternalMessageInfo
|
||||||
|
|
||||||
|
type isResourceInstanceChange_InstanceKey interface {
|
||||||
|
isResourceInstanceChange_InstanceKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceInstanceChange_Str struct {
|
||||||
|
Str string `protobuf:"bytes,5,opt,name=str,proto3,oneof"`
|
||||||
|
}
|
||||||
|
type ResourceInstanceChange_Int struct {
|
||||||
|
Int int64 `protobuf:"varint,6,opt,name=int,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ResourceInstanceChange_Str) isResourceInstanceChange_InstanceKey() {}
|
||||||
|
func (*ResourceInstanceChange_Int) isResourceInstanceChange_InstanceKey() {}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetInstanceKey() isResourceInstanceChange_InstanceKey {
|
||||||
|
if m != nil {
|
||||||
|
return m.InstanceKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetModulePath() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.ModulePath
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetMode() ResourceInstanceChange_ResourceMode {
|
||||||
|
if m != nil {
|
||||||
|
return m.Mode
|
||||||
|
}
|
||||||
|
return ResourceInstanceChange_managed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetType() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Type
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetName() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetStr() string {
|
||||||
|
if x, ok := m.GetInstanceKey().(*ResourceInstanceChange_Str); ok {
|
||||||
|
return x.Str
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetInt() int64 {
|
||||||
|
if x, ok := m.GetInstanceKey().(*ResourceInstanceChange_Int); ok {
|
||||||
|
return x.Int
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetDeposedKey() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.DeposedKey
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetChange() *Change {
|
||||||
|
if m != nil {
|
||||||
|
return m.Change
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetPrivate() *DynamicValue {
|
||||||
|
if m != nil {
|
||||||
|
return m.Private
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ResourceInstanceChange) GetRequiredReplace() []*Path {
|
||||||
|
if m != nil {
|
||||||
|
return m.RequiredReplace
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX_OneofFuncs is for the internal use of the proto package.
|
||||||
|
func (*ResourceInstanceChange) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) {
|
||||||
|
return _ResourceInstanceChange_OneofMarshaler, _ResourceInstanceChange_OneofUnmarshaler, _ResourceInstanceChange_OneofSizer, []interface{}{
|
||||||
|
(*ResourceInstanceChange_Str)(nil),
|
||||||
|
(*ResourceInstanceChange_Int)(nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ResourceInstanceChange_OneofMarshaler(msg proto.Message, b *proto.Buffer) error {
|
||||||
|
m := msg.(*ResourceInstanceChange)
|
||||||
|
// instance_key
|
||||||
|
switch x := m.InstanceKey.(type) {
|
||||||
|
case *ResourceInstanceChange_Str:
|
||||||
|
b.EncodeVarint(5<<3 | proto.WireBytes)
|
||||||
|
b.EncodeStringBytes(x.Str)
|
||||||
|
case *ResourceInstanceChange_Int:
|
||||||
|
b.EncodeVarint(6<<3 | proto.WireVarint)
|
||||||
|
b.EncodeVarint(uint64(x.Int))
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("ResourceInstanceChange.InstanceKey has unexpected type %T", x)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ResourceInstanceChange_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) {
|
||||||
|
m := msg.(*ResourceInstanceChange)
|
||||||
|
switch tag {
|
||||||
|
case 5: // instance_key.str
|
||||||
|
if wire != proto.WireBytes {
|
||||||
|
return true, proto.ErrInternalBadWireType
|
||||||
|
}
|
||||||
|
x, err := b.DecodeStringBytes()
|
||||||
|
m.InstanceKey = &ResourceInstanceChange_Str{x}
|
||||||
|
return true, err
|
||||||
|
case 6: // instance_key.int
|
||||||
|
if wire != proto.WireVarint {
|
||||||
|
return true, proto.ErrInternalBadWireType
|
||||||
|
}
|
||||||
|
x, err := b.DecodeVarint()
|
||||||
|
m.InstanceKey = &ResourceInstanceChange_Int{int64(x)}
|
||||||
|
return true, err
|
||||||
|
default:
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ResourceInstanceChange_OneofSizer(msg proto.Message) (n int) {
|
||||||
|
m := msg.(*ResourceInstanceChange)
|
||||||
|
// instance_key
|
||||||
|
switch x := m.InstanceKey.(type) {
|
||||||
|
case *ResourceInstanceChange_Str:
|
||||||
|
n += 1 // tag and wire
|
||||||
|
n += proto.SizeVarint(uint64(len(x.Str)))
|
||||||
|
n += len(x.Str)
|
||||||
|
case *ResourceInstanceChange_Int:
|
||||||
|
n += 1 // tag and wire
|
||||||
|
n += proto.SizeVarint(uint64(x.Int))
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("proto: unexpected type %T in oneof", x))
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputChange struct {
|
||||||
|
// Name of the output as defined in the root module.
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// Description of the proposed change. May use "no-op", "create",
|
||||||
|
// "update" and "delete" actions.
|
||||||
|
Change *Change `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"`
|
||||||
|
// Sensitive, if true, indicates that one or more of the values given
|
||||||
|
// in "change" is sensitive and should not be shown directly in any
|
||||||
|
// rendered plan.
|
||||||
|
Sensitive bool `protobuf:"varint,3,opt,name=sensitive,proto3" json:"sensitive,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OutputChange) Reset() { *m = OutputChange{} }
|
||||||
|
func (m *OutputChange) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*OutputChange) ProtoMessage() {}
|
||||||
|
func (*OutputChange) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{3}
|
||||||
|
}
|
||||||
|
func (m *OutputChange) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_OutputChange.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *OutputChange) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_OutputChange.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *OutputChange) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_OutputChange.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *OutputChange) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_OutputChange.Size(m)
|
||||||
|
}
|
||||||
|
func (m *OutputChange) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_OutputChange.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_OutputChange proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *OutputChange) GetName() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OutputChange) GetChange() *Change {
|
||||||
|
if m != nil {
|
||||||
|
return m.Change
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OutputChange) GetSensitive() bool {
|
||||||
|
if m != nil {
|
||||||
|
return m.Sensitive
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicValue represents a value whose type is not decided until runtime,
|
||||||
|
// often based on schema information obtained from a plugin.
|
||||||
|
//
|
||||||
|
// At present dynamic values are always encoded as msgpack, with extension
|
||||||
|
// id 0 used to represent the special "unknown" value indicating results
|
||||||
|
// that won't be known until after apply.
|
||||||
|
//
|
||||||
|
// In future other serialization formats may be used, possibly with a
|
||||||
|
// transitional period of including both as separate attributes of this type.
|
||||||
|
// Consumers must ignore attributes they don't support and fail if no supported
|
||||||
|
// attribute is present. The top-level format version will not be incremented
|
||||||
|
// for changes to the set of dynamic serialization formats.
|
||||||
|
type DynamicValue struct {
|
||||||
|
Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DynamicValue) Reset() { *m = DynamicValue{} }
|
||||||
|
func (m *DynamicValue) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*DynamicValue) ProtoMessage() {}
|
||||||
|
func (*DynamicValue) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{4}
|
||||||
|
}
|
||||||
|
func (m *DynamicValue) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_DynamicValue.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *DynamicValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_DynamicValue.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *DynamicValue) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_DynamicValue.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *DynamicValue) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_DynamicValue.Size(m)
|
||||||
|
}
|
||||||
|
func (m *DynamicValue) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_DynamicValue.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_DynamicValue proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *DynamicValue) GetMsgpack() []byte {
|
||||||
|
if m != nil {
|
||||||
|
return m.Msgpack
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash represents a hash value.
|
||||||
|
//
|
||||||
|
// At present hashes always use the SHA256 algorithm. In future other hash
|
||||||
|
// algorithms may be used, possibly with a transitional period of including
|
||||||
|
// both as separate attributes of this type. Consumers must ignore attributes
|
||||||
|
// they don't support and fail if no supported attribute is present. The
|
||||||
|
// top-level format version will not be incremented for changes to the set of
|
||||||
|
// hash algorithms.
|
||||||
|
type Hash struct {
|
||||||
|
Sha256 []byte `protobuf:"bytes,1,opt,name=sha256,proto3" json:"sha256,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Hash) Reset() { *m = Hash{} }
|
||||||
|
func (m *Hash) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Hash) ProtoMessage() {}
|
||||||
|
func (*Hash) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{5}
|
||||||
|
}
|
||||||
|
func (m *Hash) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_Hash.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *Hash) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_Hash.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *Hash) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_Hash.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *Hash) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_Hash.Size(m)
|
||||||
|
}
|
||||||
|
func (m *Hash) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_Hash.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_Hash proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *Hash) GetSha256() []byte {
|
||||||
|
if m != nil {
|
||||||
|
return m.Sha256
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path represents a set of steps to traverse into a data structure. It is
|
||||||
|
// used to refer to a sub-structure within a dynamic data structure presented
|
||||||
|
// separately.
|
||||||
|
type Path struct {
|
||||||
|
Steps []*Path_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Path) Reset() { *m = Path{} }
|
||||||
|
func (m *Path) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Path) ProtoMessage() {}
|
||||||
|
func (*Path) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{6}
|
||||||
|
}
|
||||||
|
func (m *Path) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_Path.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *Path) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_Path.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *Path) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_Path.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *Path) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_Path.Size(m)
|
||||||
|
}
|
||||||
|
func (m *Path) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_Path.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_Path proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *Path) GetSteps() []*Path_Step {
|
||||||
|
if m != nil {
|
||||||
|
return m.Steps
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Path_Step struct {
|
||||||
|
// Types that are valid to be assigned to Selector:
|
||||||
|
// *Path_Step_AttributeName
|
||||||
|
// *Path_Step_ElementKey
|
||||||
|
Selector isPath_Step_Selector `protobuf_oneof:"selector"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Path_Step) Reset() { *m = Path_Step{} }
|
||||||
|
func (m *Path_Step) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Path_Step) ProtoMessage() {}
|
||||||
|
func (*Path_Step) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_planfile_78f69d66ae9e7fdf, []int{6, 0}
|
||||||
|
}
|
||||||
|
func (m *Path_Step) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_Path_Step.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *Path_Step) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_Path_Step.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *Path_Step) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_Path_Step.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *Path_Step) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_Path_Step.Size(m)
|
||||||
|
}
|
||||||
|
func (m *Path_Step) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_Path_Step.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_Path_Step proto.InternalMessageInfo
|
||||||
|
|
||||||
|
type isPath_Step_Selector interface {
|
||||||
|
isPath_Step_Selector()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Path_Step_AttributeName struct {
|
||||||
|
AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3,oneof"`
|
||||||
|
}
|
||||||
|
type Path_Step_ElementKey struct {
|
||||||
|
ElementKey *DynamicValue `protobuf:"bytes,2,opt,name=element_key,json=elementKey,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Path_Step_AttributeName) isPath_Step_Selector() {}
|
||||||
|
func (*Path_Step_ElementKey) isPath_Step_Selector() {}
|
||||||
|
|
||||||
|
func (m *Path_Step) GetSelector() isPath_Step_Selector {
|
||||||
|
if m != nil {
|
||||||
|
return m.Selector
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Path_Step) GetAttributeName() string {
|
||||||
|
if x, ok := m.GetSelector().(*Path_Step_AttributeName); ok {
|
||||||
|
return x.AttributeName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Path_Step) GetElementKey() *DynamicValue {
|
||||||
|
if x, ok := m.GetSelector().(*Path_Step_ElementKey); ok {
|
||||||
|
return x.ElementKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX_OneofFuncs is for the internal use of the proto package.
|
||||||
|
func (*Path_Step) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) {
|
||||||
|
return _Path_Step_OneofMarshaler, _Path_Step_OneofUnmarshaler, _Path_Step_OneofSizer, []interface{}{
|
||||||
|
(*Path_Step_AttributeName)(nil),
|
||||||
|
(*Path_Step_ElementKey)(nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Path_Step_OneofMarshaler(msg proto.Message, b *proto.Buffer) error {
|
||||||
|
m := msg.(*Path_Step)
|
||||||
|
// selector
|
||||||
|
switch x := m.Selector.(type) {
|
||||||
|
case *Path_Step_AttributeName:
|
||||||
|
b.EncodeVarint(1<<3 | proto.WireBytes)
|
||||||
|
b.EncodeStringBytes(x.AttributeName)
|
||||||
|
case *Path_Step_ElementKey:
|
||||||
|
b.EncodeVarint(2<<3 | proto.WireBytes)
|
||||||
|
if err := b.EncodeMessage(x.ElementKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Path_Step.Selector has unexpected type %T", x)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Path_Step_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) {
|
||||||
|
m := msg.(*Path_Step)
|
||||||
|
switch tag {
|
||||||
|
case 1: // selector.attribute_name
|
||||||
|
if wire != proto.WireBytes {
|
||||||
|
return true, proto.ErrInternalBadWireType
|
||||||
|
}
|
||||||
|
x, err := b.DecodeStringBytes()
|
||||||
|
m.Selector = &Path_Step_AttributeName{x}
|
||||||
|
return true, err
|
||||||
|
case 2: // selector.element_key
|
||||||
|
if wire != proto.WireBytes {
|
||||||
|
return true, proto.ErrInternalBadWireType
|
||||||
|
}
|
||||||
|
msg := new(DynamicValue)
|
||||||
|
err := b.DecodeMessage(msg)
|
||||||
|
m.Selector = &Path_Step_ElementKey{msg}
|
||||||
|
return true, err
|
||||||
|
default:
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Path_Step_OneofSizer(msg proto.Message) (n int) {
|
||||||
|
m := msg.(*Path_Step)
|
||||||
|
// selector
|
||||||
|
switch x := m.Selector.(type) {
|
||||||
|
case *Path_Step_AttributeName:
|
||||||
|
n += 1 // tag and wire
|
||||||
|
n += proto.SizeVarint(uint64(len(x.AttributeName)))
|
||||||
|
n += len(x.AttributeName)
|
||||||
|
case *Path_Step_ElementKey:
|
||||||
|
s := proto.Size(x.ElementKey)
|
||||||
|
n += 1 // tag and wire
|
||||||
|
n += proto.SizeVarint(uint64(s))
|
||||||
|
n += s
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("proto: unexpected type %T in oneof", x))
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*Plan)(nil), "tfplan.Plan")
|
||||||
|
proto.RegisterMapType((map[string]*Hash)(nil), "tfplan.Plan.ProviderHashesEntry")
|
||||||
|
proto.RegisterMapType((map[string]*DynamicValue)(nil), "tfplan.Plan.VariablesEntry")
|
||||||
|
proto.RegisterType((*Change)(nil), "tfplan.Change")
|
||||||
|
proto.RegisterType((*ResourceInstanceChange)(nil), "tfplan.ResourceInstanceChange")
|
||||||
|
proto.RegisterType((*OutputChange)(nil), "tfplan.OutputChange")
|
||||||
|
proto.RegisterType((*DynamicValue)(nil), "tfplan.DynamicValue")
|
||||||
|
proto.RegisterType((*Hash)(nil), "tfplan.Hash")
|
||||||
|
proto.RegisterType((*Path)(nil), "tfplan.Path")
|
||||||
|
proto.RegisterType((*Path_Step)(nil), "tfplan.Path.Step")
|
||||||
|
proto.RegisterEnum("tfplan.Action", Action_name, Action_value)
|
||||||
|
proto.RegisterEnum("tfplan.ResourceInstanceChange_ResourceMode", ResourceInstanceChange_ResourceMode_name, ResourceInstanceChange_ResourceMode_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("planfile.proto", fileDescriptor_planfile_78f69d66ae9e7fdf) }
|
||||||
|
|
||||||
|
var fileDescriptor_planfile_78f69d66ae9e7fdf = []byte{
|
||||||
|
// 794 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x55, 0xdb, 0x6e, 0xe3, 0x36,
|
||||||
|
0x10, 0xb5, 0x6c, 0x45, 0x89, 0x27, 0x5e, 0x45, 0xcb, 0x16, 0x0b, 0x21, 0x2d, 0xb6, 0x86, 0x80,
|
||||||
|
0x76, 0x8d, 0xdd, 0xc2, 0x01, 0x52, 0xb4, 0xe9, 0xb6, 0x0f, 0x45, 0x2e, 0x02, 0x12, 0xec, 0x25,
|
||||||
|
0x06, 0xbb, 0xcd, 0x43, 0x1f, 0x6a, 0x30, 0xd2, 0x24, 0x26, 0x56, 0xa2, 0x54, 0x92, 0x32, 0xe0,
|
||||||
|
0xb7, 0xfe, 0x4c, 0xbf, 0xa0, 0x3f, 0x58, 0x90, 0xba, 0x44, 0x01, 0x02, 0x3f, 0x59, 0x73, 0xe6,
|
||||||
|
0xcc, 0x88, 0xe7, 0xcc, 0x50, 0x06, 0xbf, 0xcc, 0x98, 0xb8, 0xe3, 0x19, 0xce, 0x4b, 0x59, 0xe8,
|
||||||
|
0x82, 0x78, 0xfa, 0xce, 0x20, 0xd1, 0x3f, 0x2e, 0xb8, 0x8b, 0x8c, 0x09, 0x12, 0xc2, 0xee, 0x1a,
|
||||||
|
0xa5, 0xe2, 0x85, 0x08, 0x9d, 0xa9, 0x33, 0x73, 0x69, 0x1b, 0x92, 0xb7, 0x30, 0x5e, 0x33, 0xc9,
|
||||||
|
0xd9, 0x6d, 0x86, 0x2a, 0x1c, 0x4e, 0x47, 0xb3, 0xfd, 0xe3, 0xaf, 0xe6, 0x75, 0xf9, 0xdc, 0x94,
|
||||||
|
0xce, 0x6f, 0xda, 0x6c, 0x2c, 0xb4, 0xdc, 0xd0, 0x07, 0x36, 0xb9, 0x82, 0x40, 0xa2, 0x2a, 0x2a,
|
||||||
|
0x99, 0xe0, 0x32, 0x59, 0x31, 0x71, 0x8f, 0x2a, 0x1c, 0xd9, 0x0e, 0x2f, 0xdb, 0x0e, 0xb4, 0xc9,
|
||||||
|
0x5f, 0x09, 0xa5, 0x99, 0x48, 0xf0, 0xdc, 0xd2, 0xe8, 0x41, 0x5b, 0x57, 0xc7, 0x8a, 0xfc, 0x0a,
|
||||||
|
0x7e, 0x51, 0xe9, 0xb2, 0xd2, 0x5d, 0x23, 0xd7, 0x36, 0xfa, 0xb2, 0x6d, 0x74, 0x6d, 0xb3, 0x4d,
|
||||||
|
0xf9, 0xb3, 0xa2, 0x17, 0x29, 0xf2, 0x06, 0x9e, 0x6b, 0x94, 0x92, 0xdd, 0x15, 0x32, 0x5f, 0xb6,
|
||||||
|
0x32, 0xfd, 0xa9, 0x33, 0x1b, 0xd3, 0xa0, 0x4b, 0xdc, 0x34, 0x7a, 0xaf, 0xe0, 0xa0, 0x94, 0xc5,
|
||||||
|
0x9a, 0xa7, 0x28, 0x97, 0x2b, 0xa6, 0x56, 0xa8, 0xc2, 0x03, 0xfb, 0xaa, 0xe9, 0x23, 0xd5, 0x8b,
|
||||||
|
0x86, 0x73, 0x69, 0x29, 0xb5, 0x74, 0xbf, 0x7c, 0x04, 0x1e, 0x52, 0xf0, 0x1f, 0x9b, 0x43, 0x02,
|
||||||
|
0x18, 0x7d, 0xc6, 0x8d, 0xb5, 0x78, 0x4c, 0xcd, 0x23, 0x79, 0x0d, 0x3b, 0x6b, 0x96, 0x55, 0x18,
|
||||||
|
0x0e, 0xa7, 0x4e, 0x5f, 0xcf, 0xc5, 0x46, 0xb0, 0x9c, 0x27, 0x37, 0x26, 0x47, 0x6b, 0xca, 0x2f,
|
||||||
|
0xc3, 0x9f, 0x9d, 0xc3, 0x6b, 0xf8, 0xe2, 0x89, 0x57, 0x3f, 0xd1, 0x38, 0x7a, 0xdc, 0x78, 0xd2,
|
||||||
|
0x36, 0x36, 0x55, 0xbd, 0x86, 0xd1, 0x5f, 0xe0, 0xd5, 0x3e, 0x91, 0xef, 0xc0, 0x63, 0x89, 0x6e,
|
||||||
|
0x57, 0xc0, 0x3f, 0xf6, 0xdb, 0x92, 0x53, 0x8b, 0xd2, 0x26, 0x4b, 0xbe, 0x07, 0xcf, 0x96, 0xb7,
|
||||||
|
0xeb, 0xf0, 0xf4, 0x99, 0x1b, 0x4e, 0xf4, 0xdf, 0x08, 0x5e, 0x3c, 0x3d, 0x65, 0xf2, 0x0d, 0xec,
|
||||||
|
0xe7, 0x45, 0x5a, 0x65, 0xb8, 0x2c, 0x99, 0x5e, 0x35, 0x87, 0x87, 0x1a, 0x5a, 0x30, 0xbd, 0x22,
|
||||||
|
0xbf, 0x81, 0x9b, 0x17, 0x69, 0x2d, 0xc1, 0x3f, 0x7e, 0xb3, 0x7d, 0x69, 0x3a, 0xf8, 0x43, 0x91,
|
||||||
|
0x22, 0xb5, 0x85, 0x84, 0x80, 0xab, 0x37, 0x25, 0x86, 0x23, 0xdb, 0xda, 0x3e, 0x1b, 0x4c, 0xb0,
|
||||||
|
0x1c, 0x43, 0xb7, 0xc6, 0xcc, 0x33, 0x21, 0x30, 0x52, 0x5a, 0x86, 0x3b, 0x06, 0xba, 0x1c, 0x50,
|
||||||
|
0x13, 0x18, 0x8c, 0x0b, 0x1d, 0x7a, 0x53, 0x67, 0x36, 0x32, 0x18, 0x17, 0xda, 0x9c, 0x38, 0xc5,
|
||||||
|
0xb2, 0x50, 0x98, 0x2e, 0x8d, 0xdd, 0xbb, 0xf5, 0x89, 0x1b, 0xe8, 0x1d, 0x6e, 0x8c, 0x87, 0xf5,
|
||||||
|
0x82, 0x86, 0x7b, 0xd6, 0xf6, 0xce, 0xc3, 0x66, 0x33, 0x9b, 0x2c, 0x99, 0xc3, 0x6e, 0x29, 0xf9,
|
||||||
|
0x9a, 0x69, 0x0c, 0xc7, 0x5b, 0x06, 0xdf, 0x92, 0xc8, 0x89, 0xb9, 0x4a, 0x7f, 0x57, 0x5c, 0x62,
|
||||||
|
0xba, 0x94, 0x58, 0x66, 0x2c, 0xc1, 0x10, 0xac, 0xfb, 0xdd, 0x60, 0x8d, 0x63, 0xe6, 0xe2, 0xd4,
|
||||||
|
0x2c, 0x5a, 0x93, 0xa2, 0x6f, 0x61, 0xd2, 0xf7, 0x85, 0xec, 0xc3, 0x6e, 0xce, 0x04, 0xbb, 0xc7,
|
||||||
|
0x34, 0x18, 0x90, 0x3d, 0x70, 0x53, 0xa6, 0x59, 0xe0, 0x9c, 0xf9, 0x30, 0xe1, 0x8d, 0x9b, 0x46,
|
||||||
|
0x59, 0xb4, 0x82, 0x49, 0xff, 0x46, 0x75, 0xa6, 0x39, 0x3d, 0xd3, 0x1e, 0xb4, 0x0e, 0xb7, 0x6a,
|
||||||
|
0xfd, 0x1a, 0xc6, 0x0a, 0x85, 0xe2, 0x9a, 0xaf, 0xeb, 0x49, 0xec, 0xd1, 0x07, 0x20, 0x9a, 0xc1,
|
||||||
|
0xa4, 0x2f, 0xd9, 0x7c, 0x89, 0x72, 0x75, 0x5f, 0xb2, 0xe4, 0xb3, 0x7d, 0xd9, 0x84, 0xb6, 0x61,
|
||||||
|
0xf4, 0x12, 0x5c, 0xb3, 0xbc, 0xe4, 0x05, 0x78, 0x6a, 0xc5, 0x8e, 0x7f, 0xfc, 0xa9, 0x21, 0x34,
|
||||||
|
0x51, 0xf4, 0xaf, 0x03, 0xae, 0x5d, 0x9b, 0x57, 0xb0, 0xa3, 0x34, 0x96, 0x2a, 0x74, 0xac, 0x43,
|
||||||
|
0xcf, 0xfb, 0x0e, 0xcd, 0x7f, 0xd7, 0x58, 0xd2, 0x3a, 0x7f, 0xa8, 0xc1, 0x35, 0x21, 0x79, 0x05,
|
||||||
|
0x3e, 0xd3, 0x5a, 0xf2, 0xdb, 0x4a, 0xe3, 0xf2, 0x41, 0xe7, 0xe5, 0x80, 0x3e, 0xeb, 0xf0, 0x8f,
|
||||||
|
0x46, 0xf2, 0x09, 0xec, 0x63, 0x86, 0x39, 0x0a, 0x6d, 0xe7, 0xbf, 0xe5, 0xce, 0x5e, 0x0e, 0x28,
|
||||||
|
0x34, 0xd4, 0x77, 0xb8, 0x39, 0x03, 0xd8, 0x53, 0x98, 0x61, 0xa2, 0x0b, 0xf9, 0xfa, 0x03, 0x78,
|
||||||
|
0xf5, 0x8d, 0x32, 0xfe, 0x7f, 0xbc, 0xbe, 0x5e, 0x04, 0x03, 0x02, 0xe0, 0x9d, 0xd3, 0xf8, 0xf4,
|
||||||
|
0x53, 0x1c, 0x38, 0x06, 0xa5, 0xf1, 0xe9, 0x45, 0x30, 0x34, 0xe8, 0x1f, 0x8b, 0x0b, 0x83, 0x8e,
|
||||||
|
0xcc, 0xe0, 0x68, 0xbc, 0x78, 0x7f, 0x7a, 0x1e, 0x07, 0xae, 0x49, 0x5c, 0xc4, 0xef, 0xe3, 0x4f,
|
||||||
|
0x71, 0xb0, 0x73, 0xf6, 0xf6, 0xcf, 0x93, 0x7b, 0xae, 0x57, 0xd5, 0xed, 0x3c, 0x29, 0xf2, 0x23,
|
||||||
|
0xf3, 0xc9, 0xe2, 0x49, 0x21, 0xcb, 0xa3, 0xee, 0xcb, 0x76, 0x64, 0x0e, 0xa7, 0x8e, 0xb8, 0xd0,
|
||||||
|
0x28, 0x05, 0xcb, 0x6c, 0x68, 0xff, 0x06, 0x6e, 0x3d, 0xfb, 0xf3, 0xc3, 0xff, 0x01, 0x00, 0x00,
|
||||||
|
0xff, 0xff, 0x19, 0x9b, 0x98, 0x0d, 0x1f, 0x06, 0x00, 0x00,
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package tfplan;
|
||||||
|
|
||||||
|
// For Terraform's own parsing, the proto stub types go into an internal Go
|
||||||
|
// package. The public API is in github.com/hashicorp/terraform/plans/planfile .
|
||||||
|
option go_package = "github.com/hashicorp/terraform/plans/internal/planproto";
|
||||||
|
|
||||||
|
// Plan is the root message type for the tfplan file
|
||||||
|
message Plan {
|
||||||
|
// Version is incremented whenever there is a breaking change to
|
||||||
|
// the serialization format. Programs reading serialized plans should
|
||||||
|
// verify that version is set to the expected value and abort processing
|
||||||
|
// if not. A breaking change is any change that may cause an older
|
||||||
|
// consumer to interpret the structure incorrectly. This number will
|
||||||
|
// not be incremented if an existing consumer can either safely ignore
|
||||||
|
// changes to the format or if an existing consumer would fail to process
|
||||||
|
// the file for another message- or field-specific reason.
|
||||||
|
uint64 version = 1;
|
||||||
|
|
||||||
|
// The variables that were set when creating the plan. Each value is
|
||||||
|
// a msgpack serialization of an HCL value.
|
||||||
|
map<string, DynamicValue> variables = 2;
|
||||||
|
|
||||||
|
// An unordered set of proposed changes to resources throughout the
|
||||||
|
// configuration, including any nested modules. Use the address of
|
||||||
|
// each resource to determine which module it belongs to.
|
||||||
|
repeated ResourceInstanceChange resource_changes = 3;
|
||||||
|
|
||||||
|
// An unordered set of proposed changes to outputs in the root module
|
||||||
|
// of the configuration. This set also includes "no action" changes for
|
||||||
|
// outputs that are not changing, as context for detecting inconsistencies
|
||||||
|
// at apply time.
|
||||||
|
repeated OutputChange output_changes = 4;
|
||||||
|
|
||||||
|
// The version string for the Terraform binary that created this plan.
|
||||||
|
string terraform_version = 14;
|
||||||
|
|
||||||
|
// SHA256 digests of all of the provider plugin binaries that were used
|
||||||
|
// in the creation of this plan.
|
||||||
|
map<string, Hash> provider_hashes = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action describes the type of action planned for an object.
|
||||||
|
// Not all action values are valid for all object types.
|
||||||
|
enum Action {
|
||||||
|
NOOP = 0;
|
||||||
|
CREATE = 1;
|
||||||
|
READ = 2;
|
||||||
|
UPDATE = 3;
|
||||||
|
REPLACE = 4;
|
||||||
|
DELETE = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change represents a change made to some object, transforming it from an old
|
||||||
|
// state to a new state.
|
||||||
|
message Change {
|
||||||
|
// Not all action values are valid for all object types. Consult
|
||||||
|
// the documentation for any message that embeds Change.
|
||||||
|
Action action = 1;
|
||||||
|
|
||||||
|
// msgpack-encoded HCL values involved in the change.
|
||||||
|
// - For update and replace, two values are provided that give the old and new values,
|
||||||
|
// respectively.
|
||||||
|
// - For create, one value is provided that gives the new value to be created
|
||||||
|
// - For delete, one value is provided that describes the value being deleted
|
||||||
|
// - For read, two values are provided that give the prior value for this object
|
||||||
|
// (or null, if no prior value exists) and the value that was or will be read,
|
||||||
|
// respectively.
|
||||||
|
// - For no-op, one value is provided that is left unmodified by this non-change.
|
||||||
|
repeated DynamicValue values = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResourceInstanceChange {
|
||||||
|
// module_path is an address to the module that defined this resource.
|
||||||
|
// module_path is omitted for resources in the root module. For descendent modules
|
||||||
|
// it is a string like module.foo.module.bar as would be seen at the beginning of a
|
||||||
|
// resource address. The format of this string is not yet frozen and so external
|
||||||
|
// callers should treat it as an opaque key for filtering purposes.
|
||||||
|
string module_path = 1;
|
||||||
|
|
||||||
|
// mode is the resource mode.
|
||||||
|
ResourceMode mode = 2;
|
||||||
|
enum ResourceMode {
|
||||||
|
managed = 0; // for "resource" blocks in configuration
|
||||||
|
data = 1; // for "data" blocks in configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// type is the resource type name, like "aws_instance".
|
||||||
|
string type = 3;
|
||||||
|
|
||||||
|
// name is the logical name of the resource as defined in configuration.
|
||||||
|
// For example, in aws_instance.foo this would be "foo".
|
||||||
|
string name = 4;
|
||||||
|
|
||||||
|
// instance_key is either an integer index or a string key, depending on which iteration
|
||||||
|
// attributes ("count" or "for_each") are being used for this resource. If none
|
||||||
|
// are in use, this field is omitted.
|
||||||
|
oneof instance_key {
|
||||||
|
string str = 5;
|
||||||
|
int64 int = 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
// deposed_key, if set, indicates that this change applies to a deposed
|
||||||
|
// object for the indicated instance with the given deposed key. If not
|
||||||
|
// set, the change applies to the instance's current object.
|
||||||
|
string deposed_key = 7;
|
||||||
|
|
||||||
|
// Description of the proposed change. May use "create", "read", "update",
|
||||||
|
// "replace" and "delete" actions. "no-op" changes are not currently used here
|
||||||
|
// but consumers must accept and discard them to allow for future expansion.
|
||||||
|
Change change = 8;
|
||||||
|
|
||||||
|
// msgpack representation of an arbitrary object value provided by
|
||||||
|
// the resource provider as additional context for the change. Must
|
||||||
|
// be considered an opaque value for any consumer other than the
|
||||||
|
// provider that generated it.
|
||||||
|
DynamicValue private = 9;
|
||||||
|
|
||||||
|
// An unordered set of paths that prompted the change action to be
|
||||||
|
// "replace" rather than "update". Empty for any action other than
|
||||||
|
// "replace".
|
||||||
|
repeated Path required_replace = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OutputChange {
|
||||||
|
// Name of the output as defined in the root module.
|
||||||
|
string name = 1;
|
||||||
|
|
||||||
|
// Description of the proposed change. May use "no-op", "create",
|
||||||
|
// "update" and "delete" actions.
|
||||||
|
Change change = 2;
|
||||||
|
|
||||||
|
// Sensitive, if true, indicates that one or more of the values given
|
||||||
|
// in "change" is sensitive and should not be shown directly in any
|
||||||
|
// rendered plan.
|
||||||
|
bool sensitive = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicValue represents a value whose type is not decided until runtime,
|
||||||
|
// often based on schema information obtained from a plugin.
|
||||||
|
//
|
||||||
|
// At present dynamic values are always encoded as msgpack, with extension
|
||||||
|
// id 0 used to represent the special "unknown" value indicating results
|
||||||
|
// that won't be known until after apply.
|
||||||
|
//
|
||||||
|
// In future other serialization formats may be used, possibly with a
|
||||||
|
// transitional period of including both as separate attributes of this type.
|
||||||
|
// Consumers must ignore attributes they don't support and fail if no supported
|
||||||
|
// attribute is present. The top-level format version will not be incremented
|
||||||
|
// for changes to the set of dynamic serialization formats.
|
||||||
|
message DynamicValue {
|
||||||
|
bytes msgpack = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash represents a hash value.
|
||||||
|
//
|
||||||
|
// At present hashes always use the SHA256 algorithm. In future other hash
|
||||||
|
// algorithms may be used, possibly with a transitional period of including
|
||||||
|
// both as separate attributes of this type. Consumers must ignore attributes
|
||||||
|
// they don't support and fail if no supported attribute is present. The
|
||||||
|
// top-level format version will not be incremented for changes to the set of
|
||||||
|
// hash algorithms.
|
||||||
|
message Hash {
|
||||||
|
bytes sha256 = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path represents a set of steps to traverse into a data structure. It is
|
||||||
|
// used to refer to a sub-structure within a dynamic data structure presented
|
||||||
|
// separately.
|
||||||
|
message Path {
|
||||||
|
message Step {
|
||||||
|
oneof selector {
|
||||||
|
// Set "attribute_name" to represent looking up an attribute
|
||||||
|
// in the current object value.
|
||||||
|
string attribute_name = 1;
|
||||||
|
|
||||||
|
// Set "element_key" to represent looking up an element in
|
||||||
|
// an indexable collection type.
|
||||||
|
DynamicValue element_key = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repeated Step steps = 1;
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
package planfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
|
)
|
||||||
|
|
||||||
|
const configSnapshotPrefix = "tfconfig/"
|
||||||
|
const configSnapshotManifestFile = configSnapshotPrefix + "modules.json"
|
||||||
|
const configSnapshotModulePrefix = configSnapshotPrefix + "m-"
|
||||||
|
|
||||||
|
type configSnapshotModuleRecord struct {
|
||||||
|
Key string `json:"Key"`
|
||||||
|
SourceAddr string `json:"Source,omitempty"`
|
||||||
|
VersionStr string `json:"Version,omitempty"`
|
||||||
|
Dir string `json:"Dir"`
|
||||||
|
}
|
||||||
|
type configSnapshotModuleManifest []configSnapshotModuleRecord
|
||||||
|
|
||||||
|
func readConfigSnapshot(z *zip.Reader) (*configload.Snapshot, error) {
|
||||||
|
// Errors from this function are expected to be reported with some
|
||||||
|
// additional prefix context about them being in a config snapshot,
|
||||||
|
// so they should not themselves refer to the config snapshot.
|
||||||
|
// They are also generally indicative of an invalid file, and so since
|
||||||
|
// plan files should not be hand-constructed we don't need to worry
|
||||||
|
// about making the messages user-actionable.
|
||||||
|
|
||||||
|
snap := &configload.Snapshot{
|
||||||
|
Modules: map[string]*configload.SnapshotModule{},
|
||||||
|
}
|
||||||
|
var manifestSrc []byte
|
||||||
|
|
||||||
|
// For processing our source files, we'll just sweep over all the files
|
||||||
|
// and react to the one-by-one to start, and then clean up afterwards
|
||||||
|
// when we'll presumably have found the manifest file.
|
||||||
|
for _, file := range z.File {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case file.Name == configSnapshotManifestFile:
|
||||||
|
// It's the manifest file, so we'll just read it raw into
|
||||||
|
// manifestSrc for now and process it below.
|
||||||
|
r, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open module manifest: %s", r)
|
||||||
|
}
|
||||||
|
manifestSrc, err = ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read module manifest: %s", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasPrefix(file.Name, configSnapshotModulePrefix):
|
||||||
|
relName := file.Name[len(configSnapshotModulePrefix):]
|
||||||
|
moduleKey, fileName := path.Split(relName)
|
||||||
|
|
||||||
|
// moduleKey should currently have a trailing slash on it, which we
|
||||||
|
// can use to recognize the difference between the root module
|
||||||
|
// (just a trailing slash) and no module path at all (empty string).
|
||||||
|
if moduleKey == "" {
|
||||||
|
// ignore invalid config entry
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
moduleKey = moduleKey[:len(moduleKey)-1] // trim trailing slash
|
||||||
|
|
||||||
|
r, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open snapshot of %s from module %q: %s", fileName, moduleKey, err)
|
||||||
|
}
|
||||||
|
fileSrc, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read snapshot of %s from module %q: %s", fileName, moduleKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := snap.Modules[moduleKey]; !exists {
|
||||||
|
snap.Modules[moduleKey] = &configload.SnapshotModule{
|
||||||
|
Files: map[string][]byte{},
|
||||||
|
// Will fill in everything else afterwards, when we
|
||||||
|
// process the manifest.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snap.Modules[moduleKey].Files[fileName] = fileSrc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if manifestSrc == nil {
|
||||||
|
return nil, fmt.Errorf("config snapshot does not have manifest file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest configSnapshotModuleManifest
|
||||||
|
err := json.Unmarshal(manifestSrc, &manifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid module manifest: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range manifest {
|
||||||
|
modSnap, exists := snap.Modules[record.Key]
|
||||||
|
if !exists {
|
||||||
|
// We'll allow this, assuming that it's a module with no files.
|
||||||
|
// This is still weird, since we generally reject modules with
|
||||||
|
// no files, but we'll allow it because downstream errors will
|
||||||
|
// catch it in that case.
|
||||||
|
modSnap = &configload.SnapshotModule{
|
||||||
|
Files: map[string][]byte{},
|
||||||
|
}
|
||||||
|
snap.Modules[record.Key] = modSnap
|
||||||
|
}
|
||||||
|
modSnap.SourceAddr = record.SourceAddr
|
||||||
|
modSnap.Dir = record.Dir
|
||||||
|
if record.VersionStr != "" {
|
||||||
|
v, err := version.NewVersion(record.VersionStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("manifest has invalid version string %q for module %q", record.VersionStr, record.Key)
|
||||||
|
}
|
||||||
|
modSnap.Version = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll make sure we don't have any errant files for modules that
|
||||||
|
// aren't in the manifest.
|
||||||
|
for k := range snap.Modules {
|
||||||
|
found := false
|
||||||
|
for _, record := range manifest {
|
||||||
|
if record.Key == k {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("found files for module %q that isn't recorded in the manifest", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeConfigSnapshot adds to the given zip.Writer one or more files
|
||||||
|
// representing the given snapshot.
|
||||||
|
//
|
||||||
|
// This file creates new files in the writer, so any already-open writer
|
||||||
|
// for the file will be invalidated by this call. The writer remains open
|
||||||
|
// when this function returns.
|
||||||
|
func writeConfigSnapshot(snap *configload.Snapshot, z *zip.Writer) error {
|
||||||
|
// Errors from this function are expected to be reported with some
|
||||||
|
// additional prefix context about them being in a config snapshot,
|
||||||
|
// so they should not themselves refer to the config snapshot.
|
||||||
|
// They are also indicative of a bug in the caller, so they do not
|
||||||
|
// need to be user-actionable.
|
||||||
|
|
||||||
|
var manifest configSnapshotModuleManifest
|
||||||
|
keys := make([]string, 0, len(snap.Modules))
|
||||||
|
for k := range snap.Modules {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
// We'll re-use this fileheader for each Create we do below.
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
snapMod := snap.Modules[k]
|
||||||
|
record := configSnapshotModuleRecord{
|
||||||
|
Dir: snapMod.Dir,
|
||||||
|
Key: k,
|
||||||
|
SourceAddr: snapMod.SourceAddr,
|
||||||
|
}
|
||||||
|
if snapMod.Version != nil {
|
||||||
|
record.VersionStr = snapMod.Version.String()
|
||||||
|
}
|
||||||
|
manifest = append(manifest, record)
|
||||||
|
|
||||||
|
pathPrefix := fmt.Sprintf("%s%s/", configSnapshotModulePrefix, k)
|
||||||
|
for filename, src := range snapMod.Files {
|
||||||
|
zh := &zip.FileHeader{
|
||||||
|
Name: pathPrefix + filename,
|
||||||
|
Method: zip.Deflate,
|
||||||
|
Modified: time.Now(),
|
||||||
|
}
|
||||||
|
w, err := z.CreateHeader(zh)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create snapshot of %s from module %q: %s", zh.Name, k, err)
|
||||||
|
}
|
||||||
|
_, err = w.Write(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write snapshot of %s from module %q: %s", zh.Name, k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we'll write our manifest
|
||||||
|
{
|
||||||
|
zh := &zip.FileHeader{
|
||||||
|
Name: configSnapshotManifestFile,
|
||||||
|
Method: zip.Deflate,
|
||||||
|
Modified: time.Now(),
|
||||||
|
}
|
||||||
|
src, err := json.MarshalIndent(manifest, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to serialize module manifest: %s", err)
|
||||||
|
}
|
||||||
|
w, err := z.CreateHeader(zh)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create module manifest: %s", err)
|
||||||
|
}
|
||||||
|
_, err = w.Write(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write module manifest: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package planfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigSnapshotRoundtrip(t *testing.T) {
|
||||||
|
fixtureDir := filepath.Join("testdata", "test-config")
|
||||||
|
loader, err := configload.NewLoader(&configload.Config{
|
||||||
|
ModulesDir: filepath.Join(fixtureDir, ".terraform", "modules"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, snapIn, diags := loader.LoadConfigWithSnapshot(fixtureDir)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatal(diags.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
err = writeConfigSnapshot(snapIn, zw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write snapshot: %s", err)
|
||||||
|
}
|
||||||
|
zw.Close()
|
||||||
|
|
||||||
|
raw := buf.Bytes()
|
||||||
|
r := bytes.NewReader(raw)
|
||||||
|
zr, err := zip.NewReader(r, int64(len(raw)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapOut, err := readConfigSnapshot(zr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read snapshot: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(snapIn, snapOut) {
|
||||||
|
t.Errorf("result does not match input\nresult: %sinput: %s", spew.Sdump(snapOut), spew.Sdump(snapIn))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Package planfile deals with the file format used to serialize plans to disk
|
||||||
|
// and then deserialize them back into memory later.
|
||||||
|
//
|
||||||
|
// A plan file contains the planned changes along with the configuration and
|
||||||
|
// state snapshot that they are based on.
|
||||||
|
package planfile
|
|
@ -0,0 +1,112 @@
|
||||||
|
package planfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/plans"
|
||||||
|
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
|
"github.com/hashicorp/terraform/states"
|
||||||
|
"github.com/hashicorp/terraform/states/statefile"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundtrip(t *testing.T) {
|
||||||
|
fixtureDir := filepath.Join("testdata", "test-config")
|
||||||
|
loader, err := configload.NewLoader(&configload.Config{
|
||||||
|
ModulesDir: filepath.Join(fixtureDir, ".terraform", "modules"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, snapIn, diags := loader.LoadConfigWithSnapshot(fixtureDir)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatal(diags.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just a minimal state file so we can test that it comes out again at all.
|
||||||
|
// We don't need to test the entire thing because the state file
|
||||||
|
// serialization is already tested in its own package.
|
||||||
|
stateFileIn := &statefile.File{
|
||||||
|
TerraformVersion: version.Must(version.NewVersion("1.0.0")),
|
||||||
|
Serial: 1,
|
||||||
|
Lineage: "abc123",
|
||||||
|
State: states.NewState(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal plan too, since the serialization of the tfplan portion of the
|
||||||
|
// file is tested more fully in tfplan_test.go .
|
||||||
|
planIn := &plans.Plan{
|
||||||
|
Changes: &plans.Changes{
|
||||||
|
Resources: []*plans.ResourceInstanceChange{},
|
||||||
|
RootOutputs: map[string]*plans.OutputChange{},
|
||||||
|
},
|
||||||
|
ProviderSHA256s: map[string][]byte{},
|
||||||
|
VariableValues: map[string]plans.DynamicValue{
|
||||||
|
"foo": plans.DynamicValue([]byte("foo placeholder")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
workDir, err := ioutil.TempDir("", "tf-planfile")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
planFn := filepath.Join(workDir, "tfplan")
|
||||||
|
|
||||||
|
err = Create(planFn, snapIn, stateFileIn, planIn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create plan file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, err := Open(planFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open plan file for reading: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ReadPlan", func(t *testing.T) {
|
||||||
|
planOut, err := pr.ReadPlan()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read plan: %s", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(planIn, planOut) {
|
||||||
|
t.Errorf("plan did not survive round-trip\nresult: %sinput: %s", spew.Sdump(planOut), spew.Sdump(planIn))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ReadStateFile", func(t *testing.T) {
|
||||||
|
stateFileOut, err := pr.ReadStateFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read state: %s", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(stateFileIn, stateFileOut) {
|
||||||
|
t.Errorf("state file did not survive round-trip\nresult: %sinput: %s", spew.Sdump(stateFileOut), spew.Sdump(stateFileIn))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ReadConfigSnapshot", func(t *testing.T) {
|
||||||
|
snapOut, err := pr.ReadConfigSnapshot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read config snapshot: %s", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(snapIn, snapOut) {
|
||||||
|
t.Errorf("config snapshot did not survive round-trip\nresult: %sinput: %s", spew.Sdump(snapOut), spew.Sdump(snapIn))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ReadConfig", func(t *testing.T) {
|
||||||
|
// Reading from snapshots is tested in the configload package, so
|
||||||
|
// here we'll just test that we can successfully do it, to see if the
|
||||||
|
// glue code in _this_ package is correct.
|
||||||
|
_, diags := pr.ReadConfig()
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Errorf("when reading config: %s", diags.Err())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
package planfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/configs"
|
||||||
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
|
"github.com/hashicorp/terraform/plans"
|
||||||
|
"github.com/hashicorp/terraform/states/statefile"
|
||||||
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tfstateFilename = "tfstate"
|
||||||
|
|
||||||
|
// Reader is the main type used to read plan files. Create a Reader by calling
|
||||||
|
// Open.
|
||||||
|
//
|
||||||
|
// A plan file is a random-access file format, so methods of Reader must
|
||||||
|
// be used to access the individual portions of the file for further
|
||||||
|
// processing.
|
||||||
|
type Reader struct {
|
||||||
|
zip *zip.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open creates a Reader for the file at the given filename, or returns an
|
||||||
|
// error if the file doesn't seem to be a planfile.
|
||||||
|
func Open(filename string) (*Reader, error) {
|
||||||
|
r, err := zip.OpenReader(filename)
|
||||||
|
if err != nil {
|
||||||
|
// To give a better error message, we'll sniff to see if this looks
|
||||||
|
// like our old plan format from versions prior to 0.12.
|
||||||
|
if b, sErr := ioutil.ReadFile(filename); sErr == nil {
|
||||||
|
if bytes.HasPrefix(b, []byte("tfplan")) {
|
||||||
|
return nil, fmt.Errorf("the given plan file was created by an earlier version of Terraform; plan files cannot be shared between different Terraform versions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sniff to make sure this looks like a plan file, as opposed to any other
|
||||||
|
// random zip file the user might have around.
|
||||||
|
var planFile *zip.File
|
||||||
|
for _, file := range r.File {
|
||||||
|
if file.Name == tfplanFilename {
|
||||||
|
planFile = file
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if planFile == nil {
|
||||||
|
return nil, fmt.Errorf("the given file is not a valid plan file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, we'll just accept the presence of the tfplan file as enough,
|
||||||
|
// and wait to validate the version when the caller requests the plan
|
||||||
|
// itself.
|
||||||
|
|
||||||
|
return &Reader{
|
||||||
|
zip: r,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPlan reads the plan embedded in the plan file.
|
||||||
|
//
|
||||||
|
// Errors can be returned for various reasons, including if the plan file
|
||||||
|
// is not of an appropriate format version, if it was created by a different
|
||||||
|
// version of Terraform, if it is invalid, etc.
|
||||||
|
func (r *Reader) ReadPlan() (*plans.Plan, error) {
|
||||||
|
var planFile *zip.File
|
||||||
|
for _, file := range r.zip.File {
|
||||||
|
if file.Name == tfplanFilename {
|
||||||
|
planFile = file
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if planFile == nil {
|
||||||
|
// This should never happen because we checked for this file during
|
||||||
|
// Open, but we'll check anyway to be safe.
|
||||||
|
return nil, fmt.Errorf("the plan file is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, err := planFile.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve plan from plan file: %s", err)
|
||||||
|
}
|
||||||
|
defer pr.Close()
|
||||||
|
|
||||||
|
return readTfplan(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadStateFile reads the state file embedded in the plan file.
|
||||||
|
//
|
||||||
|
// If the plan file contains no embedded state file, the returned error is
|
||||||
|
// statefile.ErrNoState.
|
||||||
|
func (r *Reader) ReadStateFile() (*statefile.File, error) {
|
||||||
|
for _, file := range r.zip.File {
|
||||||
|
if file.Name == tfstateFilename {
|
||||||
|
r, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract state from plan file: %s", err)
|
||||||
|
}
|
||||||
|
return statefile.Read(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, statefile.ErrNoState
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfigSnapshot reads the configuration snapshot embedded in the plan
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// This is a lower-level alternative to ReadConfig that just extracts the
|
||||||
|
// source files, without attempting to parse them.
|
||||||
|
func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) {
|
||||||
|
return readConfigSnapshot(&r.zip.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfig reads the configuration embedded in the plan file.
|
||||||
|
//
|
||||||
|
// Internally this function delegates to the configs/configload package to
|
||||||
|
// parse the embedded configuration and so it returns diagnostics (rather than
|
||||||
|
// a native Go error as with other methods on Reader).
|
||||||
|
func (r *Reader) ReadConfig() (*configs.Config, tfdiags.Diagnostics) {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
snap, err := r.ReadConfigSnapshot()
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Failed to read configuration from plan file",
|
||||||
|
fmt.Sprintf("The configuration file snapshot in the plan file could not be read: %s.", err),
|
||||||
|
))
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
loader := configload.NewLoaderFromSnapshot(snap)
|
||||||
|
rootDir := snap.Modules[""].Dir // Root module base directory
|
||||||
|
config, configDiags := loader.LoadConfig(rootDir)
|
||||||
|
diags = diags.Append(configDiags)
|
||||||
|
|
||||||
|
return config, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the file, after which no other operations may be performed.
|
||||||
|
func (r *Reader) Close() error {
|
||||||
|
return r.zip.Close()
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
module "child_c" {
|
||||||
|
source = "./child_c"
|
||||||
|
}
|
4
plans/planfile/testdata/test-config/.terraform/modules/child_a/child_c/child_c.tf
vendored
Normal file
4
plans/planfile/testdata/test-config/.terraform/modules/child_a/child_c/child_c.tf
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
output "hello" {
|
||||||
|
value = "Hello from child_c"
|
||||||
|
}
|
4
plans/planfile/testdata/test-config/.terraform/modules/child_b.child_d/child_d.tf
vendored
Normal file
4
plans/planfile/testdata/test-config/.terraform/modules/child_b.child_d/child_d.tf
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
output "hello" {
|
||||||
|
value = "Hello from child_d"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
module "child_d" {
|
||||||
|
source = "example.com/foo/bar_d/baz"
|
||||||
|
# Intentionally no version here
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"Modules": [
|
||||||
|
{
|
||||||
|
"Key": "",
|
||||||
|
"Source": "",
|
||||||
|
"Dir": "testdata/test-config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": "child_a",
|
||||||
|
"Source": "example.com/foo/bar_a/baz",
|
||||||
|
"Version": "1.0.1",
|
||||||
|
"Dir": "testdata/test-config/.terraform/modules/child_a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": "child_b",
|
||||||
|
"Source": "example.com/foo/bar_b/baz",
|
||||||
|
"Version": "1.0.0",
|
||||||
|
"Dir": "testdata/test-config/.terraform/modules/child_b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": "child_a.child_c",
|
||||||
|
"Source": "./child_c",
|
||||||
|
"Dir": "testdata/test-config/.terraform/modules/child_a/child_c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": "child_b.child_d",
|
||||||
|
"Source": "example.com/foo/bar_d/baz",
|
||||||
|
"Version": "1.2.0",
|
||||||
|
"Dir": "testdata/test-config/.terraform/modules/child_b.child_d"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
module "child_a" {
|
||||||
|
source = "example.com/foo/bar_a/baz"
|
||||||
|
version = ">= 1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "child_b" {
|
||||||
|
source = "example.com/foo/bar_b/baz"
|
||||||
|
version = ">= 1.0.0"
|
||||||
|
}
|
|
@ -0,0 +1,390 @@
|
||||||
|
package planfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
"github.com/hashicorp/terraform/plans"
|
||||||
|
"github.com/hashicorp/terraform/plans/internal/planproto"
|
||||||
|
"github.com/hashicorp/terraform/states"
|
||||||
|
"github.com/hashicorp/terraform/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 := ioutil.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.Changes{
|
||||||
|
RootOutputs: map[string]*plans.OutputChange{},
|
||||||
|
Resources: []*plans.ResourceInstanceChange{},
|
||||||
|
},
|
||||||
|
|
||||||
|
ProviderSHA256s: map[string][]byte{},
|
||||||
|
}
|
||||||
|
|
||||||
|
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.RootOutputs[name] = &plans.OutputChange{
|
||||||
|
Change: *change,
|
||||||
|
Sensitive: rawOC.Sensitive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rawRC := range rawPlan.ResourceChanges {
|
||||||
|
change, err := resourceChangeFromTfplan(rawRC)
|
||||||
|
if err != nil {
|
||||||
|
// errors from resourceChangeFromTfplan already include context
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.Changes.Resources = append(plan.Changes.Resources, change)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, rawHashObj := range rawPlan.ProviderHashes {
|
||||||
|
if len(rawHashObj.Sha256) == 0 {
|
||||||
|
return nil, fmt.Errorf("no SHA256 hash for provider %q plugin", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ProviderSHA256s[name] = rawHashObj.Sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChange, 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.ResourceInstanceChange{}
|
||||||
|
|
||||||
|
moduleAddr := addrs.RootModuleInstance
|
||||||
|
if rawChange.ModulePath != "" {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
moduleAddr, diags = addrs.ParseModuleInstanceStr(rawChange.ModulePath)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return nil, diags.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode addrs.ResourceMode
|
||||||
|
switch rawChange.Mode {
|
||||||
|
case planproto.ResourceInstanceChange_managed:
|
||||||
|
mode = addrs.ManagedResourceMode
|
||||||
|
case planproto.ResourceInstanceChange_data:
|
||||||
|
mode = addrs.DataResourceMode
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("resource has invalid mode %s", rawChange.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
typeName := rawChange.Type
|
||||||
|
name := rawChange.Name
|
||||||
|
|
||||||
|
resAddr := addrs.Resource{
|
||||||
|
Mode: mode,
|
||||||
|
Type: typeName,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
var instKey addrs.InstanceKey
|
||||||
|
switch rawTk := rawChange.InstanceKey.(type) {
|
||||||
|
case *planproto.ResourceInstanceChange_Int:
|
||||||
|
instKey = addrs.IntKey(rawTk.Int)
|
||||||
|
case *planproto.ResourceInstanceChange_Str:
|
||||||
|
instKey = addrs.StringKey(rawTk.Str)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("instance of %s has invalid key type %T", resAddr.Absolute(moduleAddr), rawChange.InstanceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Addr = resAddr.Instance(instKey).Absolute(moduleAddr)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
change, err := changeFromTfplan(rawChange.Change)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid plan for resource %s: %s", ret.Addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Change = *change
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeFromTfplan(rawChange *planproto.Change) (*plans.Change, error) {
|
||||||
|
if rawChange == nil {
|
||||||
|
return nil, fmt.Errorf("change object is absent")
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := &plans.Change{}
|
||||||
|
|
||||||
|
// -1 indicates that there is no index. We'll customize these below
|
||||||
|
// depending on the change action, and then decode.
|
||||||
|
beforeIdx, afterIdx := -1, -1
|
||||||
|
|
||||||
|
switch rawChange.Action {
|
||||||
|
case planproto.Action_NOOP:
|
||||||
|
ret.Action = plans.NoOp
|
||||||
|
beforeIdx = 0
|
||||||
|
afterIdx = 0
|
||||||
|
case planproto.Action_CREATE:
|
||||||
|
ret.Action = plans.Create
|
||||||
|
afterIdx = 0
|
||||||
|
case planproto.Action_READ:
|
||||||
|
ret.Action = plans.Read
|
||||||
|
beforeIdx = 0
|
||||||
|
afterIdx = 1
|
||||||
|
case planproto.Action_UPDATE:
|
||||||
|
ret.Action = plans.Update
|
||||||
|
beforeIdx = 0
|
||||||
|
afterIdx = 1
|
||||||
|
case planproto.Action_REPLACE:
|
||||||
|
ret.Action = plans.Replace
|
||||||
|
beforeIdx = 0
|
||||||
|
afterIdx = 1
|
||||||
|
case planproto.Action_DELETE:
|
||||||
|
ret.Action = plans.Delete
|
||||||
|
beforeIdx = 0
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
rawPlan := &planproto.Plan{
|
||||||
|
Version: tfplanFormatVersion,
|
||||||
|
TerraformVersion: version.String(),
|
||||||
|
ProviderHashes: map[string]*planproto.Hash{},
|
||||||
|
|
||||||
|
Variables: map[string]*planproto.DynamicValue{},
|
||||||
|
OutputChanges: []*planproto.OutputChange{},
|
||||||
|
ResourceChanges: []*planproto.ResourceInstanceChange{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, oc := range plan.Changes.RootOutputs {
|
||||||
|
// 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.Change)
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rc := range plan.Changes.Resources {
|
||||||
|
rawRC, err := resourceChangeToTfplan(rc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawPlan.ResourceChanges = append(rawPlan.ResourceChanges, rawRC)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, hash := range plan.ProviderSHA256s {
|
||||||
|
rawPlan.ProviderHashes[name] = &planproto.Hash{
|
||||||
|
Sha256: hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, val := range plan.VariableValues {
|
||||||
|
rawPlan.Variables[name] = valueToTfplan(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 resourceChangeToTfplan(change *plans.ResourceInstanceChange) (*planproto.ResourceInstanceChange, error) {
|
||||||
|
ret := &planproto.ResourceInstanceChange{}
|
||||||
|
|
||||||
|
ret.ModulePath = change.Addr.Module.String()
|
||||||
|
|
||||||
|
relAddr := change.Addr.Resource
|
||||||
|
|
||||||
|
switch relAddr.Resource.Mode {
|
||||||
|
case addrs.ManagedResourceMode:
|
||||||
|
ret.Mode = planproto.ResourceInstanceChange_managed
|
||||||
|
case addrs.DataResourceMode:
|
||||||
|
ret.Mode = planproto.ResourceInstanceChange_data
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("resource %s has unsupported mode %s", relAddr, relAddr.Resource.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Type = relAddr.Resource.Type
|
||||||
|
ret.Name = relAddr.Resource.Name
|
||||||
|
|
||||||
|
switch tk := relAddr.Key.(type) {
|
||||||
|
case addrs.IntKey:
|
||||||
|
ret.InstanceKey = &planproto.ResourceInstanceChange_Int{
|
||||||
|
Int: int64(tk),
|
||||||
|
}
|
||||||
|
case addrs.StringKey:
|
||||||
|
ret.InstanceKey = &planproto.ResourceInstanceChange_Str{
|
||||||
|
Str: string(tk),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("resource %s has unsupported instance key type %T", relAddr, relAddr.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.DeposedKey = string(change.DeposedKey)
|
||||||
|
|
||||||
|
valChange, err := changeToTfplan(&change.Change)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to serialize resource %s change: %s", relAddr, err)
|
||||||
|
}
|
||||||
|
ret.Change = valChange
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeToTfplan(change *plans.Change) (*planproto.Change, error) {
|
||||||
|
ret := &planproto.Change{}
|
||||||
|
|
||||||
|
before := valueToTfplan(change.Before)
|
||||||
|
after := valueToTfplan(change.After)
|
||||||
|
|
||||||
|
switch change.Action {
|
||||||
|
case plans.NoOp:
|
||||||
|
ret.Action = planproto.Action_NOOP
|
||||||
|
ret.Values = []*planproto.DynamicValue{before} // before and after should be identical
|
||||||
|
case plans.Create:
|
||||||
|
ret.Action = planproto.Action_CREATE
|
||||||
|
ret.Values = []*planproto.DynamicValue{after}
|
||||||
|
case plans.Read:
|
||||||
|
ret.Action = planproto.Action_READ
|
||||||
|
ret.Values = []*planproto.DynamicValue{before, after}
|
||||||
|
case plans.Update:
|
||||||
|
ret.Action = planproto.Action_UPDATE
|
||||||
|
ret.Values = []*planproto.DynamicValue{before, after}
|
||||||
|
case plans.Replace:
|
||||||
|
ret.Action = planproto.Action_REPLACE
|
||||||
|
ret.Values = []*planproto.DynamicValue{before, after}
|
||||||
|
case plans.Delete:
|
||||||
|
ret.Action = planproto.Action_DELETE
|
||||||
|
ret.Values = []*planproto.DynamicValue{before}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid change action %s", change.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue {
|
||||||
|
if val == nil {
|
||||||
|
// protobuf can't represent nil, so we'll represent it as a
|
||||||
|
// DynamicValue that has no serializations at all.
|
||||||
|
return &planproto.DynamicValue{}
|
||||||
|
}
|
||||||
|
return &planproto.DynamicValue{
|
||||||
|
Msgpack: []byte(val),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package planfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
"github.com/hashicorp/terraform/plans"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTFPlanRoundTrip(t *testing.T) {
|
||||||
|
objTy := cty.Object(map[string]cty.Type{
|
||||||
|
"id": cty.String,
|
||||||
|
})
|
||||||
|
|
||||||
|
plan := &plans.Plan{
|
||||||
|
VariableValues: map[string]plans.DynamicValue{
|
||||||
|
"foo": mustNewDynamicValueStr("foo value"),
|
||||||
|
},
|
||||||
|
Changes: &plans.Changes{
|
||||||
|
RootOutputs: map[string]*plans.OutputChange{
|
||||||
|
"bar": {
|
||||||
|
Change: plans.Change{
|
||||||
|
Action: plans.Create,
|
||||||
|
After: mustNewDynamicValueStr("bar value"),
|
||||||
|
},
|
||||||
|
Sensitive: false,
|
||||||
|
},
|
||||||
|
"baz": {
|
||||||
|
Change: plans.Change{
|
||||||
|
Action: plans.NoOp,
|
||||||
|
Before: mustNewDynamicValueStr("baz value"),
|
||||||
|
After: mustNewDynamicValueStr("baz value"),
|
||||||
|
},
|
||||||
|
Sensitive: false,
|
||||||
|
},
|
||||||
|
"secret": {
|
||||||
|
Change: plans.Change{
|
||||||
|
Action: plans.Update,
|
||||||
|
Before: mustNewDynamicValueStr("old secret value"),
|
||||||
|
After: mustNewDynamicValueStr("new secret value"),
|
||||||
|
},
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Resources: []*plans.ResourceInstanceChange{
|
||||||
|
{
|
||||||
|
Addr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "woot",
|
||||||
|
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
|
||||||
|
Change: plans.Change{
|
||||||
|
Action: plans.Replace,
|
||||||
|
Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("foo-bar-baz"),
|
||||||
|
}), objTy),
|
||||||
|
After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
}), objTy),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Addr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "woot",
|
||||||
|
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
|
||||||
|
DeposedKey: "foodface",
|
||||||
|
Change: plans.Change{
|
||||||
|
Action: plans.Delete,
|
||||||
|
Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("bar-baz-foo"),
|
||||||
|
}), objTy),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ProviderSHA256s: map[string][]byte{
|
||||||
|
"test": []byte{
|
||||||
|
0xba, 0x5e, 0x1e, 0x55, 0xb0, 0x1d, 0xfa, 0xce,
|
||||||
|
0xef, 0xfe, 0xc7, 0xed, 0x1a, 0xbe, 0x11, 0xed,
|
||||||
|
0x5c, 0xa1, 0xab, 0x1e, 0xda, 0x7a, 0xba, 0x5e,
|
||||||
|
0x70, 0x7a, 0x11, 0xed, 0xb0, 0x07, 0xab, 0x1e,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := writeTfplan(plan, &buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPlan, err := readTfplan(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
oldDepth := deep.MaxDepth
|
||||||
|
oldCompare := deep.CompareUnexportedFields
|
||||||
|
deep.MaxDepth = 20
|
||||||
|
deep.CompareUnexportedFields = true
|
||||||
|
defer func() {
|
||||||
|
deep.MaxDepth = oldDepth
|
||||||
|
deep.CompareUnexportedFields = oldCompare
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for _, problem := range deep.Equal(newPlan, plan) {
|
||||||
|
t.Error(problem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNewDynamicValue(val cty.Value, ty cty.Type) plans.DynamicValue {
|
||||||
|
ret, err := plans.NewDynamicValue(val, ty)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNewDynamicValueStr(val string) plans.DynamicValue {
|
||||||
|
realVal := cty.StringVal(val)
|
||||||
|
ret, err := plans.NewDynamicValue(realVal, cty.String)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package planfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
|
"github.com/hashicorp/terraform/plans"
|
||||||
|
"github.com/hashicorp/terraform/states/statefile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create creates a new plan file with the given filename, overwriting any
|
||||||
|
// file that might already exist there.
|
||||||
|
//
|
||||||
|
// A plan file contains both a snapshot of the configuration and of the latest
|
||||||
|
// state file in addition to the plan itself, so that Terraform can detect
|
||||||
|
// if the world has changed since the plan was created and thus refuse to
|
||||||
|
// apply it.
|
||||||
|
func Create(filename string, configSnap *configload.Snapshot, stateFile *statefile.File, plan *plans.Plan) error {
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
zw := zip.NewWriter(f)
|
||||||
|
defer zw.Close()
|
||||||
|
|
||||||
|
// tfplan file
|
||||||
|
{
|
||||||
|
w, err := zw.CreateHeader(&zip.FileHeader{
|
||||||
|
Name: tfplanFilename,
|
||||||
|
Method: zip.Deflate,
|
||||||
|
Modified: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tfplan file: %s", err)
|
||||||
|
}
|
||||||
|
err = writeTfplan(plan, w)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write plan: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tfstate file
|
||||||
|
{
|
||||||
|
w, err := zw.CreateHeader(&zip.FileHeader{
|
||||||
|
Name: tfstateFilename,
|
||||||
|
Method: zip.Deflate,
|
||||||
|
Modified: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create embedded tfstate file: %s", err)
|
||||||
|
}
|
||||||
|
err = statefile.Write(stateFile, w)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write state snapshot: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tfconfig directory
|
||||||
|
{
|
||||||
|
err := writeConfigSnapshot(configSnap, zw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write config snapshot: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue