-
Notifications
You must be signed in to change notification settings - Fork 69
/
plugin.go
410 lines (339 loc) · 10.8 KB
/
plugin.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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
package plugin
/*
Here are a bunch of frameworky-helper functions for use when creating a new backup/restore plugin. Important things to remember:
Use plugin.Run() for starting your plugin execution.
Use plugin.PluginInfo to fill out the info for your plugin.
Make your plugin conform to the Plugin interface, by implementing Backup(), Restore(), Retrieve(), and Store(). If they don't make sense, just return plugin.UNSUPPORTED_ACTION, and a helpful errorm essage
plugin.Exec() can be used to easily run external commands sending their stdin/stdout to that of the plugin command. Keep in mind the commands don't get run in a shell, so things like '>', '<', '|' won't work the way you want them to, but you can just run /bin/bash -c <command> to solve that, right?
*/
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/jhunt/go-cli"
env "github.com/jhunt/go-envirotron"
"github.com/pborman/uuid"
)
type Opt struct {
HelpShort bool `cli:"-h"`
HelpFull bool `cli:"--help"`
Debug bool `cli:"-D, --debug",env:"DEBUG"`
Version bool `cli:"-v, --version"`
Endpoint string `cli:"-e,--endpoint"`
Key string `cli:"-k, --key"`
Text bool `cli:"--text"`
Info struct{} `cli:"info"`
Example struct{} `cli:"example"`
Validate struct{} `cli:"validate"`
Backup struct{} `cli:"backup"`
Restore struct{} `cli:"restore"`
Store struct{} `cli:"store"`
Retrieve struct{} `cli:"retrieve"`
Purge struct{} `cli:"purge"`
}
type Plugin interface {
Validate(ShieldEndpoint) error
Backup(ShieldEndpoint) error
Restore(ShieldEndpoint) error
Store(ShieldEndpoint) (string, int64, error)
Retrieve(ShieldEndpoint, string) error
Purge(ShieldEndpoint, string) error
Meta() PluginInfo
}
type Field struct {
Mode string `json:"mode"`
Name string `json:"name"`
Type string `json:"type"`
Title string `json:"title,omitempty"`
Help string `json:"help,omitempty"`
Example string `json:"example,omitempty"`
Default string `json:"default,omitempty"`
Enum []string `json:"enum,omitempty"`
Required bool `json:"required,omitempty"`
}
type PluginInfo struct {
Name string `json:"name"`
Author string `json:"author"`
Version string `json:"version"`
Features PluginFeatures `json:"features"`
Example string `json:"-"`
Defaults string `json:"-"`
Fields []Field `json:"fields"`
}
type PluginFeatures struct {
Target string `json:"target"`
Store string `json:"store"`
}
var debug bool
func DEBUG(format string, args ...interface{}) {
if debug {
content := fmt.Sprintf(format, args...)
lines := strings.Split(content, "\n")
for i, line := range lines {
lines[i] = "DEBUG> " + line
}
content = strings.Join(lines, "\n")
fmt.Fprintf(os.Stderr, "%s\n", content)
}
}
func Run(p Plugin) {
var opt Opt
info := p.Meta()
env.Override(&opt)
command, args, err := cli.Parse(&opt)
if err != nil {
fmt.Fprintf(os.Stderr, "!!! %s\n", err.Error())
fmt.Fprintf(os.Stderr, "USAGE: %s [OPTIONS...] COMMAND [OPTIONS...]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Try %s --help for more information.\n", os.Args[0])
os.Exit(USAGE)
}
if opt.Debug {
debug = true
}
if opt.HelpShort {
fmt.Fprintf(os.Stderr, "%s v%s - %s\n", info.Name, info.Version, info.Author)
fmt.Fprintf(os.Stderr, "USAGE: %s [OPTIONS...] COMMAND [OPTIONS...]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, `OPTIONS
-h, --help Get some help. (--help provides more detail; -h, less)
-D, --debug Enable debugging.
-v, --version Print the version of this plugin and exit.
COMMANDS
info Print plugin information (name / version / author)
validate -e JSON Validate endpoint JSON/configuration
backup -e JSON Backup a target
restore -e JSON Replay a backup archive to a target
store -e JSON [--text] Store a backup archive
retrieve -e JSON -k KEY Stream a backup archive from storage
purge -e JSON -k KEY Delete a backup archive from storage
`)
if info.Example != "" {
fmt.Fprintf(os.Stderr, "\nEXAMPLE ENDPOINT CONFIGURATION\n%s\n", info.Example)
}
if info.Defaults != "" {
fmt.Fprintf(os.Stderr, "\nDEFAULT ENDPOINT\n%s\n", info.Defaults)
}
os.Exit(0)
}
if opt.HelpFull {
fmt.Fprintf(os.Stderr, "%s v%s - %s\n", info.Name, info.Version, info.Author)
fmt.Fprintf(os.Stderr, "USAGE: %s [OPTIONS...] COMMAND [OPTIONS...]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, `OPTIONS
-h, --help Get some help. (--help provides more detail; -h, less)
-D, --debug Enable debugging.
-v, --version Print the version of this plugin and exit.
-e, --endpoint JSON string representing what to backup / where to back it up.
GENERAL COMMANDS
info
Print information about this plugin, in JSON format, to standard output.
validate --endpoint ENDPOINT-JSON
Validates the given ENDPOINT-JSON to ensure that it is (a) well-formed
JSON data, and (b) is semantically valid for this plugin. Checks that
required configuration is set, and verifies the format and suitability
of the given configuration.
BACKUP COMMANDS
backup --endpoint TARGET-ENDPOINT-JSON
Perform a backup of the indicated target endpoint. The raw (uncompressed)
backup archive will be written to standard output.
restore --endpoint TARGET-ENDPOINT-JSON
Reads a raw (uncompressed) backup archive on standard input and attempts to
replay it to the given target.
STORAGE COMMANDS
store --endpoint STORE-ENDPOINT-JSON [--text]
Reads a compressed backup archive on standard input and attempts to
persist it to the backing storage system indicated by --endpoint.
Upon success, writes the STORAGE-HANDLE to standard output.
If --text is given, the STORAGE-HANDLE is printed on a single line,
without any additional whitespace or formatting. By default, it will
be printed inside of a JSON structure.
retrieve --key STORAGE-HANDLE --endpoint STORE-ENDPOINT-JSON
Retrieves a compressed backup archive from the backing storage,
using the STORAGE-HANDLE given by a previous 'store' command, and
writes it to standard output.
purge --key STORAGE-HANDLE --endpoint STORE-ENDPOINT-JSON
Removes a backup archive from the backing storage, using the
STORAGE-HANDLE given by a previous 'store' command.
`)
os.Exit(0)
}
if len(args) != 0 {
fmt.Fprintf(os.Stderr, "extra arguments found, starting at %v\n", args[0])
fmt.Fprintf(os.Stderr, "USAGE: %s [OPTIONS...] COMMAND [OPTIONS...]\n\n", info.Name)
os.Exit(USAGE)
}
if opt.Version {
fmt.Printf("%s v%s - %s\n", info.Name, info.Version, info.Author)
os.Exit(0)
}
switch command {
case "info":
if os.Getenv("SHIELD_PEDANTIC_INFO") != "" {
/* validate the plugin info with great pedantry */
ok := true
for i, f := range info.Fields {
name := f.Name
if f.Name == "" {
fmt.Fprintf(os.Stderr, "!! %s: field #%d has no name\n", info.Name, i+1)
name = fmt.Sprintf("field #%d", i+1)
ok = false
}
if f.Type == "" {
fmt.Fprintf(os.Stderr, "!! %s: %s has no type\n", info.Name, name)
ok = false
}
if f.Title == "" {
fmt.Fprintf(os.Stderr, "!! %s: %s has no title\n", info.Name, name)
ok = false
}
if f.Help == "" {
fmt.Fprintf(os.Stderr, "!! %s: %s has no help\n", info.Name, name)
ok = false
} else if !strings.HasSuffix(f.Help, ".") {
fmt.Fprintf(os.Stderr, "!! %s: %s help field does not end in a period.\n", info.Name, name)
ok = false
}
if f.Type == "enum" {
if len(f.Enum) == 0 {
fmt.Fprintf(os.Stderr, "!! %s: %s is defined as an enum, but specifies no allowed values.\n", info.Name, name)
ok = false
}
} else {
if len(f.Enum) != 0 {
fmt.Fprintf(os.Stderr, "!! %s: %s is not defined as an enum, but has the following allowed values: [%s]\n", info.Name, name, f.Enum)
ok = false
}
}
}
if !ok {
os.Exit(1)
}
}
json, err := json.MarshalIndent(info, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(JSON_FAILURE)
}
fmt.Printf("%s\n", json)
os.Exit(0)
default:
err = dispatch(p, command, opt)
DEBUG("'%s' action returned %#v", command, err)
if err != nil {
switch err.(type) {
case UnsupportedActionError:
if err.(UnsupportedActionError).Action == "" {
e := err.(UnsupportedActionError)
e.Action = command
err = e
}
}
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(codeForError(err))
}
}
os.Exit(0)
}
func dispatch(p Plugin, mode string, opt Opt) error {
var err error
var key string
var size int64
var endpoint ShieldEndpoint
DEBUG("'%s' action requested with options %#v", mode, opt)
switch mode {
case "validate":
endpoint, err = getEndpoint(opt.Endpoint)
if err != nil {
return err
}
err = p.Validate(endpoint)
case "backup":
endpoint, err = getEndpoint(opt.Endpoint)
if err != nil {
return err
}
err = p.Backup(endpoint)
case "restore":
endpoint, err = getEndpoint(opt.Endpoint)
if err != nil {
return err
}
err = p.Restore(endpoint)
case "store":
endpoint, err = getEndpoint(opt.Endpoint)
if err != nil {
return err
}
key, size, err = p.Store(endpoint)
if opt.Text {
fmt.Printf("%s\n", key)
} else {
output, jsonErr := json.MarshalIndent(struct {
Key string `json:"key"`
Size int64 `json:"archive_size"`
}{Key: key, Size: size}, "", " ")
if jsonErr != nil {
return JSONError{Err: fmt.Sprintf("Could not JSON encode blob key: %s", jsonErr.Error())}
}
fmt.Printf("%s\n", string(output))
}
case "retrieve":
endpoint, err = getEndpoint(opt.Endpoint)
if err != nil {
return err
}
if opt.Key == "" {
return MissingRestoreKeyError{}
}
err = p.Retrieve(endpoint, opt.Key)
case "purge":
endpoint, err = getEndpoint(opt.Endpoint)
if err != nil {
return err
}
if opt.Key == "" {
return MissingRestoreKeyError{}
}
err = p.Purge(endpoint, opt.Key)
default:
return UnsupportedActionError{Action: mode}
}
return err
}
func pluginInfo(p Plugin) error {
json, err := json.MarshalIndent(p.Meta(), "", " ")
if err != nil {
return JSONError{Err: fmt.Sprintf("Could not create plugin metadata output: %s", err.Error())}
}
fmt.Printf("%s\n", json)
return nil
}
func GenUUID() string {
return uuid.New()
}
func Redact(raw string) string {
return fmt.Sprintf("<redacted>%s</redacted>", raw)
}
func codeForError(e error) int {
var code int
if e != nil {
switch e.(type) {
case UnsupportedActionError:
code = UNSUPPORTED_ACTION
case EndpointMissingRequiredDataError:
code = ENDPOINT_MISSING_KEY
case EndpointDataTypeMismatchError:
code = ENDPOINT_BAD_DATA
case ExecFailure:
code = EXEC_FAILURE
case JSONError:
code = JSON_FAILURE
case MissingRestoreKeyError:
code = RESTORE_KEY_REQUIRED
default:
code = PLUGIN_FAILURE
}
} else {
code = SUCCESS
}
return code
}