MINOR: cfgparse: implement a simple if/elif/else/endif macro block handler

Very often, especially since reg-tests, it would be desirable to be able
to conditionally comment out a config block, such as removing an SSL
binding when SSL is disabled, or enabling HTX only for certain versions,
etc.

This patch introduces a very simple nested block management which takes
".if", ".elif", ".else" and ".endif" directives to take or ignore a block.

For now the conditions are limited to empty string or "0" for false versus
a non-nul integer for true, which already suffices to test environment
variables. Still, it needs to be a bit more advanced with defines, versions
etc.

A set of ".notice", ".warning" and ".alert" statements are provided to
emit messages, often in order to provide advice about how to fix certain
conditions.
This commit is contained in:
Willy Tarreau 2021-02-12 17:59:10 +01:00
parent 49962b58d0
commit 4b10302fd8
2 changed files with 229 additions and 6 deletions

View File

@ -41,8 +41,9 @@ Summary
2.1. Configuration file format
2.2. Quoting and escaping
2.3. Environment variables
2.4. Time format
2.5. Examples
2.4. Conditional blocks
2.5. Time format
2.6. Examples
3. Global parameters
3.1. Process management and security
@ -743,7 +744,75 @@ file, or could be inherited by a program (See 3.7. Programs):
See also "external-check command" for other variables.
2.4. Time format
2.4. Conditional blocks
-----------------------
It may sometimes be convenient to be able to conditionally enable or disable
some arbitrary parts of the configuration, for example to enable/disable SSL or
ciphers, enable or disable some pre-production listeners without modifying the
configuration, or adjust the configuration's syntax to support two distinct
versions of HAProxy during a migration.. HAProxy brings a set of nestable
preprocessor-like directives which allow to integrate or ignore some blocks of
text. These directives must be placed on their own line and they act on the
lines that follow them. Two of them support an expression, the other ones only
switch to an alternate block or end a current level. The 4 following directives
are defined to form conditional blocks:
- .if <condition>
- .elif <condition>
- .else
- .endif
The ".if" directive nests a new level, ".elif" stays at the same level, ".else"
as well, and ".endif" closes a level. Each ".if" must be terminated by a
matching ".endif". The ".elif" may only be placed after ".if" or ".elif", and
there is no limit to the number of ".elif" that may be chained. There may be
only one ".else" per ".if" and it must always be after the ".if" or the last
".elif" of a block.
Comments may be placed on the same line if needed after a '#', they will be
ignored. The directives are tokenized like other configuration directives, and
as such it is possible to use environment variables in conditions.
The conditions are currently limited to:
- an empty string, always returns "false"
- the integer zero ('0'), always returns "false"
- a non-nul integer (e.g. '1'), always returns "true".
Other patterns are not supported yet but the purpose is to bring a few
functions to test for certain build options and supported features.
Three other directives are provided to report some status:
- .notice "message" : emit this message at level NOTICE
- .warning "message" : emit this message at level WARNING
- .alert "message" : emit this message at level ALERT
Messages emitted at level WARNING may cause the process to fail to start if the
"strict-mode" is enabled. Messages emitted at level ALERT will always cause a
fatal error. These can be used to detect some inappropriate conditions and
provide advice to the user.
Example:
.if "${A}"
.if "${B}"
.notice "A=1, B=1"
.elif "${C}"
.notice "A=1, B=0, C=1"
.elif "${D}"
.warning "A=1, B=0, C=0, D=1"
.else
.alert "A=1, B=0, C=0, D=0"
.endif
.else
.notice "A=0"
.endif
2.5. Time format
----------------
Some parameters involve values representing time, such as timeouts. These
@ -760,7 +829,7 @@ for every keyword. Supported units are :
- d : days. 1d = 24h = 1440m = 86400s = 86400000ms
2.5. Examples
2.6. Examples
-------------
# Simple configuration for an HTTP proxy listening on port 80 on all
@ -10741,7 +10810,7 @@ stick-table type {ip | integer | string [len <length>] | binary [len <length>]}
was last created, refreshed or matched. The expiration delay is
defined using the standard time format, similarly as the various
timeouts. The maximum duration is slightly above 24 days. See
section 2.4 for more information. If this delay is not specified,
section 2.5 for more information. If this delay is not specified,
the session won't automatically expire, but older entries will
be removed once full. Be sure not to use the "nopurge" parameter
if not expiration delay is specified.
@ -10924,7 +10993,7 @@ stick-table type {ip | integer | string [len <length>] | binary [len <length>]}
# computed over a sliding window of 30 seconds.
stick-table type ip size 1m expire 5m store gpc0,conn_rate(30s)
See also : "stick match", "stick on", "stick store-request", section 2.4
See also : "stick match", "stick on", "stick store-request", section 2.5
about time format and section 7 about ACLs.

View File

@ -98,6 +98,23 @@ struct cfg_kw_list cfg_keywords = {
.list = LIST_HEAD_INIT(cfg_keywords.list)
};
/* nested if/elif/else/endif block states */
enum nested_cond_state {
NESTED_COND_IF_TAKE, // "if" with a true condition
NESTED_COND_IF_DROP, // "if" with a false condition
NESTED_COND_IF_SKIP, // "if" masked by an outer false condition
NESTED_COND_ELIF_TAKE, // "elif" with a true condition from a false one
NESTED_COND_ELIF_DROP, // "elif" with a false condition from a false one
NESTED_COND_ELIF_SKIP, // "elif" masked by an outer false condition or a previously taken if
NESTED_COND_ELSE_TAKE, // taken "else" after an if false condition
NESTED_COND_ELSE_DROP, // "else" masked by outer false condition or an if true condition
};
/* 100 levels of nested conditions should already be sufficient */
#define MAXNESTEDCONDS 100
/*
* converts <str> to a list of listeners which are dynamically allocated.
* The format is "{addr|'*'}:port[-end][,{addr|'*'}:port[-end]]*", where :
@ -1798,6 +1815,8 @@ int readcfgfile(const char *file)
size_t outlinesize = 0;
int fatal = 0;
int missing_lf = -1;
int nested_cond_lvl = 0;
enum nested_cond_state nested_conds[MAXNESTEDCONDS];
if ((thisline = malloc(sizeof(*thisline) * linesize)) == NULL) {
ha_alert("parsing [%s] : out of memory.\n", file);
@ -1969,6 +1988,137 @@ next_line:
if (!**args)
continue;
/* check for config macros */
if (*args[0] == '.') {
if (strcmp(args[0], ".if") == 0) {
nested_cond_lvl++;
if (nested_cond_lvl >= MAXNESTEDCONDS) {
ha_alert("parsing [%s:%d]: too many nested '.if', max is %d.\n", file, linenum, MAXNESTEDCONDS);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
if (nested_conds[nested_cond_lvl - 1] == NESTED_COND_IF_DROP ||
nested_conds[nested_cond_lvl - 1] == NESTED_COND_IF_SKIP ||
nested_conds[nested_cond_lvl - 1] == NESTED_COND_ELIF_DROP ||
nested_conds[nested_cond_lvl - 1] == NESTED_COND_ELIF_SKIP ||
nested_conds[nested_cond_lvl - 1] == NESTED_COND_ELSE_DROP) {
nested_conds[nested_cond_lvl] = NESTED_COND_IF_SKIP;
} else if (!*args[1] || *args[1] == '0') {
/* empty = false */
nested_conds[nested_cond_lvl] = NESTED_COND_IF_DROP;
} else if (atoi(args[1]) > 0) {
/* true */
nested_conds[nested_cond_lvl] = NESTED_COND_IF_TAKE;
} else {
ha_alert("parsing [%s:%d]: unparsable conditional expression '%s'.\n", file, linenum, args[1]);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
goto next_line;
}
else if (strcmp(args[0], ".elif") == 0) {
if (!nested_cond_lvl) {
ha_alert("parsing [%s:%d]: lone '.elif' with no matching '.if'.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
if (nested_conds[nested_cond_lvl] == NESTED_COND_ELSE_TAKE ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELSE_DROP) {
ha_alert("parsing [%s:%d]: '.elif' after '.else' is not permitted.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
if (nested_conds[nested_cond_lvl] == NESTED_COND_IF_TAKE ||
nested_conds[nested_cond_lvl] == NESTED_COND_IF_SKIP ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELIF_SKIP) {
nested_conds[nested_cond_lvl] = NESTED_COND_ELIF_SKIP;
} else if (!*args[1] || *args[1] == '0') {
/* empty = false */
nested_conds[nested_cond_lvl] = NESTED_COND_ELIF_DROP;
} else if (atoi(args[1]) > 0) {
/* true */
nested_conds[nested_cond_lvl] = NESTED_COND_ELIF_TAKE;
} else {
ha_alert("parsing [%s:%d]: unparsable conditional expression '%s'.\n", file, linenum, args[1]);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
goto next_line;
}
else if (strcmp(args[0], ".else") == 0) {
if (!nested_cond_lvl) {
ha_alert("parsing [%s:%d]: lone '.else' with no matching '.if'.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
if (nested_conds[nested_cond_lvl] == NESTED_COND_ELSE_TAKE ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELSE_DROP) {
ha_alert("parsing [%s:%d]: '.else' after '.else' is not permitted.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
if (nested_conds[nested_cond_lvl] == NESTED_COND_IF_TAKE ||
nested_conds[nested_cond_lvl] == NESTED_COND_IF_SKIP ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELIF_TAKE ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELIF_SKIP) {
nested_conds[nested_cond_lvl] = NESTED_COND_ELSE_DROP;
} else {
/* otherwise we take the "else" */
nested_conds[nested_cond_lvl] = NESTED_COND_ELSE_TAKE;
}
goto next_line;
}
else if (strcmp(args[0], ".endif") == 0) {
if (!nested_cond_lvl) {
ha_alert("parsing [%s:%d]: lone '.endif' with no matching '.if'.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL;
fatal++;
break;
}
nested_cond_lvl--;
goto next_line;
}
}
if (nested_cond_lvl &&
(nested_conds[nested_cond_lvl] == NESTED_COND_IF_DROP ||
nested_conds[nested_cond_lvl] == NESTED_COND_IF_SKIP ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELIF_DROP ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELIF_SKIP ||
nested_conds[nested_cond_lvl] == NESTED_COND_ELSE_DROP)) {
/* The current block is masked out by the conditions */
goto next_line;
}
/* .warning/.error/.notice */
if (*args[0] == '.') {
if (strcmp(args[0], ".alert") == 0) {
ha_alert("parsing [%s:%d]: '%s'.\n", file, linenum, args[1]);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
goto err;
}
else if (strcmp(args[0], ".warning") == 0) {
ha_warning("parsing [%s:%d]: '%s'.\n", file, linenum, args[1]);
err_code |= ERR_WARN;
goto next_line;
}
else if (strcmp(args[0], ".notice") == 0) {
ha_notice("parsing [%s:%d]: '%s'.\n", file, linenum, args[1]);
goto next_line;
}
else {
ha_alert("parsing [%s:%d]: unknown directive '%s'.\n", file, linenum, args[0]);
err_code |= ERR_ALERT | ERR_FATAL;
fatal++;
break;
}
}
/* check for keyword modifiers "no" and "default" */
if (strcmp(args[0], "no") == 0) {
char *tmp;
@ -2046,6 +2196,10 @@ next_line:
if (cs && cs->post_section_parser)
err_code |= cs->post_section_parser();
if (nested_cond_lvl) {
ha_alert("parsing [%s:%d]: non-terminated '.if' block.\n", file, linenum);
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
}
err:
free(cfg_scope);
cfg_scope = NULL;