]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: proxy: implement publish/unpublish backend CLI
authorAmaury Denoyelle <adenoyelle@haproxy.com>
Tue, 6 Jan 2026 10:04:18 +0000 (11:04 +0100)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Thu, 15 Jan 2026 08:08:18 +0000 (09:08 +0100)
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.

doc/configuration.txt
doc/management.txt
include/haproxy/backend.h
include/haproxy/proxy-t.h
reg-tests/stream/test_content_switching.vtc
src/proxy.c

index 1813035385357a53158b274e00a129313ad21f5d..9a5363f7684477e8bde4729ff98d18667618b338 100644 (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,
index 33f32898de37afb9832f07043edf72b1a08c35b2..7cad2423bff90611cc2667a96bc5dfc3b8199df3 100644 (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
index 00414be2fa756436bc0176d889916acf299dd341..12027073076d6c385d9339e0d4813c7a71a56d0d 100644 (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 */
index 16d6b4b3fc6e0f1b86adaee6f66f98cde54d28f0..0b8ce6d389db5961035c36dd13f5612c839af5b5 100644 (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;
 
index c850faedf1522ff2c5772faea6f0a8b51e60e5c9..dd5087edcf34a6921a6de9c2701d3586c0d53648 100644 (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
index bdfe0e76f5f02510d2f1c3b76f09d12eca41e938..393cac95dffc0bda3522e9b1b6d73b9a01018e13 100644 (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 },