forked from cloudfoundry/bosh-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
/
run_errand.go
152 lines (125 loc) · 3.89 KB
/
run_errand.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
package action
import (
"errors"
"path"
"time"
boshas "github.com/cloudfoundry/bosh-agent/agent/applier/applyspec"
boshenv "github.com/cloudfoundry/bosh-agent/agent/script/pathenv"
bosherr "github.com/cloudfoundry/bosh-utils/errors"
boshlog "github.com/cloudfoundry/bosh-utils/logger"
boshsys "github.com/cloudfoundry/bosh-utils/system"
)
const runErrandActionLogTag = "runErrandAction"
type RunErrandAction struct {
specService boshas.V1Service
jobsDir string
cmdRunner boshsys.CmdRunner
logger boshlog.Logger
cancelCh chan struct{}
}
func NewRunErrand(
specService boshas.V1Service,
jobsDir string,
cmdRunner boshsys.CmdRunner,
logger boshlog.Logger,
) RunErrandAction {
return RunErrandAction{
specService: specService,
jobsDir: jobsDir,
cmdRunner: cmdRunner,
logger: logger,
// Initialize channel in a constructor to avoid race
// between initializing in Run()/Cancel()
cancelCh: make(chan struct{}, 1),
}
}
func (a RunErrandAction) IsAsynchronous(_ ProtocolVersion) bool {
return true
}
func (a RunErrandAction) IsPersistent() bool {
return false
}
func (a RunErrandAction) IsLoggable() bool {
return true
}
type ErrandResult struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitStatus int `json:"exit_code"`
}
func (a RunErrandAction) Run(errandName ...string) (ErrandResult, error) {
currentSpec, err := a.specService.Get()
if err != nil {
return ErrandResult{}, bosherr.WrapError(err, "Getting current spec")
}
var templateName string
if len(errandName) == 0 {
if len(currentSpec.JobSpec.Template) == 0 {
return ErrandResult{}, bosherr.Error("At least one job template is required to run an errand")
}
templateName = currentSpec.JobSpec.Template
} else {
foundErrand := false
for _, v := range currentSpec.JobSpec.JobTemplateSpecs {
if v.Name == errandName[0] {
foundErrand = true
}
}
if !foundErrand {
return ErrandResult{}, bosherr.Errorf("Could not find errand %s", errandName[0])
}
templateName = errandName[0]
}
command := boshsys.Command{
Name: path.Join(a.jobsDir, templateName, "bin", "run"),
Env: map[string]string{
"PATH": boshenv.Path(),
},
}
process, err := a.cmdRunner.RunComplexCommandAsync(command)
if err != nil {
return ErrandResult{}, bosherr.WrapError(err, "Running errand script")
}
var result boshsys.Result
// Can only wait once on a process but cancelling can happen multiple times
for processExitedCh := process.Wait(); processExitedCh != nil; {
select {
case result = <-processExitedCh:
processExitedCh = nil
case <-a.cancelCh:
// Ignore possible TerminateNicely error since we cannot return it
err := process.TerminateNicely(10 * time.Second)
if err != nil {
a.logger.Error(runErrandActionLogTag, "Failed to terminate %s", err.Error())
}
}
}
if result.Error != nil && result.ExitStatus == -1 {
return ErrandResult{}, bosherr.WrapError(result.Error, "Running errand script")
}
return ErrandResult{
Stdout: result.Stdout,
Stderr: result.Stderr,
ExitStatus: result.ExitStatus,
}, nil
}
func (a RunErrandAction) Resume() (interface{}, error) {
return nil, errors.New("not supported")
}
// Cancelling rules:
// 1. Cancel action MUST take constant time even if another cancel is pending/running
// 2. Cancel action DOES NOT have to cancel if another cancel is pending/running
// 3. Cancelling errand before it starts should cancel errand when it runs
// - possible optimization - do not even start errand
// (e.g. send 5 cancels, 1 is actually doing cancelling, other 4 exit immediately)
// Cancel satisfies above rules though it never returns any error
func (a RunErrandAction) Cancel() error {
select {
case a.cancelCh <- struct{}{}:
// Always return no error since we cannot wait until
// errand runs in the future and potentially fails to cancel
default:
// Cancel action is already queued up
}
return nil
}