Skip to content

Commit bd652f3

Browse files
author
Amit Kumar Das
authored
feat(timeout): add job elapsed time (#63)
This commit adds support to set a job's elapsed time. This may be used to assert if a job got executed within some desired time interval. An experiment has been added that verifies if 50 config maps get created by a job and all of this should happen within 10 seconds. In addition, several fixes and minor enhancement have gone in to make Job more usable to write testcase implementations. Signed-off-by: AmitKumarDas <[email protected]>
1 parent 9818369 commit bd652f3

17 files changed

+524
-94
lines changed

controller/job/reconciler.go

+8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ limitations under the License.
1717
package job
1818

1919
import (
20+
"k8s.io/utils/pointer"
2021
"openebs.io/metac/controller/generic"
22+
k8s "openebs.io/metac/third_party/kubernetes"
2123

2224
commonctrl "mayadata.io/d-operators/common/controller"
2325
"mayadata.io/d-operators/common/unstruct"
@@ -77,6 +79,9 @@ func (r *Reconciler) setWatchStatusAsError() {
7779
"phase": "Error",
7880
"reason": r.Err.Error(),
7981
}
82+
r.HookResponse.Labels = map[string]*string{
83+
"job.dope.metacontroller.io/phase": k8s.StringPtr("Error"),
84+
}
8085
}
8186

8287
func (r *Reconciler) setWatchStatusFromJobStatus() {
@@ -88,6 +93,9 @@ func (r *Reconciler) setWatchStatusFromJobStatus() {
8893
"taskCount": int64(r.JobStatus.TaskCount),
8994
"taskListStatus": r.JobStatus.TaskListStatus,
9095
}
96+
r.HookResponse.Labels = map[string]*string{
97+
"job.dope.metacontroller.io/phase": pointer.StringPtr(string(r.JobStatus.Phase)),
98+
}
9199
}
92100

93101
func (r *Reconciler) setWatchStatus() {

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
k8s.io/apimachinery v0.17.3
1414
k8s.io/client-go v0.17.3
1515
k8s.io/klog/v2 v2.0.0
16+
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f
1617
openebs.io/metac v0.3.0
1718
)
1819

pkg/job/assert.go

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func (a *Assertable) runAssertByPath() {
102102
Message: got.Message,
103103
Verbose: got.Verbose,
104104
Warning: got.Warning,
105+
Timeout: got.Timeout,
105106
}
106107
}
107108

@@ -125,6 +126,7 @@ func (a *Assertable) runAssertByState() {
125126
Message: got.Message,
126127
Verbose: got.Verbose,
127128
Warning: got.Warning,
129+
Timeout: got.Timeout,
128130
}
129131
}
130132

pkg/job/create.go

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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 job
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/pkg/errors"
23+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27+
types "mayadata.io/d-operators/types/job"
28+
"openebs.io/metac/dynamic/clientset"
29+
)
30+
31+
// CreatableConfig helps in creating new instance of Creatable
32+
type CreatableConfig struct {
33+
Fixture *Fixture
34+
Retry *Retryable
35+
Create *types.Create
36+
TaskName string
37+
}
38+
39+
// Creatable helps creating desired state(s) against the cluster
40+
type Creatable struct {
41+
*Fixture
42+
Retry *Retryable
43+
TaskName string
44+
Create *types.Create
45+
46+
result *types.CreateResult
47+
err error
48+
}
49+
50+
func (c *Creatable) String() string {
51+
if c.Create == nil {
52+
return ""
53+
}
54+
return fmt.Sprintf(
55+
"Create action: Resource %s %s: GVK %s: TaskName %s",
56+
c.Create.State.GetNamespace(),
57+
c.Create.State.GetName(),
58+
c.Create.State.GroupVersionKind(),
59+
c.TaskName,
60+
)
61+
}
62+
63+
// NewCreator returns a new instance of Creatable
64+
func NewCreator(config CreatableConfig) *Creatable {
65+
return &Creatable{
66+
Create: config.Create,
67+
Fixture: config.Fixture,
68+
Retry: config.Retry,
69+
TaskName: config.TaskName,
70+
result: &types.CreateResult{},
71+
}
72+
}
73+
74+
func (c *Creatable) postCreateCRD(
75+
crd *v1beta1.CustomResourceDefinition,
76+
) error {
77+
message := fmt.Sprintf(
78+
"PostCreate CRD: Kind %s: APIVersion %s: TaskName %s",
79+
crd.Spec.Names.Singular,
80+
crd.Spec.Group+"/"+crd.Spec.Version,
81+
c.TaskName,
82+
)
83+
// discover custom resource API
84+
return c.Retry.Waitf(
85+
func() (bool, error) {
86+
got := c.apiDiscovery.
87+
GetAPIForAPIVersionAndResource(
88+
crd.Spec.Group+"/"+crd.Spec.Version,
89+
crd.Spec.Names.Plural,
90+
)
91+
if got == nil {
92+
return false, errors.Errorf(
93+
"Failed to discover: Kind %s: APIVersion %s",
94+
crd.Spec.Names.Singular,
95+
crd.Spec.Group+"/"+crd.Spec.Version,
96+
)
97+
}
98+
// fetch dynamic client for the custom resource
99+
// corresponding to this CRD
100+
customResourceClient, err := c.dynamicClientset.
101+
GetClientForAPIVersionAndResource(
102+
crd.Spec.Group+"/"+crd.Spec.Version,
103+
crd.Spec.Names.Plural,
104+
)
105+
if err != nil {
106+
return false, err
107+
}
108+
_, err = customResourceClient.List(metav1.ListOptions{})
109+
if err != nil {
110+
return false, err
111+
}
112+
return true, nil
113+
},
114+
message,
115+
)
116+
}
117+
118+
func (c *Creatable) createCRD() (*types.CreateResult, error) {
119+
var crd *v1beta1.CustomResourceDefinition
120+
err := UnstructToTyped(c.Create.State, &crd)
121+
if err != nil {
122+
return nil, err
123+
}
124+
// use crd client to create crd
125+
crd, err = c.crdClient.
126+
CustomResourceDefinitions().
127+
Create(crd)
128+
if err != nil {
129+
return nil, errors.Wrapf(err, "%s", c)
130+
}
131+
// add to teardown functions
132+
c.AddToTeardown(func() error {
133+
_, err := c.crdClient.
134+
CustomResourceDefinitions().
135+
Get(
136+
crd.GetName(),
137+
metav1.GetOptions{},
138+
)
139+
if err != nil && apierrors.IsNotFound(err) {
140+
// nothing to do
141+
return nil
142+
}
143+
return c.crdClient.
144+
CustomResourceDefinitions().
145+
Delete(
146+
crd.Name,
147+
nil,
148+
)
149+
})
150+
// run an additional step to wait till this CRD
151+
// is discovered at apiserver
152+
err = c.postCreateCRD(crd)
153+
if err != nil {
154+
return nil, err
155+
}
156+
return &types.CreateResult{
157+
Phase: types.CreateStatusPassed,
158+
Message: fmt.Sprintf(
159+
"Create CRD: Kind %s: APIVersion %s",
160+
crd.Spec.Names.Singular,
161+
crd.Spec.Group+"/"+crd.Spec.Version,
162+
),
163+
}, nil
164+
}
165+
166+
func (c *Creatable) createResource(
167+
obj *unstructured.Unstructured,
168+
client *clientset.ResourceClient,
169+
) error {
170+
_, err := client.
171+
Namespace(obj.GetNamespace()).
172+
Create(
173+
obj,
174+
metav1.CreateOptions{},
175+
)
176+
if err != nil {
177+
return err
178+
}
179+
c.AddToTeardown(func() error {
180+
_, err := client.
181+
Namespace(obj.GetNamespace()).
182+
Get(
183+
obj.GetName(),
184+
metav1.GetOptions{},
185+
)
186+
if err != nil && apierrors.IsNotFound(err) {
187+
// nothing to do since resource is already deleted
188+
return nil
189+
}
190+
return client.
191+
Namespace(obj.GetNamespace()).
192+
Delete(
193+
obj.GetName(),
194+
&metav1.DeleteOptions{},
195+
)
196+
})
197+
return nil
198+
}
199+
200+
func buildNamesFromGivenState(
201+
obj *unstructured.Unstructured,
202+
replicas int,
203+
) ([]string, error) {
204+
var name string
205+
name = obj.GetGenerateName()
206+
if name == "" {
207+
name = obj.GetName()
208+
}
209+
if name == "" {
210+
return nil, errors.Errorf(
211+
"Failed to generate names: Either name or generateName required",
212+
)
213+
}
214+
if replicas == 1 {
215+
return []string{name}, nil
216+
}
217+
var out []string
218+
for i := 0; i < replicas; i++ {
219+
out = append(out, fmt.Sprintf("%s-%d", name, i))
220+
}
221+
return out, nil
222+
}
223+
224+
func (c *Creatable) createResourceReplicas() (*types.CreateResult, error) {
225+
replicas := 1
226+
if c.Create.Replicas != nil {
227+
replicas = *c.Create.Replicas
228+
}
229+
if replicas <= 0 {
230+
return nil, errors.Errorf(
231+
"Failed to create: Invalid replicas %d: %s",
232+
replicas,
233+
c,
234+
)
235+
}
236+
client, err := c.dynamicClientset.
237+
GetClientForAPIVersionAndKind(
238+
c.Create.State.GetAPIVersion(),
239+
c.Create.State.GetKind(),
240+
)
241+
if err != nil {
242+
return nil, err
243+
}
244+
names, err := buildNamesFromGivenState(c.Create.State, replicas)
245+
if err != nil {
246+
return nil, errors.Wrapf(err, "%s", c)
247+
}
248+
for _, name := range names {
249+
obj := &unstructured.Unstructured{
250+
Object: c.Create.State.Object,
251+
}
252+
obj.SetName(name)
253+
err = c.createResource(obj, client)
254+
if err != nil {
255+
return nil, errors.Wrapf(err, "%s", c)
256+
}
257+
}
258+
return &types.CreateResult{
259+
Phase: types.CreateStatusPassed,
260+
Message: c.String(),
261+
}, nil
262+
}
263+
264+
// Run creates the desired state against the cluster
265+
func (c *Creatable) Run() (*types.CreateResult, error) {
266+
if c.Create.State.GetKind() == "CustomResourceDefinition" {
267+
// create CRD
268+
return c.createCRD()
269+
}
270+
return c.createResourceReplicas()
271+
}

pkg/job/job.go

+23-6
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ limitations under the License.
1717
package job
1818

1919
import (
20+
"time"
21+
2022
"github.com/pkg/errors"
2123
apierrors "k8s.io/apimachinery/pkg/api/errors"
2224
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2325
"k8s.io/klog/v2"
26+
"k8s.io/utils/pointer"
2427
types "mayadata.io/d-operators/types/job"
2528
metacdiscovery "openebs.io/metac/dynamic/discovery"
2629
metac "openebs.io/metac/start"
@@ -139,19 +142,20 @@ func (r *Runner) buildLockRunner() *LockRunner {
139142
"name": r.Job.GetName() + "-lock",
140143
"namespace": r.Job.GetNamespace(),
141144
"labels": map[string]interface{}{
142-
"jobs.metacontroller.app/lock": "true",
145+
"job.dope.metacontroller.io/lock": "true",
143146
},
144147
},
145148
},
146149
},
147150
},
148151
}
149152
return &LockRunner{
150-
Fixture: r.fixture,
151-
Task: lock,
152-
LockForever: isLockForever,
153-
Retry: NewRetry(RetryConfig{}),
154-
ProtectedTaskCount: len(r.Job.Spec.Tasks),
153+
Fixture: r.fixture,
154+
Task: lock,
155+
LockForever: isLockForever,
156+
Retry: NewRetry(RetryConfig{}),
157+
// no of tasks + elapsed time task
158+
ProtectedTaskCount: len(r.Job.Spec.Tasks) + 1,
155159
}
156160
}
157161

@@ -191,12 +195,22 @@ func (r *Runner) getAPIDiscovery() *metacdiscovery.APIResourceDiscovery {
191195
// return apiDiscovery
192196
}
193197

198+
func (r *Runner) addJobElapsedTimeInSeconds(elapsedtime float64) {
199+
r.JobStatus.TaskListStatus["job-elapsed-time"] = types.TaskStatus{
200+
Step: len(r.Job.Spec.Tasks) + 1,
201+
Internal: pointer.BoolPtr(true),
202+
Phase: types.TaskStatusPassed,
203+
ElapsedTimeInSeconds: pointer.Float64Ptr(elapsedtime),
204+
}
205+
}
206+
194207
// runAll runs all the tasks
195208
func (r *Runner) runAll() (status *types.JobStatus, err error) {
196209
defer func() {
197210
r.fixture.TearDown()
198211
}()
199212
var failedTasks int
213+
var start = time.Now()
200214
for idx, task := range r.Job.Spec.Tasks {
201215
tr := &TaskRunner{
202216
Fixture: r.fixture,
@@ -218,6 +232,9 @@ func (r *Runner) runAll() (status *types.JobStatus, err error) {
218232
failedTasks++
219233
}
220234
}
235+
// time taken for this job
236+
elapsedSeconds := time.Since(start).Seconds()
237+
r.addJobElapsedTimeInSeconds(elapsedSeconds)
221238
// build the result
222239
if failedTasks > 0 {
223240
r.JobStatus.Phase = types.JobStatusFailed

0 commit comments

Comments
 (0)