{ inputs, lib, hostPkgs, config, ... }: let inherit (lib) getExe ; ## Some places need a dummy file that will in fact never be used. We create ## it here. dummyFile = hostPkgs.writeText "dummy" "dummy"; panelPort = 8000; panelUser = "test"; panelEmail = "test@test.com"; panelPassword = "ouiprdaaa43"; # panel's manager complains if too close to username or email fediUser = "test"; fediEmail = "test@test.com"; fediPassword = "testtest"; fediName = "Testy McTestface"; toPythonBool = b: if b then "True" else "False"; interactWithPanel = { baseUri, enableMastodon, enablePeertube, enablePixelfed, }: hostPkgs.writers.writePython3Bin "interact-with-panel" { libraries = with hostPkgs.python3Packages; [ selenium ]; flakeIgnore = [ "E302" # expected 2 blank lines, found 0 "E303" # too many blank lines "E305" # expected 2 blank lines after end of function or class "E501" # line too long (> 79 characters) "E731" # do not assign lambda expression, use a def ]; } '' from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.firefox.options import Options from selenium.webdriver.support.ui import WebDriverWait print("Create and configure driver...") options = Options() options.add_argument("--headless") options.binary_location = "${getExe hostPkgs.firefox-unwrapped}" service = webdriver.FirefoxService(executable_path="${getExe hostPkgs.geckodriver}") driver = webdriver.Firefox(options=options, service=service) driver.set_window_size(1280, 960) driver.implicitly_wait(360) driver.command_executor.set_timeout(3600) print("Open login page...") driver.get("${baseUri}/login/") print("Enter username...") driver.find_element(By.XPATH, "//input[@name = 'username']").send_keys("${panelUser}") print("Enter password...") driver.find_element(By.XPATH, "//input[@name = 'password']").send_keys("${panelPassword}") print("Click “Login” button...") driver.find_element(By.XPATH, "//button[normalize-space() = 'Login']").click() print("Open configuration page...") driver.get("${baseUri}/configuration/") # Helpers to actually set and not add or switch input values. def input_set(elt, keys): elt.clear() elt.send_keys(keys) def checkbox_set(elt, new_value): if new_value != elt.is_selected(): elt.click() print("Enable Fediversity...") checkbox_set(driver.find_element(By.XPATH, "//input[@name = 'enable']"), True) print("Fill in initialUser info...") input_set(driver.find_element(By.XPATH, "//input[@name = 'initialUser.username']"), "${fediUser}") input_set(driver.find_element(By.XPATH, "//input[@name = 'initialUser.password']"), "${fediPassword}") input_set(driver.find_element(By.XPATH, "//input[@name = 'initialUser.email']"), "${fediEmail}") input_set(driver.find_element(By.XPATH, "//input[@name = 'initialUser.displayName']"), "${fediName}") print("Enable services...") checkbox_set(driver.find_element(By.XPATH, "//input[@name = 'mastodon.enable']"), ${toPythonBool enableMastodon}) checkbox_set(driver.find_element(By.XPATH, "//input[@name = 'peertube.enable']"), ${toPythonBool enablePeertube}) checkbox_set(driver.find_element(By.XPATH, "//input[@name = 'pixelfed.enable']"), ${toPythonBool enablePixelfed}) print("Start deployment...") driver.find_element(By.XPATH, "//button[@id = 'deploy-button']").click() print("Wait for deployment status to show up...") get_deployment_result = lambda d: d.find_element(By.XPATH, "//div[@id = 'deployment-result']//p") WebDriverWait(driver, timeout=3660, poll_frequency=10).until(get_deployment_result) deployment_result = get_deployment_result(driver).get_attribute('innerHTML') print("Quit...") driver.quit() match deployment_result: case 'Deployment Succeeded': print("Deployment has succeeded; exiting normally") exit(0) case 'Deployment Failed': print("Deployment has failed; exiting with return code `1`") exit(1) case _: print(f"Unexpected deployment result: {deployment_result}; exiting with return code `2`") exit(2) ''; in { name = "deployment-panel"; ## The panel's module sets `nixpkgs.overlays` which clashes with ## `pkgsReadOnly`. We disable it here. node.pkgsReadOnly = false; nodes.deployer = { pkgs, ... }: { imports = [ (import ../../../panel { }).module ]; ## FIXME: This should be in the common stuff. security.acme = { acceptTerms = true; defaults.email = "test@test.com"; defaults.server = "https://acme.test/dir"; }; security.pki.certificateFiles = [ (import "${inputs.nixpkgs}/nixos/tests/common/acme/server/snakeoil-certs.nix").ca.cert ]; networking.extraHosts = "${config.acmeNodeIP} acme.test"; services.panel = { enable = true; production = true; domain = "deployer"; secrets = { SECRET_KEY = dummyFile; }; port = panelPort; nixops4Package = inputs.nixops4.packages.${pkgs.system}.default; deployment = { flake = "/run/fedipanel/flake"; name = "check-deployment-panel"; }; }; environment.systemPackages = [ pkgs.expect ]; ## FIXME: The following dependencies are necessary but I do not ## understand why they are not covered by the fake node. system.extraDependencies = with pkgs; [ peertube peertube.inputDerivation gixy # a configuration checker for nginx gixy.inputDerivation ]; system.extraDependenciesFromModule = { imports = [ ../../../services/fediversity ]; fediversity = { domain = "fediversity.net"; # would write `dummy` but that would not type garage.enable = true; mastodon = { enable = true; s3AccessKeyFile = dummyFile; s3SecretKeyFile = dummyFile; }; peertube = { enable = true; secretsFile = dummyFile; s3AccessKeyFile = dummyFile; s3SecretKeyFile = dummyFile; }; pixelfed = { enable = true; s3AccessKeyFile = dummyFile; s3SecretKeyFile = dummyFile; }; temp.cores = 1; temp.initialUser = { username = "dummy"; displayName = "dummy"; email = "dummy"; passwordFile = dummyFile; }; }; }; }; nodes.client = { pkgs, ... }: { environment.systemPackages = with pkgs; [ httpie dnsutils # for `dig` openssl cacert wget python3 python3Packages.selenium firefox-unwrapped geckodriver ]; security.pki.certificateFiles = [ config.nodes.acme.test-support.acme.caCert ]; networking.extraHosts = "${config.acmeNodeIP} acme.test"; }; ## NOTE: The target machines may need more RAM than the default to handle ## being deployed to, otherwise we get something like: ## ## pixelfed # [ 616.785499 ] sshd-session[1167]: Conection closed by 2001:db8:1::2 port 45004 ## deployer # error: writing to file: No space left on device ## pixelfed # [ 616.788538 ] sshd-session[1151]: pam_unix(sshd:session): session closed for user port ## pixelfed # [ 616.793929 ] systemd-logind[719]: Session 4 logged out. Waiting for processes to exit. ## deployer # Error: Could not create resource ## ## These values have been trimmed down to the gigabyte. nodes.mastodon.virtualisation.memorySize = 4 * 1024; nodes.pixelfed.virtualisation.memorySize = 4 * 1024; nodes.peertube.virtualisation.memorySize = 5 * 1024; ## FIXME: The test of presence of the services are very simple: we only ## check that there is a systemd service of the expected name on the ## machine. This proves at least that NixOps4 did something, and we cannot ## really do more for now because the services aren't actually working ## properly, in particular because of DNS issues. We should fix the services ## and check that they are working properly. extraTestScript = '' ## TODO: We want a nicer way to control where the FediPanel consumes its ## flake, which can default to the store but could also be somewhere else if ## someone wanted to change the code of the flake. ## with subtest("Give the panel access to the flake"): deployer.succeed("mkdir /run/fedipanel /run/fedipanel/flake >&2") deployer.succeed("cp -R . /run/fedipanel/flake >&2") deployer.succeed("chown -R panel:panel /run/fedipanel >&2") ## TODO: I want a programmatic way to provide an SSH key to the panel (and ## therefore NixOps4). This should happen either in the Python code, but ## maybe it is fair that that one picks up on the user's key? It could ## also be in the Nix packaging. ## with subtest("Set up the panel's SSH keys"): deployer.succeed("mkdir /home/panel/.ssh >&2") deployer.succeed("cp -R /root/.ssh/* /home/panel/.ssh >&2") deployer.succeed("chown -R panel:panel /home/panel/.ssh >&2") deployer.succeed("chmod 600 /home/panel/.ssh/* >&2") ## TODO: This is a hack to accept the root CA used by Pebble on the client ## machine. Pebble randomizes everything, so the only way to get it is to ## call the /roots/0 endpoint at runtime, leaving not much margin for a nice ## Nixy way of adding the certificate. There is no way around it as this is ## by design in Pebble, showing in fact that Pebble was not the appropriate ## tool for our use and that nixpkgs does not in fact provide an easy way to ## generate _usable_ certificates in NixOS tests. I suggest we merge this, ## and track the task to set it up in a cleaner way. I would tackle this in ## a subsequent PR, and hopefully even contribute this BetterWay(tm) to ## nixpkgs. — Niols ## with subtest("Set up ACME root CA on client"): client.succeed(""" cd /etc/ssl/certs curl -o pebble-root-ca.pem https://acme.test:15000/roots/0 curl -o pebble-intermediate-ca.pem https://acme.test:15000/intermediates/0 { cat ca-bundle.crt cat pebble-root-ca.pem cat pebble-intermediate-ca.pem } > new-ca-bundle.crt rm ca-bundle.crt ca-certificates.crt mv new-ca-bundle.crt ca-bundle.crt ln -s ca-bundle.crt ca-certificates.crt """) ## TODO: I would hope for a more declarative way to add users. This should ## be handled by the Nix packaging of the FediPanel. — Niols ## with subtest("Create panel user"): deployer.succeed(""" expect -c ' spawn manage createsuperuser --username ${panelUser} --email ${panelEmail} expect "Password: "; send "${panelPassword}\\n"; expect "Password (again): "; send "${panelPassword}\\n" interact ' >&2 """) with subtest("Check the status of the services - there should be none"): garage.fail("systemctl status garage.service") mastodon.fail("systemctl status mastodon-web.service") peertube.fail("systemctl status peertube.service") pixelfed.fail("systemctl status phpfpm-pixelfed.service") with subtest("Run deployment with no services enabled"): client.succeed("${ interactWithPanel { baseUri = "https://deployer"; enableMastodon = false; enablePeertube = false; enablePixelfed = false; } }/bin/interact-with-panel >&2") with subtest("Check the status of the services - there should still be none"): garage.fail("systemctl status garage.service") mastodon.fail("systemctl status mastodon-web.service") peertube.fail("systemctl status peertube.service") pixelfed.fail("systemctl status phpfpm-pixelfed.service") with subtest("Run deployment with Mastodon and Pixelfed enabled"): client.succeed("${ interactWithPanel { baseUri = "https://deployer"; enableMastodon = true; enablePeertube = false; enablePixelfed = true; } }/bin/interact-with-panel >&2") with subtest("Check the status of the services - expecting Garage, Mastodon and Pixelfed"): garage.succeed("systemctl status garage.service") mastodon.succeed("systemctl status mastodon-web.service") peertube.fail("systemctl status peertube.service") pixelfed.succeed("systemctl status phpfpm-pixelfed.service") with subtest("Run deployment with only Peertube enabled"): client.succeed("${ interactWithPanel { baseUri = "https://deployer"; enableMastodon = false; enablePeertube = true; enablePixelfed = false; } }/bin/interact-with-panel >&2") with subtest("Check the status of the services - expecting Garage and Peertube"): garage.succeed("systemctl status garage.service") mastodon.fail("systemctl status mastodon-web.service") peertube.succeed("systemctl status peertube.service") pixelfed.fail("systemctl status phpfpm-pixelfed.service") ''; }