This repository has been archived by the owner on Feb 5, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 251
/
jobs.go
295 lines (252 loc) · 10.3 KB
/
jobs.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
package jobs
import (
"context"
"errors"
"fmt"
"strings"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
"github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/client"
"github.com/VirtusLab/jenkins-operator/pkg/log"
"github.com/go-logr/logr"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s "sigs.k8s.io/controller-runtime/pkg/client"
)
var (
// ErrorUnexpectedBuildStatus - this is custom error returned when jenkins build has unexpected status
ErrorUnexpectedBuildStatus = errors.New("unexpected build status")
// ErrorBuildFailed - this is custom error returned when jenkins build has failed
ErrorBuildFailed = errors.New("build failed")
// ErrorAbortBuildFailed - this is custom error returned when jenkins build couldn't be aborted
ErrorAbortBuildFailed = errors.New("build abort failed")
// ErrorUnrecoverableBuildFailed - this is custom error returned when jenkins build has failed and cannot be recovered
ErrorUnrecoverableBuildFailed = errors.New("build failed and cannot be recovered")
// ErrorNotFound - this is error returned when jenkins build couldn't be found
ErrorNotFound = errors.New("404")
// BuildRetires - determines max amount of retires for failed build
BuildRetires = 3
)
// Jobs defines Jobs API tailored for operator sdk
type Jobs struct {
jenkinsClient client.Jenkins
logger logr.Logger
k8sClient k8s.Client
}
// New creates jobs client
func New(jenkinsClient client.Jenkins, k8sClient k8s.Client, logger logr.Logger) *Jobs {
return &Jobs{
jenkinsClient: jenkinsClient,
k8sClient: k8sClient,
logger: logger,
}
}
// EnsureBuildJob function takes care of jenkins build lifecycle according to the lifecycle of reconciliation loop
// implementation guarantees that jenkins build can be properly handled even after operator pod restart
// entire state is saved in Jenkins.Status.Builds section
// function return 'true' when build finished successfully or false when reconciliation loop should requeue this function
// preserveStatus determines that build won't be removed from Jenkins.Status.Builds section
func (jobs *Jobs) EnsureBuildJob(jobName, hash string, parameters map[string]string, jenkins *virtuslabv1alpha1.Jenkins, preserveStatus bool) (done bool, err error) {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Ensuring build, name:'%s' hash:'%s'", jobName, hash))
build, err := jobs.getBuildFromStatus(jobName, hash, jenkins)
if err != nil {
return false, err
}
if build != nil {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Build exists in status, %+v", build))
switch build.Status {
case virtuslabv1alpha1.BuildSuccessStatus:
return jobs.ensureSuccessBuild(*build, jenkins, preserveStatus)
case virtuslabv1alpha1.BuildRunningStatus:
return jobs.ensureRunningBuild(*build, jenkins, preserveStatus)
case virtuslabv1alpha1.BuildUnstableStatus, virtuslabv1alpha1.BuildNotBuildStatus, virtuslabv1alpha1.BuildFailureStatus, virtuslabv1alpha1.BuildAbortedStatus:
return jobs.ensureFailedBuild(*build, jenkins, parameters, preserveStatus)
case virtuslabv1alpha1.BuildExpiredStatus:
return jobs.ensureExpiredBuild(*build, jenkins, preserveStatus)
default:
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Unexpected build status, %+v", build))
return false, ErrorUnexpectedBuildStatus
}
}
// build is run first time - build job and update status
created := metav1.Now()
newBuild := virtuslabv1alpha1.Build{
JobName: jobName,
Hash: hash,
CreateTime: &created,
}
return jobs.buildJob(newBuild, parameters, jenkins)
}
func (jobs *Jobs) getBuildFromStatus(jobName string, hash string, jenkins *virtuslabv1alpha1.Jenkins) (*virtuslabv1alpha1.Build, error) {
if jenkins != nil {
builds := jenkins.Status.Builds
for _, build := range builds {
if build.JobName == jobName && build.Hash == hash {
return &build, nil
}
}
}
return nil, nil
}
func (jobs *Jobs) ensureSuccessBuild(build virtuslabv1alpha1.Build, jenkins *virtuslabv1alpha1.Jenkins, preserveStatus bool) (bool, error) {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Ensuring success build, %+v", build))
if !preserveStatus {
err := jobs.removeBuildFromStatus(build, jenkins)
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Removing build from status, %+v", build))
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't remove build from status, %+v", build))
return false, err
}
}
return true, nil
}
func (jobs *Jobs) ensureRunningBuild(build virtuslabv1alpha1.Build, jenkins *virtuslabv1alpha1.Jenkins, preserveStatus bool) (bool, error) {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Ensuring running build, %+v", build))
// FIXME (antoniaklja) implement build expiration
jenkinsBuild, err := jobs.jenkinsClient.GetBuild(build.JobName, build.Number)
if isNotFoundError(err) {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Build still running , %+v", build))
return false, nil
} else if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't get jenkins build, %+v", build))
return false, err
}
if jenkinsBuild.GetResult() != "" {
build.Status = virtuslabv1alpha1.BuildStatus(strings.ToLower(jenkinsBuild.GetResult()))
}
err = jobs.updateBuildStatus(build, jenkins)
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't update build status, %+v", build))
return false, err
}
if build.Status == virtuslabv1alpha1.BuildSuccessStatus {
jobs.logger.Info(fmt.Sprintf("Build finished successfully, %+v", build))
return true, nil
}
if build.Status == virtuslabv1alpha1.BuildFailureStatus || build.Status == virtuslabv1alpha1.BuildUnstableStatus ||
build.Status == virtuslabv1alpha1.BuildNotBuildStatus || build.Status == virtuslabv1alpha1.BuildAbortedStatus {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Build failed, %+v", build))
return false, ErrorBuildFailed
}
return false, nil
}
func (jobs *Jobs) ensureFailedBuild(build virtuslabv1alpha1.Build, jenkins *virtuslabv1alpha1.Jenkins, parameters map[string]string, preserveStatus bool) (bool, error) {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Ensuring failed build, %+v", build))
if build.Retires < BuildRetires {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Retrying build, %+v", build))
build.Retires = build.Retires + 1
_, err := jobs.buildJob(build, parameters, jenkins)
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't retry build, %+v", build))
return false, err
}
return false, nil
}
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("The retries limit was reached , %+v", build))
if !preserveStatus {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Removing build from status, %+v", build))
err := jobs.removeBuildFromStatus(build, jenkins)
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't remove build from status, %+v", build))
return false, err
}
}
return false, ErrorUnrecoverableBuildFailed
}
func (jobs *Jobs) ensureExpiredBuild(build virtuslabv1alpha1.Build, jenkins *virtuslabv1alpha1.Jenkins, preserveStatus bool) (bool, error) {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Ensuring expired build, %+v", build))
jenkinsBuild, err := jobs.jenkinsClient.GetBuild(build.JobName, build.Number)
if err != nil {
return false, err
}
_, err = jenkinsBuild.Stop()
if err != nil {
return false, err
}
jenkinsBuild, err = jobs.jenkinsClient.GetBuild(build.JobName, build.Number)
if err != nil {
return false, err
}
if virtuslabv1alpha1.BuildStatus(jenkinsBuild.GetResult()) != virtuslabv1alpha1.BuildAbortedStatus {
return false, ErrorAbortBuildFailed
}
err = jobs.updateBuildStatus(build, jenkins)
if err != nil {
return false, err
}
// TODO(antoniaklja) clean up k8s resources
if !preserveStatus {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Removing build from status, %+v", build))
err = jobs.removeBuildFromStatus(build, jenkins)
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't remove build from status, %+v", build))
return false, err
}
}
return true, nil
}
func (jobs *Jobs) removeBuildFromStatus(build virtuslabv1alpha1.Build, jenkins *virtuslabv1alpha1.Jenkins) error {
builds := make([]virtuslabv1alpha1.Build, len(jenkins.Status.Builds))
for _, existingBuild := range jenkins.Status.Builds {
if existingBuild.JobName != build.JobName && existingBuild.Hash != build.Hash {
builds = append(builds, existingBuild)
}
}
jenkins.Status.Builds = builds
err := jobs.k8sClient.Update(context.TODO(), jenkins)
if err != nil {
return err
}
return nil
}
func (jobs *Jobs) buildJob(build virtuslabv1alpha1.Build, parameters map[string]string, jenkins *virtuslabv1alpha1.Jenkins) (bool, error) {
jobs.logger.Info(fmt.Sprintf("Running job, %+v", build))
job, err := jobs.jenkinsClient.GetJob(build.JobName)
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't find jenkins job, %+v", build))
return false, err
}
nextBuildNumber := job.GetDetails().NextBuildNumber
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Running build, %+v", build))
_, err = jobs.jenkinsClient.BuildJob(build.JobName, parameters)
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't run build, %+v", build))
return false, err
}
build.Status = virtuslabv1alpha1.BuildRunningStatus
build.Number = nextBuildNumber
err = jobs.updateBuildStatus(build, jenkins)
if err != nil {
jobs.logger.V(log.VWarn).Info(fmt.Sprintf("Couldn't update build status, %+v", build))
return false, err
}
return false, nil
}
func (jobs *Jobs) updateBuildStatus(build virtuslabv1alpha1.Build, jenkins *virtuslabv1alpha1.Jenkins) error {
jobs.logger.V(log.VDebug).Info(fmt.Sprintf("Updating build status, %+v", build))
// get index of existing build from status if exists
buildIndex := -1
for index, existingBuild := range jenkins.Status.Builds {
if build.JobName == existingBuild.JobName && build.Hash == existingBuild.Hash {
buildIndex = index
}
}
// update build status
now := metav1.Now()
build.LastUpdateTime = &now
if buildIndex >= 0 {
jenkins.Status.Builds[buildIndex] = build
} else {
build.CreateTime = &now
jenkins.Status.Builds = append(jenkins.Status.Builds, build)
}
err := jobs.k8sClient.Update(context.TODO(), jenkins)
if err != nil {
return err
}
return nil
}
func isNotFoundError(err error) bool {
if err != nil {
return err.Error() == ErrorNotFound.Error()
}
return false
}