Skip to content

Commit a61dfc0

Browse files
author
sai chaithanya
authored
feat(command): add support to delete dependants of command resource (#189)
* feat(command): add support to delete dependents of command resource This PR does the following changes: - Add support to delete dependent resources when command resource is deleted. - Add support to launch the jobs periodically when command is configured for run always Signed-off-by: mittachaitu <[email protected]>
1 parent ad823c7 commit a61dfc0

File tree

13 files changed

+648
-118
lines changed

13 files changed

+648
-118
lines changed

cmd/main.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ func main() {
6262

6363
// controller name & corresponding controller reconcile function
6464
var controllers = map[string]generic.InlineInvokeFn{
65-
"sync/recipe": recipe.Sync,
66-
"finalize/recipe": recipe.Finalize,
67-
"sync/http": http.Sync,
68-
"sync/doperator": doperator.Sync,
69-
"sync/run": run.Sync,
70-
"sync/command": command.Sync,
65+
"sync/recipe": recipe.Sync,
66+
"finalize/recipe": recipe.Finalize,
67+
"sync/http": http.Sync,
68+
"sync/doperator": doperator.Sync,
69+
"sync/run": run.Sync,
70+
"sync/command": command.Sync,
71+
"finalize/command": command.Finalize,
7172
}
73+
7274
for name, ctrl := range controllers {
7375
generic.AddToInlineRegistry(name, ctrl)
7476
}

config/metac.yaml

+49-1
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,52 @@ spec:
6464
hooks:
6565
sync:
6666
inline:
67-
funcName: sync/command
67+
funcName: sync/command
68+
---
69+
apiVersion: dope/v1
70+
kind: GenericController
71+
metadata:
72+
name: finalize-command
73+
namespace: dope
74+
spec:
75+
watch:
76+
apiVersion: dope.mayadata.io/v1
77+
resource: commands
78+
attachments:
79+
# Delete pod
80+
- apiVersion: v1
81+
resource: pods
82+
advancedSelector:
83+
selectorTerms:
84+
# select Pod if its labels has following
85+
- matchReferenceExpressions:
86+
- key: metadata.namespace
87+
operator: EqualsWatchNamespace
88+
- key: metadata.labels.job-name
89+
operator: EqualsWatchName # match this lbl value against watch Name
90+
# Delete job
91+
- apiVersion: batch/v1
92+
resource: jobs
93+
advancedSelector:
94+
selectorTerms:
95+
# select job if its labels has following
96+
- matchLabels:
97+
command.dope.mayadata.io/controller: "true"
98+
matchReferenceExpressions:
99+
- key: metadata.labels.command\.dope\.mayadata\.io/uid
100+
operator: EqualsWatchUID # match this lbl value against watch UID
101+
- apiVersion: v1
102+
resource: configmaps
103+
advancedSelector:
104+
selectorTerms:
105+
# select ConfigMap if its labels has following
106+
- matchLabels:
107+
command.dope.mayadata.io/lock: "true"
108+
matchReferenceExpressions:
109+
- key: metadata.labels.command\.dope\.mayadata\.io/uid
110+
operator: EqualsWatchUID # match this lbl value against watch Name
111+
hooks:
112+
finalize:
113+
inline:
114+
funcName: finalize/command
115+
---

controller/command/finalizer.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2020 The MayaData Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package command
18+
19+
import (
20+
"openebs.io/metac/controller/generic"
21+
)
22+
23+
var (
24+
defaultDeletionResyncTime = float64(30)
25+
)
26+
27+
// Finalize implements the idempotent logic that gets executed when
28+
// Command instance is deleted. A Command instance may have child job &
29+
// dedicated lock in form of a ConfigMap.
30+
// Finalize logic tries to delete child pod, job & ConfigMap
31+
//
32+
// NOTE:
33+
// When finalize hook is set in the config metac automatically sets
34+
// a finalizer entry against the Command metadata's finalizers field .
35+
// This finalizer entry is removed when SyncHookResponse's Finalized
36+
// field is set to true
37+
//
38+
// NOTE:
39+
// SyncHookRequest is the payload received as part of finalize
40+
// request. Similarly, SyncHookResponse is the payload sent as a
41+
// response as part of finalize request.
42+
//
43+
// NOTE:
44+
// Returning error will panic this process. We would rather want this
45+
// controller to run continuously. Hence, the errors are handled.
46+
func Finalize(request *generic.SyncHookRequest, response *generic.SyncHookResponse) error {
47+
if request.Attachments.IsEmpty() {
48+
// Since no Dependents found it is safe to delete Command
49+
response.Finalized = true
50+
return nil
51+
}
52+
53+
response.ResyncAfterSeconds = defaultDeletionResyncTime
54+
// Observed attachments will get deleted
55+
response.ExplicitDeletes = request.Attachments.List()
56+
return nil
57+
}

controller/command/reconciler.go

+11
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ func (r *Reconciler) eval() {
6161
}
6262

6363
func (r *Reconciler) sync() {
64+
// Check for deletion timestamp on command resource
65+
// if set then command is marked for deletion
66+
if !r.observedCommand.DeletionTimestamp.IsZero() {
67+
klog.V(1).Infof(
68+
"Will skip command reconciliation: It is marked for deletion: Command %q / %q",
69+
r.observedCommand.GetNamespace(),
70+
r.observedCommand.GetName(),
71+
)
72+
return
73+
}
74+
6475
// creating / deleting a Kubernetes Job is part of Command reconciliation
6576
jobBuilder := command.NewJobBuilder(
6677
command.JobBuildingConfig{

deploy/crd.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,4 @@ spec:
8383
versions:
8484
- name: v1
8585
served: true
86-
storage: true
86+
storage: true

pkg/command/reconciler.go

+95-11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package command
1818

1919
import (
2020
"fmt"
21+
"strings"
2122

2223
"github.com/pkg/errors"
2324
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -57,6 +58,11 @@ type Reconciliation struct {
5758
// client to invoke CRUD operations against k8s Job
5859
jobClient *clientset.ResourceClient
5960

61+
// getChildJob will hold function to fetch the child object
62+
// from k8s cluster
63+
// NOTE: This is helpful to mocking
64+
getChildJob func() (*unstructured.Unstructured, bool, error)
65+
6066
// is Command resource supposed to run Once
6167
isRunOnce bool
6268

@@ -98,6 +104,10 @@ type Reconciliation struct {
98104
}
99105

100106
func (r *Reconciliation) initChildJobDetails() {
107+
var got *unstructured.Unstructured
108+
var found bool
109+
var err error
110+
101111
if r.childJob == nil || r.childJob.Object == nil {
102112
return
103113
}
@@ -115,7 +125,12 @@ func (r *Reconciliation) initChildJobDetails() {
115125
)
116126
return
117127
}
118-
got, found, err := r.isChildJobAvailable()
128+
129+
if r.getChildJob != nil {
130+
got, found, err = r.getChildJob()
131+
} else {
132+
got, found, err = r.isChildJobAvailable()
133+
}
119134
if err != nil {
120135
r.err = err
121136
return
@@ -162,16 +177,17 @@ func (r *Reconciliation) initChildJobDetails() {
162177
return
163178
}
164179

165-
// Extract status.active of this Job
166-
activeCount, found, err := unstructured.NestedInt64(
180+
// Extract status.conditions of this Job to know whether
181+
// job has completed its execution
182+
jobConditions, found, err := unstructured.NestedSlice(
167183
got.Object,
168184
"status",
169-
"active",
185+
"conditions",
170186
)
171187
if err != nil {
172188
r.err = errors.Wrapf(
173189
err,
174-
"Failed to get Job status.active: Kind %q: Job %q / %q",
190+
"Failed to get Job status.conditions: Kind %q: Job %q / %q",
175191
r.childJob.GetKind(),
176192
r.childJob.GetNamespace(),
177193
r.childJob.GetName(),
@@ -180,21 +196,45 @@ func (r *Reconciliation) initChildJobDetails() {
180196
}
181197
if !found {
182198
klog.V(1).Infof(
183-
"Job status.active is not set: Kind %q: Job %q / %q",
199+
"Job status.conditions is not set: Kind %q: Job %q / %q",
184200
r.childJob.GetKind(),
185201
r.childJob.GetNamespace(),
186202
r.childJob.GetName(),
187203
)
188-
// Job's status.active is not set
204+
// Job's status.conditions is not set
189205
//
190206
// Nothing to do
191207
// Wait for next reconcile
192208
return
193209
}
194-
195-
if activeCount > 0 {
196-
r.isChildJobCompleted = true
210+
// Look for condition type complete
211+
// if found then mark isChildJobCompleted as true
212+
for _, value := range jobConditions {
213+
condition, ok := value.(map[string]interface{})
214+
if !ok {
215+
r.err = errors.Errorf(
216+
"Job status.condition is not map[string]interface{} got %T: "+
217+
"kind %q: Job %q / %q",
218+
value,
219+
r.childJob.GetKind(),
220+
r.childJob.GetNamespace(),
221+
r.childJob.GetName(),
222+
)
223+
return
224+
}
225+
condType := condition["type"].(string)
226+
if condType == types.JobPhaseCompleted {
227+
condStatus := condition["status"].(string)
228+
if strings.ToLower(condStatus) == "true" {
229+
r.isChildJobCompleted = true
230+
}
231+
}
197232
}
233+
234+
// If there is no condtion with complete type then
235+
// nothing to do
236+
237+
// wait for next reconciliation
198238
}
199239

200240
func (r *Reconciliation) initCommandDetails() {
@@ -356,13 +396,17 @@ func (r *Reconciliation) isChildJobAvailable() (*unstructured.Unstructured, bool
356396
}
357397

358398
func (r *Reconciliation) deleteChildJob() (types.CommandStatus, error) {
399+
// If propagationPolicy is set to background then the garbage collector will
400+
// delete dependents in the background
401+
propagationPolicy := v1.DeletePropagationBackground
359402
err := r.jobClient.
360403
Namespace(r.childJob.GetNamespace()).
361404
Delete(
362405
r.childJob.GetName(),
363406
&v1.DeleteOptions{
364407
// Delete immediately
365408
GracePeriodSeconds: pointer.Int64(0),
409+
PropagationPolicy: &propagationPolicy,
366410
},
367411
)
368412
if err != nil && !apierrors.IsNotFound(err) {
@@ -445,12 +489,52 @@ func (r *Reconciliation) reconcileRunAlwaysCommand() (types.CommandStatus, error
445489
return r.createChildJob()
446490
}
447491
if r.isStatusSet && r.isChildJobCompleted {
492+
// Since this is for run always we are performing below steps
493+
// 1. Delete Job and wait til it gets deleted from etcd
494+
// 2. Create Job in the same reconciliation
448495
klog.V(1).Infof(
449496
"Will delete command job: Command %q / %q",
450497
r.command.GetNamespace(),
451498
r.command.GetName(),
452499
)
453-
return r.deleteChildJob()
500+
_, err := r.deleteChildJob()
501+
if err != nil {
502+
return types.CommandStatus{}, err
503+
}
504+
505+
// Logic to wait for Job resource deletion from etcd
506+
var message = fmt.Sprintf(
507+
"Waiting for command job: %q / %q deletion",
508+
r.childJob.GetNamespace(),
509+
r.childJob.GetName(),
510+
)
511+
err = r.Retry.Waitf(
512+
func() (bool, error) {
513+
_, err := r.jobClient.
514+
Namespace(r.childJob.GetNamespace()).
515+
Get(r.childJob.GetName(), v1.GetOptions{})
516+
if err != nil {
517+
if apierrors.IsNotFound(err) {
518+
return true, nil
519+
}
520+
return false, err
521+
}
522+
return false, nil
523+
},
524+
message,
525+
)
526+
527+
klog.V(1).Infof("Deleted command job: Command %q / %q successfully",
528+
r.command.GetNamespace(),
529+
r.command.GetName(),
530+
)
531+
532+
klog.V(1).Infof(
533+
"Will create command job: Command %q / %q",
534+
r.command.GetNamespace(),
535+
r.command.GetName(),
536+
)
537+
return r.createChildJob()
454538
}
455539
return types.CommandStatus{
456540
Phase: types.CommandPhaseInProgress,

0 commit comments

Comments
 (0)