init: A cli to run openDesk pipelines

This commit is contained in:
Philip Gaber 2025-07-10 13:41:02 +02:00
commit 3cc79d3bb5
No known key found for this signature in database
GPG Key ID: 8D49EBCA3F8B797C
4 changed files with 321 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.envrc
.direnv
__*

61
flake.lock generated Normal file
View File

@ -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
}

88
flake.nix Normal file
View File

@ -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
];
};
}
);
}

169
od-cli.py Executable file
View File

@ -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()