Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement schedule commands #459

Merged
merged 32 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c209817
all .md but create/update
dnr Feb 8, 2024
2b1a62e
more
dnr Feb 8, 2024
74cc793
create/update w/shared options
dnr Feb 9, 2024
97d51f0
skeleton and fix build
dnr Feb 9, 2024
f23179c
add old impl as comments
dnr Feb 9, 2024
28b3930
start implementing
dnr Feb 9, 2024
47715fc
clean up for PR
dnr Feb 15, 2024
7e3adf4
Merge remote-tracking branch 'origin/cli-rewrite' into cli-rewrite-sc…
dnr Mar 14, 2024
a703ceb
trigger/pause/unpause
dnr Mar 14, 2024
c6d170f
some work on describe
dnr Mar 14, 2024
cd12651
list, refactor
dnr Mar 14, 2024
5c53907
more list, -l
dnr Mar 14, 2024
17b64df
sa and memo
dnr Mar 14, 2024
514d0e9
start on start
dnr Mar 14, 2024
4c54b6e
timestamp type
dnr Mar 14, 2024
f380e30
calendarspec -> cron string
dnr Mar 15, 2024
daed718
update
dnr Mar 15, 2024
22951d5
refactor
dnr Mar 15, 2024
9298c57
start test
dnr Mar 15, 2024
5bfa144
get rid of --raw, json output is always raw
dnr Mar 21, 2024
3c43ed9
fix spec formatting
dnr Mar 21, 2024
c60f3dd
use raw grpc for json list
dnr Mar 21, 2024
0d96cf2
more output
dnr Mar 21, 2024
2bb0c2c
fix test, no shorthand paylods
dnr Mar 21, 2024
a627e21
add some more tests
dnr Mar 21, 2024
684aa9d
Merge branch 'cli-rewrite' of github.com:temporalio/cli into cli-rewr…
dnr Mar 21, 2024
f888d67
fix
dnr Mar 21, 2024
a44ff13
add todo for shorthand payloads
dnr Mar 22, 2024
a6e00b9
faster tests
dnr Mar 22, 2024
d0a9b16
fix TestTaskQueue_BuildId
dnr Mar 22, 2024
92f720e
fix and test roundtrip search attributes and memo
dnr Mar 22, 2024
0965de0
drop testVars
dnr Mar 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 338 additions & 8 deletions temporalcli/commands.gen.go

Large diffs are not rendered by default.

620 changes: 620 additions & 0 deletions temporalcli/commands.schedule.go

Large diffs are not rendered by default.

396 changes: 396 additions & 0 deletions temporalcli/commands.schedule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,396 @@
package temporalcli_test

import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/rand"
"regexp"
"time"

"github.com/temporalio/cli/temporalcli"
"go.temporal.io/sdk/workflow"
)

func (s *SharedServerSuite) createSchedule(args ...string) (schedId, schedWfId string, res *CommandResult) {
schedId = fmt.Sprintf("sched-%x", rand.Uint32())
schedWfId = fmt.Sprintf("my-wf-id-%x", rand.Uint32())
s.Worker.OnDevWorkflow(func(ctx workflow.Context, input any) (any, error) {
return nil, workflow.Sleep(ctx, 10*time.Second)
})
s.T().Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
options := temporalcli.CommandOptions{
Stdout: io.Discard,
Stderr: io.Discard,
Args: []string{
"schedule", "delete",
"--address", s.Address(),
"-s", schedId,
},
Fail: func(error) {},
}
temporalcli.Execute(ctx, options)
})
res = s.Execute(append([]string{
"schedule", "create",
"--address", s.Address(),
"-s", schedId,
"--task-queue", s.Worker.Options.TaskQueue,
"--type", "DevWorkflow",
"--workflow-id", schedWfId,
}, args...)...,
)
return
}

func (s *SharedServerSuite) TestSchedule_Create() {
_, _, res := s.createSchedule("--interval", "10d")
s.NoError(res.Err)
}

func (s *SharedServerSuite) TestSchedule_Delete() {
schedId, _, res := s.createSchedule("--interval", "10d")
s.NoError(res.Err)

// check exists
res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)

res = s.Execute(
"schedule", "delete",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)

// doesn't exist anymore
res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
)
s.Error(res.Err)
}

func (s *SharedServerSuite) TestSchedule_Describe() {
schedId, schedWfId, res := s.createSchedule("--interval", "2s")
s.NoError(res.Err)

// run once manually so we see a running workflow

res = s.Execute(
"schedule", "trigger",
"--address", s.Address(),
"-s", schedId,
)

// text

s.Eventually(func() bool {
res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)
out := res.Stdout.String()
s.ContainsOnSameLine(out, "ScheduleId", schedId)
s.ContainsOnSameLine(out, "Spec", "2s")
return AssertContainsOnSameLine(out, "RunningWorkflows", schedWfId+"-") == nil
}, 10*time.Second, 100*time.Millisecond)

// json

res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
"-o", "json",
)
s.NoError(res.Err)
var j struct {
Schedule struct {
Action struct {
StartWorkflow struct {
Id string `json:"workflowId"`
} `json:"startWorkflow"`
} `json:"action"`
} `json:"schedule"`
}
s.NoError(json.Unmarshal(res.Stdout.Bytes(), &j))
s.Equal(schedWfId, j.Schedule.Action.StartWorkflow.Id)
}

func (s *SharedServerSuite) TestSchedule_CreateDescribeCalendar() {
schedId, _, res := s.createSchedule("--calendar", `{"hour":"2,4","dayOfWeek":"thu,fri"}`)
s.NoError(res.Err)

res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)
out := res.Stdout.String()
s.ContainsOnSameLine(out, "ScheduleId", schedId)
s.ContainsOnSameLine(out, "Spec", "dayOfWeek")
}

func (s *SharedServerSuite) TestSchedule_CreateDescribe_SearchAttributes_Memo() {
schedId, _, res := s.createSchedule("--interval", "10d",
"--schedule-search-attribute", `CustomKeywordField="schedule-string-val"`,
"--search-attribute", `CustomKeywordField="workflow-string-val"`,
"--schedule-memo", `schedMemo="data here"`,
"--memo", `wfMemo="other data"`,
)
s.NoError(res.Err)

res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)
out := res.Stdout.String()
// TODO: We have to disable shorthand payload encoding for now so these come out as base64.
// After https://github.com/temporalio/api-go/pull/154, ensure these come out as nice strings.
b64 := func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }
s.ContainsOnSameLine(out, "SearchAttributes", "CustomKeywordField", b64(`"schedule-string-val"`))
s.ContainsOnSameLine(out, "Memo", "schedMemo", `"data here"`) // somehow this one comes out as a string anyway
s.ContainsOnSameLine(out, "Action", "CustomKeywordField", b64(`"workflow-string-val"`))
s.ContainsOnSameLine(out, "Action", "wfMemo", b64(`"other data"`))
}

func (s *SharedServerSuite) TestSchedule_List() {
schedId, _, res := s.createSchedule("--interval", "10d")
s.NoError(res.Err)

// table

s.Eventually(func() bool {
res = s.Execute(
"schedule", "list",
"--address", s.Address(),
)
s.NoError(res.Err)
out := res.Stdout.String()
return AssertContainsOnSameLine(out, schedId, "DevWorkflow", "false") == nil
}, 10*time.Second, time.Second)
dnr marked this conversation as resolved.
Show resolved Hide resolved

// table long

res = s.Execute(
"schedule", "list",
"--address", s.Address(),
"--long",
)
s.NoError(res.Err)
out := res.Stdout.String()
s.ContainsOnSameLine(out, schedId, "DevWorkflow", "false")

// table really-long

res = s.Execute(
"schedule", "list",
"--address", s.Address(),
"--really-long",
)
s.NoError(res.Err)
out = res.Stdout.String()
s.ContainsOnSameLine(out, schedId, "DevWorkflow", "0s" /*jitter*/, "false", "nil" /*memo*/)

// json

res = s.Execute(
"schedule", "list",
"--address", s.Address(),
"-o", "json",
)
s.NoError(res.Err)
var j []struct {
ScheduleId string `json:"scheduleId"`
}
s.NoError(json.Unmarshal(res.Stdout.Bytes(), &j))
ok := false
for _, entry := range j {
ok = ok || entry.ScheduleId == schedId
}
s.True(ok, "schedule not found in json result")

// jsonl

res = s.Execute(
"schedule", "list",
"--address", s.Address(),
"-o", "jsonl",
)
s.NoError(res.Err)
lines := bytes.Split(res.Stdout.Bytes(), []byte("\n"))
ok = false
for _, line := range lines {
if len(bytes.TrimSpace(line)) == 0 {
continue
}
var j struct {
ScheduleId string `json:"scheduleId"`
}
s.NoError(json.Unmarshal(line, &j))
ok = ok || j.ScheduleId == schedId
}
s.True(ok, "schedule not found in jsonl result")
}

func (s *SharedServerSuite) TestSchedule_Toggle() {
schedId, _, res := s.createSchedule("--interval", "10d")
s.NoError(res.Err)

// pause

res = s.Execute(
"schedule", "toggle",
"--address", s.Address(),
"-s", schedId,
"--pause",
"--reason", "testing",
)
s.NoError(res.Err)

res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)
out := res.Stdout.String()
s.ContainsOnSameLine(out, "Paused", "true")
s.ContainsOnSameLine(out, "Notes", "testing")

// unpause

res = s.Execute(
"schedule", "toggle",
"--address", s.Address(),
"-s", schedId,
"--unpause",
"--reason", "we're done testing",
)
s.NoError(res.Err)

res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)
out = res.Stdout.String()
s.ContainsOnSameLine(out, "Paused", "false")
s.ContainsOnSameLine(out, "Notes", "done testing")
}

func (s *SharedServerSuite) TestSchedule_Trigger() {
schedId, schedWfId, res := s.createSchedule("--interval", "10d")
s.NoError(res.Err)

res = s.Execute(
"schedule", "trigger",
"--address", s.Address(),
"-s", schedId,
)
s.NoError(res.Err)

s.Eventually(func() bool {
res = s.Execute(
"workflow", "list",
"--address", s.Address(),
"-q", fmt.Sprintf(`TemporalScheduledById = "%s"`, schedId),
)
s.NoError(res.Err)
out := res.Stdout.String()
return AssertContainsOnSameLine(out, schedWfId) == nil
}, 10*time.Second, 100*time.Millisecond)
}

func (s *SharedServerSuite) TestSchedule_Backfill() {
schedId, schedWfId, res := s.createSchedule("--interval", "10d/5h")
s.NoError(res.Err)

res = s.Execute(
"schedule", "backfill",
"--address", s.Address(),
"-s", schedId,
"--start-time", "2022-02-02T00:00:00Z",
"--end-time", "2022-02-28T00:00:00Z",
"--overlap-policy", "AllowAll",
)
s.NoError(res.Err)

s.Eventually(func() bool {
res = s.Execute(
"workflow", "list",
"--address", s.Address(),
"-q", fmt.Sprintf(`TemporalScheduledById = "%s"`, schedId),
)
s.NoError(res.Err)
out := res.Stdout.String()
re := regexp.MustCompile(regexp.QuoteMeta(schedWfId + "-2022-02"))
return len(re.FindAllString(out, -1)) == 3
}, 10*time.Second, 100*time.Millisecond)
}

func (s *SharedServerSuite) TestSchedule_Update() {
schedId, schedWfId, res := s.createSchedule("--interval", "10d")
s.NoError(res.Err)

res = s.Execute(
"schedule", "update",
"--address", s.Address(),
"-s", schedId,
"--task-queue", "SomeOtherTq",
"--type", "SomeOtherWf",
"--workflow-id", schedWfId,
"--interval", "1h",
)
s.NoError(res.Err)

s.Eventually(func() bool {
res = s.Execute(
"schedule", "describe",
"--address", s.Address(),
"-s", schedId,
"-o", "json",
)
s.NoError(res.Err)
var j struct {
Schedule struct {
Spec struct {
Interval []struct {
Interval string `json:"interval"`
} `json:"interval"`
} `json:"spec"`
Action struct {
StartWorkflow struct {
WorkflowType struct {
Name string `json:"name"`
} `json:"workflowType"`
TaskQueue struct {
Name string `json:"name"`
} `json:"taskQueue"`
} `json:"startWorkflow"`
} `json:"action"`
} `json:"schedule"`
}
s.NoError(json.Unmarshal(res.Stdout.Bytes(), &j))
return j.Schedule.Action.StartWorkflow.WorkflowType.Name == "SomeOtherWf" &&
j.Schedule.Action.StartWorkflow.TaskQueue.Name == "SomeOtherTq" &&
j.Schedule.Spec.Interval[0].Interval == "3600s"
}, 10*time.Second, 100*time.Millisecond)
}
Loading
Loading