MINOR: config: add a new "default-path" global directive

By default haproxy loads all files designated by a relative path from the
location the process is started in. In some circumstances it might be
desirable to force all relative paths to start from a different location
just as if the process was started from such locations. This is what this
directive is made for. Technically it will perform a temporary chdir() to
the designated location while processing each configuration file, and will
return to the original directory after processing each file. It takes an
argument indicating the policy to use when loading files whose path does
not start with a slash ('/').

A few options are offered, "current" (the default), "config" (files
relative to config file's dir), "parent" (files relative to config file's
parent dir), and "origin" with an absolute path.

This should address issue #1198.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 0808433..b4dc1ad 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -894,6 +894,7 @@
    - crt-base
    - cpu-map
    - daemon
+   - default-path
    - description
    - deviceatlas-json-file
    - deviceatlas-log-level
@@ -1135,6 +1136,58 @@
   disabled by the command line "-db" argument. This option is ignored in
   systemd mode.
 
+default-path { current | config | parent | origin <path> }
+  By default haproxy loads all files designated by a relative path from the
+  location the process is started in. In some circumstances it might be
+  desirable to force all relative paths to start from a different location
+  just as if the process was started from such locations. This is what this
+  directive is made for. Technically it will perform a temporary chdir() to
+  the designated location while processing each configuration file, and will
+  return to the original directory after processing each file. It takes an
+  argument indicating the policy to use when loading files whose path does
+  not start with a slash ('/'):
+    - "current" indicates that all relative files are to be loaded from the
+      directory the process is started in ; this is the default.
+
+    - "config" indicates that all relative files should be loaded from the
+      directory containing the configuration file. More specifically, if the
+      configuration file contains a slash ('/'), the longest part up to the
+      last slash is used as the directory to change to, otherwise the current
+      directory is used. This mode is convenient to bundle maps, errorfiles,
+      certificates and Lua scripts together as relocatable packages. When
+      multiple configuration files are loaded, the directory is updated for
+      each of them.
+
+    - "parent" indicates that all relative files should be loaded from the
+      parent of the directory containing the configuration file. More
+      specifically, if the configuration file contains a slash ('/'), ".."
+      is appended to the longest part up to the last slash is used as the
+      directory to change to, otherwise the directory is "..". This mode is
+      convenient to bundle maps, errorfiles,  certificates and Lua scripts
+      together as relocatable packages, but where each part is located in a
+      different subdirectory (e.g. "config/", "certs/", "maps/", ...).
+
+    - "origin" indicates that all relative files should be loaded from the
+      designated (mandatory) path. This may be used to ease management of
+      different haproxy instances running in parallel on a system, where each
+      instance uses a different prefix but where the rest of the sections are
+      made easily relocatable.
+
+  Each "default-path" directive instantly replaces any previous one and will
+  possibly result in switching to a different directory. While this should
+  always result in the desired behavior, it is really not a good practice to
+  use multiple default-path directives, and if used, the policy ought to remain
+  consistent across all configuration files.
+
+  Warning: some configuration elements such as maps or certificates are
+  uniquely identified by their configured path. By using a relocatable layout,
+  it becomes possible for several of them to end up with the same unique name,
+  making it difficult to update them at run time, especially when multiple
+  configuration files are loaded from different directories. It is essential to
+  observe a strict collision-free file naming scheme before adopting relative
+  paths. A robust approach could consist in prefixing all files names with
+  their respective site name, or in doing so at the directory level.
+
 deviceatlas-json-file <path>
   Sets the path of the DeviceAtlas JSON data file to be loaded by the API.
   The path must be a valid JSON data file and accessible by HAProxy process.
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 48e35a1..59d0e91 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -99,6 +99,17 @@
 int cfg_maxconn = 0;			/* # of simultaneous connections, (-n) */
 char *cfg_scope = NULL;                 /* the current scope during the configuration parsing */
 
+/* how to handle default paths */
+static enum default_path_mode {
+	DEFAULT_PATH_CURRENT = 0,  /* "current": paths are relative to CWD (this is the default) */
+	DEFAULT_PATH_CONFIG,       /* "config": paths are relative to config file */
+	DEFAULT_PATH_PARENT,       /* "parent": paths are relative to config file's ".." */
+	DEFAULT_PATH_ORIGIN,       /* "origin": paths are relative to default_path_origin */
+} default_path_mode;
+
+static char initial_cwd[PATH_MAX];
+static char current_cwd[PATH_MAX];
+
 /* List head of all known configuration keywords */
 struct cfg_kw_list cfg_keywords = {
 	.list = LIST_HEAD_INIT(cfg_keywords.list)
@@ -1498,6 +1509,132 @@
 	}
 }
 
+/* apply the current default_path setting for config file <file>, and
+ * optionally replace the current path to <origin> if not NULL while the
+ * default-path mode is set to "origin". Errors are returned into an
+ * allocated string passed to <err> if it's not NULL. Returns 0 on failure
+ * or non-zero on success.
+ */
+static int cfg_apply_default_path(const char *file, const char *origin, char **err)
+{
+	const char *beg, *end;
+
+	/* make path start at <beg> and end before <end>, and switch it to ""
+	 * if no slash was passed.
+	 */
+	beg = file;
+	end = strrchr(beg, '/');
+	if (!end)
+		end = beg;
+
+	if (!*initial_cwd) {
+		if (getcwd(initial_cwd, sizeof(initial_cwd)) == NULL) {
+			if (err)
+				memprintf(err, "Impossible to retrieve startup directory name: %s", strerror(errno));
+			return 0;
+		}
+	}
+	else if (chdir(initial_cwd) == -1) {
+		if (err)
+			memprintf(err, "Impossible to get back to initial directory '%s': %s", initial_cwd, strerror(errno));
+		return 0;
+	}
+
+	/* OK now we're (back) to initial_cwd */
+
+	switch (default_path_mode) {
+	case DEFAULT_PATH_CURRENT:
+		/* current_cwd never set, nothing to do */
+		return 1;
+
+	case DEFAULT_PATH_ORIGIN:
+		/* current_cwd set in the config */
+		if (origin &&
+		    snprintf(current_cwd, sizeof(current_cwd), "%s", origin) > sizeof(current_cwd)) {
+			if (err)
+				memprintf(err, "Absolute path too long: '%s'", origin);
+			return 0;
+		}
+		break;
+
+	case DEFAULT_PATH_CONFIG:
+		if (end - beg >= sizeof(current_cwd)) {
+			if (err)
+				memprintf(err, "Config file path too long, cannot use for relative paths: '%s'", file);
+			return 0;
+		}
+		memcpy(current_cwd, beg, end - beg);
+		current_cwd[end - beg] = 0;
+		break;
+
+	case DEFAULT_PATH_PARENT:
+		if (end - beg + 3 >= sizeof(current_cwd)) {
+			if (err)
+				memprintf(err, "Config file path too long, cannot use for relative paths: '%s'", file);
+			return 0;
+		}
+		memcpy(current_cwd, beg, end - beg);
+		if (end > beg)
+			memcpy(current_cwd + (end - beg), "/..\0", 4);
+		else
+			memcpy(current_cwd + (end - beg), "..\0", 3);
+		break;
+	}
+
+	if (*current_cwd && chdir(current_cwd) == -1) {
+		if (err)
+			memprintf(err, "Impossible to get back to directory '%s': %s", initial_cwd, strerror(errno));
+		return 0;
+	}
+
+	return 1;
+}
+
+/* parses a global "default-path" directive. */
+static int cfg_parse_global_def_path(char **args, int section_type, struct proxy *curpx,
+                                     const struct proxy *defpx, const char *file, int line,
+                                     char **err)
+{
+	int ret = -1;
+
+	/* "current", "config", "parent", "origin <path>" */
+
+	if (strcmp(args[1], "current") == 0)
+		default_path_mode = DEFAULT_PATH_CURRENT;
+	else if (strcmp(args[1], "config") == 0)
+		default_path_mode = DEFAULT_PATH_CONFIG;
+	else if (strcmp(args[1], "parent") == 0)
+		default_path_mode = DEFAULT_PATH_PARENT;
+	else if (strcmp(args[1], "origin") == 0)
+		default_path_mode = DEFAULT_PATH_ORIGIN;
+	else {
+		memprintf(err, "%s default-path mode '%s' for '%s', supported modes include 'current', 'config', 'parent', and 'origin'.", *args[1] ? "unsupported" : "missing", args[1], args[0]);
+		goto end;
+	}
+
+	if (default_path_mode == DEFAULT_PATH_ORIGIN) {
+		if (!*args[2]) {
+			memprintf(err, "'%s %s' expects a directory as an argument.", args[0], args[1]);
+			goto end;
+		}
+		if (!cfg_apply_default_path(file, args[2], err)) {
+			memprintf(err, "couldn't set '%s' to origin '%s': %s.", args[0], args[2], *err);
+			goto end;
+		}
+	}
+	else if (!cfg_apply_default_path(file, NULL, err)) {
+		memprintf(err, "couldn't set '%s' to '%s': %s.", args[0], args[1], *err);
+		goto end;
+	}
+
+	/* note that once applied, the path is immediately updated */
+
+	ret = 0;
+ end:
+	return ret;
+}
+
+
 /*
  * This function reads and parses the configuration file given in the argument.
  * Returns the error code, 0 if OK, -1 if the config file couldn't be opened,
@@ -1527,6 +1664,7 @@
 	int nested_cond_lvl = 0;
 	enum nested_cond_state nested_conds[MAXNESTEDCONDS];
 	int non_global_section_parsed = 0;
+	char *errmsg = NULL;
 
 	if ((thisline = malloc(sizeof(*thisline) * linesize)) == NULL) {
 		ha_alert("Out of memory trying to allocate a buffer for a configuration line.\n");
@@ -1539,6 +1677,14 @@
 		goto err;
 	}
 
+	/* change to the new dir if required */
+	if (!cfg_apply_default_path(file, NULL, &errmsg)) {
+		ha_alert("parsing [%s:%d]: failed to apply default-path: %s.\n", file, linenum, errmsg);
+		free(errmsg);
+		err_code = -1;
+		goto err;
+	}
+
 next_line:
 	while (fgets(thisline + readbytes, linesize - readbytes, f) != NULL) {
 		int arg, kwm = KWM_STD;
@@ -1919,6 +2065,12 @@
 		ha_alert("parsing [%s:%d]: non-terminated '.if' block.\n", file, linenum);
 		err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
 	}
+
+	if (*initial_cwd && chdir(initial_cwd) == -1) {
+		ha_alert("Impossible to get back to initial directory '%s' : %s\n", initial_cwd, strerror(errno));
+		err_code |= ERR_ALERT | ERR_FATAL;
+	}
+
 err:
 	ha_free(&cfg_scope);
 	cursection = NULL;
@@ -4069,6 +4221,13 @@
 REGISTER_CONFIG_SECTION("mailers",        cfg_parse_mailers,   NULL);
 REGISTER_CONFIG_SECTION("namespace_list", cfg_parse_netns,     NULL);
 
+static struct cfg_kw_list cfg_kws = {{ },{
+	{ CFG_GLOBAL, "default-path",     cfg_parse_global_def_path },
+	{ /* END */ }
+}};
+
+INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws);
+
 /*
  * Local variables:
  *  c-indent-level: 8