From 797ec6ede5b32abf232c36adbb91cfc1f2ccd820 Mon Sep 17 00:00:00 2001 From: Amaury Denoyelle Date: Tue, 6 Jan 2026 11:04:18 +0100 Subject: [PATCH] MEDIUM: proxy: implement publish/unpublish backend CLI Define a new set of CLI commands publish/unpublish backend . 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. --- doc/configuration.txt | 16 +++---- doc/management.txt | 12 +++++ include/haproxy/backend.h | 8 ++-- include/haproxy/proxy-t.h | 1 + reg-tests/stream/test_content_switching.vtc | 48 ++++++++++++++++++++ src/proxy.c | 50 +++++++++++++++++++++ 6 files changed, 123 insertions(+), 12 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 181303538..9a5363f76 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -7103,8 +7103,8 @@ default_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 [{if | unless} ] 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, diff --git a/doc/management.txt b/doc/management.txt index 33f32898d..7cad2423b 100644 --- a/doc/management.txt +++ b/doc/management.txt @@ -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 + 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 + 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 diff --git a/include/haproxy/backend.h b/include/haproxy/backend.h index 00414be2f..120270730 100644 --- a/include/haproxy/backend.h +++ b/include/haproxy/backend.h @@ -88,11 +88,11 @@ static inline int be_usable_srv(struct proxy *be) /* Returns true if 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 */ diff --git a/include/haproxy/proxy-t.h b/include/haproxy/proxy-t.h index 16d6b4b3f..0b8ce6d38 100644 --- a/include/haproxy/proxy-t.h +++ b/include/haproxy/proxy-t.h @@ -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; diff --git a/reg-tests/stream/test_content_switching.vtc b/reg-tests/stream/test_content_switching.vtc index c850faedf..dd5087edc 100644 --- a/reg-tests/stream/test_content_switching.vtc +++ b/reg-tests/stream/test_content_switching.vtc @@ -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 diff --git a/src/proxy.c b/src/proxy.c index bdfe0e76f..393cac95d 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -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 : temporarily disable specific frontend", cli_parse_disable_frontend, NULL, NULL }, { { "enable", "frontend", NULL }, "enable frontend : re-enable specific frontend", cli_parse_enable_frontend, NULL, NULL }, + { { "publish", "backend", NULL }, "publish backend : mark backend as ready for traffic", cli_parse_publish_backend, NULL, NULL }, { { "set", "maxconn", "frontend", NULL }, "set maxconn frontend : change a frontend's maxconn setting", cli_parse_set_maxconn_frontend, NULL }, { { "show","servers", "conn", NULL }, "show servers conn [] : 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 [] : 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 : stop a specific frontend", cli_parse_shutdown_frontend, NULL, NULL }, { { "set", "dynamic-cookie-key", "backend", NULL }, "set dynamic-cookie-key backend : change a backend secret key for dynamic cookies", cli_parse_set_dyncookie_key_backend, NULL }, + { { "unpublish", "backend", NULL }, "unpublish backend : remove backend for traffic processing", cli_parse_unpublish_backend, NULL, NULL }, { { "enable", "dynamic-cookie", "backend", NULL }, "enable dynamic-cookie backend : enable dynamic cookies on a specific backend", cli_parse_enable_dyncookie_backend, NULL }, { { "disable", "dynamic-cookie", "backend", NULL }, "disable dynamic-cookie backend : disable dynamic cookies on a specific backend", cli_parse_disable_dyncookie_backend, NULL }, { { "show", "errors", NULL }, "show errors [] [request|response] : report last request and/or response errors for each proxy", cli_parse_show_errors, cli_io_handler_show_errors, NULL },