1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2025-01-18 04:58:51 +02:00

Allow any option to be set in an environment variable.

This includes options that previously could only be specified on the command line, e.g. stanza.
This commit is contained in:
David Steele 2018-08-15 10:52:53 -04:00
parent 4a822d3032
commit d0b9f986a0
5 changed files with 200 additions and 6 deletions

View File

@ -814,7 +814,11 @@
Non-boolean options configured in <file>pgbackrest.conf</file> can be reset to default on the command-line by using the <id>reset-</id> prefix. This feature may be used to restore a backup directly on a repository host. Normally, <backrest/> will error because it can see that the database host is remote and restores cannot be done remotely. By adding <br-option>--reset-pg1-host</br-option> on the command-line, <backrest/> will ignore the remote database host and restore locally. It may be necessary to pass a new <br-option>--pg-path</br-option> to force the restore to happen in a specific path, i.e. not the path used on the database host.
The <id>no-</id> prefix may be used to set a boolean option to false on the command-line.</text>
The <id>no-</id> prefix may be used to set a boolean option to false on the command-line.
Any option may be set in an environment variable using the <id>PGBACKREST_</id> prefix and the option name in all caps replacing <id>-</id> with <id>_</id>, e.g. <br-option>pg1-path</br-option> becomes <id>PGBACKREST_PG1_PATH</id> and <br-option>stanza</br-option> becomes <id>PGBACKREST_STANZA</id>. Boolean options are represented as they would be in a configuration file, e.g. <id>PGBACKREST_COMPRESS="n"</id>, and <id>reset-*</id> variants are not allowed. Options that that can be specified multiple times on the command-line or in a config file can be represented by separating the values with colons, e.g. <id>PGBACKREST_DB_INCLUDE="db1:db2"</id>.
Command-line options override environment options which override config file options.</text>
<operation-general title="General Options">
<option-list>

View File

@ -40,6 +40,10 @@
<p>Exclude temporary and unlogged relation (table/index) files from backup. Implemented using the same logic as the patches adding this feature to <postgres/>, <link url="https://git.postgresql.org/pg/commitdiff/8694cc96b52a967a49725f32be7aa77fd3b6ac25">8694cc96</link> and <link url="https://git.postgresql.org/pg/commitdiff/920a5e500a119b03356fb1fb64a677eb1aa5fc6f">920a5e50</link>. Temporary relation exclusion is enabled in <postgres/> &amp;ge; <id>9.0</id>. Unlogged relation exclusion is enabled in <postgres/> &amp;ge; <id>9.1</id>, where the feature was introduced.</p>
</release-item>
<release-item>
<p>Allow any option to be set in an environment variable. This includes options that previously could only be specified on the command line, e.g. <br-option>stanza</br-option>.</p>
</release-item>
</release-feature-list>
<release-improvement-list>

View File

@ -673,6 +673,19 @@
</list>
<p><b>Note:</b> <br-option>--config</br-option>, <br-option>--config-include-path</br-option> and <br-option>--config-path</br-option> are command-line only options.</p>
<p><backrest/> can also be configured using environment variables as described in the <link url="command.html">command reference</link>.</p>
<execute-list host="{[host-pg1]}">
<title>Configure <br-option>log-path</br-option> using the environment</title>
<execute user="postgres" output="y" filter="n">
<exe-cmd>bash -c '
export PGBACKREST_LOG_PATH=/path/set/by/env &amp;&amp;
{[project-exe]} --log-level-console=error help backup log-path'</exe-cmd>
<exe-highlight>current\: \/path\/set\/by\/env</exe-highlight>
</execute>
</execute-list>
</section>
<!-- SECTION => QUICKSTART - CREATE REPOSITORY -->

View File

@ -4,6 +4,7 @@ Command and Option Parse
#include <getopt.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include "common/assert.h"
#include "common/debug.h"
@ -22,6 +23,15 @@ Standard config file name and old default path and name
#define PGBACKREST_CONFIG_FILE PGBACKREST_BIN ".conf"
#define PGBACKREST_CONFIG_ORIG_PATH_FILE "/etc/" PGBACKREST_CONFIG_FILE
/***********************************************************************************************************************************
Prefix for environment variables
***********************************************************************************************************************************/
#define PGBACKREST_ENV "PGBACKREST_"
#define PGBACKREST_ENV_SIZE (sizeof(PGBACKREST_ENV) - 1)
// In some environments this will not be extern'd
extern char **environ;
/***********************************************************************************************************************************
Standard config include path name
***********************************************************************************************************************************/
@ -516,15 +526,100 @@ configParse(unsigned int argListSize, const char *argList[], bool resetLogLevel)
if (cfgCommand() != cfgCmdLocal && cfgCommand() != cfgCmdRemote && resetLogLevel)
logInit(logLevelWarn, logLevelWarn, logLevelOff, false);
// Phase 2: parse config file unless --no-config passed
// ---------------------------------------------------------------------------------------------------------------------
// Only continue if command options need to be validated, i.e. a real command is running or we are getting help for a
// specific command and would like to display actual option values in the help.
if (cfgCommand() != cfgCmdNone &&
cfgCommand() != cfgCmdVersion &&
cfgCommand() != cfgCmdHelp)
{
// Get the command definition id
// Phase 2: parse environment variables
// ---------------------------------------------------------------------------------------------------------------------
ConfigDefineCommand commandDefId = cfgCommandDefIdFromId(cfgCommand());
unsigned int environIdx = 0;
// Loop through all environment variables and look for our env vars by matching the prefix
while (environ[environIdx] != NULL)
{
const char *keyValue = environ[environIdx];
environIdx++;
if (strstr(keyValue, PGBACKREST_ENV) == keyValue)
{
// Find the first = char
const char *equalPtr = strchr(keyValue, '=');
ASSERT_DEBUG(equalPtr != NULL);
// Get key and value
String *key = strReplaceChr(
strLower(strNewN(keyValue + PGBACKREST_ENV_SIZE, (size_t)(equalPtr - (keyValue + PGBACKREST_ENV_SIZE)))),
'_', '-');
String *value = strNew(equalPtr + 1);
// Find the option
unsigned int optionIdx = optionFind(key);
// Warn if the option not found
if (optionList[optionIdx].name == NULL)
{
LOG_WARN("environment contains invalid option '%s'", strPtr(key));
continue;
}
// Warn if negate option found in env
else if (optionList[optionIdx].val & PARSE_NEGATE_FLAG)
{
LOG_WARN("environment contains invalid negate option '%s'", strPtr(key));
continue;
}
// Warn if reset option found in env
else if (optionList[optionIdx].val & PARSE_RESET_FLAG)
{
LOG_WARN("environment contains invalid reset option '%s'", strPtr(key));
continue;
}
ConfigOption optionId = optionList[optionIdx].val & PARSE_OPTION_MASK;
ConfigDefineOption optionDefId = cfgOptionDefIdFromId(optionId);
// Continue if the option is not valid for this command
if (!cfgDefOptionValid(commandDefId, optionDefId))
continue;
if (strSize(value) == 0)
THROW_FMT(OptionInvalidValueError, "environment variable '%s' must have a value", strPtr(key));
// Continue if the option has already been specified on the command line
if (parseOptionList[optionId].found)
continue;
parseOptionList[optionId].found = true;
parseOptionList[optionId].source = cfgSourceParam;
// Convert boolean to string
if (cfgDefOptionType(optionDefId) == cfgDefOptTypeBoolean)
{
if (strEqZ(value, "n"))
parseOptionList[optionId].negate = true;
else if (!strEqZ(value, "y"))
THROW_FMT(OptionInvalidValueError, "environment boolean option '%s' must be 'y' or 'n'", strPtr(key));
}
// Else split list/hash into separate values
else if (cfgDefOptionType(optionDefId) == cfgDefOptTypeHash ||
cfgDefOptionType(optionDefId) == cfgDefOptTypeList)
{
parseOptionList[optionId].valueList = strLstNewSplitZ(value, ":");
}
// Else add the string value
else
{
parseOptionList[optionId].valueList = strLstNew();
strLstAdd(parseOptionList[optionId].valueList, value);
}
}
}
// Phase 3: parse config file unless --no-config passed
// ---------------------------------------------------------------------------------------------------------------------
// Load the configuration file(s)
String *configString = cfgFileLoad(parseOptionList,
strNew(cfgDefOptionDefault(commandDefId, cfgOptionDefIdFromId(cfgOptConfig))),
@ -672,7 +767,7 @@ configParse(unsigned int argListSize, const char *argList[], bool resetLogLevel)
}
}
// Phase 3: validate option definitions and load into configuration
// Phase 4: validate option definitions and load into configuration
// ---------------------------------------------------------------------------------------------------------------------
for (unsigned int optionOrderIdx = 0; optionOrderIdx < CFG_OPTION_TOTAL; optionOrderIdx++)
{

View File

@ -697,6 +697,7 @@ testRun(void)
TEST_RESULT_VOID(configParse(strLstSize(argList), strLstPtr(argList), false), "load remote config");
TEST_RESULT_INT(logLevelStdOut, logLevelError, "console logging is error");
TEST_RESULT_INT(logLevelStdErr, logLevelError, "stderr logging is error");
harnessLogLevelReset();
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
@ -737,11 +738,12 @@ testRun(void)
strLstAdd(argList, strNew("--pg1-path=/path/to/db"));
strLstAdd(argList, strNew("--no-config"));
strLstAdd(argList, strNew("--stanza=db"));
strLstAdd(argList, strNew("--repo1-s3-host=xxx"));
setenv("PGBACKREST_REPO1_S3_HOST", "xxx", true);
strLstAdd(argList, strNew(TEST_COMMAND_BACKUP));
TEST_ERROR(
configParse(strLstSize(argList), strLstPtr(argList), false), OptionInvalidError,
"option 'repo1-s3-host' not valid without option 'repo1-type' = 's3'");
unsetenv("PGBACKREST_REPO1_S3_HOST");
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
@ -864,6 +866,32 @@ testRun(void)
configParse(strLstSize(argList), strLstPtr(argList), false), OptionInvalidValueError,
"'bogus' is not valid for 'protocol-timeout' option");
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
strLstAdd(argList, strNew(TEST_BACKREST_EXE));
strLstAdd(argList, strNew("--pg1-path=/path/to/db"));
strLstAdd(argList, strNew("--stanza=db"));
setenv("PGBACKREST_PROTOCOL_TIMEOUT", "", true);
strLstAdd(argList, strNew(TEST_COMMAND_RESTORE));
TEST_ERROR(
configParse(strLstSize(argList), strLstPtr(argList), false), OptionInvalidValueError,
"environment variable 'protocol-timeout' must have a value");
unsetenv("PGBACKREST_PROTOCOL_TIMEOUT");
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
strLstAdd(argList, strNew(TEST_BACKREST_EXE));
strLstAdd(argList, strNew("--pg1-path=/path/to/db"));
strLstAdd(argList, strNew("--stanza=db"));
setenv("PGBACKREST_DELTA", "x", true);
strLstAdd(argList, strNew(TEST_COMMAND_RESTORE));
TEST_ERROR(
configParse(strLstSize(argList), strLstPtr(argList), false), OptionInvalidValueError,
"environment boolean option 'delta' must be 'y' or 'n'");
unsetenv("PGBACKREST_DELTA");
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
strLstAdd(argList, strNew(TEST_BACKREST_EXE));
@ -1026,6 +1054,15 @@ testRun(void)
strLstAdd(argList, strNew("--reset-backup-standby"));
strLstAdd(argList, strNew(TEST_COMMAND_BACKUP));
setenv("PGBACKRESTXXX_NOTHING", "xxx", true);
setenv("PGBACKREST_BOGUS", "xxx", true);
setenv("PGBACKREST_NO_COMPRESS", "xxx", true);
setenv("PGBACKREST_RESET_REPO1_HOST", "", true);
setenv("PGBACKREST_TARGET", "xxx", true);
setenv("PGBACKREST_ONLINE", "y", true);
setenv("PGBACKREST_START_FAST", "n", true);
setenv("PGBACKREST_PG1_SOCKET_PATH", "/path/to/socket", true);
storagePutNP(storageNewWriteNP(storageLocalWrite(), configFile), bufNewStr(strNew(
"[global]\n"
"compress-level=3\n"
@ -1037,6 +1074,7 @@ testRun(void)
"no-compress=y\n"
"reset-compress=y\n"
"archive-copy=y\n"
"start-fast=y\n"
"online=y\n"
"pg1-path=/not/path/to/db\n"
"backup-standby=y\n"
@ -1056,6 +1094,9 @@ testRun(void)
harnessLogResult(
strPtr(
strNew(
"P00 WARN: environment contains invalid option 'bogus'\n"
"P00 WARN: environment contains invalid negate option 'no-compress'\n"
"P00 WARN: environment contains invalid reset option 'reset-repo1-host'\n"
"P00 WARN: configuration file contains option 'recovery-option' invalid for section 'db:backup'\n"
"P00 WARN: configuration file contains invalid option 'bogus'\n"
"P00 WARN: configuration file contains negate option 'no-compress'\n"
@ -1066,6 +1107,12 @@ testRun(void)
TEST_RESULT_BOOL(cfgOptionTest(cfgOptPgHost), false, " pg1-host is not set (command line reset override)");
TEST_RESULT_STR(strPtr(cfgOptionStr(cfgOptPgPath)), "/path/to/db", " pg1-path is set");
TEST_RESULT_INT(cfgOptionSource(cfgOptPgPath), cfgSourceConfig, " pg1-path is source config");
TEST_RESULT_STR(strPtr(cfgOptionStr(cfgOptPgSocketPath)), "/path/to/socket", " pg1-socket-path is set");
TEST_RESULT_INT(cfgOptionSource(cfgOptPgSocketPath), cfgSourceParam, " pg1-socket-path is source param");
TEST_RESULT_BOOL(cfgOptionBool(cfgOptOnline), false, " online not is set");
TEST_RESULT_INT(cfgOptionSource(cfgOptOnline), cfgSourceParam, " online is source param");
TEST_RESULT_BOOL(cfgOptionBool(cfgOptStartFast), false, " start-fast not is set");
TEST_RESULT_INT(cfgOptionSource(cfgOptStartFast), cfgSourceParam, " start-fast is source param");
TEST_RESULT_BOOL(cfgOptionBool(cfgOptCompress), false, " compress not is set");
TEST_RESULT_INT(cfgOptionSource(cfgOptCompress), cfgSourceConfig, " compress is source config");
TEST_RESULT_BOOL(cfgOptionTest(cfgOptArchiveCheck), false, " archive-check is not set");
@ -1079,6 +1126,14 @@ testRun(void)
TEST_RESULT_BOOL(cfgOptionInt64(cfgOptBufferSize), 65536, " buffer-size is set");
TEST_RESULT_INT(cfgOptionSource(cfgOptBufferSize), cfgSourceConfig, " backup-standby is source config");
unsetenv("PGBACKREST_BOGUS");
unsetenv("PGBACKREST_NO_COMPRESS");
unsetenv("PGBACKREST_RESET_REPO1_HOST");
unsetenv("PGBACKREST_TARGET");
unsetenv("PGBACKREST_ONLINE");
unsetenv("PGBACKREST_START_FAST");
unsetenv("PGBACKREST_PG1_SOCKET_PATH");
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
strLstAdd(argList, strNew(TEST_BACKREST_EXE));
@ -1176,6 +1231,29 @@ testRun(void)
TEST_RESULT_STR(strPtr(varStr(kvGet(recoveryKv, varNewStr(strNew("f"))))), "g", "check recovery option");
TEST_RESULT_STR(strPtr(varStr(kvGet(recoveryKv, varNewStr(strNew("hijk"))))), "l", "check recovery option");
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
strLstAdd(argList, strNew(TEST_BACKREST_EXE));
strLstAdd(argList, strNew(TEST_COMMAND_RESTORE));
setenv("PGBACKREST_STANZA", "db", true);
setenv("PGBACKREST_PG1_PATH", "/path/to/db", true);
setenv("PGBACKREST_RECOVERY_OPTION", "f=g:hijk=l", true);
setenv("PGBACKREST_DB_INCLUDE", "77", true);
TEST_RESULT_VOID(configParse(strLstSize(argList), strLstPtr(argList), false), TEST_COMMAND_RESTORE " command");
TEST_ASSIGN(recoveryKv, cfgOptionKv(cfgOptRecoveryOption), "get recovery options");
TEST_RESULT_STR(strPtr(varStr(kvGet(recoveryKv, varNewStr(strNew("f"))))), "g", "check recovery option");
TEST_RESULT_STR(strPtr(varStr(kvGet(recoveryKv, varNewStr(strNew("hijk"))))), "l", "check recovery option");
TEST_RESULT_STR(strPtr(varStr(varLstGet(cfgOptionLst(cfgOptDbInclude), 0))), "77", "check db include option");
TEST_RESULT_UINT(varLstSize(cfgOptionLst(cfgOptDbInclude)), 1, "check db include option size");
unsetenv("PGBACKREST_STANZA");
unsetenv("PGBACKREST_PG1_PATH");
unsetenv("PGBACKREST_RECOVERY_OPTION");
unsetenv("PGBACKREST_DB_INCLUDE");
// Stanza options should not be loaded for commands that don't take a stanza
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();