From a61dfc0fb8644f60b995996f65ae8d0e5f40da06 Mon Sep 17 00:00:00 2001 From: sai chaithanya Date: Wed, 17 Feb 2021 10:52:12 +0530 Subject: [PATCH] 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 --- cmd/main.go | 14 +- config/metac.yaml | 50 +++- controller/command/finalizer.go | 57 +++++ controller/command/reconciler.go | 11 + deploy/crd.yaml | 2 +- pkg/command/reconciler.go | 106 +++++++- pkg/command/reconciler_test.go | 242 +++++++++++------- test/declarative/ci.yaml | 27 +- .../command-creation-deletion.yaml | 232 +++++++++++++++++ test/declarative/inference.yaml | 6 +- test/declarative/suite.sh | 15 +- test/integration/suite.sh | 2 +- types/command/command.go | 2 +- 13 files changed, 648 insertions(+), 118 deletions(-) create mode 100644 controller/command/finalizer.go create mode 100644 test/declarative/experiments/command-creation-deletion.yaml diff --git a/cmd/main.go b/cmd/main.go index 200dc8c..f253315 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -62,13 +62,15 @@ func main() { // controller name & corresponding controller reconcile function var controllers = map[string]generic.InlineInvokeFn{ - "sync/recipe": recipe.Sync, - "finalize/recipe": recipe.Finalize, - "sync/http": http.Sync, - "sync/doperator": doperator.Sync, - "sync/run": run.Sync, - "sync/command": command.Sync, + "sync/recipe": recipe.Sync, + "finalize/recipe": recipe.Finalize, + "sync/http": http.Sync, + "sync/doperator": doperator.Sync, + "sync/run": run.Sync, + "sync/command": command.Sync, + "finalize/command": command.Finalize, } + for name, ctrl := range controllers { generic.AddToInlineRegistry(name, ctrl) } diff --git a/config/metac.yaml b/config/metac.yaml index e14dc56..163c2a0 100644 --- a/config/metac.yaml +++ b/config/metac.yaml @@ -64,4 +64,52 @@ spec: hooks: sync: inline: - funcName: sync/command \ No newline at end of file + funcName: sync/command +--- +apiVersion: dope/v1 +kind: GenericController +metadata: + name: finalize-command + namespace: dope +spec: + watch: + apiVersion: dope.mayadata.io/v1 + resource: commands + attachments: + # Delete pod + - apiVersion: v1 + resource: pods + advancedSelector: + selectorTerms: + # select Pod if its labels has following + - matchReferenceExpressions: + - key: metadata.namespace + operator: EqualsWatchNamespace + - key: metadata.labels.job-name + operator: EqualsWatchName # match this lbl value against watch Name + # Delete job + - apiVersion: batch/v1 + resource: jobs + advancedSelector: + selectorTerms: + # select job if its labels has following + - matchLabels: + command.dope.mayadata.io/controller: "true" + matchReferenceExpressions: + - key: metadata.labels.command\.dope\.mayadata\.io/uid + operator: EqualsWatchUID # match this lbl value against watch UID + - apiVersion: v1 + resource: configmaps + advancedSelector: + selectorTerms: + # select ConfigMap if its labels has following + - matchLabels: + command.dope.mayadata.io/lock: "true" + matchReferenceExpressions: + - key: metadata.labels.command\.dope\.mayadata\.io/uid + operator: EqualsWatchUID # match this lbl value against watch Name + hooks: + finalize: + inline: + funcName: finalize/command +--- diff --git a/controller/command/finalizer.go b/controller/command/finalizer.go new file mode 100644 index 0000000..9f01166 --- /dev/null +++ b/controller/command/finalizer.go @@ -0,0 +1,57 @@ +/* +Copyright 2020 The MayaData Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "openebs.io/metac/controller/generic" +) + +var ( + defaultDeletionResyncTime = float64(30) +) + +// Finalize implements the idempotent logic that gets executed when +// Command instance is deleted. A Command instance may have child job & +// dedicated lock in form of a ConfigMap. +// Finalize logic tries to delete child pod, job & ConfigMap +// +// NOTE: +// When finalize hook is set in the config metac automatically sets +// a finalizer entry against the Command metadata's finalizers field . +// This finalizer entry is removed when SyncHookResponse's Finalized +// field is set to true +// +// NOTE: +// SyncHookRequest is the payload received as part of finalize +// request. Similarly, SyncHookResponse is the payload sent as a +// response as part of finalize request. +// +// NOTE: +// Returning error will panic this process. We would rather want this +// controller to run continuously. Hence, the errors are handled. +func Finalize(request *generic.SyncHookRequest, response *generic.SyncHookResponse) error { + if request.Attachments.IsEmpty() { + // Since no Dependents found it is safe to delete Command + response.Finalized = true + return nil + } + + response.ResyncAfterSeconds = defaultDeletionResyncTime + // Observed attachments will get deleted + response.ExplicitDeletes = request.Attachments.List() + return nil +} diff --git a/controller/command/reconciler.go b/controller/command/reconciler.go index 770c754..140f595 100644 --- a/controller/command/reconciler.go +++ b/controller/command/reconciler.go @@ -61,6 +61,17 @@ func (r *Reconciler) eval() { } func (r *Reconciler) sync() { + // Check for deletion timestamp on command resource + // if set then command is marked for deletion + if !r.observedCommand.DeletionTimestamp.IsZero() { + klog.V(1).Infof( + "Will skip command reconciliation: It is marked for deletion: Command %q / %q", + r.observedCommand.GetNamespace(), + r.observedCommand.GetName(), + ) + return + } + // creating / deleting a Kubernetes Job is part of Command reconciliation jobBuilder := command.NewJobBuilder( command.JobBuildingConfig{ diff --git a/deploy/crd.yaml b/deploy/crd.yaml index 5b113df..3a6ab87 100644 --- a/deploy/crd.yaml +++ b/deploy/crd.yaml @@ -83,4 +83,4 @@ spec: versions: - name: v1 served: true - storage: true \ No newline at end of file + storage: true diff --git a/pkg/command/reconciler.go b/pkg/command/reconciler.go index 0c5c58e..75a2008 100644 --- a/pkg/command/reconciler.go +++ b/pkg/command/reconciler.go @@ -18,6 +18,7 @@ package command import ( "fmt" + "strings" "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -57,6 +58,11 @@ type Reconciliation struct { // client to invoke CRUD operations against k8s Job jobClient *clientset.ResourceClient + // getChildJob will hold function to fetch the child object + // from k8s cluster + // NOTE: This is helpful to mocking + getChildJob func() (*unstructured.Unstructured, bool, error) + // is Command resource supposed to run Once isRunOnce bool @@ -98,6 +104,10 @@ type Reconciliation struct { } func (r *Reconciliation) initChildJobDetails() { + var got *unstructured.Unstructured + var found bool + var err error + if r.childJob == nil || r.childJob.Object == nil { return } @@ -115,7 +125,12 @@ func (r *Reconciliation) initChildJobDetails() { ) return } - got, found, err := r.isChildJobAvailable() + + if r.getChildJob != nil { + got, found, err = r.getChildJob() + } else { + got, found, err = r.isChildJobAvailable() + } if err != nil { r.err = err return @@ -162,16 +177,17 @@ func (r *Reconciliation) initChildJobDetails() { return } - // Extract status.active of this Job - activeCount, found, err := unstructured.NestedInt64( + // Extract status.conditions of this Job to know whether + // job has completed its execution + jobConditions, found, err := unstructured.NestedSlice( got.Object, "status", - "active", + "conditions", ) if err != nil { r.err = errors.Wrapf( err, - "Failed to get Job status.active: Kind %q: Job %q / %q", + "Failed to get Job status.conditions: Kind %q: Job %q / %q", r.childJob.GetKind(), r.childJob.GetNamespace(), r.childJob.GetName(), @@ -180,21 +196,45 @@ func (r *Reconciliation) initChildJobDetails() { } if !found { klog.V(1).Infof( - "Job status.active is not set: Kind %q: Job %q / %q", + "Job status.conditions is not set: Kind %q: Job %q / %q", r.childJob.GetKind(), r.childJob.GetNamespace(), r.childJob.GetName(), ) - // Job's status.active is not set + // Job's status.conditions is not set // // Nothing to do // Wait for next reconcile return } - - if activeCount > 0 { - r.isChildJobCompleted = true + // Look for condition type complete + // if found then mark isChildJobCompleted as true + for _, value := range jobConditions { + condition, ok := value.(map[string]interface{}) + if !ok { + r.err = errors.Errorf( + "Job status.condition is not map[string]interface{} got %T: "+ + "kind %q: Job %q / %q", + value, + r.childJob.GetKind(), + r.childJob.GetNamespace(), + r.childJob.GetName(), + ) + return + } + condType := condition["type"].(string) + if condType == types.JobPhaseCompleted { + condStatus := condition["status"].(string) + if strings.ToLower(condStatus) == "true" { + r.isChildJobCompleted = true + } + } } + + // If there is no condtion with complete type then + // nothing to do + + // wait for next reconciliation } func (r *Reconciliation) initCommandDetails() { @@ -356,6 +396,9 @@ func (r *Reconciliation) isChildJobAvailable() (*unstructured.Unstructured, bool } func (r *Reconciliation) deleteChildJob() (types.CommandStatus, error) { + // If propagationPolicy is set to background then the garbage collector will + // delete dependents in the background + propagationPolicy := v1.DeletePropagationBackground err := r.jobClient. Namespace(r.childJob.GetNamespace()). Delete( @@ -363,6 +406,7 @@ func (r *Reconciliation) deleteChildJob() (types.CommandStatus, error) { &v1.DeleteOptions{ // Delete immediately GracePeriodSeconds: pointer.Int64(0), + PropagationPolicy: &propagationPolicy, }, ) if err != nil && !apierrors.IsNotFound(err) { @@ -445,12 +489,52 @@ func (r *Reconciliation) reconcileRunAlwaysCommand() (types.CommandStatus, error return r.createChildJob() } if r.isStatusSet && r.isChildJobCompleted { + // Since this is for run always we are performing below steps + // 1. Delete Job and wait til it gets deleted from etcd + // 2. Create Job in the same reconciliation klog.V(1).Infof( "Will delete command job: Command %q / %q", r.command.GetNamespace(), r.command.GetName(), ) - return r.deleteChildJob() + _, err := r.deleteChildJob() + if err != nil { + return types.CommandStatus{}, err + } + + // Logic to wait for Job resource deletion from etcd + var message = fmt.Sprintf( + "Waiting for command job: %q / %q deletion", + r.childJob.GetNamespace(), + r.childJob.GetName(), + ) + err = r.Retry.Waitf( + func() (bool, error) { + _, err := r.jobClient. + Namespace(r.childJob.GetNamespace()). + Get(r.childJob.GetName(), v1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + } + return false, nil + }, + message, + ) + + klog.V(1).Infof("Deleted command job: Command %q / %q successfully", + r.command.GetNamespace(), + r.command.GetName(), + ) + + klog.V(1).Infof( + "Will create command job: Command %q / %q", + r.command.GetNamespace(), + r.command.GetName(), + ) + return r.createChildJob() } return types.CommandStatus{ Phase: types.CommandPhaseInProgress, diff --git a/pkg/command/reconciler_test.go b/pkg/command/reconciler_test.go index 40d41e4..ef9d8a2 100644 --- a/pkg/command/reconciler_test.go +++ b/pkg/command/reconciler_test.go @@ -21,7 +21,9 @@ package command import ( "testing" + "github.com/pkg/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/rest" "openebs.io/metac/dynamic/discovery" "openebs.io/metac/server" @@ -30,98 +32,154 @@ import ( types "mayadata.io/d-operators/types/command" ) -// func TestReconcilerInitChildJobDetails(t *testing.T) { -// var tests = map[string]struct { -// ChildJob *unstructured.Unstructured -// ExpectChildJob bool -// ExpectCompletedChildJob bool -// IsError bool -// }{ -// "no child job": {}, -// "empty child job": { -// ChildJob: &unstructured.Unstructured{}, -// }, -// "invalid child job": { -// ChildJob: &unstructured.Unstructured{ -// Object: map[string]interface{}{}, -// }, -// IsError: true, -// }, -// "invalid child kind": { -// ChildJob: &unstructured.Unstructured{ -// Object: map[string]interface{}{ -// "kind": "MyJob", -// "apiVersion": types.JobAPIVersion, -// }, -// }, -// IsError: true, -// }, -// "invalid child api version": { -// ChildJob: &unstructured.Unstructured{ -// Object: map[string]interface{}{ -// "kind": types.KindJob, -// "apiVersion": "v12", -// }, -// }, -// IsError: true, -// }, -// "valid child kind & apiversion": { -// ChildJob: &unstructured.Unstructured{ -// Object: map[string]interface{}{ -// "kind": types.KindJob, -// "apiVersion": types.JobAPIVersion, -// }, -// }, -// ExpectChildJob: true, -// }, -// "valid and completed child job": { -// ChildJob: &unstructured.Unstructured{ -// Object: map[string]interface{}{ -// "kind": types.KindJob, -// "apiVersion": types.JobAPIVersion, -// "status": map[string]interface{}{ -// "phase": types.JobPhaseCompleted, -// }, -// }, -// }, -// ExpectChildJob: true, -// ExpectCompletedChildJob: true, -// }, -// } -// for name, mock := range tests { -// name := name -// mock := mock -// t.Run(name, func(t *testing.T) { -// r := &Reconciliation{ -// childJob: mock.ChildJob, -// } -// r.initChildJobDetails() -// if mock.IsError && r.err == nil { -// t.Fatalf("Expected error got none") -// } -// if !mock.IsError && r.err != nil { -// t.Fatalf("Expected no error got %s", r.err.Error()) -// } -// if mock.IsError { -// return -// } -// if mock.ExpectChildJob != r.isChildJobFound { -// t.Fatalf( -// "Expected child job %t got %t", -// mock.ExpectChildJob, -// r.isChildJobFound, -// ) -// } -// if mock.ExpectCompletedChildJob != r.isChildJobCompleted { -// t.Fatalf( -// "Expected child job as completed %t got %t", -// mock.ExpectCompletedChildJob, -// r.isChildJobCompleted, -// ) -// } -// }) -// } -// } +func getFakeChildJob( + jobObj *unstructured.Unstructured, + isError bool) func() (*unstructured.Unstructured, bool, error) { + return func() (*unstructured.Unstructured, bool, error) { + // Return fake error + if isError { + return nil, false, errors.Errorf("fake error") + } + // If job is not available in system + if jobObj == nil { + return nil, false, nil + } + return jobObj, true, nil + } +} + +func TestReconcilerInitChildJobDetails(t *testing.T) { + var tests = map[string]struct { + ChildJob *unstructured.Unstructured + ExpectChildJob bool + ExpectCompletedChildJob bool + IsError bool + InjectAPIError bool + }{ + "no child job": {}, + "empty child job": { + ChildJob: &unstructured.Unstructured{}, + }, + "invalid child job": { + ChildJob: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + IsError: true, + }, + "invalid child kind": { + ChildJob: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": "MyJob", + "apiVersion": types.JobAPIVersion, + }, + }, + IsError: true, + }, + "invalid child api version": { + ChildJob: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": types.KindJob, + "apiVersion": "v12", + }, + }, + IsError: true, + }, + "valid child kind & apiversion with API error": { + ChildJob: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": types.KindJob, + "apiVersion": types.JobAPIVersion, + "metadata": map[string]interface{}{ + "name": "valid-job", + "namespace": "kubera", + }, + }, + }, + IsError: true, + InjectAPIError: true, + ExpectChildJob: true, + }, + "valid child kind & apiversion with Job Inprogress": { + ChildJob: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": types.KindJob, + "apiVersion": types.JobAPIVersion, + "metadata": map[string]interface{}{ + "name": "valid-job", + "namespace": "kubera", + }, + }, + }, + ExpectChildJob: true, + }, + "valid and completed child job": { + ChildJob: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": types.KindJob, + "apiVersion": types.JobAPIVersion, + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "lastProbeTime": "2021-02-10T17:31:19Z", + "lastTransitionTime": "2021-02-10T17:31:19Z", + "status": "True", + "type": "Complete", + }, + }, + }, + }, + }, + ExpectChildJob: true, + ExpectCompletedChildJob: true, + }, + "valid and failed child job": { + ChildJob: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": types.KindJob, + "apiVersion": types.JobAPIVersion, + "status": map[string]interface{}{ + "failed": int64(2), + }, + }, + }, + ExpectChildJob: true, + }, + } + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + r := &Reconciliation{ + childJob: mock.ChildJob, + getChildJob: getFakeChildJob(mock.ChildJob, mock.InjectAPIError), + } + r.initChildJobDetails() + if mock.IsError && r.err == nil { + t.Fatalf("Expected error got none") + } + if !mock.IsError && r.err != nil { + t.Fatalf("Expected no error got %s", r.err.Error()) + } + if mock.IsError { + return + } + if mock.ExpectChildJob != r.isChildJobFound { + t.Fatalf( + "Expected child job %t got %t", + mock.ExpectChildJob, + r.isChildJobFound, + ) + } + if mock.ExpectCompletedChildJob != r.isChildJobCompleted { + t.Fatalf( + "Expected child job as completed %t got %t", + mock.ExpectCompletedChildJob, + r.isChildJobCompleted, + ) + } + }) + } +} func TestReconcilerInitCommandDetails(t *testing.T) { var tests = map[string]struct { diff --git a/test/declarative/ci.yaml b/test/declarative/ci.yaml index 8c3f098..f85def0 100644 --- a/test/declarative/ci.yaml +++ b/test/declarative/ci.yaml @@ -60,6 +60,31 @@ spec: description: Description of this phase JSONPath: .status.reason --- +# Command custom resource is used to write command test cases +# in a declarative approach +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + name: commands.dope.mayadata.io +spec: + group: dope.mayadata.io + names: + kind: Command + listKind: CommandList + plural: commands + shortNames: + - cmds + singular: command + scope: Namespaced + subresources: + status: {} + version: v1 + versions: + - name: v1 + served: true + storage: true +--- # HTTP custom resource is used to invoke http request # in a declarative way apiVersion: apiextensions.k8s.io/v1beta1 @@ -155,4 +180,4 @@ spec: - -v=1 # increasing level will make the ci logs verbose - --workers-count=20 # higher value helps in executing experiments faster - --discovery-interval=30s - - --cache-flush-interval=60s # reconciliation interval \ No newline at end of file + - --cache-flush-interval=60s # reconciliation interval diff --git a/test/declarative/experiments/command-creation-deletion.yaml b/test/declarative/experiments/command-creation-deletion.yaml new file mode 100644 index 0000000..57a9508 --- /dev/null +++ b/test/declarative/experiments/command-creation-deletion.yaml @@ -0,0 +1,232 @@ +## Test will verify creation & deletion of command and their dependents +## 1. Create new Namespace +## 2. Create service account +## 3. Create clusterrole +## 4. Create clusterrolebinding +## 5. Create Command resource +## 6. Make sure k8s ConfigMap is created for above command +## 6. Make sure k8s Job is created for above command +## 7. Make sure k8s pod is created for above command +## 8. Delete Command resource +## 9. Make sure K8s ConfigMap, Job & Pod related to above +## command resource should be deleted +## 10. Delete clusterrolebinding +## 11. Delete clusterrole +## 12. Delete namespace +apiVersion: dope.mayadata.io/v1 +kind: Recipe +metadata: + name: create-and-assert-command-resource + namespace: d-testing + labels: + d-testing.dope.mayadata.io/inference: "true" +spec: + tasks: + - name: create-command-test-namespace + create: + state: + kind: Namespace + apiVersion: v1 + metadata: + name: recipe-integration-cmd-testing + - name: create-service-account + create: + state: + kind: ServiceAccount + apiVersion: v1 + metadata: + name: recipe-integration-cmd-testing-sa + namespace: recipe-integration-cmd-testing + - name: create-rbac-clusterrole + create: + state: + kind: ClusterRole + apiVersion: rbac.authorization.k8s.io/v1 + metadata: + name: recipe-integration-cmd-testing-dope + rules: + - apiGroups: + - "*" + resources: + - "*" + verbs: + - "*" + - name: create-rbac-cluster-role + create: + state: + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + metadata: + name: recipe-integration-cmd-testing-dope + subjects: + - kind: ServiceAccount + name: recipe-integration-cmd-testing-sa + namespace: recipe-integration-cmd-testing + roleRef: + kind: ClusterRole + name: recipe-integration-cmd-testing-dope + apiGroup: rbac.authorization.k8s.io + - name: create-command-resource + create: + state: + kind: Command + apiVersion: dope.mayadata.io/v1 + metadata: + name: testing-command + namespace: recipe-integration-cmd-testing + labels: + command.dope.mayadata.io/name: testing-command + spec: + commands: + - name: Sleep-test + script: sleep 3 + template: + job: + apiVersion: batch/v1 + kind: Job + spec: + template: + spec: + serviceAccountName: recipe-integration-cmd-testing-sa + containers: + - command: + - /usr/bin/daction + image: localhost:5000/daction + imagePullPolicy: IfNotPresent + name: daction + args: + - -v=3 + - --command-name=testing-command + - --command-ns=recipe-integration-cmd-testing + - name: assert-cm-lock-creation + assert: + state: + kind: ConfigMap + apiVersion: v1 + metadata: + namespace: recipe-integration-cmd-testing + labels: + command.dope.mayadata.io/name: testing-command + stateCheck: + stateCheckOperator: ListCountEquals + count: 1 + - name: assert-job-creation-via-command + assert: + state: + kind: Job + apiVersion: batch/v1 + metadata: + name: testing-command + namespace: recipe-integration-cmd-testing + labels: + command.dope.mayadata.io/controller: "true" + - name: assert-pod-creation-via-command-creation + assert: + state: + kind: Pod + apiVersion: v1 + metadata: + namespace: recipe-integration-cmd-testing + labels: + job-name: testing-command + stateCheck: + stateCheckOperator: ListCountEquals + count: 1 + - name: assert-job-completion + assert: + state: + kind: Job + apiVersion: batch/v1 + metadata: + name: testing-command + namespace: recipe-integration-cmd-testing + status: + succeeded: 1 + - name: assert-command-finalizer + assert: + state: + kind: Command + apiVersion: dope.mayadata.io/v1 + metadata: + name: testing-command + namespace: recipe-integration-cmd-testing + finalizers: + - protect.gctl.metac.openebs.io/dope-finalize-command + - name: assert-command-completion + assert: + state: + kind: Command + apiVersion: dope.mayadata.io/v1 + metadata: + name: testing-command + namespace: recipe-integration-cmd-testing + labels: + command.dope.mayadata.io/phase: Completed + command.dope.mayadata.io/name: testing-command + status: + phase: Completed + - name: delete-command + delete: + state: + kind: Command + apiVersion: dope.mayadata.io/v1 + metadata: + name: testing-command + namespace: recipe-integration-cmd-testing + - name: assert-pod-deletion-via-command-deletion + assert: + state: + kind: Pod + apiVersion: v1 + metadata: + namespace: recipe-integration-cmd-testing + labels: + job-name: testing-command + stateCheck: + stateCheckOperator: ListCountEquals + count: 0 + - name: assert-job-deletion-via-command-deletion + assert: + state: + kind: Job + apiVersion: batch/v1 + metadata: + name: testing-command + namespace: recipe-integration-cmd-testing + labels: + command.dope.mayadata.io/controller: true + stateCheck: + stateCheckOperator: NotFound + - name: assert-cm-lock-deletion-via-command-deletion + assert: + state: + kind: ConfigMap + apiVersion: v1 + metadata: + namespace: recipe-integration-cmd-testing + labels: + command.dope.mayadata.io/name: testing-command + stateCheck: + stateCheckOperator: ListCountEquals + count: 0 + - name: delete-clusterrole + delete: + state: + kind: ClusterRole + apiVersion: rbac.authorization.k8s.io/v1 + metadata: + name: recipe-integration-cmd-testing-dope + - name: delete-clusterrolebinding + delete: + state: + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + metadata: + name: recipe-integration-cmd-testing-dope + - name: delete-namespace + delete: + state: + kind: Namespace + apiVersion: v1 + metadata: + name: recipe-integration-cmd-testing diff --git a/test/declarative/inference.yaml b/test/declarative/inference.yaml index 13a3091..75733bd 100644 --- a/test/declarative/inference.yaml +++ b/test/declarative/inference.yaml @@ -35,7 +35,7 @@ spec: values: - NotEligible when: ListCountEquals - count: 7 + count: 8 resync: onNotEligibleResyncInSeconds: 5 tasks: @@ -54,7 +54,7 @@ spec: stateCheck: stateCheckOperator: ListCountEquals # These many Recipes should succeed - count: 7 + count: 8 # Then assert the count of Recipes that should fail - name: assert-count-of-tests-that-failed assert: @@ -85,4 +85,4 @@ spec: stateCheckOperator: ListCountEquals # These many Recipes should error out count: 0 ---- \ No newline at end of file +--- diff --git a/test/declarative/suite.sh b/test/declarative/suite.sh index cf41594..31143dd 100755 --- a/test/declarative/suite.sh +++ b/test/declarative/suite.sh @@ -50,6 +50,9 @@ echo "--------------------------" # Name of the targeted controller binary under test ctrlbin="dope" +# Name of the daction controller binary +dactionctrlbin="daction" + # group that defines the Recipe custom resource group="recipes.dope.mayadata.io" @@ -62,17 +65,27 @@ docker image remove $ctrlbin:e2e || true echo -e "\n Remove locally cached image localhost:5000/$ctrlbin" docker image remove localhost:5000/$ctrlbin || true +echo -e "\n Remove locally cached image localhost:5000/$dactionctrlbin" +docker image remove localhost:5000/$dactionctrlbin || true + echo -e "\n Run local docker registry at port 5000" docker run -d -p 5000:5000 --restart=always --name e2eregistry registry:2 echo -e "\n Build $ctrlbin image as $ctrlbin:e2e" docker build -t $ctrlbin:e2e ./../../ +echo -e "\n Build $dactionctrlbin image as $dactionctrlbin:e2e" +docker build -t $dactionctrlbin:e2e ./../../tools/d-action + echo -e "\n Tag $ctrlbin:e2e image as localhost:5000/$ctrlbin" docker tag $ctrlbin:e2e localhost:5000/$ctrlbin +echo -e "\n Tag $dactionctrlbin:e2e image as localhost:5000/$dactionctrlbin" +docker tag $dactionctrlbin:e2e localhost:5000/$dactionctrlbin + echo -e "\n Push the image to local registry running at localhost:5000" docker push localhost:5000/$ctrlbin +docker push localhost:5000/$dactionctrlbin echo -e "\n Setup K3s registries path" mkdir -p "/etc/rancher/k3s/" @@ -128,4 +141,4 @@ fi echo "" echo "--------------------------" echo "++ E to E suite passed" -echo "--------------------------" \ No newline at end of file +echo "--------------------------" diff --git a/test/integration/suite.sh b/test/integration/suite.sh index 07e127b..03510f0 100755 --- a/test/integration/suite.sh +++ b/test/integration/suite.sh @@ -130,4 +130,4 @@ fi echo "" echo "--------------------------" echo "++ Integration test suite passed" -echo "--------------------------" \ No newline at end of file +echo "--------------------------" diff --git a/types/command/command.go b/types/command/command.go index b380aec..b029800 100644 --- a/types/command/command.go +++ b/types/command/command.go @@ -89,7 +89,7 @@ const ( const ( // JobPhaseCompleted represents a Kubernetes Job with // Completed status - JobPhaseCompleted string = "Completed" + JobPhaseCompleted string = "Complete" // KindJob defines the constant to represent the Job's kind KindJob string = "Job"