diff --git a/docs/container-labels.md b/docs/container-labels.md index 9b9bfe2..6e634c9 100644 --- a/docs/container-labels.md +++ b/docs/container-labels.md @@ -6,20 +6,22 @@ sidebar_position: 11 ## Container (Docker or Swarm) labels -| Label | Description | Default | Example | -|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------------------------------| -| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com | -| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp | -| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 | -| easyhaproxy.[definition].localport | (Optional) Port container is listening. | 80 | 8080 | -| easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | *empty* | \{"foo.com":"https://bla.com", "bar.com":"https://bar.org"} | -| easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `certbot` is enabled. | *empty* | base64 cert + key | -| easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false | -| easyhaproxy.[definition].ssl-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl | -| easyhaproxy.[definition].certbot | (Optional) Generate certificate with certbot. Do not use with `sslcert` parameter. More info [here](acme.md). | false | true OR false | -| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false | +| Label | Description | Default | Example | +|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------------------------------| +| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com | +| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp | +| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 | +| easyhaproxy.[definition].localport | (Optional) Port container is listening. | 80 | 8080 | +| easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | *empty* | \{"foo.com":"https://bla.com", "bar.com":"https://bar.org"} | +| easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `certbot` is enabled. | *empty* | base64 cert + key | +| easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false | +| easyhaproxy.[definition].ssl-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl | +| easyhaproxy.[definition].certbot | (Optional) Generate certificate with certbot. Do not use with `sslcert` parameter. More info [here](acme.md). | false | true OR false | +| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false | | easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use** this with `ssl` or `certbot` parameters | false | true OR false | -| easyhaproxy.[definition].balance | (Optional) HAProxy balance algorithm. See [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance) | roundrobin | roundrobin, source, uri, url_param, hdr, rdp-cookie, leastconn, first, static-rr, rdp-cookie, hdr_dom, map-based | +| easyhaproxy.[definition].balance | (Optional) HAProxy balance algorithm. See [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance) | roundrobin | roundrobin, source, uri, url_param, hdr, rdp-cookie, leastconn, first, static-rr, rdp-cookie, hdr_dom, map-based | +| easyhaproxy.[definition].proto | (Optional) Backend server protocol (e.g., fcgi for PHP-FPM, h2 for HTTP/2) | *empty* | fcgi, h2 | +| easyhaproxy.[definition].socket | (Optional) Unix socket path for backend connection (alternative to host:port) | *empty* | /run/php/php-fpm.sock | :::info Understanding Definitions The `[definition]` is a string identifier that groups related configuration labels together. Different definitions create separate HAProxy configurations. @@ -93,6 +95,54 @@ docker run \ some/tcp-service ``` +### FastCGI (PHP-FPM) Support + +EasyHAProxy supports FastCGI protocol for PHP-FPM and other FastCGI applications. + +#### Using Unix Socket + +```yaml title="PHP-FPM with Unix socket" +version: "3" + +services: + php-fpm: + image: php:8.2-fpm + labels: + easyhaproxy.fcgi.host: phpapp.local + easyhaproxy.fcgi.port: 80 + easyhaproxy.fcgi.socket: /run/php/php-fpm.sock + easyhaproxy.fcgi.proto: fcgi + volumes: + - /run/php:/run/php +``` + +#### Using TCP Connection + +```yaml title="PHP-FPM with TCP connection" +version: "3" + +services: + php-fpm: + image: php:8.2-fpm + labels: + easyhaproxy.fcgi.host: phpapp.local + easyhaproxy.fcgi.port: 80 + easyhaproxy.fcgi.localport: 9000 + easyhaproxy.fcgi.proto: fcgi +``` + +**Generated HAProxy Configuration:** + +``` +backend srv_phpapp_local_80 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 /run/php/php-fpm.sock check weight 1 proto fcgi +``` + ### Redirect Domains ```bash title="Domain redirect configuration" diff --git a/src/easymapping/__init__.py b/src/easymapping/__init__.py index 24b5277..eb810f9 100644 --- a/src/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -175,13 +175,33 @@ class HaproxyConfigGenerator: "" ) + # Protocol for backend server communication (e.g., fcgi, h2) + proto = self.label.get( + self.label.create([definition, "proto"]), + "" + ) + + # Unix socket path (alternative to host:port) + socket_path = self.label.get( + self.label.create([definition, "socket"]), + "" + ) + for hostname in sorted(d[host_label].split(",")): hostname = hostname.strip() self.serving_hosts.append("%s:%s" % (hostname, port)) easymapping[port]["hosts"].setdefault(hostname, {}) easymapping[port]["hosts"][hostname].setdefault("containers", []) easymapping[port]["hosts"][hostname].setdefault("certbot", False) - easymapping[port]["hosts"][hostname]["containers"] += ["{}:{}".format(container, ct_port)] + easymapping[port]["hosts"][hostname].setdefault("proto", proto) + + # Determine server address: Unix socket or TCP host:port + if socket_path: + server_address = socket_path + else: + server_address = "{}:{}".format(container, ct_port) + + easymapping[port]["hosts"][hostname]["containers"] += [server_address] easymapping[port]["hosts"][hostname]["certbot"] = certbot easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool( self.label.create([definition, "redirect_ssl"]) diff --git a/src/templates/haproxy.cfg.j2 b/src/templates/haproxy.cfg.j2 index f830e0d..ca69453 100644 --- a/src/templates/haproxy.cfg.j2 +++ b/src/templates/haproxy.cfg.j2 @@ -97,7 +97,7 @@ backend srv_{{ host }} tcp-check connect{{ " ssl" if o["ssl-check"] == "ssl" }} {% endif %} {% for c in o["hosts"][k]["containers"] %} - server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["ssl-check"] == "ssl" }} + server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["ssl-check"] == "ssl" }}{{ " proto " + o["hosts"][k]["proto"] if o["hosts"][k].get("proto") }} {% endfor %} {% endfor %} {% endfor %} diff --git a/src/tests/test_parser.py b/src/tests/test_parser.py index 1aa0f4b..519ffa0 100644 --- a/src/tests/test_parser.py +++ b/src/tests/test_parser.py @@ -124,6 +124,7 @@ def test_parser_finds_services_raw(): "my-stack_agent:9001" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] } @@ -143,6 +144,7 @@ def test_parser_finds_services_raw(): "my-stack_cadvisor:8080" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] }, @@ -152,6 +154,7 @@ def test_parser_finds_services_raw(): "my-stack_node-exporter:9100" ], "certbot": True, + "proto": "", "redirect_ssl": False, "plugin_configs": [] } @@ -171,6 +174,7 @@ def test_parser_finds_services_raw(): "my-stack_node-exporter:9100" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] }, @@ -180,6 +184,7 @@ def test_parser_finds_services_raw(): "some-service:80" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] } @@ -204,6 +209,7 @@ def test_parser_finds_services_raw(): "some-service:80" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] } @@ -471,6 +477,7 @@ def test_parser_finds_services_clone_to_ssl_raw(): "10.152.183.215:8080" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] }, @@ -480,6 +487,7 @@ def test_parser_finds_services_clone_to_ssl_raw(): "10.152.183.62:8080" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] }, @@ -489,6 +497,7 @@ def test_parser_finds_services_clone_to_ssl_raw(): "10.152.183.62:8080" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] } @@ -508,6 +517,7 @@ def test_parser_finds_services_clone_to_ssl_raw(): "10.152.183.215:8080" ], "certbot": False, + "proto": "", "redirect_ssl": False, "plugin_configs": [] } @@ -525,6 +535,37 @@ def test_parser_finds_services_clone_to_ssl_raw(): assert parsed_object == processed assert [] == cfg.certbot_hosts +def test_parser_fcgi(): + """Test FastCGI support with proto and socket parameters""" + line_list = load_fixture("services-fcgi") + + result = { + "customerrors": False, + "stats": { + "port": 0 + } + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(line_list) + + assert len(haproxy_config) > 0 + + # Verify proto fcgi is in the output + assert "proto fcgi" in haproxy_config + + # Verify Unix socket path is used + assert "/run/php/php-fpm.sock" in haproxy_config + + # Verify TCP connection is also present + assert "172.17.0.3:9000" in haproxy_config + + path = os.path.dirname(os.path.realpath(__file__)) + with open(path + "/expected/services-fcgi.txt", 'r') as expected_file: + assert expected_file.read() == haproxy_config + assert [] == cfg.certbot_hosts + + # test_parser_finds_services_raw() # test_parser_tcp() # test_parser_multiple_hosts()