(ns metabase.plugins.dependencies (:require [clojure.string :as str] [environ.core :as env] [metabase.plugins.classloader :as classloader] [metabase.util :as u] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log])) | |
(set! *warn-on-reflection* true) | |
(def ^:private plugins-with-unsatisfied-deps (atom #{})) | |
(defn- dependency-type [{classname :class, plugin :plugin, env-var :env-var}] (cond classname :class plugin :plugin env-var :env-var :else :unknown)) | |
(defmulti ^:private dependency-satisfied? {:arglists '([initialized-plugin-names info dependency])} (fn [_ _ dep] (dependency-type dep))) | |
(defmethod dependency-satisfied? :default [_ {{plugin-name :name} :info} dep] (log/error (u/format-color :red "Plugin %s declares a dependency that Metabase does not understand: %s" plugin-name dep) "Refer to the plugin manifest reference for a complete list of valid plugin dependencies:" "https://github.com/metabase/metabase/wiki/Metabase-Plugin-Manifest-Reference") false) | |
(defonce ^:private already-logged (atom #{})) | |
Log a message a single time, such as warning that a plugin cannot be initialized because of required dependencies. Subsequent calls with duplicate messages are automatically ignored. | (defn log-once ([message] (log-once nil message)) ([plugin-name-or-nil message] (let [k [plugin-name-or-nil message]] (when-not (contains? @already-logged k) (swap! already-logged conj k) (log/info message))))) |
(defn- warn-about-required-dependencies [plugin-name message] (log-once plugin-name (str (u/format-color 'red (trs "Metabase cannot initialize plugin {0} due to required dependencies." plugin-name)) " " message))) | |
(defmethod dependency-satisfied? :class [_ {{plugin-name :name} :info} {^String classname :class, message :message, :as _dep}] (try (Class/forName classname false (classloader/the-classloader)) (catch ClassNotFoundException _ (warn-about-required-dependencies plugin-name (or message (trs "Class not found: {0}" classname))) false))) | |
(defmethod dependency-satisfied? :plugin [initialized-plugin-names {{plugin-name :name} :info} {dep-plugin-name :plugin}] (log-once plugin-name (trs "Plugin ''{0}'' depends on plugin ''{1}''" plugin-name dep-plugin-name)) ((set initialized-plugin-names) dep-plugin-name)) | |
(defmethod dependency-satisfied? :env-var [_ {{plugin-name :name} :info} {env-var-name :env-var}] (if (str/blank? (env/env (keyword env-var-name))) (do (log-once plugin-name (trs "Plugin ''{0}'' depends on environment variable ''{1}'' being set to something" plugin-name env-var-name)) false) true)) | |
(defn- all-dependencies-satisfied?* [initialized-plugin-names {:keys [dependencies], {plugin-name :name} :info, :as info}] (let [dep-satisfied? (fn [dep] (u/prog1 (dependency-satisfied? initialized-plugin-names info dep) (log-once plugin-name (trs "{0} dependency {1} satisfied? {2}" plugin-name (dissoc dep :message) (boolean <>)))))] (every? dep-satisfied? dependencies))) | |
Check whether all dependencies are satisfied for a plugin; return truthy if all are; otherwise log explanations about why they are not, and return falsey. For plugins that might have their dependencies satisfied in the near future | (defn all-dependencies-satisfied? [initialized-plugin-names info] (or (all-dependencies-satisfied?* initialized-plugin-names info) (do (swap! plugins-with-unsatisfied-deps conj info) (log-once (u/format-color 'yellow (trs "Plugins with unsatisfied deps: {0}" (mapv (comp :name :info) @plugins-with-unsatisfied-deps)))) false))) |
(defn- remove-plugins-with-satisfied-deps [plugins initialized-plugin-names ready-for-init-atom] ;; since `remove-plugins-with-satisfied-deps` could theoretically be called multiple times we need to reset the atom ;; used to return the plugins ready for init so we don't accidentally include something in there twice etc. (reset! ready-for-init-atom nil) (set (for [info plugins :let [ready? (when (all-dependencies-satisfied?* initialized-plugin-names info) (swap! ready-for-init-atom conj info))] :when (not ready?)] info))) | |
Updates internal list of plugins that still have unmet dependencies; returns sequence of plugin infos for all plugins that are now ready for initialization. | (defn update-unsatisfied-deps! [initialized-plugin-names] (let [ready-for-init (atom nil)] (swap! plugins-with-unsatisfied-deps remove-plugins-with-satisfied-deps initialized-plugin-names ready-for-init) @ready-for-init)) |