-
Notifications
You must be signed in to change notification settings - Fork 8
/
docker.go
307 lines (258 loc) · 7.53 KB
/
docker.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
package docker
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path"
"strings"
)
type (
// Login defines Docker login parameters.
Login struct {
Registry string // Docker registry address
Username string // Docker registry username
Password string // Docker registry password
Email string // Docker registry email
}
Host struct {
Host string // Docker host string, e.g.: tcp://example.com:2376
UseTLS bool // Authenticate server based on public/default CA pool
TLSVerify bool // Authenticate server based on given CA
}
Deploy struct {
Name string // Docker deploy stack name
Compose []string // Docker compose file(s)
Prune bool // Docker deploy prune
}
Certs struct {
TLSKey string // Contents of key.pem
TLSCert string // Contents of cert.pem
TLSCACert string // Contents of ca.pem
}
SSH struct {
Key string // Contents of ssh key
}
// Plugin defines the Docker plugin parameters.
Plugin struct {
Login Login // Docker login configuration
Deploy Deploy // Docker stack deploy configuration
Certs Certs // Docker certs configuration
SSH SSH // Docker ssh configuration
Host Host // Docker host and global configuration
}
)
const dockerExe = "/usr/bin/docker"
const sshHostAlias = "remote"
// Exec executes the plugin step
func (p Plugin) Exec() error {
var envs []string
if strings.HasPrefix(p.Host.Host, "tcp://") {
envs = append(envs, fmt.Sprintf("DOCKER_HOST=%s", p.Host.Host))
homedir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("cannot get homedir: %w", err)
}
certDir := path.Join(homedir, ".docker/certs")
err = setupCerts(p.Certs, p.Host, certDir)
if err != nil {
return fmt.Errorf("cannot setup certificates: %w", err)
}
envs = append(envs, fmt.Sprintf("DOCKER_CERT_PATH=%s", certDir))
} else if strings.HasPrefix(p.Host.Host, "ssh://") {
err := setupSSH(p.SSH, p.Host)
if err != nil {
return fmt.Errorf("failed to setup ssh: %w", err)
}
envs = append(envs, fmt.Sprintf("DOCKER_HOST=ssh://%s", sshHostAlias))
} else if p.Host.Host != "" {
envs = append(envs, fmt.Sprintf("DOCKER_HOST=%s", p.Host.Host))
}
if p.Deploy.Name == "" {
return fmt.Errorf("docker stack name must be present")
}
if p.Host.TLSVerify {
envs = append(envs, "DOCKER_TLS_VERIFY=1")
} else if p.Host.UseTLS {
envs = append(envs, "DOCKER_TLS=1")
}
var registryAuth bool
// login to the Docker registry
if p.Login.Password != "" {
registryAuth = true
cmd := commandLogin(p.Login)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = envs
err := cmd.Run()
if err != nil {
return fmt.Errorf("error authenticating: %s", err)
}
} else {
registryAuth = false
fmt.Println("Registry credentials not provided. Guest mode enabled.")
}
var cmds []*exec.Cmd
cmds = append(cmds, commandVersion()) // docker version
cmds = append(cmds, commandInfo()) // docker info
cmds = append(cmds, commandDeploy(p.Deploy, registryAuth)) // docker stack deploy
// execute all commands in batch mode.
for _, cmd := range cmds {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), envs...)
trace(cmd)
err := cmd.Run()
if err != nil {
return err
}
}
return nil
}
func base64Decode(str string) []byte {
result, err := base64.StdEncoding.DecodeString(str)
if err != nil {
// decode failed, use string as is
return []byte(str)
} else {
return result
}
}
func setupCerts(certs Certs, host Host, certDir string) error {
if host.UseTLS || host.TLSVerify {
// create certs directory
err := os.MkdirAll(certDir, 0755)
if err != nil {
return fmt.Errorf("cannot create cert directory: %s", err)
}
// both certs must be either present or absent
if (certs.TLSKey != "") == (certs.TLSCert != "") {
// both certs are present
if certs.TLSKey != "" {
err := ioutil.WriteFile(path.Join(certDir, "key.pem"), base64Decode(certs.TLSKey), 0600)
if err != nil {
return fmt.Errorf("cannot create key.pem: %s", err)
}
err = ioutil.WriteFile(path.Join(certDir, "cert.pem"), base64Decode(certs.TLSCert), 0644)
if err != nil {
return fmt.Errorf("cannot create cert.pem: %s", err)
}
}
} else if certs.TLSKey != "" {
fmt.Printf("the client certificate must be present")
} else {
fmt.Printf("the client key must be present")
}
if host.TLSVerify {
// CA cert must be present
if certs.TLSCACert != "" {
err := ioutil.WriteFile(path.Join(certDir, "ca.pem"), base64Decode(certs.TLSCACert), 0644)
if err != nil {
return fmt.Errorf("cannot create ca.pem: %s", err)
}
} else {
return fmt.Errorf("cannot use tlsverify without a given CA")
}
}
}
return nil
}
func setupSSH(ssh SSH, host Host) error {
sshUrl, err := url.Parse(host.Host)
if err != nil {
return fmt.Errorf("invalid ssh host: %w", err)
}
homedir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("cannot get homedir: %w", err)
}
sshDir := path.Join(homedir, ".ssh")
pemPath := path.Join(sshDir, "key.pem")
if _, err := os.Stat(path.Join(sshDir, "config")); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("ssh config file already exist or cannot be checked")
}
err = os.MkdirAll(sshDir, 0755)
if err != nil {
return fmt.Errorf("cannot create ssh directory: %w", err)
}
err = ioutil.WriteFile(pemPath, base64Decode(ssh.Key), 0600)
if err != nil {
return fmt.Errorf("cannot create ssh key: %s", err)
}
var sshConfig []byte
w := bytes.NewBuffer(sshConfig)
w.WriteString(fmt.Sprintf("Host %s\n", sshHostAlias))
w.WriteString(fmt.Sprintf(" HostName %s\n", sshUrl.Hostname()))
if sshUrl.User.Username() != "" {
w.WriteString(fmt.Sprintf(" User %s\n", sshUrl.User.Username()))
}
if sshUrl.Port() != "" {
w.WriteString(fmt.Sprintf(" Port %s\n", sshUrl.Port()))
}
if ssh.Key != "" {
w.WriteString(fmt.Sprintf(" IdentityFile %s\n", pemPath))
}
w.WriteString(" StrictHostKeyChecking no\n")
err = ioutil.WriteFile(path.Join(sshDir, "config"), w.Bytes(), 0600)
if err != nil {
return fmt.Errorf("cannot write ssh config file: %w", err)
}
fmt.Printf("SSH Config:\n\n%s\n", w.String())
return nil
}
// helper function to create the docker login command.
func commandLogin(login Login) *exec.Cmd {
if login.Email != "" {
return commandLoginEmail(login)
}
return exec.Command(
dockerExe, "login",
"-u", login.Username,
"-p", login.Password,
login.Registry,
)
}
func commandLoginEmail(login Login) *exec.Cmd {
return exec.Command(
dockerExe, "login",
"-u", login.Username,
"-p", login.Password,
"-e", login.Email,
login.Registry,
)
}
// helper function to create the docker info command.
func commandVersion() *exec.Cmd {
return exec.Command(dockerExe, "version")
}
// helper function to create the docker info command.
func commandInfo() *exec.Cmd {
return exec.Command(dockerExe, "info")
}
// helper function to create the docker stack deploy command.
func commandDeploy(deploy Deploy, auth bool) *exec.Cmd {
args := []string{
"stack",
"deploy",
deploy.Name,
}
for _, compose := range deploy.Compose {
args = append(args, "-c", compose)
}
if deploy.Prune {
args = append(args, "--prune")
}
if auth {
args = append(args, "--with-registry-auth")
}
return exec.Command(dockerExe, args...)
}
// trace writes each command to stdout with the command wrapped in an xml
// tag so that it can be extracted and displayed in the logs.
func trace(cmd *exec.Cmd) {
_, _ = fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " "))
}