Functions for commands that can be ran from the command-line with the Clojure CLI or the Metabase JAR. These are ran as follows: for example, running the clojure -M:run migrate force java --add-opens java.base/java.nio=ALL-UNNAMED -jar metabase.jar migrate force Logic below translates resolves the command itself to a function marked with You can see what commands are available by running the command | (ns metabase.cmd (:refer-clojure :exclude [load import]) (:require [clojure.string :as str] [clojure.tools.cli :as cli] [metabase.config :as config] [metabase.legacy-mbql.util :as mbql.u] [metabase.plugins.classloader :as classloader] [metabase.util :as u] [metabase.util.encryption :as encryption] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log])) |
(set! *warn-on-reflection* true) | |
Command processing and option parsing utilities, etc. | |
Proxy function to System/exit to enable the use of | (defn- system-exit! [return-code] (flush) (System/exit return-code)) |
Looks up a command var by name | (defn- cmd->var [command-name] (ns-resolve 'metabase.cmd (symbol command-name))) |
Resolves enterprise command by symbol and calls with args, or else throws error if not EE | (defn- call-enterprise
[symb & args]
(let [f (try
(classloader/require (symbol (namespace symb)))
(or (resolve symb)
(throw (ex-info (trs "{0} does not exist" symb) {})))
(catch Throwable e
(throw (ex-info (trs "The ''{0}'' command is only available in Metabase Enterprise Edition." (name symb))
{:command symb}
e))))]
(apply f args))) |
(defn- get-parsed-options [iref options] (:options (cli/parse-opts options (:arg-spec (meta iref))))) | |
Command implementations | |
Run database migrations. Valid options for | (defn ^:command migrate [direction] (classloader/require 'metabase.cmd.migrate) ((resolve 'metabase.cmd.migrate/migrate!) direction)) |
Transfer data from existing H2 database to the newly created MySQL or Postgres DB specified by env vars. | (defn ^:command load-from-h2 ([] (load-from-h2 nil)) ([h2-connection-string] (classloader/require 'metabase.cmd.load-from-h2) ((resolve 'metabase.cmd.load-from-h2/load-from-h2!) h2-connection-string))) |
(defn ^:command dump-to-h2
{:doc "Transfer data from existing database to newly created H2 DB with specified filename.
Target H2 file is deleted before dump, unless the --keep-existing flag is given."
:arg-spec [["-k" "--keep-existing" "Do not delete target H2 file if it exists."
:id :keep-existing?]
["-p" "--dump-plaintext" "Do not encrypt dumped contents."
:id :dump-plaintext?]]}
[h2-filename & opts]
(classloader/require 'metabase.cmd.dump-to-h2)
(try
(let [options (get-parsed-options #'dump-to-h2 opts)]
((resolve 'metabase.cmd.dump-to-h2/dump-to-h2!) h2-filename options)
(println "Dump complete")
(system-exit! 0))
(catch Throwable e
(log/error e "Failed to dump application database to H2 file")
(system-exit! 1)))) | |
Reset the password for a user with | (defn ^:command reset-password [email-address] (classloader/require 'metabase.cmd.reset-password) ((resolve 'metabase.cmd.reset-password/reset-password!) email-address)) |
Show this help message listing valid Metabase commands. | (defn ^:command help
([command-name]
(let [{:keys [doc arg-spec arglists]} (meta (cmd->var command-name))]
(doseq [arglist arglists]
(apply println command-name arglist))
(when doc
(doseq [doc-line (str/split doc #"\n\s+")]
(println "\t" doc-line)))
(when (seq arg-spec)
(println "\t" "Options:")
(doseq [opt-line (str/split (:summary (cli/parse-opts [] arg-spec)) #"\n")]
(println "\t" opt-line)))))
([]
(println "Valid commands are:")
(doseq [[symb varr] (sort (ns-interns 'metabase.cmd))
:when (:command (meta varr))]
(help symb)
(println))
(println "\nSome other commands you might find useful:\n")
(println "java -cp metabase.jar org.h2.tools.Shell -url jdbc:h2:/path/to/metabase.db")
(println "\tOpen an SQL shell for the Metabase H2 DB"))) |
Print version information about Metabase and the current system. | (defn ^:command version
[]
(println "Metabase version:" config/mb-version-info)
(println "\nOS:"
(System/getProperty "os.name")
(System/getProperty "os.version")
(System/getProperty "os.arch"))
(println "\nJava version:"
(System/getProperty "java.vm.name")
(System/getProperty "java.version"))
(println "\nCountry:" (System/getProperty "user.country"))
(println "System timezone:" (System/getProperty "user.timezone"))
(println "Language:" (System/getProperty "user.language"))
(println "File encoding:" (System/getProperty "file.encoding"))) |
Generate a markdown file containing documentation for all API endpoints. This is written to a file called
| (defn ^:command api-documentation [] (classloader/require 'metabase.cmd.endpoint-dox) ((resolve 'metabase.cmd.endpoint-dox/generate-dox!))) |
Generates a markdown file containing documentation for environment variables relevant to configuring Metabase. The command only includes environment variables registered as defsettings. For a full list of environment variables, see https://www.metabase.com/docs/latest/configuring-metabase/environment-variables. | (defn ^:command environment-variables-documentation [] (classloader/require 'metabase.cmd.env-var-dox) ((resolve 'metabase.cmd.env-var-dox/generate-dox!))) |
Generates a markdown file with some documentation and an example configuration file in YAML. The YAML template includes Metabase settings and their defaults.
Metabase will save the template as | (defn ^:command config-template [] (classloader/require 'metabase.cmd.config-file-gen) ((resolve 'metabase.cmd.config-file-gen/generate-config-file-doc!))) |
Print a list of all multimethods available for a driver to implement, optionally with their docstrings. | (defn ^:command driver-methods ([] (classloader/require 'metabase.cmd.driver-methods) ((resolve 'metabase.cmd.driver-methods/print-available-multimethods) false)) ([_docs] (classloader/require 'metabase.cmd.driver-methods) ((resolve 'metabase.cmd.driver-methods/print-available-multimethods) true))) |
(defn ^:command load
{:doc "Note: this command is deprecated. Use `import` instead.
Load serialized Metabase instance as created by [[dump]] command from directory `path`."
:arg-spec [["-m" "--mode (skip|update)" "Update or skip on conflicts."
:default :skip
:default-desc "skip"
:parse-fn mbql.u/normalize-token
:validate [#{:skip :update} "Must be 'skip' or 'update'"]]
["-e" "--on-error (continue|abort)" "Abort or continue on error."
:default :continue
:default-desc "continue"
:parse-fn mbql.u/normalize-token
:validate [#{:continue :abort} "Must be 'continue' or 'abort'"]]]}
[path & options]
(log/warn (u/colorize :red "'load' is deprecated and will be removed in a future release. Please migrate to 'import'."))
(call-enterprise 'metabase-enterprise.serialization.cmd/v1-load! path (get-parsed-options #'load options))) | |
(defn ^:command ^:requires-init import
{:doc "Load serialized Metabase instance as created by the [[export]] command from directory `path`."
:arg-spec [["-e" "--continue-on-error" "Do not break execution on errors."]
[ "--full-stacktrace" "Output full stacktraces on errors."]]}
[path & options]
(call-enterprise 'metabase-enterprise.serialization.cmd/v2-load! path (get-parsed-options #'import options))) | |
(defn ^:command dump
{:doc "Note: this command is deprecated. Use `export` instead.
Serializes Metabase instance into directory `path`."
:arg-spec [["-u" "--user EMAIL" "Export collections owned by the specified user"]
["-s" "--state (active|all)" "When set to `active`, do not dump archived entities. Default behavior is `all`."
:default :all
:default-desc "all"
:parse-fn mbql.u/normalize-token
:validate [#{:active :all} "Must be 'active' or 'all'"]]
[nil "--include-entity-id" "Include entity_id property in all dumped entities. Default: false."]]}
[path & options]
(log/warn (u/colorize :red "'dump' is deprecated and will be removed in a future release. Please migrate to 'export'."))
(call-enterprise 'metabase-enterprise.serialization.cmd/v1-dump! path (get-parsed-options #'dump options))) | |
(defn ^:command export
{:doc "Serialize Metabase instance into directory at `path`."
:arg-spec [["-c" "--collection ID" "Export only specified ID(s). Use commas to separate multiple IDs. Pass either PKs or entity IDs."
:id :collection-ids
:parse-fn (fn [raw-string] (->> (str/split raw-string #"\s*,\s*")
(map (fn [v]
(cond
(str/starts-with? v "eid:") v
(= (count v) 21) v
:else (parse-long v))))))]
["-C" "--no-collections" "Do not export any content in collections."]
["-S" "--no-settings" "Do not export settings.yaml"]
["-D" "--no-data-model" "Do not export any data model entities; useful for subsequent exports."]
["-f" "--include-field-values" "Include field values along with field metadata."]
["-s" "--include-database-secrets" "Include database connection details (in plain text; use caution)."]
["-e" "--continue-on-error" "Do not break execution on errors."]
[ "--full-stacktrace" "Output full stacktraces on errors."]]}
[path & options]
(call-enterprise 'metabase-enterprise.serialization.cmd/v2-dump! path (get-parsed-options #'export options))) | |
Add entity IDs for instances of serializable models that don't already have them. | (defn ^:command seed-entity-ids
[]
(when-not (call-enterprise 'metabase-enterprise.serialization.cmd/seed-entity-ids!)
(throw (Exception. "Error encountered while seeding entity IDs")))) |
Drop entity IDs for instances of serializable models. Useful for migrating from v1 serialization (x.46 and earlier) to v2 (x.47+). | (defn ^:command drop-entity-ids
[]
(when-not (call-enterprise 'metabase-enterprise.serialization.cmd/drop-entity-ids!)
(throw (Exception. "Error encountered while dropping entity IDs")))) |
Rotate the encryption key of a metabase database. The MBENCRYPTIONSECRET_KEY environment variable has to be set to
the current key, and the parameter | (defn ^:command rotate-encryption-key
[new-key]
(classloader/require 'metabase.cmd.rotate-encryption-key)
(try
((resolve 'metabase.cmd.rotate-encryption-key/rotate-encryption-key!) new-key)
(log/info "Encryption key rotation OK.")
(system-exit! 0)
(catch Throwable e
(log/error e "ERROR ROTATING KEY.")
(system-exit! 1)))) |
Decrypts data in the metabase database. The MBENCRYPTIONSECRET_KEY environment variable has to be set to the current key | (defn ^:command remove-encryption
[]
(classloader/require 'metabase.cmd.remove-encryption)
(when-not (encryption/default-encryption-enabled?)
(log/error "MB_ENCRYPTION_SECRET_KEY environment variable has not been set")
(system-exit! 1))
(try
((resolve 'metabase.cmd.remove-encryption/remove-encryption!))
(log/info "Encryption removed OK.")
(system-exit! 0)
(catch Throwable e
(log/error e "ERROR REMOVING ENCRYPTION.")
(system-exit! 1)))) |
------------------------------------------------ Validate Commands ---------------------------------------------- | |
(defn- arg-list-count-ok? [arg-list arg-count]
(if (some #{'&} arg-list)
;; subtract 1 for the & and 1 for the symbol after &
;; e.g. [a b & c] => 2
(>= arg-count (- (count arg-list) 2))
(= arg-count (count arg-list)))) | |
(defn- arg-count-errors
[command-name args]
(let [arg-lists (-> command-name cmd->var meta :arglists)]
(when-not (some #(arg-list-count-ok? % (count args)) arg-lists)
(str "The '" command-name "' command requires "
(when (> 1 (count arg-lists)) "one of ")
"the following arguments: "
(str/join " | " (map pr-str arg-lists))
", but received: " (pr-str (vec args)) ".")))) | |
------------------------------------------------ Running Commands ------------------------------------------------ | |
Returns [error-message] if there is an error, otherwise [nil command-fn] | (defn- validate
[command-name args]
(let [varr (cmd->var command-name)
{:keys [command arg-spec]} (meta varr)
err (arg-count-errors command-name args)]
(cond
(not command)
[(str "Unrecognized command: '" command-name "'")
(str "Valid commands: " (str/join ", " (map key (filter (comp :command meta val) (ns-interns 'metabase.cmd)))))]
err
[err]
arg-spec
(:errors (cli/parse-opts args arg-spec))))) |
(defn- requires-init? [command-name] (-> command-name cmd->var meta :requires-init)) | |
(defn- fail!
[& messages]
(doseq [msg messages]
(println (u/format-color 'red msg)))
(flush)
(System/exit 1)) | |
Run | (defn run-cmd
[command-name init-fn args]
(if-let [errors (validate command-name args)]
(do
(when (cmd->var command-name)
(println "Usage:")
(help command-name))
(apply fail! errors))
(try
(when (requires-init? command-name)
(init-fn))
(apply @(cmd->var command-name) args)
(catch Throwable e
(when (:cmd/exit (ex-data e)) ;; fast-track for commands that have their own error handling
(flush)
(System/exit 1))
(.printStackTrace e)
(fail! (str "Command failed with exception: " (.getMessage e))))))
(flush)
(System/exit 0)) |