From 3cc79d3bb5a2ae4b9853ac3d53d1770d06623715 Mon Sep 17 00:00:00 2001 From: Philip Gaber Date: Thu, 10 Jul 2025 13:41:02 +0200 Subject: [PATCH] init: A cli to run openDesk pipelines --- .gitignore | 3 + flake.lock | 61 +++++++++++++++++++ flake.nix | 88 ++++++++++++++++++++++++++++ od-cli.py | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 321 insertions(+) create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix create mode 100755 od-cli.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f56e18f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.envrc +.direnv +__* diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..673489b --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1750134718, + "narHash": "sha256-v263g4GbxXv87hMXMCpjkIxd/viIF7p3JpJrwgKdNiI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9e83b64f727c88a7711a2c463a7b16eedb69a84c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c8bf84f --- /dev/null +++ b/flake.nix @@ -0,0 +1,88 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + with pkgs; + let + py-typer = python313.pkgs.buildPythonPackage rec { + pname = "typer"; + version = "0.16.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "fastapi"; + repo = "typer"; + tag = version; + hash = "sha256-WB9PIxagTHutfk3J+mNTVK8bC7TMDJquu3GLBQgaras="; + }; + + build-system = [ python313Packages.pdm-backend ]; + + dependencies = [ + python313Packages.click + python313Packages.typing-extensions + # Build includes the standard optional by default + # https://github.com/tiangolo/typer/blob/0.12.3/pyproject.toml#L71-L72 + ] ++ optional-dependencies.standard; + + optional-dependencies = { + standard = [ + python313Packages.rich + python313Packages.shellingham + ]; + }; + + nativeCheckInputs = + [ + python313Packages.coverage # execs coverage in tests + python313Packages.pytest-xdist + python313Packages.pytestCheckHook + writableTmpDirAsHomeHook + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + procps + ]; + + disabledTests = + [ + "test_scripts" + # Likely related to https://github.com/sarugaku/shellingham/issues/35 + # fails also on Linux + "test_show_completion" + "test_install_completion" + ] + ++ lib.optionals (stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isAarch64) [ + "test_install_completion" + ]; + + pythonImportsCheck = [ "typer" ]; + + meta = { + description = "Library for building CLI applications"; + homepage = "https://typer.tiangolo.com/"; + changelog = "https://github.com/tiangolo/typer/releases/tag/${version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ winpat ]; + }; + }; + + pythonEnv = python313.withPackages (p: [ + p.python-gitlab py-typer + ]); + in + { + devShells.default = mkShell rec { + packages = [ + pythonEnv + ]; + }; + } + ); +} diff --git a/od-cli.py b/od-cli.py new file mode 100755 index 0000000..fdf1216 --- /dev/null +++ b/od-cli.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# TODOs: +# - Log for past runs (id/link to pipeline) +# - Stop / Restart Pipeline +import gitlab +import typer +import os + +from enum import StrEnum +from typing_extensions import Annotated +from typing import List + +app = typer.Typer() +gl = gitlab.Gitlab( + url=os.environ.get("OD_GL_URL", ""), private_token=os.environ.get("OD_GL_TOKEN", "") +) +MASTER_PASSWORD = os.environ.get( + "OD_MASTER_PASSWORD", "sovereign-workplace" +) +USER = os.environ.get("OD_USER", "od-user") +GL_USER = os.environ.get("OD_GL_USER", "od-gl-user") +GL_PROJECT = os.environ.get("OD_GL_PROJECT", "1317") + +class Clusters(StrEnum): + qa = "qa" + run = "run" + +class Apps(StrEnum): + all = "all" + none = "none" + migrations = "migrations" + services = "services" + ums = "ums" + collabora = "collabora" + cryptpad = "cryptpad" + element = "element" + ox = "ox" + xwiki = "xwiki" + nextcloud = "nextcloud" + openproject = "openproject" + jitsi = "jitsi" + notes = "notes" + + +@app.command() +def pipelines(n=15, username=GL_USER): + # gl.enable_debug() + opendesk = gl.projects.get(1317) + pipelines = opendesk.pipelines.list(iterator=True, username=username) + + # Show last N pipelines + for i, p in enumerate(pipelines): + if i > int(n): + break + match p.status: + case "success": + status = "✔" + case "failed": + status = "❌" + case "running": + status = "🕑" + case _: + status = p.status + print( + f"[{p.created_at[:-5].replace('T', ' ')}]-({p.ref}) {status}: {p.web_url}" + ) + + +@app.command() +def pipeline(pid: str): + opendesk = gl.projects.get() + pipeline = opendesk.pipelines.get(pid) + variables = pipeline.variables.list(get_all=True) + print(variables) + + +def _new_pipeline(ref: str, variables: str): + variables = _parse_variables(variables) + opendesk = gl.projects.get(1317) + new_pipeline = opendesk.pipelines.create({"ref": ref, "variables": variables}) + print(new_pipeline) + + +@app.command() +def new_pipeline( + ref: str, + cluster: Annotated[Clusters, typer.Option(case_sensitive=False)], + namespace: str = f"{USER}-py-ce", + test: bool = False, + test_branch: str = "develop", + ee: bool = False, + env_stop: bool = True, + debug: bool = True, + default_accounts: bool = True, + deploy: Annotated[List[Apps], typer.Option(case_sensitive=False)] = [Apps.none], +): + if test: + debug = False + + variables = [ + f"CLUSTER:{cluster}", + f"NAMESPACE:{namespace}", + f"MASTER_PASSWORD_WEB_VAR:{MASTER_PASSWORD}", + f"ENV_STOP_BEFORE:{_tf_to_yn(env_stop)}", + f"RUN_TESTS:{_tf_to_yn(test)}", + f"TESTS_BRANCH:{test_branch}", + f"DEBUG_ENABLED:{_tf_to_yn(debug)}", + f"CREATE_DEFAULT_ACCOUNTS:{_tf_to_yn(default_accounts)}", + f"OPENDESK_ENTERPRISE:{'true' if ee else 'false'}", + ] + + if Apps.none in deploy: + pass + elif Apps.all in deploy and len(deploy) == 1: + variables.append("DEPLOY_ALL_COMPONENTS:yes") + elif Apps.all in deploy and len(deploy) > 1: + print("You cannot deploy 'all' but also specify specific apps at the same time") + exit(1) + else: + variables += [ + f"DEPLOY_{app.value.upper()}:'yes'" + for app in deploy + ] + + print(variables) + + _new_pipeline(ref, ",".join(variables)) + + +def _parse_variables(var_str: str) -> list[dict[str, str]]: + parts = var_str.split(",") + return [{"key": k, "value": v} for k, v in (p.strip().split(":") for p in parts)] + + +def _yn_to_tf(yn: str | bool) -> bool: + if type(yn) is bool: + return yn + else: + return True if yn == "yes" else False + + +def _tf_to_yn(tf: bool) -> str: + if type(tf) is not bool: + return tf + else: + return "yes" if tf else "no" + + +@app.command() +def self_test(): + _test_yn() + _test_tf() + + +def _test_yn(): + should = [False, False, False, False, False, True, True] + for yn, tf in zip([False, "no", "nope", "y", "n", "yes", True], should): + assert _yn_to_tf(yn) == tf, f"{yn} != {tf} but is {_yn_to_tf(yn)}" + + +def _test_tf(): + should = ["no", "yes"] + for tf, yn in zip([False, True], should): + assert _tf_to_yn(tf) == yn, f"{tf} != {yn} but is {_tf_to_yn(tf)}" + + +if __name__ == "__main__": + gl.auth() + app()