From 85707e89f3befe8f4ebcb07725109d8d1d21131b Mon Sep 17 00:00:00 2001
From: Karol Kokoszka <karol.kokoszka@scylladb.com>
Date: Thu, 28 Mar 2024 17:51:40 +0100
Subject: [PATCH] fix(tasks): list tasks with better formatting

---
 pkg/command/flag/flag.go            |  1 -
 pkg/managerclient/model.go          | 16 +++++++++++++++-
 pkg/restapi/task.go                 |  3 +++
 pkg/service/scheduler/model.go      | 16 +++++++++-------
 pkg/service/scheduler/model_test.go |  8 ++++----
 5 files changed, 31 insertions(+), 13 deletions(-)

diff --git a/pkg/command/flag/flag.go b/pkg/command/flag/flag.go
index feb60ba623..e47eb23e22 100644
--- a/pkg/command/flag/flag.go
+++ b/pkg/command/flag/flag.go
@@ -146,7 +146,6 @@ func (w Wrapper) interval(p *Duration) {
 
 func (w Wrapper) startDate(p *StartDate) {
 	w.fs.VarP(p, "start-date", "s", usage["start-date"])
-	w.MustMarkDeprecated("start-date", "use cron instead")
 }
 
 func (w Wrapper) numRetries(p *int, def int) {
diff --git a/pkg/managerclient/model.go b/pkg/managerclient/model.go
index 3e1acb6b35..82821df410 100644
--- a/pkg/managerclient/model.go
+++ b/pkg/managerclient/model.go
@@ -3,6 +3,7 @@
 package managerclient
 
 import (
+	"encoding/json"
 	"fmt"
 	"io"
 	"sort"
@@ -14,7 +15,9 @@ import (
 	"github.com/pkg/errors"
 	"github.com/scylladb/go-set/strset"
 	"github.com/scylladb/scylla-manager/v3/pkg/managerclient/table"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/scheduler"
 	"github.com/scylladb/scylla-manager/v3/pkg/util/inexlist"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
 	"github.com/scylladb/scylla-manager/v3/pkg/util/version"
 	"github.com/scylladb/scylla-manager/v3/swagger/gen/scylla-manager/models"
 	"github.com/scylladb/termtables"
@@ -513,7 +516,18 @@ func (li TaskListItems) Render(w io.Writer) error {
 
 		var schedule string
 		if t.Schedule.Cron != "" {
-			schedule = t.Schedule.Cron
+			var cronSpec scheduler.CronSpecification
+			err := json.Unmarshal([]byte(t.Schedule.Cron), &cronSpec)
+			if err != nil {
+				schedule = t.Schedule.Cron
+			} else {
+				schedule = cronSpec.Spec
+				if cronSpec.StartDate.After(timeutc.Now()) {
+					c := scheduler.MustCron(cronSpec.Spec, cronSpec.StartDate)
+					schedule += fmt.Sprintf(" with first activation after %s",
+						c.Next(cronSpec.StartDate).Format("2006-01-02 15:04:05"))
+				}
+			}
 		} else if t.Schedule.Interval != "" {
 			schedule = t.Schedule.Interval
 		}
diff --git a/pkg/restapi/task.go b/pkg/restapi/task.go
index c6cec1fd9e..e5af12e1f5 100644
--- a/pkg/restapi/task.go
+++ b/pkg/restapi/task.go
@@ -151,6 +151,9 @@ func (h *taskHandler) parseTask(r *http.Request) (*scheduler.Task, error) {
 	if err := render.DecodeJSON(r.Body, &t); err != nil {
 		return nil, err
 	}
+	if !t.Sched.StartDate.IsZero() && t.Sched.Cron.Spec != "" {
+		t.Sched.Cron.StartDate = t.Sched.StartDate
+	}
 	t.ClusterID = mustClusterIDFromCtx(r)
 	return &t, nil
 }
diff --git a/pkg/service/scheduler/model.go b/pkg/service/scheduler/model.go
index 02e97500d4..c33cdc9d32 100644
--- a/pkg/service/scheduler/model.go
+++ b/pkg/service/scheduler/model.go
@@ -70,11 +70,13 @@ func (t *TaskType) UnmarshalText(text []byte) error {
 // Cron implements a trigger based on cron expression.
 // It supports the extended syntax including @monthly, @weekly, @daily, @midnight, @hourly, @every <time.Duration>.
 type Cron struct {
-	cronSpecification
+	CronSpecification
 	inner scheduler.Trigger
 }
 
-type cronSpecification struct {
+// CronSpecification combines specification for cron together with the start dates
+// that defines the moment when the cron is being started.
+type CronSpecification struct {
 	Spec      string    `json:"spec"`
 	StartDate time.Time `json:"start_date"`
 }
@@ -86,7 +88,7 @@ func NewCron(spec string, startDate time.Time) (Cron, error) {
 	}
 
 	return Cron{
-		cronSpecification: cronSpecification{
+		CronSpecification: CronSpecification{
 			Spec:      spec,
 			StartDate: startDate,
 		},
@@ -120,9 +122,9 @@ func (c Cron) Next(now time.Time) time.Time {
 }
 
 func (c Cron) MarshalText() (text []byte, err error) {
-	bytes, err := json.Marshal(c.cronSpecification)
+	bytes, err := json.Marshal(c.CronSpecification)
 	if err != nil {
-		return nil, errors.Wrapf(err, "cannot json marshal {%v}", c.cronSpecification)
+		return nil, errors.Wrapf(err, "cannot json marshal {%v}", c.CronSpecification)
 	}
 	return bytes, nil
 }
@@ -132,11 +134,11 @@ func (c *Cron) UnmarshalText(text []byte) error {
 		return nil
 	}
 
-	var cronSpec cronSpecification
+	var cronSpec CronSpecification
 	err := json.Unmarshal(text, &cronSpec)
 	if err != nil {
 		// fallback to the < 3.2.6 approach where cron was not coupled with start date
-		cronSpec = cronSpecification{
+		cronSpec = CronSpecification{
 			Spec: string(text),
 		}
 	}
diff --git a/pkg/service/scheduler/model_test.go b/pkg/service/scheduler/model_test.go
index 1cecbd5f43..a8bf966b50 100644
--- a/pkg/service/scheduler/model_test.go
+++ b/pkg/service/scheduler/model_test.go
@@ -66,12 +66,12 @@ func TestCronMarshalUnmarshal(t *testing.T) {
 	for _, tc := range []struct {
 		name         string
 		data         []byte
-		expectedSpec cronSpecification
+		expectedSpec CronSpecification
 	}{
 		{
 			name: "(3.2.6 backward compatibility) unmarshal spec",
 			data: []byte("@every 15s"),
-			expectedSpec: cronSpecification{
+			expectedSpec: CronSpecification{
 				Spec:      "@every 15s",
 				StartDate: time.Time{},
 			},
@@ -79,7 +79,7 @@ func TestCronMarshalUnmarshal(t *testing.T) {
 		{
 			name: "unmarshal spec full struct zero time",
 			data: []byte(`{"spec": "@every 15s", "start_date": "0001-01-01T00:00:00Z"}`),
-			expectedSpec: cronSpecification{
+			expectedSpec: CronSpecification{
 				Spec:      "@every 15s",
 				StartDate: time.Time{},
 			},
@@ -87,7 +87,7 @@ func TestCronMarshalUnmarshal(t *testing.T) {
 		{
 			name: "unmarshal spec full struct non-zero time",
 			data: []byte(`{"spec": "@every 15s", "start_date": "` + nonZeroTimeString + `"}`),
-			expectedSpec: cronSpecification{
+			expectedSpec: CronSpecification{
 				Spec:      "@every 15s",
 				StartDate: nonZeroTime,
 			},