Skip to content

Commit d6d9677

Browse files
clamoriniere1Asdminonne
authored andcommitted
DaemonSetJob: implement jobTemplate update (AmadeusITGroup#78)
Issue: AmadeusITGroup#71 - Add JobSpec md5 hash in Job annotation. - Add more unittests for daemonsetjobcontroller.Sync method - Add end2end test for DaemonSetJob Signed-off-by: cedric lamoriniere <cedric.lamoriniere@amadeus.com>
1 parent 457cc08 commit d6d9677

11 files changed

+652
-25
lines changed

app/workflow-controller.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func NewWorkflowControllerApp(c *Config) *WorkflowController {
129129
// configure readiness and liveness probes
130130
health := healthcheck.NewHandler()
131131
workflowCtrl.AddHealthCheck(health)
132-
cronWorkflowCtrl.AddHealthCheck(health)
132+
//cronWorkflowCtrl.AddHealthCheck(health)
133133
daemonSetJobCtrl.AddHealthCheck(health)
134134

135135
return &WorkflowController{

hack/run-test-e2e.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ popd
3030
# - check if kubeconfig is there
3131
# - check if cluster is up
3232

33-
./test/e2e/e2e.test --kubeconfig=$HOME/.kube/config
33+
./test/e2e/e2e.test --kubeconfig=$HOME/.kube/config --ginkgo.slowSpecThreshold 60

pkg/api/daemonsetjob/v1/default.go

-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,5 @@ func DefaultDaemonSetJob(undefaultedDaemonSetJob *DaemonSetJob) *DaemonSetJob {
2525
}
2626
common.SetDefaults_Job(dummyJob)
2727
d.Spec.JobTemplate.Spec = dummyJob.Spec
28-
2928
return d
3029
}

pkg/controller/common.go

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
utilerrors "k8s.io/apimachinery/pkg/util/errors"
1010

1111
batchv1listers "k8s.io/client-go/listers/batch/v1"
12+
13+
batch "k8s.io/api/batch/v1"
1214
)
1315

1416
func deleteJobsFromLabelSelector(namespace string, jobsSelector labels.Selector, lister batchv1listers.JobLister, controler JobControlInterface) error {
@@ -29,3 +31,10 @@ func deleteJobsFromLabelSelector(namespace string, jobsSelector labels.Selector,
2931
}
3032
return utilerrors.NewAggregate(errs)
3133
}
34+
35+
func compareJobSpecMD5Hash(hash string, job *batch.Job) bool {
36+
if val, ok := job.Annotations[DaemonSetJobMD5AnnotationKey]; ok && val == hash {
37+
return true
38+
}
39+
return false
40+
}

pkg/controller/common_test.go

+82-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package controller
22

33
import (
4+
"testing"
5+
46
batch "k8s.io/api/batch/v1"
57
batchv2 "k8s.io/api/batch/v2alpha1"
68
api "k8s.io/api/core/v1"
7-
89
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
910
)
1011

@@ -33,3 +34,83 @@ func newJobTemplateSpec() *batchv2.JobTemplateSpec {
3334
},
3435
}
3536
}
37+
38+
func Test_compareJobSpecMD5Hash(t *testing.T) {
39+
jobSpec1 := batch.JobSpec{Template: api.PodTemplateSpec{Spec: api.PodSpec{Containers: []api.Container{{Name: "foo", Image: "foo:3.0.3"}}}}}
40+
jobSpec2 := batch.JobSpec{Template: api.PodTemplateSpec{Spec: api.PodSpec{Containers: []api.Container{{Name: "foo", Image: "foo:4.0.8"}}}}}
41+
jobSpec3 := batch.JobSpec{Template: api.PodTemplateSpec{Spec: api.PodSpec{Containers: []api.Container{{Name: "foo", Image: "foo:3.0.3"}, {Name: "bar", Image: "bar:latest"}}}}}
42+
hashspec1, _ := generateMD5JobSpec(&jobSpec1)
43+
t.Logf("hashspec1:%s", hashspec1)
44+
hashspec2, _ := generateMD5JobSpec(&jobSpec2)
45+
t.Logf("hashspec2:%s", hashspec2)
46+
hashspec3, _ := generateMD5JobSpec(&jobSpec3)
47+
t.Logf("hashspec3:%s", hashspec3)
48+
49+
type args struct {
50+
hash string
51+
job *batch.Job
52+
}
53+
tests := []struct {
54+
name string
55+
args args
56+
want bool
57+
}{
58+
{
59+
name: "similar JobSpec",
60+
args: args{
61+
hash: hashspec1,
62+
job: &batch.Job{
63+
ObjectMeta: metav1.ObjectMeta{
64+
Annotations: map[string]string{DaemonSetJobMD5AnnotationKey: hashspec1},
65+
},
66+
Spec: jobSpec1,
67+
},
68+
},
69+
want: true,
70+
},
71+
{
72+
name: "different JobSpec",
73+
args: args{
74+
hash: hashspec1,
75+
job: &batch.Job{
76+
ObjectMeta: metav1.ObjectMeta{
77+
Annotations: map[string]string{DaemonSetJobMD5AnnotationKey: hashspec2},
78+
},
79+
Spec: jobSpec2,
80+
},
81+
},
82+
want: false,
83+
},
84+
{
85+
name: "PodTemplate changed",
86+
args: args{
87+
hash: hashspec1,
88+
job: &batch.Job{
89+
ObjectMeta: metav1.ObjectMeta{
90+
Annotations: map[string]string{DaemonSetJobMD5AnnotationKey: string(hashspec3)},
91+
},
92+
Spec: jobSpec2,
93+
},
94+
},
95+
want: false,
96+
},
97+
{
98+
name: "missing annotation",
99+
args: args{
100+
hash: hashspec2,
101+
job: &batch.Job{
102+
ObjectMeta: metav1.ObjectMeta{},
103+
Spec: jobSpec2,
104+
},
105+
},
106+
want: false,
107+
},
108+
}
109+
for _, tt := range tests {
110+
t.Run(tt.name, func(t *testing.T) {
111+
if got := compareJobSpecMD5Hash(tt.args.hash, tt.args.job); got != tt.want {
112+
t.Errorf("compareJobSpecMD5Hash() = %v, want %v", got, tt.want)
113+
}
114+
})
115+
}
116+
}

pkg/controller/controller_util.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func getJobLabelsSetFromDaemonSetJob(daemonsetjob *dapi.DaemonSetJob, template *
8989
// Job should also get template.metadata.labels for monitoring metrics.
9090
// These would ordinarily only be applied to the Pod running the Job,
9191
// but the CronJob controller does apply them to its Job.
92-
for k, v := range template.Spec.Template.ObjectMeta.Labels {
92+
for k, v := range template.ObjectMeta.Labels {
9393
desiredLabels[k] = v
9494
}
9595
desiredLabels[DaemonSetJobLabelKey] = daemonsetjob.Name // add daemonsetjob name to the job labels

pkg/controller/daemonsetjob.go

+38-12
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ type DaemonSetJobControllerConfig struct {
4343

4444
// DaemonSetJobLabelKey defines the key of label to be injected by DaemonSetJob controller
4545
const (
46-
DaemonSetJobLabelKey = "kubernetes.io/DaemonSetJob"
46+
DaemonSetJobLabelKey = "kubernetes.io/DaemonSetJob"
47+
DaemonSetJobMD5AnnotationKey = "workflow.k8s.io/jobspec-md5"
4748
)
4849

4950
// DaemonSetJobController represents the DaemonSetJob controller
@@ -288,6 +289,11 @@ func (d *DaemonSetJobController) manageDaemonSetJob(daemonsetjob *dapi.DaemonSet
288289
daemonsetjobComplete := false
289290
daemonsetjobFailed := false
290291

292+
md5JobSpec, err := generateMD5JobSpec(&daemonsetjob.Spec.JobTemplate.Spec)
293+
if err != nil {
294+
return fmt.Errorf("unable to generates the JobSpec MD5, %v", err)
295+
}
296+
291297
jobSelector := labels.Set{DaemonSetJobLabelKey: daemonsetjob.Name}
292298
jobs, err := d.JobLister.List(jobSelector.AsSelectorPreValidated())
293299
jobsByNodeName := make(map[string]*batch.Job)
@@ -306,20 +312,40 @@ func (d *DaemonSetJobController) manageDaemonSetJob(daemonsetjob *dapi.DaemonSet
306312
}
307313
errs := []error{}
308314
allJobs := []*batch.Job{}
315+
jobToDelete := []*batch.Job{} // corresponding to the all DaemonSetJob Spec
309316
for _, node := range nodes {
317+
newJobCreation := false
310318
job, ok := jobsByNodeName[node.Name]
311319
if !ok {
320+
glog.V(6).Infof("Job for the node %s not found %s/%s", node.Name, daemonsetjob.Namespace, daemonsetjob.Name)
321+
newJobCreation = true
322+
} else if job != nil {
323+
if !compareJobSpecMD5Hash(md5JobSpec, job) {
324+
glog.V(6).Infof("JobTemplateSpec has changed, %s/%s, previousMD5:%s, current:%s", job.Namespace, job.Name, md5JobSpec, job.GetAnnotations()[DaemonSetJobMD5AnnotationKey])
325+
jobToDelete = append(jobToDelete, job)
326+
newJobCreation = true
327+
} else {
328+
allJobs = append(allJobs, job)
329+
}
330+
}
331+
332+
if newJobCreation {
312333
job, err = d.JobControl.CreateJobFromDaemonSetJob(daemonsetjob.Namespace, daemonsetjob.Spec.JobTemplate, daemonsetjob, node.Name)
313334
if err != nil {
314335
errs = append(errs, err)
315336
}
316337
daemonsetjobToBeUpdated = true
317338
}
339+
}
318340

319-
if job != nil {
320-
allJobs = append(allJobs, job)
341+
// Delete old jobs
342+
for _, job := range jobToDelete {
343+
// TODO maybe wait that the job is finished (success or failure) before deleting, need to define policy configuration
344+
// Or maybe wait that the job is finished before creating the new job
345+
err = d.JobControl.DeleteJob(job.Namespace, job.Name, job)
346+
if err != nil {
347+
errs = append(errs, err)
321348
}
322-
323349
}
324350

325351
// build status
@@ -337,7 +363,6 @@ func (d *DaemonSetJobController) manageDaemonSetJob(daemonsetjob *dapi.DaemonSet
337363
daemonsetjob.Status.Failed = failedJobs
338364
daemonsetjobToBeUpdated = true
339365
}
340-
341366
updateDaemonSetJobStatusConditions(&daemonsetjob.Status, now, daemonsetjobComplete, daemonsetjobFailed)
342367
if daemonsetjobComplete {
343368
glog.Infof("Workflow %s/%s complete.", daemonsetjob.Namespace, daemonsetjob.Name)
@@ -541,6 +566,13 @@ func IsDaemonSetJobFinished(d *dapi.DaemonSetJob) bool {
541566
return false
542567
}
543568

569+
// InferDaemonSetJobLabelSelectorForJobs returns labels.Selector corresponding to the associated Jobs
570+
func InferDaemonSetJobLabelSelectorForJobs(daemonsetjob *dapi.DaemonSetJob) labels.Selector {
571+
set := fetchLabelsSetFromLabelSelector(daemonsetjob.Spec.Selector)
572+
set[DaemonSetJobLabelKey] = daemonsetjob.Name
573+
return labels.SelectorFromSet(set)
574+
}
575+
544576
// get daemonsetjob by key method
545577
func (d *DaemonSetJobController) getDaemonSetJobByKey(key string) (*dapi.DaemonSetJob, error) {
546578
namespace, name, err := cache.SplitMetaNamespaceKey(key)
@@ -590,7 +622,7 @@ func (d *DaemonSetJobController) pastActiveDeadline(daemonsetjob *dapi.DaemonSet
590622
func (d *DaemonSetJobController) deleteDaemonSetJobJobs(daemonsetjob *dapi.DaemonSetJob) error {
591623
glog.V(6).Infof("deleting all jobs for daemonsetjob %s/%s", daemonsetjob.Namespace, daemonsetjob.Name)
592624

593-
jobsSelector := inferDaemonSetJobLabelSelectorForJobs(daemonsetjob)
625+
jobsSelector := InferDaemonSetJobLabelSelectorForJobs(daemonsetjob)
594626
return deleteJobsFromLabelSelector(daemonsetjob.Namespace, jobsSelector, d.JobLister, d.JobControl)
595627
}
596628

@@ -656,9 +688,3 @@ func newDaemonSetJobStatusCondition(conditionType dapi.DaemonSetJobConditionType
656688
Message: message,
657689
}
658690
}
659-
660-
func inferDaemonSetJobLabelSelectorForJobs(daemonsetjob *dapi.DaemonSetJob) labels.Selector {
661-
set := fetchLabelsSetFromLabelSelector(daemonsetjob.Spec.Selector)
662-
set[DaemonSetJobLabelKey] = daemonsetjob.Name
663-
return labels.SelectorFromSet(set)
664-
}

0 commit comments

Comments
 (0)