diff --git a/.gitignore b/.gitignore index 785652d..19f6f44 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,6 @@ __pycache__ # Build artifacts dist/ -build/ -!build/assets/ *.egg-info/ /tests_e2e/static/conf/config.yml diff --git a/build/Dockerfile b/build/Dockerfile index feb3676..ac0e39f 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,16 +1,39 @@ -FROM alpine:3.23 +# ============================================================================== +# Stage 1: Build Python virtual environment and run tests +# ============================================================================== +FROM haproxy:3.3-alpine AS builder + +USER root + +RUN apk add --no-cache python3 bash curl build-base python3-dev musl-dev linux-headers \ + && curl -LsSf https://astral.sh/uv/install.sh | sh \ + && ln -s /root/.local/bin/uv /usr/local/bin/uv + +WORKDIR /scripts + +COPY pyproject.toml uv.lock LICENSE README.md ./ +COPY src/ ./src/ +COPY tests/ ./tests/ + +RUN uv sync --frozen +RUN uv run pytest -s -vv tests/ +RUN uv sync --no-dev +RUN rm -rf tests/ + +# ============================================================================== +# Stage 2: Lean runtime image +# ============================================================================== +FROM haproxy:3.3-alpine ARG RELEASE_VERSION_ARG ENV RELEASE_VERSION=$RELEASE_VERSION_ARG ENV TZ="Etc/UTC" -RUN apk add --no-cache haproxy bash python3 certbot openssl curl \ - && apk add --no-cache --virtual .build-deps build-base python3-dev musl-dev linux-headers \ - && curl -LsSf https://astral.sh/uv/install.sh | sh \ - && ln -s /root/.local/bin/uv /usr/local/bin/uv +USER root -RUN mkdir -p /etc/easyhaproxy/haproxy \ +RUN apk add --no-cache certbot openssl bash curl su-exec \ + && mkdir -p /etc/easyhaproxy/haproxy \ && openssl dhparam -out /etc/easyhaproxy/haproxy/dhparam 2048 \ && openssl dhparam -out /etc/easyhaproxy/haproxy/dhparam-1024 1024 \ && mkdir -p /etc/easyhaproxy/certs/certbot /etc/easyhaproxy/certs/haproxy \ @@ -22,18 +45,12 @@ RUN mkdir -p /etc/easyhaproxy/haproxy \ && cat /tmp/placeholder.crt /tmp/placeholder.key > /etc/easyhaproxy/certs/haproxy/placeholder.pem \ && rm /tmp/placeholder.key /tmp/placeholder.crt -WORKDIR /scripts - COPY build/assets / +COPY build/entrypoint.sh /entrypoint.sh +COPY --from=builder /scripts /scripts -COPY pyproject.toml uv.lock LICENSE README.md /scripts/ -COPY src/ /scripts/src/ -COPY tests/ /scripts/tests/ +RUN chmod +x /entrypoint.sh \ + && chown -R haproxy:haproxy /etc/easyhaproxy /scripts -RUN cd /scripts && uv sync --frozen - -RUN apk del .build-deps - -RUN cd /scripts && uv run pytest -s -vv tests/ && uv sync --no-dev - -CMD ["/scripts/.venv/bin/easy-haproxy"] +ENTRYPOINT ["/entrypoint.sh"] +CMD ["--base-path", "/etc/easyhaproxy"] \ No newline at end of file diff --git a/build/assets/certs/haproxy/place_holder_cert.pem b/build/assets/certs/haproxy/place_holder_cert.pem deleted file mode 100644 index 49558f4..0000000 --- a/build/assets/certs/haproxy/place_holder_cert.pem +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDyTCCArGgAwIBAgIUVpIokbXupa29drpQBmRlKTViM+UwDQYJKoZIhvcNAQEL -BQAwdDELMAkGA1UEBhMCQVUxFTATBgNVBAgMDFBsYWNlIEhvbGRlcjEVMBMGA1UE -BwwMUGxhY2UgSG9sZGVyMSEwHwYDVQQKDBhQbGFjZSBIb2xkZXIgQ2VydGlmaWNh -dGUxFDASBgNVBAMMC2V4YW1wbGUub3JnMB4XDTIyMDgxNTA0NTEwMFoXDTMyMDgx -MjA0NTEwMFowdDELMAkGA1UEBhMCQVUxFTATBgNVBAgMDFBsYWNlIEhvbGRlcjEV -MBMGA1UEBwwMUGxhY2UgSG9sZGVyMSEwHwYDVQQKDBhQbGFjZSBIb2xkZXIgQ2Vy -dGlmaWNhdGUxFDASBgNVBAMMC2V4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAqufw4FdqYcJep7mHHcYGUN79GNBLpvAIdg+1NbKx+cB/ -PtaDuozqVkkT8CmM0Mruay4vCbYkMytCeKgHj2+hLMy7oUQvx2pK/V0i0foPAC0m -gAvgmaWZbQTENHX4A0Rwvim0yixgeBVhz4hTMOIunilSXbKRkFBUidCnYQe1Nzy1 -dbH/fh8++fzLCglDE2kydrE3Zq/54G2xFOxPt1DZRnQ3RBYaMIR/uPPjpVxRWl+p -w4ucklAIZcu2htlOpGl7/3baMtnhpTo9LrkWzSNS7CJQvj6BvDULbcN3+hNPOoVE -MM9MCaM9V+FS25kf+DfyaVUVDVIv0thpwa+f3tCXSwIDAQABo1MwUTAdBgNVHQ4E -FgQUV831A1qfpV+Sy/J+lN6LKLgopsQwHwYDVR0jBBgwFoAUV831A1qfpV+Sy/J+ -lN6LKLgopsQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhnf7 -aW/jpr8JWC0l2lo0z1HpOaSNehhcfNyM6Y43lR3Lr1avibbIXkAOzCuSWanFNnex -dOq8PbG9bmO1ncM6qzXRBzk4pLVJmzzDZs/fPVghSZumY2bzzJFtdCQ5FiLaaE/c -cKtBPvoUjfvrBU9OwFSb9UaoQdxtateb/Kk6JfzWi6YZqxSXFNa0ZTWJaoRFQVj5 -bZZe+wgpGRz46p+2YMwsNolXxa+7yY9x6kOMqZP6++5LZGXm5iWxjWbN4WtqmNnN -cK35fAdLlg4d3wn5tVuTkpKH0FcaRlgpBSjVKejgnFTCxKcOwOCqOfu+nuSSWpa7 -aRqdAbMTeVFQhyLXhQ== ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCq5/DgV2phwl6n -uYcdxgZQ3v0Y0Eum8Ah2D7U1srH5wH8+1oO6jOpWSRPwKYzQyu5rLi8JtiQzK0J4 -qAePb6EszLuhRC/Hakr9XSLR+g8ALSaAC+CZpZltBMQ0dfgDRHC+KbTKLGB4FWHP -iFMw4i6eKVJdspGQUFSJ0KdhB7U3PLV1sf9+Hz75/MsKCUMTaTJ2sTdmr/ngbbEU -7E+3UNlGdDdEFhowhH+48+OlXFFaX6nDi5ySUAhly7aG2U6kaXv/dtoy2eGlOj0u -uRbNI1LsIlC+PoG8NQttw3f6E086hUQwz0wJoz1X4VLbmR/4N/JpVRUNUi/S2GnB -r5/e0JdLAgMBAAECggEAI0nY9rmWAbF8ke1A9OjajQA+Ck2YEVQmqxn7NKc9EHCq -1XK9qFtIV6CnOUObC9GbAQ58L+kn+FjKVNd9GCTYhsOPSnEl3GsaKM5+ThTv2/12 -oaHSMmd7EoOVb6+cEjCjhuBdsBERqjngBFYFt2Y8cfPeSfKBE+dCTWKD7QkGZe0Q -tEII2NeE1AoQwG34TANxyn+HbZG809i+k21Pm0tPNyAFwhPvotpjSftMNgco8jcx -39DCt7rG1etpc4VhIUUl8GqsNAHYg8/SfscE18PYeX7RJpKGS128E1mKCqWqn+Ie -VCiMmWoQ4ZBlUFp3tS4+FWcQtDr8Z82QTZQn3ODwIQKBgQDV1NR+SxjBBnzpHDZC -cTxCK0PU37VmPjyr6BAXlftCPLGLgKEVIDRuna6EoiZ0/K0RMDZRoaJgDQPoAXDV -UwJOWYtlJBzUxwQbBJc3S7C3kEC+qeRT9lRwIrn7P6gT1DMcSRsV4ULwGRWAMa2Q -6apbK/K+56Ds7LiStu3OcJSxDwKBgQDMnAvLm0uMo9ZceKG515ruqzQj2YPz2+Zc -f38pD1hESDRj1bzgT09FAsejPnlN7KPp8TFgRUB9Rqb6DZCkx49zdtcDkuZIKfQH -Ga7ITBXnTwe8M+nwq2Q2LJYPdB/p8mBqh4ujA2XIS7ZHCKqOFGsseP4H1uxZeORI -pIPQp4C+BQKBgQDKHw9tAZc4feV8g4pWa6rF8ReBFKTnLFU1OXpckQybo7s/Xirl -STfGh437GTq4wk7lPGlb6CkQGb1jhFkfjANWBBZbWDNYfXZIA6LcRdOY7+YDU5vc -Ma/G/0xFTfqWI7LcPc44dGFNiqhkMJEbtYOuAnDGOzRGP8yIAhnvVUN3yQKBgQC1 -eaYglYGBoQMMi1Xt7iQVkbWyIkedr6l22wJe2aRRE7Wb4sQeM1m8fMWyrUOL8NpF -MU6481NKibKp0AQ9kl5Sa9Iy8kTbNpKhBY93SbyXpwnWTDku4+UDA7KozDdOGVKY -ydX45JeO+lAWWsJjOAsCq+Gr9F020jmvkHL1SsuuPQKBgQDHHCSOlUycLnzxEAks -uiu5MseFzkmdWN1ShrjPkcJ4HLmSD5zRgCySpBB92WPU+yBANlY7WN6fJHVJk/d7 -sNAg78GuAxcWrNAQwu6DRjhnb8zTVICJX8HmDLmsoOHwLsShdckWMks5XEk3NzoV -4ym6aG0tDX+rkBkP/VIjSXA+Cg== ------END PRIVATE KEY----- diff --git a/build/entrypoint.sh b/build/entrypoint.sh new file mode 100644 index 0000000..9c0978f --- /dev/null +++ b/build/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +## When docker.sock is mounted its GID comes from the host and is unknown at +## build time. Detect it at startup and add the haproxy user to that group so +## Docker/Swarm discovery works without running the whole process as root. +#if [ -S /var/run/docker.sock ]; then +# SOCK_GID=$(stat -c '%g' /var/run/docker.sock) +# getent group "$SOCK_GID" >/dev/null 2>&1 || addgroup -g "$SOCK_GID" dockersock +# addgroup haproxy "$(getent group "$SOCK_GID" | cut -d: -f1)" 2>/dev/null || true +#fi + +exec /scripts/.venv/bin/easy-haproxy "$@" \ No newline at end of file diff --git a/src/functions/__init__.py b/src/functions/__init__.py index 62cf6f8..52fdcad 100644 --- a/src/functions/__init__.py +++ b/src/functions/__init__.py @@ -354,8 +354,12 @@ class DaemonizeHAProxy: self.thread = Process(target=self.__start, args=()) self.thread.start() + @staticmethod + def get_haproxy_bin() -> str: + return shutil.which('haproxy') or '/usr/sbin/haproxy' + def get_haproxy_command(self, action, pid_file="/run/haproxy.pid"): - haproxy_bin = shutil.which('haproxy') or '/usr/sbin/haproxy' + haproxy_bin = DaemonizeHAProxy.get_haproxy_bin() custom_config_files = "" if len(list(self.get_custom_config_files().keys())) != 0: custom_config_files = f"-f {self.custom_config_folder}" diff --git a/tests/test_daemonize.py b/tests/test_daemonize.py index 6ddb9f7..d418e74 100644 --- a/tests/test_daemonize.py +++ b/tests/test_daemonize.py @@ -3,6 +3,8 @@ from functions import Consts from functions import DaemonizeHAProxy +BIN = DaemonizeHAProxy.get_haproxy_bin() + def test_daemonize_haproxy(): daemon = DaemonizeHAProxy() @@ -16,12 +18,12 @@ def test_daemonize_haproxy_check_config(): def test_daemonize_haproxy_get_haproxy_command_start(): daemon = DaemonizeHAProxy() command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START) - assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /run/haproxy.pid -S /var/run/haproxy.sock" + assert command == f"{BIN} -W -f {Consts.haproxy_config} -p /run/haproxy.pid -S /var/run/haproxy.sock" def test_daemonize_haproxy_get_haproxy_command_reload_nopid(): daemon = DaemonizeHAProxy() command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD) - assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /run/haproxy.pid -S /var/run/haproxy.sock" + assert command == f"{BIN} -W -f {Consts.haproxy_config} -p /run/haproxy.pid -S /var/run/haproxy.sock" def test_daemonize_haproxy_get_haproxy_command_reload_pidinvalid(): daemon = DaemonizeHAProxy() @@ -29,7 +31,7 @@ def test_daemonize_haproxy_get_haproxy_command_reload_pidinvalid(): with open("/tmp/temp.pid", 'w') as file: file.write("-1001") command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid") - assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /tmp/temp.pid -S /var/run/haproxy.sock" + assert command == f"{BIN} -W -f {Consts.haproxy_config} -p /tmp/temp.pid -S /var/run/haproxy.sock" finally: assert not os.path.exists("/tmp/temp.pid") @@ -39,7 +41,7 @@ def test_daemonize_haproxy_get_haproxy_command_reload_existing_pin(): with open("/tmp/temp.pid", 'w') as file: file.write("1") command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid") - assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /tmp/temp.pid -x /var/run/haproxy.sock -sf 1" + assert command == f"{BIN} -W -f {Consts.haproxy_config} -p /tmp/temp.pid -x /var/run/haproxy.sock -sf 1" finally: assert os.path.exists("/tmp/temp.pid") os.unlink("/tmp/temp.pid") @@ -55,4 +57,4 @@ def test_daemonize_haproxy2_check_config(): def test_daemonize_haproxy2_get_haproxy_command_start(): daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures') command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START) - assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -f {os.path.dirname(__file__)}/fixtures -p /run/haproxy.pid -S /var/run/haproxy.sock" + assert command == f"{BIN} -W -f {Consts.haproxy_config} -f {os.path.dirname(__file__)}/fixtures -p /run/haproxy.pid -S /var/run/haproxy.sock" diff --git a/tests_e2e/generate-keys.sh b/tests_e2e/generate-keys.sh index a386c3e..a1efb2a 100755 --- a/tests_e2e/generate-keys.sh +++ b/tests_e2e/generate-keys.sh @@ -24,6 +24,7 @@ openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \ -keyout "$SCRIPT_DIR/static/host1.local.pem" \ -out "$SCRIPT_DIR/static/host1.local.pem" \ -subj "/C=US/ST=State/L=City/O=Organization/CN=host1.local" +chmod 644 "$SCRIPT_DIR/static/host1.local.pem" # Copy to swarm directory cp "$SCRIPT_DIR/static/host1.local.pem" "$SCRIPT_DIR/swarm/certs/host1.local.pem" @@ -40,6 +41,7 @@ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout "$SCRIPT_DIR/docker/host2.local.pem" \ -out "$SCRIPT_DIR/docker/host2.local.pem" \ -subj "/C=US/ST=State/L=City/O=Organization/CN=host2.local" +chmod 644 "$SCRIPT_DIR/docker/host2.local.pem" # Copy to swarm directory cp "$SCRIPT_DIR/docker/host2.local.pem" "$SCRIPT_DIR/swarm/certs/host2.local.pem" @@ -55,9 +57,11 @@ echo "Generating JWT RSA key pair (2048-bit)..." # Generate private key openssl genrsa -out "$SCRIPT_DIR/docker/jwt_private.pem" 2048 +chmod 644 "$SCRIPT_DIR/docker/jwt_private.pem" # Extract public key openssl rsa -in "$SCRIPT_DIR/docker/jwt_private.pem" -pubout -out "$SCRIPT_DIR/docker/jwt_pubkey.pem" +chmod 644 "$SCRIPT_DIR/docker/jwt_pubkey.pem" echo "✓ Created JWT key pair (2048-bit)" echo " - $SCRIPT_DIR/docker/jwt_private.pem (private key)" @@ -72,6 +76,7 @@ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout "$SCRIPT_DIR/docker/certs/haproxy/.place_holder_cert.pem" \ -out "$SCRIPT_DIR/docker/certs/haproxy/.place_holder_cert.pem" \ -subj "/C=US/ST=State/L=City/O=Organization/CN=placeholder" +chmod 644 "$SCRIPT_DIR/docker/certs/haproxy/.place_holder_cert.pem" echo "✓ Created placeholder certificate" echo " - $SCRIPT_DIR/docker/certs/haproxy/.place_holder_cert.pem"