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

Explain crontab entry in schedule command in plain English #103

Merged
merged 5 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ file. This change log follows the conventions of

## [Unreleased]

### Added

- Subcommand `schedule explain <crontab>` to explain a given `crontab`
in plain English
([#102](https://github.com/pilosus/dienstplan/issues/102))

## [1.1.90] - 2023-08-03

### Added
Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ make all

5. Update current tag mentions in `README.md`:

- Docker version
- Usage examples

6. After release preparation, add a tag with `git tag X.Y.Z`
16 changes: 8 additions & 8 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@

;; Logging
org.clojure/tools.logging {:mvn/version "1.2.4"}
ch.qos.logback/logback-classic {:mvn/version "1.4.8"}
ch.qos.logback/logback-classic {:mvn/version "1.4.11"}
ch.qos.logback.contrib/logback-json-classic {:mvn/version "0.1.5"}
ch.qos.logback.contrib/logback-jackson {:mvn/version "0.1.5"}
com.fasterxml.jackson.core/jackson-databind {:mvn/version "2.15.2"}

;; Alerts
io.sentry/sentry-clj {:mvn/version "6.26.199"}
io.sentry/sentry-clj {:mvn/version "6.28.200"}

;; Validation
expound/expound {:mvn/version "0.9.0"}
Expand All @@ -44,10 +44,10 @@
org.postgresql/postgresql {:mvn/version "42.6.0"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
dev.weavejester/ragtime {:mvn/version "0.9.3"}
com.github.seancorfield/honeysql {:mvn/version "2.4.1045"}
com.github.seancorfield/honeysql {:mvn/version "2.4.1066"}

;; Cron
org.pilosus/kairos {:mvn/version "0.1.14"}
org.pilosus/kairos {:mvn/version "0.2.22"}

;; Cryptography
buddy/buddy-core {:mvn/version "1.11.423"}}
Expand All @@ -56,7 +56,7 @@
;; Building
;; clojure -T:build (clean|jar|uberjar)
:build
{:replace-deps {io.github.clojure/tools.build {:git/tag "v0.9.4" :git/sha "76b78fe"}}
{:replace-deps {io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
:ns-default build}

;; Running
Expand All @@ -77,10 +77,10 @@
{io.github.athos/clj-check {:git/tag "0.1.0" :git/sha "0ca84df"}
cloverage/cloverage {:mvn/version "1.2.4"}
jonase/eastwood {:mvn/version "1.4.0"}
io.github.weavejester/cljfmt {:git/tag "0.11.1" :git/sha "0882f99"}
io.github.weavejester/cljfmt {:git/tag "0.11.2" :git/sha "fb26b22"}
io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}
nrepl/nrepl {:mvn/version "1.0.0"}
cider/cider-nrepl {:mvn/version "0.32.0"}
cider/cider-nrepl {:mvn/version "0.37.0"}
com.bhauman/rebel-readline {:mvn/version "0.1.4"}
org.clojure/test.check {:mvn/version "1.1.1"}}}

Expand Down Expand Up @@ -167,7 +167,7 @@
;; clojure -T:outdated :upgrade true :force true
:outdated
{:replace-paths ["."]
:replace-deps {com.github.liquidz/antq {:mvn/version "2.5.1095"}
:replace-deps {com.github.liquidz/antq {:mvn/version "2.5.1109"}
org.slf4j/slf4j-nop {:mvn/version "2.0.7"}}
:exec-fn antq.tool/outdated
:exec-args {:directory ["."] ;; default
Expand Down
16 changes: 15 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ A meta-command to create, delete or list schedules.

where:

- `<subcommand>` is one of: `[create, delete, list]`
- `<subcommand>` is one of: `[create, delete, list, explain]`
- `"<executalbe>"` is a command for a bot to run on schedule
- `<crontab>` is a crontab file line in
[vixie-cron](https://man7.org/linux/man-pages/man5/crontab.5.html)
Expand Down Expand Up @@ -182,6 +182,20 @@ List all the schedules in the channel:
@dienstplan schedule list
```

#### Explain

Explain a given `crontab` in plain English, e.g.:

```
@dienstplan schedule explain 0 22 * */2 Mon-Fri
```

returns:

```
Crontab `0 22 * */2 Mon-Fri` means the executable will be run at minute 0, past hour 22, on every day of week from Monday through Friday, in every 2nd month
```

### Help

Show a help message for the bot:
Expand Down
46 changes: 32 additions & 14 deletions src/dienstplan/commands.clj
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ Example:
@dienstplan schedule <subcommand> \"<executable>\" <crontab>
```

where <subcommand> is one of: [create, delete, list]
where <subcommand> is one of: [create, delete, list, explain]
\"<executalbe>\" is a command for a bot to run on schedule
<crontab> is a crontab file line, e.g. `0 9 * * Mon-Fri`

Expand All @@ -239,6 +239,7 @@ Example:
@dienstplan schedule create \"rotate my-rota\" 0 7 * * Mon-Fri
@dienstplan schedule delete \"rotate my-rota\"
@dienstplan schedule list
@dienstplan schedule explain 0 6,10-18/2,22 * * Mon-Fri
```

Caveats:
Expand All @@ -259,7 +260,7 @@ Caveats:
"(?s) is a pattern flag for dot matching all symbols including newlines"
#"^[^<@]*(?s)(?<userid><@[^>]+>)[\u00A0|\u2007|\u202F|\s]+(?<command>\w+)[\u00A0|\u2007|\u202F|\s]*(?<rest>.*)")

(def regex-schedule #"(?s)\b(?<subcommand>create|delete|list|shout)[\u00A0|\u2007|\u202F|\s]*(?<enclosed>\"(?<executable>.*)\")?[\u00A0|\u2007|\u202F|\s]*(?<crontab>.*)?")
(def regex-schedule #"(?s)\b(?<subcommand>create|delete|list|shout|explain)[\u00A0|\u2007|\u202F|\s]*(?<enclosed>\"(?<executable>.*)\")?[\u00A0|\u2007|\u202F|\s]*(?<crontab>.*)?")

(def commands->data
{:create {:spec ::spec/bot-cmd-create-or-update
Expand Down Expand Up @@ -644,34 +645,50 @@ Caveats:
(defn schedule-args-validation
[command-map]
(let [{:keys [subcommand executable crontab]} (get command-map :args)
executable-ok? (or (= subcommand "list")
executable-ok? (or (contains? #{"explain" "list"} subcommand)
(->> executable
(format "<@placeholder> %s")
parse-command
:command
some?))
crontab-validation (helpers/cron-validation crontab)
crontab-ok? (or (contains? #{"delete" "list" "shout"} subcommand)
(helpers/cron-valid? crontab))]
(:ok? crontab-validation))]
(cond
(not executable-ok?) :executable
(not crontab-ok?) :crontab
:else :valid)))
(not executable-ok?)
{:ok? false :error "`<executable>` cannot be parsed"}
(not crontab-ok?)
{:ok? false :error (format "`<crontab>` error: %s"
(:error crontab-validation))}
:else
{:ok? true})))

(defn fmt-schedule-invalid-arg
[invalid-arg]
(format "Invalid <%s> argument for `schedule` command\n\n%s"
(name invalid-arg)
help-cmd-schedule))
(let [{:keys [error]} invalid-arg]
(format "`schedule` command failed: %s\n\n%s"
error
help-cmd-schedule)))

(defn schedule-shout
[query-params]
(let [{:keys [executable crontab]} query-params
text (format "Executing `%s` with schedule `%s`" executable crontab)]
(let [{:keys [executable crontab explain]} query-params
text (format "Executing `%s` with schedule `%s` (%s)"
executable
crontab
explain)]
{:result text}))

(defn schedule-explain
"Return text description of a given crontab"
[crontab]
(let [explain (helpers/cron->text crontab)
text (format "Crontab `%s` means the executable will be run %s" crontab explain)]
{:result text}))

(defmethod command-exec! :schedule [command-map]
(let [args-validation (schedule-args-validation command-map)]
(if (= args-validation :valid)
(if (= (:ok? args-validation) true)
(let [crontab (get-in command-map [:args :crontab])
query-params {:channel (get-in command-map [:context :channel])
:executable (get-in command-map [:args :executable])
Expand All @@ -682,7 +699,8 @@ Caveats:
:create (db/schedule-insert! query-params)
:delete (db/schedule-delete! query-params)
:list (db/schedule-list query-params)
:shout (schedule-shout query-params))
:shout (schedule-shout query-params)
:explain (schedule-explain crontab))
message (or result (:message error))]
message)
(fmt-schedule-invalid-arg args-validation))))
Expand Down
11 changes: 8 additions & 3 deletions src/dienstplan/db.clj
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,16 @@
(defn schedule-insert!
[params]
(jdbc/with-transaction [^java.sql.Connection conn db]
(try (let [inserted (sql/insert! conn :schedule params)]
(try (let [inserted (sql/insert! conn :schedule params)
explain (-> params
:crontab
helpers/cron->text)]
(log/debugf "Schedule inserted: %s" inserted)
{:result (when (-> inserted :schedule/id int?)
(format "Executable `%s` successfully scheduled with `%s`"
(:executable params) (:crontab params)))})
(format "Executable `%s` successfully scheduled with `%s` (%s)"
(:executable params)
(:crontab params)
explain))})
(catch PSQLException e
(let [message (.getMessage e)
duplicate? (string/includes? (.toLowerCase message) "duplicate key")
Expand Down
15 changes: 9 additions & 6 deletions src/dienstplan/helpers.clj
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,18 @@
"Return java.sql.Timestamp for the next run for a given crontab string"
^Timestamp [crontab]
(try (-> crontab
(kairos/get-dt-seq)
(kairos/cron->dt)
^ZonedDateTime first
.toInstant
java.sql.Timestamp/from)
(catch Exception _ nil)))

(defn cron-valid?
"Return true if crontab is valid"
(defn cron-validation
"Return crontab validation result"
[crontab]
(-> crontab
kairos/parse-cron
some?))
(kairos/cron-validate crontab))

(defn cron->text
"Explain crontab in plain English"
[crontab]
(kairos/cron->text crontab))
2 changes: 1 addition & 1 deletion src/dienstplan/spec.clj
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@
:bot-cmd-common/command
:bot-cmd-create-or-update/args]))

(s/def :bot-schedule-args/subcommand #{"create" "delete" "list" "shout"})
(s/def :bot-schedule-args/subcommand #{"create" "delete" "list" "shout" "explain"})
(s/def :bot-schedule-args/executable ::nillable-str)
(s/def :bot-schedule-args/crontab ::nillable-str)

Expand Down
51 changes: 41 additions & 10 deletions test/dienstplan/api_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@
(is (= 200 (:status schedule-create-rotate-response)))
(is (=
{:channel "C123"
:text "Executable `rotate my-rota` successfully scheduled with `5 0 * * Mon-Fri`"}
:text "Executable `rotate my-rota` successfully scheduled with `5 0 * * Mon-Fri` (at minute 5, past hour 0, on every day of week from Monday through Friday, in every month)"}
(-> schedule-create-rotate-response
:body
(json/parse-string true))))
Expand All @@ -724,7 +724,7 @@
(is (= 200 (:status schedule-create-who-response)))
(is (=
{:channel "C123"
:text "Executable `who my-rota` successfully scheduled with `0 9 * * Mon-Fri`"}
:text "Executable `who my-rota` successfully scheduled with `0 9 * * Mon-Fri` (at minute 0, past hour 9, on every day of week from Monday through Friday, in every month)"}
(-> schedule-create-who-response
:body
(json/parse-string true))))
Expand Down Expand Up @@ -774,6 +774,38 @@
:body
(json/parse-string true)))))))

(def params-test-schedule-explain
[["0 22 * * *"
"Crontab `0 22 * * *` means the executable will be run at minute 0, past hour 22, on every day, in every month"
"ok"]
["67 22 * * *"
(format "`schedule` command failed: `<crontab>` error: Value error in 'minute' field. Given value: [67, 68). Expected: [0, 60)\n\n%s" cmd/help-cmd-schedule)
"Wrong value"]
["Mon-Fri"
(format "`schedule` command failed: `<crontab>` error: Invalid crontab format\n\n%s" cmd/help-cmd-schedule)
"Parding error"]])

(deftest ^:integration test-schedule-explain
(testing "Explain a schedule"
(doseq [[crontab expected description] params-test-schedule-explain]
(testing description
(let [command (format "<@U001> schedule explain %s" crontab)
response (http/request
(merge
events-request-base
{:body
(json/generate-string
{:event
{:text command
:ts "1640250011.000100"
:team "T123"
:channel "C123"}})}))]
(is (= 200 (:status response)))
(is (= {:channel "C123" :text expected}
(-> response
:body
(json/parse-string true)))))))))

(deftest ^:integration test-schedule-runner-ok
(testing "Test background task for schedule processing"
(with-redefs
Expand Down Expand Up @@ -856,14 +888,12 @@
event-after (first events-after-processing)]
(is (= event-before event-after)))))))

(def schedule-invalid-args "Invalid arguments for `schedule` command: %s")

(def params-schedule-command-invalid-args
[["<@U001> schedule create who my rota 0 9 * * Mon-Fri"
:executable
[["<@u001> schedule create who my rota 0 9 * * Mon-Fri"
{:ok? false :error "`<executable>` cannot be parsed"}
"Invalid executable, double quotes omitted"]
["<@U001> schedule create \"who my rota\" Mon-Fri"
:crontab
["<@u001> schedule create \"who my rota\" Mon-Fri"
{:ok? false :error "`<crontab>` error: Invalid crontab format"}
"Invalid crontab"]])

(deftest ^:integration test-schedule-command-invalid-args
Expand Down Expand Up @@ -891,6 +921,7 @@
(testing "Duplicate schedule"
(let [executable "rotate my-rota"
crontab "0 9 * * Mon-Fri"
explain "at minute 0, past hour 9, on every day of week from Monday through Friday, in every month"
command (format "<@U001> schedule create \"%s\" %s"
executable
crontab)
Expand All @@ -913,8 +944,8 @@
:body
(json/parse-string true)
:text)]
(is (= created (format "Executable `%s` successfully scheduled with `%s`"
executable crontab)))
(is (= created (format "Executable `%s` successfully scheduled with `%s` (%s)"
executable crontab explain)))
(is (= duplicate (format "Duplicate schedule for `%s` in the channel"
executable))))))

Expand Down