diff --git a/pkg/apis/config/feature_flags.go b/pkg/apis/config/feature_flags.go index 463eacfaebf..4a6a3fd00d4 100644 --- a/pkg/apis/config/feature_flags.go +++ b/pkg/apis/config/feature_flags.go @@ -59,6 +59,8 @@ const ( DefaultSendCloudEventsForRuns = false // DefaultEmbeddedStatus is the default value for "embedded-status". DefaultEmbeddedStatus = FullEmbeddedStatus + // DefaultEnableCancelUsingEntrypoint is the default value for "enable-cancel-using-entrypoint" + DefaultEnableCancelUsingEntrypoint = false disableAffinityAssistantKey = "disable-affinity-assistant" disableCredsInitKey = "disable-creds-init" @@ -70,6 +72,7 @@ const ( scopeWhenExpressionsToTask = "scope-when-expressions-to-task" sendCloudEventsForRuns = "send-cloudevents-for-runs" embeddedStatus = "embedded-status" + enableCancelUsingEntrypoint = "enable-cancel-using-entrypoint" ) // FeatureFlags holds the features configurations @@ -85,6 +88,7 @@ type FeatureFlags struct { EnableAPIFields string SendCloudEventsForRuns bool EmbeddedStatus string + EnableCancelUsingEntrypoint bool } // GetFeatureFlagsConfigName returns the name of the configmap containing all @@ -136,6 +140,9 @@ func NewFeatureFlagsFromMap(cfgMap map[string]string) (*FeatureFlags, error) { if err := setEmbeddedStatus(cfgMap, DefaultEmbeddedStatus, &tc.EmbeddedStatus); err != nil { return nil, err } + if err := setFeature(enableCancelUsingEntrypoint, DefaultEnableCancelUsingEntrypoint, &tc.EnableCancelUsingEntrypoint); err != nil { + return nil, err + } // Given that they are alpha features, Tekton Bundles and Custom Tasks should be switched on if // enable-api-fields is "alpha". If enable-api-fields is not "alpha" then fall back to the value of diff --git a/pkg/apis/config/feature_flags_test.go b/pkg/apis/config/feature_flags_test.go index dccf4138596..30b8cd218ec 100644 --- a/pkg/apis/config/feature_flags_test.go +++ b/pkg/apis/config/feature_flags_test.go @@ -53,6 +53,7 @@ func TestNewFeatureFlagsFromConfigMap(t *testing.T) { EnableAPIFields: "alpha", SendCloudEventsForRuns: true, EmbeddedStatus: "both", + EnableCancelUsingEntrypoint: true, }, fileName: "feature-flags-all-flags-set", }, diff --git a/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml b/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml index f80f3be96c2..ed1e6ed17a2 100644 --- a/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml +++ b/pkg/apis/config/testdata/feature-flags-all-flags-set.yaml @@ -27,3 +27,4 @@ data: enable-api-fields: "alpha" send-cloudevents-for-runs: "true" embedded-status: "both" + enable-cancel-using-entrypoint: "true" diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 46e03eb1120..af555e9c22c 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -140,7 +140,12 @@ func (c *Reconciler) ReconcileKind(ctx context.Context, tr *v1beta1.TaskRun) pkg // If the TaskRun is cancelled, kill resources and update status if tr.IsCancelled() { message := fmt.Sprintf("TaskRun %q was cancelled", tr.Name) - err := c.cancelTaskRun(ctx, tr, v1beta1.TaskRunReasonCancelled, message) + var err error + if isCancelUsingEntrypointEnabled(ctx) { + err = c.cancelTaskRun(ctx, tr, v1beta1.TaskRunReasonCancelled, message) + } else { + err = c.failTaskRun(ctx, tr, v1beta1.TaskRunReasonCancelled, message) + } return c.finishReconcileUpdateEmitEvents(ctx, tr, before, err) } @@ -866,3 +871,11 @@ func willOverwritePodSetAffinity(taskRun *v1beta1.TaskRun) bool { } return taskRun.Annotations[workspace.AnnotationAffinityAssistantName] != "" && podTemplate.Affinity != nil } + +// isCancelUsingEntrypointEnabled returns a bool indicating whether the cancellation of tasks +// should be performed using the entrypoint binary. The default behaviour is to delete the pods +// corresponding to a task. +func isCancelUsingEntrypointEnabled(ctx context.Context) bool { + cfg := config.FromContextOrDefaults(ctx) + return cfg.FeatureFlags.EnableCancelUsingEntrypoint +} diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index 0b71a726796..604e6761d8d 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -1922,6 +1922,111 @@ func TestReconcileOnCancelledTaskRun(t *testing.T) { } } +func TestReconcileOnCancelledTaskRunWithFeatureFlag(t *testing.T) { + taskRun := &v1beta1.TaskRun{ + ObjectMeta: objectMeta("test-taskrun-run-cancelled", "foo"), + Spec: v1beta1.TaskRunSpec{ + TaskRef: &v1beta1.TaskRef{ + Name: simpleTask.Name, + }, + Status: v1beta1.TaskRunSpecStatusCancelled, + }, + Status: v1beta1.TaskRunStatus{ + Status: duckv1beta1.Status{ + Conditions: duckv1beta1.Conditions{ + apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionUnknown, + }, + }, + }, + }, + } + pod, err := makePod(taskRun, simpleTask) + if err != nil { + t.Fatalf("MakePod: %v", err) + } + taskRun.Status.PodName = pod.Name + + expectedStatus := &apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: "TaskRunCancelled", + Message: `TaskRun "test-taskrun-run-cancelled" was cancelled`, + } + + testCases := []struct { + name string + cancelEnabled bool + }{ + { + name: "Taskrun cancellation with entrypoint cancel enabled", + cancelEnabled: true, + }, + { + name: "Taskrun cancellation without entrypoint cancel", + cancelEnabled: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d := test.Data{ + TaskRuns: []*v1beta1.TaskRun{taskRun}, + Tasks: []*v1beta1.Task{simpleTask}, + Pods: []*corev1.Pod{pod}, + } + + if tc.cancelEnabled { + d.ConfigMaps = []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()}, + Data: map[string]string{ + "enable-cancel-using-entrypoint": "true", + }, + }, + } + } + + testAssets, cancel := getTaskRunController(t, d) + defer cancel() + c := testAssets.Controller + clients := testAssets.Clients + + if err := c.Reconciler.Reconcile(testAssets.Ctx, getRunName(taskRun)); err != nil { + t.Fatalf("Unexpected error when reconciling completed TaskRun : %v", err) + } + newTr, err := clients.Pipeline.TektonV1beta1().TaskRuns(taskRun.Namespace).Get(testAssets.Ctx, taskRun.Name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Expected completed TaskRun %s to exist but instead got error when getting it: %v", taskRun.Name, err) + } + + if d := cmp.Diff(expectedStatus, newTr.Status.GetCondition(apis.ConditionSucceeded), ignoreLastTransitionTime); d != "" { + t.Fatalf("Did not get expected condition %s", diff.PrintWantGot(d)) + } + + wantEvents := []string{ + "Normal Started", + "Warning Failed TaskRun \"test-taskrun-run-cancelled\" was cancelled", + } + err = eventstest.CheckEventsOrdered(t, testAssets.Recorder.Events, "test-reconcile-on-cancelled-taskrun", wantEvents) + if !(err == nil) { + t.Errorf(err.Error()) + } + + _, err = clients.Kube.CoreV1().Pods(pod.ObjectMeta.Namespace).Get(testAssets.Ctx, pod.ObjectMeta.Name, metav1.GetOptions{}) + if tc.cancelEnabled { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + } else { + if !k8sapierrors.IsNotFound(err) { + t.Errorf("expected a NotFound response, got: %v", err) + } + } + }) + } +} + func TestReconcileTimeouts(t *testing.T) { type testCase struct { name string