Support for initializing Metabase with configuration from a This logic is meant to be executed after the application database is set up and driver plugins have been initialized. The config file itself is a YAML file containing a map where each key corresponds to a different init section. For example, it might look something like this: version: 1 config: users: - first_name: Cam last_name: Saul password: 2cans email: cam@example.com - first_name: Cam last_name: Era password: 2cans email: cam.era@example.com databases: - type: postgres host: localhost port: 5432 name: test-data password: {{ env MYPOSTGRESPASSWORD }} settings: my-setting: 1234 Each section is handled by its corresponding [[initialize-section!]] method; the shape of each section may vary. VERSIONINGConfig files are required to have a These are not semantic versions! They're just simple floating point version numbers. That should be enough for our purposes. The idea here is that if we want to make changes to the config file shape in the future we'll be able to do so without having older Metabase code suddenly break in mysterious ways because it doesn't understand the new config shape, or newer Metabase code breaking if you try to use a config file using the older shape. For the time being, the minimum version we'll support is 1.0, which is the initial version of the config spec that
we're shipping with Metabase 45. We'll support all the way up to For example in Metabase 46 if we want to add some extra required keys that Metabase 45 can safely ignore, we can
define a new version 1.1 of the spec and specify Metabase 46 works with config versions If we want to introduce a breaking change that should not be backwards-compatible, such as introducing a new
template type, we can increment the major version to Spec validationThe contents of each section are automatically validated against the [[section-spec]] for that section. This validation is done before template expansion to avoid leaking sensitive values in the error messages that get logged. TemplatesAfter spec validation, the config map is walked and A template form like ``` {{env BIRDTYPE}} => (expand-parsed-template-form '(env BIRDTYPE)) => "toucan" ``` At the time of this writing, `env````yaml {{env MYENVVAR}} ``` Replaces the template with the value of an environment variable. The template consisting of two parts: the word
```yaml Java system property user.dir{{env user-dir}} ``` | (ns ^{:added "0.45.0"} metabase-enterprise.advanced-config.file (:require [clojure.edn :as edn] [clojure.spec.alpha :as s] [clojure.string :as str] [clojure.walk :as walk] [environ.core :as env] [metabase-enterprise.advanced-config.file.databases] [metabase-enterprise.advanced-config.file.interface :as advanced-config.file.i] [metabase-enterprise.advanced-config.file.settings] [metabase-enterprise.advanced-config.file.users] [metabase.driver.common.parameters] [metabase.driver.common.parameters.parse :as params.parse] [metabase.public-settings.premium-features :as premium-features] [metabase.util :as u] [metabase.util.files :as u.files] [metabase.util.i18n :refer [trs tru]] [metabase.util.log :as log] [metabase.util.yaml :as yaml])) |
(comment ;; for parameter parsing metabase.driver.common.parameters/keep-me ;; for `settings:` section code metabase-enterprise.advanced-config.file.settings/keep-me ;; for `databases:` section code metabase-enterprise.advanced-config.file.databases/keep-me ;; for `users:` section code metabase-enterprise.advanced-config.file.users/keep-me) | |
(set! *warn-on-reflection* true) | |
(s/def :metabase.config.file.config/config (s/and map? (fn validate-section-configs [m] (doseq [[section-name section-config] m :let [spec (advanced-config.file.i/section-spec section-name)]] (s/assert* spec section-config)) true))) | |
Range of config file versions (inclusive) that we'll support. If the version is out of this range, spec validation will fail and trigger an error. See ns documentation for [[metabase.config.file]] for more details. | (def ^:private ^:dynamic *supported-versions* {:min 1.0, :max 1.999}) |
(defn- supported-version? [n] (<= (:min *supported-versions*) n (:max *supported-versions*))) | |
(s/def :metabase.config.file.config/version (s/and number? supported-version?)) | |
(s/def ::config (s/keys :req-un [:metabase.config.file.config/version :metabase.config.file.config/config])) | |
Environment variables and system properties used in this namespace. This is a dynamic version of [[environ.core/env]]; it is dynamic for test mocking purposes. Yes, [[metabase.test/with-temp-env-var-value!]] exists, but it is not allowed inside parallel tests. This is an experiment that I may adapt into a new pattern in the future to allow further test parallelization. | (def ^:private ^:dynamic *env* env/env) |
Path for the YAML config file Metabase should use for initialization and Settings values. | (defn- path ^java.nio.file.Path [] (let [path* (or (some-> (get *env* :mb-config-file-path) u.files/get-path) (u.files/get-path (System/getProperty "user.dir") "config.yml"))] (if (u.files/exists? path*) (log/info (u/format-color :magenta "Found config file at path %s; Metabase will be initialized with values from this file" (pr-str (str path*))) (u/emoji "🗄️")) (log/info (u/format-color :yellow "No config file found at path %s" (pr-str (str path*))))) path*)) |
Override the config contents as returned by [[config]], for test mocking purposes. | (def ^:private ^:dynamic *config* nil) |
(defmulti ^:private expand-parsed-template-form {:arglists '([form])} (fn [form] (symbol (first form)))) | |
(defmethod expand-parsed-template-form :default [form] (throw (ex-info (trs "Don''t know how to expand template form: {0}" (pr-str form)) {:form form}))) | |
(defmethod expand-parsed-template-form 'env [[_template-type env-var-name]] (get *env* (keyword (u/->kebab-case-en env-var-name)))) | |
(defmulti ^:private expand-template-str-part {:arglists '([part])} type) | |
(defmethod expand-template-str-part String [s] s) | |
(defn- valid-template-type? [symb] (and (symbol? symb) (get (methods expand-parsed-template-form) symb))) | |
(s/def ::template-form (s/or :env (s/cat :template-type (s/and symbol? valid-template-type?) :env-var-name symbol?))) | |
(defmethod expand-template-str-part metabase.driver.common.parameters.Param [{s :k}] {:pre [(string? s)]} (when (seq s) (when-let [obj (try (not-empty (edn/read-string (str "( " s " )"))) (catch Throwable e (throw (ex-info (trs "Error parsing template string {0}: {1}" (pr-str s) (ex-message e)) {:template-string s}))))] (s/assert* ::template-form obj) (expand-parsed-template-form obj)))) | |
(defmethod expand-template-str-part metabase.driver.common.parameters.Optional [{:keys [args]}] (let [parts (map expand-template-str-part args)] (when (every? seq parts) (str/join parts)))) | |
(defn- expand-templates-in-str [s] (if-let [[_, raw-string] (re-matches #"\{\{\{(.+)\}\}\}" s)] (str/trim raw-string) (str/join (map expand-template-str-part (params.parse/parse s))))) | |
(defn- expand-templates [m] (walk/postwalk (fn [form] (cond-> form (string? form) expand-templates-in-str)) m)) | |
Contents of the config file if it exists, otherwise | (defn- config [] (when-let [m (or *config* (yaml/from-file (str (path))))] (s/assert* ::config m) (expand-templates m))) |
Sort the various config sections. The | (defn- sort-by-initialization-order [config-sections] (let [{settings-sections true, other-sections false} (group-by (fn [[section-name]] (= section-name :settings)) config-sections)] (concat settings-sections other-sections))) |
Initialize Metabase according to the directives in the config file, if it exists. | (defn ^{:added "0.45.0"} initialize! [] ;; TODO -- this should only do anything if we have an appropriate token (we should get a token for testing this before ;; enabling that check tho) (when-let [m (config)] (doseq [[section-name section-config] (sort-by-initialization-order (:config m))] ;; you can only use the config-from-file stuff with an EE/Pro token with the `:config-text-file` feature. Since you ;; might have to use the `:settings` section to set the token, skip the check for Settings. But check it for the ;; other sections. (when-not (= section-name :settings) (when-not (premium-features/enable-config-text-file?) (throw (ex-info (tru "Metabase config files require a Premium token with the :config-text-file feature.") {})))) (log/info (u/format-color :magenta "Initializing %s from config file..." section-name) (u/emoji "🗄️")) (advanced-config.file.i/initialize-section! section-name section-config)) (log/info (u/colorize :magenta "Done initializing from file.") (u/emoji "🗄️"))) :ok) |