forked from hashicorp/terraform
/
remote.go
359 lines (305 loc) · 9.86 KB
/
remote.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package command
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
)
// remoteCommandConfig is used to encapsulate our configuration
type remoteCommandConfig struct {
disableRemote bool
pullOnDisable bool
statePath string
backupPath string
}
// RemoteCommand is a Command implementation that is used to
// enable and disable remote state management
type RemoteCommand struct {
Meta
conf remoteCommandConfig
remoteConf terraform.RemoteState
}
func (c *RemoteCommand) Run(args []string) int {
args = c.Meta.process(args, false)
var address, accessToken, name, path string
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
cmdFlags.StringVar(&address, "address", "", "")
cmdFlags.StringVar(&accessToken, "access-token", "", "")
cmdFlags.StringVar(&name, "name", "", "")
cmdFlags.StringVar(&path, "path", "", "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Show help if given no inputs
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" &&
name == "" && accessToken == "" {
cmdFlags.Usage()
return 1
}
// Populate the various configurations
c.remoteConf.Config = map[string]string{
"address": address,
"access_token": accessToken,
"name": name,
"path": path,
}
// Check if have an existing local state file
haveLocal, err := remote.HaveLocalState()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err))
return 1
}
// Check if we have the non-managed state file
haveNonManaged, err := remote.ExistsFile(c.conf.statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err))
return 1
}
// Check if remote state is being disabled
if c.conf.disableRemote {
if !haveLocal {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1
}
if haveNonManaged {
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath))
return 1
}
return c.disableRemoteState()
}
// Ensure there is no conflict
switch {
case haveLocal && haveNonManaged:
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath))
return 1
case !haveLocal && !haveNonManaged:
// If we don't have either state file, initialize a blank state file
return c.initBlankState()
case haveLocal && !haveNonManaged:
// Update the remote state target potentially
return c.updateRemoteConfig()
case !haveLocal && haveNonManaged:
// Enable remote state management
return c.enableRemoteState()
}
panic("unhandled case")
}
// disableRemoteState is used to disable remote state management,
// and move the state file into place.
func (c *RemoteCommand) disableRemoteState() int {
// Get the local state
local, _, err := remote.ReadLocalState()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
return 1
}
// Ensure we have the latest state before disabling
if c.conf.pullOnDisable {
log.Printf("[INFO] Refreshing local state from remote server")
change, err := remote.RefreshState(local.Remote)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %v", err))
return 1
}
// Exit if we were unable to update
if !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change))
return 1
} else {
log.Printf("[INFO] %s", change)
}
// Reload the local state after the refresh
local, _, err = remote.ReadLocalState()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
return 1
}
}
// Clear the remote management, and copy into place
local.Remote = nil
fh, err := os.Create(c.conf.statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to create state file '%s': %v",
c.conf.statePath, err))
return 1
}
defer fh.Close()
if err := terraform.WriteState(local, fh); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %v",
c.conf.statePath, err))
return 1
}
// Remove the old state file
path, err := remote.HiddenStatePath()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get local state path: %v", err))
return 1
}
if err := os.Remove(path); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1
}
return 0
}
// validateRemoteConfig is used to verify that the remote configuration
// we have is valid
func (c *RemoteCommand) validateRemoteConfig() error {
err := remote.ValidConfig(&c.remoteConf)
if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
}
return err
}
// initBlank state is used to initialize a blank state that is
// remote enabled
func (c *RemoteCommand) initBlankState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make the hidden directory
if err := remote.EnsureDirectory(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Make a blank state, attach the remote configuration
blank := terraform.NewState()
blank.Remote = &c.remoteConf
// Persist the state
if err := remote.PersistState(blank); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
// Success!
c.Ui.Output("Initialized blank state with remote state enabled!")
return 0
}
// updateRemoteConfig is used to update the configuration of the
// remote state store
func (c *RemoteCommand) updateRemoteConfig() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read in the local state
local, _, err := remote.ReadLocalState()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
return 1
}
// Update the configuration
local.Remote = &c.remoteConf
if err := remote.PersistState(local); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Success!
c.Ui.Output("Remote configuration updated")
return 0
}
// enableRemoteState is used to enable remote state management
// and to move a state file into place
func (c *RemoteCommand) enableRemoteState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make the hidden directory
if err := remote.EnsureDirectory(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Read the provided state file
raw, err := ioutil.ReadFile(c.conf.statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read '%s': %v", c.conf.statePath, err))
return 1
}
state, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to decode '%s': %v", c.conf.statePath, err))
return 1
}
// Backup the state file before we modify it
backupPath := c.conf.backupPath
if backupPath != "-" {
// Provide default backup path if none provided
if backupPath == "" {
backupPath = c.conf.statePath + DefaultBackupExtention
}
log.Printf("[INFO] Writing backup state to: %s", backupPath)
f, err := os.Create(backupPath)
if err == nil {
err = terraform.WriteState(state, f)
f.Close()
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
}
// Update the local configuration, move into place
state.Remote = &c.remoteConf
if err := remote.PersistState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Remove the state file
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
if err := os.Remove(c.conf.statePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
c.conf.statePath, err))
return 1
}
// Success!
c.Ui.Output("Remote state management enabled")
return 0
}
func (c *RemoteCommand) Help() string {
helpText := `
Usage: terraform remote [options]
Configures Terraform to use a remote state server. This allows state
to be pulled down when necessary and then pushed to the server when
updated. In this mode, the state file does not need to be stored durably
since the remote server provides the durability.
Options:
-address=url URL of the remote storage server.
Required for HTTP backend, optional for Atlas and Consul.
-access-token=token Authentication token for state storage server.
Required for Atlas backend, optional for Consul.
-backend=Atlas Specifies the type of remote backend. Must be one
of Atlas, Consul, or HTTP. Defaults to Atlas.
-backup=path Path to backup the existing state file before
modifying. Defaults to the "-state" path with
".backup" extension. Set to "-" to disable backup.
-disable Disables remote state management and migrates the state
to the -state path.
-name=name Name of the state file in the state storage server.
Required for Atlas backend.
-path=path Path of the remote state in Consul. Required for the
Consul backend.
-pull=true Controls if the remote state is pulled before disabling.
This defaults to true to ensure the latest state is cached
before disabling.
-state=path Path to read state. Defaults to "terraform.tfstate"
unless remote state is enabled.
`
return strings.TrimSpace(helpText)
}
func (c *RemoteCommand) Synopsis() string {
return "Configures remote state management"
}