diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 4ec5836..d1ff749 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -21,3 +21,9 @@ jobs: - uses: actions/checkout@v4 - run: cd website && nix-build -A tests - run: cd website && nix-build -A build + + check-peertube: + runs-on: native + steps: + - uses: actions/checkout@v4 + - run: nix build .#checks.x86_64-linux.peertube -L diff --git a/flake.lock b/flake.lock index 0bbdc67..e65b00c 100644 --- a/flake.lock +++ b/flake.lock @@ -836,11 +836,11 @@ }, "nixpkgs_6": { "locked": { - "lastModified": 1732350895, - "narHash": "sha256-GcOQbOgmwlsRhpLGSwZJwLbo3pu9ochMETuRSS1xpz4=", + "lastModified": 1734323986, + "narHash": "sha256-m/lh6hYMIWDYHCAsn81CDAiXoT3gmxXI9J987W5tZrE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0c582677378f2d9ffcb01490af2f2c678dcb29d3", + "rev": "394571358ce82dff7411395829aa6a3aad45b907", "type": "github" }, "original": { diff --git a/services/fediversity/peertube.nix b/services/fediversity/peertube.nix index 1d1ea08..bb2b618 100644 --- a/services/fediversity/peertube.nix +++ b/services/fediversity/peertube.nix @@ -11,6 +11,10 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) { networking.firewall.allowedTCPPorts = [ 80 443 + + ## For Live streaming and Live streaming when RTMPS is enabled. + 1935 + 1936 ]; services.garage = { @@ -70,6 +74,8 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) { enabled = true; endpoint = config.fediversity.internal.garage.api.url; region = "garage"; + upload_acl.public = null; # Garage does not support ACL + upload_acl.private = null; # Garage does not support ACL # not supported by garage # SEE: https://garagehq.deuxfleurs.fr/documentation/connect/apps/#peertube @@ -101,7 +107,11 @@ lib.mkIf (config.fediversity.enable && config.fediversity.peertube.enable) { ## Proxying through Nginx - services.peertube.configureNginx = true; + services.peertube = { + configureNginx = true; + listenWeb = 443; + enableWebHttps = true; + }; services.nginx.virtualHosts.${config.services.peertube.localDomain} = { forceSSL = true; enableACME = true; diff --git a/services/flake-part.nix b/services/flake-part.nix index 5563878..485e261 100644 --- a/services/flake-part.nix +++ b/services/flake-part.nix @@ -9,6 +9,7 @@ checks = { mastodon = import ./tests/mastodon.nix { inherit self pkgs; }; pixelfed-garage = import ./tests/pixelfed-garage.nix { inherit self pkgs; }; + peertube = import ./tests/peertube.nix { inherit self pkgs; }; }; }; } diff --git a/services/tests/green.mp4 b/services/tests/green.mp4 new file mode 100644 index 0000000..82ce4c1 Binary files /dev/null and b/services/tests/green.mp4 differ diff --git a/services/tests/mastodon.nix b/services/tests/mastodon.nix index 7bd36e1..ef6a667 100644 --- a/services/tests/mastodon.nix +++ b/services/tests/mastodon.nix @@ -43,7 +43,7 @@ let in pkgs.nixosTest { - name = "test-mastodon-garage"; + name = "mastodon"; nodes = { server = diff --git a/services/tests/peertube.nix b/services/tests/peertube.nix new file mode 100644 index 0000000..6a5161b --- /dev/null +++ b/services/tests/peertube.nix @@ -0,0 +1,233 @@ +## This file is a basic test of Peertube functionalities. + +{ pkgs, self }: + +let + lib = pkgs.lib; + + testVideo = pkgs.copyPathToStore ./green.mp4; + testVideoColour = "#00FF00"; + + postVideoInBrowser = + pkgs.writers.writePython3Bin "post-video-in-browser" + { + libraries = with pkgs.python3Packages; [ selenium ]; + flakeIgnore = [ "E501" ]; # welcome to the 21st century + } + '' + import sys + from urllib.parse import urlparse + + 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 + from selenium.common.exceptions import NoSuchElementException + + options = Options() + print("########################################", file=sys.stderr) + print("A", file=sys.stderr) + options.add_argument("--headless") + print("########################################", file=sys.stderr) + print("B", file=sys.stderr) + service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") + print("########################################", file=sys.stderr) + print("C", file=sys.stderr) + driver = webdriver.Firefox(options=options, service=service) + print("########################################", file=sys.stderr) + print("D", file=sys.stderr) + driver.set_window_size(4096, 2160) + print("########################################", file=sys.stderr) + print("E", file=sys.stderr) + driver.implicitly_wait(360) + print("########################################", file=sys.stderr) + print("F", file=sys.stderr) + wait = WebDriverWait(driver, timeout=360, poll_frequency=10) + print("########################################", file=sys.stderr) + + ############################################################ + # Login + + + def load(driver, page): + print(f"Loading page {page}...", file=sys.stderr) + driver.get(page) + + print("Waiting until page is loaded...", file=sys.stderr) + wait.until(lambda d: d.execute_script("return document.readyState") == "complete") + + if urlparse(driver.current_url).path == "/login": + print("Hit a login page.", file=sys.stderr) + + print("Enter username...", file=sys.stderr) + driver.find_element(By.ID, "username").send_keys("root") + + print("Enter password...", file=sys.stderr) + driver.find_element(By.ID, "password").send_keys(sys.argv[1]) + + print("Click “Login” button...", file=sys.stderr) + driver.find_element(By.XPATH, "//input[@value='Login']").click() + + print("Waiting until we are logged-in...", file=sys.stderr) + wait.until(lambda d: urlparse(d.current_url).path == urlparse(page).path) + + print("Waiting until page is loaded...", file=sys.stderr) + wait.until(lambda d: d.execute_script("return document.readyState") == "complete") + + print("Clicking the annoying setup wizard away...", file=sys.stderr) + try: + driver.find_element(By.XPATH, "//input[@value='Remind me later']").click() + except NoSuchElementException: + # Somehow, sometimes, the wizard just does not show up; then we + # ignore the error and carry on like nothing happened. + print("Setup wizard did not show up.", file=sys.stderr) + + print(f"Done loading page {page}.", file=sys.stderr) + + + ############################################################ + # Upload video and take a screenshot + + print("Go to the upload page...", file=sys.stderr) + load(driver, "http://peertube.localhost/videos/upload") + + print("Submit video file...", file=sys.stderr) + driver.find_element(By.XPATH, "//input[@type='file']").send_keys("${testVideo}") + + print("Wait for file to upload, then publish it...", file=sys.stderr) + publish_button = driver.find_element(By.XPATH, "//button[.//span[normalize-space()='Publish']]") + wait.until(lambda _d: "disabled" not in publish_button.get_attribute("class")) + publish_button.click() + + print("Waiting until we are redirected...", file=sys.stderr) + wait.until(lambda d: urlparse(d.current_url).path != "/videos/upload") + wait.until(lambda d: d.execute_script("return document.readyState") == "complete") + + # FIXME: The video cannot play and we get “Failed to play video”. I + # believe it is a codec problem, and it possibly has to do with video + # codecs enabled in Firefox in headless mode -- after all, who would + # want to play a video? The following is a list of things that I have + # tried without success. Maybe one day we can manage? + # + # video = driver.find_element(By.XPATH, "//video") + # wait.until(lambda _d: not video.get_attribute("src").startswith("blob:")) + # wait.until(lambda d: d.execute_script("return arguments[0].readyState", video) == 4) + # driver.find_element(By.XPATH, "//*[contains(text(), 'Failed to play video')]") + # + # def detect_image_in_screen(d): + # print("Taking a screenshot...", file=sys.stderr) + # d.save_screenshot("/screenshot.png") + # print("Checking it...", file=sys.stderr) + # displayed_colours = subprocess.run( + # [ + # "magick", + # "/screenshot.png", + # "-define", + # "histogram:unique-colors=true", + # "-format", + # "%c", + # "histogram:info:", + # ], + # capture_output=True, + # text=True, + # check=True, + # ).stdout + # return bool(re.match(".*#${testVideoColour}.*", displayed_colours, re.S)) + # + # print("Wait until the image shows in screen...", file=sys.stderr) + # wait.until(detect_image_in_screen) + + ############################################################ + + print("Done; bye!", file=sys.stderr) + driver.close() + ''; + + acquireRootPassword = pkgs.writeShellScriptBin "acquire-root-password" '' + readonly retry=30 + readonly wait=60 + + for _ in $(seq $retry); do + password=$(journalctl -u peertube | perl -ne '/password: (.*)/ && print $1') + if [ -n "$password" ]; then + echo "$password" + exit 0 + fi + sleep $wait + done + + echo "Could not acquire root password in $((retry * wait))s." + exit 1 + ''; +in + +pkgs.nixosTest { + name = "peertube"; + + nodes = { + server = + { config, ... }: + { + imports = with self.nixosModules; [ + fediversity + ../vm/garage-vm.nix + ../vm/peertube-vm.nix + ../vm/interactive-vm.nix + ]; + + virtualisation = { + memorySize = lib.mkVMOverride 8192; + cores = 8; + }; + + environment.systemPackages = with pkgs; [ + python3 + firefox-unwrapped + geckodriver + toot + acquireRootPassword + postVideoInBrowser + imagemagick + ffmpeg # to identify videos + expect + ]; + + ## FIXME: The CI is very slow, so the default timeout of 120s is not + ## good enough. We bump it drastically. + systemd.services.postgresql.serviceConfig.TimeoutSec = lib.mkForce 3600; + + environment.variables = { + AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.peertube.id; + AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.peertube.secret; + PT_INITIAL_ROOT_PASSWORD = "testtest"; + }; + }; + }; + + testScript = + { nodes, ... }: + '' + server.start() + + # FIXME: I think this trick to look for a password can be replaced by + # services.peertube.serviceEnvironmentFile.PT_INITIAL_ROOT_PASSWORD=testtest + + with subtest("Peertube starts"): + server.wait_for_unit("peertube.service") + root_password = server.succeed("acquire-root-password").rstrip() + + with subtest("Post a video in the browser"): + server.succeed(f"post-video-in-browser {root_password}") + + with subtest("Find video in garage"): + server.succeed("mc alias set garage ${nodes.server.fediversity.internal.garage.api.url} --api s3v4 --path off $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY") + video = server.succeed("mc find garage --regex '\\.mp4'").rstrip() + if video == "": + raise Exception("Could not find any .mp4 video stored in Garage") + server.succeed(f"mc cat {video} > /video.mp4") + garage_hash = server.succeed("identify -quiet -format '%#' /video.mp4") + hash = server.succeed("identify -quiet -format '%#' ${testVideo}") + if garage_hash != hash: + raise Exception("The video stored in Garage does not correspond to the original one.") + ''; +} diff --git a/services/vm/peertube-vm.nix b/services/vm/peertube-vm.nix index 9ba7c00..0e2c992 100644 --- a/services/vm/peertube-vm.nix +++ b/services/vm/peertube-vm.nix @@ -1,11 +1,23 @@ -{ modulesPath, ... }: +{ + modulesPath, + pkgs, + ... +}: { - imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ]; + fediversity = { + enable = true; + domain = "localhost"; + peertube.enable = true; + + temp.peertubeSecretsFile = pkgs.writeText "secret" '' + 574e093907d1157ac0f8e760a6deb1035402003af5763135bae9cbd6abe32b24 + ''; + }; + services.peertube = { - enableWebHttps = false; settings = { listen.hostname = "0.0.0.0"; instance.name = "PeerTube Test VM";