forked from donny-dont/drone-exec
-
Notifications
You must be signed in to change notification settings - Fork 0
/
exec.go
273 lines (249 loc) · 8.19 KB
/
exec.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
package exec
import (
"fmt"
"io"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"github.com/drone/drone-exec/docker"
"github.com/drone/drone-exec/parser"
"github.com/drone/drone-exec/runner"
"github.com/drone/drone-exec/yaml"
"github.com/drone/drone-exec/yaml/inject"
"github.com/drone/drone-exec/yaml/path"
"github.com/drone/drone-exec/yaml/secure"
"github.com/drone/drone-exec/yaml/shasum"
"github.com/drone/drone-plugin-go/plugin"
"github.com/samalba/dockerclient"
log "github.com/Sirupsen/logrus"
)
// Payload defines the raw plugin payload that
// stores the build metadata and configuration.
type Payload struct {
Yaml string `json:"config"`
YamlEnc string `json:"secret"`
Repo *plugin.Repo `json:"repo"`
Build *plugin.Build `json:"build"`
BuildLast *plugin.Build `json:"build_last"`
Job *plugin.Job `json:"job"`
Netrc *plugin.Netrc `json:"netrc"`
Keys *plugin.Keypair `json:"keys"`
System *plugin.System `json:"system"`
Workspace *plugin.Workspace `json:"workspace"`
}
// Options defines execution options.
type Options struct {
Cache bool // execute cache steps
Clone bool // execute clone steps
Build bool // execute build steps
Deploy bool // execute deploy steps
Notify bool // execute notify steps
Debug bool // execute in debug mode
Force bool // force pull plugin images
Mount string // mounts the volume on the host machine
}
// Error reports an error during execution of a build.
type Error struct {
ExitCode int // exit code
}
func (e *Error) Error() string { return fmt.Sprintf("build failed (exit code %d)", e.ExitCode) }
// Exec executes a build with the given payload and options. If the
// build fails, an *Error is returned.
func Exec(payload Payload, opt Options, outw, errw io.Writer) error {
var sec *secure.Secure
if payload.Keys != nil && len(payload.YamlEnc) != 0 {
var err error
sec, err = secure.Parse(payload.YamlEnc, payload.Keys.Private)
if err != nil {
return fmt.Errorf("decrypting encrypted secrets: %s", err)
}
log.Debugln("Successfully decrypted secrets")
}
// TODO This block of code (and the above block) need to be cleaned
// up and written in a manner that facilitates better unit testing.
if sec != nil {
verified := shasum.Check(payload.Yaml, sec.Checksum)
// the checksum should be invalidated if the repository is
// public, and the build is a pull request, and the checksum
// value was not provided.
if payload.Build.Event == plugin.EventPull && !payload.Repo.IsPrivate && len(sec.Checksum) == 0 {
verified = false
}
if verified {
// if verified we know that the checksum protects secrets when injected
// into the compose section of the .drone.yml and we trust that the user
// has taken precautions for pull_request events.
log.Debugln("Injected secrets into Yaml")
payload.Yaml = inject.Inject(payload.Yaml, sec.Environment.Map())
} else {
// if we can't validate the Yaml file we don't inject
// secrets, and therefore shouldn't bother running the
// deploy and notify tests.
opt.Deploy = false
opt.Notify = false
log.Errorln("Unable to validate Yaml checksum. This means that secrets will not be injected. In addition the Deploy and Notify steps will not be executed. To resolve this please regenerate the secrets file.", sec.Checksum)
}
}
// injects the matrix configuration parameters
// into the yaml prior to parsing.
injectParams := map[string]string{
"COMMIT_SHORT": payload.Build.Commit, // DEPRECATED
"COMMIT": payload.Build.Commit,
"BRANCH": payload.Build.Branch,
"BUILD_NUMBER": strconv.Itoa(payload.Build.Number),
}
if payload.Build.Event == plugin.EventTag {
injectParams["TAG"] = strings.TrimPrefix(payload.Build.Ref, "refs/tags/")
}
payload.Yaml = inject.Inject(payload.Yaml, payload.Job.Environment)
payload.Yaml = inject.Inject(payload.Yaml, injectParams)
// safely inject global variables
var globals = map[string]string{}
for _, s := range payload.System.Globals {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
continue
}
globals[parts[0]] = parts[1]
}
if payload.Repo.IsPrivate {
payload.Yaml = inject.Inject(payload.Yaml, globals)
} else {
payload.Yaml, _ = inject.InjectSafe(payload.Yaml, globals)
}
// extracts the clone path from the yaml. If
// the clone path doesn't exist it uses a path
// derrived from the repository uri.
payload.Workspace = &plugin.Workspace{Keys: payload.Keys, Netrc: payload.Netrc}
payload.Workspace.Path = path.Parse(payload.Yaml, payload.Repo.Link)
payload.Workspace.Root = "/drone/src"
log.Debugf("Using workspace %s", payload.Workspace.Path)
rules := []parser.RuleFunc{
parser.ImageName,
parser.ImageMatchFunc(payload.System.Plugins),
parser.ImagePullFunc(opt.Force),
parser.SanitizeFunc(payload.Repo.IsTrusted), //&& !plugin.PullRequest(payload.Build)
parser.CacheFunc(payload.Repo.FullName),
parser.DebugFunc(yaml.ParseDebugString(payload.Yaml)),
parser.Escalate,
parser.HttpProxy,
parser.DefaultNotifyFilter,
}
if len(opt.Mount) != 0 {
log.Debugf("Mounting %s as workspace %s",
opt.Mount,
payload.Workspace.Path,
)
rules = append(rules, parser.MountFunc(
opt.Mount,
payload.Workspace.Path,
))
}
tree, err := parser.Parse(payload.Yaml, rules)
if err != nil {
// TODO(sqs): There was a comment here saying "print error
// messages in debug mode only". Is this because of security
// (e.g., the decrypted YAML secrets could leak in the error
// message)? If so, don't return the err here; instead, return
// a simple error message such as "error parsing yaml".
return err
}
r := runner.Load(tree)
client, err := dockerclient.NewDockerClient("unix:///var/run/docker.sock", nil)
if err != nil {
return err
}
// // creates a wrapper Docker client that uses an ambassador
// // container to create a pod-like environment.
controller, err := docker.NewClient(client)
if err != nil {
return fmt.Errorf("creating docker ambassador container: %s", err)
}
defer controller.Destroy()
// watch for sigkill (timeout or cancel build)
killc := make(chan os.Signal, 1)
signal.Notify(killc, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-killc
log.Println("Cancel request received, killing process")
controller.Destroy() // possibe race here. implement lock on the other end
os.Exit(130) // cancel is treated like ctrl+c
}()
go func() {
var timeout = payload.Repo.Timeout
if timeout == 0 {
timeout = 60
}
<-time.After(time.Duration(timeout) * time.Minute)
log.Println("Timeout request received, killing process")
controller.Destroy() // possibe race here. implement lock on the other end
os.Exit(128) // cancel is treated like ctrl+c
}()
state := &runner.State{
Client: controller,
Stdout: outw,
Stderr: errw,
Repo: payload.Repo,
Build: payload.Build,
BuildLast: payload.BuildLast,
Job: payload.Job,
System: payload.System,
Workspace: payload.Workspace,
}
if opt.Cache {
log.Debugln("Running Cache step")
err = r.RunNode(state, parser.NodeCache)
if err != nil {
log.Debugln(err)
}
}
if opt.Clone {
log.Debugln("Running Clone step")
err = r.RunNode(state, parser.NodeClone)
if err != nil {
log.Debugln(err)
}
}
if opt.Build && !state.Failed() {
log.Debugln("Running Build and Compose steps")
err = r.RunNode(state, parser.NodeCompose|parser.NodeBuild)
if err != nil {
log.Debugln(err)
}
}
if opt.Deploy && !state.Failed() {
log.Debugln("Running Publish and Deploy steps")
err = r.RunNode(state, parser.NodePublish|parser.NodeDeploy)
if err != nil {
log.Debugln(err)
}
}
// if the build is not failed, at this point
// we can mark as successful
if !state.Failed() {
state.Job.Status = plugin.StateSuccess
state.Build.Status = plugin.StateSuccess
}
if opt.Cache {
log.Debugln("Running post-Build Cache steps")
err = r.RunNode(state, parser.NodeCache)
if err != nil {
log.Debugln(err)
}
}
if opt.Notify {
log.Debugln("Running Notify steps")
err = r.RunNode(state, parser.NodeNotify)
if err != nil {
log.Debugln(err)
}
}
if state.Failed() {
controller.Destroy()
return &Error{ExitCode: state.ExitCode()}
}
return nil
}