-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
270 lines (233 loc) · 8.08 KB
/
main.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
package main
import (
"bytes"
"encoding/json"
"fmt"
"github.com/docopt/docopt-go"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/terraform"
"github.com/mattn/go-isatty"
"github.com/yudai/gojsondiff"
"github.com/yudai/gojsondiff/formatter"
"io/ioutil"
"os"
)
// 1 2 3 4 5 6 7 8
//345678901234567890123456789012345678901234567890123456789012345678901234567890
const usage = `terraform-ebs-attach
Usage:
tf-ebs-attach import [-i f] [-o f] <inst-name> <vol-name> <att-name> <dev>
tf-ebs-attach diff [-i f] [-c m] <inst-name> <vol-name> <att-name> <dev>
tf-ebs-attach show <inst-id> <vol-name> <vol-id> <att-name> <dev>
tf-ebs-attach -h|--help
This tool lets you "import" an AWS EBS volume attachment into your Terraform
state file.
While Terraform lets you import AWS instances and EBS volumes, it doesn't seem
to support importing the synthetic "aws_volume_attachment" resource that has no
identifiable counterpart in AWS, so this hack provides a workaround.
Options:
-i file Read existing Terraform state from "file" [default: terraform.tfstate]
-o file Write updated Terraform state to "file" [default: terraform.tfstate]
-c mode Use coloured output (mode = auto/no/yes) [default: auto]
inst-name: Name of the "aws_instance" resource in your Terraform code
vol-name: Name of the "aws_ebs_volume" resource in your Terraform code
att-name: Name of the "aws_volume_attachment" resource in your Terraform code
inst-id: EC2 Instance ID (i-abcd123)
vol-id: EBS Volume ID (vol-abcd123)
dev: Value of "device_name" from "aws_volume_attachment"
Modes:
import: Reads in a terraform state file, locates the definitions for
<inst-name> and <vol-name> and injects a new definition for the volume
attachment <vol-name>.
diff: Prints a diff of the changes that would be made to the input file
show: Prints out the resource object that would be inserted given the
specified instance and volume. Doesn't use a terraform state file.
Examples:
tf-ebs-attach import mysrv mysrv_dsk0 mysrv_dsk0_attch /dev/sdg
tf-ebs-attach diff -i foo.state mysrv mysrv_dsk0 mysrv_dsk0_attch /dev/sdg
tf-ebs-attach show i-abc123 mysrv_dsk0 vol-123abc mysrv_dsk0_att /dev/sdg
`
func main() {
opts, err := docopt.ParseDoc(usage)
if err != nil {
die("Internal error parsing docopt string: %s", err)
}
switch os.Args[1] {
case "show":
showMode(opts)
case "diff":
diffMode(opts)
case "import":
importMode(opts)
}
}
func die(message string, err error) {
if err != nil {
fmt.Printf(message+"\n", err)
} else {
fmt.Printf(message + "\n")
}
os.Exit(1)
}
// Show the ResourceState that would be created from the values in opts
func showMode(opts docopt.Opts) {
instanceID, _ := opts.String("<inst-id>")
volumeName, _ := opts.String("<vol-name>")
volumeID, _ := opts.String("<vol-id>")
attachmentName, _ := opts.String("<att-name>")
deviceName, _ := opts.String("<dev>")
result := make(map[string]*terraform.ResourceState)
result["aws_volume_attachment."+attachmentName] =
newAwsVolumeAttachmentState(instanceID, volumeName, volumeID, deviceName)
outputData, err := json.MarshalIndent(result, "", " ")
if err != nil {
die("Error encoding output to JSON: %s", err)
}
fmt.Print(string(outputData) + "\n")
}
// Show a text diff between the current tfstate ("-i") and the result of importing
// the attachment specified in opts
func diffMode(opts docopt.Opts) {
// Read and modify tfstate
tfstate, inputBytes := readTfState(opts)
injectVolumeAttachment(opts, &tfstate)
outputBytes, err := json.MarshalIndent(tfstate, "", " ")
if err != nil {
die("Error encoding output to JSON: %s", err)
}
// Generate diff
colors := false
cArg, _ := opts.String("-c")
switch cArg {
case "":
fallthrough
case "auto":
colors = isatty.IsTerminal(os.Stdout.Fd())
case "yes":
colors = true
}
diff, err := gojsondiff.New().Compare(inputBytes, outputBytes)
if err != nil {
die("Error comparing JSON: %s", err)
}
var inputJson map[string]interface{}
err = json.Unmarshal(inputBytes, &inputJson)
if err != nil {
die("Error unmarshaling JSON: %s", err)
}
diffString, err := formatter.NewAsciiFormatter(
inputJson,
formatter.AsciiFormatterConfig{
ShowArrayIndex: true,
Coloring: colors,
},
).Format(diff)
if err != nil {
die("Error formatting diff: %s", err)
}
fmt.Printf(diffString)
}
// Import the attachment specified in opts, reading from "-i", writing to "-o"
func importMode(opts docopt.Opts) {
// Read input file
tfstate, _ := readTfState(opts)
// Modify it
injectVolumeAttachment(opts, &tfstate)
// Encode and write out tfstate
writeTfState(opts, tfstate)
}
// Read tfstate from the file specified by "-i"
func readTfState(opts docopt.Opts) (terraform.State, []byte) {
// Parse options
inputFileName, _ := opts.String("-i")
if inputFileName == "-" {
inputFileName = "/dev/stdin"
}
if inputFileName == "" {
inputFileName = "terraform.tfstate"
}
// Read in Terraform state
tfstate := terraform.State{}
inputData, err := ioutil.ReadFile(inputFileName)
if err != nil {
die("Error reading input file: %s", err)
}
if err = json.Unmarshal(inputData, &tfstate); err != nil {
die("Error parsing input file as JSON: %s", err)
}
return tfstate, inputData
}
// Write out the tfstate to the file specified by "-o"
func writeTfState(opts docopt.Opts, tfstate terraform.State) {
outputFileName, _ := opts.String("-o")
if outputFileName == "-" {
outputFileName = "/dev/stdout"
}
if outputFileName == "" {
outputFileName = "terraform.tfstate"
}
outputData, err := json.MarshalIndent(tfstate, "", " ")
if err != nil {
die("Error encoding output to JSON: %s", err)
}
outputData = append(outputData, []byte("\n")[0])
err = ioutil.WriteFile(outputFileName, outputData, 0644)
if err != nil {
die("Error writing output file: %s", err)
}
}
// Modify the given tfstate by adding the volume attachment specified in opts
func injectVolumeAttachment(opts docopt.Opts, tfstate *terraform.State) {
instanceName, _ := opts.String("<inst-name>")
volumeName, _ := opts.String("<vol-name>")
attachmentName, _ := opts.String("<att-name>")
deviceName, _ := opts.String("<dev>")
// Locate our instance and volume
instanceResourceID := "aws_instance." + instanceName
volumeResourceID := "aws_ebs_volume." + volumeName
for _, moduleState := range tfstate.Modules {
//fmt.Printf("moduleState[%d]: %+v\n", i, moduleState)
instanceState, found := moduleState.Resources[instanceResourceID]
if !found {
continue
}
volumeState, found := moduleState.Resources[volumeResourceID]
if found {
moduleState.Resources["aws_volume_attachment."+attachmentName] =
newAwsVolumeAttachmentState(instanceState.Primary.ID, volumeName, volumeState.Primary.ID, deviceName)
return
}
}
die(fmt.Sprintf("Could not locate module in tfstate containing (\"%s\", \"%s\")",
instanceResourceID, volumeResourceID), nil)
}
// Generate a new ResourceState describing our volume attachment
func newAwsVolumeAttachmentState(instanceID, volumeName, volumeID, deviceName string) *terraform.ResourceState {
return &terraform.ResourceState{
Type: "aws_volume_attachment",
Dependencies: []string{
fmt.Sprintf("aws_ebs_volume.%s", volumeName),
},
Primary: &terraform.InstanceState{
ID: volumeAttachmentID(deviceName, volumeID, instanceID),
Attributes: map[string]string{
"id": volumeAttachmentID(deviceName, volumeID, instanceID),
"device_name": deviceName,
"instance_id": instanceID,
"volume_id": volumeID,
},
Meta: make(map[string]interface{}),
Tainted: false,
},
Deposed: []*terraform.InstanceState{},
}
}
// Calculate the "vai-xxx" value
// From https://github.com/foxsy/tfvolattid/blob/master/tfvolattid.go
func volumeAttachmentID(name, volumeID, instanceID string) string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%s-", name))
buf.WriteString(fmt.Sprintf("%s-", instanceID))
buf.WriteString(fmt.Sprintf("%s-", volumeID))
return fmt.Sprintf("vai-%d", hashcode.String(buf.String()))
}