MEDIUM: proxy: implement publish/unpublish backend CLI

Define a new set of CLI commands publish/unpublish backend <be>. The
objective is to be able to change the status of a backend to
unpublished. Such a backend is considered ineligible to traffic : this
allows to skip use_backend rules which target it.

Note that contrary to disabled/stopped proxies, an unpublished backend
still has server checks running on it.

Internally, a new proxy flags PR_FL_BE_UNPUBLISHED is defined. CLI
commands handler "publish backend" and "unpublish backend" are executed
under thread isolation. This guarantees that the flag can safely be set
or remove in the CLI handlers, and read during content-switching
processing.
This commit is contained in:
Amaury Denoyelle 2026-01-06 11:04:18 +01:00
parent 21fb0a3f58
commit 797ec6ede5
6 changed files with 123 additions and 12 deletions

View File

@ -7103,8 +7103,8 @@ default_backend <backend>
used when no rule has matched. It generally is the dynamic backend which
will catch all undetermined requests.
If a backend used as default is disabled, no traffic will be redirected to
it.
If a backend is disabled or unpublished, default_backend rules targetting it
will be ignored and stream processing will remain on the original proxy.
Example :
@ -14756,16 +14756,16 @@ use_backend <backend> [{if | unless} <condition>]
There may be as many "use_backend" rules as desired. All of these rules are
evaluated in their declaration order, and the first one which matches will
assign the backend. This is even the case if the backend is considered as
down. However, if a matching rule targets a disabled backend, it is ignored
instead and rules evaluation continue.
down. However, if a matching rule targets a disabled or unpublished backend,
it is ignored instead and rules evaluation continue.
In the first form, the backend will be used if the condition is met. In the
second form, the backend will be used if the condition is not met. If no
condition is valid, the backend defined with "default_backend" will be used
unless it is disabled. If no default backend is defined, either the servers
in the same section are used (in case of a "listen" section) or, in case of a
frontend, no server is used and a 503 service unavailable response is
returned.
unless it is disabled or unpublished. If no default backend is available,
either the servers in the same section are used (in case of a "listen"
section) or, in case of a frontend, no server is used and a 503 service
unavailable response is returned.
Note that it is possible to switch from a TCP frontend to an HTTP backend. In
this case, either the frontend has already checked that the protocol is HTTP,

View File

@ -2474,6 +2474,11 @@ prompt [help | n | i | p | timed]*
advanced scripts, and the non-interactive mode (default) to basic scripts.
Note that the non-interactive mode is not available for the master socket.
publish backend <backend>
Activates content switching to a backend instance. This is the reverse
operation of "unpublish backend" command. This command is restricted and can
only be issued on sockets configured for levels "operator" or "admin".
quit
Close the connection when in interactive mode.
@ -2842,6 +2847,13 @@ operator
increased. It also drops expert and experimental mode. See also "show cli
level".
unpublish backend <backend>
Marks the backend as unqualified for future traffic selection. In effect,
use_backend / default_backend rules which reference it are ignored and the
next content switching rules are evaluated. Contrary to disabled backends,
servers health checks remain active. This command is restricted and can only
be issued on sockets configured for levels "operator" or "admin".
user
Decrease the CLI level of the current CLI session to user. It can't be
increased. It also drops expert and experimental mode. See also "show cli

View File

@ -88,11 +88,11 @@ static inline int be_usable_srv(struct proxy *be)
/* Returns true if <be> backend can be used as target to a switching rules. */
static inline int be_is_eligible(const struct proxy *be)
{
/* A disabled backend cannot be selected for traffic. Note that STOPPED
* state is ignored as there is a risk of breaking requests during
* soft-stop.
/* A disabled or unpublished backend cannot be selected for traffic.
* Note that STOPPED state is ignored as there is a risk of breaking
* requests during soft-stop.
*/
return !(be->flags & PR_FL_DISABLED);
return !(be->flags & (PR_FL_DISABLED|PR_FL_BE_UNPUBLISHED));
}
/* set the time of last session on the backend */

View File

@ -247,6 +247,7 @@ enum PR_SRV_STATE_FILE {
#define PR_FL_IMPLICIT_REF 0x10 /* The default proxy is implicitly referenced by another proxy */
#define PR_FL_PAUSED 0x20 /* The proxy was paused at run time (reversible) */
#define PR_FL_CHECKED 0x40 /* The proxy configuration was fully checked (including postparsing checks) */
#define PR_FL_BE_UNPUBLISHED 0x80 /* The proxy cannot be targetted by content switching rules */
struct stream;

View File

@ -42,6 +42,7 @@ haproxy h1 -conf {
use_backend %[req.hdr("x-target")] if { req.hdr("x-dyn") "1" }
use_backend be_disabled if { req.hdr("x-target") "be_disabled" }
use_backend be
use_backend be2
default_backend be_default
listen li
@ -52,6 +53,9 @@ haproxy h1 -conf {
backend be
http-request return status 200 hdr "x-be" %[be_name]
backend be2
http-request return status 200 hdr "x-be" %[be_name]
backend be_disabled
disabled
http-request return status 200 hdr "x-be" %[be_name]
@ -108,3 +112,47 @@ client c3 -connect ${h1_liS_sock} {
expect resp.status == 200
expect resp.http.x-be == "li"
} -run
haproxy h1 -cli {
send "unpublish backend be_unknown"
expect ~ "No such backend."
send "unpublish backend be_disabled"
expect ~ "No effect on a disabled backend."
send "unpublish backend be"
expect ~ "Backend unpublished."
}
client c4 -connect ${h1_fe2S_sock} {
# Static rule on unpublished backend -> continue to next rule
txreq
rxresp
expect resp.status == 200
expect resp.http.x-be == "be2"
# Dynamic rule on unpublished backend -> continue to next rule
txreq -hdr "x-dyn: 1" -hdr "x-target: be"
rxresp
expect resp.status == 200
expect resp.http.x-be == "be2"
} -run
haproxy h1 -cli {
send "publish backend be"
expect ~ "Backend published."
}
client c5 -connect ${h1_fe2S_sock} {
# Static rule matching on republished backend
txreq -hdr "x-target: be"
rxresp
expect resp.status == 200
expect resp.http.x-be == "be"
# Dynamic rule matching on republished backend
txreq -hdr "x-dyn: 1" -hdr "x-target: be"
rxresp
expect resp.status == 200
expect resp.http.x-be == "be"
} -run

View File

@ -3360,6 +3360,54 @@ static int cli_parse_enable_frontend(char **args, char *payload, struct appctx *
return 1;
}
static int cli_parse_publish_backend(char **args, char *payload, struct appctx *appctx, void *private)
{
struct proxy *px;
usermsgs_clr("CLI");
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
return 1;
px = cli_find_backend(appctx, args[2]);
if (!px)
return cli_err(appctx, "No such backend.\n");
if (px->flags & PR_FL_DISABLED)
return cli_err(appctx, "No effect on a disabled backend.\n");
thread_isolate();
px->flags &= ~PR_FL_BE_UNPUBLISHED;
thread_release();
ha_notice("Backend published.\n");
return cli_umsg(appctx, LOG_INFO);
}
static int cli_parse_unpublish_backend(char **args, char *payload, struct appctx *appctx, void *private)
{
struct proxy *px;
usermsgs_clr("CLI");
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
return 1;
px = cli_find_backend(appctx, args[2]);
if (!px)
return cli_err(appctx, "No such backend.\n");
if (px->flags & PR_FL_DISABLED)
return cli_err(appctx, "No effect on a disabled backend.\n");
thread_isolate();
px->flags |= PR_FL_BE_UNPUBLISHED;
thread_release();
ha_notice("Backend unpublished.\n");
return cli_umsg(appctx, LOG_INFO);
}
/* appctx context used during "show errors" */
struct show_errors_ctx {
struct proxy *px; /* current proxy being dumped, NULL = not started yet. */
@ -3574,12 +3622,14 @@ static int cli_io_handler_show_errors(struct appctx *appctx)
static struct cli_kw_list cli_kws = {{ },{
{ { "disable", "frontend", NULL }, "disable frontend <frontend> : temporarily disable specific frontend", cli_parse_disable_frontend, NULL, NULL },
{ { "enable", "frontend", NULL }, "enable frontend <frontend> : re-enable specific frontend", cli_parse_enable_frontend, NULL, NULL },
{ { "publish", "backend", NULL }, "publish backend <backend> : mark backend as ready for traffic", cli_parse_publish_backend, NULL, NULL },
{ { "set", "maxconn", "frontend", NULL }, "set maxconn frontend <frontend> <value> : change a frontend's maxconn setting", cli_parse_set_maxconn_frontend, NULL },
{ { "show","servers", "conn", NULL }, "show servers conn [<backend>] : dump server connections status (all or for a single backend)", cli_parse_show_servers, cli_io_handler_servers_state },
{ { "show","servers", "state", NULL }, "show servers state [<backend>] : dump volatile server information (all or for a single backend)", cli_parse_show_servers, cli_io_handler_servers_state },
{ { "show", "backend", NULL }, "show backend : list backends in the current running config", NULL, cli_io_handler_show_backend },
{ { "shutdown", "frontend", NULL }, "shutdown frontend <frontend> : stop a specific frontend", cli_parse_shutdown_frontend, NULL, NULL },
{ { "set", "dynamic-cookie-key", "backend", NULL }, "set dynamic-cookie-key backend <bk> <k> : change a backend secret key for dynamic cookies", cli_parse_set_dyncookie_key_backend, NULL },
{ { "unpublish", "backend", NULL }, "unpublish backend <backend> : remove backend for traffic processing", cli_parse_unpublish_backend, NULL, NULL },
{ { "enable", "dynamic-cookie", "backend", NULL }, "enable dynamic-cookie backend <bk> : enable dynamic cookies on a specific backend", cli_parse_enable_dyncookie_backend, NULL },
{ { "disable", "dynamic-cookie", "backend", NULL }, "disable dynamic-cookie backend <bk> : disable dynamic cookies on a specific backend", cli_parse_disable_dyncookie_backend, NULL },
{ { "show", "errors", NULL }, "show errors [<px>] [request|response] : report last request and/or response errors for each proxy", cli_parse_show_errors, cli_io_handler_show_errors, NULL },