(ns metabase.plugins (:require [clojure.core.memoize :as memoize] [clojure.java.classpath :as classpath] [clojure.java.io :as io] [clojure.string :as str] [environ.core :as env] [metabase.config :as config] [metabase.plugins.classloader :as classloader] [metabase.plugins.initialize :as plugins.init] [metabase.util.files :as u.files] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log] [metabase.util.yaml :as yaml]) (:import (java.io File) (java.nio.file Files Path))) | |
(set! *warn-on-reflection* true) | |
(defn- plugins-dir-filename ^String []
(or (env/env :mb-plugins-dir)
(.getAbsolutePath (io/file "plugins")))) | |
(def ^:private plugins-dir*
;; Memoized so we don't log the error messages multiple times if the plugins directory doesn't change
(memoize/memo
(fn [filename]
(try
;; attempt to create <current-dir>/plugins if it doesn't already exist. Check that the directory is readable.
(let [path (u.files/get-path filename)]
(u.files/create-dir-if-not-exists! path)
(assert (Files/isWritable path)
(trs "Metabase does not have permissions to write to plugins directory {0}" filename))
{:path path, :temp false})
;; If we couldn't create the directory, or the directory is not writable, fall back to a temporary directory
;; rather than failing to launch entirely. Log instructions for what should be done to fix the problem.
(catch Throwable e
(log/warn
e
(format "Metabase cannot use the plugins directory %s" filename)
"\n"
"Please make sure the directory exists and that Metabase has permission to write to it."
"You can change the directory Metabase uses for modules by setting the environment variable MB_PLUGINS_DIR."
"Falling back to a temporary directory for now.")
;; Check whether the fallback temporary directory is writable. If it's not, there's no way for us to
;; gracefully proceed here. Throw an Exception detailing the critical issues.
(let [path (u.files/get-path (System/getProperty "java.io.tmpdir"))]
(assert (Files/isWritable path)
(trs "Metabase cannot write to temporary directory. Please set MB_PLUGINS_DIR to a writable directory and restart Metabase."))
{:path path, :temp true})))))) | |
Map with a :path key containing the | (defn plugins-dir-info ^Path [] (plugins-dir* (plugins-dir-filename))) |
Get a This is a wrapper around | (defn plugins-dir [] (:path (plugins-dir-info))) |
(defn- extract-system-modules! []
(when (io/resource "modules")
(let [plugins-path (plugins-dir)]
(u.files/with-open-path-to-resource [modules-path "modules"]
(u.files/copy-files! modules-path plugins-path))))) | |
+----------------------------------------------------------------------------------------------------------------+ | loading/initializing plugins | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- add-to-classpath! [^Path jar-path] (classloader/add-url-to-classpath! (-> jar-path .toUri .toURL))) | |
(defn- plugin-info [^Path jar-path]
(some-> (u.files/slurp-file-from-archive jar-path "metabase-plugin.yaml")
yaml/parse-string)) | |
Initiaize plugin using parsed info from a plugin maifest. Returns truthy if plugin was successfully initialized; falsey otherwise. | (defn- init-plugin-with-info! [info] (plugins.init/init-plugin-with-info! info)) |
Init plugin JAR file; returns truthy if plugin initialization was successful. | (defn- init-plugin!
[^Path jar-path]
(if-let [info (plugin-info jar-path)]
;; for plugins that include a metabase-plugin.yaml manifest run the normal init steps, don't add to classpath yet
(init-plugin-with-info! (assoc info :add-to-classpath! #(add-to-classpath! jar-path)))
;; for all other JARs just add to classpath and call it a day
(add-to-classpath! jar-path))) |
+----------------------------------------------------------------------------------------------------------------+ | load-plugins! | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- plugins-paths []
(for [^Path path (u.files/files-seq (plugins-dir))
:when (and (u.files/regular-file? path)
(u.files/readable? path)
(str/ends-with? (.getFileName path) ".jar")
(or (not (str/ends-with? (.getFileName path) "spark-deps.jar"))
;; if the JAR in question is the spark deps JAR we cannot load it because it's signed, and
;; the Metabase JAR itself as well as plugins no longer are; Java will throw an Exception
;; if different JARs with `metabase` packages have different signing keys. Go ahead and
;; ignore it but let people know they can get rid of it.
(log/warn
"spark-deps.jar is no longer needed by Metabase 0.32.0+. You can delete it from the plugins directory.")))]
path)) | |
Return a sequence of [[java.io.File]] paths for | (when (or config/is-dev? config/is-test?)
(defn- load-local-plugin-manifest! [^Path path]
(some-> (slurp (str path)) yaml/parse-string plugins.init/init-plugin-with-info!))
(defn- driver-manifest-paths
[]
;; only include plugin manifests if they're on the system classpath.
(concat
(for [^File file (classpath/system-classpath)
:when (and (.isDirectory file)
(not (.isHidden file))
(str/includes? (str file) "modules/drivers")
(or (str/ends-with? (str file) "resources")
(str/ends-with? (str file) "resources-ee")))
:let [manifest-file (io/file file "metabase-plugin.yaml")]
:when (.exists manifest-file)]
manifest-file)
;; for hacking on 3rd-party drivers locally: set
;; `-Dmb.dev.additional.driver.manifest.paths=/path/to/whatever/metabase-plugin.yaml` or
;; `MB_DEV_ADDITIONAL_DRIVER_MANIFEST_PATHS=...` to have that plugin manifest get loaded during startup. Specify
;; multiple plugin manifests by comma-separating them.
(when-let [additional-paths (env/env :mb-dev-additional-driver-manifest-paths)]
(map u.files/get-path (str/split additional-paths #",")))))
(defn- load-local-plugin-manifests!
[]
;; TODO - this should probably do an actual search in case we ever add any additional directories
(doseq [manifest-path (driver-manifest-paths)]
(log/infof "Loading local plugin manifest at %s" (str manifest-path))
(load-local-plugin-manifest! manifest-path)))) |
(defn- has-manifest? ^Boolean [^Path path] (boolean (u.files/file-exists-in-archive? path "metabase-plugin.yaml"))) | |
(defn- init-plugins! [paths]
;; sort paths so that ones that correspond to JARs with no plugin manifest (e.g. a dependency like the Oracle JDBC
;; driver `ojdbc8.jar`) always get initialized (i.e., added to the classpath) first; that way, Metabase drivers that
;; depend on them (such as Oracle) can be initialized the first time we see them.
;;
;; In Clojure world at least `false` < `true` so we can use `sort-by` to get non-Metabase-plugin JARs in front
(doseq [^Path path (sort-by has-manifest? paths)]
(try
(init-plugin! path)
(catch Throwable e
(log/errorf e "Failied to initialize plugin %s" (.getFileName path)))))) | |
(defn- load! []
(log/infof "Loading plugins in %s..." (str (plugins-dir)))
(extract-system-modules!)
(let [paths (plugins-paths)]
(init-plugins! paths))
(when (or config/is-dev? config/is-test?)
(load-local-plugin-manifests!))) | |
(defonce ^:private loaded? (atom false)) | |
Load Metabase plugins. The are JARs shipped as part of Metabase itself, under the When loading plugins, Metabase performs the following steps:
This function will only perform loading steps the first time it is called — it is safe to call this function more than once. | (defn load-plugins!
[]
(when-not @loaded?
(locking loaded?
(when-not @loaded?
(load!)
(reset! loaded? true))))) |