diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index ae294fdc4..a7e158cf3 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -375,6 +375,8 @@ jobs: run: ./tests/main.py "docker" - name: Run Autoconf tests run: ./tests/main.py "autoconf" + - name: Run Swarm tests + run: ./tests/main.py "swarm" - name: Temp stop tests run: exit 1 - name: Run autoconf tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 6011b042d..936f1e410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Fix various documentation errors/typos and add various enhancements - Fix ui.env not read when using Linux integration - Fix check if BunkerNet is activated on default server -- Fix request crash when mmdb lookup is fails +- Fix request crash when mmdb lookup fails - Add \*_CUSTOM_CONF_\* setting to automatically add custom config files from setting value - Add DENY_HTTP_STATUS setting to choose standard 403 error page (default) or 444 to close connection when access is denied - Add CORS (Cross-Origin Resource Sharing) core plugin diff --git a/examples/authelia/swarm.yml b/examples/authelia/swarm.yml new file mode 100644 index 000000000..46c980824 --- /dev/null +++ b/examples/authelia/swarm.yml @@ -0,0 +1,96 @@ +version: '3' + +services: + + # APPLICATIONS + app1: + image: node + working_dir: /home/node/app + networks: + - bw-services + volumes: + - ./js-app:/home/node/app + environment: + - NODE_ENV=production + command: bash -c "npm install express && node index.js" + deploy: + placement: + constraints: + - "node.role==worker" + labels: + - bunkerweb.SERVER_NAME=app1.example.com + - bunkerweb.USE_REVERSE_PROXY=yes + - bunkerweb.REVERSE_PROXY_URL=/ + - bunkerweb.REVERSE_PROXY_HOST=http://app1:3000 + - bunkerweb.REVERSE_PROXY_AUTH_REQUEST=/authelia + - bunkerweb.REVERSE_PROXY_AUTH_REQUEST_SIGNIN_URL=https://auth.example.com/?rd=$$scheme%3A%2F%2F$$host$$request_uri + - bunkerweb.REVERSE_PROXY_AUTH_REQUEST_SET=$$user $$upstream_http_remote_user;$$groups $$upstream_http_remote_groups;$$name $$upstream_http_remote_name;$$email $$upstream_http_remote_email + - bunkerweb.REVERSE_PROXY_HEADERS=Remote-User $$user;Remote-Groups $$groups;Remote-Name $$name;Remote-Email $$email + - bunkerweb.REVERSE_PROXY_URL_999=/authelia + - bunkerweb.REVERSE_PROXY_HOST_999=http://authelia:9091/api/verify + - bunkerweb.REVERSE_PROXY_HEADERS_999=X-Original-URL $$scheme://$$http_host$$request_uri;Content-Length "" + + app2: + image: tutum/hello-world + networks: + - bw-services + deploy: + placement: + constraints: + - "node.role==worker" + labels: + - bunkerweb.SERVER_NAME=app2.example.com + - bunkerweb.USE_REVERSE_PROXY=yes + - bunkerweb.REVERSE_PROXY_URL=/ + - bunkerweb.REVERSE_PROXY_HOST=http://app2 + - bunkerweb.REVERSE_PROXY_AUTH_REQUEST=/authelia + - bunkerweb.REVERSE_PROXY_AUTH_REQUEST_SIGNIN_URL=https://auth.example.com/?rd=$$scheme%3A%2F%2F$$host$$request_uri + - bunkerweb.REVERSE_PROXY_AUTH_REQUEST_SET=$$user $$upstream_http_remote_user;$$groups $$upstream_http_remote_groups;$$name $$upstream_http_remote_name;$$email $$upstream_http_remote_email + - bunkerweb.REVERSE_PROXY_HEADERS=Remote-User $$user;Remote-Groups $$groups;Remote-Name $$name;Remote-Email $$email + - bunkerweb.REVERSE_PROXY_URL_999=/authelia + - bunkerweb.REVERSE_PROXY_HOST_999=http://authelia:9091/api/verify + - bunkerweb.REVERSE_PROXY_HEADERS_999=X-Original-URL $$scheme://$$http_host$$request_uri;Content-Length "" + + # AUTHELIA + authelia: + image: authelia/authelia + networks: + - bw-services + volumes: + - ./authelia:/config + restart: unless-stopped + healthcheck: + disable: true + environment: + - TZ=Europe/Paris + deploy: + placement: + constraints: + - "node.role==worker" + labels: + - bunkerweb.SERVER_NAME=auth.example.com + - bunkerweb.USE_REVERSE_PROXY=yes + - bunkerweb.REVERSE_PROXY_URL=/ + - bunkerweb.REVERSE_PROXY_HOST=http://authelia:9091 + - bunkerweb.REVERSE_PROXY_INTERCEPT_ERRORS=no + + redis: + image: redis:alpine + networks: + - bw-services + volumes: + - ./redis:/data + expose: + - 6379 + restart: unless-stopped + environment: + - TZ=Europe/Paris + deploy: + placement: + constraints: + - "node.role==worker" + +networks: + bw-services: + external: + name: bw-services \ No newline at end of file diff --git a/tests/SwarmTest.py b/tests/SwarmTest.py new file mode 100644 index 000000000..6bff9068d --- /dev/null +++ b/tests/SwarmTest.py @@ -0,0 +1,124 @@ +from Test import Test +from os.path import isdir, join, isfile +from os import chown, walk, getenv, listdir +from shutil import copytree, rmtree +from traceback import format_exc +from subprocess import run +from time import sleep +from logger import log + +class SwarmTest(Test) : + + def __init__(self, name, timeout, tests) : + super().__init__(name, "swarm", timeout, tests) + self._domains = { + r"www\.example\.com": getenv("TEST_DOMAIN1"), + r"auth\.example\.com": getenv("TEST_DOMAIN1"), + r"app1\.example\.com": getenv("TEST_DOMAIN1"), + r"app2\.example\.com": getenv("TEST_DOMAIN2"), + r"app3\.example\.com": getenv("TEST_DOMAIN3") + } + + def init() : + try : + if not Test.init() : + return False + proc = run("sudo chown -R root:root /tmp/bw-data", shell=True) + if proc.returncode != 0 : + raise(Exception("chown failed (swarm stack)")) + if isdir("/tmp/swarm") : + rmtree("/tmp/swarm") + copytree("./integrations/swarm", "/tmp/swarm") + compose = "/tmp/swarm/stack.yml" + Test.replace_in_file(compose, r"bunkerity/bunkerweb:.*$", "10.20.1.1:5000/bw-tests:latest") + Test.replace_in_file(compose, r"bunkerity/bunkerweb-autoconf:.*$", "10.20.1.1:5000/bw-autoconf-tests:latest") + Test.replace_in_file(compose, r"\./bw\-data:/", "/tmp/bw-data:/") + proc = run("docker stack deploy -c stack.yml bunkerweb", cwd="/tmp/swarm", shell=True) + if proc.returncode != 0 : + raise(Exception("docker stack deploy failed (swarm stack)")) + i = 0 + healthy = False + while i < 30 : + proc = run('docker stack ps --no-trunc --format "{{ .CurrentState }}" bunkerweb | grep -v "Running"', cwd="/tmp/swarm", shell=True, capture_output=True) + if proc.returncode != 0 : + raise(Exception("docker stack ps failed (swarm stack)")) + if "" == proc.stdout.decode() : + healthy = True + break + sleep(1) + i += 1 + if not healthy : + raise(Exception("swarm stack is not healthy")) + except : + log("SWARM", "❌", "exception while running SwarmTest.init()\n" + format_exc()) + return False + return True + + def end() : + ret = True + try : + if not Test.end() : + return False + proc = run("docker stack rm bunkerweb", shell=True) + if proc.returncode != 0 : + ret = False + proc = run("docker network rm services_net autoconf_net", shell=True) + if proc.returncode != 0 : + ret = False + rmtree("/tmp/swarm") + except : + log("SWARM", "❌", "exception while running SwarmTest.end()\n" + format_exc()) + return False + return ret + + def _setup_test(self) : + try : + super()._setup_test() + test = "/tmp/tests/" + self._name + compose = "/tmp/tests/" + self._name + "/swarm.yml" + example_data = "./examples/" + self._name + "/bw-data" + for ex_domain, test_domain in self._domains.items() : + Test.replace_in_files(test, ex_domain, test_domain) + Test.rename(test, ex_domain, test_domain) + setup = test + "/setup-swarm.sh" + if isfile(setup) : + proc = run("sudo ./setup-swarm.sh", cwd=test, shell=True) + if proc.returncode != 0 : + raise(Exception("setup-swarm failed")) + if isdir(example_data) : + for cp_dir in listdir(example_data) : + if isdir(join(example_data, cp_dir)) : + copytree(join(example_data, cp_dir), join("/tmp/bw-data", cp_dir)) + proc = run('docker stack deploy -c swarm.yml "' + self._name + '"', shell=True, cwd=test) + if proc.returncode != 0 : + raise(Exception("docker stack deploy failed")) + except : + log("SWARM", "❌", "exception while running SwarmTest._setup_test()\n" + format_exc()) + self._cleanup_test() + return False + return True + + def _cleanup_test(self) : + try : + proc = run('docker stack rm "' + self._name + '"', shell=True) + if proc.returncode != 0 : + raise(Exception("docker stack rm failed")) + proc = run('docker config ls --format "{{ .ID }}"', shell=True, capture_output=True) + if proc.returncode != 0 : + raise(Exception("docker config ls failed")) + for config in proc.stdout.decode().splitlines() : + proc = run('docker config rm "' + config + '"', shell=True) + if proc.returncode != 0 : + raise(Exception("docker config rm failed")) + super()._cleanup_test() + except : + log("SWARM", "❌", "exception while running SwarmTest._cleanup_test()\n" + format_exc()) + return False + return True + + def _debug_fail(self) : + run("docker service logs bunkerweb_mybunker", shell=True) + run("docker service logs bunkerweb_myautoconf", shell=True) + proc = run('docker stack services --format "{{ .Name }}" "' + self._name + '"', shell=True, capture_output=True) + for service in proc.stdout.decode().readlines() : + run('docker service logs "' + service + '"', shell=True) \ No newline at end of file diff --git a/tests/main.py b/tests/main.py index 63276f42d..eb5713981 100755 --- a/tests/main.py +++ b/tests/main.py @@ -12,6 +12,7 @@ path.append(getcwd() + "/tests") from Test import Test from DockerTest import DockerTest from AutoconfTest import AutoconfTest +from SwarmTest import SwarmTest from logger import log if len(argv) != 2 : @@ -32,6 +33,9 @@ if test_type == "docker" : elif test_type == "autoconf" : ret = AutoconfTest.init() end_fun = AutoconfTest.end +elif test_type == "swarm" : + ret = SwarmTest.init() + end_fun = SwarmTest.end if not ret : log("TESTS", "❌", "Test.init() failed") exit(1) @@ -49,6 +53,8 @@ for example in glob("./examples/*") : test_obj = DockerTest(tests["name"], tests["timeout"], tests["tests"]) elif test_type == "autoconf" : test_obj = AutoconfTest(tests["name"], tests["timeout"], tests["tests"]) + elif test_type == "swarm" : + test_obj = SwarmTest(tests["name"], tests["timeout"], tests["tests"]) if not test_obj.run_tests() : log("TESTS", "❌", "Tests failed for " + tests["name"]) end_fun()