/
state_push.go
177 lines (143 loc) · 4.95 KB
/
state_push.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package command
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/states/statefile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/mitchellh/cli"
)
// StatePushCommand is a Command implementation that shows a single resource.
type StatePushCommand struct {
Meta
StateMeta
}
func (c *StatePushCommand) Run(args []string) int {
args, err := c.Meta.process(args, true)
if err != nil {
return 1
}
var flagForce bool
cmdFlags := c.Meta.defaultFlagSet("state push")
cmdFlags.BoolVar(&flagForce, "force", false, "")
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
return 1
}
args = cmdFlags.Args()
if len(args) != 1 {
c.Ui.Error("Exactly one argument expected.\n")
return cli.RunResultHelp
}
// Determine our reader for the input state. This is the filepath
// or stdin if "-" is given.
var r io.Reader = os.Stdin
if args[0] != "-" {
f, err := os.Open(args[0])
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// Note: we don't need to defer a Close here because we do a close
// automatically below directly after the read.
r = f
}
// Read the state
srcStateFile, err := statefile.Read(r)
if c, ok := r.(io.Closer); ok {
// Close the reader if possible right now since we're done with it.
c.Close()
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err))
return 1
}
// Load the backend
b, backendDiags := c.Backend(nil)
if backendDiags.HasErrors() {
c.showDiagnostics(backendDiags)
return 1
}
// Get the state manager for the currently-selected workspace
env := c.Workspace()
stateMgr, err := b.StateMgr(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
return 1
}
if c.stateLock {
stateLocker := clistate.NewLocker(context.Background(), c.stateLockTimeout, c.Ui, c.Colorize())
if err := stateLocker.Lock(stateMgr, "state-push"); err != nil {
c.Ui.Error(fmt.Sprintf("Error locking state: %s", err))
return 1
}
defer stateLocker.Unlock(nil)
}
if err := stateMgr.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to refresh destination state: %s", err))
return 1
}
if srcStateFile == nil {
// We'll push a new empty state instead
srcStateFile = statemgr.NewStateFile()
}
// Import it, forcing through the lineage/serial if requested and possible.
if err := statemgr.Import(srcStateFile, stateMgr, flagForce); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
return 1
}
if err := stateMgr.WriteState(srcStateFile.State); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
return 1
}
if err := stateMgr.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to persist state: %s", err))
return 1
}
return 0
}
func (c *StatePushCommand) Help() string {
helpText := `
Usage: terraform state push [options] PATH
Update remote state from a local state file at PATH.
This command "pushes" a local state and overwrites remote state
with a local state file. The command will protect you against writing
an older serial or a different state file lineage unless you specify the
"-force" flag.
This command works with local state (it will overwrite the local
state), but is less useful for this use case.
If PATH is "-", then this command will read the state to push from stdin.
Data from stdin is not streamed to the backend: it is loaded completely
(until pipe close), verified, and then pushed.
Options:
-force Write the state even if lineages don't match or the
remote serial is higher.
-lock=true Lock the state file when locking is supported.
-lock-timeout=0s Duration to retry a state lock.
`
return strings.TrimSpace(helpText)
}
func (c *StatePushCommand) Synopsis() string {
return "Update remote state from a local state file"
}
const errStatePushLineage = `
The lineages do not match! The state will not be pushed.
The "lineage" is a unique identifier given to a state on creation. It helps
protect Terraform from overwriting a seemingly unrelated state file since it
represents potentially losing real state.
Please verify you're pushing the correct state. If you're sure you are, you
can force the behavior with the "-force" flag.
`
const errStatePushSerialNewer = `
The destination state has a higher serial number! The state will not be pushed.
A higher serial could indicate that there is data in the destination state
that was not present when the source state was created. As a protection measure,
Terraform will not automatically overwrite this state.
Please verify you're pushing the correct state. If you're sure you are, you
can force the behavior with the "-force" flag.
`