diff --git a/.claude/agents/doc-reviewer.md b/.claude/agents/doc-reviewer.md new file mode 100644 index 0000000..5ab941d --- /dev/null +++ b/.claude/agents/doc-reviewer.md @@ -0,0 +1,63 @@ +--- +name: doc-reviewer +description: Documentation reviewer with persistent memory. Use when the user wants to review a doc, run a docs review cycle, or asks about documentation staleness. Reviews docs for accuracy, links, and structure. +tools: Read, Glob, Grep, Bash +model: sonnet +memory: project +--- + +You are a documentation reviewer for the BlumeOps homelab infrastructure project. + +## Workflow + +1. Run `mise run docs-review` to see the staleness table and identify the most stale doc +2. **Review exactly ONE document** — the single most stale doc from the table. Do not review multiple docs in one cycle. The main conversation will invoke you again if more reviews are needed. +3. Read the identified doc thoroughly +4. Perform the review checklist (below) +5. Check your agent memory for notes from past reviews of this doc or related docs +6. Present your findings as a structured report +7. Update your agent memory with anything you learned + +## Review Checklist + +For each doc, evaluate: + +- **Accuracy:** Is the information still correct? Cross-reference with actual source files (manifests, playbooks, configs) when possible +- **Wiki-links:** Do all `[[wiki-links]]` point to existing docs? Run `mise run docs-check-links` if unsure +- **Cross-references:** Should this doc link to other related docs that it doesn't currently reference? +- **Structure:** Is the doc in the right Diataxis category (reference/how-to/explanation/tutorial)? +- **Frontmatter:** Are tags, title, and dates correct? +- **Size:** Is the doc too large (should split) or too small (should merge)? +- **Staleness signals:** Are there version numbers, URLs, or process descriptions that may have drifted + +## Output Format + +Present findings as: +1. **One-line verdict:** healthy / needs minor updates / needs significant revision +2. **Issues found** (if any), grouped by severity +3. **Suggested changes** — be specific about what to change and where +4. **Proposed frontmatter update** — the `last-reviewed: YYYY-MM-DD` line to add + +## Memory Guidelines + +After each review, save notes about: +- Recurring issues you've seen across docs (e.g., "many docs still reference old routing pattern") +- Docs that reference each other and should be reviewed together +- Services or areas where documentation tends to drift fastest + +Before each review, check your memory for relevant context. + +## Important + +- Do NOT edit files directly. Present your findings so the main conversation can implement changes. +- Wiki-link format: `[[card-stem]]` — prefer simple links without alternate text unless grammatically needed. +- The docs directory is at `docs/` with Diataxis structure (reference/, how-to/, explanation/, tutorials/). + +## Handoff to Main Conversation + +Your output goes back to the main conversation, which will: +1. Present your findings to the user +2. Offer to implement the suggested changes +3. Run `mise run docs-preview` for visual verification before committing + +So make your suggested changes **specific and actionable** — include exact text replacements, frontmatter updates, and wiki-links to add/fix. The main conversation needs enough detail to implement without re-reading the entire doc. diff --git a/.gitattributes b/.dagger/.gitattributes similarity index 100% rename from .gitattributes rename to .dagger/.gitattributes diff --git a/.dagger/.gitignore b/.dagger/.gitignore new file mode 100644 index 0000000..2343dfc --- /dev/null +++ b/.dagger/.gitignore @@ -0,0 +1,4 @@ +/.venv +/**/__pycache__ +/sdk +/.env diff --git a/pyproject.toml b/.dagger/pyproject.toml similarity index 58% rename from pyproject.toml rename to .dagger/pyproject.toml index f612759..721a14a 100644 --- a/pyproject.toml +++ b/.dagger/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "blumeops" +name = "blumeops-ci" version = "0.1.0" requires-python = ">=3.13" dependencies = ["dagger-io"] @@ -10,10 +10,3 @@ build-backend = "uv_build" [tool.uv.sources] dagger-io = { path = "sdk", editable = true } - -[tool.ty.environment] -python-version = "3.13" -extra-paths = ["sdk/src"] - -[tool.ty.src] -exclude = ["pulumi/", "containers/transmission-exporter/"] diff --git a/.dagger/src/blumeops_ci/__init__.py b/.dagger/src/blumeops_ci/__init__.py new file mode 100644 index 0000000..a3601d0 --- /dev/null +++ b/.dagger/src/blumeops_ci/__init__.py @@ -0,0 +1,3 @@ +"""BlumeOps CI — Dagger build functions for container images.""" + +from .main import BlumeopsCi as BlumeopsCi diff --git a/src/blumeops/main.py b/.dagger/src/blumeops_ci/main.py similarity index 72% rename from src/blumeops/main.py rename to .dagger/src/blumeops_ci/main.py index 9bbd12f..f24b9e8 100644 --- a/src/blumeops/main.py +++ b/.dagger/src/blumeops_ci/main.py @@ -1,49 +1,17 @@ -from pathlib import Path - import dagger from dagger import dag, function, object_type -from .containers import discover - -NIX_IMAGE = "nixos/nix:2.34.4" - -# Module root is src/blumeops/, repo root is two levels up -_REPO_ROOT = Path(__file__).parent.parent.parent -_CONTAINERS_DIR = _REPO_ROOT / "containers" +NIX_IMAGE = "nixos/nix:2.33.3" @object_type -class Blumeops: +class BlumeopsCi: @function - async def build( - self, src: dagger.Directory, container_name: str - ) -> dagger.Container: - """Build a container by name. - - Uses the native Dagger pipeline from containers//container.py - if available, otherwise falls back to docker_build() for containers - still using Dockerfiles. - """ - registry = discover(_CONTAINERS_DIR) - if container_name in registry: - mod = registry[container_name] - return await mod.build(src) - # Legacy fallback for containers still using Dockerfiles + def build(self, src: dagger.Directory, container_name: str) -> dagger.Container: + """Build a container from containers//Dockerfile.""" context = src.directory(f"containers/{container_name}") return context.docker_build() - @function - async def container_version(self, container_name: str) -> str: - """Return the VERSION declared in a container's container.py. - - Used by CI and mise tasks to extract version without parsing - Dockerfiles. Returns empty string if no container.py exists. - """ - registry = discover(_CONTAINERS_DIR) - if container_name in registry: - return getattr(registry[container_name], "VERSION", "") - return "" - @function async def publish( self, @@ -59,7 +27,7 @@ class Blumeops: Tag format: {version}-{commit_sha} (e.g. v1.0.0-abc1234) """ - ctr = await self.build(src, container_name) + ctr = self.build(src, container_name) if registry_password is not None: ctr = ctr.with_registry_auth(registry, registry_username, registry_password) ref = f"{registry}/blumeops/{container_name}:{version}-{commit_sha}" @@ -80,10 +48,6 @@ class Blumeops: "git", "clone", "--depth=1", - # Pin to last v4 release. v5.0.0 restructured config - # layout (.quartz/plugins, ../quartz imports) and breaks - # our quartz.config.ts/quartz.layout.ts. See changelog. - "--branch=v4.5.2", "https://github.com/jackyzha0/quartz.git", "/tmp/quartz", ] @@ -292,52 +256,23 @@ class Blumeops: @function async def flake_update( - self, - src: dagger.Directory, - flake_path: str = "nixos/ringtail", - skip_inputs: str = "nixpkgs-services", + self, src: dagger.Directory, flake_path: str = "nixos/ringtail" ) -> dagger.File: - """Update rolling flake inputs to latest and return updated flake.lock. - - Dynamically discovers all flake inputs, filters out skip_inputs - (comma-separated), and passes the rest as positional args to - `nix flake update`. This avoids hardcoding input names. - - Args: - src: Source directory containing the flake. - flake_path: Path to the flake within src. - skip_inputs: Comma-separated input names to exclude from update. - """ - # nix has no --exclude flag; instead we enumerate inputs via - # `nix flake metadata --json` and pass the ones we want as - # positional args. - update_script = ( - "set -e; " - "SKIP='$SKIP_INPUTS'; " - "ALL=$(nix --extra-experimental-features 'nix-command flakes' " - "flake metadata --json 2>/dev/null " - "| nix-instantiate --eval -E " - '"builtins.concatStringsSep \\" \\" ' - "(builtins.attrNames " - "(builtins.fromJSON (builtins.readFile /dev/stdin))" - '.locks.nodes.root.inputs)" ' - "| tr -d '\"'); " - "INPUTS=''; " - "for i in $ALL; do " - ' case ",$SKIP," in *",$i,"*) continue ;; esac; ' - ' INPUTS="$INPUTS $i"; ' - "done; " - 'echo "Updating inputs:$INPUTS"; ' - 'echo "Skipping: $SKIP"; ' - "nix --extra-experimental-features 'nix-command flakes' " - "flake update $INPUTS --accept-flake-config" - ) + """Update all flake inputs to latest and return updated flake.lock.""" return await ( dag.container() .from_(NIX_IMAGE) .with_directory("/workspace", src) .with_workdir(f"/workspace/{flake_path}") - .with_env_variable("SKIP_INPUTS", skip_inputs) - .with_exec(["sh", "-c", update_script]) + .with_exec( + [ + "nix", + "--extra-experimental-features", + "nix-command flakes", + "flake", + "update", + "--accept-flake-config", + ] + ) .file(f"/workspace/{flake_path}/flake.lock") ) diff --git a/.dagger/uv.lock b/.dagger/uv.lock new file mode 100644 index 0000000..13acbbf --- /dev/null +++ b/.dagger/uv.lock @@ -0,0 +1,768 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "blumeops-ci" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "dagger-io" }, +] + +[package.metadata] +requires-dist = [{ name = "dagger-io", editable = "sdk" }] + +[[package]] +name = "cattrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "dagger-io" +version = "0.0.0" +source = { editable = "sdk" } +dependencies = [ + { name = "anyio" }, + { name = "beartype" }, + { name = "cattrs" }, + { name = "exceptiongroup" }, + { name = "gql", extra = ["httpx"] }, + { name = "httpcore" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-sdk" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=3.6.2" }, + { name = "beartype", specifier = ">=0.22.0" }, + { name = "cattrs", specifier = ">=25.1.0" }, + { name = "exceptiongroup", specifier = ">=1.3.0" }, + { name = "gql", extras = ["httpx"], specifier = ">=4.0" }, + { name = "httpcore", specifier = ">=1.0.8" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.23.0" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.54b1" }, + { name = "opentelemetry-sdk", specifier = ">=1.23.0" }, + { name = "platformdirs", specifier = ">=2.6.2" }, + { name = "rich", specifier = ">=10.11.0" }, + { name = "typing-extensions", specifier = ">=4.13.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "aiohttp", specifier = ">=3.9.3" }, + { name = "codegen", editable = "sdk/codegen" }, + { name = "mypy", specifier = ">=1.8.0" }, + { name = "pytest", specifier = ">=8.0.2" }, + { name = "pytest-httpx", specifier = ">=0.30.0" }, + { name = "pytest-mock", specifier = ">=3.12.0" }, + { name = "pytest-subprocess", specifier = ">=1.5.0" }, + { name = "ruff", specifier = ">=0.3.4" }, + { name = "sphinx", specifier = ">=7.2.6" }, + { name = "sphinx-rtd-theme", specifier = ">=2.0.0" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "gql" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "backoff" }, + { name = "graphql-core" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/9f/cf224a88ed71eb223b7aa0b9ff0aa10d7ecc9a4acdca2279eb046c26d5dc/gql-4.0.0.tar.gz", hash = "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", size = 215644, upload-time = "2025-08-17T14:32:35.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/94/30bbd09e8d45339fa77a48f5778d74d47e9242c11b3cd1093b3d994770a5/gql-4.0.0-py3-none-any.whl", hash = "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479", size = 89900, upload-time = "2025-08-17T14:32:34.029Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "httpx" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/9b/037a640a2983b09aed4a823f9cf1729e6d780b0671f854efa4727a7affbe/graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c", size = 513484, upload-time = "2025-11-01T22:30:40.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262, upload-time = "2025-11-01T22:30:38.912Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/a6/4515895b383113677fd2ad21813df5e56108a2df14ebb7916c962c9a0234/opentelemetry_instrumentation_logging-0.60b1.tar.gz", hash = "sha256:98f4b9c7aeb9314a30feee7c002c7ea9abea07c90df5f97fb058b850bc45b89a", size = 9968, upload-time = "2025-12-11T13:37:03.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/f9/8a4ce3901bc52277794e4b18c4ac43dc5929806eff01d22812364132f45f/opentelemetry_instrumentation_logging-0.60b1-py3-none-any.whl", hash = "sha256:f2e18cbc7e1dd3628c80e30d243897fdc93c5b7e0c8ae60abd2b9b6a99f82343", size = 12577, upload-time = "2025-12-11T13:36:08.123Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/.forgejo/workflows/build-blumeops.yaml b/.forgejo/workflows/build-blumeops.yaml index c6e6c3c..383542f 100644 --- a/.forgejo/workflows/build-blumeops.yaml +++ b/.forgejo/workflows/build-blumeops.yaml @@ -178,11 +178,10 @@ jobs: echo "## Documentation" echo "" - echo "Download \`$TARBALL\` directly, or bump \`docs_version\`" - echo "in \`ansible/roles/docs/defaults/main.yml\` and run:" + echo "Download \`$TARBALL\` and configure the quartz container with:" echo "" echo "\`\`\`" - echo "mise run provision-indri -- --tags docs" + echo "DOCS_RELEASE_URL=https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL" echo "\`\`\`" } > /tmp/release_body.txt @@ -224,16 +223,18 @@ jobs: echo "" echo "Release created successfully!" - - name: Bump docs_version in ansible role + - name: Update docs deployment run: | VERSION="${{ steps.version.outputs.version }}" - DEFAULTS_FILE="ansible/roles/docs/defaults/main.yml" + TARBALL="docs-${VERSION}.tar.gz" + DEPLOYMENT_FILE="argocd/manifests/docs/deployment.yaml" + RELEASE_URL="https://forge.eblu.me/eblume/blumeops/releases/download/${VERSION}/${TARBALL}" - echo "Bumping docs_version in $DEFAULTS_FILE to ${VERSION}..." - yq -i ".docs_version = \"${VERSION}\"" "$DEFAULTS_FILE" + echo "Updating $DEPLOYMENT_FILE with new release URL..." + yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"DOCS_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE" - echo "Updated defaults:" - grep -E "^docs_version:" "$DEFAULTS_FILE" + echo "Updated deployment:" + grep -A1 "DOCS_RELEASE_URL" "$DEPLOYMENT_FILE" - name: Commit release changes env: @@ -247,7 +248,7 @@ jobs: git config user.email "actions@forge.ops.eblu.me" # Stage deployment changes - git add ansible/roles/docs/defaults/main.yml + git add argocd/manifests/docs/deployment.yaml # Stage changelog changes if updated if [ "$CHANGELOG_UPDATED" = "true" ]; then @@ -269,6 +270,34 @@ jobs: echo "Changes committed and pushed" fi + - name: Deploy docs + env: + ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }} + run: | + echo "Syncing docs app via ArgoCD..." + + # Sync docs app (uses ARGOCD_AUTH_TOKEN env var for auth) + argocd app sync docs \ + --server argocd.ops.eblu.me \ + --grpc-web \ + --prune + + # Wait for sync to complete + argocd app wait docs \ + --server argocd.ops.eblu.me \ + --grpc-web \ + --timeout 120 + + echo "Docs app synced successfully!" + + - name: Purge Fly.io proxy cache + env: + FLY_API_TOKEN: ${{ secrets.FLY_DEPLOY_TOKEN }} + run: | + echo "Purging nginx cache on Fly.io proxy..." + fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'" + echo "Cache purged" + - name: Summary run: | VERSION="${{ steps.version.outputs.version }}" @@ -280,12 +309,5 @@ jobs: echo "Release URL:" echo " https://forge.eblu.me/eblume/blumeops/releases/tag/$VERSION" echo "" - echo "Asset URL:" + echo "Asset URL (for DOCS_RELEASE_URL ConfigMap):" echo " https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL" - echo "" - echo "To deploy on indri, run from gilbert:" - echo " mise run provision-indri -- --tags docs" - echo "" - echo "Then purge the Fly.io proxy cache:" - echo " fly ssh console -a blumeops-proxy -C \\" - echo " \"sh -c 'rm -rf /tmp/cache && nginx -s reload'\"" diff --git a/.forgejo/workflows/build-container.yaml b/.forgejo/workflows/build-container.yaml index 1fcfd7f..6e5ed38 100644 --- a/.forgejo/workflows/build-container.yaml +++ b/.forgejo/workflows/build-container.yaml @@ -1,13 +1,14 @@ # Unified container build workflow -# Manual dispatch only — use `mise run container-build-and-release `. -# Shared Dagger helpers (src/blumeops/) make path-based auto-triggers unreliable, -# so all container builds are triggered explicitly. -# Routes to the correct runner: -# - Dockerfile/Dagger containers build on k8s (indri) via Dagger +# Triggers on pushes to main that modify containers/*, or via manual dispatch. +# Detects which containers changed and routes to the correct runner: +# - Dockerfile containers build on k8s (indri) via Dagger # - Nix containers build on nix-container-builder (ringtail) via nix-build + skopeo name: Build Container on: + push: + branches: [main] + paths: ['containers/**'] workflow_dispatch: inputs: container: @@ -23,7 +24,7 @@ jobs: detect: runs-on: k8s outputs: - dagger: ${{ steps.classify.outputs.dagger }} + dockerfile: ${{ steps.classify.outputs.dockerfile }} nix: ${{ steps.classify.outputs.nix }} steps: - name: Checkout @@ -32,19 +33,26 @@ jobs: ref: ${{ inputs.ref || github.sha }} fetch-depth: 2 - - name: Classify container build type + - name: Detect and classify changed containers id: classify run: | - CHANGED='["${{ inputs.container }}"]' - echo "Building container: $CHANGED" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + CHANGED='["${{ inputs.container }}"]' + else + CHANGED=$(git diff --name-only HEAD~1 HEAD -- containers/ \ + | cut -d/ -f2 | sort -u \ + | jq -R -s -c 'split("\n") | map(select(length > 0))') + fi + + echo "Changed containers: $CHANGED" # Classify each container by build type (a container can appear in both) - DAGGER='[]' + DOCKERFILE='[]' NIX='[]' for name in $(echo "$CHANGED" | jq -r '.[]'); do has_any=false - if [ -f "containers/$name/container.py" ] || [ -f "containers/$name/Dockerfile" ]; then - DAGGER=$(echo "$DAGGER" | jq -c --arg n "$name" '. + [$n]') + if [ -f "containers/$name/Dockerfile" ]; then + DOCKERFILE=$(echo "$DOCKERFILE" | jq -c --arg n "$name" '. + [$n]') has_any=true fi if [ -f "containers/$name/default.nix" ]; then @@ -52,27 +60,22 @@ jobs: has_any=true fi if [ "$has_any" = "false" ]; then - echo "Warning: $name has neither container.py, Dockerfile, nor default.nix — skipping" + echo "Warning: $name has neither Dockerfile nor default.nix — skipping" fi done - echo "dagger=$DAGGER" >> "$GITHUB_OUTPUT" + echo "dockerfile=$DOCKERFILE" >> "$GITHUB_OUTPUT" echo "nix=$NIX" >> "$GITHUB_OUTPUT" - echo "Dagger builds: $DAGGER" + echo "Dockerfile builds: $DOCKERFILE" echo "Nix builds: $NIX" - build-dagger: + build-dockerfile: needs: detect - if: needs.detect.outputs.dagger != '[]' + if: needs.detect.outputs.dockerfile != '[]' runs-on: k8s - env: - # Send Dagger OTLP telemetry to Tempo. Without a real backend the - # engine's internal proxy returns 500 on /v1/metrics, causing noisy - # retry warnings in every build. - OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo.tracing.svc.cluster.local:4318 strategy: matrix: - container: ${{ fromJson(needs.detect.outputs.dagger) }} + container: ${{ fromJson(needs.detect.outputs.dockerfile) }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -82,19 +85,12 @@ jobs: - name: Extract version and SHA id: meta run: | - CONTAINER="${{ matrix.container }}" - - # Try native Dagger pipeline (container.py) first, fall back to Dockerfile - if [ -f "containers/$CONTAINER/container.py" ]; then - VERSION=$(dagger call container-version --container-name="$CONTAINER") - elif [ -f "containers/$CONTAINER/Dockerfile" ]; then - VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \ - "containers/$CONTAINER/Dockerfile" \ - | sed 's/^ARG CONTAINER_APP_VERSION=//') - fi + VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \ + "containers/${{ matrix.container }}/Dockerfile" \ + | sed 's/^ARG CONTAINER_APP_VERSION=//') if [ -z "$VERSION" ]; then - echo "Error: Could not extract version for $CONTAINER" + echo "Error: No CONTAINER_APP_VERSION found in Dockerfile" exit 1 fi diff --git a/.forgejo/workflows/cv-deploy.yaml b/.forgejo/workflows/cv-deploy.yaml index 001aa36..f99352d 100644 --- a/.forgejo/workflows/cv-deploy.yaml +++ b/.forgejo/workflows/cv-deploy.yaml @@ -1,14 +1,12 @@ # CV Deploy Workflow # -# Bumps cv_version in ansible/roles/cv/defaults/main.yml and pushes the change. -# Deployment to indri is manual (runner has no SSH access to indri): -# mise run provision-indri -- --tags cv +# Updates the CV deployment to a specific package version, commits +# the change, and syncs via ArgoCD. # # Usage: # 1. Release a new CV package from the cv repo first # 2. Go to Actions > Deploy CV > Run workflow # 3. Enter the version to deploy, or leave as "latest" -# 4. Run the command above on gilbert to apply name: Deploy CV @@ -62,16 +60,18 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Bump cv_version in ansible role + - name: Update CV deployment run: | VERSION="${{ steps.version.outputs.version }}" - DEFAULTS_FILE="ansible/roles/cv/defaults/main.yml" + TARBALL="cv-${VERSION}.tar.gz" + DEPLOYMENT_FILE="argocd/manifests/cv/deployment.yaml" + RELEASE_URL="https://forge.eblu.me/api/packages/eblume/generic/cv/${VERSION}/${TARBALL}" - echo "Bumping cv_version in $DEFAULTS_FILE to ${VERSION}..." - yq -i ".cv_version = \"${VERSION}\"" "$DEFAULTS_FILE" + echo "Updating $DEPLOYMENT_FILE with CV_RELEASE_URL..." + yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"CV_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE" - echo "Updated defaults:" - grep -E "^cv_version:" "$DEFAULTS_FILE" + echo "Updated deployment:" + grep -A1 "CV_RELEASE_URL" "$DEPLOYMENT_FILE" - name: Commit release changes env: @@ -82,7 +82,7 @@ jobs: git config user.name "Forgejo Actions" git config user.email "actions@forge.ops.eblu.me" - git add ansible/roles/cv/defaults/main.yml + git add argocd/manifests/cv/deployment.yaml if git diff --cached --quiet; then echo "No changes to commit (already at $VERSION)" @@ -94,16 +94,38 @@ jobs: echo "Changes committed and pushed" fi + - name: Deploy CV + env: + ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }} + run: | + echo "Syncing CV app via ArgoCD..." + + argocd app sync cv \ + --server argocd.ops.eblu.me \ + --grpc-web \ + --prune + + argocd app wait cv \ + --server argocd.ops.eblu.me \ + --grpc-web \ + --timeout 120 + + echo "CV app synced successfully!" + + - name: Purge Fly.io proxy cache + env: + FLY_API_TOKEN: ${{ secrets.FLY_DEPLOY_TOKEN }} + run: | + echo "Purging nginx cache on Fly.io proxy..." + fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'" + echo "Cache purged" + - name: Summary run: | VERSION="${{ steps.version.outputs.version }}" echo "================================================" - echo "CV version bumped: $VERSION" + echo "CV Deployed: $VERSION" echo "================================================" echo "" - echo "To deploy on indri, run from gilbert:" - echo " mise run provision-indri -- --tags cv" - echo "" - echo "Then purge the Fly.io proxy cache:" - echo " fly ssh console -a blumeops-proxy -C \\" - echo " \"sh -c 'rm -rf /tmp/cache && nginx -s reload'\"" + echo "CV should now be live at:" + echo " https://cv.ops.eblu.me/" diff --git a/.gitignore b/.gitignore index 09e937c..b39d114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .claude/settings.local.json .claude/agent-memory/ -.claude/scheduled_tasks.lock # Python __pycache__/ @@ -8,10 +7,5 @@ __pycache__/ *.pyo .venv/ -# Dagger (auto-generated SDK) -/sdk/ - # OS .DS_Store -/**/__pycache__ -/.env diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c64af40..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,171 +0,0 @@ -# AGENTS.md - -Guidance for AI agents working in this repository. See also [[ai-assistance-guide]]. - -## Overview - -blumeops is Erich Blume's GitOps repository for personal infrastructure, orchestrated via tailnet `tail8d86e.ts.net`. - -**CRITICAL: Public repo at github.com/eblume/blumeops - never commit secrets!** - -**Shell:** The user's interactive shell may differ from the current harness shell. Prefer repo-safe, non-interactive commands when possible, and match the user's shell conventions when giving interactive examples. - -## Rules - -1. **Always run `mise run ai-docs` at session start** - This will refresh your context with important information you will be assumed to know and follow. - **Read the full output** — never truncate, pipe to `head`/`tail`, or skip sections. - For problems with a large surface area, ask the user if `mise run ai-sources` should also be run — it concatenates all non-doc source files (~270K tokens) for deep codebase context. -2. **Always use `--context=minikube-indri` with kubectl** (or `--context=k3s-ringtail` for ringtail services) - work contexts must never be touched - **NEVER run `minikube delete`** — it destroys all PVs, etcd, and cluster state. Use `minikube stop`/`minikube start` for restarts. If minikube is stuck, see [[restart-indri]]. Full rebuild from scratch requires the DR procedure in [[rebuild-minikube-cluster]]. -3. **Classify the change as C0/C1/C2 before starting** (see below) — this determines branching and PR requirements -4. **Feature branches + PRs for C1/C2** - checkout main, pull, create branch, open PR via `tea pr create`. C0 goes direct to main. -5. **Check PR comments with `mise run pr-comments `** before proceeding -6. **Add changelog fragments (all change levels)** - `docs/changelog.d/..md` - Types: `feature`, `bugfix`, `infra`, `doc`, `ai`, `misc` - Applies to C0, C1, and C2 whenever the change is user-visible or noteworthy. - - **C1/C2:** Use branch name: `..md` - - **C0:** Use orphan prefix: `+..md` (avoids `main.*` collisions) -7. **Test before applying** - dry runs (`--check --diff`), syntax checks, `ssh indri '...'` -8. **Wait for user review before deploying** (C1/C2) -9. **Never merge PRs or push to main without explicit request** (C0 commits to main are fine) -10. **Verify deployments** - `mise run services-check` - -## Change Classification - -Before starting work, classify the change: - -| Class | Name | When to use | Key trait | -|-------|------|-------------|-----------| -| **C0** | Quick Fix | Small, low-risk, fix-forward safe | Direct to main, no PR | -| **C1** | Human Review | Moderate complexity or risk | Feature branch + PR, docs-first | -| **C2** | Mikado Chain | Multi-phase, multi-session, high complexity | Mikado Branch Invariant | - -**C0** — commit directly to main. No branch or PR needed. Fix forward if problems arise. - -**C1** — feature branch with early PR. Search related docs first, write documentation changes before code, deploy from the unmerged branch (ArgoCD `--revision`, Ansible from checkout). Upgrade to C2 if complexity spirals. - -**C2** — branch `mikado/` governed by the Mikado Branch Invariant: all card commits first, then code progress, then card closures. Commits use `C2(): plan/impl/close/finalize` convention. Reset the branch when new prerequisites are discovered. Resume with `mise run docs-mikado --resume`. - -See [[agent-change-process]] for the full methodology. - -## Project Structure - -``` -./docs/ # documentation (Diataxis, Quartz) -./docs/changelog.d/ # towncrier fragments -./.dagger/ # dagger pipelines -./.forgejo/ # forgejo-runner actions and workflows -./mise-tasks/ # scripts via `mise run` -./ansible/playbooks/ # ansible (indri.yml primary) -./ansible/roles/ # indri service roles -./argocd/apps/ # ArgoCD Application definitions -./argocd/manifests/ # k8s manifests per service -./fly/ # fly.io proxy for public routing -./pulumi/ # Pulumi IaC (tailnet ACLs, dns, cloud) -~/.config/{nvim,fish} # user's shell config, managed by chezmoi -~/code/personal/ # user's projects -~/code/personal/zk # user's zettelkasten (Obsidian-sync). Reference-data source; migrating into heph docs (hephaestus). -~/code/3rd/ # mirrored external projects -~/code/work # FORBIDDEN -``` -Other code paths will be listed via ai-docs, this is just an overview. When you -encounter wiki-links (`[[like-this]]`) it is referring to docs/ cards. - -## Service Deployment - -### Kubernetes (ArgoCD) - -Most services run in minikube on indri via ArgoCD (app-of-apps, manual sync). GPU workloads (Frigate, ntfy) run on ringtail's k3s cluster, also managed by ArgoCD. - -**PR workflow:** -1. Create branch, modify `argocd/manifests//` -2. Push. Sync 'apps' app if service definition changed (set --revision to branch). -3. Test on branch: `argocd app set --revision && argocd app sync ` -4. After merge: `argocd app set --revision main && argocd app sync ` - -**Commands:** `argocd app list|get|diff|sync ` - -**Login:** `argocd login argocd.ops.eblu.me --sso` (opens browser for Authentik SSO). Admin fallback for break-glass: `argocd login argocd.ops.eblu.me --username admin --password "$(op read 'op://vg6xf6vvfmoh5hqjjhlhbeoaie/srogeebssulhtb6tnqd7ls6qey/password')"` - -### Indri (Ansible) - -Native services: Forgejo, Zot, Caddy, Borgmatic, Alloy - -```fish -mise run provision-indri # full -mise run provision-indri -- --tags # specific -mise run provision-indri -- --check --diff # dry run -``` - -### Routing - -| Domain | Mechanism | Reachable from | -|--------|-----------|----------------| -| `*.eblu.me` | Fly.io proxy (Tailscale tunnel) | public internet | -| `*.ops.eblu.me` | Caddy on indri | k8s pods, containers, tailnet | -| `*.tail8d86e.ts.net` | Tailscale MagicDNS | tailnet clients only | - -Check tailscale serve: `ssh indri 'tailscale serve status --json'` - -## Container Releases - -```fish -mise run container-list # show images/tags -mise run container-release # tag and build -``` -The goal is to eventually use only locally built containers in all cases, with -full supply chain control via forge.ops.eblu.me repositories, mirroring source -from upstream. - -**After triggering a build** (manual dispatch or push to main), verify the -workflow succeeded before proceeding: - -```fish -mise run runner-logs # find the run number -mise run runner-logs # see jobs in the run -mise run runner-logs -j # fetch logs on failure -``` - -This also works for other forge repos (`--repo eblume/hermes`). - -## Third-Party Projects - -Ask user to mirror on forge first, then clone to `~/code/3rd//`. - -### Sporked Projects - -Some mirrored projects are "sporked" — a floating-branch soft-fork strategy -where local patches are continuously rebased on top of upstream. See -[[spork-strategy]] and [[create-a-spork]] for the full methodology. - -Sporked projects live in `~/code/3rd//` with three remotes: -`origin` (eblume/ fork on forge), `mirror` (mirrors/ on forge), `upstream` -(canonical). The `blumeops` branch is the default; `deploy` merges everything. - -Create a new spork: `mise run spork-create ` - -## Task Discovery - -BlumeOps tasks live in [hephaestus](https://github.com/eblume/hephaestus) (`heph`), -the user's self-hosted context/task system. Fetch them with the CLI: - -```fish -heph list --project Blumeops --json # outstanding Blumeops tasks as JSON -``` - -(This replaced the retired `blumeops-tasks` mise task, which read from Todoist.) - -Most operational scripts are stored in `./mise-tasks/`. For scripts with any logic or -complexity, use uv run --script 's with explicit dependencies. Complex -workflows with artifacts should become dagger pipelines. Mise tasks are for -development processes and operations - tools for the user or the agent. - -## Credentials - -Root store is 1Password. Never grab directly - use existing patterns (ansible -pre_tasks, external-secrets, scripts with `op` CLI). It's ok to use `op item -get` without `--reveal` to explore what secrets are available, however. - -Prefer `op read "op://vault/item/field"` over `op item get --fields` to avoid -quoting issues with multi-line values. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0499154..ced38b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,422 +12,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [v1.17.0] - 2026-06-03 - -### Features - -- Deploy the Adelaide / Heidi / Addie baby shower app — guest splash, raffle - picker, and prize assignment console — on ringtail k3s with `shower.eblu.me` - as the public entry and `shower.ops.eblu.me` as the tailnet admin host. App - source: [`adelaide-baby-shower-app`](https://forge.eblu.me/eblume/adelaide-baby-shower-app). -- Deploy adelaide-baby-shower-app v1.1.0 to ringtail k3s. Replaces the - boolean lock with a four-phase `ShowerState` (`pre_event` → `party` → - `prizes_locked` → `event_locked`), adds an append-only "guest memories" - panel where guests can leave photos and comments for the baby, and - polishes the admin and QR views. Three Django migrations - (`0009_shower_phase`, `0010_guest_memories`, `0011_book_description`) - run automatically in the entrypoint against the SQLite PV. No config - or env-var changes. - - Container build also gains a Forgejo-PyPI workaround: Forgejo's simple - index returns absolute file URLs hardcoded to the public ROOT_URL - (`forge.eblu.me`), which the Fly edge 403s on `/api/packages/*`. The - wheel and sdist are now both pulled via direct `fetchurl` against - `forge.ops.eblu.me` (tailnet-only) and the wheel is handed to pip as - a local path. -- `review-compliance-reports` now also fetches and summarizes the weekly Prowler container-image and IaC scans (previously only the K8s CIS in-cluster scan was processed). For each scan it shows status counts, severity breakdown, week-over-week delta, and — for the high-volume image/IaC scans — top-N tables grouped by check ID and resource instead of per-finding listings. -- runner-logs now authenticates with Forgejo API token and auto-detects the repo from git remote. Job logs are fetched via SSH to indri (reading Forgejo's on-disk zstd log files) instead of the web endpoint, which doesn't support token auth for private repos. - -### Bug Fixes - -- Fix nightly borgmatic backups failing for 2 days. The shower SQLite - dump hook referenced `kubectl --context=k3s-ringtail`, but indri's - kubeconfig deliberately doesn't carry the ringtail credentials. The - `before_backup` hook's failure aborted the entire run, taking out - *both* the local sifaka repo and the BorgBase offsite. Replaced - the inline-shell dump with a `~/bin/borgmatic-k8s-sqlite-dump` - helper deployed by the ansible role. Each dump entry now declares a - `target` of either `local:` (mealie — kubectl uses indri's - kubeconfig) or `ssh:` (shower — ssh into ringtail and - run `k3s kubectl` there, no indri-side kubeconfig needed; k3s.yaml - on ringtail is mode 644 so no sudo required). Bytes stream back via - `kubectl exec ... -- cat` rather than `kubectl cp`, since `kubectl - cp` requires `tar` inside the pod and nix-built images like shower - don't bundle it. -- Shower app container now bakes the wheel + Python deps into the image - at build time via `buildPythonPackage` instead of pip-installing on - first boot. Boots are deterministic and don't depend on forge PyPI - being reachable from the pod. The `wheelHash` in - `containers/shower/default.nix` is the sha256 sourced from the - [forge PyPI simple index](https://forge.eblu.me/api/packages/eblume/pypi/simple/adelaide-baby-shower-app/); - bumping the version means bumping that hash too. - - Borgmatic now covers the shower app: SQLite is dumped from the live - pod via `kubectl exec` (mirroring the existing mealie entry, with - `context: k3s-ringtail`), and the prize-photo media share is picked up - through `/Volumes/shower` (sifaka SMB mount on indri, same pattern as - `/Volumes/photos`). -- Disabled adaptive sync (VRR) on ringtail's DP-1 output. The OMEN 27i IPS panel pumps brightness when its refresh rate swings into the low VRR range during low-framerate content (e.g. game cutscenes), producing a flicker that worsened over a session until a reboot. Pinning the panel to a fixed 165Hz eliminates it. -- Fixed forge.eblu.me static assets (CSS, JS, images, fonts) not loading — the proxy's static asset cache block was missing the `Host` header, so Caddy couldn't route the requests. -- Fixed homepage container EACCES on cold start: the nix-built image now chowns - `/app/config` to uid 1000 at build time via `fakeRootCommands`, matching the - behavior of the old Dockerfile. Without this, homepage couldn't seed missing - skeleton configs (proxmox.yaml etc.) or create `/app/config/logs`, crashing on - its first uncached request. Caught during the ringtail cutover. -- Fixed sway keybindings on ringtail — the home-manager `keybindings` block was replacing the module's defaults entirely, leaving only explicit overrides (no workspace switching, focus, move, splits, resize mode, etc). Switched to `lib.mkOptionDefault` with `lib.mkForce` on the conflicting custom binds (`Mod+Return`, `Mod+d`, `Mod+space`, `Mod+l`) so defaults merge back in. Also added `Mod+F1` to show a filterable fuzzel list of current keybindings. - - Fixed fuzzel config errors on launch — `border-radius` and `border-width` were under `[main]`, but fuzzel expects them as `radius`/`width` under a `[border]` section. -- Pin the Quartz docs build to v4.5.2. The Dagger `build_docs` pipeline cloned Quartz from the default branch unpinned; Quartz v5.0.0 restructured its config layout (`.quartz/plugins`, `../quartz` imports) and broke the docs build against our existing `quartz.config.ts`/`quartz.layout.ts`. - -### Infrastructure - -- Wire the ringtail `blumeops-pg` cluster (which holds the wave-1-migrated - paperless + teslamate databases) into backups and Grafana. Adds a Tailscale - LoadBalancer Service (`blumeops-pg-ringtail.tail8d86e.ts.net`) and a Caddy L4 - route (`pg.ops.eblu.me:5434`), then repoints borgmatic's `teslamate` + - `paperless` postgres dumps and the `mealie` SQLite dump at ringtail, and the - Grafana TeslaMate datasource at the ringtail DB. Closes the backup gap that - opened at cutover (the migrated live data was still being backed up from the - now-frozen minikube copies) and unblocks the wave-1 decommission. -- Migrated homepage dashboard from minikube (indri/arm64) to k3s (ringtail/amd64). - The container is now built via nix (`containers/homepage/default.nix`), adapted - from nixpkgs `homepage-dashboard` with the upstream Next.js cache patches and - wrapped with `dockerTools.buildLayeredImage`. Autodiscovery shifts: services on - minikube (ArgoCD, Immich, Kiwix, Mealie, Miniflux, Grafana, Prometheus, - Navidrome, Paperless, TeslaMate, Transmission) become explicit static entries - in `services.yaml`; ringtail services (Authentik, Frigate/NVR, Ntfy, Ollama) - auto-populate via Ingress annotations. -- Migrated CV (`cv.eblu.me`) and Docs (`docs.eblu.me`) from minikube Deployments to indri-native ansible roles. Caddy now serves the extracted release tarballs directly via a new `kind: static` service-block in the Caddy template — no daemon, no container — replacing the prior nginx-in-a-pod layer. Removes a network hop on every request and shrinks minikube's footprint. See [[cv-on-indri]] and [[docs-on-indri]]. Part of the broader minikube wind-down. -- Migrated devpi (PyPI mirror at `pypi.ops.eblu.me`) from a minikube StatefulSet to a launchd-managed service on indri. devpi-server now runs in a uv-managed venv with pinned `devpi-server` and `devpi-web` versions, listens on `127.0.0.1:3141`, and is fronted by Caddy. The minikube StatefulSet was crash-looping under memory pressure (and breaking the Python toolchain everywhere); the new layout removes a layer of dependency on cluster health for critical-path tooling. See [[devpi-on-indri]]. -- Move the entire Immich stack — server, machine-learning, valkey, - and the PostgreSQL+VectorChord cluster — off `minikube-indri` and - onto `k3s-ringtail`. Postgres data migrated zero-loss via CNPG - `pg_basebackup` (replica catch-up then promote); row counts on - `asset`, `user`, `album`, `smart_search`, `activity`, `asset_face` - verified equal between source and replica before cutover. The ML - pod now uses ringtail's RTX 4080 via the nvidia-device-plugin - (time-slicing bumped 2 → 4 to share with frigate + ollama). Caddy - routing at `photos.ops.eblu.me` is unchanged (still - `photos.tail8d86e.ts.net`, the device just lives on ringtail now). - Borgmatic backups continue against the same `immich-pg` tailnet - hostname. First concrete chain in the broader indri-k8s - decommission effort. -- Add local nix container build for `tailscale` (`containers/tailscale/default.nix`) so ringtail's tailscale-operator ProxyClass proxy pods pull from the forge mirror instead of `docker.io/tailscale/tailscale`. Pinned at v1.94.2 to match `service-versions.yaml`. Indri's tailscale-operator continues to use upstream during the k8s-to-ringtail migration. -- Address the 6 critical Prowler IaC findings against `argocd/manifests/`. Prowler's IaC provider hardcodes `self._mutelist = None` and delegates filtering to Trivy, but doesn't plumb `--ignorefile` through — so the documented "use Trivy filtering" path is actually broken. Added a shim around `trivy` in the Prowler image that injects `--ignorefile $TRIVY_IGNOREFILE` for `trivy fs` invocations when the env var points at a real file. The IaC cronjob now mounts `mutelist/trivyignore.yaml` (Trivy's per-path schema) and sets the env var, muting the `external-secrets` and `kube-state-metrics` Secret-access findings (KSV-0041, KSV-0114). Separately, `grafana-clusterrole` is tightened to remove `secrets` access entirely: the dashboard sidecar already only consumes ConfigMap-labeled dashboards, so its `RESOURCE` env var is now `configmap` instead of `both`. -- Pin ringtail's wired IP to `192.168.1.21` via NixOS scripted networking; NetworkManager no longer manages `enp5s0`. Removes DHCP lease renewal as a failure mode after a silent lease teardown took ringtail offline. Also explicitly enables `net.ipv4.ip_forward` (previously set implicitly by scripted-DHCP) so k3s pod networking and Tailscale routing continue to work with static networking. -- Ripped out the compensating-controls (CC) framework: deleted `compensating-controls.yaml`, the `review-compensating-controls` mise task, and the associated how-to / explanation docs. Prowler and Kingfisher continue to run weekly and produce reports; the Prowler mutelist YAML files remain in place but no longer carry `CC: ` prefixes — each entry just keeps a free-form `Description` of why the finding is muted. The CC review cadence proved to be more overhead than this single-operator homelab needed. -- Wire shower app for public exposure: fly nginx `shower.eblu.me` server - block as a guest-only surface — splash page, `/prizes//`, static - assets, media. Everything authenticated (`/admin/`, `/host/`, - `/accounts/`) returns 403 with a "tailnet only" pointer. Staff hit - `shower.ops.eblu.me` for the operator console + admin; the app's - v1.0.1 `DJANGO_PUBLIC_URL_BASE` setting makes QR codes generated on - the tailnet point back at the WAN host for guests. Plus a Caddy route - on indri, Pulumi Gandi CNAME, and a Grafana APM dashboard tracking - request rate, error rate, latency, bandwidth, and access logs. -- Mirror Valkey 8.1 locally as `registry.ops.eblu.me/blumeops/valkey`. Replaces direct pulls of `docker.io/valkey/valkey:8.1-alpine` for paperless and immich sidecars. Built via native Dagger pipeline on Alpine 3.22. Stateless swap — no data migration. Authentik's nix-built Redis remains separate. -- Add nix-built amd64 valkey for ringtail (`containers/valkey/default.nix`) so immich-ringtail can stop pulling the upstream multi-arch `docker.io/valkey/valkey` image. Existing `container.py` continues to build Alpine arm64 for paperless on indri. Both bump to valkey 8.1.7 (Alpine 3.22 8.1.7-r0 / nixpkgs 8.1.7). -- Upgrade Grafana Alloy v1.14.0 → v1.16.0 across all four service deployments - (alloy-k8s, alloy-ringtail, alloy-tracing-ringtail on k8s; alloy native on - indri). Pulls in stable database observability (v1.15) and the OTel Collector - v0.147.0 bump. Container build also migrated from Dockerfile to native Dagger - `container.py` per the build-container-image migration playbook. -- Upgraded Dagger from v0.20.1 to v0.20.6 (engine, CLI pin, and SDK regen) and migrated `runner-job-image` from a Debian-based Dockerfile to a native Dagger `container.py` on Alpine 3.23, reusing the shared `alpine_runtime` helper. -- Decommission the wave-1 services on minikube-indri now that paperless, - teslamate, and mealie run on ringtail with their data backed up. Removes the - minikube `paperless`/`teslamate`/`mealie` manifest dirs + ArgoCD app - definitions (pruning the parked Deployments, Services, and the redundant - minikube mealie/paperless PVCs), and drops the `paperless`/`teslamate` roles - from the minikube `blumeops-pg` cluster. The `paperless` and `teslamate` - databases are dropped from indri's blumeops-pg as the finalization step. - miniflux + authentik remain on the minikube cluster (later waves). -- Upgraded the k8s Forgejo runner to the v12.8 line, switched it from first-boot registration to declarative `server.connections` credentials from 1Password, and consolidated the supporting runner how-to documentation. -- Move paperless, teslamate, and mealie off `minikube-indri` onto - `k3s-ringtail`, shedding ~1.1 GiB of resident load from the - OOM-thrashing 8 GiB minikube node (the kernel OOM killer had been - killing `kube-apiserver`/`dockerd`/argocd, flapping every - minikube-hosted service at once). paperless + teslamate databases - move into a fresh CNPG `blumeops-pg` cluster on ringtail via a cold - `pg_dump`/`pg_restore` from the quiesced source — row counts verified - equal before any routing flip; source DBs dropped only after the - ringtail side serves traffic. mealie's SQLite PVC is copied as-is. - paperless media stays on sifaka NFS. Downtime-tolerant cold cutover - (no streaming replication); rollback is repoint-and-scale-up with the - source untouched. Second chain in the indri-k8s decommission after - [[migrate-immich-to-ringtail]]. -- Recurring maintenance batch: - - - Ringtail flake inputs refreshed (`disko`, `home-manager`, `nixpkgs`). - - Tooling deps bumped: prek hooks (trufflehog v3.95.3, kingfisher v1.101.0, ruff v0.15.14, `ansible-core` 2.21.0); fly proxy base images (nginx 1.30.1-alpine, alloy v1.16.1); `typer==0.26.2` in mise tasks. -- Updated `nixos/ringtail/flake.lock` (weekly cadence): `disko`, `home-manager`, and `nixpkgs` inputs refreshed. `nixpkgs-services` skipped per overlay convention. -- Reviewed `mealie` service version freshness; upstream is 5 minor versions ahead (v3.17.0 vs deployed v3.12.0). Marked reviewed; upgrade deferred. -- Deploy shower v1.1.2 — bump container build to new app release. -- Upgrade unpoller v2.34.0 → v3.2.0 and migrate container build from Dockerfile to native Dagger (container.py). v3.0.0 carries breaking UniFi API changes; v3.2.0 introduces a 60s background poll (cached scrapes) by default — set `interval = 0` in `up.conf` to restore on-demand polling. -- Monthly tooling dependency refresh: prek hooks (trufflehog, kingfisher, ruff, shfmt, prettier, actionlint, ansible-lint), fly proxy base images (nginx 1.30.0, tailscale v1.94.2, alloy v1.16.0), normalize pyyaml lower bound in mise-tasks. -- Add GE-Proton (`pkgs.proton-ge-bin`) to `programs.steam.extraCompatPackages` - on ringtail. Subnautica 2 hangs at Mercuna plugin init under Proton - Experimental + DXVK D3D12; GE-Proton is available as a Steam per-game - compatibility option to work around it. -- Add `sn2-prelaunch` Steam launch wrapper on ringtail that removes - Subnautica 2's stale `Saved/running.dat` and `Saved/beforelobby.dat` - lockfiles before each launch. SN2 pops up an invisible (0×0-sized) - Error dialog when it detects an unclean exit, blocking GameThread - forever; this is observable only as a black screen with a spinning - loader. Use via Steam launch option: `sn2-prelaunch %command%`. -- Add local nix container build for `frigate-notify` (`containers/frigate-notify/default.nix`) so the Frigate→ntfy bridge is rebuilt on ringtail from the forge mirror instead of pulled from `ghcr.io/0x2142/frigate-notify`. -- Add resource limits to all ArgoCD pods to prevent unbounded resource consumption during node-wide pressure events. -- Black-hole the `/mirrors/*` repositories at the Fly proxy edge (`return 403` → `forge.ops.eblu.me`). A surprise $29.60 Fly bill traced to ~1.24 TB/30d of egress on `forge.eblu.me`, 99.95% of all proxy egress — of which ~71% was AI scrapers (Meta `meta-externalagent`, OpenAI `GPTBot`, Amazonbot) crawling the near-infinite git-history URL space of the public mirror repos and timing out Forgejo in the process. Mirrors exist for supply-chain control and are consumed over the tailnet, so their public web UI had no legitimate audience. `robots.txt` already disallowed `/mirrors/`, but the offending agents ignore it. Tier-2 mitigations (user-agent denylist, Anubis proof-of-work gateway) are documented in `docs/explanation/ai-scraper-mitigation.md`. -- Bump paperless and immich kustomizations to the main-SHA-built valkey tag (`v8.1.6-r0-fabca04`). Routine post-merge follow-up to keep production manifests pointing at images built from a commit on main. -- Bump shower container to v1.1.1 (probe FOD hash). -- Bumped shower app to v1.1.3 (wheel/sdist + FOD hashes probed on ringtail). -- Cap systemd-coredump on ringtail (ProcessSizeMax/ExternalSizeMax 1G, MaxUse 2G) so multi-GB Wine/Proton game crash dumps no longer thrash the disk and lock up the desktop. -- Deploy shower v1.1.1 to ringtail (kustomize newTag bump). -- Deployed shower v1.1.3 to ringtail (image built and pushed from ringtail; runner bypassed due to indri overload). -- Fix three follow-ups from the wave-1 decommission: grant the local - break-glass `admin` account ArgoCD admin rights (`g, admin, role:admin` — - previously only the Authentik `admins` group had access, so admin was - locked out whenever its token expired), and repoint the alloy blackbox - probe for teslamate from the deleted minikube service to - `https://tesla.ops.eblu.me/` (through Caddy over Tailscale). The orphaned - paperless/teslamate roles + ExternalSecrets left on the minikube - blumeops-pg are also cleaned up. -- Moved the Immich blackbox health probe from indri's alloy to ringtail's alloy. After the immich migration to ringtail, the probe still targeted `immich-server.immich.svc.cluster.local` on indri's cluster where the service no longer exists, causing a persistent `ServiceProbeFailure` alert. -- Pin shower v1.1.1 FOD outputHash (probed locally on ringtail). -- Rebuild Prowler container against main HEAD (v5.23.0-495e45d) after merging the IaC mutelist Dockerfile changes. -- Rebuild and retag alloy v1.16.0 container images from the main-branch SHA - following the squash-merge of #345, per the build-container-image - squash-merge convention. Both images (`registry.ops.eblu.me/blumeops/alloy`) - now reference `9564435` rather than the branch SHA `26a3ab5`, restoring - source traceability after branch cleanup. -- Rebuild shower from the post-merge commit on main so the container's - SHA tag points at a commit that will still exist after the 30-day - branch-cleanup window. Functionally identical to the branch-tag image - already deployed, just preserves source traceability per - [[build-container-image#Squash-merge and container tags]]. -- Rebuild unpoller container from squashed main commit so the image SHA tag matches a commit in main's history (was tagged with the pre-squash branch SHA). -- Rebuild valkey container from squashed main commit (both arm64 dagger and amd64 nix variants), and update paperless + immich-ringtail kustomizations to the main-SHA tags `v8.1.7-ecded30` and `v8.1.7-ecded30-nix`. -- Retired the `blumeops-tasks` mise task (Todoist API) in favor of `heph list --project Blumeops --json` from the self-hosted [hephaestus](https://github.com/eblume/hephaestus) system. Updated docs to point task discovery and rotation reminders at heph, and noted that the `~/code/personal/zk` zettelkasten is migrating into heph docs. -- Switch the Fly proxy deploy strategy from `bluegreen` to `immediate` in `fly/fly.toml`. With a single proxy machine, bluegreen offers little benefit — the green machine routinely failed to reach "started" inside Fly's default 5-minute deploy timeout (the cold-start sequence of `tailscaled` → `tailscale up` → wait-for-MagicDNS → nginx startup eats most of the budget), and the failed deploys would roll back. `immediate` replaces the machine in place with a brief downtime (~5–10s) but actually completes. -- Switch the ringtail provisioning playbook's blumeops clone URL from `forge.eblu.me` (public, via Fly proxy) to `forge.ops.eblu.me` (tailnet, direct via Caddy on indri). Ringtail is always on the tailnet, so the WAN round-trip is pure overhead — it also made `provision-ringtail` brittle whenever the Fly proxy was slow or down. -- Switched Grafana's deployment strategy from `RollingUpdate` to `Recreate`. With an RWO PVC holding the SQLite database and Bleve search index, `RollingUpdate` reliably crashloops the new pod on the index lock until rollout timeout. `Recreate` terminates the old pod first so the new one acquires the lock cleanly. -- Update `tailscale-operator-ringtail` ProxyClass to reference the `0108b68` main-SHA build of the tailscale container. Routine post-merge cleanup so the deployed image traces to a commit that survives PR branch cleanup. -- Update the ringtail NixOS flake lockfile (`nixos/ringtail/flake.lock`): bump - `nixpkgs` (b77b3de → 25f5383) and `disko` (5ba0c95 → 115e521) to latest. - `nixpkgs-services` was intentionally left pinned (skipped by the - `flake-update` pipeline). Routine recurring maintenance per [[manage-lockfile]]. -- Upgrade native macOS Alloy on indri to v1.16.0. Built on gilbert with Go - 1.26.2 + CGO (required for the macOS native DNS resolver, which Tailscale - MagicDNS depends on), scp'd to `~/.local/bin/alloy` on indri, codesigned, - and the LaunchAgent reloaded. Completes the v1.16.0 fleet upgrade started - in #345 — all four Alloy services (alloy-k8s, alloy-ringtail, - alloy-tracing-ringtail, alloy ansible) now run v1.16.0. -- Upgraded zot on indri from v2.1.15 to v2.1.16 (security fixes: TLS verification on metrics client, CORS Allow-Credentials suppression on wildcard origins, manifest/API-key body size limits). - -### Documentation - -- Reviewed `replicating-blumeops` tutorial: fixed "BluemeOps" typos (also in `contributing.md`) and added `last-reviewed` frontmatter. -- Reviewed [[indri]] reference card: added `devpi`, `cv`, and `docs` to the native-services list; widened the k8s note to reflect the growing set of apps now on ringtail and the planned indri-minikube decommission; added CPU/RAM specs. -- New how-to: rotate-fly-deploy-token. Documents the 75-day rotation cadence, why we use `org`-scoped tokens (silences the cosmetic metrics-token warning on `fly status` with marginal blast-radius cost given the single-app personal org), and the procedure for rotation + Forgejo Actions secret sync. -- Add `docs/explanation/ai-scraper-mitigation.md` — the egress-cost / AI-crawler threat model for the public Fly proxy, the tiered mitigation plan (Tier 1: mirror black-hole, shipped; Tier 2: user-agent denylist + Anubis; Tier 3: Cloudflare, rejected on principle), and the data behind it. -- Fix manage-forgejo-mirrors verify step — sync button is on the repo settings page ("Synchronize now"), not the main repo page. -- Fixed the `op item edit` invocation in the [[zot]] API-key rotation procedure: the previous `pbpaste | op item edit ... "field[password]=-"` stdin syntax is rejected by op 2.34 as "invalid JSON" (recent op versions treat piped input as a full JSON template, not a single field value). Procedure now reads the clipboard into a local fish variable and passes it as an inline assignment. -- Fixed the export-filename step in [[run-1password-backup]]: 1Password's desktop app names the export `1PasswordExport--.1pux` automatically rather than letting you save to a fixed name, so the procedure now points the task at that glob instead of pretending the default name is `1Password-export.1pux`. -- Refresh the contributing tutorial: add `last-reviewed`, include the `.ai.md` changelog fragment type, and clarify that `prek` is pinned via `mise`. -- Review and refresh the Navidrome reference card: add `last-reviewed`, correct the scanner env var name, document the current image/version, and record routing and runtime details from the manifests. -- Review and refresh the Ollama reference card: add `last-reviewed`, bump the documented image tag to 0.20.4, and add the two `qwen3.5` models now declared in `models.txt`. -- Reviewed [[1password]] reference card: added the `blumeops` vs `Personal` vault split, noted that `onepassword-connect` runs on both indri and ringtail (not just one cluster), and pulled the `op read` vs `op item get --fields` guidance up from agent memory into the card. -- Reviewed `index.md`; added ringtail to the infrastructure overview and stamped `last-reviewed`. -- Reviewed transmission card: corrected storage layout (`/config/` is emptyDir, watch dir disabled) and noted the Prometheus exporter sidecar. -- rotate-fly-deploy-token: combine mint+store into one command with both fish and bash forms; document the `op item edit` "Password item requires ps value" validator gotcha and the placeholder-password workaround. - -### AI Assistance - -- Adopt `AGENTS.md` as the canonical agent instruction file, keep `CLAUDE.md` as a compatibility shim, and update docs to reference the neutral file and the correct agent-change-process path. -- CLAUDE.md now imports AGENTS.md via `@AGENTS.md` instead of telling agents to go read it. Claude Code only auto-loads CLAUDE.md, so the prose shim was easy to skip; the import inlines AGENTS.md into the session prompt unconditionally. - -### Miscellaneous - -- Removed the dead minikube manifests, container builds, and tooling shims left behind after the cv + docs migration to indri-native (#342). Deletes `argocd/{apps,manifests}/{cv,docs}/`, `containers/{cv,quartz}/`, and the `quartz`→`docs` mapping in `mise-tasks/container-version-check`. Bumps `docs.current-version` to `v1.16.0` (the blumeops release tag) now that the legacy nginx-base version pin is gone. -- Rebuild shower v1.1.0 container from main HEAD (`3c7967e`) and bump the - kustomization tag to `v1.1.0-3c7967e-nix`. The PR was squash-merged, so - the branch commit `444ff91` baked into the prior tag isn't reachable - from main's history. The new tag points at a commit that exists on - main; image content is byte-identical because the FOD output is content - addressed and the inputs didn't change. -- Rebuild shower v1.1.2 from main HEAD (a33fa47) and retag — PR #358 was squash-merged so the branch SHA baked into the prior image tag isn't reachable from main. FOD is content-addressed, so image bytes are identical; only provenance changes. -- Remove the duplicate Homepage tiles for Mealie, Paperless, Immich, and - TeslaMate. Homepage runs on ringtail and autodiscovers ringtail Ingresses via - `gethomepage.dev/*` annotations; once these services migrated to ringtail they - were discovered automatically, making their leftover static `services.yaml` - entries (needed only while they lived on minikube) redundant. -- Removed the now-unused `containers/devpi/` Dagger build artifact. Devpi runs natively on indri via uv venv; the container image is no longer referenced anywhere. Doc examples in `docs/reference/tools/dagger.md` updated to use `miniflux` as the example container name. -- `container-build-and-release` now prints the specific `mise run runner-logs ` command after dispatching, polling the Forgejo API to resolve the run number for the commit it just triggered. -- `mise run runner-logs -j ` now reports a clear error when the log file doesn't exist on indri (e.g. a runner crash that left `action_task.log_in_storage = 0`). Previously it printed only the header and exited 0, because `zstdcat` exits 0 with a "can't stat … -- ignored" stderr message and ssh+fish on indri swallows the remote exit code. - - -## [v1.16.0] - 2026-04-18 - -### Infrastructure - -- Route Fly.io proxy through Caddy on indri with direct WireGuard peering, reducing public-facing latency from 20+ seconds (DERP relay) to sub-second. Fixed Beyla eBPF tracing on ringtail (memlock rlimit + BPF permissions). Restored trace collection to Tempo. - - -## [v1.15.7] - 2026-04-18 - -### Bug Fixes - -- Fix borgmatic LaunchAgent failing silently due to macOS TCC permission dialogs. LaunchAgents now call borgmatic directly instead of routing through `mise x`, which triggered "wants to access Documents" dialogs that hung headless sessions. The ansible role now also manages borgmatic installation via `mise install`. - -### Infrastructure - -- Automate verification of Prowler MANUAL findings (kubelet file perms, kubelet config, etcd CA, RBAC cluster-admin) in `review-compliance-reports` and mute them with `node-config-automated-verification` compensating control. -- Migrate transmission and transmission-exporter containers from Dockerfile to native Dagger builds (`container.py`). Updates base images to Alpine 3.23 and Python 3.14, pins uv to 0.11.6. -- Switched Fly proxy to upstream keepalive pools, reducing forge.eblu.me latency from 35s+ p50 to sub-second. Added `mise run fly-reload` for DNS re-resolution without redeploy. -- Upgrade Prowler from 5.22.0 to 5.23.0; remove init container workaround for broken `--registry` flag (upstream fix in PR #10470). -- Added `robots.txt` to `forge.eblu.me` blocking crawlers from `/mirrors/` to reduce load from Facebook scraping. -- Container builds are now manual-only via `mise run container-build-and-release`. Removed auto-trigger on push to main — shared Dagger helpers made path-based detection unreliable. -- Migrate devpi container from Dockerfile to native Dagger build; bump devpi-server 6.19.1→6.19.3 and devpi-web 5.0.1→5.0.2. -- Migrated kiwix-serve container from Dockerfile to native Dagger build, bumping Alpine base from 3.22 to 3.23. -- Mitigated Forgejo archive endpoint DoS: redirect public archive requests to tailnet, expanded robots.txt, enabled archive cleanup cron, cached release downloads at proxy. -- Refactored Dagger container pipelines: extended `go_build()` helper with `buildmode` and `extra_env` params, migrated miniflux and forgejo-runner to use it, and standardized all Alpine bases from 3.22 to 3.23. - -### Miscellaneous - -- Review compensating control `sso-gated-admin-tools`: tightened scope to ArgoCD only, removed Grafana reference. -- container-build-and-release now verifies the commit exists on the remote before dispatching a build. - - -## [v1.15.6] - 2026-04-14 - -### Bug Fixes - -- Rotate ArgoCD workflow-bot token and admin password after DR rebuild invalidated signing keys, fixing build-blumeops workflow failures. - - -## [v1.15.5] - 2026-04-14 - -### Features - -- Deploy Paperless-ngx document management system at paperless.ops.eblu.me with OCR, Authentik SSO, and NFS storage on sifaka. -- Add `ty` (Astral) Python typechecker to prek hooks, configured for Dagger SDK and container.py modules. Add `type: mise` to service-versions.yaml for tracking development tool versions (dagger, ansible-core, prek, pulumi, ty) through the standard service review process. -- Upgrade grafana-sidecar from 1.28.0 to 2.6.0, adding health probes and porting build to native Dagger container.py. -- Upgrade Navidrome to v0.61.1 — major artwork overhaul with per-disc cover art, rebuilt search engine (SQLite FTS5), server-managed transcoding, and WebP performance fix. -- Add `mise run review-compliance-reports` task for weekly compliance report review with muted/unmuted distinction and week-over-week delta - -### Bug Fixes - -- Add paperless database to borgmatic backup configuration. Previously the only service DB not included in nightly pg_dump backups. -- Fix Fly.io proxy rate limiting to key on real client IP instead of Fly's internal proxy IP, so crawlers no longer consume the shared rate limit bucket for all clients. -- Fix UnPoller (UniFi) Grafana dashboards failing to load due to UID exceeding Grafana 12's 40-character limit. -- Fix blumeops-tasks swallowing wiki-link brackets in task descriptions (rich markup escaping) -- Fix dagger flake-update pipeline: replace nonexistent `--exclude` flag with dynamic input discovery -- Fix services-check to display all firing alerts for a given alert name, not just the first one. -- Pin Fly.io proxy Tailscale to v1.94.1 — the `:stable` tag pulled v1.96.5 which has a MagicDNS regression (SERVFAIL on tailnet names), breaking all public routing through forge.eblu.me, docs.eblu.me, and cv.eblu.me. -- Rewrite `mise run runner-logs` CLI: list runs by run number (not task ID), drill into jobs per run, fetch logs via Forgejo web API instead of SSH+filesystem. Fixes broken log retrieval caused by incorrect hex path calculation and stale data directory. Added `--repo` to query any forge repo (e.g. sporks) and `--limit`/`-n` to control listing size (0 for all). -- Route Dagger build telemetry to Tempo, fixing OTEL metrics exporter warnings. -- Switch paperless redis sidecar from amd64-only nix-built `authentik-redis` image to upstream `valkey:8.1-alpine` (multi-arch). The nix image was previously running under QEMU emulation on arm64 minikube. - -### Infrastructure - -- Build forgejo-runner container locally via native Dagger pipeline instead of pulling from upstream. -- Build kube-state-metrics container locally (Dockerfile + nix) from forge mirror, replacing upstream registry.k8s.io image on both indri and ringtail. -- Upgrade miniflux from 2.2.17 to 2.2.19 and migrate from Dockerfile to native Dagger container.py build (second container after navidrome). Refactor `alpine_runtime()` with `create_user` parameter to support Alpine's built-in nobody user. Pin all mise.toml tool versions to explicit versions instead of "latest". -- Migrate Dagger module from .dagger/ to repo root (src/blumeops/) and replace docker_build() with native Dagger pipelines for container builds. Navidrome is the first container migrated, with full build error visibility. -- Migrate teslamate container build from legacy Dockerfile to native Dagger container.py. -- Add seccomp RuntimeDefault profiles to alloy-k8s and immich pods, resolving 4 unmuted Prowler findings -- Full DR recovery from power loss and minikube cluster rebuild. Validated bootstrap procedure, identified circular dependencies (forge.eblu.me, Zot/Authentik OIDC), Tailscale device name collision issues, and documented recovery steps for restart-indri. -- Set Frigate preview quality to CRF 8 (from default 1) to reduce preview file sizes and improve review timeline loading over NFS. -- Track Fly.io proxy component versions (Tailscale, nginx, Alloy) in service-versions.yaml with new `fly` service type. -- Upgrade ArgoCD from v3.3.2 to v3.3.6 (bug-fix patches), SHA-pin install manifest -- Upgrade authentik 2026.2.0 → 2026.2.2 (bug-fix patch release) -- Upgrade ollama from 0.17.5 to 0.20.4 (adds Gemma 4 support, benchmark tooling, Apple Silicon perf improvements) - -### Documentation - -- Delete outdated install-dagger-on-nix-runner card; add service-versions reference card; clean up zot.md and review-services.md links. -- Enhanced the adding-a-service tutorial with kustomization setup, corrected Tailscale ingress format, updated ArgoCD repoURL, and added a step for creating service reference cards. -- Review gandi.md: add missing forge.eblu.me CNAME, fix program description, stamp review date. - - -## [v1.15.4] - 2026-04-06 - -### Infrastructure - -- Migrate 1Password Connect from Helm to kustomize (1.8.1 → 1.8.2), completing the no-helm-policy migration. - -### Documentation - -- Rewrite observability stack tutorial: replace Helm instructions with actual kustomize/ArgoCD patterns, fix typos, document Alloy as core component - - -## [v1.15.3] - 2026-04-05 - -### Infrastructure - -- Build Tempo container from source via forge mirror; bump 2.10.1 → 2.10.3 -- Pin NixOS service versions (forgejo-runner, snowflake, k3s) via `nixpkgs-services` overlay in ringtail flake, preventing silent upgrades from `nix flake update`. Add k3s and minikube to service-versions.yaml tracking. Fix stale nix-container-builder version (was 12.6.4, actually running 12.7.2). -- Migrate Immich from Helm chart to kustomize manifests and upgrade from v2.5.6 to v2.6.3 -- Upgrade Grafana from 12.3.3 to 12.4.2 — patches 7 CVEs including an unauthenticated DoS (CVE-2026-27880). - -### Documentation - -- First compensating control review: verified `single-user-cluster` still in effect. Added aspirational how-to card for PCI DSS evidence collection. -- Prowler `--registry` fix merged upstream (PR #10470); initContainer workaround documented as pending release. - - -## [v1.15.2] - 2026-03-30 - -### Features - -- Build custom Kingfisher container from sporked deploy branch, replacing upstream image with locally-built version including --clone-url-base patch. -- Add Kingfisher secret scanner as a weekly CronJob scanning all Forgejo repos, with HTML and JSON reports written to sifaka NFS. -- Add MongoDB Kingfisher secret scanner as a prek hook alongside TruffleHog for comparative coverage evaluation. -- Add spork strategy: floating-branch soft-fork tooling (`mise run spork-create`) and documentation for maintaining local patches against upstream projects. - -### Infrastructure - -- Add compensating controls framework: tracking file, review mise task, and how-to doc. Map all Prowler mutelist entries to named controls with CC: prefixes. -- Add Prowler mutelist to suppress expected findings from system components, operator-managed pods, and accepted operational needs. Fix missing seccomp profile on kube-state-metrics. -- Borgmatic photos backup: restrict to library/ and upload/ (skip regenerable dirs), add SSH keepalives and checkpoint interval to prevent broken pipe failures on large initial syncs. -- Upgrade forgejo-runner from 12.7.0 to 12.7.3 (bug fixes, security dep update). Add service reference card. - -### Documentation - -- Add service reference documentation for Kingfisher secret scanner. -- Review and update Ansible reference doc: add missing roles, sibling playbooks, and clarify Ansible's role in the IaC stack. - - -## [v1.15.1] - 2026-03-28 - -### Features - -- Add Tor Snowflake proxy on ringtail as a systemd service to support anti-censorship efforts. -- Add offsite backup for immich photo library to BorgBase, running daily at 4 AM from indri via sifaka SMB mount. -- Add QArt Tuner — a Go tool that generates QR codes whose data modules form a recognizable image, with an interactive web UI for parameter tuning. Based on the [QArt technique](https://research.swtch.com/qart) by Russ Cox. Lives in `utils/qart/`. - -### Infrastructure - -- Migrate Forgejo from Homebrew to source build with mcquack LaunchAgent, matching the pattern used by zot, caddy, and alloy. Upgrades to v14.0.3 (7 security fixes including PKCE bypass and OAuth scope bypass). -- Add borgmatic pg_dump backups for authentik and immich databases. Authentik uses the existing blumeops-pg cluster on port 5432. Immich requires a new borgmatic role on the immich-pg cluster, a Tailscale service, and Caddy L4 proxy on port 5433. -- Upgrade External Secrets Operator from v1.3.2 to v2.2.0 and migrate from Helm chart to static kustomize manifests. -- Add post-deploy maintenance docs and generation pruning task for ringtail. -- Fix Immich Helm values: resource limits and probe timeouts were silently ignored due to wrong value keys. Resources now actually apply to pods, and liveness/readiness probe timeouts increased from 1s to 5s to prevent kubelet from killing pods during ML inference. -- Reduce PodNotReady alert lookback window from 5m to 60s to clear faster after rollouts. -- Tighten ArgoCDAppOutOfSync alert: reduce pending duration from 30m to 5m and lookback window from 5m to 1m so alerts clear faster after sync. -- Update ringtail flake inputs (nixpkgs, home-manager). -- Upgrade Homepage dashboard from v1.10.1 to v1.11.0 -- Upgrade nvidia-device-plugin from v0.18.2 to v0.19.0 - -### Documentation - -- Review and fix CV service doc (correct URL, forge domain, container tag link) and add private forge repo review guidance to review-services process. -- Review tailscale-setup tutorial: fix macOS install steps, add `--accept-routes` tip, correct tag name, add ACL apply instructions, add `[[tailscale-operator]]` cross-reference. - -### Miscellaneous - -- Add `preserve/*` branch prefix exclusion to `branch-cleanup` task; document Pyroscope profiling work and blockers in observability reference. - - ## [v1.15.0] - 2026-03-24 ### Features diff --git a/CLAUDE.md b/CLAUDE.md index 43c994c..289725a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,141 @@ -@AGENTS.md +# CLAUDE.md + +Guidance for Claude Code working in this repository. See also [[ai-assistance-guide]]. + +## Overview + +blumeops is Erich Blume's GitOps repository for personal infrastructure, orchestrated via tailnet `tail8d86e.ts.net`. + +**CRITICAL: Public repo at github.com/eblume/blumeops - never commit secrets!** + +**Shell:** The user's shell is **fish**. Use `$status` not `$?` for exit codes. Use fish syntax in interactive examples. + +## Rules + +1. **Always run `mise run ai-docs` at session start** + This will refresh your context with important information you will be assumed to know and follow. + **Read the full output** — never truncate, pipe to `head`/`tail`, or skip sections. + For problems with a large surface area, ask the user if `mise run ai-sources` should also be run — it concatenates all non-doc source files (~270K tokens) for deep codebase context. +2. **Always use `--context=minikube-indri` with kubectl** (or `--context=k3s-ringtail` for ringtail services) - work contexts must never be touched +3. **Classify the change as C0/C1/C2 before starting** (see below) — this determines branching and PR requirements +4. **Feature branches + PRs for C1/C2** - checkout main, pull, create branch, open PR via `tea pr create`. C0 goes direct to main. +5. **Check PR comments with `mise run pr-comments `** before proceeding +6. **Add changelog fragments (all change levels)** - `docs/changelog.d/..md` + Types: `feature`, `bugfix`, `infra`, `doc`, `ai`, `misc` + Applies to C0, C1, and C2 whenever the change is user-visible or noteworthy. + - **C1/C2:** Use branch name: `..md` + - **C0:** Use orphan prefix: `+..md` (avoids `main.*` collisions) +7. **Test before applying** - dry runs (`--check --diff`), syntax checks, `ssh indri '...'` +8. **Wait for user review before deploying** (C1/C2) +9. **Never merge PRs or push to main without explicit request** (C0 commits to main are fine) +10. **Verify deployments** - `mise run services-check` + +## Change Classification + +Before starting work, classify the change: + +| Class | Name | When to use | Key trait | +|-------|------|-------------|-----------| +| **C0** | Quick Fix | Small, low-risk, fix-forward safe | Direct to main, no PR | +| **C1** | Human Review | Moderate complexity or risk | Feature branch + PR, docs-first | +| **C2** | Mikado Chain | Multi-phase, multi-session, high complexity | Mikado Branch Invariant | + +**C0** — commit directly to main. No branch or PR needed. Fix forward if problems arise. + +**C1** — feature branch with early PR. Search related docs first, write documentation changes before code, deploy from the unmerged branch (ArgoCD `--revision`, Ansible from checkout). Upgrade to C2 if complexity spirals. + +**C2** — branch `mikado/` governed by the Mikado Branch Invariant: all card commits first, then code progress, then card closures. Commits use `C2(): plan/impl/close/finalize` convention. Reset the branch when new prerequisites are discovered. Resume with `mise run docs-mikado --resume`. + +See [[agent-change-process]] for the full methodology. + +## Project Structure + +``` +./docs/ # documentation (Diataxis, Quartz) +./docs/changelog.d/ # towncrier fragments +./.dagger/ # dagger pipelines +./.forgejo/ # forgejo-runner actions and workflows +./mise-tasks/ # scripts via `mise run` +./ansible/playbooks/ # ansible (indri.yml primary) +./ansible/roles/ # indri service roles +./argocd/apps/ # ArgoCD Application definitions +./argocd/manifests/ # k8s manifests per service +./fly/ # fly.io proxy for public routing +./pulumi/ # Pulumi IaC (tailnet ACLs, dns, cloud) +~/.config/{nvim,fish} # user's shell config, managed by chezmoi +~/code/personal/ # user's projects +~/code/personal/zk # user's Obsidian-sync managed zettelkasten. Potential source for reference data. +~/code/3rd/ # mirrored external projects +~/code/work # FORBIDDEN +``` +Other code paths will be listed via ai-docs, this is just an overview. When you +encounter wiki-links (`[[like-this]]`) it is referring to docs/ cards. + +## Service Deployment + +### Kubernetes (ArgoCD) + +Most services run in minikube on indri via ArgoCD (app-of-apps, manual sync). GPU workloads (Frigate, ntfy) run on ringtail's k3s cluster, also managed by ArgoCD. + +**PR workflow:** +1. Create branch, modify `argocd/manifests//` +2. Push. Sync 'apps' app if service definition changed (set --revision to branch). +3. Test on branch: `argocd app set --revision && argocd app sync ` +4. After merge: `argocd app set --revision main && argocd app sync ` + +**Commands:** `argocd app list|get|diff|sync ` + +**Login:** `argocd login argocd.ops.eblu.me --username admin --password "$(op read 'op://vg6xf6vvfmoh5hqjjhlhbeoaie/srogeebssulhtb6tnqd7ls6qey/password')"` + +### Indri (Ansible) + +Native services: Forgejo, Zot, Caddy, Borgmatic, Alloy + +```fish +mise run provision-indri # full +mise run provision-indri -- --tags # specific +mise run provision-indri -- --check --diff # dry run +``` + +### Routing + +| Domain | Mechanism | Reachable from | +|--------|-----------|----------------| +| `*.eblu.me` | Fly.io proxy (Tailscale tunnel) | public internet | +| `*.ops.eblu.me` | Caddy on indri | k8s pods, containers, tailnet | +| `*.tail8d86e.ts.net` | Tailscale MagicDNS | tailnet clients only | + +Check tailscale serve: `ssh indri 'tailscale serve status --json'` + +## Container Releases + +```fish +mise run container-list # show images/tags +mise run container-release # tag and build +``` +The goal is to eventually use only locally built containers in all cases, with +full supply chain control via forge.ops.eblu.me repositories, mirroring source +from upstream. + +## Third-Party Projects + +Ask user to mirror on forge first, then clone to `~/code/3rd//`. + +## Task Discovery + +```fish +mise run blumeops-tasks # fetch from Todoist, sorted by priority +``` +Most tasks are stored in `./mise-tasks/`. For scripts with any logic or +complexity, use uv run --script 's with explicit dependencies. Complex +workflows with artifacts should become dagger pipelines. Mise tasks are for +development processes and operations - tools for the user or the agent. + +## Credentials + +Root store is 1Password. Never grab directly - use existing patterns (ansible +pre_tasks, external-secrets, scripts with `op` CLI). It's ok to use `op item +get` without `--reveal` to explore what secrets are available, however. + +Prefer `op read "op://vault/item/field"` over `op item get --fields` to avoid +quoting issues with multi-line values. diff --git a/README.md b/README.md index e5945e5..8ba6b8d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Tools and configuration for Erich Blume's personal infrastructure, orchestrated across a Tailscale tailnet. This is a homelab, but it's also a testing ground for AI-assisted -infrastructure development. Much of this codebase was initially co-authored with [Claude +infrastructure development. Much of this codebase was co-authored with [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview), and the repo places heavy emphasis on documentation, process, and change classification to make that collaboration work well. I don't know entirely how @@ -77,7 +77,7 @@ mise run container-list # list tracked container images ## AI-assisted development This repo is designed to be worked on by both humans and AI agents. The -[`AGENTS.md`](AGENTS.md) file provides shared instructions for agentic tools, and the +[`CLAUDE.md`](CLAUDE.md) file provides instructions for Claude Code, and the [`docs/tutorials/ai-assistance-guide.md`](docs/tutorials/ai-assistance-guide.md) explains the full workflow. @@ -87,7 +87,7 @@ Changes are classified before starting work: - **C1** - feature branch + PR, documentation written before code - **C2** - multi-phase work using the Mikado method for dependency tracking -See the [agent change process](docs/explanation/agent-change-process.md) for +See the [agent change process](docs/how-to/agent-change-process.md) for details. ## License diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index 1e33bb1..ce6a930 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -212,23 +212,6 @@ no_log: true tags: [forgejo_metrics] - # Devpi root password (PyPI mirror admin) - - name: Fetch devpi root password - ansible.builtin.command: - cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/kyhzfifryqnuk7jeyibmmjvxxm/add more/root password" - delegate_to: localhost - register: _devpi_root_password - changed_when: false - no_log: true - check_mode: false - tags: [devpi] - - - name: Set devpi root password fact - ansible.builtin.set_fact: - devpi_root_password: "{{ _devpi_root_password.stdout }}" - no_log: true - tags: [devpi] - roles: - role: alloy tags: alloy @@ -244,8 +227,6 @@ tags: zot - role: zot_metrics tags: zot_metrics - - role: devpi - tags: devpi - role: minikube tags: minikube - role: minikube_metrics @@ -256,11 +237,5 @@ tags: jellyfin_metrics - role: forgejo_metrics tags: forgejo_metrics - - role: cv - tags: cv - - role: docs - tags: docs - - role: heph - tags: heph - role: caddy tags: caddy diff --git a/ansible/playbooks/ringtail.yml b/ansible/playbooks/ringtail.yml index b05d67a..ee5604b 100644 --- a/ansible/playbooks/ringtail.yml +++ b/ansible/playbooks/ringtail.yml @@ -57,7 +57,7 @@ tasks: - name: Ensure blumeops repo is present ansible.builtin.git: - repo: "https://forge.ops.eblu.me/eblume/blumeops.git" + repo: "https://forge.eblu.me/eblume/blumeops.git" dest: /etc/blumeops version: "{{ ringtail_commit | default('main') }}" force: true diff --git a/ansible/roles/alloy/defaults/main.yml b/ansible/roles/alloy/defaults/main.yml index 4cf7432..fa840d4 100644 --- a/ansible/roles/alloy/defaults/main.yml +++ b/ansible/roles/alloy/defaults/main.yml @@ -101,10 +101,6 @@ alloy_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie alloy_op_postgres_item: guxu3j7ajhjyey6xxl2ovsl2ui alloy_op_postgres_field: alloy-user-pw -# Forgejo metrics collection -alloy_collect_forgejo: true -alloy_forgejo_port: 3001 - # macOS power metrics collection (via powermetrics, requires root) alloy_collect_power_metrics: true alloy_power_metrics_script: /usr/local/bin/macos-power-metrics diff --git a/ansible/roles/alloy/templates/config.alloy.j2 b/ansible/roles/alloy/templates/config.alloy.j2 index 39e4dad..51d2c94 100644 --- a/ansible/roles/alloy/templates/config.alloy.j2 +++ b/ansible/roles/alloy/templates/config.alloy.j2 @@ -74,18 +74,6 @@ prometheus.scrape "zot" { } {% endif %} -{% if alloy_collect_forgejo | default(false) %} -// ============== FORGEJO METRICS ============== - -// Scrape Forgejo's native metrics endpoint -prometheus.scrape "forgejo" { - targets = [{"__address__" = "localhost:{{ alloy_forgejo_port }}"}] - metrics_path = "/metrics" - forward_to = [prometheus.relabel.instance.receiver] - scrape_interval = "{{ alloy_scrape_interval }}" -} -{% endif %} - {% if alloy_collect_logs %} // ============== LOG COLLECTION ============== diff --git a/ansible/roles/borgmatic/defaults/main.yml b/ansible/roles/borgmatic/defaults/main.yml index a743161..c7a9793 100644 --- a/ansible/roles/borgmatic/defaults/main.yml +++ b/ansible/roles/borgmatic/defaults/main.yml @@ -6,16 +6,6 @@ borgmatic_log_dir: /Users/erichblume/Library/Logs # Full path to borg binary since LaunchAgent doesn't have homebrew in PATH borgmatic_local_path: /opt/homebrew/bin/borg -# Borgmatic version — keep in sync with mise.toml in the repo root. -# Ansible installs this via `mise install` so indri doesn't need the repo cloned. -borgmatic_version: "2.1.4" - -# Full path to borgmatic binary — called directly by LaunchAgents to avoid -# routing through mise, which triggers macOS TCC permission dialogs for -# protected folders (e.g. ~/Documents) that hang headless LaunchAgent sessions. -# Uses mise's "latest" symlink so version bumps don't break the LaunchAgent path. -borgmatic_bin: /Users/erichblume/.local/share/mise/installs/pipx-borgmatic/latest/bin/borgmatic - # Schedule: runs daily at 2:00 AM borgmatic_schedule_hour: 2 borgmatic_schedule_minute: 0 @@ -27,9 +17,6 @@ borgmatic_source_directories: - /Users/erichblume/.config/borgmatic - /Users/erichblume/Documents - /Users/erichblume/.local/share/borgmatic/k8s-dumps - # Shower app prize-photo uploads (sifaka SMB mount). Mounted manually - # on indri via Finder — see docs/how-to/operations/shower-app.md. - - /Volumes/shower # Backup repositories borgmatic_repositories: @@ -56,17 +43,7 @@ borgmatic_k8s_sqlite_dumps: namespace: mealie label_selector: app=mealie db_path: /app/data/mealie.db - # migrated to ringtail (wave-1); ssh to ringtail and run k3s kubectl - # there, same as shower below. - target: ssh:eblume@ringtail - - name: shower - namespace: shower - label_selector: app=shower - db_path: /app/data/db.sqlite3 - # ssh to ringtail and run k3s kubectl there — avoids needing a - # ringtail kubeconfig on indri. k3s.yaml on ringtail is - # world-readable (mode 644), so no sudo required. - target: ssh:eblume@ringtail + context: minikube # Exclude patterns borgmatic_exclude_patterns: [] @@ -84,9 +61,7 @@ borgmatic_keep_yearly: 1000 # pg_dump_command must be full path since LaunchAgent doesn't have homebrew in PATH # --- Immich photo library backup (BorgBase offsite only) --- borgmatic_photos_config: /Users/erichblume/.config/borgmatic/photos.yaml -borgmatic_photos_source_directories: - - /Volumes/photos/library - - /Volumes/photos/upload +borgmatic_photos_source_dir: /Volumes/photos borgmatic_photos_borgbase_repo: ssh://xcrtl5tg@xcrtl5tg.repo.borgbase.com/./repo # Schedule: runs daily at 4:00 AM (offset from main backup at 2:00 AM) borgmatic_photos_schedule_hour: 4 @@ -103,18 +78,13 @@ borgmatic_postgresql_databases: hostname: pg.ops.eblu.me port: 5432 username: borgmatic - - name: authentik + - name: teslamate hostname: pg.ops.eblu.me port: 5432 username: borgmatic - # migrated to ringtail blumeops-pg (wave-1); port 5434 = Caddy L4 route - - name: teslamate + - name: authentik hostname: pg.ops.eblu.me - port: 5434 - username: borgmatic - - name: paperless - hostname: pg.ops.eblu.me - port: 5434 + port: 5432 username: borgmatic # immich-pg cluster (VectorChord) via Caddy L4 on port 5433 - name: immich diff --git a/ansible/roles/borgmatic/tasks/main.yml b/ansible/roles/borgmatic/tasks/main.yml index 36d3bb6..dd6efdd 100644 --- a/ansible/roles/borgmatic/tasks/main.yml +++ b/ansible/roles/borgmatic/tasks/main.yml @@ -1,11 +1,6 @@ --- -# Borgmatic is installed via mise (pipx) and called directly by LaunchAgents. -# This role manages installation, config, and the scheduled LaunchAgents. - -- name: Install borgmatic via mise - ansible.builtin.command: mise install pipx:borgmatic@{{ borgmatic_version }} - register: borgmatic_install - changed_when: "'installed' in borgmatic_install.stderr" +# Note: borgmatic is installed via mise (pipx), not managed here. +# This role manages the config file and scheduled LaunchAgent. - name: Ensure borgmatic config directory exists ansible.builtin.file: @@ -19,10 +14,8 @@ ansible.builtin.copy: content: | # Managed by ansible (borgmatic role) - k8s PostgreSQL backup credentials - # 5432 = minikube blumeops-pg, 5433 = immich-pg, 5434 = ringtail blumeops-pg pg.ops.eblu.me:5432:*:borgmatic:{{ borgmatic_db_password }} pg.ops.eblu.me:5433:*:borgmatic:{{ borgmatic_db_password }} - pg.ops.eblu.me:5434:*:borgmatic:{{ borgmatic_db_password }} dest: ~/.pgpass mode: '0600' no_log: true @@ -51,20 +44,6 @@ mode: '0700' when: borgmatic_k8s_sqlite_dumps | length > 0 -- name: Ensure ~/bin exists - ansible.builtin.file: - path: "{{ ansible_env.HOME }}/bin" - state: directory - mode: '0755' - when: borgmatic_k8s_sqlite_dumps | length > 0 - -- name: Deploy k8s SQLite dump helper script - ansible.builtin.template: - src: k8s-sqlite-dump.sh.j2 - dest: "{{ ansible_env.HOME }}/bin/borgmatic-k8s-sqlite-dump" - mode: '0755' - when: borgmatic_k8s_sqlite_dumps | length > 0 - - name: Deploy borgmatic configuration ansible.builtin.template: src: config.yaml.j2 diff --git a/ansible/roles/borgmatic/templates/borgmatic-photos.plist.j2 b/ansible/roles/borgmatic/templates/borgmatic-photos.plist.j2 index d5b5578..6e69159 100644 --- a/ansible/roles/borgmatic/templates/borgmatic-photos.plist.j2 +++ b/ansible/roles/borgmatic/templates/borgmatic-photos.plist.j2 @@ -14,7 +14,10 @@ ProgramArguments - {{ borgmatic_bin }} + /opt/homebrew/opt/mise/bin/mise + x + -- + borgmatic --config {{ borgmatic_photos_config }} create diff --git a/ansible/roles/borgmatic/templates/borgmatic.plist.j2 b/ansible/roles/borgmatic/templates/borgmatic.plist.j2 index a6422fe..c7da8e8 100644 --- a/ansible/roles/borgmatic/templates/borgmatic.plist.j2 +++ b/ansible/roles/borgmatic/templates/borgmatic.plist.j2 @@ -14,7 +14,10 @@ ProgramArguments - {{ borgmatic_bin }} + /opt/homebrew/opt/mise/bin/mise + x + -- + borgmatic --config {{ borgmatic_config }} create diff --git a/ansible/roles/borgmatic/templates/config.yaml.j2 b/ansible/roles/borgmatic/templates/config.yaml.j2 index 0893dbc..85804b7 100644 --- a/ansible/roles/borgmatic/templates/config.yaml.j2 +++ b/ansible/roles/borgmatic/templates/config.yaml.j2 @@ -32,20 +32,12 @@ exclude_patterns: encryption_passcommand: {{ borgmatic_encryption_passcommand }} {% if borgmatic_k8s_sqlite_dumps %} -# Pre-backup: dump SQLite databases from k8s pods. -# Uses sqlite3.backup() for a safe, consistent copy. -# -# Quoting/escaping is delegated to ~/bin/borgmatic-k8s-sqlite-dump -# (deployed by the borgmatic ansible role). Each entry's `target` -# is either: -# - local: -> local kubectl with --context (mealie etc.) -# - ssh: -> ssh + k3s kubectl on the cluster host, -# used for ringtail since indri's kubeconfig -# deliberately doesn't carry that context. +# Pre-backup: dump SQLite databases from k8s pods +# Uses sqlite3 .backup for a safe, consistent copy (no corruption from concurrent writes) before_backup: - mkdir -p {{ borgmatic_k8s_dump_dir }} {% for db in borgmatic_k8s_sqlite_dumps %} - - {{ ansible_env.HOME }}/bin/borgmatic-k8s-sqlite-dump {{ db.target }} {{ db.namespace }} {{ db.label_selector }} {{ db.db_path }} {{ db.name }} {{ borgmatic_k8s_dump_dir }}/{{ db.name }}.db + - /opt/homebrew/bin/kubectl --context={{ db.context }} exec -n {{ db.namespace }} deploy/{{ db.name }} -- python3 -c "import sqlite3; sqlite3.connect('{{ db.db_path }}').backup(sqlite3.connect('/tmp/{{ db.name }}-backup.db'))" && /opt/homebrew/bin/kubectl --context={{ db.context }} cp {{ db.namespace }}/$(/opt/homebrew/bin/kubectl --context={{ db.context }} get pod -n {{ db.namespace }} -l {{ db.label_selector }} -o jsonpath='{.items[0].metadata.name}'):/tmp/{{ db.name }}-backup.db {{ borgmatic_k8s_dump_dir }}/{{ db.name }}.db {% endfor %} {% endif %} diff --git a/ansible/roles/borgmatic/templates/k8s-sqlite-dump.sh.j2 b/ansible/roles/borgmatic/templates/k8s-sqlite-dump.sh.j2 deleted file mode 100644 index 9cc24da..0000000 --- a/ansible/roles/borgmatic/templates/k8s-sqlite-dump.sh.j2 +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -# {{ ansible_managed }} -# -# Helper script invoked by borgmatic's before_backup hook to capture a -# k8s pod's SQLite database. Keeps the borgmatic config readable by -# pulling all the quoting out of YAML. -# -# Usage: -# borgmatic-k8s-sqlite-dump \ -# -# -# is one of: -# local: - run local kubectl with --context= -# ssh: - ssh to host and run k3s kubectl there -# (no indri-side kubeconfig needed) -# -# - k8s namespace of the pod -# - label selector to find the pod (e.g. app=shower) -# - absolute path inside the pod to the SQLite DB -# - short name used for temp filenames -# - file on this host to receive the dump -set -euo pipefail - -target=${1:?missing target} -namespace=${2:?missing namespace} -selector=${3:?missing selector} -db_path=${4:?missing db path} -name=${5:?missing name} -dump_target=${6:?missing dump target} - -# Stage the backup next to the source DB (a guaranteed-writable volume); -# minimal nix images (e.g. mealie) have no /tmp. -pod_tmp="$(dirname "$db_path")/.borgmatic-backup-${name}.db" - -python_backup='import sqlite3; sqlite3.connect("'"$db_path"'").backup(sqlite3.connect("'"$pod_tmp"'"))' - -mode=${target%%:*} -ref=${target#*:} - -case "$mode" in - local) - # Pulls dump bytes out via "kubectl exec -- cat" rather than - # "kubectl cp", which would otherwise need tar inside the pod - # (nix-built images like shower don't bundle tar). - context=$ref - kubectl="/opt/homebrew/bin/kubectl --context=$context -n $namespace" - pod=$($kubectl get pod -l "$selector" \ - -o jsonpath='{.items[0].metadata.name}') - $kubectl exec "$pod" -- python3 -c "$python_backup" - $kubectl exec "$pod" -- cat "$pod_tmp" > "$dump_target" - $kubectl exec "$pod" -- rm -f "$pod_tmp" - ;; - ssh) - host=$ref - # Force bash on the remote (user's login shell on ringtail is - # fish). Pipe the script via stdin to dodge nested quoting. - # The dump bytes come back over the ssh stdout stream — no - # intermediate scp, no tar requirement in the pod. - ssh "$host" bash < "$dump_target" -set -euo pipefail -export KUBECONFIG=/etc/rancher/k3s/k3s.yaml -pod=\$(k3s kubectl -n "$namespace" get pod -l "$selector" -o jsonpath='{.items[0].metadata.name}') -k3s kubectl -n "$namespace" exec "\$pod" -- python3 -c '$python_backup' 1>&2 -k3s kubectl -n "$namespace" exec "\$pod" -- cat "$pod_tmp" -k3s kubectl -n "$namespace" exec "\$pod" -- rm -f "$pod_tmp" 1>&2 -EOF - ;; - *) - echo "borgmatic-k8s-sqlite-dump: unknown target mode: $mode" >&2 - echo " expected local: or ssh:" >&2 - exit 1 - ;; -esac diff --git a/ansible/roles/borgmatic/templates/photos.yaml.j2 b/ansible/roles/borgmatic/templates/photos.yaml.j2 index 2bd0a4f..1c118df 100644 --- a/ansible/roles/borgmatic/templates/photos.yaml.j2 +++ b/ansible/roles/borgmatic/templates/photos.yaml.j2 @@ -1,10 +1,7 @@ # {{ ansible_managed }} # # Borgmatic config for immich photo library backup. -# Backs up library/ and upload/ from /Volumes/photos (sifaka SMB mount) -# to BorgBase offsite ONLY. Excludes encoded-video/, thumbs/, backups/ -# since those are regenerable from originals. -# +# Backs up /Volumes/photos (sifaka SMB mount) to BorgBase offsite ONLY. # Separate from the main borgmatic config to keep concerns isolated: # - main config: indri data → sifaka + borgbase # - this config: sifaka photos → borgbase (different repo) @@ -12,9 +9,7 @@ local_path: {{ borgmatic_local_path }} source_directories: -{% for dir in borgmatic_photos_source_directories %} - - {{ dir }} -{% endfor %} + - {{ borgmatic_photos_source_dir }} source_directories_must_exist: true @@ -26,10 +21,7 @@ repositories: encryption_passcommand: {{ borgmatic_encryption_passcommand }} -ssh_command: ssh -o IdentitiesOnly=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=5 -i {{ borgmatic_borgbase_ssh_key_path }} - -# Save checkpoints every 10 minutes so interrupted backups don't lose all progress -checkpoint_interval: 600 +ssh_command: ssh -o IdentitiesOnly=yes -i {{ borgmatic_borgbase_ssh_key_path }} # Retention policy — photos are precious, keep more history keep_daily: {{ borgmatic_photos_keep_daily }} diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index e6d7385..784738f 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -51,10 +51,7 @@ caddy_services: backend: "https://feed.tail8d86e.ts.net" - name: devpi host: "pypi.{{ caddy_domain }}" - backend: "http://localhost:3141" - - name: heph - host: "heph.{{ caddy_domain }}" - backend: "http://localhost:8787" # hephaestus hub (server mode) + PWA shell + backend: "https://pypi.tail8d86e.ts.net" - name: kiwix host: "kiwix.{{ caddy_domain }}" backend: "https://kiwix.tail8d86e.ts.net" @@ -75,23 +72,16 @@ caddy_services: backend: "https://go.tail8d86e.ts.net" - name: docs host: "docs.{{ caddy_domain }}" - kind: static - root: "{{ docs_content_dir }}" - try_html: true # Quartz: path → path/ → path.html → 404.html + backend: "https://docs.tail8d86e.ts.net" - name: cv host: "cv.{{ caddy_domain }}" - kind: static - root: "{{ cv_content_dir }}" - download_paths: - - path: /resume.pdf - filename: erich-blume-resume.pdf + backend: "https://cv.tail8d86e.ts.net" - name: nvr host: "nvr.{{ caddy_domain }}" backend: "https://nvr.tail8d86e.ts.net" - name: authentik host: "authentik.{{ caddy_domain }}" backend: "https://authentik.tail8d86e.ts.net" - cache_policy: spa - name: ntfy host: "ntfy.{{ caddy_domain }}" backend: "https://ntfy.tail8d86e.ts.net" @@ -101,12 +91,6 @@ caddy_services: - name: mealie host: "meals.{{ caddy_domain }}" backend: "https://meals.tail8d86e.ts.net" - - name: paperless - host: "paperless.{{ caddy_domain }}" - backend: "https://paperless.tail8d86e.ts.net" - - name: shower - host: "shower.{{ caddy_domain }}" - backend: "https://shower.tail8d86e.ts.net" - name: sifaka host: "nas.{{ caddy_domain }}" backend: "http://sifaka:5000" @@ -120,8 +104,6 @@ caddy_tcp_services: backend: "pg.tail8d86e.ts.net:5432" # PostgreSQL (blumeops-pg) - port: 5433 backend: "immich-pg.tail8d86e.ts.net:5432" # PostgreSQL (immich-pg) - - port: 5434 - backend: "blumeops-pg-ringtail.tail8d86e.ts.net:5432" # PostgreSQL (blumeops-pg on ringtail) - port: "{{ sifaka_node_exporter_port }}" backend: "sifaka:{{ sifaka_node_exporter_port }}" # Sifaka node_exporter - port: "{{ sifaka_smartctl_exporter_port }}" diff --git a/ansible/roles/caddy/templates/Caddyfile.j2 b/ansible/roles/caddy/templates/Caddyfile.j2 index f6b5f64..dc3c7ff 100644 --- a/ansible/roles/caddy/templates/Caddyfile.j2 +++ b/ansible/roles/caddy/templates/Caddyfile.j2 @@ -31,33 +31,6 @@ {% for service in caddy_services %} @{{ service.name }} host {{ service.host }} handle @{{ service.name }} { -{% if service.kind | default('proxy') == 'static' %} - root * {{ service.root }} - encode gzip - # Long-cache fingerprinted assets; everything else stays default. - @{{ service.name }}_assets path_regexp \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ - header @{{ service.name }}_assets Cache-Control "public, max-age=31536000, immutable" -{% for dl in service.download_paths | default([]) %} - @{{ service.name }}_dl{{ loop.index }} path {{ dl.path }} - header @{{ service.name }}_dl{{ loop.index }} Content-Disposition `attachment; filename="{{ dl.filename }}"` -{% endfor %} -{% if service.try_html | default(false) %} - # Quartz clean URLs: path → path/ → path.html → /404.html (200). - # Caddy's handle_errors is a top-level directive and can't live in - # this nested handle, so the 404 page rides as the final try_files - # candidate (served with 200 — acceptable for a human-facing 404). - try_files {path} {path}/ {path}.html /404.html -{% endif %} - file_server -{% else %} -{% if service.cache_policy | default('') == 'spa' %} - # SPA cache policy: hashed static assets are immutable, HTML must revalidate. - # Prevents stale HTML from referencing chunk hashes that no longer exist. - @{{ service.name }}_static path /static/dist/* - header @{{ service.name }}_static Cache-Control "public, max-age=31536000, immutable" - @{{ service.name }}_html path /if/* - header @{{ service.name }}_html Cache-Control "no-cache" -{% endif %} {% if service.backend.startswith('https://') %} reverse_proxy {{ service.backend }} { # Caddy v2.11+ rewrites Host to upstream for HTTPS backends. @@ -66,7 +39,6 @@ } {% else %} reverse_proxy {{ service.backend }} -{% endif %} {% endif %} } diff --git a/ansible/roles/cv/defaults/main.yml b/ansible/roles/cv/defaults/main.yml deleted file mode 100644 index a18cc82..0000000 --- a/ansible/roles/cv/defaults/main.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# CV / resume static site (native, replaces minikube Deployment) -# Caddy serves cv_content_dir directly via the static-kind service block. - -cv_version: "v1.0.3" -cv_release_url: "https://forge.ops.eblu.me/api/packages/eblume/generic/cv/{{ cv_version }}/cv-{{ cv_version }}.tar.gz" - -cv_home: /Users/erichblume/blumeops/cv -cv_content_dir: "{{ cv_home }}/content" -cv_version_sentinel: "{{ cv_home }}/.installed-version" diff --git a/ansible/roles/cv/tasks/main.yml b/ansible/roles/cv/tasks/main.yml deleted file mode 100644 index c254325..0000000 --- a/ansible/roles/cv/tasks/main.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -# cv role — download and extract the CV release tarball into cv_content_dir. -# Caddy serves the directory directly; there is no daemon to manage. -# -# Idempotency: a sentinel file records the installed cv_version. The -# download/extract steps only run when the sentinel doesn't match cv_version. -# -# We use curl rather than ansible.builtin.get_url because the forge generic- -# packages endpoint returns 405 on HEAD requests, which get_url issues before -# downloading. - -- name: Ensure cv home exists - ansible.builtin.file: - path: "{{ cv_home }}" - state: directory - mode: '0755' - -- name: Read installed cv version sentinel - ansible.builtin.slurp: - src: "{{ cv_version_sentinel }}" - register: cv_installed_raw - failed_when: false - changed_when: false - -- name: Set installed cv version fact - ansible.builtin.set_fact: - cv_installed_version: >- - {{ (cv_installed_raw.content | b64decode).strip() - if (cv_installed_raw.content is defined) else '' }} - -- name: Recreate cv content dir - ansible.builtin.file: - path: "{{ cv_content_dir }}" - state: "{{ item }}" - mode: '0755' - loop: - - absent - - directory - when: cv_installed_version != cv_version - -- name: Download and extract cv release tarball - ansible.builtin.shell: - cmd: >- - set -euo pipefail; - curl -fsSL {{ cv_release_url | quote }} -o {{ cv_home }}/cv.tar.gz && - tar -xzf {{ cv_home }}/cv.tar.gz -C {{ cv_content_dir }} && - rm -f {{ cv_home }}/cv.tar.gz - executable: /bin/bash - when: cv_installed_version != cv_version - changed_when: true - -- name: Write cv version sentinel - ansible.builtin.copy: - content: "{{ cv_version }}\n" - dest: "{{ cv_version_sentinel }}" - mode: '0644' - when: cv_installed_version != cv_version diff --git a/ansible/roles/devpi/defaults/main.yml b/ansible/roles/devpi/defaults/main.yml deleted file mode 100644 index 6d52b9b..0000000 --- a/ansible/roles/devpi/defaults/main.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -# devpi PyPI caching mirror (native launchd, replaces minikube StatefulSet) - -devpi_home: /Users/erichblume/devpi -devpi_venv: "{{ devpi_home }}/venv" -devpi_server_dir: "{{ devpi_home }}/server-dir" -devpi_binary: "{{ devpi_venv }}/bin/devpi-server" -devpi_init_binary: "{{ devpi_venv }}/bin/devpi-init" - -devpi_python_version: "3.12" -devpi_server_version: "6.19.3" -devpi_web_version: "5.0.2" - -devpi_host: 127.0.0.1 -devpi_port: 3141 -devpi_outside_url: "https://pypi.ops.eblu.me" - -devpi_log_dir: /Users/erichblume/Library/Logs - -# uv binary on indri — mise shim so version bumps via `mise upgrade uv` flow through transparently -devpi_uv_binary: /Users/erichblume/.local/share/mise/shims/uv diff --git a/ansible/roles/devpi/handlers/main.yml b/ansible/roles/devpi/handlers/main.yml deleted file mode 100644 index 2765850..0000000 --- a/ansible/roles/devpi/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Restart devpi - ansible.builtin.shell: | - launchctl unload ~/Library/LaunchAgents/mcquack.eblume.devpi.plist 2>/dev/null || true - launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi.plist - changed_when: true diff --git a/ansible/roles/devpi/tasks/main.yml b/ansible/roles/devpi/tasks/main.yml deleted file mode 100644 index 985ca46..0000000 --- a/ansible/roles/devpi/tasks/main.yml +++ /dev/null @@ -1,71 +0,0 @@ ---- -# devpi role — devpi-server in a uv-managed venv, run via LaunchAgent. -# Replaces the prior minikube StatefulSet; see [[devpi-on-indri]]. -# -# The root password is fetched in the indri.yml playbook pre_tasks and -# exposed as `devpi_root_password`. - -- name: Ensure devpi home exists - ansible.builtin.file: - path: "{{ devpi_home }}" - state: directory - mode: '0755' - -- name: Ensure devpi server-dir exists - ansible.builtin.file: - path: "{{ devpi_server_dir }}" - state: directory - mode: '0700' - -- name: Create devpi venv if missing - ansible.builtin.command: - cmd: "{{ devpi_uv_binary }} venv --python {{ devpi_python_version }} {{ devpi_venv }}" - creates: "{{ devpi_venv }}/bin/python" - -- name: Install devpi-server and devpi-web into venv - # Always bootstrap from upstream PyPI — devpi is the index it would otherwise resolve through, - # and that's a circular dependency (devpi cannot install itself from itself). - ansible.builtin.command: - cmd: >- - {{ devpi_uv_binary }} pip install - --python {{ devpi_venv }}/bin/python - --index-url https://pypi.org/simple/ - devpi-server=={{ devpi_server_version }} - devpi-web=={{ devpi_web_version }} - register: devpi_pip_install - changed_when: "'Installed' in devpi_pip_install.stdout or 'Uninstalled' in devpi_pip_install.stdout" - notify: Restart devpi - -- name: Check if devpi server-dir is initialized - ansible.builtin.stat: - path: "{{ devpi_server_dir }}/.serverversion" - register: devpi_serverversion - -- name: Initialize devpi server-dir - ansible.builtin.command: - cmd: >- - {{ devpi_init_binary }} - --serverdir {{ devpi_server_dir }} - --root-passwd {{ devpi_root_password }} - when: not devpi_serverversion.stat.exists - changed_when: true - no_log: true - -- name: Deploy devpi LaunchAgent plist - ansible.builtin.template: - src: devpi.plist.j2 - dest: ~/Library/LaunchAgents/mcquack.eblume.devpi.plist - mode: '0644' - notify: Restart devpi - -- name: Check if devpi LaunchAgent is loaded - ansible.builtin.command: launchctl list mcquack.eblume.devpi - register: devpi_launchctl_check - changed_when: false - failed_when: false - -- name: Load devpi LaunchAgent if not loaded - ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi.plist - when: devpi_launchctl_check.rc != 0 - changed_when: true - failed_when: false diff --git a/ansible/roles/devpi/templates/devpi.plist.j2 b/ansible/roles/devpi/templates/devpi.plist.j2 deleted file mode 100644 index b9485e6..0000000 --- a/ansible/roles/devpi/templates/devpi.plist.j2 +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - Label - mcquack.eblume.devpi - ProgramArguments - - {{ devpi_binary }} - --serverdir - {{ devpi_server_dir }} - --host - {{ devpi_host }} - --port - {{ devpi_port }} - --outside-url - {{ devpi_outside_url }} - - RunAtLoad - - KeepAlive - - EnvironmentVariables - - PATH - {{ devpi_venv }}/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin - - StandardOutPath - {{ devpi_log_dir }}/mcquack.devpi.out.log - StandardErrorPath - {{ devpi_log_dir }}/mcquack.devpi.err.log - - diff --git a/ansible/roles/docs/defaults/main.yml b/ansible/roles/docs/defaults/main.yml deleted file mode 100644 index a5a1a8a..0000000 --- a/ansible/roles/docs/defaults/main.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# Docs (Quartz-built static site) — replaces minikube Deployment. -# Caddy serves docs_content_dir directly via the static-kind service block, -# with Quartz-style try_files (path → path/ → path.html → 404). - -docs_version: "v1.17.0" -docs_release_url: "https://forge.eblu.me/eblume/blumeops/releases/download/{{ docs_version }}/docs-{{ docs_version }}.tar.gz" -docs_home: /Users/erichblume/blumeops/docs -docs_content_dir: "{{ docs_home }}/content" -docs_version_sentinel: "{{ docs_home }}/.installed-version" diff --git a/ansible/roles/docs/tasks/main.yml b/ansible/roles/docs/tasks/main.yml deleted file mode 100644 index dec775e..0000000 --- a/ansible/roles/docs/tasks/main.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -# docs role — download and extract the Quartz-built docs tarball into -# docs_content_dir. Caddy serves the directory directly with Quartz-style -# try_files; there is no daemon to manage. -# -# Idempotency: a sentinel file records the installed docs_version. The -# download/extract steps only run when the sentinel doesn't match docs_version. -# -# Mirrors the cv role's curl-based download for consistency, even though the -# forge releases endpoint here does support HEAD. - -- name: Ensure docs home exists - ansible.builtin.file: - path: "{{ docs_home }}" - state: directory - mode: '0755' - -- name: Read installed docs version sentinel - ansible.builtin.slurp: - src: "{{ docs_version_sentinel }}" - register: docs_installed_raw - failed_when: false - changed_when: false - -- name: Set installed docs version fact - ansible.builtin.set_fact: - docs_installed_version: >- - {{ (docs_installed_raw.content | b64decode).strip() - if (docs_installed_raw.content is defined) else '' }} - -- name: Recreate docs content dir - ansible.builtin.file: - path: "{{ docs_content_dir }}" - state: "{{ item }}" - mode: '0755' - loop: - - absent - - directory - when: docs_installed_version != docs_version - -- name: Download and extract docs release tarball - ansible.builtin.shell: - cmd: >- - set -euo pipefail; - curl -fsSL {{ docs_release_url | quote }} -o {{ docs_home }}/docs.tar.gz && - tar -xzf {{ docs_home }}/docs.tar.gz -C {{ docs_content_dir }} && - rm -f {{ docs_home }}/docs.tar.gz - executable: /bin/bash - when: docs_installed_version != docs_version - changed_when: true - -- name: Write docs version sentinel - ansible.builtin.copy: - content: "{{ docs_version }}\n" - dest: "{{ docs_version_sentinel }}" - mode: '0644' - when: docs_installed_version != docs_version diff --git a/ansible/roles/forgejo/defaults/main.yml b/ansible/roles/forgejo/defaults/main.yml index a178d99..435caf1 100644 --- a/ansible/roles/forgejo/defaults/main.yml +++ b/ansible/roles/forgejo/defaults/main.yml @@ -4,21 +4,16 @@ forgejo_app_name: Forgejo forgejo_app_slogan: "Beyond coding. We Forge." -forgejo_run_user: erichblume +forgejo_run_user: forgejo forgejo_run_mode: prod -# Source build paths -forgejo_repo_dir: /Users/erichblume/code/3rd/forgejo -forgejo_binary: "{{ forgejo_repo_dir }}/forgejo" - -# Data paths (migrated from brew to ~/forgejo) -forgejo_work_path: /Users/erichblume/forgejo +# Paths (brew-managed for now, will change to mcquack per migrate-forgejo-from-brew) +forgejo_work_path: /opt/homebrew/var/forgejo forgejo_config_path: "{{ forgejo_work_path }}/custom/conf/app.ini" forgejo_data_path: "{{ forgejo_work_path }}/data" forgejo_repo_root: "{{ forgejo_data_path }}/forgejo-repositories" forgejo_lfs_path: "{{ forgejo_data_path }}/lfs" forgejo_log_path: "{{ forgejo_work_path }}/log" -forgejo_log_dir: /Users/erichblume/Library/Logs # Server settings forgejo_http_addr: 0.0.0.0 diff --git a/ansible/roles/forgejo/handlers/main.yml b/ansible/roles/forgejo/handlers/main.yml index c00a3dd..dc67cbc 100644 --- a/ansible/roles/forgejo/handlers/main.yml +++ b/ansible/roles/forgejo/handlers/main.yml @@ -1,6 +1,4 @@ --- - name: Restart forgejo - ansible.builtin.shell: | - launchctl unload ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist 2>/dev/null || true - launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist + ansible.builtin.command: brew services restart forgejo changed_when: true diff --git a/ansible/roles/forgejo/tasks/main.yml b/ansible/roles/forgejo/tasks/main.yml index 7cafb12..a6d27b9 100644 --- a/ansible/roles/forgejo/tasks/main.yml +++ b/ansible/roles/forgejo/tasks/main.yml @@ -1,34 +1,16 @@ --- -# Forgejo role — source-built binary with LaunchAgent +# Forgejo role # -# ONE-TIME SETUP (before running ansible): -# -# 1. Clone forgejo from codeberg (avoid circular dependency): -# ssh indri 'git clone https://codeberg.org/forgejo/forgejo.git ~/code/3rd/forgejo' -# -# 2. Add forge mirror as secondary remote: -# ssh indri 'cd ~/code/3rd/forgejo && git remote add forge https://forge.eblu.me/mirrors/forgejo.git' -# -# 3. Build (mise.toml handles Go/Node versions and build tags): -# ssh indri 'cd ~/code/3rd/forgejo && mise run build' -# -# 4. Run ansible to deploy config and LaunchAgent +# Currently uses brew-managed forgejo. Phase 3 of ci-cd-bootstrap will +# transition to mcquack LaunchAgent with CI-built binary. # # Secrets (lfs_jwt_secret, internal_token, oauth2_jwt_secret) are fetched # from 1Password in the playbook pre_tasks. -- name: Verify forgejo binary exists - ansible.builtin.stat: - path: "{{ forgejo_binary }}" - register: forgejo_binary_stat - -- name: Fail if forgejo binary not found - ansible.builtin.fail: - msg: | - Forgejo binary not found at {{ forgejo_binary }}. - Please build from source first: - ssh indri 'cd ~/code/3rd/forgejo && mise run build' - when: not forgejo_binary_stat.stat.exists +- name: Install forgejo via homebrew + community.general.homebrew: + name: forgejo + state: present - name: Ensure forgejo config directory exists ansible.builtin.file: @@ -43,21 +25,8 @@ mode: '0600' notify: Restart forgejo -- name: Deploy forgejo LaunchAgent plist - ansible.builtin.template: - src: forgejo.plist.j2 - dest: ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist - mode: '0644' - notify: Restart forgejo - -- name: Check if forgejo LaunchAgent is loaded - ansible.builtin.command: launchctl list mcquack.eblume.forgejo - register: forgejo_launchctl_check - changed_when: false - failed_when: false - -- name: Load forgejo LaunchAgent if not loaded - ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist - when: forgejo_launchctl_check.rc != 0 - changed_when: true +- name: Ensure forgejo service is started + ansible.builtin.command: brew services start forgejo + register: forgejo_brew_start + changed_when: "'Successfully started' in forgejo_brew_start.stdout" failed_when: false diff --git a/ansible/roles/forgejo/templates/app.ini.j2 b/ansible/roles/forgejo/templates/app.ini.j2 index 9c5b4d5..de931be 100644 --- a/ansible/roles/forgejo/templates/app.ini.j2 +++ b/ansible/roles/forgejo/templates/app.ini.j2 @@ -61,12 +61,6 @@ MIN_INTERVAL = 10m [cron.update_checker] ENABLED = false -[cron.archive_cleanup] -ENABLED = true -RUN_AT_START = true -SCHEDULE = @midnight -OLDER_THAN = 2h - [session] PROVIDER = {{ forgejo_session_provider }} @@ -95,11 +89,6 @@ ACCOUNT_LINKING = login USERNAME = nickname REGISTER_EMAIL_CONFIRM = false -[metrics] -ENABLED = true -ENABLED_ISSUE_BY_LABEL = false -ENABLED_ISSUE_BY_REPOSITORY = false - [actions] ENABLED = {{ forgejo_actions_enabled | lower }} DEFAULT_ACTIONS_URL = {{ forgejo_actions_default_url }} diff --git a/ansible/roles/forgejo/templates/forgejo.plist.j2 b/ansible/roles/forgejo/templates/forgejo.plist.j2 deleted file mode 100644 index 85d63f3..0000000 --- a/ansible/roles/forgejo/templates/forgejo.plist.j2 +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - Label - mcquack.eblume.forgejo - ProgramArguments - - {{ forgejo_binary }} - -w - {{ forgejo_work_path }} - -c - {{ forgejo_config_path }} - web - - RunAtLoad - - KeepAlive - - StandardOutPath - {{ forgejo_log_dir }}/mcquack.forgejo.out.log - StandardErrorPath - {{ forgejo_log_dir }}/mcquack.forgejo.err.log - - diff --git a/ansible/roles/heph/defaults/main.yml b/ansible/roles/heph/defaults/main.yml deleted file mode 100644 index 88d2240..0000000 --- a/ansible/roles/heph/defaults/main.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -# hephaestus hub — the canonical heph replica (server mode) on indri. -# Other devices (e.g. gilbert) are spokes that sync against this hub. -# See [[set-up-sync-hub]] and [[host-heph-pwa]] in the hephaestus repo. - -# Pinned release used for the initial `cargo install` and the PWA shell. -# After bootstrap, hephd's own --self-update keeps the binary current; this -# pin only governs the first install and the bundled PWA shell version. -heph_version: v1.2.1 - -# Anonymous public HTTPS clone — matches hephd's INSTALL_GIT_URL so the initial -# install and unattended self-update build from the same source (no ssh-agent). -heph_repo_url: https://forge.eblu.me/eblume/hephaestus.git - -heph_bin_dir: /Users/erichblume/.cargo/bin -heph_binary: "{{ heph_bin_dir }}/hephd" - -# rustc/cargo here are rustup shims. The bare (non-mise) environment that the -# launchagent and ansible run in falls back to rustup's *default* toolchain, -# which can lag behind heph's rust-version floor (Cargo.toml: 1.89). Pin the -# channel explicitly so both the bootstrap build and unattended self-update -# always use a current toolchain regardless of the host's rustup default. -heph_rust_toolchain: stable - -heph_data_dir: /Users/erichblume/.local/share/heph -heph_db: "{{ heph_data_dir }}/heph.db" -heph_socket: "{{ heph_data_dir }}/hephd.sock" -heph_log_dir: /Users/erichblume/Library/Logs - -# Version-pinned source checkout; the PWA static shell is served directly from -# its heph-pwa/ subdir (no copy), keeping shell and hub in lockstep at heph_version. -heph_pwa_src_dir: /Users/erichblume/.cache/heph-pwa-src -heph_web_root: "{{ heph_pwa_src_dir }}/heph-pwa" - -# Hub listens on all interfaces so tailnet spokes can reach it directly -# (http://indri.tail8d86e.ts.net:8787) and Caddy can proxy heph.ops.eblu.me. -# Access is gated by Authentik OIDC regardless — tailnet reachability is not -# enough (this is the owner's most sensitive data). -heph_http_addr: 0.0.0.0:8787 -heph_port: 8787 -heph_external_url: https://heph.ops.eblu.me - -# Authentik OIDC — issuer + audience together turn hub auth on. The audience is -# the device-code client id (see argocd/manifests/authentik heph blueprint). -heph_oidc_issuer: https://authentik.ops.eblu.me/application/o/heph/ -heph_oidc_audience: heph - -# Self-update poll interval (seconds). 10 minutes. -heph_self_update_interval_secs: 600 diff --git a/ansible/roles/heph/handlers/main.yml b/ansible/roles/heph/handlers/main.yml deleted file mode 100644 index 92fe9d7..0000000 --- a/ansible/roles/heph/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Restart heph - ansible.builtin.shell: | - launchctl unload ~/Library/LaunchAgents/mcquack.eblume.heph.plist 2>/dev/null || true - launchctl load ~/Library/LaunchAgents/mcquack.eblume.heph.plist - changed_when: true diff --git a/ansible/roles/heph/tasks/main.yml b/ansible/roles/heph/tasks/main.yml deleted file mode 100644 index 7a45fe3..0000000 --- a/ansible/roles/heph/tasks/main.yml +++ /dev/null @@ -1,82 +0,0 @@ ---- -# hephaestus hub (server mode) on indri. -# -# DATA SEEDING (one-time, Path A — do this BEFORE the first provision so the hub -# adopts gilbert's existing data instead of being born empty): -# -# 1. On the seed device (gilbert): heph daemon stop -# 2. Copy its store to indri: scp ~/.local/share/heph/heph.db \ -# indri:~/.local/share/heph/heph.db -# 3. On indri, give the hub its OWN device origin (keeps gilbert's owner_id + -# data; hephd regenerates a fresh origin on next start when it is missing): -# sqlite3 ~/.local/share/heph/heph.db "DELETE FROM meta WHERE key='origin';" -# 4. Run this role (installs hephd, stages the PWA, loads the launchagent). -# -# hephd auto-creates an empty store on first start if none exists, so seeding is -# optional — skip it only if you intend a fresh, empty hub. - -- name: Ensure heph data directory exists - ansible.builtin.file: - path: "{{ heph_data_dir }}" - state: directory - mode: '0700' - -- name: Check for installed hephd binary - ansible.builtin.stat: - path: "{{ heph_binary }}" - register: heph_binary_stat - -# Bootstrap install only when hephd is absent. Thereafter hephd's own -# --self-update keeps it current; ansible must not fight (or downgrade) it. -# This builds from source and can take several minutes on a cold cargo cache. -- name: Bootstrap-install heph + hephd from the forge ({{ heph_version }}) - ansible.builtin.command: - cmd: >- - {{ heph_bin_dir }}/cargo install --locked - --git {{ heph_repo_url }} - --tag {{ heph_version }} - heph hephd - environment: - PATH: "{{ heph_bin_dir }}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" - RUSTUP_TOOLCHAIN: "{{ heph_rust_toolchain }}" - when: not heph_binary_stat.stat.exists - changed_when: true - notify: Restart heph - -# Checkout provides the PWA shell at {{ heph_web_root }} (heph-pwa/ subdir), -# served directly by hephd. Static files are read from disk per request, so a -# version bump needs no restart; the service worker (CACHE = "heph-pwa-vN") -# evicts stale assets on next load. -- name: Ensure heph cache parent directory exists - ansible.builtin.file: - path: "{{ heph_pwa_src_dir | dirname }}" - state: directory - mode: '0755' - -- name: Stage heph-pwa source at {{ heph_version }} - ansible.builtin.git: - repo: "{{ heph_repo_url }}" - dest: "{{ heph_pwa_src_dir }}" - version: "{{ heph_version }}" - depth: 1 - single_branch: true - force: true - -- name: Deploy heph LaunchAgent plist - ansible.builtin.template: - src: heph.plist.j2 - dest: ~/Library/LaunchAgents/mcquack.eblume.heph.plist - mode: '0644' - notify: Restart heph - -- name: Check if heph LaunchAgent is loaded - ansible.builtin.command: launchctl list mcquack.eblume.heph - register: heph_launchctl_check - changed_when: false - failed_when: false - -- name: Load heph LaunchAgent if not loaded - ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.heph.plist - when: heph_launchctl_check.rc != 0 - changed_when: true - failed_when: false diff --git a/ansible/roles/heph/templates/heph.plist.j2 b/ansible/roles/heph/templates/heph.plist.j2 deleted file mode 100644 index 19a2367..0000000 --- a/ansible/roles/heph/templates/heph.plist.j2 +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - Label - mcquack.eblume.heph - ProgramArguments - - {{ heph_binary }} - --mode - server - --http-addr - {{ heph_http_addr }} - --db - {{ heph_db }} - --socket - {{ heph_socket }} - --web-root - {{ heph_web_root }} - --oidc-issuer - {{ heph_oidc_issuer }} - --oidc-audience - {{ heph_oidc_audience }} - --self-update - --self-update-interval-secs - {{ heph_self_update_interval_secs }} - - RunAtLoad - - KeepAlive - - EnvironmentVariables - - - PATH - {{ heph_bin_dir }}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin - HOME - /Users/erichblume - - RUSTUP_TOOLCHAIN - {{ heph_rust_toolchain }} - - StandardOutPath - {{ heph_log_dir }}/mcquack.heph.out.log - StandardErrorPath - {{ heph_log_dir }}/mcquack.heph.err.log - - diff --git a/ansible/roles/minikube/tasks/main.yml b/ansible/roles/minikube/tasks/main.yml index e79f4de..c09d420 100644 --- a/ansible/roles/minikube/tasks/main.yml +++ b/ansible/roles/minikube/tasks/main.yml @@ -77,37 +77,6 @@ msg: "WARNING: minikube may not have started properly. Run 'minikube start' manually on indri if needed. Status: {{ minikube_final_status.stdout | default('unknown') }}" when: minikube_final_status.rc != 0 -# The storage-provisioner is a bare Pod (no controller). If the node restarts -# via Docker Desktop rather than `minikube start`, kubelet brings back static -# pods (apiserver, etcd) but bare pods like storage-provisioner are lost. -# `minikube start` on a running cluster is safe and re-applies all addons. -- name: Check storage-provisioner pod is running - ansible.builtin.command: - cmd: kubectl -n kube-system get pod storage-provisioner -o jsonpath='{.status.phase}' - register: minikube_storage_provisioner - changed_when: false - failed_when: false - when: minikube_final_status.rc == 0 - -- name: Re-run minikube start to restore addons - ansible.builtin.command: - cmd: > - minikube start - --driver={{ minikube_driver }} - --container-runtime={{ minikube_container_runtime }} - --cpus={{ minikube_cpus }} - --memory={{ minikube_memory }} - --disk-size={{ minikube_disk_size }} - {% for name in minikube_apiserver_names %} - --apiserver-names={{ name }} - {% endfor %} - --apiserver-port={{ minikube_apiserver_port }} - --listen-address={{ minikube_listen_address }} - when: - - minikube_final_status.rc == 0 - - minikube_storage_provisioner.stdout | default('') != 'Running' - changed_when: true - # Configure containerd to use zot registry as pull-through cache # With docker driver, use host.minikube.internal to reach the host # Zot runs on indri:5050 and caches images from docker.io, ghcr.io, quay.io diff --git a/argocd/apps/1password-connect-ringtail.yaml b/argocd/apps/1password-connect-ringtail.yaml index 60c6e43..620bfab 100644 --- a/argocd/apps/1password-connect-ringtail.yaml +++ b/argocd/apps/1password-connect-ringtail.yaml @@ -1,5 +1,5 @@ # 1Password Connect for ringtail k3s cluster -# Same manifests as indri, different destination +# Same chart/values as indri, different destination # # Prerequisites: # 1. Bootstrap secrets via ansible (provision-ringtail creates 1password namespace, @@ -13,10 +13,17 @@ metadata: namespace: argocd spec: project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/1password-connect + sources: + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/connect-helm-charts.git + targetRevision: connect-2.3.0 + path: charts/connect + helm: + releaseName: onepassword-connect + valueFiles: + - $values/argocd/manifests/1password-connect/values.yaml + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + ref: values destination: server: https://ringtail.tail8d86e.ts.net:6443 namespace: 1password diff --git a/argocd/apps/1password-connect.yaml b/argocd/apps/1password-connect.yaml index ba0a474..4831868 100644 --- a/argocd/apps/1password-connect.yaml +++ b/argocd/apps/1password-connect.yaml @@ -1,7 +1,7 @@ # 1Password Connect - Secrets Automation Server # Provides REST API access to 1Password vault items for External Secrets Operator # -# Manifests rendered from connect-helm-charts v2.4.1, maintained as plain kustomize. +# Chart mirrored from https://github.com/1Password/connect-helm-charts # # Prerequisites (one-time setup): # 1. Create Connect server: op connect server create blumeops --vaults blumeops @@ -19,10 +19,17 @@ metadata: namespace: argocd spec: project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/1password-connect + sources: + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/connect-helm-charts.git + targetRevision: connect-2.3.0 + path: charts/connect + helm: + releaseName: onepassword-connect + valueFiles: + - $values/argocd/manifests/1password-connect/values.yaml + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + ref: values destination: server: https://kubernetes.default.svc namespace: 1password diff --git a/argocd/apps/cloudnative-pg-ringtail.yaml b/argocd/apps/cloudnative-pg-ringtail.yaml deleted file mode 100644 index fa7bba0..0000000 --- a/argocd/apps/cloudnative-pg-ringtail.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# CloudNativePG Operator for ringtail k3s cluster -# Deploys the operator only; PostgreSQL clusters are created separately -# -# Sibling of cloudnative-pg.yaml (minikube). Same mirror, same release, -# different destination. Both apps will coexist during the immich -# migration; the minikube one is removed at the end of the broader -# indri-k8s decommission. -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: cloudnative-pg-ringtail - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/cloudnative-pg.git - targetRevision: v1.27.1 - path: releases - directory: - include: 'cnpg-1.27.1.yaml' - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: cnpg-system - syncPolicy: - syncOptions: - - CreateNamespace=true - - ServerSideApply=true # Required for large CRDs that exceed annotation size limit diff --git a/argocd/apps/kingfisher.yaml b/argocd/apps/cv.yaml similarity index 68% rename from argocd/apps/kingfisher.yaml rename to argocd/apps/cv.yaml index ad659eb..ad09a8d 100644 --- a/argocd/apps/kingfisher.yaml +++ b/argocd/apps/cv.yaml @@ -1,17 +1,18 @@ +--- apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: kingfisher + name: cv namespace: argocd spec: project: default source: repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git targetRevision: main - path: argocd/manifests/kingfisher + path: argocd/manifests/cv destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: kingfisher + server: https://kubernetes.default.svc + namespace: cv syncPolicy: syncOptions: - CreateNamespace=true diff --git a/argocd/apps/databases-ringtail.yaml b/argocd/apps/databases-ringtail.yaml deleted file mode 100644 index 00de4e3..0000000 --- a/argocd/apps/databases-ringtail.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# Databases on ringtail k3s. -# -# Today: only immich-pg (CNPG Cluster) + its borgmatic ExternalSecret. -# More databases may move here as the indri-k8s decommission proceeds. -# -# Prerequisites: -# - cloudnative-pg-ringtail (operator must exist before the Cluster CR) -# - external-secrets-ringtail + 1password-connect-ringtail (for the -# immich-pg-borgmatic ExternalSecret to sync) -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: databases-ringtail - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/databases-ringtail - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: databases - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/apps/devpi.yaml b/argocd/apps/devpi.yaml new file mode 100644 index 0000000..4a15672 --- /dev/null +++ b/argocd/apps/devpi.yaml @@ -0,0 +1,29 @@ +# devpi PyPI Caching Proxy +# Provides PyPI cache and private package hosting +# +# After first deployment, initialize devpi: +# kubectl -n devpi exec -it devpi-0 -- devpi-init --serverdir /devpi --root-passwd +# kubectl -n devpi rollout restart statefulset devpi +# +# Then create user/index: +# uvx devpi use https://pypi.tail8d86e.ts.net +# uvx devpi login root +# uvx devpi user -c eblume email=blume.erich@gmail.com +# uvx devpi index -c eblume/dev bases=root/pypi +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: devpi + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/devpi + destination: + server: https://kubernetes.default.svc + namespace: devpi + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/docs.yaml b/argocd/apps/docs.yaml new file mode 100644 index 0000000..cd8db35 --- /dev/null +++ b/argocd/apps/docs.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: docs + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/docs + destination: + server: https://kubernetes.default.svc + namespace: docs + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/external-secrets-ringtail.yaml b/argocd/apps/external-secrets-ringtail.yaml index 0bb8bd7..e2f5898 100644 --- a/argocd/apps/external-secrets-ringtail.yaml +++ b/argocd/apps/external-secrets-ringtail.yaml @@ -15,7 +15,7 @@ spec: source: repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git targetRevision: main - path: argocd/manifests/external-secrets-ringtail + path: argocd/manifests/external-secrets destination: server: https://ringtail.tail8d86e.ts.net:6443 namespace: external-secrets diff --git a/argocd/apps/homepage.yaml b/argocd/apps/homepage.yaml index 22147f2..86a0f8d 100644 --- a/argocd/apps/homepage.yaml +++ b/argocd/apps/homepage.yaml @@ -14,7 +14,7 @@ spec: targetRevision: main path: argocd/manifests/homepage destination: - server: https://ringtail.tail8d86e.ts.net:6443 + server: https://kubernetes.default.svc namespace: homepage syncPolicy: syncOptions: diff --git a/argocd/apps/immich-ringtail.yaml b/argocd/apps/immich-ringtail.yaml deleted file mode 100644 index c93cbee..0000000 --- a/argocd/apps/immich-ringtail.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# Immich on ringtail k3s. -# -# Staging deployment; the minikube `immich` app remains in parallel -# until cutover. See [[immich-cutover-and-decommission]] for the -# routing flip + minikube cleanup. -# -# Prerequisites: -# - cnpg-on-ringtail + databases-ringtail (postgres) -# - 1password-connect-ringtail + external-secrets-ringtail (not used -# by this app today — immich-db Secret is created manually, -# matching the minikube pattern) -# - The immich-db Secret in the immich namespace, holding the -# password for the `immich` postgres role (copied from the source -# immich-pg-app Secret at migration time). -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: immich-ringtail - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/immich-ringtail - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: immich - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/apps/immich-storage.yaml b/argocd/apps/immich-storage.yaml new file mode 100644 index 0000000..7227681 --- /dev/null +++ b/argocd/apps/immich-storage.yaml @@ -0,0 +1,25 @@ +# Immich Storage - PersistentVolume and PVC for photo library +# Must be synced BEFORE the main immich app +# +# Prerequisites: +# 1. NFS share on sifaka at /volume1/photos with permissions for indri +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: immich-storage + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/immich + # Only deploy storage resources (PV/PVC/Ingress), not Helm values.yaml + directory: + include: "{pv-nfs.yaml,pvc.yaml,ingress-tailscale.yaml}" + destination: + server: https://kubernetes.default.svc + namespace: immich + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/immich.yaml b/argocd/apps/immich.yaml new file mode 100644 index 0000000..22b95cc --- /dev/null +++ b/argocd/apps/immich.yaml @@ -0,0 +1,37 @@ +# Immich - Self-hosted photo and video management +# High-performance Google Photos/iCloud alternative with AI features +# +# Chart mirrored from https://github.com/immich-app/immich-charts to forge +# +# Prerequisites: +# 1. Mirror immich-charts to forge: https://github.com/immich-app/immich-charts +# 2. Create immich namespace and secrets: +# kubectl create namespace immich +# op inject -i argocd/manifests/immich/secret-db.yaml.tpl | kubectl apply -f - +# 3. Create immich-pg database and user (see immich-pg app) +# 4. Mount photos directory from indri to minikube +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: immich + namespace: argocd +spec: + project: default + sources: + # Helm chart from forge mirror (SSH via egress) + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/immich-charts.git + targetRevision: immich-0.10.3 + path: charts/immich + helm: + releaseName: immich + valueFiles: + - $values/argocd/manifests/immich/values.yaml + - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: immich + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/mealie-ringtail.yaml b/argocd/apps/mealie-ringtail.yaml deleted file mode 100644 index 2f014a9..0000000 --- a/argocd/apps/mealie-ringtail.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# Mealie on ringtail k3s. -# -# Wave-1 indri-k8s decommission. Staging deployment; the minikube `mealie` -# app stays in parallel until cutover (copy SQLite PVC, drop the minikube -# tailscale ingress, flip Caddy). See [[migrate-wave1-ringtail]]. -# -# Prerequisites: -# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore) -# - mealie-data PVC contents copied from minikube at cutover -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: mealie-ringtail - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/mealie-ringtail - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: mealie - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/apps/mealie.yaml b/argocd/apps/mealie.yaml new file mode 100644 index 0000000..af33469 --- /dev/null +++ b/argocd/apps/mealie.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: mealie + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/mealie + destination: + server: https://kubernetes.default.svc + namespace: mealie + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/paperless-ringtail.yaml b/argocd/apps/paperless-ringtail.yaml deleted file mode 100644 index bec98e9..0000000 --- a/argocd/apps/paperless-ringtail.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Paperless-ngx on ringtail k3s. -# -# Wave-1 indri-k8s decommission. Staging deployment; the minikube -# `paperless` app stays in parallel until cutover (drop the minikube -# tailscale ingress to free the name, then flip Caddy). See -# [[migrate-wave1-ringtail]]. -# -# Prerequisites: -# - databases-ringtail blumeops-pg (paperless database + role) -# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore) -# - sifaka NFS rule granting ringtail access to /volume1/paperless -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: paperless-ringtail - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/paperless-ringtail - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: paperless - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/apps/shower.yaml b/argocd/apps/shower.yaml deleted file mode 100644 index c4a7a62..0000000 --- a/argocd/apps/shower.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Adelaide / Heidi / Addie baby shower app — Django guest/raffle/prize system. -# Public landing page at shower.eblu.me (via fly proxy), staff console + admin -# at shower.ops.eblu.me (tailnet only). Built from forge PyPI wheel. -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: shower - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/shower - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: shower - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/apps/teslamate-ringtail.yaml b/argocd/apps/teslamate-ringtail.yaml deleted file mode 100644 index b7b3491..0000000 --- a/argocd/apps/teslamate-ringtail.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# TeslaMate on ringtail k3s. -# -# Wave-1 indri-k8s decommission. Staging deployment; the minikube -# `teslamate` app stays in parallel until cutover (migrate the teslamate -# database, drop the minikube tailscale ingress, flip Caddy). See -# [[migrate-wave1-ringtail]]. -# -# Prerequisites: -# - databases-ringtail blumeops-pg (teslamate database + role; cube + -# earthdistance extensions created by superuser at cutover) -# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore) -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: teslamate-ringtail - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/teslamate-ringtail - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: teslamate - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/apps/teslamate.yaml b/argocd/apps/teslamate.yaml new file mode 100644 index 0000000..60247da --- /dev/null +++ b/argocd/apps/teslamate.yaml @@ -0,0 +1,32 @@ +# TeslaMate Tesla Data Logger +# Requires: CloudNativePG PostgreSQL cluster and manual secret setup +# +# Before syncing, create the namespace and secrets: +# kubectl create namespace teslamate +# op inject -i argocd/manifests/databases/secret-teslamate.yaml.tpl | kubectl apply -f - +# op inject -i argocd/manifests/teslamate/secret-encryption-key.yaml.tpl | kubectl apply -f - +# op inject -i argocd/manifests/teslamate/secret-db.yaml.tpl | kubectl apply -f - +# +# Then create the database: +# PGPASSWORD=$(op read "op://blumeops/postgres/password") \ +# psql -h pg.ops.eblu.me -U eblume -c "CREATE DATABASE teslamate OWNER teslamate;" +# +# After syncing, access the TeslaMate UI at https://tesla.tail8d86e.ts.net to complete +# Tesla API authentication via OAuth flow. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: teslamate + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/teslamate + destination: + server: https://kubernetes.default.svc + namespace: teslamate + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/1password-connect/README.md b/argocd/manifests/1password-connect/README.md index 26989f3..29e6748 100644 --- a/argocd/manifests/1password-connect/README.md +++ b/argocd/manifests/1password-connect/README.md @@ -55,15 +55,6 @@ op inject -i argocd/manifests/1password-connect/secret-credentials.yaml.tpl | \ kubectl --context=minikube-indri apply -f - ``` -## Version Management - -Image versions are pinned in `kustomization.yaml` via `images[].newTag`. To upgrade: - -1. Update `newTag` for both `1password/connect-api` and `1password/connect-sync` -2. Sync via ArgoCD - -The manifests were rendered from `connect-helm-charts v2.4.1` and are maintained as plain kustomize. - ## Deployment ```bash diff --git a/argocd/manifests/1password-connect/deployment.yaml b/argocd/manifests/1password-connect/deployment.yaml deleted file mode 100644 index 3296e19..0000000 --- a/argocd/manifests/1password-connect/deployment.yaml +++ /dev/null @@ -1,131 +0,0 @@ -# Rendered from connect-helm-charts v2.4.1 with blumeops values, then de-Helmed. -# Image tags managed by kustomization.yaml images[] — do not edit here. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: onepassword-connect - namespace: 1password - labels: - app.kubernetes.io/component: connect - app.kubernetes.io/name: connect -spec: - replicas: 1 - selector: - matchLabels: - app: onepassword-connect - template: - metadata: - labels: - app: onepassword-connect - app.kubernetes.io/component: connect - spec: - securityContext: - fsGroup: 999 - runAsGroup: 999 - runAsNonRoot: true - runAsUser: 999 - seccompProfile: - type: RuntimeDefault - volumes: - - name: shared-data - emptyDir: {} - - name: credentials - secret: - secretName: op-credentials - items: - - key: 1password-credentials.json - path: 1password-credentials.json - containers: - - name: connect-api - image: 1password/connect-api:kustomized - imagePullPolicy: IfNotPresent - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - resources: - limits: - cpu: 200m - memory: 256Mi - requests: - cpu: 50m - memory: 64Mi - env: - - name: OP_SESSION - value: /home/opuser/.op/1password-credentials.json - - name: OP_BUS_PORT - value: "11220" - - name: OP_BUS_PEERS - value: localhost:11221 - - name: OP_HTTP_PORT - value: "8080" - - name: OP_LOG_LEVEL - value: "info" - readinessProbe: - httpGet: - path: /health - scheme: HTTP - port: 8080 - initialDelaySeconds: 15 - livenessProbe: - httpGet: - path: /heartbeat - scheme: HTTP - port: 8080 - failureThreshold: 3 - periodSeconds: 30 - initialDelaySeconds: 15 - volumeMounts: - - mountPath: /home/opuser/.op/data - name: shared-data - - name: credentials - mountPath: /home/opuser/.op/1password-credentials.json - subPath: 1password-credentials.json - - name: connect-sync - image: 1password/connect-sync:kustomized - imagePullPolicy: IfNotPresent - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - resources: - limits: - cpu: 200m - memory: 256Mi - requests: - cpu: 50m - memory: 64Mi - env: - - name: OP_HTTP_PORT - value: "8081" - - name: OP_SESSION - value: /home/opuser/.op/1password-credentials.json - - name: OP_BUS_PORT - value: "11221" - - name: OP_BUS_PEERS - value: localhost:11220 - - name: OP_LOG_LEVEL - value: "info" - readinessProbe: - httpGet: - path: /health - port: 8081 - initialDelaySeconds: 15 - livenessProbe: - httpGet: - path: /heartbeat - port: 8081 - scheme: HTTP - failureThreshold: 3 - periodSeconds: 30 - initialDelaySeconds: 15 - volumeMounts: - - mountPath: /home/opuser/.op/data - name: shared-data - - name: credentials - mountPath: /home/opuser/.op/1password-credentials.json - subPath: 1password-credentials.json diff --git a/argocd/manifests/1password-connect/kustomization.yaml b/argocd/manifests/1password-connect/kustomization.yaml deleted file mode 100644 index d6da84d..0000000 --- a/argocd/manifests/1password-connect/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: 1password - -resources: - - deployment.yaml - - service.yaml - -images: - - name: 1password/connect-api - newTag: "1.8.2" - - name: 1password/connect-sync - newTag: "1.8.2" diff --git a/argocd/manifests/1password-connect/service.yaml b/argocd/manifests/1password-connect/service.yaml deleted file mode 100644 index 1ea8a7e..0000000 --- a/argocd/manifests/1password-connect/service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# Rendered from connect-helm-charts v2.4.1, then de-Helmed. -apiVersion: v1 -kind: Service -metadata: - name: onepassword-connect - namespace: 1password - labels: - app.kubernetes.io/component: connect - app.kubernetes.io/name: connect -spec: - type: ClusterIP - selector: - app: onepassword-connect - ports: - - port: 8081 - name: connect-sync - - port: 8080 - name: connect-api diff --git a/argocd/manifests/1password-connect/values.yaml b/argocd/manifests/1password-connect/values.yaml new file mode 100644 index 0000000..443290b --- /dev/null +++ b/argocd/manifests/1password-connect/values.yaml @@ -0,0 +1,33 @@ +# 1Password Connect Helm values for blumeops +# Chart: https://github.com/1Password/connect-helm-charts +# +# The credentials are bootstrapped manually via secret-credentials.yaml.tpl +# before deploying this chart. + +connect: + # Use pre-created credentials secret (from bootstrap) + credentialsKey: 1password-credentials.json + credentialsName: op-credentials + + # Resource limits for minikube + api: + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + + sync: + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + +# We don't use the 1Password Operator (using External Secrets instead) +operator: + create: false diff --git a/argocd/manifests/alloy-k8s/config.alloy b/argocd/manifests/alloy-k8s/config.alloy index 2940b0b..a716ddc 100644 --- a/argocd/manifests/alloy-k8s/config.alloy +++ b/argocd/manifests/alloy-k8s/config.alloy @@ -159,10 +159,8 @@ prometheus.exporter.blackbox "services" { } target { - // devpi runs natively on indri (LaunchAgent), not in-cluster. - // We probe through Caddy (https://pypi.ops.eblu.me) which the cluster can reach via Tailscale. name = "devpi" - address = "https://pypi.ops.eblu.me/+api" + address = "http://devpi.devpi.svc.cluster.local:3141/+api" module = "http_2xx" } @@ -191,9 +189,14 @@ prometheus.exporter.blackbox "services" { } target { - // Migrated to ringtail (wave-1); probe through Caddy over Tailscale. name = "teslamate" - address = "https://tesla.ops.eblu.me/" + address = "http://teslamate.teslamate.svc.cluster.local:4000/" + module = "http_2xx" + } + + target { + name = "immich" + address = "http://immich-server.immich.svc.cluster.local:2283/api/server/ping" module = "http_2xx" } diff --git a/argocd/manifests/alloy-k8s/daemonset.yaml b/argocd/manifests/alloy-k8s/daemonset.yaml index f1758cd..60b8883 100644 --- a/argocd/manifests/alloy-k8s/daemonset.yaml +++ b/argocd/manifests/alloy-k8s/daemonset.yaml @@ -17,8 +17,6 @@ spec: serviceAccountName: alloy securityContext: fsGroup: 473 # alloy user group - seccompProfile: - type: RuntimeDefault containers: - name: alloy image: registry.ops.eblu.me/blumeops/alloy:kustomized diff --git a/argocd/manifests/alloy-k8s/kustomization.yaml b/argocd/manifests/alloy-k8s/kustomization.yaml index 3503ead..f51bd3a 100644 --- a/argocd/manifests/alloy-k8s/kustomization.yaml +++ b/argocd/manifests/alloy-k8s/kustomization.yaml @@ -10,7 +10,7 @@ resources: images: - name: registry.ops.eblu.me/blumeops/alloy - newTag: v1.16.0-9564435 + newTag: v1.14.0-fd0bebb configMapGenerator: - name: alloy-config diff --git a/argocd/manifests/alloy-ringtail/config.alloy b/argocd/manifests/alloy-ringtail/config.alloy index e5cc045..e92ab0f 100644 --- a/argocd/manifests/alloy-ringtail/config.alloy +++ b/argocd/manifests/alloy-ringtail/config.alloy @@ -45,26 +45,6 @@ prometheus.scrape "kube_state_metrics" { forward_to = [prometheus.remote_write.prometheus.receiver] } -// ============== SERVICE HEALTH PROBES ============== - -// Blackbox-style HTTP probes for in-cluster services on ringtail -prometheus.exporter.blackbox "services" { - config = "{ modules: { http_2xx: { prober: http, timeout: 5s } } }" - - target { - name = "immich" - address = "http://immich-server.immich.svc.cluster.local:2283/api/server/ping" - module = "http_2xx" - } -} - -// Scrape blackbox probe results -prometheus.scrape "blackbox" { - targets = prometheus.exporter.blackbox.services.targets - scrape_interval = "30s" - forward_to = [prometheus.remote_write.prometheus.receiver] -} - // Push metrics to indri Prometheus prometheus.remote_write "prometheus" { external_labels = { cluster = "ringtail" } diff --git a/argocd/manifests/alloy-ringtail/kustomization.yaml b/argocd/manifests/alloy-ringtail/kustomization.yaml index 526fec5..df472aa 100644 --- a/argocd/manifests/alloy-ringtail/kustomization.yaml +++ b/argocd/manifests/alloy-ringtail/kustomization.yaml @@ -10,7 +10,7 @@ resources: images: - name: registry.ops.eblu.me/blumeops/alloy - newTag: v1.16.0-9564435-nix + newTag: v1.14.0-fd0bebb-nix configMapGenerator: - name: alloy-config diff --git a/argocd/manifests/alloy-tracing-ringtail/daemonset.yaml b/argocd/manifests/alloy-tracing-ringtail/daemonset.yaml index b3de1de..e56cc9d 100644 --- a/argocd/manifests/alloy-tracing-ringtail/daemonset.yaml +++ b/argocd/manifests/alloy-tracing-ringtail/daemonset.yaml @@ -46,7 +46,6 @@ spec: mountPath: /var/lib/alloy/data securityContext: privileged: true - runAsUser: 0 tolerations: - operator: Exists volumes: diff --git a/argocd/manifests/alloy-tracing-ringtail/kustomization.yaml b/argocd/manifests/alloy-tracing-ringtail/kustomization.yaml index b1e6338..5c8e683 100644 --- a/argocd/manifests/alloy-tracing-ringtail/kustomization.yaml +++ b/argocd/manifests/alloy-tracing-ringtail/kustomization.yaml @@ -9,7 +9,7 @@ resources: images: - name: registry.ops.eblu.me/blumeops/alloy - newTag: v1.16.0-9564435-nix + newTag: v1.14.0-fd0bebb-nix configMapGenerator: - name: alloy-tracing-config diff --git a/argocd/manifests/argocd/README.md b/argocd/manifests/argocd/README.md index 2eaf4d4..615e3bb 100644 --- a/argocd/manifests/argocd/README.md +++ b/argocd/manifests/argocd/README.md @@ -25,7 +25,7 @@ kubectl wait --for=condition=available deployment/argocd-server -n argocd --time kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d && echo # 5. Login and change password -argocd login argocd.tail8d86e.ts.net --username admin +argocd login argocd.tail8d86e.ts.net --username admin --grpc-web argocd account update-password # 6. Apply repo-creds-forge credential template for SSH access to all forge repos @@ -114,4 +114,4 @@ spec: Future improvement: integrate with a secrets operator (e.g., External Secrets). - The credential template (`repo-creds`) uses a URL prefix to match all repos on forge. - ArgoCD uses Tailscale Ingress with Let's Encrypt for TLS termination. -- After Authentik is up, prefer `argocd login argocd.ops.eblu.me --sso` over the admin password login above; admin is only needed during bootstrap or as break-glass. +- The `--grpc-web` flag is required for CLI access through the Tailscale ingress. diff --git a/argocd/manifests/argocd/argocd-cm-patch.yaml b/argocd/manifests/argocd/argocd-cm-patch.yaml index 54e4ede..cb7e27f 100644 --- a/argocd/manifests/argocd/argocd-cm-patch.yaml +++ b/argocd/manifests/argocd/argocd-cm-patch.yaml @@ -16,6 +16,7 @@ data: name: Authentik issuer: https://authentik.ops.eblu.me/application/o/argocd/ clientID: argocd + clientSecret: $argocd-oidc-authentik:client-secret requestedScopes: - openid - profile diff --git a/argocd/manifests/argocd/argocd-rbac-cm-patch.yaml b/argocd/manifests/argocd/argocd-rbac-cm-patch.yaml index 4914587..c2ea095 100644 --- a/argocd/manifests/argocd/argocd-rbac-cm-patch.yaml +++ b/argocd/manifests/argocd/argocd-rbac-cm-patch.yaml @@ -2,9 +2,6 @@ # # - workflow-bot: minimal CI/CD permissions (sync, get) # - admins: Authentik admins group mapped to ArgoCD admin role -# - admin: local break-glass account — keeps ArgoCD admin rights for when -# Authentik SSO is unavailable (without this it has no permissions, since -# policy.default is unset) # apiVersion: v1 kind: ConfigMap @@ -17,4 +14,3 @@ data: p, role:workflow-bot, applications, get, *, allow g, workflow-bot, role:workflow-bot g, admins, role:admin - g, admin, role:admin diff --git a/argocd/manifests/argocd/argocd-resources-patch.yaml b/argocd/manifests/argocd/argocd-resources-patch.yaml deleted file mode 100644 index 1ae0675..0000000 --- a/argocd/manifests/argocd/argocd-resources-patch.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: argocd-server -spec: - template: - spec: - containers: - - name: argocd-server - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: argocd-repo-server -spec: - template: - spec: - containers: - - name: argocd-repo-server - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: argocd-application-controller -spec: - template: - spec: - containers: - - name: argocd-application-controller - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: "1" - memory: 1Gi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: argocd-applicationset-controller -spec: - template: - spec: - containers: - - name: argocd-applicationset-controller - resources: - requests: - cpu: 25m - memory: 64Mi - limits: - cpu: 250m - memory: 256Mi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: argocd-dex-server -spec: - template: - spec: - containers: - - name: dex - resources: - requests: - cpu: 25m - memory: 64Mi - limits: - cpu: 250m - memory: 256Mi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: argocd-redis -spec: - template: - spec: - containers: - - name: redis - resources: - requests: - cpu: 25m - memory: 64Mi - limits: - cpu: 250m - memory: 256Mi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: argocd-notifications-controller -spec: - template: - spec: - containers: - - name: argocd-notifications-controller - resources: - requests: - cpu: 25m - memory: 64Mi - limits: - cpu: 250m - memory: 256Mi diff --git a/argocd/manifests/argocd/external-secret-oidc-authentik.yaml b/argocd/manifests/argocd/external-secret-oidc-authentik.yaml new file mode 100644 index 0000000..475a713 --- /dev/null +++ b/argocd/manifests/argocd/external-secret-oidc-authentik.yaml @@ -0,0 +1,31 @@ +# ExternalSecret for ArgoCD OIDC client secret (Authentik) +# +# Referenced from argocd-cm as $argocd-oidc-authentik:client-secret +# Must have app.kubernetes.io/part-of: argocd label for ArgoCD to read it +# +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: argocd-oidc-authentik + namespace: argocd +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: argocd-oidc-authentik + creationPolicy: Owner + template: + metadata: + labels: + app.kubernetes.io/part-of: argocd + data: + - secretKey: client-secret + remoteRef: + conversionStrategy: Default + decodingStrategy: None + key: "Authentik (blumeops)" + metadataPolicy: None + property: argocd-client-secret diff --git a/argocd/manifests/argocd/kustomization.yaml b/argocd/manifests/argocd/kustomization.yaml index 6deb7ec..89b5970 100644 --- a/argocd/manifests/argocd/kustomization.yaml +++ b/argocd/manifests/argocd/kustomization.yaml @@ -5,14 +5,13 @@ namespace: argocd resources: # Pin to specific version for intentional upgrades - # ArgoCD v3.3.6 - - https://raw.githubusercontent.com/argoproj/argo-cd/998fb59dc355653c0657908a6ea2f87136e022d1/manifests/install.yaml + - https://raw.githubusercontent.com/argoproj/argo-cd/v3.3.2/manifests/install.yaml - ingress-tailscale.yaml - external-secret-repo-forge.yaml + - external-secret-oidc-authentik.yaml patches: - path: argocd-cmd-params-cm.yaml - path: argocd-ssh-known-hosts-cm.yaml - path: argocd-cm-patch.yaml - path: argocd-rbac-cm-patch.yaml - - path: argocd-resources-patch.yaml diff --git a/argocd/manifests/authentik/configmap-blueprint.yaml b/argocd/manifests/authentik/configmap-blueprint.yaml index cc97dea..cc3ff43 100644 --- a/argocd/manifests/authentik/configmap-blueprint.yaml +++ b/argocd/manifests/authentik/configmap-blueprint.yaml @@ -262,15 +262,14 @@ data: name: ArgoCD authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] - client_type: public + client_type: confidential client_id: argocd + client_secret: !Env AUTHENTIK_ARGOCD_CLIENT_SECRET redirect_uris: - matching_mode: strict url: https://argocd.ops.eblu.me/auth/callback - matching_mode: strict url: https://argocd.tail8d86e.ts.net/auth/callback - - matching_mode: strict - url: http://localhost:8085/auth/callback signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] property_mappings: - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] @@ -347,50 +346,6 @@ data: meta_launch_url: https://jellyfin.ops.eblu.me policy_engine_mode: all - paperless.yaml: | - version: 1 - metadata: - name: BlumeOps Paperless SSO - labels: - blueprints.goauthentik.io/description: "Paperless-ngx OIDC provider and application" - entries: - # OAuth2 provider for Paperless-ngx (confidential client) - - model: authentik_providers_oauth2.oauth2provider - id: paperless-provider - identifiers: - name: Paperless - attrs: - name: Paperless - authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] - invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] - client_type: confidential - client_id: paperless - client_secret: !Env AUTHENTIK_PAPERLESS_CLIENT_SECRET - redirect_uris: - - matching_mode: strict - url: https://paperless.ops.eblu.me/accounts/oidc/authentik/login/callback/ - - matching_mode: strict - url: https://paperless.tail8d86e.ts.net/accounts/oidc/authentik/login/callback/ - signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] - property_mappings: - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] - sub_mode: hashed_user_id - include_claims_in_id_token: true - - # Paperless application — all authenticated users allowed - - model: authentik_core.application - id: paperless-app - identifiers: - slug: paperless - attrs: - name: Paperless - slug: paperless - provider: !KeyOf paperless-provider - meta_launch_url: https://paperless.ops.eblu.me - policy_engine_mode: all - mealie.yaml: | version: 1 metadata: @@ -434,93 +389,3 @@ data: provider: !KeyOf mealie-provider meta_launch_url: https://meals.ops.eblu.me policy_engine_mode: all - - heph.yaml: | - version: 1 - metadata: - name: BlumeOps Heph SSO - labels: - blueprints.goauthentik.io/description: "Hephaestus hub OIDC (device-code) provider, application, and device-code flow" - entries: - # Device-code flow (RFC 8628). authentik ships no default for this, so we - # create one and bind it to the brand below. An empty stage_configuration - # flow is sufficient: the already-authenticated user just confirms the code. - - model: authentik_flows.flow - id: device-code-flow - identifiers: - slug: default-device-code-flow - attrs: - name: Device code flow - title: Device code flow - slug: default-device-code-flow - designation: stage_configuration - authentication: require_authenticated - - # Enable the device-code grant globally by binding the flow to the default - # brand (domain authentik-default). Partial update — only sets this field. - - model: authentik_brands.brand - identifiers: - domain: authentik-default - attrs: - flow_device_code: !KeyOf device-code-flow - - # OAuth2 provider for heph — PUBLIC client (device-code + PKCE, no secret). - # client_id doubles as the token audience the hub verifies (--oidc-audience heph), - # and the app slug 'heph' is the issuer path (/application/o/heph/). - - model: authentik_providers_oauth2.oauth2provider - id: heph-provider - identifiers: - name: Heph - attrs: - name: Heph - authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] - invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] - client_type: public - client_id: heph - # CLI/TUI use the device-code grant (no redirect). The heph-pwa browser - # login uses Authorization Code + PKCE, which DOES redirect back to the - # app's origin — register those here (Authentik also keys token-endpoint - # CORS off these origins). Trailing slash matters: the PWA's redirect_uri - # is its base dir, e.g. https://heph.ops.eblu.me/. - redirect_uris: - - matching_mode: strict - url: https://heph.ops.eblu.me/ - - matching_mode: strict - url: http://localhost:8787/ # local dev (hephd --web-root) - signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] - property_mappings: - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] - # offline_access: heph CLI requests "openid offline_access"; without - # this mapping the refresh token is session-bound and hephd's - # refresh_token grant 400s once the session lapses (spoke sync dies). - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] - sub_mode: hashed_user_id - include_claims_in_id_token: true - - # Heph application — linked to the OAuth2 provider - - model: authentik_core.application - id: heph-app - identifiers: - slug: heph - attrs: - name: Hephaestus - slug: heph - provider: !KeyOf heph-provider - meta_launch_url: https://heph.ops.eblu.me - policy_engine_mode: any - - # Policy binding — restrict heph to admins group (single-owner, sensitive data) - - model: authentik_policies.policybinding - identifiers: - order: 0 - target: !KeyOf heph-app - group: !Find [authentik_core.group, [name, admins]] - attrs: - target: !KeyOf heph-app - group: !Find [authentik_core.group, [name, admins]] - order: 0 - enabled: true - negate: false - timeout: 30 diff --git a/argocd/manifests/authentik/deployment-worker.yaml b/argocd/manifests/authentik/deployment-worker.yaml index 053fa3d..ed8a753 100644 --- a/argocd/manifests/authentik/deployment-worker.yaml +++ b/argocd/manifests/authentik/deployment-worker.yaml @@ -75,26 +75,26 @@ spec: secretKeyRef: name: authentik-config key: jellyfin-client-secret + - name: AUTHENTIK_ARGOCD_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: authentik-config + key: argocd-client-secret - name: AUTHENTIK_MEALIE_CLIENT_SECRET valueFrom: secretKeyRef: name: authentik-config key: mealie-client-secret - - name: AUTHENTIK_PAPERLESS_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: authentik-config - key: paperless-client-secret volumeMounts: - name: blueprints mountPath: /blueprints/custom readOnly: true resources: requests: - memory: "512Mi" + memory: "256Mi" cpu: "100m" limits: - memory: "2Gi" + memory: "1Gi" cpu: "1000m" volumes: - name: blueprints diff --git a/argocd/manifests/authentik/external-secret.yaml b/argocd/manifests/authentik/external-secret.yaml index 93de499..fb22f2b 100644 --- a/argocd/manifests/authentik/external-secret.yaml +++ b/argocd/manifests/authentik/external-secret.yaml @@ -53,11 +53,11 @@ spec: remoteRef: key: "Authentik (blumeops)" property: jellyfin-client-secret + - secretKey: argocd-client-secret + remoteRef: + key: "Authentik (blumeops)" + property: argocd-client-secret - secretKey: mealie-client-secret remoteRef: key: "Authentik (blumeops)" property: mealie-client-secret - - secretKey: paperless-client-secret - remoteRef: - key: "Authentik (blumeops)" - property: paperless-client-secret diff --git a/argocd/manifests/authentik/kustomization.yaml b/argocd/manifests/authentik/kustomization.yaml index cae2c7f..8aade37 100644 --- a/argocd/manifests/authentik/kustomization.yaml +++ b/argocd/manifests/authentik/kustomization.yaml @@ -13,7 +13,7 @@ resources: - ingress-tailscale.yaml images: - name: registry.ops.eblu.me/blumeops/authentik - newTag: v2026.2.2-2eb2830-nix + newTag: v2026.2.0-fd0bebb-nix - name: docker.io/library/redis newName: registry.ops.eblu.me/blumeops/authentik-redis newTag: v8.2.3-fd0bebb-nix diff --git a/argocd/manifests/cv/deployment.yaml b/argocd/manifests/cv/deployment.yaml new file mode 100644 index 0000000..f2b00e6 --- /dev/null +++ b/argocd/manifests/cv/deployment.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cv + namespace: cv +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app: cv + template: + metadata: + labels: + app: cv + spec: + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: cv + image: registry.ops.eblu.me/blumeops/cv:kustomized + ports: + - containerPort: 80 + name: http + env: + - name: CV_RELEASE_URL + value: "https://forge.eblu.me/api/packages/eblume/generic/cv/v1.0.3/cv-v1.0.3.tar.gz" + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + livenessProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/argocd/manifests/cv/ingress-tailscale.yaml b/argocd/manifests/cv/ingress-tailscale.yaml new file mode 100644 index 0000000..489f95a --- /dev/null +++ b/argocd/manifests/cv/ingress-tailscale.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cv-tailscale + namespace: cv + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + tailscale.com/tags: "tag:k8s,tag:flyio-target" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "CV" + gethomepage.dev/group: "Services" + gethomepage.dev/icon: "mdi-file-document" + gethomepage.dev/description: "Resume / CV" + gethomepage.dev/href: "https://cv.eblu.me" + gethomepage.dev/pod-selector: "app=cv" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: cv + port: + number: 80 + tls: + - hosts: + - cv diff --git a/argocd/manifests/cv/kustomization.yaml b/argocd/manifests/cv/kustomization.yaml new file mode 100644 index 0000000..199108d --- /dev/null +++ b/argocd/manifests/cv/kustomization.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: cv +resources: + - deployment.yaml + - service.yaml + - ingress-tailscale.yaml + - pdb.yaml +images: + - name: registry.ops.eblu.me/blumeops/cv + newTag: v1.0.3-613f05d diff --git a/argocd/manifests/cv/pdb.yaml b/argocd/manifests/cv/pdb.yaml new file mode 100644 index 0000000..db5240d --- /dev/null +++ b/argocd/manifests/cv/pdb.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: cv +spec: + minAvailable: 1 + selector: + matchLabels: + app: cv diff --git a/argocd/manifests/cv/service.yaml b/argocd/manifests/cv/service.yaml new file mode 100644 index 0000000..23e0e94 --- /dev/null +++ b/argocd/manifests/cv/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: cv + namespace: cv +spec: + selector: + app: cv + ports: + - name: http + port: 80 + targetPort: 80 diff --git a/argocd/manifests/databases-ringtail/blumeops-pg.yaml b/argocd/manifests/databases-ringtail/blumeops-pg.yaml deleted file mode 100644 index 3a37249..0000000 --- a/argocd/manifests/databases-ringtail/blumeops-pg.yaml +++ /dev/null @@ -1,97 +0,0 @@ -# PostgreSQL Cluster for blumeops services on ringtail k3s. -# -# Wave-1 indri-k8s decommission target (see [[migrate-wave1-ringtail]]). -# Holds the paperless and teslamate databases migrated off the minikube -# blumeops-pg via cold pg_dump/pg_restore at cutover. miniflux + authentik -# stay where they are for now (later waves), so this cluster only carries -# the wave-1 roles. -# -# Apps reach this in-cluster at blumeops-pg-rw.databases.svc.cluster.local -# — the same name they used on minikube, so teslamate's DATABASE_HOST is -# unchanged. -# -# Database creation is deferred to cutover, mirroring the minikube cluster -# (where only the bootstrap database is declared and the rest were created -# out-of-band): -# - paperless: the bootstrap database below (restored into at cutover). -# - teslamate: created at its cutover by the eblume superuser, because the -# dump's `earthdistance` extension is untrusted and CREATE EXTENSION -# needs superuser. (cube + earthdistance ownership then transferred to -# the teslamate role so it can ALTER EXTENSION UPDATE.) -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: blumeops-pg - namespace: databases -spec: - instances: 1 - imageName: ghcr.io/cloudnative-pg/postgresql:18.3 - - storage: - size: 10Gi - storageClass: local-path - - bootstrap: - initdb: - database: paperless - owner: paperless - - managed: - roles: - # eblume superuser for admin + privileged restore steps (extensions) - - name: eblume - login: true - superuser: true - createdb: true - createrole: true - connectionLimit: -1 - ensure: present - inherit: true - passwordSecret: - name: blumeops-pg-eblume - # borgmatic read-only user for backups - - name: borgmatic - login: true - connectionLimit: -1 - ensure: present - inherit: true - inRoles: - - pg_read_all_data - passwordSecret: - name: blumeops-pg-borgmatic - # paperless user (also the bootstrap database owner above; the - # managed role sets its password from the 1Password-backed secret) - - name: paperless - login: true - connectionLimit: -1 - ensure: present - inherit: true - passwordSecret: - name: blumeops-pg-paperless - # teslamate user. Extension ownership (cube, earthdistance) is - # transferred to this role at cutover so it can ALTER EXTENSION UPDATE. - - name: teslamate - login: true - connectionLimit: -1 - ensure: present - inherit: true - passwordSecret: - name: blumeops-pg-teslamate - - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "1Gi" - cpu: "500m" - - postgresql: - parameters: - max_connections: "50" - shared_buffers: "128MB" - password_encryption: "scram-sha-256" - pg_hba: - # Password auth from anywhere; network security is via Tailscale. - - host all all 0.0.0.0/0 scram-sha-256 - - host all all ::/0 scram-sha-256 diff --git a/argocd/manifests/databases-ringtail/external-secret-eblume.yaml b/argocd/manifests/databases-ringtail/external-secret-eblume.yaml deleted file mode 100644 index a324c7d..0000000 --- a/argocd/manifests/databases-ringtail/external-secret-eblume.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# ExternalSecret for eblume superuser password -# -# Replaces the manual op inject workflow from secret-eblume.yaml.tpl -# -# 1Password item: "postgres" in blumeops vault -# Field: "password" -# -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: blumeops-pg-eblume - namespace: databases -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: blumeops-pg-eblume - creationPolicy: Owner - template: - type: kubernetes.io/basic-auth - data: - username: eblume - password: "{{ .password }}" - data: - - secretKey: password - remoteRef: - key: postgres - property: password diff --git a/argocd/manifests/databases-ringtail/external-secret-immich-borgmatic.yaml b/argocd/manifests/databases-ringtail/external-secret-immich-borgmatic.yaml deleted file mode 100644 index 3d1fc14..0000000 --- a/argocd/manifests/databases-ringtail/external-secret-immich-borgmatic.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# ExternalSecret for borgmatic backup user password on immich-pg cluster -# (ringtail k3s). -# -# Mirror of argocd/manifests/databases/external-secret-immich-borgmatic.yaml. -# The onepassword-blumeops ClusterSecretStore exists on ringtail via the -# external-secrets-ringtail app. -# -# 1Password item: "borgmatic" in blumeops vault -# Field: "db-password" -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: immich-pg-borgmatic - namespace: databases -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: immich-pg-borgmatic - creationPolicy: Owner - template: - type: kubernetes.io/basic-auth - data: - username: borgmatic - password: "{{ .password }}" - data: - - secretKey: password - remoteRef: - key: borgmatic - property: db-password diff --git a/argocd/manifests/databases-ringtail/external-secret-paperless.yaml b/argocd/manifests/databases-ringtail/external-secret-paperless.yaml deleted file mode 100644 index e5742be..0000000 --- a/argocd/manifests/databases-ringtail/external-secret-paperless.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# ExternalSecret for Paperless database user password -# -# 1Password item: "Paperless (blumeops)" in blumeops vault -# Field: "postgresql-password" -# -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: blumeops-pg-paperless - namespace: databases -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: blumeops-pg-paperless - creationPolicy: Owner - template: - type: kubernetes.io/basic-auth - data: - username: paperless - password: "{{ .password }}" - data: - - secretKey: password - remoteRef: - key: Paperless (blumeops) - property: postgresql-password diff --git a/argocd/manifests/databases-ringtail/immich-pg.yaml b/argocd/manifests/databases-ringtail/immich-pg.yaml deleted file mode 100644 index 982bc43..0000000 --- a/argocd/manifests/databases-ringtail/immich-pg.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# PostgreSQL Cluster for Immich on ringtail k3s. -# -# Initially bootstrapped via CNPG pg_basebackup from the minikube -# immich-pg cluster on 2026-05-13, then promoted to primary. The -# externalClusters + bootstrap.pg_basebackup blocks have been pruned -# from this manifest now that the migration is complete — leaving -# them around is a footgun (re-enabling replica.enabled=true would -# try to demote this cluster against a stale source). See -# [[immich-pg-data-migration]] for the procedure used. -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: immich-pg - namespace: databases -spec: - instances: 1 - imageName: ghcr.io/tensorchord/cloudnative-vectorchord:17-0.5.0 - - storage: - size: 10Gi - storageClass: local-path - - # Managed roles - managed: - roles: - - name: borgmatic - login: true - connectionLimit: -1 - ensure: present - inherit: true - inRoles: - - pg_read_all_data - passwordSecret: - name: immich-pg-borgmatic - - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "1Gi" - cpu: "500m" - - postgresql: - shared_preload_libraries: - - "vchord.so" - parameters: - max_connections: "50" - shared_buffers: "128MB" - password_encryption: "scram-sha-256" - pg_hba: - - host all all 0.0.0.0/0 scram-sha-256 - - host all all ::/0 scram-sha-256 diff --git a/argocd/manifests/databases-ringtail/kustomization.yaml b/argocd/manifests/databases-ringtail/kustomization.yaml deleted file mode 100644 index 143345c..0000000 --- a/argocd/manifests/databases-ringtail/kustomization.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: databases - -resources: - - immich-pg.yaml - - external-secret-immich-borgmatic.yaml - - service-immich-pg-tailscale.yaml - # wave-1 indri-k8s decommission: blumeops-pg (paperless + teslamate) - - blumeops-pg.yaml - - service-blumeops-pg-tailscale.yaml - - external-secret-eblume.yaml - - external-secret-borgmatic.yaml - - external-secret-paperless.yaml - - external-secret-teslamate.yaml diff --git a/argocd/manifests/databases-ringtail/service-blumeops-pg-tailscale.yaml b/argocd/manifests/databases-ringtail/service-blumeops-pg-tailscale.yaml deleted file mode 100644 index f7ca5ef..0000000 --- a/argocd/manifests/databases-ringtail/service-blumeops-pg-tailscale.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Tailscale LoadBalancer for the ringtail blumeops-pg cluster. -# Canonical hostname: blumeops-pg-ringtail.tail8d86e.ts.net (distinct from -# the minikube blumeops-pg, which still owns pg.tail8d86e.ts.net until the -# wave-1 decommission). Borgmatic on indri and the Grafana TeslaMate -# datasource reach it via the Caddy L4 route pg.ops.eblu.me:5434. -apiVersion: v1 -kind: Service -metadata: - name: blumeops-pg-tailscale - namespace: databases - annotations: - tailscale.com/hostname: "blumeops-pg-ringtail" - tailscale.com/proxy-class: "default" -spec: - type: LoadBalancer - loadBalancerClass: tailscale - selector: - cnpg.io/cluster: blumeops-pg - role: primary - ports: - - name: postgresql - port: 5432 - targetPort: 5432 - protocol: TCP diff --git a/argocd/manifests/databases/blumeops-pg.yaml b/argocd/manifests/databases/blumeops-pg.yaml index 37aef23..ea68c9b 100644 --- a/argocd/manifests/databases/blumeops-pg.yaml +++ b/argocd/manifests/databases/blumeops-pg.yaml @@ -44,9 +44,17 @@ spec: - pg_read_all_data passwordSecret: name: blumeops-pg-borgmatic - # teslamate + paperless roles removed: migrated to ringtail blumeops-pg - # (wave-1 decommission). Their databases were dropped from this cluster - # after the cutover was verified and backed up. + # teslamate user for TeslaMate Tesla data logger + # Note: superuser required for extension management during migrations + - name: teslamate + login: true + superuser: true + connectionLimit: -1 + ensure: present + inherit: true + createdb: true + passwordSecret: + name: blumeops-pg-teslamate # authentik user for Authentik identity provider (runs on ringtail) - name: authentik login: true diff --git a/argocd/manifests/databases-ringtail/external-secret-borgmatic.yaml b/argocd/manifests/databases/external-secret-immich-borgmatic.yaml similarity index 73% rename from argocd/manifests/databases-ringtail/external-secret-borgmatic.yaml rename to argocd/manifests/databases/external-secret-immich-borgmatic.yaml index ee600e3..8801c1a 100644 --- a/argocd/manifests/databases-ringtail/external-secret-borgmatic.yaml +++ b/argocd/manifests/databases/external-secret-immich-borgmatic.yaml @@ -1,14 +1,13 @@ -# ExternalSecret for borgmatic backup user password -# -# Replaces the manual op inject workflow from secret-borgmatic.yaml.tpl +# ExternalSecret for borgmatic backup user password on immich-pg cluster # +# Reuses the same 1Password item as blumeops-pg-borgmatic. # 1Password item: "borgmatic" in blumeops vault # Field: "db-password" # apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: - name: blumeops-pg-borgmatic + name: immich-pg-borgmatic namespace: databases spec: refreshInterval: 1h @@ -16,7 +15,7 @@ spec: kind: ClusterSecretStore name: onepassword-blumeops target: - name: blumeops-pg-borgmatic + name: immich-pg-borgmatic creationPolicy: Owner template: type: kubernetes.io/basic-auth diff --git a/argocd/manifests/databases-ringtail/external-secret-teslamate.yaml b/argocd/manifests/databases/external-secret-teslamate.yaml similarity index 100% rename from argocd/manifests/databases-ringtail/external-secret-teslamate.yaml rename to argocd/manifests/databases/external-secret-teslamate.yaml diff --git a/argocd/manifests/databases/immich-pg.yaml b/argocd/manifests/databases/immich-pg.yaml new file mode 100644 index 0000000..74c6f4e --- /dev/null +++ b/argocd/manifests/databases/immich-pg.yaml @@ -0,0 +1,69 @@ +# PostgreSQL Cluster for Immich +# Uses VectorChord (successor to pgvecto.rs) for AI-powered vector search +# See: https://github.com/immich-app/immich/discussions/9060 +# Managed by CloudNativePG operator +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: immich-pg + namespace: databases +spec: + instances: 1 + # VectorChord image for PostgreSQL 17 with VectorChord 0.5.0 + # Immich v2.4.1 requires VectorChord >=0.3 <0.6 + # See: https://github.com/tensorchord/VectorChord + imageName: ghcr.io/tensorchord/cloudnative-vectorchord:17-0.5.0 + + storage: + size: 10Gi + storageClass: standard + + # Bootstrap creates initial database and owner + bootstrap: + initdb: + database: immich + owner: immich + postInitSQL: + # Extensions required by Immich + - CREATE EXTENSION IF NOT EXISTS vector; + - CREATE EXTENSION IF NOT EXISTS vchord CASCADE; + - CREATE EXTENSION IF NOT EXISTS cube CASCADE; + - CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE; + + # Managed roles + # Note: connectionLimit, ensure, inherit are CNPG defaults added to prevent ArgoCD drift + managed: + roles: + # borgmatic read-only user for backups + - name: borgmatic + login: true + connectionLimit: -1 + ensure: present + inherit: true + inRoles: + - pg_read_all_data + passwordSecret: + name: immich-pg-borgmatic + + # Resource limits for minikube environment + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "500m" + + # PostgreSQL configuration + postgresql: + # VectorChord requires vchord.so in shared_preload_libraries + shared_preload_libraries: + - "vchord.so" + parameters: + max_connections: "50" + shared_buffers: "128MB" + password_encryption: "scram-sha-256" + pg_hba: + # Allow connections from k8s pods + - host all all 0.0.0.0/0 scram-sha-256 + - host all all ::/0 scram-sha-256 diff --git a/argocd/manifests/databases/kustomization.yaml b/argocd/manifests/databases/kustomization.yaml index 0393757..68d28b2 100644 --- a/argocd/manifests/databases/kustomization.yaml +++ b/argocd/manifests/databases/kustomization.yaml @@ -5,8 +5,12 @@ namespace: databases resources: - blumeops-pg.yaml + - immich-pg.yaml - service-tailscale.yaml + - service-immich-pg-tailscale.yaml - service-metrics-tailscale.yaml - external-secret-eblume.yaml - external-secret-borgmatic.yaml + - external-secret-immich-borgmatic.yaml + - external-secret-teslamate.yaml - external-secret-authentik.yaml diff --git a/argocd/manifests/databases-ringtail/service-immich-pg-tailscale.yaml b/argocd/manifests/databases/service-immich-pg-tailscale.yaml similarity index 57% rename from argocd/manifests/databases-ringtail/service-immich-pg-tailscale.yaml rename to argocd/manifests/databases/service-immich-pg-tailscale.yaml index 92deb14..78891dd 100644 --- a/argocd/manifests/databases-ringtail/service-immich-pg-tailscale.yaml +++ b/argocd/manifests/databases/service-immich-pg-tailscale.yaml @@ -1,8 +1,6 @@ -# Tailscale LoadBalancer for immich-pg PostgreSQL access on ringtail. -# Canonical hostname: immich-pg.tail8d86e.ts.net (claimed from the -# minikube side after the minikube service was removed during the -# immich-to-ringtail migration). Borgmatic on indri uses this -# hostname for nightly backups. +# Tailscale LoadBalancer for immich-pg PostgreSQL access +# Canonical hostname: immich-pg.tail8d86e.ts.net +# Caddy L4 proxies pg.ops.eblu.me:5433 → this service for borgmatic backups apiVersion: v1 kind: Service metadata: diff --git a/argocd/manifests/devpi/README.md b/argocd/manifests/devpi/README.md new file mode 100644 index 0000000..11fd697 --- /dev/null +++ b/argocd/manifests/devpi/README.md @@ -0,0 +1,72 @@ +# devpi PyPI Caching Proxy + +devpi-server running in Kubernetes, providing: +- PyPI caching proxy at `root/pypi` +- Private package hosting at `eblume/dev` + +## Setup + +### 1. Create the root password secret + +```fish +kubectl create namespace devpi +op inject -i argocd/manifests/devpi/secret-root.yaml.tpl | kubectl apply -f - +``` + +### 2. Deploy via ArgoCD + +```fish +argocd app sync apps +argocd app sync devpi +``` + +The container will auto-initialize on first startup using the root password from the secret. + +### 3. Create user and index (first time only) + +After the pod is running: + +```fish +# Login to devpi as root +uvx --from devpi-client devpi use https://pypi.tail8d86e.ts.net +uvx --from devpi-client devpi login root +# Enter root password when prompted + +# Create eblume user (prompts for password - use the one from 1Password) +uvx --from devpi-client devpi user -c eblume email=blume.erich@gmail.com + +# Create private index inheriting from PyPI +uvx --from devpi-client devpi index -c eblume/dev bases=root/pypi +``` + +## Usage + +### As pip index (caching proxy) + +Configure `~/.config/pip/pip.conf`: + +```ini +[global] +index-url = https://pypi.tail8d86e.ts.net/root/pypi/+simple/ +trusted-host = pypi.tail8d86e.ts.net +``` + +### Upload private packages + +```fish +cd ~/code/personal/your-package +uv build +uv publish --publish-url https://pypi.tail8d86e.ts.net/eblume/dev/ +``` + +## URLs + +- Web UI: https://pypi.tail8d86e.ts.net +- PyPI cache: https://pypi.tail8d86e.ts.net/root/pypi/+simple/ +- Private index: https://pypi.tail8d86e.ts.net/eblume/dev/+simple/ + +## Credentials + +Stored in 1Password vault `blumeops`, item `kyhzfifryqnuk7jeyibmmjvxxm`: +- `root password` - devpi root user +- `password` - eblume user password diff --git a/argocd/manifests/devpi/external-secret.yaml b/argocd/manifests/devpi/external-secret.yaml new file mode 100644 index 0000000..290ea67 --- /dev/null +++ b/argocd/manifests/devpi/external-secret.yaml @@ -0,0 +1,25 @@ +# ExternalSecret for devpi root password +# +# Replaces the manual op inject workflow from secret-root.yaml.tpl +# +# 1Password item: "devpi" in blumeops vault +# Field: "root password" +# +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: devpi-root + namespace: devpi +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: devpi-root + creationPolicy: Owner + data: + - secretKey: password + remoteRef: + key: devpi + property: root password diff --git a/argocd/manifests/devpi/ingress-tailscale.yaml b/argocd/manifests/devpi/ingress-tailscale.yaml new file mode 100644 index 0000000..474bf72 --- /dev/null +++ b/argocd/manifests/devpi/ingress-tailscale.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: devpi-tailscale + namespace: devpi + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "PyPI" + gethomepage.dev/group: "Infrastructure" + gethomepage.dev/icon: "pypi.png" + gethomepage.dev/description: "PyPI cache" + gethomepage.dev/href: "https://pypi.ops.eblu.me" + gethomepage.dev/pod-selector: "app=devpi" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: devpi + port: + number: 3141 + tls: + - hosts: + - pypi diff --git a/argocd/manifests/devpi/kustomization.yaml b/argocd/manifests/devpi/kustomization.yaml new file mode 100644 index 0000000..a9cc2a4 --- /dev/null +++ b/argocd/manifests/devpi/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: devpi + +resources: + - statefulset.yaml + - service.yaml + - ingress-tailscale.yaml + - external-secret.yaml + +images: + - name: registry.ops.eblu.me/blumeops/devpi + newTag: v6.19.1-613f05d diff --git a/argocd/manifests/shower/service.yaml b/argocd/manifests/devpi/service.yaml similarity index 53% rename from argocd/manifests/shower/service.yaml rename to argocd/manifests/devpi/service.yaml index 0a73aab..42e1543 100644 --- a/argocd/manifests/shower/service.yaml +++ b/argocd/manifests/devpi/service.yaml @@ -1,13 +1,13 @@ apiVersion: v1 kind: Service metadata: - name: shower - namespace: shower + name: devpi + namespace: devpi spec: selector: - app: shower + app: devpi ports: - name: http - port: 8000 - targetPort: 8000 + port: 3141 + targetPort: 3141 protocol: TCP diff --git a/argocd/manifests/devpi/statefulset.yaml b/argocd/manifests/devpi/statefulset.yaml new file mode 100644 index 0000000..91875df --- /dev/null +++ b/argocd/manifests/devpi/statefulset.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: devpi + namespace: devpi +spec: + serviceName: devpi + replicas: 1 + selector: + matchLabels: + app: devpi + template: + metadata: + labels: + app: devpi + spec: + securityContext: + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: devpi + image: registry.ops.eblu.me/blumeops/devpi:kustomized + env: + - name: DEVPI_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: devpi-root + key: password + - name: DEVPI_OUTSIDE_URL + value: "https://pypi.ops.eblu.me" + ports: + - containerPort: 3141 + name: http + volumeMounts: + - name: data + mountPath: /devpi + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "2Gi" # High limit for initial PyPI index build, reclaimed after + cpu: "500m" + livenessProbe: + httpGet: + path: /+api + port: 3141 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /+api + port: 3141 + initialDelaySeconds: 10 + periodSeconds: 10 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 50Gi diff --git a/argocd/manifests/docs/deployment.yaml b/argocd/manifests/docs/deployment.yaml new file mode 100644 index 0000000..c1203dd --- /dev/null +++ b/argocd/manifests/docs/deployment.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: docs + namespace: docs +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app: docs + template: + metadata: + labels: + app: docs + spec: + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: docs + image: registry.ops.eblu.me/blumeops/quartz:kustomized + ports: + - containerPort: 80 + name: http + env: + - name: DOCS_RELEASE_URL + value: "https://forge.eblu.me/eblume/blumeops/releases/download/v1.15.0/docs-v1.15.0.tar.gz" + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + livenessProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/argocd/manifests/docs/ingress-tailscale.yaml b/argocd/manifests/docs/ingress-tailscale.yaml new file mode 100644 index 0000000..047e823 --- /dev/null +++ b/argocd/manifests/docs/ingress-tailscale.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: docs-tailscale + namespace: docs + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + tailscale.com/tags: "tag:k8s,tag:flyio-target" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Docs" + gethomepage.dev/group: "Services" + gethomepage.dev/icon: "mdi-book-open-page-variant" + gethomepage.dev/description: "BlumeOps Documentation" + gethomepage.dev/href: "https://docs.eblu.me" + gethomepage.dev/pod-selector: "app=docs" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: docs + port: + number: 80 + tls: + - hosts: + - docs diff --git a/argocd/manifests/docs/kustomization.yaml b/argocd/manifests/docs/kustomization.yaml new file mode 100644 index 0000000..a16185f --- /dev/null +++ b/argocd/manifests/docs/kustomization.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: docs +resources: + - deployment.yaml + - service.yaml + - ingress-tailscale.yaml + - pdb.yaml +images: + - name: registry.ops.eblu.me/blumeops/quartz + newTag: v1.28.2-613f05d diff --git a/argocd/manifests/docs/pdb.yaml b/argocd/manifests/docs/pdb.yaml new file mode 100644 index 0000000..a87b8e9 --- /dev/null +++ b/argocd/manifests/docs/pdb.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: docs +spec: + minAvailable: 1 + selector: + matchLabels: + app: docs diff --git a/argocd/manifests/docs/service.yaml b/argocd/manifests/docs/service.yaml new file mode 100644 index 0000000..62b0f83 --- /dev/null +++ b/argocd/manifests/docs/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: docs + namespace: docs +spec: + selector: + app: docs + ports: + - name: http + port: 80 + targetPort: 80 diff --git a/argocd/manifests/external-secrets-ringtail/kustomization.yaml b/argocd/manifests/external-secrets-ringtail/kustomization.yaml deleted file mode 100644 index 9fd4e2f..0000000 --- a/argocd/manifests/external-secrets-ringtail/kustomization.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Ringtail (amd64) overlay for external-secrets. -# -# Reuses the shared indri manifest as a base and only overrides the controller -# image to the nix-built amd64 variant (`-nix` tag). The base sets the arm64 -# image (built via containers/external-secrets/container.py on indri's Dagger -# runner); ringtail's k3s is amd64 and needs the image built by -# containers/external-secrets/default.nix on the nix-container-builder. -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../external-secrets - -images: - - name: registry.ops.eblu.me/blumeops/external-secrets - newTag: v2.2.0-13895bb-nix diff --git a/argocd/manifests/external-secrets/kustomization.yaml b/argocd/manifests/external-secrets/kustomization.yaml index 639db66..574aaa7 100644 --- a/argocd/manifests/external-secrets/kustomization.yaml +++ b/argocd/manifests/external-secrets/kustomization.yaml @@ -12,5 +12,4 @@ resources: images: - name: ghcr.io/external-secrets/external-secrets - newName: registry.ops.eblu.me/blumeops/external-secrets - newTag: v2.2.0-13895bb + newTag: v2.2.0 diff --git a/argocd/manifests/forgejo-runner/config.yaml b/argocd/manifests/forgejo-runner/config.yaml index 01ede7c..c92d616 100644 --- a/argocd/manifests/forgejo-runner/config.yaml +++ b/argocd/manifests/forgejo-runner/config.yaml @@ -1,8 +1,9 @@ -# Reviewed against v12.8.2 defaults (2026-04-20) +# Reviewed against v12.7.0 defaults (2026-02-22) log: level: info runner: + file: /data/.runner capacity: 2 timeout: 3h shutdown_timeout: 3h @@ -12,15 +13,7 @@ runner: TZ: America/Los_Angeles container: + # Job execution image is set via RUNNER_LABELS in deployment.yaml network: "host" # Connect to DinD sidecar via TCP (not socket) docker_host: tcp://127.0.0.1:2375 - -server: - connections: - forgejo: - url: https://forge.ops.eblu.me/ - uuid: ${FORGEJO_RUNNER_UUID} - token: ${FORGEJO_RUNNER_TOKEN} - labels: - - k8s:docker://registry.ops.eblu.me/blumeops/runner-job-image:v0.20.6-50f8c2a diff --git a/argocd/manifests/forgejo-runner/deployment.yaml b/argocd/manifests/forgejo-runner/deployment.yaml index 7db7798..1eda6dc 100644 --- a/argocd/manifests/forgejo-runner/deployment.yaml +++ b/argocd/manifests/forgejo-runner/deployment.yaml @@ -25,6 +25,14 @@ spec: env: - name: TZ value: America/Los_Angeles + - name: DOCKER_HOST + value: tcp://localhost:2375 + - name: FORGEJO_URL + value: "https://forge.ops.eblu.me" + - name: RUNNER_NAME + value: "k8s-runner" + - name: RUNNER_LABELS + value: "k8s:docker://registry.ops.eblu.me/blumeops/runner-job-image:v0.20.1-24f7512" command: - /bin/sh - -c @@ -36,11 +44,19 @@ spec: done echo "Docker daemon ready" - # Render config with credentials from ExternalSecret. - envsubst < /config/config.yaml > /tmp/config.yaml + # Register if not already registered + if [ ! -f /data/.runner ]; then + echo "Registering runner..." + forgejo-runner register \ + --instance "$FORGEJO_URL" \ + --token "$RUNNER_TOKEN" \ + --name "$RUNNER_NAME" \ + --labels "$RUNNER_LABELS" \ + --no-interactive + fi # Start daemon - exec forgejo-runner daemon --config /tmp/config.yaml + exec forgejo-runner daemon --config /config/config.yaml envFrom: - secretRef: name: forgejo-runner-env @@ -58,8 +74,6 @@ spec: image: docker:kustomized securityContext: privileged: true - seccompProfile: - type: Unconfined env: - name: DOCKER_TLS_CERTDIR value: "" diff --git a/argocd/manifests/forgejo-runner/external-secret.yaml b/argocd/manifests/forgejo-runner/external-secret.yaml index ab7a691..fce28bb 100644 --- a/argocd/manifests/forgejo-runner/external-secret.yaml +++ b/argocd/manifests/forgejo-runner/external-secret.yaml @@ -1,7 +1,11 @@ -# ExternalSecret for Forgejo Runner credentials +# ExternalSecret for Forgejo Runner token # # 1Password item: "Forgejo Secrets" in blumeops vault -# Fields: runner_k8s_uuid, runner_k8s_token +# Field: runner_reg (runner registration token) +# +# Non-secret env vars (FORGEJO_URL, RUNNER_NAME, RUNNER_LABELS) live in the +# deployment spec so that changes (e.g. image version bumps) trigger a rollout +# automatically. # apiVersion: external-secrets.io/v1 kind: ExternalSecret @@ -17,11 +21,7 @@ spec: name: forgejo-runner-env creationPolicy: Owner data: - - secretKey: FORGEJO_RUNNER_UUID + - secretKey: RUNNER_TOKEN remoteRef: key: Forgejo Secrets - property: runner_k8s_uuid - - secretKey: FORGEJO_RUNNER_TOKEN - remoteRef: - key: Forgejo Secrets - property: runner_k8s_token + property: runner_reg diff --git a/argocd/manifests/forgejo-runner/kustomization.yaml b/argocd/manifests/forgejo-runner/kustomization.yaml index 93cd33b..67527de 100644 --- a/argocd/manifests/forgejo-runner/kustomization.yaml +++ b/argocd/manifests/forgejo-runner/kustomization.yaml @@ -10,8 +10,7 @@ resources: images: - name: code.forgejo.org/forgejo/runner - newName: registry.ops.eblu.me/blumeops/forgejo-runner - newTag: v12.8.2-1425bf1 + newTag: "12.7.0" - name: docker newTag: 27-dind diff --git a/argocd/manifests/frigate/deployment-notify.yaml b/argocd/manifests/frigate/deployment-notify.yaml index 91f4237..740d104 100644 --- a/argocd/manifests/frigate/deployment-notify.yaml +++ b/argocd/manifests/frigate/deployment-notify.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: frigate-notify - image: registry.ops.eblu.me/blumeops/frigate-notify:kustomized + image: ghcr.io/0x2142/frigate-notify:kustomized env: - name: TZ value: America/Los_Angeles diff --git a/argocd/manifests/frigate/frigate-config.yml b/argocd/manifests/frigate/frigate-config.yml index 3033dd4..35f7ccd 100644 --- a/argocd/manifests/frigate/frigate-config.yml +++ b/argocd/manifests/frigate/frigate-config.yml @@ -69,8 +69,6 @@ model: record: enabled: true - preview: - quality: very_low continuous: days: 30 motion: diff --git a/argocd/manifests/frigate/kustomization.yaml b/argocd/manifests/frigate/kustomization.yaml index a61c758..b424bd0 100644 --- a/argocd/manifests/frigate/kustomization.yaml +++ b/argocd/manifests/frigate/kustomization.yaml @@ -17,8 +17,8 @@ images: newTag: "1.37" - name: ghcr.io/blakeblackshear/frigate newTag: 0.17.1-tensorrt - - name: registry.ops.eblu.me/blumeops/frigate-notify - newTag: v0.5.4-e928054-nix + - name: ghcr.io/0x2142/frigate-notify + newTag: v0.5.4 configMapGenerator: - name: frigate-config diff --git a/argocd/manifests/grafana-config/dashboards/configmap-flyio.yaml b/argocd/manifests/grafana-config/dashboards/configmap-flyio.yaml index abbf2b3..981f7ea 100644 --- a/argocd/manifests/grafana-config/dashboards/configmap-flyio.yaml +++ b/argocd/manifests/grafana-config/dashboards/configmap-flyio.yaml @@ -249,11 +249,9 @@ data: "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.50, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{instance=\"flyio-proxy\"}[5m])))", "legendFormat": "p50", "refId": "A" }, - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.90, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{instance=\"flyio-proxy\"}[5m])))", "legendFormat": "p90", "refId": "B" }, - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.99, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{instance=\"flyio-proxy\"}[5m])))", "legendFormat": "p99", "refId": "C" } + { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.95, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{instance=\"flyio-proxy\"}[5m])))", "legendFormat": "p95", "refId": "A" } ], - "title": "Latency Percentiles (all hosts)", + "title": "Upstream Response Time p95", "type": "timeseries" } ], diff --git a/argocd/manifests/grafana-config/dashboards/configmap-forgejo.yaml b/argocd/manifests/grafana-config/dashboards/configmap-forgejo.yaml index 782135e..f0de4aa 100644 --- a/argocd/manifests/grafana-config/dashboards/configmap-forgejo.yaml +++ b/argocd/manifests/grafana-config/dashboards/configmap-forgejo.yaml @@ -858,52 +858,6 @@ data: "title": "Proxy: Bandwidth", "type": "timeseries" }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "seconds", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { "legend": false, "tooltip": false, "viz": false }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { "type": "linear" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { "group": "A", "mode": "none" }, - "thresholdsStyle": { "mode": "off" } - }, - "mappings": [], - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 45 }, - "id": 22, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.50, sum by (le) (rate(flyio_nginx_upstream_response_time_seconds_bucket{host=\"forge.eblu.me\"}[5m])))", "legendFormat": "upstream p50", "refId": "A" }, - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.90, sum by (le) (rate(flyio_nginx_upstream_response_time_seconds_bucket{host=\"forge.eblu.me\"}[5m])))", "legendFormat": "upstream p90", "refId": "B" }, - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.99, sum by (le) (rate(flyio_nginx_upstream_response_time_seconds_bucket{host=\"forge.eblu.me\"}[5m])))", "legendFormat": "upstream p99", "refId": "C" } - ], - "title": "Forgejo: Upstream Response Time", - "type": "timeseries" - }, { "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 45 }, diff --git a/argocd/manifests/grafana-config/dashboards/configmap-shower-apm.yaml b/argocd/manifests/grafana-config/dashboards/configmap-shower-apm.yaml deleted file mode 100644 index 96348e8..0000000 --- a/argocd/manifests/grafana-config/dashboards/configmap-shower-apm.yaml +++ /dev/null @@ -1,229 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-dashboard-shower-apm - namespace: monitoring - labels: - grafana_dashboard: "1" -data: - shower-apm.json: | - { - "annotations": { "list": [] }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 1, - "id": null, - "links": [], - "panels": [ - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisLabel": "req/s", - "drawStyle": "line", - "fillOpacity": 20, - "lineInterpolation": "linear", - "lineWidth": 1, - "showPoints": "never", - "spanNulls": false, - "stacking": { "group": "A", "mode": "normal" } - }, - "mappings": [], - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, - "unit": "reqps" - }, - "overrides": [] - }, - "gridPos": { "h": 8, "w": 16, "x": 0, "y": 0 }, - "id": 1, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum by (status) (rate(flyio_nginx_http_requests_total{host=\"shower.eblu.me\"}[5m]))", "legendFormat": "{{status}}", "refId": "A" } - ], - "title": "Request Rate by Status", - "type": "timeseries" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 0.01 }, { "color": "red", "value": 0.05 }] }, - "unit": "percentunit" - }, - "overrides": [] - }, - "gridPos": { "h": 4, "w": 8, "x": 16, "y": 0 }, - "id": 2, - "options": { - "colorMode": "background", - "graphMode": "area", - "justifyMode": "center", - "orientation": "auto", - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "textMode": "auto" - }, - "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(rate(flyio_nginx_http_requests_total{host=\"shower.eblu.me\",status=~\"5..\"}[5m])) / sum(rate(flyio_nginx_http_requests_total{host=\"shower.eblu.me\"}[5m]))", "refId": "A" } - ], - "title": "Error Rate (5xx)", - "type": "stat" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 1 }, { "color": "red", "value": 5 }] }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { "h": 4, "w": 4, "x": 16, "y": 4 }, - "id": 3, - "options": { - "colorMode": "background", - "graphMode": "area", - "justifyMode": "center", - "orientation": "auto", - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "textMode": "auto" - }, - "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(increase(flyio_nginx_http_requests_total{host=\"shower.eblu.me\",request_uri=~\"/admin/login.*\",status=~\"4..\"}[$__range]))", "refId": "A" } - ], - "title": "Failed admin logins (range)", - "type": "stat" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "thresholds" }, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, - "unit": "reqps" - }, - "overrides": [] - }, - "gridPos": { "h": 4, "w": 4, "x": 20, "y": 4 }, - "id": 4, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "orientation": "auto", - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, - "textMode": "auto" - }, - "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(rate(flyio_nginx_http_requests_total{host=\"shower.eblu.me\"}[5m]))", "refId": "A" } - ], - "title": "Current RPS", - "type": "stat" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisLabel": "seconds", - "drawStyle": "line", - "fillOpacity": 10, - "lineInterpolation": "linear", - "lineWidth": 1, - "showPoints": "never", - "spanNulls": false, - "stacking": { "group": "A", "mode": "none" } - }, - "mappings": [], - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, - "id": 5, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, - "tooltip": { "mode": "multi", "sort": "desc" } - }, - "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.50, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{host=\"shower.eblu.me\"}[5m])))", "legendFormat": "p50", "refId": "A" }, - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.90, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{host=\"shower.eblu.me\"}[5m])))", "legendFormat": "p90", "refId": "B" }, - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.99, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{host=\"shower.eblu.me\"}[5m])))", "legendFormat": "p99", "refId": "C" } - ], - "title": "Latency Percentiles", - "type": "timeseries" - }, - { - "datasource": { "type": "prometheus", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisLabel": "", - "drawStyle": "line", - "fillOpacity": 20, - "lineInterpolation": "linear", - "lineWidth": 1, - "showPoints": "never", - "spanNulls": false, - "stacking": { "group": "A", "mode": "none" } - }, - "mappings": [], - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, - "unit": "Bps" - }, - "overrides": [] - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, - "id": 6, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, - "tooltip": { "mode": "single", "sort": "none" } - }, - "targets": [ - { "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(rate(flyio_nginx_http_response_bytes_total{host=\"shower.eblu.me\"}[5m]))", "legendFormat": "Bandwidth", "refId": "A" } - ], - "title": "Bandwidth", - "type": "timeseries" - }, - { - "datasource": { "type": "loki", "uid": "loki" }, - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 }, - "id": 7, - "options": { - "dedupStrategy": "none", - "enableLogDetails": true, - "prettifyLogMessage": false, - "showCommonLabels": false, - "showLabels": false, - "showTime": true, - "sortOrder": "Descending", - "wrapLogMessage": false - }, - "targets": [ - { "datasource": { "type": "loki", "uid": "loki" }, "expr": "{instance=\"flyio-proxy\", job=\"flyio-nginx\"} |= \"shower.eblu.me\" | json | line_format \"{{.client_ip}} {{.request_method}} {{.request_uri}} {{.status}} {{.request_time}}s\"", "refId": "A" } - ], - "title": "Recent Access Logs", - "type": "logs" - } - ], - "refresh": "30s", - "schemaVersion": 38, - "tags": ["shower", "flyio", "apm"], - "templating": { "list": [] }, - "time": { "from": "now-6h", "to": "now" }, - "timepicker": {}, - "timezone": "", - "title": "Shower APM", - "uid": "shower-apm", - "version": 1, - "weekStart": "" - } diff --git a/argocd/manifests/grafana-config/kustomization.yaml b/argocd/manifests/grafana-config/kustomization.yaml index b518043..a6e8000 100644 --- a/argocd/manifests/grafana-config/kustomization.yaml +++ b/argocd/manifests/grafana-config/kustomization.yaml @@ -22,7 +22,6 @@ resources: - dashboards/configmap-transmission.yaml - dashboards/configmap-cv-apm.yaml - dashboards/configmap-docs-apm.yaml - - dashboards/configmap-shower-apm.yaml - dashboards/configmap-flyio.yaml - dashboards/configmap-sifaka-disks.yaml - dashboards/configmap-forgejo.yaml diff --git a/argocd/manifests/grafana/alerting.yaml b/argocd/manifests/grafana/alerting.yaml index 4ae70d3..b220044 100644 --- a/argocd/manifests/grafana/alerting.yaml +++ b/argocd/manifests/grafana/alerting.yaml @@ -373,66 +373,6 @@ groups: type: and refId: C - - orgId: 1 - name: flyio-proxy-health - folder: Infrastructure Alerts - interval: 30s - rules: - - uid: flyio-upstream-unreachable - title: FlyioUpstreamUnreachable - condition: C - for: 3m - noDataState: OK - execErrState: Alerting - annotations: - summary: >- - Fly.io proxy returning elevated 502s — upstream DNS may be stale. Run: mise run fly-reload - runbook_url: https://docs.eblu.me/how-to/operations/manage-flyio-proxy - labels: - severity: warning - service: flyio-proxy - data: - - refId: A - datasourceUid: prometheus - relativeTimeRange: - from: 300 - to: 0 - model: - expr: >- - sum(rate(flyio_nginx_http_requests_total{instance="flyio-proxy",status="502"}[5m])) - / sum(rate(flyio_nginx_http_requests_total{instance="flyio-proxy"}[5m])) - > 0.5 - interval: "" - refId: A - - refId: B - datasourceUid: "__expr__" - relativeTimeRange: - from: 0 - to: 0 - model: - type: reduce - expression: A - reducer: last - settings: - mode: dropNN - refId: B - - refId: C - datasourceUid: "__expr__" - relativeTimeRange: - from: 0 - to: 0 - model: - type: threshold - expression: B - conditions: - - evaluator: - type: gt - params: - - 0 - operator: - type: and - refId: C - templates: - orgId: 1 name: ntfy-infra diff --git a/argocd/manifests/grafana/datasources.yaml b/argocd/manifests/grafana/datasources.yaml index 64ed2bf..5a3d0f3 100644 --- a/argocd/manifests/grafana/datasources.yaml +++ b/argocd/manifests/grafana/datasources.yaml @@ -63,7 +63,5 @@ datasources: password: $TESLAMATE_DB_PASSWORD type: postgres uid: TeslaMate - # teslamate DB migrated to ringtail blumeops-pg (wave-1); reached via the - # Caddy L4 route on indri (pg.ops.eblu.me:5434 -> blumeops-pg-ringtail). - url: pg.ops.eblu.me:5434 + url: blumeops-pg-rw.databases.svc.cluster.local:5432 user: teslamate diff --git a/argocd/manifests/grafana/deployment.yaml b/argocd/manifests/grafana/deployment.yaml index cbba267..5fbb8eb 100644 --- a/argocd/manifests/grafana/deployment.yaml +++ b/argocd/manifests/grafana/deployment.yaml @@ -14,9 +14,7 @@ spec: app.kubernetes.io/name: grafana app.kubernetes.io/instance: grafana strategy: - # RWO PVC for SQLite + Bleve index — RollingUpdate spawns the new pod - # before the old one terminates, and it crashloops on the index lock. - type: Recreate + type: RollingUpdate template: metadata: labels: @@ -125,9 +123,8 @@ spec: sed -i 's/"uid": *"\${DS_PROMETHEUS}"/"uid": "prometheus"/g' "$DEST"/*.json # Stamp stable top-level UIDs so stars/bookmarks survive pod restarts. # Match root-level uid (2-space indent) to avoid clobbering datasource refs. - # UIDs must be ≤40 chars (Grafana 12+ enforcement). for f in "$DEST"/*.json; do - slug=$(basename "$f" .json | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | sed 's/^unifi-poller-//') + slug=$(basename "$f" .json | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') uid="unpoller-${slug}" sed -i "s/^ \"uid\": *\"[^\"]*\"/ \"uid\": \"${uid}\"/" "$f" done @@ -158,9 +155,7 @@ spec: - name: FOLDER value: /tmp/dashboards - name: RESOURCE - # ConfigMap-only — no dashboards are sourced from Secrets, - # so the ServiceAccount has no read access to secrets. - value: configmap + value: both - name: FOLDER_ANNOTATION value: grafana_folder securityContext: @@ -187,7 +182,7 @@ spec: - name: FOLDER value: /tmp/dashboards - name: RESOURCE - value: configmap + value: both - name: FOLDER_ANNOTATION value: grafana_folder - name: REQ_USERNAME @@ -204,18 +199,6 @@ spec: value: http://localhost:3000/api/admin/provisioning/dashboards/reload - name: REQ_METHOD value: POST - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 10 securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/argocd/manifests/grafana/kustomization.yaml b/argocd/manifests/grafana/kustomization.yaml index a511fe1..3aeaa26 100644 --- a/argocd/manifests/grafana/kustomization.yaml +++ b/argocd/manifests/grafana/kustomization.yaml @@ -16,9 +16,9 @@ images: - name: docker.io/library/busybox newTag: 1.31.1 - name: registry.ops.eblu.me/blumeops/grafana-sidecar - newTag: v2.6.0-61fcd5d + newTag: v1.28.0-613f05d - name: registry.ops.eblu.me/blumeops/grafana - newTag: v12.4.2-4c54774 + newTag: v12.3.3-613f05d configMapGenerator: - name: grafana diff --git a/argocd/manifests/grafana/rbac.yaml b/argocd/manifests/grafana/rbac.yaml index 1c2dee3..d0d0c843 100644 --- a/argocd/manifests/grafana/rbac.yaml +++ b/argocd/manifests/grafana/rbac.yaml @@ -7,7 +7,7 @@ metadata: app.kubernetes.io/instance: grafana rules: - apiGroups: [""] - resources: ["configmaps"] + resources: ["configmaps", "secrets"] verbs: ["get", "watch", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/argocd/manifests/homepage/kustomization.yaml b/argocd/manifests/homepage/kustomization.yaml index 31b6847..27de0eb 100644 --- a/argocd/manifests/homepage/kustomization.yaml +++ b/argocd/manifests/homepage/kustomization.yaml @@ -17,7 +17,7 @@ resources: images: - name: registry.ops.eblu.me/blumeops/homepage - newTag: v1.11.0-678f26b-nix + newTag: v1.11.0-e375859 configMapGenerator: - name: homepage-config diff --git a/argocd/manifests/homepage/services.yaml b/argocd/manifests/homepage/services.yaml index cc1adf4..58b8bb7 100644 --- a/argocd/manifests/homepage/services.yaml +++ b/argocd/manifests/homepage/services.yaml @@ -1,6 +1,3 @@ -# Homepage runs on ringtail (k3s) — its k8s autodiscovery only sees ringtail -# Ingresses (frigate→NVR, authentik, ntfy, ollama). Services that live on -# minikube (and indri-native) need explicit static entries here. - Host Services: - Forgejo: href: https://forge.eblu.me @@ -15,10 +12,6 @@ href: https://registry.ops.eblu.me icon: zot-registry description: Container registry - - Devpi: - href: https://pypi.ops.eblu.me - icon: mdi-language-python - description: PyPI caching mirror - Sifaka NAS: href: https://nas.ops.eblu.me icon: synology @@ -60,6 +53,10 @@ # type: caddy # url: http://indri.tail8d86e.ts.net:2019 - Home: + - NVR: + href: https://nvr.ops.eblu.me + icon: frigate.png + description: Network video recorder - Jellyfin: href: https://jellyfin.ops.eblu.me icon: jellyfin @@ -71,62 +68,12 @@ enableBlocks: true enableNowPlaying: false fields: ["movies", "series", "episodes"] - - DJ: - href: https://dj.ops.eblu.me - icon: navidrome.png - description: Music streaming server - widget: - type: navidrome - url: https://dj.ops.eblu.me - user: "{{HOMEPAGE_VAR_NAVIDROME_USER}}" - token: "{{HOMEPAGE_VAR_NAVIDROME_TOKEN}}" - salt: "{{HOMEPAGE_VAR_NAVIDROME_SALT}}" -- Content: - - Kiwix: - href: https://kiwix.ops.eblu.me - icon: kiwix.png - description: Offline Wikipedia - - Miniflux: - href: https://feed.ops.eblu.me - icon: miniflux.png - description: RSS reader - widget: - type: miniflux - url: https://feed.ops.eblu.me - key: "{{HOMEPAGE_VAR_MINIFLUX_API_KEY}}" - fields: ["unread"] - Infrastructure: - - ArgoCD: - href: https://argocd.ops.eblu.me - icon: argo-cd.png - description: GitOps CD - - Grafana: - href: https://grafana.ops.eblu.me - icon: grafana.png - description: Metrics dashboards - widget: - type: grafana - url: https://grafana.ops.eblu.me - username: "{{HOMEPAGE_VAR_GRAFANA_USERNAME}}" - password: "{{HOMEPAGE_VAR_GRAFANA_PASSWORD}}" - fields: ["dashboards", "totalalerts", "alertstriggered"] - - Prometheus: - href: https://prometheus.ops.eblu.me - icon: prometheus.png - description: Metrics storage -- Services: - # CV and Docs were previously auto-discovered from k8s Ingresses; after - # the indri-native migration ([[cv-on-indri]], [[docs-on-indri]]) there - # is no Ingress to discover, so they live here as static entries. - - CV: - href: https://cv.eblu.me - icon: mdi-file-document - description: Resume / CV - - Docs: - href: https://docs.eblu.me - icon: mdi-book-open-page-variant - description: BlumeOps Documentation - - Transmission: - href: https://torrent.ops.eblu.me - icon: transmission.png - description: Torrent client + - Authentik: + href: https://authentik.ops.eblu.me + icon: authentik + description: Identity provider + - Ntfy: + href: https://ntfy.ops.eblu.me + icon: ntfy.png + description: Push notifications diff --git a/argocd/manifests/immich-ringtail/deployment-ml.yaml b/argocd/manifests/immich-ringtail/deployment-ml.yaml deleted file mode 100644 index 5ea8035..0000000 --- a/argocd/manifests/immich-ringtail/deployment-ml.yaml +++ /dev/null @@ -1,69 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: immich-machine-learning - namespace: immich -spec: - replicas: 1 - selector: - matchLabels: - app: immich - component: machine-learning - template: - metadata: - labels: - app: immich - component: machine-learning - spec: - runtimeClassName: nvidia - securityContext: - seccompProfile: - type: RuntimeDefault - containers: - - name: machine-learning - # ringtail uses the -cuda tag (set in kustomization.yaml) - # to take advantage of the RTX 4080 via the nvidia - # device plugin. Time-slicing is configured for 4 replicas - # so frigate + ollama + this pod can share. - image: ghcr.io/immich-app/immich-machine-learning:kustomized - ports: - - name: http - containerPort: 3003 - env: - - name: TZ - value: "America/Los_Angeles" - - name: TRANSFORMERS_CACHE - value: /cache - - name: HF_XET_CACHE - value: /cache/huggingface-xet - - name: MPLCONFIGDIR - value: /cache/matplotlib-config - volumeMounts: - - name: cache - mountPath: /cache - livenessProbe: - httpGet: - path: /ping - port: 3003 - initialDelaySeconds: 30 - periodSeconds: 30 - timeoutSeconds: 5 - readinessProbe: - httpGet: - path: /ping - port: 3003 - initialDelaySeconds: 15 - periodSeconds: 10 - timeoutSeconds: 5 - resources: - requests: - memory: "512Mi" - cpu: "100m" - limits: - memory: "4Gi" - nvidia.com/gpu: "1" - volumes: - - name: cache - persistentVolumeClaim: - claimName: immich-ml-cache diff --git a/argocd/manifests/immich-ringtail/deployment-server.yaml b/argocd/manifests/immich-ringtail/deployment-server.yaml deleted file mode 100644 index 8ac7ab0..0000000 --- a/argocd/manifests/immich-ringtail/deployment-server.yaml +++ /dev/null @@ -1,74 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: immich-server - namespace: immich -spec: - replicas: 1 - selector: - matchLabels: - app: immich - component: server - template: - metadata: - labels: - app: immich - component: server - spec: - securityContext: - seccompProfile: - type: RuntimeDefault - containers: - - name: server - image: ghcr.io/immich-app/immich-server:kustomized - ports: - - name: http - containerPort: 2283 - env: - - name: TZ - value: "America/Los_Angeles" - - name: DB_HOSTNAME - value: "immich-pg-rw.databases.svc.cluster.local" - - name: DB_PORT - value: "5432" - - name: DB_DATABASE_NAME - value: "immich" - - name: DB_USERNAME - value: "immich" - - name: DB_PASSWORD - valueFrom: - secretKeyRef: - name: immich-db - key: password - - name: REDIS_HOSTNAME - value: immich-valkey - - name: IMMICH_MACHINE_LEARNING_URL - value: "http://immich-machine-learning:3003" - volumeMounts: - - name: library - mountPath: /usr/src/app/upload - livenessProbe: - httpGet: - path: /api/server/ping - port: 2283 - initialDelaySeconds: 30 - periodSeconds: 30 - timeoutSeconds: 5 - readinessProbe: - httpGet: - path: /api/server/ping - port: 2283 - initialDelaySeconds: 15 - periodSeconds: 10 - timeoutSeconds: 5 - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "2Gi" - volumes: - - name: library - persistentVolumeClaim: - claimName: immich-library diff --git a/argocd/manifests/immich-ringtail/deployment-valkey.yaml b/argocd/manifests/immich-ringtail/deployment-valkey.yaml deleted file mode 100644 index 1cf3346..0000000 --- a/argocd/manifests/immich-ringtail/deployment-valkey.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: immich-valkey - namespace: immich -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app: immich - component: valkey - template: - metadata: - labels: - app: immich - component: valkey - spec: - securityContext: - seccompProfile: - type: RuntimeDefault - containers: - - name: valkey - image: docker.io/valkey/valkey:kustomized - ports: - - name: redis - containerPort: 6379 - volumeMounts: - - name: data - mountPath: /data - resources: - requests: - memory: "64Mi" - cpu: "25m" - limits: - memory: "256Mi" - volumes: - - name: data - emptyDir: - sizeLimit: 1Gi diff --git a/argocd/manifests/immich-ringtail/kustomization.yaml b/argocd/manifests/immich-ringtail/kustomization.yaml deleted file mode 100644 index 2fa131c..0000000 --- a/argocd/manifests/immich-ringtail/kustomization.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: immich - -resources: - - deployment-server.yaml - - deployment-ml.yaml - - deployment-valkey.yaml - - service.yaml - - service-ml.yaml - - service-valkey.yaml - - pvc-ml-cache.yaml - - pv-nfs.yaml - - pvc.yaml - - ingress-tailscale.yaml - -images: - - name: ghcr.io/immich-app/immich-server - newTag: v2.6.3 - - name: ghcr.io/immich-app/immich-machine-learning - # CUDA variant of the same release — ringtail has an RTX 4080 - newTag: v2.6.3-cuda - # amd64 valkey built via nix on the ringtail nix-container-builder - # (see containers/valkey/default.nix). The Alpine container.py build - # is arm64-only and serves paperless on indri. - - name: docker.io/valkey/valkey - newName: registry.ops.eblu.me/blumeops/valkey - newTag: v8.1.7-ecded30-nix diff --git a/argocd/manifests/immich-ringtail/pv-nfs.yaml b/argocd/manifests/immich-ringtail/pv-nfs.yaml deleted file mode 100644 index 3d5a682..0000000 --- a/argocd/manifests/immich-ringtail/pv-nfs.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# NFS PersistentVolume for Immich photo library on ringtail k3s. -# -# Mirror of argocd/manifests/immich/pv-nfs.yaml (minikube) but with -# a distinct name (minikube and ringtail are separate clusters, so PV -# names don't collide cluster-side, but using the same name in two -# manifests is confusing). -# -# The sifaka NFS export for /volume1/photos already permits -# 192.168.1.0/24 + 100.64.0.0/10. Ringtail's wired IP (192.168.1.21) -# falls in the first CIDR, so no DSM rule changes are needed. -# -# Verified 2026-05-13: ringtail pod can read existing dirs, write -# new files, and delete them. DNS resolves sifaka to 192.168.1.203 -# (LAN), so NFS traffic stays off the tailnet — avoids the known -# sifaka-tailscale-userspace bite. -apiVersion: v1 -kind: PersistentVolume -metadata: - name: immich-library-nfs-pv-ringtail -spec: - capacity: - storage: 2Ti - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - storageClassName: "" - nfs: - server: sifaka - path: /volume1/photos diff --git a/argocd/manifests/immich-ringtail/service-ml.yaml b/argocd/manifests/immich-ringtail/service-ml.yaml deleted file mode 100644 index 9bb935a..0000000 --- a/argocd/manifests/immich-ringtail/service-ml.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: immich-machine-learning - namespace: immich -spec: - selector: - app: immich - component: machine-learning - ports: - - name: http - port: 3003 - targetPort: 3003 diff --git a/argocd/manifests/immich-ringtail/service-valkey.yaml b/argocd/manifests/immich-ringtail/service-valkey.yaml deleted file mode 100644 index eb42d3b..0000000 --- a/argocd/manifests/immich-ringtail/service-valkey.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: immich-valkey - namespace: immich -spec: - selector: - app: immich - component: valkey - ports: - - name: redis - port: 6379 - targetPort: 6379 diff --git a/argocd/manifests/immich-ringtail/service.yaml b/argocd/manifests/immich-ringtail/service.yaml deleted file mode 100644 index d35410f..0000000 --- a/argocd/manifests/immich-ringtail/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: immich-server - namespace: immich -spec: - selector: - app: immich - component: server - ports: - - name: http - port: 2283 - targetPort: 2283 diff --git a/argocd/manifests/immich/README.md b/argocd/manifests/immich/README.md new file mode 100644 index 0000000..76e8ac9 --- /dev/null +++ b/argocd/manifests/immich/README.md @@ -0,0 +1,101 @@ +# Immich + +Self-hosted photo and video management solution with AI-powered search and face recognition. + +## Prerequisites + +1. **NFS Share**: Create `/volume1/photos` on sifaka with NFS permissions for indri +2. **PostgreSQL**: The `immich-pg` cluster (with pgvecto.rs) must be healthy +3. **Secrets**: Create the database password secret + +## Deployment Order + +1. Sync `blumeops-pg` (to get CloudNativePG operator if not already running) +2. Sync `immich-storage` (creates PV, PVC, and Tailscale Ingress) +3. Wait for `immich-pg` cluster to be healthy +4. Create secrets (see below) +5. Sync `immich` (deploys the Helm chart) +6. Run `mise run provision-indri -- --tags caddy` to update Caddy config + +## Secret Setup + +The `immich-db` secret contains the database password, which is auto-generated by CloudNativePG +in the `immich-pg-app` secret. To create or regenerate the secret: + +```bash +# Create namespace if needed +kubectl --context=minikube-indri create namespace immich + +# Copy password from CNPG secret to immich namespace +kubectl --context=minikube-indri create secret generic immich-db -n immich \ + --from-literal=password="$(kubectl --context=minikube-indri -n databases get secret immich-pg-app -o jsonpath='{.data.password}' | base64 -d)" +``` + +Note: This secret is not managed by ExternalSecrets since the source of truth is the CNPG-generated secret. + +## Access + +- **URL**: https://photos.ops.eblu.me (after Caddy is updated) +- **Tailscale**: https://photos.tail8d86e.ts.net (direct) + +## First-Time Setup + +1. Navigate to https://photos.ops.eblu.me +2. Create an admin account +3. Configure external library (optional - for importing existing photos) + +## External Library (iCloud Photos) + +To import existing photos from iCloud sync on indri: + +1. In Immich Admin > External Libraries, create a new library +2. Set the import path to the location where iCloud photos sync +3. Configure scan schedule or trigger manual scan + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ +│ immich-server │────▶│ immich-pg │ +│ (web/api) │ │ (PostgreSQL │ +└────────┬────────┘ │ + pgvecto.rs) │ + │ └─────────────────┘ + │ +┌────────▼────────┐ ┌─────────────────┐ +│ immich-ml │ │ valkey │ +│ (ML inference) │ │ (Redis cache) │ +└─────────────────┘ └─────────────────┘ + │ +┌────────▼────────┐ +│ sifaka NFS │ +│ /volume1/photos│ +└─────────────────┘ +``` + +## Helm Values + +The Helm chart is configured via `values.yaml`. Key settings: + +- `image.tag`: Immich version (update manually) +- `immich.persistence.library.existingClaim`: Points to `immich-library` PVC +- `machine-learning.enabled`: AI features for face/object recognition +- `valkey.enabled`: Redis cache included in chart + +## Troubleshooting + +```bash +# Check pods +kubectl -n immich get pods + +# Check immich-pg cluster +kubectl -n databases get cluster immich-pg + +# View server logs +kubectl -n immich logs -l app.kubernetes.io/name=immich-server + +# View ML logs +kubectl -n immich logs -l app.kubernetes.io/name=immich-machine-learning + +# Check PVC binding +kubectl -n immich get pvc +``` diff --git a/argocd/manifests/immich-ringtail/ingress-tailscale.yaml b/argocd/manifests/immich/ingress-tailscale.yaml similarity index 57% rename from argocd/manifests/immich-ringtail/ingress-tailscale.yaml rename to argocd/manifests/immich/ingress-tailscale.yaml index f0b5fe1..777bfb6 100644 --- a/argocd/manifests/immich-ringtail/ingress-tailscale.yaml +++ b/argocd/manifests/immich/ingress-tailscale.yaml @@ -1,9 +1,6 @@ -# Tailscale ProxyGroup Ingress for Immich on ringtail. -# -# Production hostname: photos.tail8d86e.ts.net -# (during the cutover window this was photos-ringtail; the minikube -# ingress was torn down before this was renamed to photos to avoid -# the Tailscale device-name collision.) +# Tailscale Ingress for Immich +# Exposes Immich at photos.tail8d86e.ts.net +# Caddy will proxy photos.ops.eblu.me to this endpoint apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -18,7 +15,13 @@ metadata: gethomepage.dev/icon: "immich.png" gethomepage.dev/description: "Photo management" gethomepage.dev/href: "https://photos.ops.eblu.me" - gethomepage.dev/pod-selector: "app=immich,component=server" + gethomepage.dev/pod-selector: "app.kubernetes.io/name=server" + # TODO: Add Immich widget - requires API key from Account Settings > API Keys + # See: https://gethomepage.dev/widgets/services/immich/ + # gethomepage.dev/widget.type: "immich" + # gethomepage.dev/widget.url: "https://photos.ops.eblu.me" + # gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_IMMICH_API_KEY}}" + # gethomepage.dev/widget.version: "2" spec: ingressClassName: tailscale rules: diff --git a/argocd/manifests/immich/kustomization.yaml b/argocd/manifests/immich/kustomization.yaml new file mode 100644 index 0000000..1c1c6d8 --- /dev/null +++ b/argocd/manifests/immich/kustomization.yaml @@ -0,0 +1,11 @@ +# Immich non-Helm resources (storage) +# These must be deployed before the Helm chart +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: immich + +resources: + - pv-nfs.yaml + - pvc.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/immich/pv-nfs.yaml b/argocd/manifests/immich/pv-nfs.yaml new file mode 100644 index 0000000..0bd6ee2 --- /dev/null +++ b/argocd/manifests/immich/pv-nfs.yaml @@ -0,0 +1,22 @@ +# NFS PersistentVolume for Immich photo library +# Requires: NFS share on sifaka at /volume1/photos with NFS permissions for indri +# +# To create on Synology: +# 1. Control Panel > Shared Folder > Create +# 2. Name: photos, Location: Volume 1 +# 3. Control Panel > File Services > NFS > NFS Rules +# 4. Add rule for "photos" share: Hostname=indri, Privilege=Read/Write, Squash=No mapping +apiVersion: v1 +kind: PersistentVolume +metadata: + name: immich-library-nfs-pv +spec: + capacity: + storage: 2Ti + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + nfs: + server: sifaka + path: /volume1/photos diff --git a/argocd/manifests/immich-ringtail/pvc.yaml b/argocd/manifests/immich/pvc.yaml similarity index 54% rename from argocd/manifests/immich-ringtail/pvc.yaml rename to argocd/manifests/immich/pvc.yaml index 5bfc052..c764636 100644 --- a/argocd/manifests/immich-ringtail/pvc.yaml +++ b/argocd/manifests/immich/pvc.yaml @@ -1,5 +1,5 @@ -# PersistentVolumeClaim for Immich photo library on ringtail. -# Binds to immich-library-nfs-pv-ringtail (sifaka:/volume1/photos). +# PersistentVolumeClaim for Immich photo library +# Binds to the NFS PV for sifaka:/volume1/photos apiVersion: v1 kind: PersistentVolumeClaim metadata: @@ -9,7 +9,7 @@ spec: accessModes: - ReadWriteMany storageClassName: "" - volumeName: immich-library-nfs-pv-ringtail + volumeName: immich-library-nfs-pv resources: requests: storage: 2Ti diff --git a/argocd/manifests/immich/values.yaml b/argocd/manifests/immich/values.yaml new file mode 100644 index 0000000..493d9b1 --- /dev/null +++ b/argocd/manifests/immich/values.yaml @@ -0,0 +1,71 @@ +# Immich Helm values for blumeops +# Chart: https://github.com/immich-app/immich-charts (v0.10.3) +# +# Immich requires: +# - PostgreSQL with VectorChord extension (separate immich-pg cluster) +# - Redis/Valkey (included in chart) +# - Library storage PVC (photos directory from sifaka NFS) + +# Shared environment variables +env: + TZ: "America/Los_Angeles" + +# Shared controller settings - image tag and DB connection +controllers: + main: + containers: + main: + image: + tag: v2.5.6 + env: + DB_HOSTNAME: "immich-pg-rw.databases.svc.cluster.local" + DB_PORT: "5432" + DB_DATABASE_NAME: "immich" + DB_USERNAME: "immich" + DB_PASSWORD: + valueFrom: + secretKeyRef: + name: immich-db + key: password + +# Immich server configuration +immich: + persistence: + library: + existingClaim: immich-library + +# Machine Learning service +machine-learning: + enabled: true + persistence: + cache: + enabled: true + type: persistentVolumeClaim + accessMode: ReadWriteOnce + size: 10Gi + resources: + requests: + memory: "512Mi" + cpu: "100m" + limits: + memory: "4Gi" + cpu: "2000m" + +# Valkey (Redis fork) - included in chart +valkey: + enabled: true + persistence: + data: + enabled: true + type: emptyDir + size: 1Gi + +# Server resources for minikube +server: + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "2Gi" + cpu: "2000m" diff --git a/argocd/manifests/kingfisher/cronjob.yaml b/argocd/manifests/kingfisher/cronjob.yaml deleted file mode 100644 index 3c47528..0000000 --- a/argocd/manifests/kingfisher/cronjob.yaml +++ /dev/null @@ -1,67 +0,0 @@ ---- -apiVersion: batch/v1 -kind: CronJob -metadata: - name: kingfisher - namespace: kingfisher -spec: - schedule: "0 4 * * 0" # Sunday 4am (after Prowler k8s scan at 3am) - concurrencyPolicy: Forbid - jobTemplate: - spec: - ttlSecondsAfterFinished: 604800 # Auto-delete after 7 days - template: - spec: - securityContext: - seccompProfile: - type: RuntimeDefault - containers: - - name: kingfisher - image: registry.ops.eblu.me/blumeops/kingfisher:kustomized - command: ["/bin/bash", "-c"] - args: - - | - set -e - STAMP=$(date +%Y%m%d-%H%M%S) - OUTDIR=/reports/kingfisher/$(date +%Y-%m-%d) - mkdir -p "$OUTDIR" - - # Exit codes: 0=clean, 200=findings, 205=validated findings. - # All are successful scans; only other codes are real errors. - rc=0 - kingfisher scan gitea \ - --api-url https://forge.ops.eblu.me/api/v1/ \ - --clone-url-base https://forge.ops.eblu.me/ \ - --user eblume \ - --repo-type all \ - --no-update-check \ - --tls-mode lax \ - --allow-internal-ips \ - --format html \ - --output "$OUTDIR/scan-${STAMP}.html" \ - || rc=$? - - if [ "$rc" -eq 0 ] || [ "$rc" -eq 200 ] || [ "$rc" -eq 205 ]; then - exit 0 - fi - exit "$rc" - env: - - name: KF_GITEA_TOKEN - valueFrom: - secretKeyRef: - name: kingfisher-forgejo-token - key: KF_GITEA_TOKEN - volumeMounts: - - name: reports - mountPath: /reports - resources: - requests: - memory: 256Mi - cpu: 100m - limits: - memory: 1Gi - restartPolicy: OnFailure - volumes: - - name: reports - persistentVolumeClaim: - claimName: kingfisher-reports diff --git a/argocd/manifests/kingfisher/external-secret.yaml b/argocd/manifests/kingfisher/external-secret.yaml deleted file mode 100644 index 6f6a5f2..0000000 --- a/argocd/manifests/kingfisher/external-secret.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# ExternalSecret for Forgejo API token used by Kingfisher to enumerate repos -# -# 1Password item: "Forgejo Secrets" in blumeops vault -# Field: api-token -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: kingfisher-forgejo-token - namespace: kingfisher -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: kingfisher-forgejo-token - creationPolicy: Owner - data: - - secretKey: KF_GITEA_TOKEN - remoteRef: - key: Forgejo Secrets - property: api-token diff --git a/argocd/manifests/kingfisher/kustomization.yaml b/argocd/manifests/kingfisher/kustomization.yaml deleted file mode 100644 index d501bbb..0000000 --- a/argocd/manifests/kingfisher/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: kingfisher - -resources: - - pv-nfs.yaml - - pvc.yaml - - external-secret.yaml - - cronjob.yaml - -images: - - name: registry.ops.eblu.me/blumeops/kingfisher - newTag: v165768b-0fe0eed-nix diff --git a/argocd/manifests/kingfisher/pv-nfs.yaml b/argocd/manifests/kingfisher/pv-nfs.yaml deleted file mode 100644 index 61ca4ce..0000000 --- a/argocd/manifests/kingfisher/pv-nfs.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# NFS PersistentVolume for Kingfisher secret scan reports -# Reuses the same sifaka:/volume1/reports share as Prowler -apiVersion: v1 -kind: PersistentVolume -metadata: - name: kingfisher-reports-nfs-pv -spec: - capacity: - storage: 1Gi - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - storageClassName: "" - nfs: - server: sifaka - path: /volume1/reports diff --git a/argocd/manifests/kingfisher/pvc.yaml b/argocd/manifests/kingfisher/pvc.yaml deleted file mode 100644 index f48da95..0000000 --- a/argocd/manifests/kingfisher/pvc.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: kingfisher-reports - namespace: kingfisher -spec: - accessModes: - - ReadWriteMany - storageClassName: "" - volumeName: kingfisher-reports-nfs-pv - resources: - requests: - storage: 1Gi diff --git a/argocd/manifests/kiwix/kustomization.yaml b/argocd/manifests/kiwix/kustomization.yaml index 1e11bdb..2af4065 100644 --- a/argocd/manifests/kiwix/kustomization.yaml +++ b/argocd/manifests/kiwix/kustomization.yaml @@ -10,7 +10,7 @@ resources: images: - name: registry.ops.eblu.me/blumeops/kiwix-serve - newTag: v3.8.2-7a42aeb + newTag: v3.8.2-613f05d - name: registry.ops.eblu.me/blumeops/transmission newTag: v4.1.1-r1-613f05d - name: registry.ops.eblu.me/blumeops/kubectl diff --git a/argocd/manifests/kube-state-metrics-ringtail/kustomization.yaml b/argocd/manifests/kube-state-metrics-ringtail/kustomization.yaml index 9d36e2d..005cba8 100644 --- a/argocd/manifests/kube-state-metrics-ringtail/kustomization.yaml +++ b/argocd/manifests/kube-state-metrics-ringtail/kustomization.yaml @@ -6,5 +6,4 @@ resources: - service.yaml images: - name: registry.k8s.io/kube-state-metrics/kube-state-metrics - newName: registry.ops.eblu.me/blumeops/kube-state-metrics - newTag: v2.18.0-f59f885-nix + newTag: v2.18.0 diff --git a/argocd/manifests/kube-state-metrics/deployment.yaml b/argocd/manifests/kube-state-metrics/deployment.yaml index ddaf3e2..ae34339 100644 --- a/argocd/manifests/kube-state-metrics/deployment.yaml +++ b/argocd/manifests/kube-state-metrics/deployment.yaml @@ -51,5 +51,3 @@ spec: capabilities: drop: - ALL - seccompProfile: - type: RuntimeDefault diff --git a/argocd/manifests/kube-state-metrics/kustomization.yaml b/argocd/manifests/kube-state-metrics/kustomization.yaml index efac6ff..005cba8 100644 --- a/argocd/manifests/kube-state-metrics/kustomization.yaml +++ b/argocd/manifests/kube-state-metrics/kustomization.yaml @@ -6,5 +6,4 @@ resources: - service.yaml images: - name: registry.k8s.io/kube-state-metrics/kube-state-metrics - newName: registry.ops.eblu.me/blumeops/kube-state-metrics - newTag: v2.18.0-f59f885 + newTag: v2.18.0 diff --git a/argocd/manifests/mealie-ringtail/pvc.yaml b/argocd/manifests/mealie-ringtail/pvc.yaml deleted file mode 100644 index 89c38ef..0000000 --- a/argocd/manifests/mealie-ringtail/pvc.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# SQLite data volume for Mealie on ringtail. Contents copied from the -# minikube mealie-data PVC at cutover (recipes, meal plans, uploaded media). -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: mealie-data - namespace: mealie -spec: - accessModes: - - ReadWriteOnce - storageClassName: local-path - resources: - requests: - storage: 2Gi diff --git a/argocd/manifests/mealie-ringtail/deployment.yaml b/argocd/manifests/mealie/deployment.yaml similarity index 89% rename from argocd/manifests/mealie-ringtail/deployment.yaml rename to argocd/manifests/mealie/deployment.yaml index 10d06ab..bdcf91e 100644 --- a/argocd/manifests/mealie-ringtail/deployment.yaml +++ b/argocd/manifests/mealie/deployment.yaml @@ -1,9 +1,3 @@ -# Mealie on ringtail k3s — Nix image. -# -# Single gunicorn process (the Nix image's default `mealie-run` entrypoint -# runs init_db then gunicorn), serving the prebuilt frontend. DB is SQLite -# on the mealie-data PVC; its contents are copied from the minikube PVC at -# cutover. See [[migrate-wave1-ringtail]]. apiVersion: apps/v1 kind: Deployment metadata: @@ -11,8 +5,6 @@ metadata: namespace: mealie spec: replicas: 1 - strategy: - type: Recreate selector: matchLabels: app: mealie diff --git a/argocd/manifests/mealie-ringtail/external-secret.yaml b/argocd/manifests/mealie/external-secret.yaml similarity index 100% rename from argocd/manifests/mealie-ringtail/external-secret.yaml rename to argocd/manifests/mealie/external-secret.yaml diff --git a/argocd/manifests/mealie-ringtail/ingress-tailscale.yaml b/argocd/manifests/mealie/ingress-tailscale.yaml similarity index 100% rename from argocd/manifests/mealie-ringtail/ingress-tailscale.yaml rename to argocd/manifests/mealie/ingress-tailscale.yaml diff --git a/argocd/manifests/mealie-ringtail/kustomization.yaml b/argocd/manifests/mealie/kustomization.yaml similarity index 88% rename from argocd/manifests/mealie-ringtail/kustomization.yaml rename to argocd/manifests/mealie/kustomization.yaml index ad65785..fb0713b 100644 --- a/argocd/manifests/mealie-ringtail/kustomization.yaml +++ b/argocd/manifests/mealie/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: registry.ops.eblu.me/blumeops/mealie - newTag: v3.16.0-e0057b4-nix + newTag: v3.12.0-613f05d diff --git a/argocd/manifests/immich-ringtail/pvc-ml-cache.yaml b/argocd/manifests/mealie/pvc.yaml similarity index 58% rename from argocd/manifests/immich-ringtail/pvc-ml-cache.yaml rename to argocd/manifests/mealie/pvc.yaml index 1e5a3d6..f473e07 100644 --- a/argocd/manifests/immich-ringtail/pvc-ml-cache.yaml +++ b/argocd/manifests/mealie/pvc.yaml @@ -2,11 +2,12 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: immich-ml-cache - namespace: immich + name: mealie-data + namespace: mealie spec: accessModes: - ReadWriteOnce + storageClassName: standard resources: requests: - storage: 10Gi + storage: 2Gi diff --git a/argocd/manifests/mealie-ringtail/service.yaml b/argocd/manifests/mealie/service.yaml similarity index 100% rename from argocd/manifests/mealie-ringtail/service.yaml rename to argocd/manifests/mealie/service.yaml diff --git a/argocd/manifests/miniflux/kustomization.yaml b/argocd/manifests/miniflux/kustomization.yaml index 1acc708..8207d55 100644 --- a/argocd/manifests/miniflux/kustomization.yaml +++ b/argocd/manifests/miniflux/kustomization.yaml @@ -10,4 +10,4 @@ resources: images: - name: registry.ops.eblu.me/blumeops/miniflux - newTag: v2.2.19-352b95c + newTag: v2.2.17-613f05d diff --git a/argocd/manifests/navidrome/kustomization.yaml b/argocd/manifests/navidrome/kustomization.yaml index 41689f4..df5677e 100644 --- a/argocd/manifests/navidrome/kustomization.yaml +++ b/argocd/manifests/navidrome/kustomization.yaml @@ -11,4 +11,4 @@ resources: - ingress-tailscale.yaml images: - name: registry.ops.eblu.me/blumeops/navidrome - newTag: v0.61.1-3ecd888 + newTag: v0.60.3-613f05d diff --git a/argocd/manifests/nvidia-device-plugin/kustomization.yaml b/argocd/manifests/nvidia-device-plugin/kustomization.yaml index f5a33ae..a46edf6 100644 --- a/argocd/manifests/nvidia-device-plugin/kustomization.yaml +++ b/argocd/manifests/nvidia-device-plugin/kustomization.yaml @@ -10,4 +10,4 @@ resources: images: - name: nvcr.io/nvidia/k8s-device-plugin - newTag: v0.19.2 + newTag: v0.19.0 diff --git a/argocd/manifests/nvidia-device-plugin/time-slicing-config.yaml b/argocd/manifests/nvidia-device-plugin/time-slicing-config.yaml index 100e7a9..dee2fd7 100644 --- a/argocd/manifests/nvidia-device-plugin/time-slicing-config.yaml +++ b/argocd/manifests/nvidia-device-plugin/time-slicing-config.yaml @@ -11,4 +11,4 @@ data: timeSlicing: resources: - name: nvidia.com/gpu - replicas: 4 + replicas: 2 diff --git a/argocd/manifests/ollama/kustomization.yaml b/argocd/manifests/ollama/kustomization.yaml index fd54eec..75add74 100644 --- a/argocd/manifests/ollama/kustomization.yaml +++ b/argocd/manifests/ollama/kustomization.yaml @@ -11,7 +11,7 @@ resources: images: - name: ollama/ollama - newTag: "0.20.4" + newTag: "0.17.5" configMapGenerator: - name: ollama-models diff --git a/argocd/manifests/paperless-ringtail/deployment.yaml b/argocd/manifests/paperless-ringtail/deployment.yaml deleted file mode 100644 index de4f456..0000000 --- a/argocd/manifests/paperless-ringtail/deployment.yaml +++ /dev/null @@ -1,201 +0,0 @@ -# Paperless-ngx on ringtail k3s — Nix image, multi-process. -# -# The upstream s6 image ran web + worker + scheduler + consumer (and DB -# migrations) in one container. The Nix image (containers/paperless/ -# default.nix) ships the binaries but no supervisor, so we run those as -# four containers in one pod, sharing the local data/consume dirs -# (emptyDir) and the NFS media volume; redis is colocated so -# PAPERLESS_REDIS=localhost works for all. A migrate initContainer runs -# DB migrations once before the app containers start. -# -# DB points in-cluster at the ringtail blumeops-pg (was pg.ops.eblu.me on -# indri). PAPERLESS_{DATA_DIR,MEDIA_ROOT,CONSUMPTION_DIR} are set -# explicitly because the Nix package does not default to the upstream -# /usr/src/paperless paths. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: paperless - namespace: paperless -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app: paperless - template: - metadata: - labels: - app: paperless - spec: - securityContext: - seccompProfile: - type: RuntimeDefault - initContainers: - # redis as a native sidecar (restartPolicy: Always): starts before - # the migrate init and stays running for the app containers, so all - # of them reach PAPERLESS_REDIS=localhost:6379. - - name: redis - image: docker.io/library/redis:kustomized - restartPolicy: Always - ports: - - containerPort: 6379 - volumeMounts: - - name: redis-data - mountPath: /data - resources: - requests: - memory: "32Mi" - cpu: "10m" - limits: - memory: "128Mi" - - name: migrate - image: registry.ops.eblu.me/blumeops/paperless:kustomized - command: ["paperless-ngx", "migrate", "--no-input"] - env: &paperless-env - - name: PAPERLESS_URL - value: "https://paperless.ops.eblu.me" - - name: PAPERLESS_REDIS - value: "redis://localhost:6379" - - name: PAPERLESS_DBHOST - value: "blumeops-pg-rw.databases.svc.cluster.local" - - name: PAPERLESS_DBPORT - value: "5432" - - name: PAPERLESS_DBNAME - value: "paperless" - - name: PAPERLESS_DBUSER - value: "paperless" - - name: PAPERLESS_DBPASS - valueFrom: - secretKeyRef: - name: paperless-secrets - key: db-password - # Explicit port to override the k8s-injected PAPERLESS_PORT - # (service named 'paperless' would set PAPERLESS_PORT=tcp://...) - - name: PAPERLESS_PORT - value: "8000" - - name: PAPERLESS_DATA_DIR - value: "/usr/src/paperless/data" - - name: PAPERLESS_MEDIA_ROOT - value: "/usr/src/paperless/media" - - name: PAPERLESS_CONSUMPTION_DIR - value: "/usr/src/paperless/consume" - - name: PAPERLESS_SECRET_KEY - valueFrom: - secretKeyRef: - name: paperless-secrets - key: secret-key - - name: PAPERLESS_TIME_ZONE - value: "America/Los_Angeles" - - name: PAPERLESS_OCR_LANGUAGE - value: "eng" - - name: PAPERLESS_TASK_WORKERS - value: "1" - - name: PAPERLESS_ADMIN_USER - value: "eblume" - - name: PAPERLESS_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: paperless-secrets - key: admin-password - - name: PAPERLESS_ADMIN_MAIL - value: "blume.erich@gmail.com" - - name: PAPERLESS_APPS - value: "allauth.socialaccount.providers.openid_connect" - - name: PAPERLESS_SOCIALACCOUNT_PROVIDERS - valueFrom: - secretKeyRef: - name: paperless-secrets - key: socialaccount-providers - - name: PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS - value: "true" - - name: PAPERLESS_SOCIAL_AUTO_SIGNUP - value: "true" - - name: PAPERLESS_ACCOUNT_ALLOW_SIGNUPS - value: "false" - - name: PAPERLESS_REDIRECT_LOGIN_TO_SSO - value: "false" - volumeMounts: &paperless-mounts - - name: data - mountPath: /usr/src/paperless/data - - name: media - mountPath: /usr/src/paperless/media - - name: consume - mountPath: /usr/src/paperless/consume - containers: - - name: web - image: registry.ops.eblu.me/blumeops/paperless:kustomized - ports: - - containerPort: 8000 - name: http - env: *paperless-env - volumeMounts: *paperless-mounts - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "1Gi" - cpu: "1000m" - livenessProbe: - httpGet: - path: / - port: 8000 - initialDelaySeconds: 60 - periodSeconds: 30 - readinessProbe: - httpGet: - path: / - port: 8000 - initialDelaySeconds: 30 - periodSeconds: 10 - - - name: worker - image: registry.ops.eblu.me/blumeops/paperless:kustomized - command: ["celery", "--app", "paperless", "worker", "--loglevel", "INFO"] - env: *paperless-env - volumeMounts: *paperless-mounts - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "1Gi" - cpu: "1000m" - - - name: beat - image: registry.ops.eblu.me/blumeops/paperless:kustomized - command: ["celery", "--app", "paperless", "beat", "--loglevel", "INFO"] - env: *paperless-env - volumeMounts: *paperless-mounts - resources: - requests: - memory: "64Mi" - cpu: "20m" - limits: - memory: "256Mi" - - - name: consumer - image: registry.ops.eblu.me/blumeops/paperless:kustomized - command: ["paperless-ngx", "document_consumer"] - env: *paperless-env - volumeMounts: *paperless-mounts - resources: - requests: - memory: "128Mi" - cpu: "50m" - limits: - memory: "512Mi" - - volumes: - - name: data - emptyDir: {} - - name: media - persistentVolumeClaim: - claimName: paperless-media - - name: consume - emptyDir: {} - - name: redis-data - emptyDir: - sizeLimit: 1Gi diff --git a/argocd/manifests/paperless-ringtail/external-secret.yaml b/argocd/manifests/paperless-ringtail/external-secret.yaml deleted file mode 100644 index 750b7c5..0000000 --- a/argocd/manifests/paperless-ringtail/external-secret.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: paperless-secrets - namespace: paperless -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: paperless-secrets - creationPolicy: Owner - data: - - secretKey: db-password - remoteRef: - key: "Paperless (blumeops)" - property: postgresql-password - - secretKey: secret-key - remoteRef: - key: "Paperless (blumeops)" - property: secret-key - - secretKey: admin-password - remoteRef: - key: "Paperless (blumeops)" - property: admin-password - - secretKey: socialaccount-providers - remoteRef: - key: "Paperless (blumeops)" - property: socialaccount-providers diff --git a/argocd/manifests/paperless-ringtail/ingress-tailscale.yaml b/argocd/manifests/paperless-ringtail/ingress-tailscale.yaml deleted file mode 100644 index d09ef67..0000000 --- a/argocd/manifests/paperless-ringtail/ingress-tailscale.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: paperless-tailscale - namespace: paperless - annotations: - tailscale.com/proxy-class: "default" - tailscale.com/proxy-group: "ingress" - gethomepage.dev/enabled: "true" - gethomepage.dev/name: "Paperless" - gethomepage.dev/group: "Home" - gethomepage.dev/icon: "paperless-ngx.png" - gethomepage.dev/description: "Document management" - gethomepage.dev/href: "https://paperless.ops.eblu.me" - gethomepage.dev/pod-selector: "app=paperless" -spec: - ingressClassName: tailscale - defaultBackend: - service: - name: paperless - port: - number: 8000 - tls: - - hosts: - - paperless diff --git a/argocd/manifests/paperless-ringtail/kustomization.yaml b/argocd/manifests/paperless-ringtail/kustomization.yaml deleted file mode 100644 index 41665b8..0000000 --- a/argocd/manifests/paperless-ringtail/kustomization.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: paperless - -resources: - - deployment.yaml - - service.yaml - - pv-nfs.yaml - - pvc.yaml - - ingress-tailscale.yaml - - external-secret.yaml - -images: - - name: registry.ops.eblu.me/blumeops/paperless - newTag: v2.20.15-fcac8e5-nix - # amd64 valkey built via nix (the v8.1.7-ecded30 tag without -nix is the - # arm64 Alpine build for indri and fails on ringtail with exec format error) - - name: docker.io/library/redis - newName: registry.ops.eblu.me/blumeops/valkey - newTag: v8.1.7-ecded30-nix diff --git a/argocd/manifests/paperless-ringtail/pv-nfs.yaml b/argocd/manifests/paperless-ringtail/pv-nfs.yaml deleted file mode 100644 index 2990d1a..0000000 --- a/argocd/manifests/paperless-ringtail/pv-nfs.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# NFS PersistentVolume for the Paperless document library, mounted from -# ringtail. Same sifaka export (/volume1/paperless) as the minikube PV, -# but a distinct PV name so both clusters can declare it during the -# parallel-run before cutover. -# -# Prerequisite: sifaka must have an NFS rule granting ringtail Read/Write -# (Squash=No mapping) on the paperless share — the same step done for -# immich. See [[sifaka-nfs-from-ringtail]]. -apiVersion: v1 -kind: PersistentVolume -metadata: - name: paperless-media-nfs-pv-ringtail -spec: - capacity: - storage: 500Gi - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - storageClassName: "" - nfs: - server: sifaka - path: /volume1/paperless diff --git a/argocd/manifests/paperless-ringtail/pvc.yaml b/argocd/manifests/paperless-ringtail/pvc.yaml deleted file mode 100644 index 8b44660..0000000 --- a/argocd/manifests/paperless-ringtail/pvc.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# PersistentVolumeClaim for the Paperless document library on ringtail. -# Binds the NFS PV for sifaka:/volume1/paperless. -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: paperless-media - namespace: paperless -spec: - accessModes: - - ReadWriteMany - storageClassName: "" - volumeName: paperless-media-nfs-pv-ringtail - resources: - requests: - storage: 500Gi diff --git a/argocd/manifests/paperless-ringtail/service.yaml b/argocd/manifests/paperless-ringtail/service.yaml deleted file mode 100644 index cff2972..0000000 --- a/argocd/manifests/paperless-ringtail/service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: paperless - namespace: paperless -spec: - selector: - app: paperless - ports: - - name: http - port: 8000 - targetPort: 8000 - protocol: TCP diff --git a/argocd/manifests/prowler/cronjob-iac-scan.yaml b/argocd/manifests/prowler/cronjob-iac-scan.yaml index c1303a5..c2e2fac 100644 --- a/argocd/manifests/prowler/cronjob-iac-scan.yaml +++ b/argocd/manifests/prowler/cronjob-iac-scan.yaml @@ -18,37 +18,22 @@ spec: containers: - name: prowler image: registry.ops.eblu.me/blumeops/prowler:kustomized - command: ["/bin/sh", "-c"] - # Prowler's --mutelist-file is a no-op for the IaC provider - # (it delegates to Trivy). The Prowler image's trivy shim - # injects --ignorefile $TRIVY_IGNOREFILE when set; see - # containers/prowler/Dockerfile. - env: - - name: TRIVY_IGNOREFILE - value: /mutelist/trivyignore.yaml args: - - | - DATEDIR=/reports/prowler-iac/$(date +%Y-%m-%d) - mkdir -p "$DATEDIR" - prowler iac \ - --scan-repository-url https://forge.ops.eblu.me/eblume/blumeops.git \ - -z \ - --output-formats html csv json-ocsf \ - --output-directory "$DATEDIR" + - iac + - --scan-repository-url + - https://forge.ops.eblu.me/eblume/blumeops.git + - -z + - --output-formats + - html + - csv + - json-ocsf + - --output-directory + - /reports/prowler-iac volumeMounts: - name: reports mountPath: /reports - - name: mutelist - mountPath: /mutelist - readOnly: true restartPolicy: OnFailure volumes: - name: reports persistentVolumeClaim: claimName: prowler-reports - - name: mutelist - configMap: - name: prowler-mutelist - items: - - key: trivyignore.yaml - path: trivyignore.yaml diff --git a/argocd/manifests/prowler/cronjob-image-scan.yaml b/argocd/manifests/prowler/cronjob-image-scan.yaml index b779d08..b69ad63 100644 --- a/argocd/manifests/prowler/cronjob-image-scan.yaml +++ b/argocd/manifests/prowler/cronjob-image-scan.yaml @@ -15,25 +15,60 @@ spec: securityContext: seccompProfile: type: RuntimeDefault + initContainers: + # Workaround: Prowler's --registry flag is broken (registry args + # not passed to provider constructor). Generate image list from + # zot catalog API instead. + # See: https://github.com/prowler-cloud/prowler/issues/10457 + - name: enumerate-images + image: registry.ops.eblu.me/blumeops/prowler:kustomized + command: ["python3", "-c"] + args: + - | + import json, urllib.request + + REGISTRY = "https://registry.ops.eblu.me" + catalog = json.loads(urllib.request.urlopen(f"{REGISTRY}/v2/_catalog").read()) + images = [] + for repo in catalog["repositories"]: + if not repo.startswith("blumeops/"): + continue + tags = json.loads(urllib.request.urlopen(f"{REGISTRY}/v2/{repo}/tags/list").read()) + for tag in tags.get("tags") or []: + images.append(f"registry.ops.eblu.me/{repo}:{tag}") + + with open("/shared/images.txt", "w") as f: + f.write("\n".join(images) + "\n") + print(f"Discovered {len(images)} images") + for img in images: + print(img) + volumeMounts: + - name: shared + mountPath: /shared containers: - name: prowler image: registry.ops.eblu.me/blumeops/prowler:kustomized - command: ["/bin/sh", "-c"] args: - - | - DATEDIR=/reports/prowler-images/$(date +%Y-%m-%d) - mkdir -p "$DATEDIR" - prowler image \ - --registry https://registry.ops.eblu.me \ - --image-filter "^blumeops/" \ - -z \ - --output-formats html csv json-ocsf \ - --output-directory "$DATEDIR" + - image + - --image-list + - /shared/images.txt + - -z + - --output-formats + - html + - csv + - json-ocsf + - --output-directory + - /reports/prowler-images volumeMounts: - name: reports mountPath: /reports + - name: shared + mountPath: /shared + readOnly: true restartPolicy: OnFailure volumes: - name: reports persistentVolumeClaim: claimName: prowler-reports + - name: shared + emptyDir: {} diff --git a/argocd/manifests/prowler/cronjob.yaml b/argocd/manifests/prowler/cronjob.yaml index 95b7dee..545a9c8 100644 --- a/argocd/manifests/prowler/cronjob.yaml +++ b/argocd/manifests/prowler/cronjob.yaml @@ -15,48 +15,23 @@ spec: securityContext: seccompProfile: type: RuntimeDefault - initContainers: - - name: merge-mutelist - image: registry.ops.eblu.me/blumeops/prowler:kustomized - command: ["python3", "-c"] - args: - - | - import yaml, glob, pathlib - merged = {"Mutelist": {"Accounts": {"*": {"Checks": {}}}}} - for f in sorted(glob.glob("/mutelist-parts/*.yaml")): - with open(f) as fh: - data = yaml.safe_load(fh) - checks = data.get("Mutelist", {}).get("Accounts", {}).get("*", {}).get("Checks", {}) - merged["Mutelist"]["Accounts"]["*"]["Checks"].update(checks) - pathlib.Path("/tmp/mutelist").mkdir(exist_ok=True) - with open("/tmp/mutelist/mutelist.yaml", "w") as fh: - yaml.dump(merged, fh, default_flow_style=False) - print(f"Merged {len(merged['Mutelist']['Accounts']['*']['Checks'])} checks from {len(glob.glob('/mutelist-parts/*.yaml'))} files") - volumeMounts: - - name: mutelist-parts - mountPath: /mutelist-parts - - name: mutelist-merged - mountPath: /tmp/mutelist containers: - name: prowler image: registry.ops.eblu.me/blumeops/prowler:kustomized - command: ["/bin/sh", "-c"] args: - - | - DATEDIR=/reports/prowler/$(date +%Y-%m-%d) - mkdir -p "$DATEDIR" - prowler kubernetes \ - --compliance cis_1.11_kubernetes \ - --mutelist-file /tmp/mutelist/mutelist.yaml \ - -z \ - --output-formats html csv json-ocsf \ - --output-directory "$DATEDIR" + - kubernetes + - --compliance + - cis_1.11_kubernetes + - -z + - --output-formats + - html + - csv + - json-ocsf + - --output-directory + - /reports/prowler volumeMounts: - name: reports mountPath: /reports - - name: mutelist-merged - mountPath: /tmp/mutelist - readOnly: true - name: var-lib-kubelet mountPath: /var/lib/kubelet readOnly: true @@ -72,11 +47,6 @@ spec: - name: reports persistentVolumeClaim: claimName: prowler-reports - - name: mutelist-parts - configMap: - name: prowler-mutelist - - name: mutelist-merged - emptyDir: {} - name: var-lib-kubelet hostPath: path: /var/lib/kubelet diff --git a/argocd/manifests/prowler/kustomization.yaml b/argocd/manifests/prowler/kustomization.yaml index 1d92a6b..b34b2c1 100644 --- a/argocd/manifests/prowler/kustomization.yaml +++ b/argocd/manifests/prowler/kustomization.yaml @@ -13,18 +13,6 @@ resources: - cronjob-image-scan.yaml - cronjob-iac-scan.yaml -configMapGenerator: - - name: prowler-mutelist - options: - disableNameSuffixHash: true - files: - - mutelist/apiserver.yaml - - mutelist/control-plane.yaml - - mutelist/core-pod-security.yaml - - mutelist/manual-node-checks.yaml - - mutelist/rbac.yaml - - mutelist/trivyignore.yaml - images: - name: registry.ops.eblu.me/blumeops/prowler - newTag: v5.23.0-495e45d + newTag: v5.22.0-6960243 diff --git a/argocd/manifests/prowler/mutelist/apiserver.yaml b/argocd/manifests/prowler/mutelist/apiserver.yaml deleted file mode 100644 index fd077e8..0000000 --- a/argocd/manifests/prowler/mutelist/apiserver.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# Minikube apiserver — flags managed by static pod manifests. -Mutelist: - Accounts: - "*": - Checks: - "apiserver_always_pull_images_plugin": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Only the operator has cluster access; all images pulled from private zot registry." - "apiserver_audit_log_maxage_set": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Alloy/Loki provides pod-level audit trail." - "apiserver_audit_log_maxbackup_set": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Alloy/Loki provides pod-level audit trail." - "apiserver_audit_log_maxsize_set": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Alloy/Loki provides pod-level audit trail." - "apiserver_audit_log_path_set": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Alloy/Loki provides pod-level audit trail." - "apiserver_deny_service_external_ips": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "No external IPs routable; cluster only reachable via tailnet." - "apiserver_disable_profiling": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Profiling endpoint unreachable from public internet." - "apiserver_encryption_provider_config_set": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Etcd not network-exposed; only operator has node access." - "apiserver_kubelet_cert_auth": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Kubelet API not exposed outside the node; minikube auto-generates certificates." - "apiserver_request_timeout_set": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "API server only reachable via tailnet; DoS risk limited to trusted clients." - "apiserver_service_account_lookup_true": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "Only operator manages service accounts; no revoked tokens in circulation." - "apiserver_strong_ciphers_only": - Regions: ["*"] - Resources: ["^kube-apiserver-minikube$"] - Description: "API server traffic encrypted by WireGuard at the network layer." diff --git a/argocd/manifests/prowler/mutelist/control-plane.yaml b/argocd/manifests/prowler/mutelist/control-plane.yaml deleted file mode 100644 index d3cc34a..0000000 --- a/argocd/manifests/prowler/mutelist/control-plane.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Minikube control-plane components — managed by static pod manifests. -Mutelist: - Accounts: - "*": - Checks: - "controllermanager_disable_profiling": - Regions: ["*"] - Resources: ["^kube-controller-manager-minikube$"] - Description: "Profiling endpoint unreachable from public internet." - "scheduler_profiling": - Regions: ["*"] - Resources: ["^kube-scheduler-minikube$"] - Description: "Profiling endpoint unreachable from public internet." - "kubelet_tls_cert_and_key": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "Kubelet API not exposed outside node; minikube auto-generates certificates." diff --git a/argocd/manifests/prowler/mutelist/core-pod-security.yaml b/argocd/manifests/prowler/mutelist/core-pod-security.yaml deleted file mode 100644 index b1e986e..0000000 --- a/argocd/manifests/prowler/mutelist/core-pod-security.yaml +++ /dev/null @@ -1,87 +0,0 @@ -# Pod security checks — system pods, operator-managed pods, and accepted -# operational needs. Each check ID appears once with all matching resources. -Mutelist: - Accounts: - "*": - Checks: - "core_minimize_hostNetwork_containers": - Regions: ["*"] - Resources: - # Minikube control plane - - "^etcd-minikube$" - - "^kube-apiserver-minikube$" - - "^kube-controller-manager-minikube$" - - "^kube-scheduler-minikube$" - # Minikube system pods - - "^kube-proxy-" - - "^kindnet-" - - "^storage-provisioner$" - Description: >- - Control-plane and networking pods require hostNetwork by design. - Host network itself is only reachable via tailnet. - "core_minimize_privileged_containers": - Regions: ["*"] - Resources: - # Minikube system - - "^kube-proxy-" - # Tailscale operator-managed proxies - - "^ts-" - - "^ingress-" - # Forgejo runner - - "^forgejo-runner-" - Description: >- - kube-proxy: system pod, single-user cluster. ts-*/ingress-*: - Tailscale operator-managed. forgejo-runner: DinD limited to - trusted private forge repos. - "core_seccomp_profile_docker_default": - Regions: ["*"] - Resources: - # Minikube system pods - - "^coredns-" - - "^kube-proxy-" - - "^kindnet-" - - "^storage-provisioner$" - # Tailscale operator-managed pods - - "^ts-" - - "^operator-" - - "^nameserver-" - - "^ingress-" - Description: >- - System pods managed by minikube and Tailscale operator; - seccomp profiles set by upstream. Single-user cluster limits - exploit surface. - "core_minimize_hostPID_containers": - Regions: ["*"] - Resources: - - "^prowler-" - Description: >- - Prowler CIS scanner requires hostPID for file permission - checks. Runs as CronJob with 7-day TTL, not a persistent - workload. - "core_minimize_root_containers_admission": - Regions: ["*"] - Resources: - - "^grafana-" - Description: >- - Root limited to init-chown-data container; all runtime - containers run as UID 472 with caps dropped. - "core_minimize_containers_added_capabilities": - Regions: ["*"] - Resources: - # Minikube system pods - - "^coredns-" - - "^kindnet-" - # Grafana init-chown-data - - "^grafana-" - Description: >- - System pods: capabilities required by function - (minikube-managed). Grafana: CHOWN limited to init phase; - runtime containers drop ALL. - "core_minimize_containers_capabilities_assigned": - Regions: ["*"] - Resources: - - "^coredns-" - - "^kindnet-" - - "^grafana-" - Description: >- - See core_minimize_containers_added_capabilities. diff --git a/argocd/manifests/prowler/mutelist/manual-node-checks.yaml b/argocd/manifests/prowler/mutelist/manual-node-checks.yaml deleted file mode 100644 index c91a2a6..0000000 --- a/argocd/manifests/prowler/mutelist/manual-node-checks.yaml +++ /dev/null @@ -1,59 +0,0 @@ -# Node-level and RBAC checks that Prowler reports as MANUAL because it -# cannot evaluate them from inside a pod. Verified out-of-band by the -# node-verification block in `mise run review-compliance-reports`, which -# SSHes into the minikube node and checks each condition directly. -Mutelist: - Accounts: - "*": - Checks: - "etcd_unique_ca": - Regions: ["*"] - Resources: ["^etcd-minikube$"] - Description: "Etcd CA fingerprint verified different from cluster CA by review-compliance-reports." - "kubelet_conf_file_ownership": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "File ownership verified root:root by review-compliance-reports." - "kubelet_conf_file_permissions": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "File permissions verified 600 by review-compliance-reports." - "kubelet_config_yaml_ownership": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "File ownership verified root:root by review-compliance-reports." - "kubelet_config_yaml_permissions": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "File permissions verified 644 by review-compliance-reports." - "kubelet_service_file_ownership_root": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "File ownership verified root:root by review-compliance-reports." - "kubelet_service_file_permissions": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "File permissions verified 644 by review-compliance-reports." - "kubelet_disable_read_only_port": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "readOnlyPort absence (defaults to 0) verified by review-compliance-reports." - "kubelet_event_record_qps": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "eventRecordQPS absence (defaults to 5) verified by review-compliance-reports." - "kubelet_manage_iptables": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "makeIPTablesUtilChains absence (defaults to true) verified by review-compliance-reports." - "kubelet_strong_ciphers_only": - Regions: ["*"] - Resources: ["^kubelet-config$"] - Description: "Go default ciphers used; all traffic WireGuard-encrypted via tailnet." - "rbac_cluster_admin_usage": - Regions: ["*"] - Resources: - - "^cluster-admin$" - - "^kubeadm:cluster-admins$" - - "^minikube-rbac$" - Description: "Only built-in/minikube cluster-admin bindings present; verified by review-compliance-reports." diff --git a/argocd/manifests/prowler/mutelist/rbac.yaml b/argocd/manifests/prowler/mutelist/rbac.yaml deleted file mode 100644 index 324809d..0000000 --- a/argocd/manifests/prowler/mutelist/rbac.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# RBAC checks — built-in Kubernetes roles and operator roles that require -# broad permissions by design. -Mutelist: - Accounts: - "*": - Checks: - "rbac_minimize_wildcard_use_roles": - Regions: ["*"] - Resources: - # Built-in Kubernetes roles - - "^cluster-admin$" - - "^system:" - # ArgoCD - - "^argocd-" - Description: >- - Built-in K8s roles: only operator can bind them. ArgoCD: - requires broad access but is SSO-gated via Authentik OIDC. - "rbac_minimize_pod_creation_access": - Regions: ["*"] - Resources: - # Built-in Kubernetes roles - - "^admin$" - - "^edit$" - - "^system:" - # CloudNativePG operator - - "^cnpg-manager$" - Description: >- - Built-in K8s roles and CNPG operator. Only the operator can - assign these roles; no untrusted users have cluster access. - "rbac_minimize_service_account_token_creation": - Regions: ["*"] - Resources: - - "^system:" - Description: >- - kube-controller-manager requires token creation for SA - management. Only operator manages service accounts. diff --git a/argocd/manifests/prowler/mutelist/trivyignore.yaml b/argocd/manifests/prowler/mutelist/trivyignore.yaml deleted file mode 100644 index 87af966..0000000 --- a/argocd/manifests/prowler/mutelist/trivyignore.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# Trivy ignorefile for Prowler IaC scan. -# -# Prowler's `--mutelist-file` flag is a no-op for the IaC provider -# (iac_provider.py sets self._mutelist = None and delegates to Trivy). -# Trivy in turn does not auto-discover this YAML form from cwd, so the -# Prowler image ships a shim wrapper around `trivy` that injects -# --ignorefile $TRIVY_IGNOREFILE when the env var is set. The cronjob -# mounts this file and sets TRIVY_IGNOREFILE accordingly. -# -# Schema: https://trivy.dev/latest/docs/configuration/filtering/ -# IDs use the hyphenated form Trivy displays (KSV-0041, not KSV0041). -misconfigurations: - - id: KSV-0041 - paths: - - "argocd/manifests/external-secrets/rbac.yaml" - statement: >- - external-secrets-operator's entire function is to read and - synthesize Secret objects; ClusterRole over secrets is its - purpose. Both the controller and cert-controller are - upstream-defined. - - id: KSV-0041 - paths: - - "argocd/manifests/kube-state-metrics/rbac.yaml" - - "argocd/manifests/kube-state-metrics-ringtail/rbac.yaml" - statement: >- - KSM exposes only Secret metadata (name, namespace, type, labels), - never the data field. list/watch on secrets is required for - kube_secret_info / kube_secret_labels metrics. - - id: KSV-0114 - paths: - - "argocd/manifests/external-secrets/rbac.yaml" - statement: >- - cert-controller manages the external-secrets validating webhook - configurations to inject its own rotating CA bundle. RBAC is - scoped to two named webhooks (secretstore-validate, - externalsecret-validate) via resourceNames; KSV-0114 doesn't see - the resourceNames restriction so reports the full ClusterRole. diff --git a/argocd/manifests/shower/configmap.yaml b/argocd/manifests/shower/configmap.yaml deleted file mode 100644 index 6102c1e..0000000 --- a/argocd/manifests/shower/configmap.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: shower-app-config - namespace: shower -data: - DJANGO_DEBUG: "0" - # The app's settings.py hardcodes ALLOWED_HOSTS = ["shower.eblu.me", - # "localhost", "127.0.0.1"] and exposes this env var as a comma-separated - # extras list. shower.ops.eblu.me is what Caddy on indri and the - # Tailscale ProxyGroup both send as the Host header, so the app needs to - # accept it. - DJANGO_ALLOWED_HOSTS: "shower.ops.eblu.me" - # /host/, /admin/, and Django's login surface are all tailnet-only — the - # public proxy 403s everything outside of `/` and `/prizes//`. - # /host/'s "Django admin" link follows DJANGO_ADMIN_URL. - DJANGO_ADMIN_URL: "https://shower.ops.eblu.me/admin/" - # /host/ is served on shower.ops.eblu.me (tailnet), but the QR codes it - # generates need to point at the public WAN hostname so guest phones can - # reach them. PUBLIC_URL_BASE overrides Django's request.build_absolute_uri() - # in the QR views — see shower/views.py:_public_url. Added in app v1.0.1. - DJANGO_PUBLIC_URL_BASE: "https://shower.eblu.me" diff --git a/argocd/manifests/shower/deployment.yaml b/argocd/manifests/shower/deployment.yaml deleted file mode 100644 index 70547aa..0000000 --- a/argocd/manifests/shower/deployment.yaml +++ /dev/null @@ -1,81 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: shower - namespace: shower -spec: - replicas: 1 - # SQLite + RWO data PVC: only one writer at a time. Recreate ensures the - # old pod's lock on the local-path volume is released before the new one - # mounts it. - strategy: - type: Recreate - selector: - matchLabels: - app: shower - template: - metadata: - labels: - app: shower - spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - containers: - - name: shower - image: registry.ops.eblu.me/blumeops/shower:kustomized - securityContext: - runAsNonRoot: true - allowPrivilegeEscalation: false - ports: - - containerPort: 8000 - name: http - envFrom: - - configMapRef: - name: shower-app-config - - secretRef: - name: shower-app-secrets - volumeMounts: - - name: media - mountPath: /app/media - - name: data - mountPath: /app/data - resources: - requests: - memory: "128Mi" - cpu: "50m" - limits: - memory: "512Mi" - cpu: "500m" - livenessProbe: - httpGet: - path: / - port: 8000 - httpHeaders: - - name: Host - value: shower.ops.eblu.me - - name: X-Forwarded-Proto - value: https - initialDelaySeconds: 30 - periodSeconds: 30 - readinessProbe: - httpGet: - path: / - port: 8000 - httpHeaders: - - name: Host - value: shower.ops.eblu.me - - name: X-Forwarded-Proto - value: https - initialDelaySeconds: 10 - periodSeconds: 10 - volumes: - - name: media - persistentVolumeClaim: - claimName: shower-media - - name: data - persistentVolumeClaim: - claimName: shower-data diff --git a/argocd/manifests/shower/external-secret.yaml b/argocd/manifests/shower/external-secret.yaml deleted file mode 100644 index 005a7e9..0000000 --- a/argocd/manifests/shower/external-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ ---- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: shower-app-secrets - namespace: shower -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: shower-app-secrets - creationPolicy: Owner - data: - - secretKey: DJANGO_SECRET_KEY - remoteRef: - key: "Shower (blumeops)" - property: secret-key diff --git a/argocd/manifests/shower/ingress-tailscale.yaml b/argocd/manifests/shower/ingress-tailscale.yaml deleted file mode 100644 index d09a696..0000000 --- a/argocd/manifests/shower/ingress-tailscale.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Tailscale Ingress for shower app. -# Exposes at shower.tail8d86e.ts.net. -# Caddy on indri proxies shower.ops.eblu.me here. The fly proxy then proxies -# shower.eblu.me through Caddy to this same endpoint (fly does not contact -# the k8s service directly — all traffic routes through indri's Caddy). -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: shower-tailscale - namespace: shower - annotations: - tailscale.com/proxy-class: "default" - tailscale.com/proxy-group: "ingress" - gethomepage.dev/enabled: "true" - gethomepage.dev/name: "Shower" - gethomepage.dev/group: "Home" - gethomepage.dev/icon: "mdi-baby" - gethomepage.dev/description: "Adelaide baby shower" - gethomepage.dev/href: "https://shower.ops.eblu.me" - gethomepage.dev/pod-selector: "app=shower" -spec: - ingressClassName: tailscale - defaultBackend: - service: - name: shower - port: - number: 8000 - tls: - - hosts: - - shower diff --git a/argocd/manifests/shower/kustomization.yaml b/argocd/manifests/shower/kustomization.yaml deleted file mode 100644 index 1c29224..0000000 --- a/argocd/manifests/shower/kustomization.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: shower - -resources: - - configmap.yaml - - external-secret.yaml - - pv-nfs.yaml - - pvc.yaml - - service.yaml - - ingress-tailscale.yaml - - deployment.yaml - -images: - - name: registry.ops.eblu.me/blumeops/shower - newTag: v1.1.3-3645098-nix diff --git a/argocd/manifests/shower/pv-nfs.yaml b/argocd/manifests/shower/pv-nfs.yaml deleted file mode 100644 index 7354fb5..0000000 --- a/argocd/manifests/shower/pv-nfs.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# NFS PersistentVolume for shower app media uploads (prize photos). -# -# Requires the `shower` share on sifaka with NFS exports matching the -# blumeops standard (192.168.1.0/24 + 100.64.0.0/10, all_squash → admin). -# See docs/how-to/operations/shower-app.md for the Synology web-UI walk -# and docs/reference/storage/sifaka.md for the exports table. -# -# Because all_squash rewrites every NFS write to admin:users (1024:100), -# the in-pod runAsUser does NOT have to match an on-disk uid. Mode 0777 -# on /volume1/shower lets the pod read back what it wrote. -apiVersion: v1 -kind: PersistentVolume -metadata: - name: shower-media-nfs-pv -spec: - capacity: - storage: 10Gi - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - storageClassName: "" - nfs: - server: sifaka - path: /volume1/shower diff --git a/argocd/manifests/shower/pvc.yaml b/argocd/manifests/shower/pvc.yaml deleted file mode 100644 index 47fee54..0000000 --- a/argocd/manifests/shower/pvc.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Media PVC — RWX NFS share for /app/media (prize photo uploads). -# SQLite DB lives in a separate local-path PVC; NFS file locking is not -# reliable enough for SQLite's WAL/journal. -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: shower-media - namespace: shower -spec: - accessModes: - - ReadWriteMany - storageClassName: "" - volumeName: shower-media-nfs-pv - resources: - requests: - storage: 10Gi ---- -# Database PVC — k3s local-path (default storage class) for SQLite. -# RWO is fine: the deployment runs with a single replica. -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: shower-data - namespace: shower -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 2Gi diff --git a/argocd/manifests/tailscale-operator-base/kustomization.yaml b/argocd/manifests/tailscale-operator-base/kustomization.yaml index 9d117ef..4519af6 100644 --- a/argocd/manifests/tailscale-operator-base/kustomization.yaml +++ b/argocd/manifests/tailscale-operator-base/kustomization.yaml @@ -6,11 +6,8 @@ namespace: tailscale # Upstream Tailscale operator manifest from forge mirror. # To upgrade: update the ref in the URL AND the newTag below. -# Must use the tailnet host forge.ops.eblu.me — the public forge.eblu.me -# black-holes /mirrors/ at the Fly edge (AI-scraper mitigation), which the -# in-cluster ArgoCD repo-server would otherwise hit and fail with a 403. resources: - - https://forge.ops.eblu.me/mirrors/tailscale/raw/tag/v1.94.2/cmd/k8s-operator/deploy/manifests/operator.yaml + - https://forge.eblu.me/mirrors/tailscale/raw/tag/v1.94.2/cmd/k8s-operator/deploy/manifests/operator.yaml - proxyclass.yaml - dnsconfig.yaml diff --git a/argocd/manifests/tailscale-operator-base/proxyclass.yaml b/argocd/manifests/tailscale-operator-base/proxyclass.yaml index 9fb46d6..a5c4675 100644 --- a/argocd/manifests/tailscale-operator-base/proxyclass.yaml +++ b/argocd/manifests/tailscale-operator-base/proxyclass.yaml @@ -21,9 +21,5 @@ spec: pod: tailscaleContainer: image: docker.io/tailscale/tailscale:v1.94.2 - resources: - requests: - cpu: 100m - memory: 128Mi tailscaleInitContainer: image: docker.io/tailscale/tailscale:v1.94.2 diff --git a/argocd/manifests/tailscale-operator-ringtail/kustomization.yaml b/argocd/manifests/tailscale-operator-ringtail/kustomization.yaml index 2d9ceb2..a14ca81 100644 --- a/argocd/manifests/tailscale-operator-ringtail/kustomization.yaml +++ b/argocd/manifests/tailscale-operator-ringtail/kustomization.yaml @@ -8,17 +8,3 @@ resources: - ../tailscale-operator-base - proxygroup-ingress.yaml - external-secret.yaml - -# Rewrite the proxyclass image to our local nix-built mirror. -# Scoped to ringtail only; indri's tailscale-operator/kustomization.yaml still -# pulls from upstream docker.io. A strategic merge patch is used instead of -# kustomize's `images:` directive because that directive only rewrites images -# in standard k8s container fields, not custom-resource fields like -# ProxyClass.spec.statefulSet.pod.tailscaleContainer.image. -patches: - - path: proxyclass-image.yaml - target: - group: tailscale.com - version: v1alpha1 - kind: ProxyClass - name: default diff --git a/argocd/manifests/tailscale-operator-ringtail/proxyclass-image.yaml b/argocd/manifests/tailscale-operator-ringtail/proxyclass-image.yaml deleted file mode 100644 index d1bf2a4..0000000 --- a/argocd/manifests/tailscale-operator-ringtail/proxyclass-image.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: tailscale.com/v1alpha1 -kind: ProxyClass -metadata: - name: default -spec: - statefulSet: - pod: - tailscaleContainer: - image: registry.ops.eblu.me/blumeops/tailscale:v1.94.2-0108b68-nix - tailscaleInitContainer: - image: registry.ops.eblu.me/blumeops/tailscale:v1.94.2-0108b68-nix diff --git a/argocd/manifests/tempo/kustomization.yaml b/argocd/manifests/tempo/kustomization.yaml index 1ccbdc8..68a209c 100644 --- a/argocd/manifests/tempo/kustomization.yaml +++ b/argocd/manifests/tempo/kustomization.yaml @@ -11,8 +11,7 @@ resources: images: - name: grafana/tempo - newName: registry.ops.eblu.me/blumeops/tempo - newTag: "v2.10.3-75f9ba4" + newTag: "2.10.1" configMapGenerator: - name: tempo-config diff --git a/argocd/manifests/teslamate/README.md b/argocd/manifests/teslamate/README.md new file mode 100644 index 0000000..7e1f9fc --- /dev/null +++ b/argocd/manifests/teslamate/README.md @@ -0,0 +1,69 @@ +# TeslaMate + +TeslaMate is a self-hosted Tesla data logger that collects and visualizes vehicle data. + +## Prerequisites + +### 1. Create 1Password Secrets + +Create two items in the blumeops 1Password vault: + +1. **TeslaMate DB Password** + - Generate a secure password for the teslamate PostgreSQL user + - Add a field named `password` with the generated value + +2. **TeslaMate Encryption Key** + - Generate with: `openssl rand -base64 32` + - Add a field named `key` with the generated value + - This encrypts Tesla API tokens at rest in the database + +### 2. Apply Kubernetes Secrets + +```bash +# Create namespace +kubectl create namespace teslamate + +# Apply database user secret (for CNPG) +op inject -i argocd/manifests/databases/secret-teslamate.yaml.tpl | kubectl apply -f - + +# Apply teslamate secrets +op inject -i argocd/manifests/teslamate/secret-encryption-key.yaml.tpl | kubectl apply -f - +op inject -i argocd/manifests/teslamate/secret-db.yaml.tpl | kubectl apply -f - +``` + +### 3. Create Database + +After the teslamate user exists in PostgreSQL (sync blumeops-pg first): + +```bash +PGPASSWORD=$(op read "op://blumeops/postgres/password") \ + psql -h pg.ops.eblu.me -U eblume -c "CREATE DATABASE teslamate OWNER teslamate;" +``` + +## Deployment + +```bash +# Sync ArgoCD apps +argocd app sync apps +argocd app sync blumeops-pg teslamate grafana grafana-config +``` + +## Tesla API Setup + +1. Access TeslaMate UI at https://tesla.tail8d86e.ts.net +2. Click "Sign in with Tesla" +3. Complete OAuth flow in browser +4. Tokens are encrypted and stored in database +5. Verify vehicle appears and data collection starts + +## Grafana Dashboards + +TeslaMate dashboards are available in Grafana at https://grafana.tail8d86e.ts.net + +They use the "TeslaMate" PostgreSQL datasource (not Prometheus). + +## Notes + +- MQTT is disabled (can be enabled later for Home Assistant integration) +- Timezone is set to America/Los_Angeles +- Encryption key protects Tesla API tokens at rest diff --git a/argocd/manifests/teslamate-ringtail/deployment.yaml b/argocd/manifests/teslamate/deployment.yaml similarity index 81% rename from argocd/manifests/teslamate-ringtail/deployment.yaml rename to argocd/manifests/teslamate/deployment.yaml index cf8cc73..42859a7 100644 --- a/argocd/manifests/teslamate-ringtail/deployment.yaml +++ b/argocd/manifests/teslamate/deployment.yaml @@ -1,10 +1,3 @@ -# TeslaMate on ringtail k3s — Nix image. -# -# The Nix image's Entrypoint waits for postgres, runs migrations -# (TeslaMate.Release.migrate), then starts the release — so no command -# override is needed. Stateless; all data lives in the teslamate database -# on the ringtail blumeops-pg (DATABASE_HOST already an in-cluster name, -# unchanged from minikube). See [[migrate-wave1-ringtail]]. apiVersion: apps/v1 kind: Deployment metadata: diff --git a/argocd/manifests/teslamate-ringtail/external-secret-db.yaml b/argocd/manifests/teslamate/external-secret-db.yaml similarity index 100% rename from argocd/manifests/teslamate-ringtail/external-secret-db.yaml rename to argocd/manifests/teslamate/external-secret-db.yaml diff --git a/argocd/manifests/teslamate-ringtail/external-secret-encryption-key.yaml b/argocd/manifests/teslamate/external-secret-encryption-key.yaml similarity index 100% rename from argocd/manifests/teslamate-ringtail/external-secret-encryption-key.yaml rename to argocd/manifests/teslamate/external-secret-encryption-key.yaml diff --git a/argocd/manifests/teslamate-ringtail/ingress-tailscale.yaml b/argocd/manifests/teslamate/ingress-tailscale.yaml similarity index 100% rename from argocd/manifests/teslamate-ringtail/ingress-tailscale.yaml rename to argocd/manifests/teslamate/ingress-tailscale.yaml diff --git a/argocd/manifests/teslamate-ringtail/kustomization.yaml b/argocd/manifests/teslamate/kustomization.yaml similarity index 90% rename from argocd/manifests/teslamate-ringtail/kustomization.yaml rename to argocd/manifests/teslamate/kustomization.yaml index acb623e..ac9e1ea 100644 --- a/argocd/manifests/teslamate-ringtail/kustomization.yaml +++ b/argocd/manifests/teslamate/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: registry.ops.eblu.me/blumeops/teslamate - newTag: v3.0.0-fcac8e5-nix + newTag: v3.0.0-613f05d diff --git a/argocd/manifests/teslamate-ringtail/service.yaml b/argocd/manifests/teslamate/service.yaml similarity index 100% rename from argocd/manifests/teslamate-ringtail/service.yaml rename to argocd/manifests/teslamate/service.yaml diff --git a/argocd/manifests/torrent/kustomization.yaml b/argocd/manifests/torrent/kustomization.yaml index 671687c..08e5414 100644 --- a/argocd/manifests/torrent/kustomization.yaml +++ b/argocd/manifests/torrent/kustomization.yaml @@ -10,6 +10,6 @@ resources: - ingress-tailscale.yaml images: - name: registry.ops.eblu.me/blumeops/transmission - newTag: v4.1.1-r1-2c483ce + newTag: v4.1.1-r1-613f05d - name: registry.ops.eblu.me/blumeops/transmission-exporter - newTag: v1.0.1-2c483ce + newTag: v1.0.1-613f05d diff --git a/argocd/manifests/unpoller/kustomization.yaml b/argocd/manifests/unpoller/kustomization.yaml index bf776bb..5b7a9e2 100644 --- a/argocd/manifests/unpoller/kustomization.yaml +++ b/argocd/manifests/unpoller/kustomization.yaml @@ -10,7 +10,7 @@ resources: images: - name: registry.ops.eblu.me/blumeops/unpoller - newTag: v3.2.0-4d1f4af + newTag: v2.34.0-613f05d configMapGenerator: - name: unpoller-config diff --git a/containers/alloy/Dockerfile b/containers/alloy/Dockerfile new file mode 100644 index 0000000..f2f30f6 --- /dev/null +++ b/containers/alloy/Dockerfile @@ -0,0 +1,68 @@ +# Grafana Alloy telemetry collector +# Three-stage build: Web UI (Node), server (Go), runtime (Alpine) + +ARG CONTAINER_APP_VERSION=1.14.0 +ARG ALLOY_VERSION=v${CONTAINER_APP_VERSION} +ARG ALLOY_COMMIT=626a738319812d58ebc25ca6d71651f4925b8b18 + +FROM node:22-alpine AS ui-build + +ARG ALLOY_COMMIT +RUN apk add --no-cache git + +RUN mkdir /app && cd /app \ + && git init \ + && git remote add origin https://forge.ops.eblu.me/mirrors/alloy.git \ + && git fetch --depth 1 origin ${ALLOY_COMMIT} \ + && git checkout FETCH_HEAD + +WORKDIR /app/internal/web/ui +RUN npm ci +RUN npx tsc -b && npx vite build + +FROM golang:1.25-alpine3.22 AS build + +ARG ALLOY_VERSION +ARG ALLOY_COMMIT +RUN apk add --no-cache build-base git + +RUN mkdir /app && cd /app \ + && git init \ + && git remote add origin https://forge.ops.eblu.me/mirrors/alloy.git \ + && git fetch --depth 1 origin ${ALLOY_COMMIT} \ + && git checkout FETCH_HEAD + +WORKDIR /app + +# Copy pre-built web UI assets +COPY --from=ui-build /app/internal/web/ui/dist /app/internal/web/ui/dist + +ENV CGO_ENABLED=1 + +# promtail_journal_enabled omitted: requires systemd headers (libsystemd-dev) +# and our k8s deployments read pod logs from the filesystem, not journald +RUN RELEASE_BUILD=1 VERSION=${ALLOY_VERSION} \ + GO_TAGS="netgo embedalloyui" \ + SKIP_UI_BUILD=1 \ + make alloy + +FROM alpine:3.22 + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Alloy" +LABEL org.opencontainers.image.description="Grafana Alloy is an OpenTelemetry Collector distribution" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +RUN apk --no-cache add ca-certificates tzdata \ + && addgroup -g 473 alloy \ + && adduser -D -u 473 -G alloy alloy \ + && mkdir -p /var/lib/alloy/data \ + && chown -R alloy:alloy /var/lib/alloy + +COPY --from=build --chown=473:473 /app/build/alloy /bin/alloy + +ENTRYPOINT ["/bin/alloy"] +ENV ALLOY_DEPLOY_MODE=docker +CMD ["run", "/etc/alloy/config.alloy", "--storage.path=/var/lib/alloy/data"] diff --git a/containers/alloy/container.py b/containers/alloy/container.py deleted file mode 100644 index 41d3995..0000000 --- a/containers/alloy/container.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Grafana Alloy — telemetry collector, native Dagger build. - -Three-stage build: Node (UI), Go (server via upstream Makefile with embedded -UI assets), Alpine (runtime). Source cloned from forge mirror. - -Notes: - - Builds via `make alloy` rather than plain `go build` so version stamping, - release flags, and the netgo+embedalloyui tags match upstream releases. - - promtail_journal_enabled is intentionally omitted: it requires - libsystemd-dev and our k8s deployments read pod logs from the filesystem, - not journald. - - Uses golang:alpine3.23 (currently Go 1.26.2 — matches alloy v1.16.0's - go.mod toolchain requirement and the go_build helper's image choice). -""" - -import dagger -from dagger import dag - -from blumeops.containers import ( - alpine_runtime, - clone_from_forge, - node_build, - oci_labels, -) - -VERSION = "v1.16.0" - - -async def build(src: dagger.Directory) -> dagger.Container: - source = clone_from_forge("alloy", VERSION) - - # Stage 1: Build the web UI (tsc + vite, not the package.json default). - ui = node_build( - source, - "internal/web/ui", - build_cmd=["sh", "-c", "npx tsc -b && npx vite build"], - ) - - # Stage 2: Build alloy via the upstream Makefile with embedded UI assets. - builder = ( - dag.container() - .from_("golang:alpine3.23") - .with_exec(["apk", "add", "--no-cache", "build-base", "git", "make"]) - .with_directory("/app", source) - .with_directory( - "/app/internal/web/ui/dist", - ui.directory("/app/internal/web/ui/dist"), - ) - .with_workdir("/app") - .with_env_variable("CGO_ENABLED", "1") - .with_env_variable("RELEASE_BUILD", "1") - .with_env_variable("VERSION", VERSION) - .with_env_variable("GO_TAGS", "netgo embedalloyui") - .with_env_variable("SKIP_UI_BUILD", "1") - .with_exec(["make", "alloy"]) - ) - - # Stage 3: Runtime as uid/gid 473 alloy. - runtime = alpine_runtime( - extra_apk=["ca-certificates", "tzdata"], - uid=473, - gid=473, - username="alloy", - ) - runtime = oci_labels( - runtime, - title="Alloy", - description="Grafana Alloy is an OpenTelemetry Collector distribution", - version=VERSION, - ) - return ( - runtime.with_file( - "/bin/alloy", - builder.file("/app/build/alloy"), - permissions=0o555, - ) - .with_exec( - [ - "sh", - "-c", - "mkdir -p /var/lib/alloy/data && chown -R alloy:alloy /var/lib/alloy", - ] - ) - .with_env_variable("ALLOY_DEPLOY_MODE", "docker") - .with_exposed_port(12345) - .with_user("alloy") - .with_entrypoint(["/bin/alloy"]) - .with_default_args( - args=[ - "run", - "/etc/alloy/config.alloy", - "--storage.path=/var/lib/alloy/data", - ] - ) - ) diff --git a/containers/alloy/default.nix b/containers/alloy/default.nix index c884704..e508a10 100644 --- a/containers/alloy/default.nix +++ b/containers/alloy/default.nix @@ -1,24 +1,24 @@ # Nix-built Grafana Alloy telemetry collector -# Builds v1.16.0 from forge mirror with embedded web UI +# Builds v1.14.0 from forge mirror with embedded web UI # Uses stdenv + make (not buildGoModule) due to multi-module workspace # with local replace directives (collector/ -> ../, ../syntax, ../extension) # Built with dockerTools.buildLayeredImage for efficient layer caching { pkgs ? import { } }: let - version = "1.16.0"; + version = "1.14.0"; src = pkgs.fetchgit { url = "https://forge.ops.eblu.me/mirrors/alloy.git"; rev = "v${version}"; - hash = "sha256-q5R2noxBZ3OPyZqmB+bx3iJKWFxC2WIprcgh9RwjLzk="; + hash = "sha256-gxNz4XDE8XSl6LsP3k8DERqDdMLcmbWKfXZGGyRULkg="; }; ui = pkgs.buildNpmPackage { inherit version; pname = "alloy-ui"; src = "${src}/internal/web/ui"; - npmDepsHash = "sha256-vResNUT4auDsK9ngnJYfMUUOYr/ikPhrvakqCjGq2Q8="; + npmDepsHash = "sha256-GT0yisPn+3FCtWL3he0i5zPMlaWNparQDefU69G4Yis="; buildPhase = '' runHook preBuild @@ -40,12 +40,11 @@ let pname = "alloy-go-modules"; inherit src version; - nativeBuildInputs = with pkgs; [ go_1_26 git cacert ]; + nativeBuildInputs = with pkgs; [ go git cacert ]; buildPhase = '' export GOPATH=$TMPDIR/go export GOFLAGS=-modcacherw - export GOTOOLCHAIN=local # Download modules for all three go.mod files go mod download cd syntax && go mod download && cd .. @@ -57,7 +56,7 @@ let ''; outputHashMode = "recursive"; - outputHash = "sha256-9/v85HyDInJB+9qHauKVuDol6Yf5mkXfMWgCr7RdRTk="; + outputHash = "sha256-rD7zqomSVv4d8NaC7jXXgihuQvK8guaAN0KrsBRWMVQ="; outputHashAlgo = "sha256"; }; @@ -66,7 +65,7 @@ let pname = "alloy"; nativeBuildInputs = with pkgs; [ - go_1_26 + go git gnumake cacert @@ -78,7 +77,6 @@ let export HOME=$TMPDIR export GOPATH=$TMPDIR/go export GOFLAGS=-modcacherw - export GOTOOLCHAIN=local # Populate module cache from pre-fetched modules mkdir -p $GOPATH/pkg diff --git a/containers/authentik/default.nix b/containers/authentik/default.nix index e65467a..5b965bd 100644 --- a/containers/authentik/default.nix +++ b/containers/authentik/default.nix @@ -12,7 +12,7 @@ let sources = import ./sources.nix { inherit pkgs; }; # Duplicated from sources.nix so build-container-nix.yaml can grep it - version = "2026.2.2"; + version = "2026.2.0"; webui = import ./webui.nix { inherit pkgs sources; }; authentik-django = import ./authentik-django.nix { inherit pkgs sources webui; }; authentik-server = import ./authentik-server.nix { inherit pkgs sources authentik-django webui; }; diff --git a/containers/authentik/python-deps.nix b/containers/authentik/python-deps.nix index 030bac4..17d557c 100644 --- a/containers/authentik/python-deps.nix +++ b/containers/authentik/python-deps.nix @@ -119,7 +119,7 @@ pkgs.stdenv.mkDerivation { outputHashMode = "recursive"; outputHashAlgo = "sha256"; - outputHash = "sha256-PMNooWKoEWy/G0G3BLAWEJTqvj3FJi34ibougjwdE+c="; + outputHash = "sha256-DtpcYQyI07m7v84D/UC28Tj35R9wye6IX+1D0gMZPgY="; dontFixup = true; } diff --git a/containers/authentik/sources.nix b/containers/authentik/sources.nix index 96aed12..9134fa8 100644 --- a/containers/authentik/sources.nix +++ b/containers/authentik/sources.nix @@ -1,9 +1,9 @@ -# Centralized version and source pinning for authentik 2026.2.2 +# Centralized version and source pinning for authentik 2026.2.0 # All sources fetched from forge mirrors for supply chain control { pkgs ? import { } }: let - version = "2026.2.2"; + version = "2026.2.0"; in { inherit version; @@ -12,14 +12,14 @@ in src = pkgs.fetchgit { url = "https://forge.ops.eblu.me/mirrors/authentik.git"; rev = "version/${version}"; - hash = "sha256-Xq7JGI/8ppIydIuWd9KRJKUrh7UpeniwvZ4NAtXbYJ4="; + hash = "sha256-pVQ34cZYX3hlk6hF1aZ/n32xMqTF4Jmp0G0VGDU7iXc="; }; # Go API client repo — provides config.yaml, go.mod, go.sum, templates client-go-src = pkgs.fetchgit { url = "https://forge.ops.eblu.me/mirrors/authentik-client-go.git"; - rev = "v3.2026.2.1"; - hash = "sha256-sFj+KAFHe3ajOFUtfBl9X3AVIvMCO8+Xba+/Jsy7Cgo="; + rev = "v3.${version}"; + hash = "sha256-DwXw/0QcSDYQKVhPA8tStrSoZooriQex/9FxSJtR/QY="; }; meta = with pkgs.lib; { diff --git a/containers/cv/Dockerfile b/containers/cv/Dockerfile new file mode 100644 index 0000000..9bfebe0 --- /dev/null +++ b/containers/cv/Dockerfile @@ -0,0 +1,30 @@ +# CV/Resume Static Site Server +# Downloads and serves a CV site tarball (HTML+CSS+PDF) via nginx +# +# Configuration (via environment): +# CV_RELEASE_URL - URL to download the CV content tarball +# +# The container downloads the tarball on startup, extracts it, and serves with nginx. + +ARG CONTAINER_APP_VERSION=1.0.3 + +FROM nginx:alpine + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="CV" +LABEL org.opencontainers.image.description="Static site server for CV/resume" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +# Install curl for downloading release assets +RUN apk add --no-cache curl + +# Copy startup script and nginx config +COPY start.sh /start.sh +COPY default.conf /etc/nginx/conf.d/default.conf +RUN chmod +x /start.sh + +EXPOSE 80 + +CMD ["/start.sh"] diff --git a/containers/cv/default.conf b/containers/cv/default.conf new file mode 100644 index 0000000..7c89b08 --- /dev/null +++ b/containers/cv/default.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; + + # Cache static assets + location ~* \.(css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Force PDF download + location = /resume.pdf { + add_header Content-Disposition 'attachment; filename="erich-blume-resume.pdf"'; + } + + # Serve files directly + location / { + try_files $uri $uri/ =404; + } + + # Health check endpoint + location /healthz { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } +} diff --git a/containers/cv/start.sh b/containers/cv/start.sh new file mode 100644 index 0000000..bb81c20 --- /dev/null +++ b/containers/cv/start.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +HTML_DIR="/usr/share/nginx/html" + +# Check for required environment variable +if [ -z "$CV_RELEASE_URL" ]; then + echo "Error: CV_RELEASE_URL environment variable is required" + echo "Set it to the URL of the CV content tarball to serve" + exit 1 +fi + +echo "Downloading CV content from: $CV_RELEASE_URL" + +# Download the tarball +if ! curl -fsSL "$CV_RELEASE_URL" -o /tmp/cv.tar.gz; then + echo "Error: Failed to download CV content from $CV_RELEASE_URL" + exit 1 +fi + +# Clear existing content and extract +rm -rf "${HTML_DIR:?}"/* +echo "Extracting CV content to $HTML_DIR" +tar -xzf /tmp/cv.tar.gz -C "$HTML_DIR" +rm /tmp/cv.tar.gz + +echo "CV content extracted successfully" +echo "Starting nginx..." + +# Start nginx in foreground +exec nginx -g "daemon off;" diff --git a/containers/devpi/Dockerfile b/containers/devpi/Dockerfile new file mode 100644 index 0000000..69e14c3 --- /dev/null +++ b/containers/devpi/Dockerfile @@ -0,0 +1,33 @@ +ARG CONTAINER_APP_VERSION=6.19.1 + +FROM python:3.12-slim + +ARG CONTAINER_APP_VERSION +ARG DEVPI_SERVER_VERSION=${CONTAINER_APP_VERSION} + +LABEL org.opencontainers.image.title="devpi" +LABEL org.opencontainers.image.description="devpi PyPI server and caching proxy" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" +ARG DEVPI_WEB_VERSION=5.0.1 + +# Install devpi-server and devpi-web +RUN pip install --no-cache-dir \ + devpi-server==${DEVPI_SERVER_VERSION} \ + devpi-web==${DEVPI_WEB_VERSION} + +# Create non-root user +RUN useradd -r -u 1000 devpi && mkdir -p /devpi && chown devpi:devpi /devpi + +# Add startup script +COPY --chown=devpi:devpi start.sh /usr/local/bin/start.sh +RUN chmod +x /usr/local/bin/start.sh + +USER devpi +WORKDIR /devpi + +# Expose default port +EXPOSE 3141 + +ENTRYPOINT ["/usr/local/bin/start.sh"] diff --git a/containers/devpi/start.sh b/containers/devpi/start.sh new file mode 100644 index 0000000..8ed46a2 --- /dev/null +++ b/containers/devpi/start.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +SERVERDIR="${DEVPI_SERVERDIR:-/devpi}" +HOST="${DEVPI_HOST:-0.0.0.0}" +# Note: Can't use DEVPI_PORT - Kubernetes auto-sets it for service discovery +PORT="${DEVPI_LISTEN_PORT:-3141}" +OUTSIDE_URL="${DEVPI_OUTSIDE_URL:-}" + +# Check if devpi is initialized +if [ ! -f "$SERVERDIR/.serverversion" ]; then + echo "Initializing devpi server..." + + if [ -z "$DEVPI_ROOT_PASSWORD" ]; then + echo "ERROR: DEVPI_ROOT_PASSWORD environment variable must be set for initialization" + exit 1 + fi + + devpi-init --serverdir "$SERVERDIR" --root-passwd "$DEVPI_ROOT_PASSWORD" + echo "Devpi initialized successfully" +fi + +# Build command +CMD=(devpi-server --serverdir "$SERVERDIR" --host "$HOST" --port "$PORT") + +if [ -n "$OUTSIDE_URL" ]; then + CMD+=(--outside-url "$OUTSIDE_URL") +fi + +echo "Starting devpi-server..." +exec "${CMD[@]}" diff --git a/containers/external-secrets/container.py b/containers/external-secrets/container.py deleted file mode 100644 index 6be5765..0000000 --- a/containers/external-secrets/container.py +++ /dev/null @@ -1,51 +0,0 @@ -"""External Secrets Operator — native Dagger build. - -Two-stage build: Go binary (all providers), Alpine runtime. -Source cloned from forge mirror. - -A single binary serves as the controller, webhook, and cert-controller; the -Deployments select the role via a subcommand passed in `args:`, so the image -ENTRYPOINT must be the binary itself (matching upstream's distroless image). -""" - -import dagger - -from blumeops.containers import ( - alpine_runtime, - clone_from_forge, - go_build, - oci_labels, -) - -VERSION = "v2.2.0" - - -async def build(src: dagger.Directory) -> dagger.Container: - source = clone_from_forge("external-secrets", VERSION) - - # Upstream `make build` compiles every secret provider into a single - # static binary (`-tags all_providers`, CGO disabled). Mirror that so the - # local image is functionally identical to ghcr.io/.../external-secrets. - backend = go_build( - source, - "/external-secrets", - tags="all_providers", - ) - - runtime = alpine_runtime( - extra_apk=["ca-certificates"], - create_user=False, - ) - runtime = oci_labels( - runtime, - title="External Secrets Operator", - description=( - "Kubernetes operator that integrates external secret management systems" - ), - version=VERSION, - ) - return ( - runtime.with_file("/bin/external-secrets", backend.file("/external-secrets")) - .with_user("65534") - .with_entrypoint(["/bin/external-secrets"]) - ) diff --git a/containers/external-secrets/default.nix b/containers/external-secrets/default.nix deleted file mode 100644 index eabe03d..0000000 --- a/containers/external-secrets/default.nix +++ /dev/null @@ -1,56 +0,0 @@ -# Nix-built External Secrets Operator (amd64, for ringtail k3s). -# Builds v2.2.0 from the forge mirror with all secret providers compiled in, -# faithful to upstream's `make build` (-tags all_providers). The container.py -# sibling builds the arm64 image for indri's minikube; this default.nix builds -# the amd64 image on ringtail's nix-container-builder. -{ pkgs ? import { } }: - -let - version = "2.2.0"; - - src = pkgs.fetchgit { - url = "https://forge.ops.eblu.me/mirrors/external-secrets.git"; - rev = "v${version}"; - hash = "sha256-eAocOAp5s4CFRrpKfQr2lf3Ji+6nQQ1A5/eTw5B7v9U="; - }; - - # external-secrets v2.2.0 requires Go >= 1.26.1; nixpkgs default go is 1.25.x. - external-secrets = (pkgs.buildGoModule.override { go = pkgs.go_1_26; }) { - inherit src version; - pname = "external-secrets"; - vendorHash = "sha256-0xuBK3fjAplPLAElHvKB6d+2lDz+De/s91fV4dPZwjE="; - - doCheck = false; - - subPackages = [ "." ]; - - tags = [ "all_providers" ]; - - ldflags = [ "-s" "-w" ]; - - meta = with pkgs.lib; { - description = "Kubernetes operator that integrates external secret management systems"; - homepage = "https://github.com/external-secrets/external-secrets"; - license = licenses.asl20; - mainProgram = "external-secrets"; - }; - }; -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/external-secrets"; - contents = [ - external-secrets - pkgs.cacert - pkgs.tzdata - ]; - - config = { - Entrypoint = [ "${external-secrets}/bin/external-secrets" ]; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - ]; - User = "65534"; - }; -} diff --git a/containers/forgejo-runner/container.py b/containers/forgejo-runner/container.py deleted file mode 100644 index dfb2edf..0000000 --- a/containers/forgejo-runner/container.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Forgejo Runner — native Dagger build. - -Two-stage build: Go (static binary with CGO for SQLite), Alpine (runtime). -Source cloned from forge mirror. -""" - -import dagger - -from blumeops.containers import ( - alpine_runtime, - clone_from_forge, - go_build, - oci_labels, -) - -VERSION = "12.8.2" - - -async def build(src: dagger.Directory) -> dagger.Container: - source = clone_from_forge("forgejo-runner", f"v{VERSION}") - - # Stage 1: Build Go binary (static, CGO enabled for SQLite) - backend = go_build( - source, - "/forgejo-runner", - tags="netgo osusergo", - ldflags=( - '-extldflags "-static" -s -w' - f' -X "code.forgejo.org/forgejo/runner/v12/internal/pkg/ver.version=v{VERSION}"' - ), - cgo_enabled=True, - extra_env={"CGO_CFLAGS": "-DSQLITE_MAX_VARIABLE_NUMBER=32766"}, - ) - - # Stage 2: Runtime - runtime = alpine_runtime( - extra_apk=["git", "bash", "ca-certificates", "gettext-envsubst"], - uid=1000, - gid=1000, - username="runner", - ) - runtime = oci_labels( - runtime, - title="Forgejo Runner", - description="A runner for Forgejo Actions", - version=VERSION, - ) - return ( - runtime.with_file("/bin/forgejo-runner", backend.file("/forgejo-runner")) - .with_env_variable("HOME", "/data") - .with_user("1000:1000") - .with_workdir("/data") - .with_default_args(args=["/bin/forgejo-runner"]) - ) diff --git a/containers/frigate-notify/default.nix b/containers/frigate-notify/default.nix deleted file mode 100644 index 701b194..0000000 --- a/containers/frigate-notify/default.nix +++ /dev/null @@ -1,66 +0,0 @@ -# Nix-built frigate-notify — polls Frigate webapi and pushes alerts to ntfy. -{ pkgs ? import { } }: - -let - version = "0.5.4"; - - src = pkgs.fetchgit { - url = "https://forge.ops.eblu.me/mirrors/frigate-notify.git"; - rev = "v${version}"; - hash = "sha256-c/QOSQNNJ+ElMDm45lBOsru/ujBhCWethiRefj3hBOk="; - }; - - frigate-notify = pkgs.buildGoModule { - inherit src version; - pname = "frigate-notify"; - - vendorHash = "sha256-Ho9oaK01wJDPf3ufV2klV1dG4qFNVNJkWmWvEgAy10s="; - - doCheck = false; - subPackages = [ "." ]; - - # `goolm` swaps the matrix crypto backend from libolm (CGO) to pure-Go olm, - # avoiding the libolm.h dependency. Our deployment doesn't use matrix, but - # the package is imported unconditionally. - tags = [ "goolm" ]; - - ldflags = [ "-s" "-w" ]; - - meta = with pkgs.lib; { - description = "Bridge between Frigate NVR events and notification services"; - homepage = "https://github.com/0x2142/frigate-notify"; - license = licenses.mit; - mainProgram = "frigate-notify"; - }; - }; -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/frigate-notify"; - contents = [ - frigate-notify - pkgs.cacert - pkgs.tzdata - ]; - - # Upstream Dockerfile expects WORKDIR=/app (config at ./config.yml, logfile at - # ./log/app.log via lumberjack). Create /app world-writable so nonroot can - # write logs; the config is mounted in from a ConfigMap. - extraCommands = '' - mkdir -p app - chmod 1777 app - ''; - - config = { - Entrypoint = [ "${frigate-notify}/bin/frigate-notify" ]; - WorkingDir = "/app"; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - ]; - ExposedPorts = { - "8000/tcp" = { }; - }; - User = "65534"; - }; -} diff --git a/containers/grafana-sidecar/Dockerfile b/containers/grafana-sidecar/Dockerfile new file mode 100644 index 0000000..28dd983 --- /dev/null +++ b/containers/grafana-sidecar/Dockerfile @@ -0,0 +1,36 @@ +# Grafana dashboard sidecar - watches ConfigMaps and syncs into Grafana +# Two-stage build: Python venv (builder), runtime (Alpine) + +ARG CONTAINER_APP_VERSION=1.28.0 + +FROM python:3.12-alpine3.22 AS base + +FROM base AS builder +ARG CONTAINER_APP_VERSION +WORKDIR /app +RUN apk add --no-cache git gcc musl-dev +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/kiwigrid-grafana-sidecar.git /tmp/k8s-sidecar +RUN python -m venv .venv && \ + .venv/bin/pip install --no-cache-dir -U pip setuptools && \ + .venv/bin/pip install --no-cache-dir -r /tmp/k8s-sidecar/src/requirements.txt && \ + cp /tmp/k8s-sidecar/src/*.py /app/ && \ + find /app/.venv \( -type d -a -name test -o -name tests \) \ + -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) -exec rm -rf '{}' \+ + +FROM base + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Grafana Sidecar" +LABEL org.opencontainers.image.description="K8s sidecar to sync ConfigMap dashboards into Grafana" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +ENV PYTHONUNBUFFERED=1 +WORKDIR /app +COPY --from=builder /app /app +ENV PATH="/app/.venv/bin:$PATH" + +USER 65534:65534 +CMD ["python", "-u", "/app/sidecar.py"] diff --git a/containers/grafana-sidecar/container.py b/containers/grafana-sidecar/container.py deleted file mode 100644 index 83950a7..0000000 --- a/containers/grafana-sidecar/container.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Grafana dashboard sidecar — native Dagger build. - -Two-stage build: Python venv (builder), Python Alpine (runtime). -Source cloned from forge mirror. -""" - -import dagger -from dagger import dag - -from blumeops.containers import clone_from_forge, oci_labels - -VERSION = "2.6.0" - -PYTHON_BASE = "python:3.14-alpine3.23" - - -async def build(src: dagger.Directory) -> dagger.Container: - source = clone_from_forge("kiwigrid-grafana-sidecar", VERSION) - - # Stage 1: Build Python venv with dependencies - builder = ( - dag.container() - .from_(PYTHON_BASE) - .with_exec(["apk", "add", "--no-cache", "gcc", "musl-dev"]) - .with_workdir("/app") - .with_exec( - ["python", "-m", "venv", ".venv"], - ) - .with_exec( - [".venv/bin/pip", "install", "--no-cache-dir", "-U", "pip", "setuptools"], - ) - .with_file("/app/pyproject.toml", source.file("pyproject.toml")) - .with_directory("/app/src", source.directory("src")) - .with_exec([".venv/bin/pip", "install", "--no-cache-dir", "."]) - # Strip test dirs and bytecode from venv to shrink the image - .with_exec( - [ - "sh", - "-c", - "find /app/.venv" - " \\( -type d -a -name test -o -name tests \\)" - " -o \\( -type f -a -name '*.pyc' -o -name '*.pyo' \\)" - " -exec rm -rf {} +", - ] - ) - ) - - # Stage 2: Runtime - runtime = dag.container().from_(PYTHON_BASE) - runtime = oci_labels( - runtime, - title="Grafana Sidecar", - description="K8s sidecar to sync ConfigMap dashboards into Grafana", - version=VERSION, - ) - return ( - runtime.with_env_variable("PYTHONUNBUFFERED", "1") - .with_workdir("/app") - .with_directory("/app/.venv", builder.directory("/app/.venv")) - .with_env_variable("PATH", "/app/.venv/bin:$PATH") - .with_user("65534:65534") - .with_default_args(args=["python", "-u", "-m", "sidecar"]) - ) diff --git a/containers/grafana/Dockerfile b/containers/grafana/Dockerfile index 3b33dd9..3d5b12b 100644 --- a/containers/grafana/Dockerfile +++ b/containers/grafana/Dockerfile @@ -1,4 +1,4 @@ -ARG CONTAINER_APP_VERSION=12.4.2 +ARG CONTAINER_APP_VERSION=12.3.3 FROM alpine:3.22 diff --git a/containers/homepage/Dockerfile b/containers/homepage/Dockerfile new file mode 100644 index 0000000..6e53e1c --- /dev/null +++ b/containers/homepage/Dockerfile @@ -0,0 +1,47 @@ +# Homepage - self-hosted services dashboard +# Two-stage build: Node.js build, Alpine runtime + +ARG CONTAINER_APP_VERSION=v1.11.0 +ARG HOMEPAGE_VERSION=${CONTAINER_APP_VERSION} + +FROM node:24-slim AS builder + +ARG HOMEPAGE_VERSION +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 --branch ${HOMEPAGE_VERSION} \ + https://forge.ops.eblu.me/mirrors/homepage.git /app + +WORKDIR /app +RUN mkdir -p config \ + && corepack enable && corepack prepare pnpm@latest --activate \ + && pnpm install --frozen-lockfile \ + && NEXT_TELEMETRY_DISABLED=1 pnpm run build + +FROM node:24-alpine + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Homepage" +LABEL org.opencontainers.image.description="A self-hosted services landing page" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +WORKDIR /app + +COPY --from=builder --chown=1000:1000 /app/public ./public +COPY --from=builder --chown=1000:1000 /app/.next/standalone/ ./ +COPY --from=builder --chown=1000:1000 /app/.next/static/ ./.next/static + +RUN mkdir -p /app/config && chown 1000:1000 /app/config + +ENV NODE_ENV=production +ENV PORT=3000 +EXPOSE 3000 + +HEALTHCHECK --interval=10s --timeout=3s --start-period=20s \ + CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/healthcheck || exit 1 + +USER 1000 +CMD ["node", "server.js"] diff --git a/containers/homepage/default.nix b/containers/homepage/default.nix deleted file mode 100644 index 6217847..0000000 --- a/containers/homepage/default.nix +++ /dev/null @@ -1,130 +0,0 @@ -# Nix-built gethomepage/homepage dashboard -# Builds v1.11.0 from forge mirror. -# -# Adapted from nixpkgs pkgs/by-name/ho/homepage-dashboard (commit master), -# changed to fetch from our forge mirror and wrap with dockerTools for an -# amd64 image runnable on ringtail's k3s. -# -# The preBuild substitutions are not optional — without them Next.js writes -# its file-system-cache to a read-only path and prerender state breaks after -# restart (nixpkgs issues #328621 and #458494). -{ pkgs ? import { } }: - -let - version = "1.11.0"; - - homepage = pkgs.stdenv.mkDerivation (finalAttrs: { - pname = "homepage-dashboard"; - inherit version; - - src = pkgs.fetchgit { - url = "https://forge.ops.eblu.me/mirrors/homepage.git"; - rev = "v${version}"; - hash = "sha256-jnv9PnClm/jIQ4uU6c4A1UiAmwoihG0l6k3fUbD47I4="; - }; - - pnpmDeps = pkgs.fetchPnpmDeps { - inherit (finalAttrs) pname version src; - pnpm = pkgs.pnpm_10; - fetcherVersion = 3; - hash = "sha256-X5j9XppbcasGuC7fUsj4XzbaQFM9WcRcXjgJHN/inR8="; - }; - - nativeBuildInputs = [ - pkgs.makeBinaryWrapper - pkgs.nodejs_24 - pkgs.pnpmConfigHook - pkgs.pnpm_10 - ]; - - buildInputs = [ - pkgs.nodePackages.node-gyp-build - ]; - - env.PYTHON = "${pkgs.python3}/bin/python"; - - preBuild = '' - substituteInPlace node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js \ - --replace-fail 'this.serverDistDir = ctx.serverDistDir;' \ - 'this.serverDistDir = require("path").join((process.env.NIXPKGS_HOMEPAGE_CACHE_DIR || "/tmp/homepage-cache"), "homepage");' - - for bundle in node_modules/next/dist/compiled/next-server/*.runtime.prod.js; do - substituteInPlace "$bundle" \ - --replace-fail 'this.serverDistDir=e.serverDistDir' \ - 'this.serverDistDir=(process.env.NIXPKGS_HOMEPAGE_CACHE_DIR||"/tmp/homepage-cache")+"/homepage"' - done - ''; - - buildPhase = '' - runHook preBuild - mkdir -p config - pnpm build - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - - mkdir -p $out/{bin,share} - cp -r .next/standalone $out/share/homepage/ - cp -r public $out/share/homepage/public - chmod +x $out/share/homepage/server.js - - mkdir -p $out/share/homepage/.next - cp -r .next/static $out/share/homepage/.next/static - - makeWrapper "${pkgs.lib.getExe pkgs.nodejs_24}" $out/bin/homepage \ - --set-default PORT 3000 \ - --set-default HOMEPAGE_CONFIG_DIR /app/config \ - --set-default NIXPKGS_HOMEPAGE_CACHE_DIR /tmp/homepage-cache \ - --add-flags "$out/share/homepage/server.js" \ - --prefix PATH : "${pkgs.lib.makeBinPath [ pkgs.unixtools.ping ]}" - - runHook postInstall - ''; - - doDist = false; - }); -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/homepage"; - contents = [ - homepage - pkgs.cacert - pkgs.tzdata - ]; - - extraCommands = '' - mkdir -p tmp - chmod 1777 tmp - ''; - - # /app/config must be writable by the runtime user (1000): homepage seeds - # missing skeleton configs (proxmox.yaml, etc.) and writes /app/config/logs. - # The deployment mounts ConfigMap files at /app/config/.yaml via - # subPath, which leaves the parent dir as image filesystem — so its - # ownership has to be set at build time. - fakeRootCommands = '' - mkdir -p app/config - chown -R 1000:1000 app - ''; - enableFakechroot = true; - - config = { - Entrypoint = [ "${homepage}/bin/homepage" ]; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - "TMPDIR=/tmp" - "NIXPKGS_HOMEPAGE_CACHE_DIR=/tmp/homepage-cache" - "HOMEPAGE_CONFIG_DIR=/app/config" - "NEXT_TELEMETRY_DISABLED=1" - "PORT=3000" - ]; - ExposedPorts = { - "3000/tcp" = { }; - }; - User = "1000"; - }; -} diff --git a/containers/kingfisher/Cargo.lock b/containers/kingfisher/Cargo.lock deleted file mode 100644 index 0612332..0000000 --- a/containers/kingfisher/Cargo.lock +++ /dev/null @@ -1,10002 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "anymap2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arc-swap" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" -dependencies = [ - "rustversion", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "asar" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9051a11bd40b01b4f8b926956b9f22ff5ef08fcbc7eb34693e2a73982ff85e" -dependencies = [ - "byteorder", - "clap", - "color-eyre", - "hex", - "is_executable", - "serde", - "serde_json", - "serde_with 3.18.0", - "sha2", - "thiserror 1.0.69", - "walkdir", - "wax", -] - -[[package]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "assert_cmd" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" -dependencies = [ - "anstyle", - "bstr", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-compression" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" -dependencies = [ - "compression-codecs", - "compression-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-config" -version = "1.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 1.4.0", - "sha1", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" -dependencies = [ - "aws-lc-sys", - "untrusted 0.7.1", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "bytes-utils", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-dynamodb" -version = "1.110.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c597424385739456cd04535982831c15bd58f97ca28c5bcb232c0d730d5cb39" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ec2" -version = "1.220.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d073e665c1303edc348511ec2509b58a2cee148196f72db33aef5cd47c3573" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-iam" -version = "1.107.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb72c86b8b7aeb3967a3a9cebcb773d88350c04b9d0770ad75bea8f76c89bfa" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-kms" -version = "1.104.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41ae6a33da941457e89075ef8ca5b4870c8009fe4dceeba82fce2f30f313ac6" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-lambda" -version = "1.119.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bcacc7c2d94698c49fb73086d16ccdff68442ed21a70fbaa924c46158e37a93" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.127.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "151783f64e0dcddeb4965d08e36c276b4400a46caa88805a2e36d497deaf031a" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "http-body 1.0.1", - "lru 0.16.3", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-secretsmanager" -version = "1.103.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae64963d3d16d8070aaa2fb79c11cd3b13f44d2f13bba3fe8f49dcd2c42f2987" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.97.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.99.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.101.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-observability", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "percent-encoding", - "sha2", - "time", - "tracing", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.64.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6750f3dd509b0694a4377f0293ed2f9630d710b1cebe281fa8bac8f099f88bc6" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "crc-fast", - "hex", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.63.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2", - "http 1.4.0", - "hyper", - "hyper-rustls", - "hyper-util", - "pin-project-lite", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.62.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.4.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "axum" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" -dependencies = [ - "axum-core", - "bytes", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base32" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake3" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures 0.2.17", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bloomfilter" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6d7f06817e48ea4e17532fa61bc4e8b9a101437f0623f69d2ea54284f3a817" -dependencies = [ - "getrandom 0.2.17", - "siphasher", -] - -[[package]] -name = "bollard-stubs" -version = "1.42.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" -dependencies = [ - "serde", - "serde_with 1.14.0", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bson" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" -dependencies = [ - "ahash", - "base64 0.22.1", - "bitvec", - "getrandom 0.2.17", - "getrandom 0.3.4", - "hex", - "indexmap 2.13.0", - "js-sys", - "once_cell", - "rand 0.9.2", - "serde", - "serde_bytes", - "serde_json", - "time", - "uuid", -] - -[[package]] -name = "bson" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3f109694c4f45353972af96bf97d8a057f82e2d6e496457f4d135b9867a518c" -dependencies = [ - "ahash", - "base64 0.22.1", - "bitvec", - "getrandom 0.3.4", - "hex", - "indexmap 2.13.0", - "js-sys", - "rand 0.9.2", - "serde", - "serde_bytes", - "simdutf8", - "thiserror 2.0.18", - "time", - "uuid", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "btoi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" -dependencies = [ - "num-traits", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "bytesize" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" - -[[package]] -name = "bzip2-rs" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beeb59e7e4c811ab37cc73680c798c7a5da77fc9989c62b09138e31ee740f735" -dependencies = [ - "crc32fast", - "tinyvec", -] - -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", -] - -[[package]] -name = "cc" -version = "1.2.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.0", -] - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "chrono-tz" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf 0.11.3", -] - -[[package]] -name = "chrono-tz-build" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" -dependencies = [ - "parse-zoneinfo", - "phf 0.11.3", - "phf_codegen", -] - -[[package]] -name = "clap" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap-cargo" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936551935c8258754bb8216aec040957d261f977303754b9bf1a213518388006" -dependencies = [ - "anstyle", - "clap", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim 0.11.1", - "terminal_size", - "unicase", - "unicode-width", -] - -[[package]] -name = "clap_derive" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "clru" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "color-backtrace" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308329d5d62e877ba02943db3a8e8c052de9fde7ab48283395ba0e6494efbabd" -dependencies = [ - "backtrace", - "termcolor", -] - -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compression-codecs" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" -dependencies = [ - "brotli", - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" - -[[package]] -name = "console" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" -dependencies = [ - "encode_unicode", - "libc", - "unicode-width", - "windows-sys 0.61.2", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "content_inspector" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" -dependencies = [ - "memchr", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" -dependencies = [ - "cookie", - "document-features", - "idna", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "time", - "url", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc-fast" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" -dependencies = [ - "crc", - "digest", - "rustversion", - "spin", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "cron" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" -dependencies = [ - "chrono", - "once_cell", - "winnow 0.6.26", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-skiplist" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core 0.13.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.12", - "serde", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "deadpool" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" -dependencies = [ - "deadpool-runtime", - "lazy_static", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" - -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "der_derive", - "flagset", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive-syn-parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive-where" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_builder" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" -dependencies = [ - "derive_builder_macro 0.12.0", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro 0.20.2", -] - -[[package]] -name = "derive_builder_core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_builder_macro" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" -dependencies = [ - "derive_builder_core 0.12.0", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core 0.20.2", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "signature", - "subtle", - "zeroize", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "encoding_rs_io" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" -dependencies = [ - "encoding_rs", -] - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "version_check", -] - -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "faster-hex" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" -dependencies = [ - "heapless", - "serde", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gcloud-auth" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b43924e3df02cb3b846ca66a7ee58e8c13eb2556d0308c71f6154083f6980365" -dependencies = [ - "async-trait", - "base64 0.22.1", - "gcloud-metadata", - "home", - "jsonwebtoken 10.3.0", - "reqwest 0.13.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "time", - "token-source", - "tokio", - "tracing", - "urlencoding", -] - -[[package]] -name = "gcloud-metadata" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3152612316be627be52fe9ca72331eb48425059b3a6a700e7adde223e061d5" -dependencies = [ - "reqwest 0.13.2", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "gcloud-storage" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17f9662a6966402de91daf0edb5accaae05c87f1a85479e57b95d2af7284b9f" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bytes", - "futures-util", - "gcloud-auth", - "gcloud-metadata", - "hex", - "once_cell", - "percent-encoding", - "pkcs8", - "regex", - "reqwest 0.13.2", - "reqwest-middleware 0.5.1", - "ring", - "serde", - "serde_json", - "sha2", - "thiserror 2.0.18", - "time", - "token-source", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "generic-array" -version = "0.14.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "getset" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "git2" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" -dependencies = [ - "bitflags 2.11.0", - "libc", - "libgit2-sys", - "log", - "url", -] - -[[package]] -name = "gitlab" -version = "0.1801.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba984dbd32d06cf8e360163134974cefb351f78a021e9a7e84ca749b3590f4" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "chrono", - "cron", - "derive_builder 0.20.2", - "futures-util", - "graphql_client", - "http 1.4.0", - "itertools 0.14.0", - "log", - "percent-encoding", - "reqwest 0.12.28", - "serde", - "serde_json", - "serde_urlencoded", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "gix" -version = "0.81.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0473c64d9ccbcfb9953a133b47c8b9a335b87ac6c52b983ee4b03d49000b0f3f" -dependencies = [ - "gix-actor", - "gix-archive", - "gix-attributes", - "gix-blame", - "gix-command", - "gix-commitgraph", - "gix-config", - "gix-credentials", - "gix-date", - "gix-diff", - "gix-dir", - "gix-discover", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-hashtable", - "gix-ignore", - "gix-index", - "gix-lock", - "gix-mailmap", - "gix-merge", - "gix-negotiate", - "gix-object", - "gix-odb", - "gix-pack", - "gix-path", - "gix-pathspec", - "gix-prompt", - "gix-protocol", - "gix-ref", - "gix-refspec", - "gix-revision", - "gix-revwalk", - "gix-sec", - "gix-shallow", - "gix-status", - "gix-submodule", - "gix-tempfile", - "gix-trace", - "gix-transport", - "gix-traverse", - "gix-url", - "gix-utils", - "gix-validate", - "gix-worktree", - "gix-worktree-state", - "gix-worktree-stream", - "nonempty", - "parking_lot 0.12.5", - "regex", - "serde", - "signal-hook", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-actor" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5e5b518339d5e6718af108fd064d4e9ba33caf728cf487352873d76411df35" -dependencies = [ - "bstr", - "gix-date", - "gix-error", - "serde", - "winnow 0.7.15", -] - -[[package]] -name = "gix-archive" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "651c99be11aac9b303483193ae50b45eb6e094da4f5ed797019b03948f51aad6" -dependencies = [ - "bstr", - "gix-date", - "gix-error", - "gix-object", - "gix-worktree-stream", -] - -[[package]] -name = "gix-attributes" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c233d6eaa098c0ca5ce03236fd7a96e27f1abe72fad74b46003fbd11fe49563c" -dependencies = [ - "bstr", - "gix-glob", - "gix-path", - "gix-quote", - "gix-trace", - "kstring", - "serde", - "smallvec", - "thiserror 2.0.18", - "unicode-bom", -] - -[[package]] -name = "gix-bitmap" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7add20f40d060db8c9b1314d499bac6ed7480f33eb113ce3e1cf5d6ff85d989" -dependencies = [ - "gix-error", -] - -[[package]] -name = "gix-blame" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77aaf9f7348f4da3ebfbfbbc35fa0d07155d98377856198dde6f695fd648705" -dependencies = [ - "gix-commitgraph", - "gix-date", - "gix-diff", - "gix-error", - "gix-hash", - "gix-object", - "gix-revwalk", - "gix-trace", - "gix-traverse", - "gix-worktree", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-chunk" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1096b6608fbe5d27fb4984e20f992b4e76fb8c613f6acb87d07c5831b53a6959" -dependencies = [ - "gix-error", -] - -[[package]] -name = "gix-command" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b849c65a609f50d02f8a2774fe371650b3384a743c79c2a070ce0da49b7fb7da" -dependencies = [ - "bstr", - "gix-path", - "gix-quote", - "gix-trace", - "shell-words", -] - -[[package]] -name = "gix-commitgraph" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3196655fd1443f3c58a48c114aa480be3e4e87b393d7292daaa0d543862eb445" -dependencies = [ - "bstr", - "gix-chunk", - "gix-error", - "gix-hash", - "memmap2", - "nonempty", - "serde", -] - -[[package]] -name = "gix-config" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08939b4c4ed7a663d0e64be9e1e9bdf23a1fb4fcee1febdf449f12229542e50d" -dependencies = [ - "bstr", - "gix-config-value", - "gix-features", - "gix-glob", - "gix-path", - "gix-ref", - "gix-sec", - "memchr", - "smallvec", - "thiserror 2.0.18", - "unicode-bom", - "winnow 0.7.15", -] - -[[package]] -name = "gix-config-value" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441a300bc3645a1f45cba495b9175f90f47256ce43f2ee161da0031e3ac77c92" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "gix-path", - "libc", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-credentials" -version = "0.37.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b2a34b8715e3bbd514f3d1705f5d51c4b250e5bfe506b9fb60b133c85c93d9" -dependencies = [ - "bstr", - "gix-command", - "gix-config-value", - "gix-date", - "gix-path", - "gix-prompt", - "gix-sec", - "gix-trace", - "gix-url", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-date" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39acf819aa9fee65e4838a2eec5cb2506e47ebb89e02a5ab9918196e491571ea" -dependencies = [ - "bstr", - "gix-error", - "itoa", - "jiff", - "serde", - "smallvec", -] - -[[package]] -name = "gix-diff" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f3b3475e5d3877d7c30c40827cc2441936ce890efc226e5ba4afe3a7ae33f0" -dependencies = [ - "bstr", - "gix-attributes", - "gix-command", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-worktree", - "imara-diff 0.1.8", - "imara-diff 0.2.0", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-dir" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da4604a360988f0ba8efe6f90093ca5a844f4a7f8e1a3dcda501ec44e600ea9" -dependencies = [ - "bstr", - "gix-discover", - "gix-fs", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-trace", - "gix-utils", - "gix-worktree", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-discover" -version = "0.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65bd3330fe0cb9d40d875bf862fd5e8ad6fa4164ddbc4842fbeb889c3f0b2c6" -dependencies = [ - "bstr", - "dunce", - "gix-fs", - "gix-path", - "gix-ref", - "gix-sec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e86d01da904d4a9265def43bd42a18c5e6dc7000a73af512946ba14579c9fbd" -dependencies = [ - "bstr", -] - -[[package]] -name = "gix-features" -version = "0.46.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "752493cd4b1d5eaaa0138a7493f65c96863fefa990fc021e0e519579e389ab20" -dependencies = [ - "bytes", - "bytesize", - "crc32fast", - "crossbeam-channel", - "gix-path", - "gix-trace", - "gix-utils", - "libc", - "once_cell", - "parking_lot 0.12.5", - "prodash", - "thiserror 2.0.18", - "walkdir", - "zlib-rs", -] - -[[package]] -name = "gix-filter" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37598282a6566da6fb52667570c7fe0aedcb122ac886724a9e62a2180523e35" -dependencies = [ - "bstr", - "encoding_rs", - "gix-attributes", - "gix-command", - "gix-hash", - "gix-object", - "gix-packetline", - "gix-path", - "gix-quote", - "gix-trace", - "gix-utils", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-fs" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a964b4aec683eb0bacb87533defa80805bb4768056371a47ab38b00a2d377b72" -dependencies = [ - "bstr", - "fastrand", - "gix-features", - "gix-path", - "gix-utils", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-glob" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03e6cd88cc0dc1eafa1fddac0fb719e4e74b6ea58dd016e71125fde4a326bee" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "gix-features", - "gix-path", - "serde", -] - -[[package]] -name = "gix-hash" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fb896a02d9ab96fa518475a5f30ad3952010f801a8de5840f633f4a6b985dfb" -dependencies = [ - "faster-hex", - "gix-features", - "serde", - "sha1-checked", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-hashtable" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2664216fc5e89b51e756a4a3ac676315602ce2dac07acf1da959a22038d69b33" -dependencies = [ - "gix-hash", - "hashbrown 0.16.1", - "parking_lot 0.12.5", -] - -[[package]] -name = "gix-ignore" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f915dcf6911e3027537166d34e13f0fe101ed12225178d2ae29cd1272cff26" -dependencies = [ - "bstr", - "gix-glob", - "gix-path", - "gix-trace", - "serde", - "unicode-bom", -] - -[[package]] -name = "gix-index" -version = "0.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bae54ab14e4e74d5dda60b82ea7afad7c8eb3be68283d6d5f29bd2e6d47fff7" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "filetime", - "fnv", - "gix-bitmap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-traverse", - "gix-utils", - "gix-validate", - "hashbrown 0.16.1", - "itoa", - "libc", - "memmap2", - "rustix", - "serde", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-lock" -version = "21.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054fbd0989700c69dc5aa80bc66944f05df1e15aa7391a9e42aca7366337905f" -dependencies = [ - "gix-tempfile", - "gix-utils", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-mailmap" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b4818da522786ec7e32a00884ee8fc40fa4c215c3997c0b15f7b62684d1199" -dependencies = [ - "bstr", - "gix-actor", - "gix-date", - "gix-error", - "serde", -] - -[[package]] -name = "gix-merge" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4606747466512d22c2dffc019142e1941238f543987ea51353c938cca80c500" -dependencies = [ - "bstr", - "gix-command", - "gix-diff", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-index", - "gix-object", - "gix-path", - "gix-quote", - "gix-revision", - "gix-revwalk", - "gix-tempfile", - "gix-trace", - "gix-worktree", - "imara-diff 0.1.8", - "nonempty", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-negotiate" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea064c7595eea08fdd01c70748af747d9acc40f727b61f4c8a2145a5c5fc28c" -dependencies = [ - "bitflags 2.11.0", - "gix-commitgraph", - "gix-date", - "gix-hash", - "gix-object", - "gix-revwalk", -] - -[[package]] -name = "gix-object" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cafb802bb688a7c1e69ef965612ff5ff859f046bfb616377e4a0ba4c01e43d47" -dependencies = [ - "bstr", - "gix-actor", - "gix-date", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-path", - "gix-utils", - "gix-validate", - "itoa", - "serde", - "smallvec", - "thiserror 2.0.18", - "winnow 0.7.15", -] - -[[package]] -name = "gix-odb" -version = "0.78.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24833ae9323b4f7079575fb9f961cf9c414b0afbec428a536ab8e7dd93bc002b" -dependencies = [ - "arc-swap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-pack", - "gix-path", - "gix-quote", - "parking_lot 0.12.5", - "serde", - "tempfile", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-pack" -version = "0.68.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3484119cd19859d7d7639413c27e192478fa354d3f4ff5f7e3c041e8040f0f4" -dependencies = [ - "clru", - "gix-chunk", - "gix-error", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-path", - "gix-tempfile", - "memmap2", - "parking_lot 0.12.5", - "serde", - "smallvec", - "thiserror 2.0.18", - "uluru", -] - -[[package]] -name = "gix-packetline" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be19313dcdb7dff75a3ce2f99be00878458295bcc3b6c7f0005591597573345c" -dependencies = [ - "bstr", - "faster-hex", - "gix-trace", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-path" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c31d4373bda7fab9eb01822927b55185a378d6e1bf737e0a54c743ad806658" -dependencies = [ - "bstr", - "gix-trace", - "gix-validate", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-pathspec" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89611f13544ca5ebeb68a502673814ef57200df60c24a61c2ce7b96f612f08b" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "gix-attributes", - "gix-config-value", - "gix-glob", - "gix-path", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-prompt" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f61f6264e1f6c5a951531fe127722c7522bc02ebda80c4528286bda4642055f" -dependencies = [ - "gix-command", - "gix-config-value", - "parking_lot 0.12.5", - "rustix", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-protocol" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38666350736b5877c79f57ddae02bde07a4ce186d889adc391e831cddcbe76" -dependencies = [ - "bstr", - "gix-credentials", - "gix-date", - "gix-features", - "gix-hash", - "gix-lock", - "gix-negotiate", - "gix-object", - "gix-ref", - "gix-refspec", - "gix-revwalk", - "gix-shallow", - "gix-trace", - "gix-transport", - "gix-utils", - "maybe-async", - "nonempty", - "serde", - "thiserror 2.0.18", - "winnow 0.7.15", -] - -[[package]] -name = "gix-quote" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68533db71259c8776dd4e770d2b7b98696213ecdc1f5c9e3507119e274e0c578" -dependencies = [ - "bstr", - "gix-error", - "gix-utils", -] - -[[package]] -name = "gix-ref" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2159978abb99b7027c8579d15211e262ef0ef2594d5cecb3334fbcbdfe2997c" -dependencies = [ - "gix-actor", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-utils", - "gix-validate", - "memmap2", - "serde", - "thiserror 2.0.18", - "winnow 0.7.15", -] - -[[package]] -name = "gix-refspec" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc806ee13f437428f8a1ba4c72ecfaa3f20e14f5f0d4c2bc17d0b33e794aa6ac" -dependencies = [ - "bstr", - "gix-error", - "gix-glob", - "gix-hash", - "gix-revision", - "gix-validate", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-revision" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c08f1ec5d1e6a524f8ba291c41f0ccaef64e48ed0e8cf790b3461cae45f6d3d" -dependencies = [ - "bitflags 2.11.0", - "bstr", - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-revwalk", - "gix-trace", - "nonempty", - "serde", -] - -[[package]] -name = "gix-revwalk" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4b2b87772b21ca449249e86d32febadba5cba32b0fcce804ab9cefc6f2111c" -dependencies = [ - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-hashtable", - "gix-object", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-sec" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf82ae037de9c62850ce67beaa92ec8e3e17785ea307cdde7618edc215603b4f" -dependencies = [ - "bitflags 2.11.0", - "gix-path", - "libc", - "serde", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-shallow" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf60711c9083b2364b3fac8a352444af76b17201f3682fdebe74fa66d89a772" -dependencies = [ - "bstr", - "gix-hash", - "gix-lock", - "nonempty", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-status" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d6c598e3fdbc352fba1c5ba7e709e69402fafbc44d9295edad2e3c4738996b" -dependencies = [ - "bstr", - "filetime", - "gix-diff", - "gix-dir", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-index", - "gix-object", - "gix-path", - "gix-pathspec", - "gix-worktree", - "portable-atomic", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-submodule" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5c3929c5e6821f651d35e8420f72fea3cfafe9fc1e928a61e718b462c72a5" -dependencies = [ - "bstr", - "gix-config", - "gix-path", - "gix-pathspec", - "gix-refspec", - "gix-url", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-tempfile" -version = "21.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22227f6b203f511ff451c33c89899e87e4f571fc596b06f68e6e613a6508528" -dependencies = [ - "dashmap", - "gix-fs", - "libc", - "parking_lot 0.12.5", - "signal-hook", - "signal-hook-registry", - "tempfile", -] - -[[package]] -name = "gix-trace" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0" - -[[package]] -name = "gix-transport" -version = "0.55.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a521e39c6235ce63ed6c001e2dd79818c830b82c3b7b59247ee7b229c39ec9bb" -dependencies = [ - "bstr", - "gix-command", - "gix-features", - "gix-packetline", - "gix-quote", - "gix-sec", - "gix-url", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-traverse" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963dc2afcdb611092aa587c3f9365e749ac0a0892ff27662dbc75f26c953fbec" -dependencies = [ - "bitflags 2.11.0", - "gix-commitgraph", - "gix-date", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-revwalk", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-url" -version = "0.35.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d28e8af3d42581190da884f013caf254d2fd4d6ab102408f08d21bfa11de6c8d" -dependencies = [ - "bstr", - "gix-path", - "percent-encoding", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-utils" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "befcdbdfb1238d2854591f760a48711bed85e72d80a10e8f2f93f656746ef7c5" -dependencies = [ - "bstr", - "fastrand", - "unicode-normalization", -] - -[[package]] -name = "gix-validate" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec1eff98d91941f47766367cba1be746bab662bad761d9891ae6f7882f7840b" -dependencies = [ - "bstr", -] - -[[package]] -name = "gix-worktree" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6bd5830cbc43c9c00918b826467d2afad685b195cb82329cde2b2d116d2c578" -dependencies = [ - "bstr", - "gix-attributes", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-validate", - "serde", -] - -[[package]] -name = "gix-worktree-state" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644a1681f96e1be43c2a8384337d9d220e7624f50db54beda70997052aebf707" -dependencies = [ - "bstr", - "gix-features", - "gix-filter", - "gix-fs", - "gix-index", - "gix-object", - "gix-path", - "gix-worktree", - "io-close", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-worktree-stream" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e3fb70a1f650a5cec7d5b8d10d6d6fe86daf3cf15bde08ba0c70988a2932c3" -dependencies = [ - "gix-attributes", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-object", - "gix-path", - "gix-traverse", - "parking_lot 0.12.5", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "globset" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "globwalk" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" -dependencies = [ - "bitflags 2.11.0", - "ignore", - "walkdir", -] - -[[package]] -name = "gouqi" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360ceeadce44b226639db2d4f3ac716ce243ccd56b9fe194a1b133a1a5b8fdb0" -dependencies = [ - "futures", - "humantime-serde", - "reqwest 0.12.28", - "serde", - "serde_json", - "skeptic", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "graphql-introspection-query" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2a4732cf5140bd6c082434494f785a19cfb566ab07d1382c3671f5812fed6d" -dependencies = [ - "serde", -] - -[[package]] -name = "graphql-parser" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a818c0d883d7c0801df27be910917750932be279c7bc82dc541b8769425f409" -dependencies = [ - "combine", - "thiserror 1.0.69", -] - -[[package]] -name = "graphql_client" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50cfdc7f34b7f01909d55c2dcb71d4c13cbcbb4a1605d6c8bd760d654c1144b" -dependencies = [ - "graphql_query_derive", - "serde", - "serde_json", -] - -[[package]] -name = "graphql_client_codegen" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e27ed0c2cf0c0cc52c6bcf3b45c907f433015e580879d14005386251842fb0a" -dependencies = [ - "graphql-introspection-query", - "graphql-parser", - "heck 0.4.1", - "lazy_static", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 1.0.109", -] - -[[package]] -name = "graphql_query_derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83febfa838f898cfa73dfaa7a8eb69ff3409021ac06ee94cfb3d622f6eeb1a97" -dependencies = [ - "graphql_client_codegen", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "grep-matcher" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d7b71093325ab22d780b40d7df3066ae4aebb518ba719d38c697a8228a8023" -dependencies = [ - "memchr", -] - -[[package]] -name = "grep-searcher" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac63295322dc48ebb20a25348147905d816318888e64f531bfc2a2bc0577dc34" -dependencies = [ - "bstr", - "encoding_rs", - "encoding_rs_io", - "grep-matcher", - "log", - "memchr", - "memmap2", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.2", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot 0.12.5", - "rand 0.9.2", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-auth" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" -dependencies = [ - "memchr", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.4.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.4.0", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "human_format" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" - -[[package]] -name = "humansize" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" -dependencies = [ - "libm", -] - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "humantime-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" -dependencies = [ - "humantime", - "serde", -] - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http 1.4.0", - "http-body 1.0.1", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.4.0", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.6", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.3", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "ignore" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - -[[package]] -name = "imara-diff" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "imara-diff" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c" -dependencies = [ - "hashbrown 0.15.5", - "memchr", -] - -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "glob", - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indicatif" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" -dependencies = [ - "console", - "portable-atomic", - "unicode-segmentation", - "unicode-width", - "unit-prefix", - "web-time", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "io-close" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "ipconfig" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" -dependencies = [ - "socket2 0.6.3", - "widestring", - "windows-registry", - "windows-result 0.4.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_executable" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" -dependencies = [ - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-sys 0.61.2", -] - -[[package]] -name = "jiff-static" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.117", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "jsonwebtoken" -version = "10.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" -dependencies = [ - "aws-lc-rs", - "base64 0.22.1", - "getrandom 0.2.17", - "js-sys", - "pem", - "serde", - "serde_json", - "signature", - "simple_asn1", -] - -[[package]] -name = "jwt" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" -dependencies = [ - "base64 0.13.1", - "crypto-common", - "digest", - "hmac", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "keyed_priority_queue" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" -dependencies = [ - "indexmap 2.13.0", -] - -[[package]] -name = "kingfisher" -version = "1.91.0" -dependencies = [ - "anyhow", - "asar", - "assert_cmd", - "aws-config", - "aws-credential-types", - "aws-sdk-dynamodb", - "aws-sdk-ec2", - "aws-sdk-iam", - "aws-sdk-kms", - "aws-sdk-lambda", - "aws-sdk-s3", - "aws-sdk-secretsmanager", - "aws-sdk-sts", - "aws-smithy-http-client", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "axum", - "base32", - "base64 0.22.1", - "blake3", - "bloomfilter", - "bson 3.1.0", - "bstr", - "byteorder", - "bytes", - "bzip2-rs", - "chrono", - "clap", - "color-backtrace", - "console", - "content_inspector", - "crc32fast", - "crossbeam-channel", - "crossbeam-skiplist", - "dashmap", - "ed25519-dalek", - "fixedbitset", - "flate2", - "futures", - "gcloud-storage", - "git2", - "gitlab", - "gix", - "globset", - "gouqi", - "h2", - "hex", - "hmac", - "http 1.4.0", - "humantime", - "ignore", - "include_dir", - "indenter", - "indicatif", - "ipnet", - "jsonwebtoken 10.3.0", - "kingfisher-core", - "kingfisher-rules", - "kingfisher-scanner", - "lazy_static", - "liquid", - "liquid-core", - "lzma-rs", - "memchr", - "memmap2", - "mimalloc", - "mongodb", - "mysql_async", - "num_cpus", - "oci-client", - "octorust", - "once_cell", - "p256", - "parking_lot 0.12.5", - "path-dedot", - "pem", - "percent-encoding", - "petgraph", - "predicates", - "pretty_assertions", - "proptest", - "quick-xml 0.39.2", - "rand 0.10.0", - "rand_chacha 0.10.0", - "rayon", - "regex", - "reqwest 0.12.28", - "reqwest-middleware 0.4.2", - "reqwest-middleware 0.5.1", - "ring", - "roaring", - "rusqlite", - "rustc-hash", - "rustls", - "rustls-native-certs", - "schemars 0.8.22", - "self_update", - "semver", - "serde", - "serde-sarif", - "serde_json", - "serde_yaml", - "sha1", - "sha2", - "smallvec", - "streaming-iterator", - "strum 0.26.3", - "strum_macros 0.28.0", - "sysinfo", - "tar", - "temp-env", - "tempfile", - "testcontainers", - "thiserror 2.0.18", - "thousands", - "thread_local", - "tikv-jemallocator", - "time", - "tokei", - "tokio", - "tokio-postgres", - "tokio-postgres-rustls", - "tokio-rustls", - "toon-format", - "tracing", - "tracing-core", - "tracing-subscriber", - "tree-sitter", - "tree-sitter-bash", - "tree-sitter-c", - "tree-sitter-c-sharp", - "tree-sitter-cpp", - "tree-sitter-css", - "tree-sitter-go", - "tree-sitter-html", - "tree-sitter-java", - "tree-sitter-javascript", - "tree-sitter-php", - "tree-sitter-python", - "tree-sitter-regex", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-toml-ng", - "tree-sitter-typescript", - "tree-sitter-yaml", - "tree_magic_mini", - "url", - "uuid", - "vectorscan-rs", - "walkdir", - "webbrowser", - "wiremock", - "xxhash-rust", - "zip 2.4.2", -] - -[[package]] -name = "kingfisher-core" -version = "0.1.0" -dependencies = [ - "anyhow", - "bstr", - "console", - "dashmap", - "gix", - "hex", - "memchr", - "memmap2", - "once_cell", - "parking_lot 0.12.5", - "pretty_assertions", - "rustc-hash", - "schemars 0.8.22", - "serde", - "serde_json", - "sha1", - "smallvec", - "thiserror 2.0.18", - "tokei", -] - -[[package]] -name = "kingfisher-rules" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64 0.22.1", - "crc32fast", - "hmac", - "ignore", - "include_dir", - "kingfisher-core", - "lazy_static", - "liquid", - "liquid-core", - "percent-encoding", - "pretty_assertions", - "proptest", - "rand 0.10.0", - "regex", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_yaml", - "sha1", - "sha2", - "thiserror 2.0.18", - "time", - "tracing", - "uuid", - "vectorscan-rs", - "walkdir", - "xxhash-rust", -] - -[[package]] -name = "kingfisher-scanner" -version = "0.1.0" -dependencies = [ - "anyhow", - "aws-config", - "aws-credential-types", - "aws-sdk-iam", - "aws-sdk-sts", - "aws-smithy-http-client", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "base32", - "base64 0.22.1", - "bson 3.1.0", - "bstr", - "byteorder", - "chrono", - "crossbeam-skiplist", - "ed25519-dalek", - "hex", - "hmac", - "http 1.4.0", - "jsonwebtoken 10.3.0", - "kingfisher-core", - "kingfisher-rules", - "liquid", - "liquid-core", - "mongodb", - "mysql_async", - "once_cell", - "p256", - "parking_lot 0.12.5", - "pem", - "percent-encoding", - "pretty_assertions", - "quick-xml 0.39.2", - "rand 0.10.0", - "regex", - "reqwest 0.12.28", - "ring", - "rustc-hash", - "rustls", - "rustls-native-certs", - "schemars 0.8.22", - "serde", - "serde_json", - "sha1", - "sha2", - "smallvec", - "tempfile", - "thiserror 2.0.18", - "thread_local", - "tokio", - "tokio-postgres", - "tokio-postgres-rustls", - "tracing", - "url", - "vectorscan-rs", - "xxhash-rust", -] - -[[package]] -name = "kstring" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" -dependencies = [ - "serde", - "static_assertions", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libgit2-sys" -version = "0.18.3+1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libmimalloc-sys" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "libredox" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" -dependencies = [ - "bitflags 2.11.0", - "libc", - "plain", - "redox_syscall 0.7.3", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-sys" -version = "1.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "liquid" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a494c3f9dad3cb7ed16f1c51812cbe4b29493d6c2e5cd1e2b87477263d9534d" -dependencies = [ - "liquid-core", - "liquid-derive", - "liquid-lib", - "serde", -] - -[[package]] -name = "liquid-core" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc623edee8a618b4543e8e8505584f4847a4e51b805db1af6d9af0a3395d0d57" -dependencies = [ - "anymap2", - "itertools 0.14.0", - "kstring", - "liquid-derive", - "pest", - "pest_derive", - "regex", - "serde", - "time", -] - -[[package]] -name = "liquid-derive" -version = "0.26.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de66c928222984aea59fcaed8ba627f388aaac3c1f57dcb05cc25495ef8faefe" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "liquid-lib" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9befeedd61f5995bc128c571db65300aeb50d62e4f0542c88282dbcb5f72372a" -dependencies = [ - "itertools 0.14.0", - "liquid-core", - "percent-encoding", - "regex", - "time", - "unicode-segmentation", -] - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "serde_core", -] - -[[package]] -name = "lru" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "macro_magic" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" -dependencies = [ - "macro_magic_core", - "macro_magic_macros", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "macro_magic_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" -dependencies = [ - "const-random", - "derive-syn-parse", - "macro_magic_core_macros", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "macro_magic_core_macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "macro_magic_macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" -dependencies = [ - "macro_magic_core", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "maybe-async" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memmap2" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -dependencies = [ - "libc", -] - -[[package]] -name = "mimalloc" -version = "0.1.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" -dependencies = [ - "libmimalloc-sys", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", -] - -[[package]] -name = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot 0.12.5", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "mongocrypt" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" -dependencies = [ - "bson 2.15.0", - "mongocrypt-sys", - "once_cell", - "serde", -] - -[[package]] -name = "mongocrypt-sys" -version = "0.1.5+1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" - -[[package]] -name = "mongodb" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5941683db2ab2697f71e58dc0319024e808d3b28e7cf20f4bfb445fe54a30b" -dependencies = [ - "aws-config", - "aws-credential-types", - "aws-sigv4", - "base64 0.22.1", - "bitflags 2.11.0", - "bson 2.15.0", - "chrono", - "derive-where", - "derive_more", - "futures-core", - "futures-io", - "futures-util", - "hex", - "hickory-proto", - "hickory-resolver", - "hmac", - "http 1.4.0", - "macro_magic", - "md-5", - "mongocrypt", - "mongodb-internal-macros", - "pbkdf2", - "percent-encoding", - "rand 0.9.2", - "rustc_version_runtime", - "rustls", - "rustversion", - "serde", - "serde_bytes", - "serde_with 3.18.0", - "sha1", - "sha2", - "socket2 0.6.3", - "stringprep", - "strsim 0.11.1", - "take_mut", - "thiserror 2.0.18", - "tokio", - "tokio-rustls", - "tokio-util", - "typed-builder", - "uuid", - "webpki-roots 1.0.6", -] - -[[package]] -name = "mongodb-internal-macros" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47021a12bbf0dffde9c890fa2d36ff6ae342c532016226b04a42301b2b912660" -dependencies = [ - "macro_magic", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "mysql-common-derive" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66f62cad7623a9cb6f8f64037f0c4f69c8db8e82914334a83c9788201c2c1bfa" -dependencies = [ - "darling 0.20.11", - "heck 0.5.0", - "num-bigint", - "proc-macro-crate", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", - "termcolor", - "thiserror 2.0.18", -] - -[[package]] -name = "mysql_async" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "277ce2f2459b2af4cc6d0a0b7892381f80800832f57c533f03e2845f4ea331ea" -dependencies = [ - "bytes", - "crossbeam-queue", - "flate2", - "futures-core", - "futures-sink", - "futures-util", - "keyed_priority_queue", - "lru 0.14.0", - "mysql_common", - "pem", - "percent-encoding", - "rand 0.9.2", - "rustls", - "rustls-pemfile", - "serde", - "serde_json", - "socket2 0.5.10", - "thiserror 2.0.18", - "tokio", - "tokio-rustls", - "tokio-util", - "twox-hash", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "mysql_common" -version = "0.35.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb9f371618ce723f095c61fbcdc36e8936956d2b62832f9c7648689b338e052" -dependencies = [ - "base64 0.22.1", - "bitflags 2.11.0", - "btoi", - "byteorder", - "bytes", - "crc32fast", - "flate2", - "getrandom 0.3.4", - "mysql-common-derive", - "num-bigint", - "num-traits", - "regex", - "saturating", - "serde", - "serde_json", - "sha1", - "sha2", - "thiserror 2.0.18", - "uuid", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "nonempty" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" -dependencies = [ - "serde", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" -dependencies = [ - "arrayvec", - "itoa", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.11.0", - "objc2", -] - -[[package]] -name = "objc2-system-configuration" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" -dependencies = [ - "objc2-core-foundation", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "oci-client" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b74df13319e08bc386d333d3dc289c774c88cc543cae31f5347db07b5ec2172" -dependencies = [ - "bytes", - "chrono", - "futures-util", - "http 1.4.0", - "http-auth", - "jwt", - "lazy_static", - "oci-spec", - "olpc-cjson", - "regex", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2", - "thiserror 2.0.18", - "tokio", - "tracing", - "unicase", -] - -[[package]] -name = "oci-spec" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3da52b83ce3258fbf29f66ac784b279453c2ac3c22c5805371b921ede0d308" -dependencies = [ - "const_format", - "derive_builder 0.20.2", - "getset", - "regex", - "serde", - "serde_json", - "strum 0.27.2", - "strum_macros 0.27.2", - "thiserror 2.0.18", -] - -[[package]] -name = "octorust" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c488b641cf652f023d371f6d472191bf3b3fd8075a11f274e95d4fe6e5e3878" -dependencies = [ - "async-recursion", - "async-trait", - "bytes", - "chrono", - "http 1.4.0", - "jsonwebtoken 9.3.1", - "log", - "mime", - "parse_link_header", - "pem", - "percent-encoding", - "reqwest 0.12.28", - "reqwest-conditional-middleware", - "reqwest-middleware 0.4.2", - "reqwest-retry", - "reqwest-tracing", - "ring", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_urlencoded", - "thiserror 1.0.69", - "tokio", - "url", - "uuid", -] - -[[package]] -name = "olpc-cjson" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" -dependencies = [ - "serde", - "serde_json", - "unicode-normalization", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "owo-colors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - -[[package]] -name = "parse_link_header" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3687fe9debbbf2a019f381a8bc6b42049b22647449b39af54b3013985c0cf6de" -dependencies = [ - "http 0.2.12", - "lazy_static", - "regex", -] - -[[package]] -name = "path-dedot" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", -] - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "serde", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_shared 0.13.1", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "pori" -version = "0.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a63d338dec139f56dacc692ca63ad35a6be6a797442479b55acd611d79e906" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "postgres-protocol" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "hmac", - "md-5", - "memchr", - "rand 0.9.2", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" -dependencies = [ - "bytes", - "fallible-iterator 0.2.0", - "postgres-protocol", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "predicates" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" -dependencies = [ - "anstyle", - "difflib", - "float-cmp", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" - -[[package]] -name = "predicates-tree" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.8+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prodash" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" -dependencies = [ - "bytesize", - "human_format", - "parking_lot 0.12.5", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.11.0", - "num-traits", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "pulldown-cmark" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" -dependencies = [ - "bitflags 2.11.0", - "memchr", - "unicase", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.39.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.6.3", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.3", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.0", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" -dependencies = [ - "ppv-lite86", - "rand_core 0.10.0", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams 0.4.2", - "web-sys", - "webpki-roots 1.0.6", -] - -[[package]] -name = "reqwest" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams 0.5.0", - "web-sys", -] - -[[package]] -name = "reqwest-conditional-middleware" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad7fdf5c0a015763fcd164bee294b13fb7b6f89f1b55961d40f00c3e32d6b" -dependencies = [ - "async-trait", - "http 1.4.0", - "reqwest 0.12.28", - "reqwest-middleware 0.4.2", -] - -[[package]] -name = "reqwest-middleware" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" -dependencies = [ - "anyhow", - "async-trait", - "http 1.4.0", - "reqwest 0.12.28", - "serde", - "thiserror 1.0.69", - "tower-service", -] - -[[package]] -name = "reqwest-middleware" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199dda04a536b532d0cc04d7979e39b1c763ea749bf91507017069c00b96056f" -dependencies = [ - "anyhow", - "async-trait", - "http 1.4.0", - "reqwest 0.13.2", - "serde", - "thiserror 2.0.18", - "tower-service", -] - -[[package]] -name = "reqwest-retry" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "getrandom 0.2.17", - "http 1.4.0", - "hyper", - "parking_lot 0.11.2", - "reqwest 0.12.28", - "reqwest-middleware 0.4.2", - "retry-policies", - "thiserror 1.0.69", - "tokio", - "tracing", - "wasm-timer", -] - -[[package]] -name = "reqwest-tracing" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d70ea85f131b2ee9874f0b160ac5976f8af75f3c9badfe0d955880257d10bd83" -dependencies = [ - "anyhow", - "async-trait", - "getrandom 0.2.17", - "http 1.4.0", - "matchit", - "reqwest 0.12.28", - "reqwest-middleware 0.4.2", - "tracing", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "retry-policies" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" -dependencies = [ - "rand 0.8.5", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "roaring" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" -dependencies = [ - "bytemuck", - "byteorder", -] - -[[package]] -name = "rsqlite-vfs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" -dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", -] - -[[package]] -name = "rusqlite" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" -dependencies = [ - "bitflags 2.11.0", - "fallible-iterator 0.3.0", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustc_version_runtime" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" -dependencies = [ - "rustc_version", - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni 0.21.1", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "saturating" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemafy_core" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bec29dddcfe60f92f3c0d422707b8b56473983ef0481df8d5236ed3ab8fdf24" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "schemafy_lib" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3d87f1df246a9b7e2bfd1f4ee5f88e48b11ef9cfc62e63f0dead255b1a6f5f" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "schemafy_core", - "serde", - "serde_derive", - "serde_json", - "syn 1.0.109", - "uriparse", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "bytes", - "chrono", - "dyn-clone", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.11.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "self-replace" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" -dependencies = [ - "fastrand", - "tempfile", - "windows-sys 0.52.0", -] - -[[package]] -name = "self_update" -version = "0.43.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6644febaa58f323b28f7321d04e24d0020d117c27619ab869d6abdf76be9aac6" -dependencies = [ - "either", - "flate2", - "http 1.4.0", - "indicatif", - "log", - "quick-xml 0.38.4", - "regex", - "reqwest 0.12.28", - "self-replace", - "semver", - "serde", - "serde_json", - "tar", - "tempfile", - "ureq", - "urlencoding", - "zip 6.0.0", - "zipsign-api", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-sarif" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d878dc2c454b118932f9e8aef2a228ec99dcac000d6204a0061d9b2ef322304" -dependencies = [ - "anyhow", - "derive_builder 0.12.0", - "prettyplease", - "proc-macro2", - "quote", - "schemafy_lib", - "serde", - "serde_json", - "strum 0.25.0", - "strum_macros 0.24.3", - "syn 2.0.117", - "thiserror 1.0.69", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "indexmap 2.13.0", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros 1.5.2", -] - -[[package]] -name = "serde_with" -version = "3.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.13.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros 3.18.0", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling 0.13.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "serde_with_macros" -version = "3.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.13.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", - "sha1-asm", -] - -[[package]] -name = "sha1-asm" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286acebaf8b67c1130aedffad26f594eff0c1292389158135327d2e23aed582b" -dependencies = [ - "cc", -] - -[[package]] -name = "sha1-checked" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" -dependencies = [ - "digest", - "sha1", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "skeptic" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" -dependencies = [ - "bytecount", - "cargo_metadata", - "error-chain", - "glob", - "pulldown-cmark", - "tempfile", - "walkdir", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slug" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" -dependencies = [ - "deunicode", - "wasm-bindgen", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "socks" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" -dependencies = [ - "byteorder", - "libc", - "winapi", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlite-wasm-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" -dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.31.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows", -] - -[[package]] -name = "table_formatter" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beef5d3fd5472c911d41286849de6a9aee93327f7fae9fb9148fe9ff0102c17d" -dependencies = [ - "colored", - "itertools 0.11.0", - "thiserror 1.0.69", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "take_mut" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "temp-env" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" -dependencies = [ - "parking_lot 0.12.5", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tera" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" -dependencies = [ - "chrono", - "chrono-tz", - "globwalk", - "humansize", - "lazy_static", - "percent-encoding", - "pest", - "pest_derive", - "rand 0.8.5", - "regex", - "serde", - "serde_json", - "slug", - "unicode-segmentation", -] - -[[package]] -name = "term_size" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "terminal_size" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" -dependencies = [ - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "testcontainers" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" -dependencies = [ - "bollard-stubs", - "futures", - "hex", - "hmac", - "log", - "rand 0.8.5", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thousands" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tikv-jemalloc-sys" -version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "tikv-jemallocator" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" -dependencies = [ - "libc", - "tikv-jemalloc-sys", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokei" -version = "14.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de7875c0c312f30e090edb0da5df9f6586033132d36d94c8f9133e82826797" -dependencies = [ - "aho-corasick", - "arbitrary", - "clap", - "clap-cargo", - "colored", - "crossbeam-channel", - "dashmap", - "encoding_rs_io", - "env_logger", - "etcetera", - "grep-searcher", - "ignore", - "json5", - "log", - "num-format", - "once_cell", - "parking_lot 0.12.5", - "rayon", - "regex", - "serde", - "serde_json", - "table_formatter", - "tera", - "term_size", - "toml", -] - -[[package]] -name = "token-source" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75746ae15bef509f21039a652383104424208fdae172a964a8930858b9a78412" -dependencies = [ - "async-trait", -] - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot 0.12.5", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.3", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-postgres" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "futures-channel", - "futures-util", - "log", - "parking_lot 0.12.5", - "percent-encoding", - "phf 0.13.1", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.9.2", - "socket2 0.6.3", - "tokio", - "tokio-util", - "whoami", -] - -[[package]] -name = "tokio-postgres-rustls" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d684bad428a0f2481f42241f821db42c54e2dc81d8c00db8536c506b0a0144" -dependencies = [ - "const-oid", - "ring", - "rustls", - "tokio", - "tokio-postgres", - "tokio-rustls", - "x509-cert", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "1.1.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_write", - "winnow 0.7.15", -] - -[[package]] -name = "toml_edit" -version = "0.25.8+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.1.0+spec-1.1.0", - "toml_parser", - "winnow 1.0.0", -] - -[[package]] -name = "toml_parser" -version = "1.1.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" -dependencies = [ - "winnow 1.0.0", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toon-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d25e33e50b37f95f3b55b6e664218cac7e1a50f056a75bb4c7a6cccfbc8a8c4" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "async-compression", - "bitflags 2.11.0", - "bytes", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "iri-string", - "pin-project-lite", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "tree-sitter" -version = "0.26.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a6592b1aec0109df37b6bafea77eb4e61466e37b0a5a98bef4f89bfb81b7a2" -dependencies = [ - "cc", - "regex", - "regex-syntax", - "serde_json", - "streaming-iterator", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-bash" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-c" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3aad8f0129083a59fe8596157552d2bb7148c492d44c21558d68ca1c722707" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-c-sharp" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f06accca7b45351758663b8215089e643d53bd9a660ce0349314263737fcb0" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-cpp" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-css" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5cbc5e18f29a2c6d6435891f42569525cf95435a3e01c2f1947abcde178686f" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-go" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-html" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-java" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-javascript" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-language" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" - -[[package]] -name = "tree-sitter-php" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c17c3ab69052c5eeaa7ff5cd972dd1bc25d1b97ee779fec391ad3b5df5592" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-python" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-regex" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8a59be9f0ac131fd8f062eaaba14882b2fa5a6a7882a20134cb1d60df2e625" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-ruby" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-rust" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-toml-ng" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9adc2c898ae49730e857d75be403da3f92bb81d8e37a2f918a08dd10de5ebb1" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-typescript" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-yaml" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c223db85f05e34794f065454843b0668ebc15d240ada63e2b5939f43ce7c97" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree_magic_mini" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" -dependencies = [ - "memchr", - "nom 8.0.0", - "petgraph", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - -[[package]] -name = "typed-builder" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" -dependencies = [ - "typed-builder-macro", -] - -[[package]] -name = "typed-builder-macro" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uluru" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bom" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" -dependencies = [ - "base64 0.22.1", - "cookie_store", - "encoding_rs", - "flate2", - "log", - "percent-encoding", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "socks", - "ureq-proto", - "utf8-zero", - "webpki-roots 1.0.6", -] - -[[package]] -name = "ureq-proto" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" -dependencies = [ - "base64 0.22.1", - "http 1.4.0", - "httparse", - "log", -] - -[[package]] -name = "uriparse" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" -dependencies = [ - "fnv", - "lazy_static", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8-zero" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "vectorscan-rs" -version = "0.0.5" -dependencies = [ - "bitflags 2.11.0", - "foreign-types", - "libc", - "thiserror 1.0.69", - "vectorscan-rs-sys", -] - -[[package]] -name = "vectorscan-rs-sys" -version = "0.0.5" -dependencies = [ - "cmake", - "flate2", - "tar", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasite" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" -dependencies = [ - "wasi 0.14.7+wasi-0.2.4", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasm-timer" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.11.2", - "pin-utils", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver", -] - -[[package]] -name = "wax" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d12a78aa0bab22d2f26ed1a96df7ab58e8a93506a3e20adb47c51a93b4e1357" -dependencies = [ - "const_format", - "itertools 0.11.0", - "nom 7.1.3", - "pori", - "regex", - "thiserror 1.0.69", - "walkdir", -] - -[[package]] -name = "web-sys" -version = "0.3.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webbrowser" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" -dependencies = [ - "core-foundation", - "jni 0.22.4", - "log", - "ndk-context", - "objc2", - "objc2-foundation", - "url", - "web-sys", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "whoami" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" -dependencies = [ - "libc", - "libredox", - "objc2-system-configuration", - "wasite", - "web-sys", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link", - "windows-result 0.4.1", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result 0.4.1", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" -dependencies = [ - "memchr", -] - -[[package]] -name = "wiremock" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" -dependencies = [ - "assert-json-diff", - "base64 0.22.1", - "deadpool", - "futures", - "http 1.4.0", - "http-body-util", - "hyper", - "hyper-util", - "log", - "once_cell", - "regex", - "serde", - "serde_json", - "tokio", - "url", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.13.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.13.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x509-cert" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" -dependencies = [ - "const-oid", - "der", - "spki", - "tls_codec", -] - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zip" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "indexmap 2.13.0", - "memchr", - "thiserror 2.0.18", - "time", - "zopfli", -] - -[[package]] -name = "zip" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" -dependencies = [ - "arbitrary", - "crc32fast", - "indexmap 2.13.0", - "memchr", - "time", -] - -[[package]] -name = "zipsign-api" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0" -dependencies = [ - "base64 0.22.1", - "ed25519-dalek", - "thiserror 2.0.18", -] - -[[package]] -name = "zlib-rs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] diff --git a/containers/kingfisher/default.nix b/containers/kingfisher/default.nix deleted file mode 100644 index 8618b88..0000000 --- a/containers/kingfisher/default.nix +++ /dev/null @@ -1,122 +0,0 @@ -# Nix-built Kingfisher secret scanner -# Built from upstream main + sporked feature branches applied as patches. -# Runs on ringtail (amd64) via nix-container-builder runner. -# -# How it works: -# 1. builtins.fetchGit fetches upstream and feature branches at eval time -# 2. diff generates patches from upstream→feature in a sandboxed derivation -# 3. buildRustPackage applies patches to the upstream source and builds -# -# To update: -# 1. Update upstreamRev to the new main SHA -# 2. Rebase feature branches onto new main (mirror-sync does this daily) -# 3. Update feature revs to the new rebased SHAs -# 4. Update Cargo.lock if dependencies changed -# -# The upstream rev must be an ancestor of each feature rev. -{ pkgs ? import { } }: - -let - version = "165768b"; - repoUrl = "https://forge.ops.eblu.me/eblume/kingfisher.git"; - - upstreamRev = "165768b5ca9a85c2e8c64bed19bb197e82b45360"; - - features = [ - { - name = "clone-url-base"; - ref = "feature/upstream/clone-url-base"; - rev = "4d5ce57a12650ec54c41b909f8623a1d395aa0a9"; - } - ]; - - # Fetch upstream source at the pinned rev (eval-time, network access) - upstreamSrc = builtins.fetchGit { - url = repoUrl; - ref = "main"; - rev = upstreamRev; - }; - - # Fetch each feature branch source and generate a patch against upstream - featurePatches = map (f: - let - featureSrc = builtins.fetchGit { - url = repoUrl; - ref = f.ref; - rev = f.rev; - }; - in - pkgs.runCommand "spork-${f.name}.patch" { - nativeBuildInputs = [ pkgs.diffutils pkgs.gnused ]; - } '' - diff -ruN --no-dereference ${upstreamSrc} ${featureSrc} \ - | sed -e 's|${upstreamSrc}/|a/|g' -e 's|${featureSrc}/|b/|g' \ - > $out || true - '' - ) features; - - kingfisher = pkgs.rustPlatform.buildRustPackage { - pname = "kingfisher"; - inherit version; - src = upstreamSrc; - - patches = featurePatches; - - # Cargo.lock is not committed upstream; we vendor a copy alongside default.nix - cargoLock.lockFile = ./Cargo.lock; - - # Patch the source to include Cargo.lock (buildRustPackage needs it in-tree) - postPatch = '' - cp ${./Cargo.lock} Cargo.lock - chmod +w Cargo.lock - ''; - - nativeBuildInputs = with pkgs; [ - cmake - pkg-config - python3 - ]; - - buildInputs = with pkgs; [ - boost - openssl - ]; - - # Don't run tests — they need network access for wiremock - doCheck = false; - - meta = with pkgs.lib; { - description = "Secret detection and live validation tool"; - homepage = "https://github.com/mongodb/kingfisher"; - license = licenses.asl20; - mainProgram = "kingfisher"; - }; - }; -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/kingfisher"; - contents = [ - kingfisher - pkgs.bashInteractive - pkgs.coreutils - pkgs.cacert - pkgs.git - pkgs.tzdata - ]; - - extraCommands = '' - mkdir -p tmp - chmod 1777 tmp - ''; - - config = { - Entrypoint = [ "${kingfisher}/bin/kingfisher" ]; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - "TMPDIR=/tmp" - ]; - User = "65534"; - }; -} diff --git a/containers/kiwix-serve/Dockerfile b/containers/kiwix-serve/Dockerfile new file mode 100644 index 0000000..17167e5 --- /dev/null +++ b/containers/kiwix-serve/Dockerfile @@ -0,0 +1,55 @@ +# kiwix-serve container +# Downloads pre-built binary from kiwix mirror + +ARG CONTAINER_APP_VERSION=3.8.2 + +FROM alpine:3.22 + +ARG TARGETPLATFORM +ARG CONTAINER_APP_VERSION +ARG KIWIX_VERSION=${CONTAINER_APP_VERSION} + +RUN set -e && \ + apk --no-cache add dumb-init curl && \ + # Detect architecture - use TARGETPLATFORM if set, otherwise detect from uname + if [ -n "$TARGETPLATFORM" ]; then \ + echo "TARGETPLATFORM: $TARGETPLATFORM"; \ + case "$TARGETPLATFORM" in \ + linux/arm64*) ARCH="aarch64" ;; \ + linux/amd64*) ARCH="x86_64" ;; \ + *) ARCH="" ;; \ + esac; \ + else \ + echo "TARGETPLATFORM not set, detecting from uname..."; \ + UNAME_ARCH=$(uname -m); \ + echo "uname -m: $UNAME_ARCH"; \ + case "$UNAME_ARCH" in \ + aarch64|arm64) ARCH="aarch64" ;; \ + x86_64) ARCH="x86_64" ;; \ + *) ARCH="" ;; \ + esac; \ + fi && \ + if [ -z "$ARCH" ]; then \ + echo "ERROR: Unsupported architecture"; \ + exit 1; \ + fi && \ + url="http://mirror.download.kiwix.org/release/kiwix-tools/kiwix-tools_linux-$ARCH-$KIWIX_VERSION.tar.gz" && \ + echo "URL: $url" && \ + curl -k -L $url | tar -xz -C /usr/local/bin/ --strip-components 1 && \ + apk del curl + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="kiwix-serve" +LABEL org.opencontainers.image.description="Kiwix content server for offline ZIM files" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +EXPOSE 80 + +# Run as non-root +RUN adduser -D -u 1000 kiwix +USER kiwix + +ENTRYPOINT ["/usr/bin/dumb-init", "--"] +CMD ["/bin/sh", "-c", "echo 'Use: kiwix-serve [options] ' && kiwix-serve --help"] diff --git a/containers/kiwix-serve/container.py b/containers/kiwix-serve/container.py deleted file mode 100644 index 1e6596e..0000000 --- a/containers/kiwix-serve/container.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Kiwix content server — native Dagger build. - -Downloads pre-built kiwix-tools binary from the Kiwix mirror. -Multi-arch support (aarch64, x86_64) via platform detection. -""" - -import dagger - -from blumeops.containers import alpine_runtime, oci_labels - -VERSION = "3.8.2" - -MIRROR = "http://mirror.download.kiwix.org/release/kiwix-tools" - - -async def build(src: dagger.Directory) -> dagger.Container: - runtime = alpine_runtime( - extra_apk=["dumb-init", "curl"], - uid=1000, - gid=1000, - username="kiwix", - ) - - # Download and install the pre-built binary for the target platform - runtime = runtime.with_exec( - [ - "sh", - "-c", - f"ARCH=$(uname -m) && " - f'case "$ARCH" in aarch64|arm64) ARCH=aarch64;; x86_64) ARCH=x86_64;; *) echo "Unsupported: $ARCH"; exit 1;; esac && ' - f'curl -fsSL "{MIRROR}/kiwix-tools_linux-$ARCH-{VERSION}.tar.gz" | tar -xz -C /usr/local/bin/ --strip-components 1', - ] - ).with_exec(["apk", "del", "curl"]) - - runtime = oci_labels( - runtime, - title="kiwix-serve", - description="Kiwix content server for offline ZIM files", - version=VERSION, - ) - - return ( - runtime.with_exposed_port(80) - .with_user("1000") - .with_entrypoint(["/usr/bin/dumb-init", "--"]) - .with_default_args( - args=[ - "/bin/sh", - "-c", - "echo 'Use: kiwix-serve [options] ' && kiwix-serve --help", - ] - ) - ) diff --git a/containers/kube-state-metrics/Dockerfile b/containers/kube-state-metrics/Dockerfile deleted file mode 100644 index ebaf8e6..0000000 --- a/containers/kube-state-metrics/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -# kube-state-metrics — Kubernetes state metrics exporter -# Two-stage build: Go binary, Alpine runtime - -ARG CONTAINER_APP_VERSION=2.18.0 -ARG KSM_VERSION=v${CONTAINER_APP_VERSION} -ARG KSM_COMMIT=ab562f78ebf4cb97cc2f87c1235e457076035d16 - -FROM golang:alpine3.22 AS build - -ARG KSM_VERSION -ARG KSM_COMMIT -RUN apk add --no-cache build-base git - -RUN mkdir /app && cd /app \ - && git init \ - && git remote add origin https://forge.ops.eblu.me/mirrors/kube-state-metrics.git \ - && git fetch --depth 1 origin ${KSM_COMMIT} \ - && git checkout FETCH_HEAD - -WORKDIR /app - -ENV CGO_ENABLED=0 - -RUN go build \ - -o /kube-state-metrics \ - -ldflags "-s -w -X k8s.io/kube-state-metrics/v2/pkg/version.Version=${KSM_VERSION}" - -FROM alpine:3.22 - -ARG CONTAINER_APP_VERSION -LABEL org.opencontainers.image.title="kube-state-metrics" -LABEL org.opencontainers.image.description="Generates metrics about the state of Kubernetes objects" -LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" -LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" -LABEL org.opencontainers.image.vendor="blumeops" - -RUN apk --no-cache add ca-certificates tzdata - -COPY --from=build /kube-state-metrics /usr/bin/kube-state-metrics - -EXPOSE 8080 8081 - -USER 65534 -ENTRYPOINT ["/usr/bin/kube-state-metrics"] diff --git a/containers/kube-state-metrics/default.nix b/containers/kube-state-metrics/default.nix deleted file mode 100644 index bd83db5..0000000 --- a/containers/kube-state-metrics/default.nix +++ /dev/null @@ -1,59 +0,0 @@ -# Nix-built kube-state-metrics -# Builds v2.18.0 from forge mirror -# Built with dockerTools.buildLayeredImage for efficient layer caching -{ pkgs ? import { } }: - -let - version = "2.18.0"; - - src = pkgs.fetchgit { - url = "https://forge.ops.eblu.me/mirrors/kube-state-metrics.git"; - rev = "v${version}"; - hash = "sha256-oLkIjc6VC3hTrFg9LmgSUtwt4ek0dT7h2u2DfNRx5Gg="; - }; - - kube-state-metrics = pkgs.buildGoModule { - inherit src version; - pname = "kube-state-metrics"; - vendorHash = "sha256-ccP34lywpQnIx3R5IyGURuvb4ijNfCu2VVAeVjBrN0w="; - - doCheck = false; - - subPackages = [ "." ]; - - ldflags = [ - "-s" - "-w" - "-X k8s.io/kube-state-metrics/v2/pkg/version.Version=v${version}" - ]; - - meta = with pkgs.lib; { - description = "Generates metrics about the state of Kubernetes objects"; - homepage = "https://github.com/kubernetes/kube-state-metrics"; - license = licenses.asl20; - mainProgram = "kube-state-metrics"; - }; - }; -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/kube-state-metrics"; - contents = [ - kube-state-metrics - pkgs.cacert - pkgs.tzdata - ]; - - config = { - Entrypoint = [ "${kube-state-metrics}/bin/kube-state-metrics" ]; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - ]; - ExposedPorts = { - "8080/tcp" = { }; - "8081/tcp" = { }; - }; - User = "65534"; - }; -} diff --git a/containers/mealie/Dockerfile b/containers/mealie/Dockerfile new file mode 100644 index 0000000..8df38bf --- /dev/null +++ b/containers/mealie/Dockerfile @@ -0,0 +1,145 @@ +# Mealie — self-hosted recipe manager +# Built from source via forge mirror of mealie-recipes/mealie +# Based on upstream docker/Dockerfile (multi-stage: Node frontend + Python backend) + +ARG CONTAINER_APP_VERSION=v3.12.0 + +############################################### +# Frontend Build +############################################### +FROM node:24-slim AS frontend-builder + +ARG CONTAINER_APP_VERSION +RUN apt-get update && apt-get install --no-install-recommends -y git ca-certificates && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/mealie.git /src + +WORKDIR /src/frontend + +RUN yarn install \ + --prefer-offline \ + --frozen-lockfile \ + --non-interactive \ + --production=false \ + --network-timeout 1000000 + +RUN yarn generate + +############################################### +# Python Base +############################################### +FROM python:3.12-slim AS python-base + +ENV MEALIE_HOME="/app" +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + VENV_PATH="/opt/mealie" + +ENV PATH="$VENV_PATH/bin:$PATH" + +RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \ + && usermod -G users abc \ + && mkdir $MEALIE_HOME + +############################################### +# Backend Package Build +############################################### +FROM python-base AS backend-builder + +ARG CONTAINER_APP_VERSION +RUN apt-get update \ + && apt-get install --no-install-recommends -y curl git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install uv + +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/mealie.git /src + +WORKDIR /src + +COPY --from=frontend-builder /src/frontend/dist ./mealie/frontend + +RUN uv build --out-dir dist + +RUN uv export --no-editable --no-emit-project --extra pgsql --format requirements-txt --output-file dist/requirements.txt \ + && MEALIE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") \ + && echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt \ + && pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \ + && echo " \\" >> dist/requirements.txt \ + && pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt + +############################################### +# Python Venv Build +############################################### +FROM python-base AS venv-builder + +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + build-essential \ + libpq-dev \ + libwebp-dev \ + ffmpeg \ + libsasl2-dev libldap2-dev libssl-dev \ + gnupg gnupg2 gnupg1 \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m venv --upgrade-deps $VENV_PATH + +COPY --from=backend-builder /src/dist /dist + +RUN . $VENV_PATH/bin/activate \ + && pip install --require-hashes -r /dist/requirements.txt --find-links /dist + +############################################### +# Production Image +############################################### +FROM python-base AS production + +ENV PRODUCTION=true +ENV TESTING=false + +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + curl \ + ffmpeg \ + gosu \ + iproute2 \ + libldap-common \ + libldap2 \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /run/secrets + +COPY --from=venv-builder $VENV_PATH $VENV_PATH + +ENV NLTK_DATA="/nltk_data/" +RUN mkdir -p $NLTK_DATA +RUN python -m nltk.downloader -d $NLTK_DATA averaged_perceptron_tagger_eng + +VOLUME ["$MEALIE_HOME/data/"] +ENV APP_PORT=9000 + +EXPOSE ${APP_PORT} + +COPY --from=backend-builder /src/docker/healthcheck.sh $MEALIE_HOME/healthcheck.sh +RUN chmod +x $MEALIE_HOME/healthcheck.sh +HEALTHCHECK CMD $MEALIE_HOME/healthcheck.sh + +ENV HOST=0.0.0.0 + +COPY --from=backend-builder /src/docker/entry.sh $MEALIE_HOME/run.sh +RUN chmod +x $MEALIE_HOME/run.sh + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Mealie" +LABEL org.opencontainers.image.description="Self-hosted recipe manager" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +ENTRYPOINT ["/app/run.sh"] diff --git a/containers/mealie/default.nix b/containers/mealie/default.nix deleted file mode 100644 index e55efe3..0000000 --- a/containers/mealie/default.nix +++ /dev/null @@ -1,69 +0,0 @@ -# Nix-built Mealie for ringtail (amd64). -# -# Replaces the from-source Dockerfile build (Node frontend + Python venv) -# with nixpkgs' mealie, which ships a single `mealie` gunicorn entrypoint -# serving the prebuilt frontend + backend — so this is a clean single- -# process wrap (unlike paperless, which is multi-process). -# -# Mealie stores its DB as SQLite under DATA_DIR (the mealie-data PVC at -# /app/data); there is no postgres. The run wrapper mirrors the nixpkgs -# mealie NixOS module: run `libexec/init_db` (Alembic migrations) first, -# then exec gunicorn. -# -# Self-pins nixos-unstable: stable nixpkgs lags at 3.9.2, unstable carries -# 3.16.0. This is a forward 4-minor bump from the v3.12.0 Dockerfile build -# (the deferred upgrade) — mealie auto-migrates the SQLite DB forward on -# startup via init_db; the source PVC is retained for rollback. The version -# assertion makes nix-build fail if a pin bump changes the version. -let - nixpkgs = fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/331800de5053fcebacf6813adb5db9c9dca22a0c.tar.gz"; - sha256 = "1p54fm6dkbq62kpi55cr4wyx7b1nsajpsnjgs64cmp073fwi15f7"; - }; - pkgs = import nixpkgs { system = "x86_64-linux"; }; - - version = "3.16.0"; - - app = pkgs.mealie; - - # Mirror the NixOS module's mealie service: init_db (Alembic) then - # gunicorn bound to the app port. DATA_DIR/env come from the image + - # k8s manifest. - mealie-run = pkgs.writeShellScriptBin "mealie-run" '' - set -e - ${app}/libexec/init_db - exec ${pkgs.lib.getExe app} -b 0.0.0.0:9000 - ''; -in - -assert app.version == version; - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/mealie"; - - contents = [ - app - mealie-run - pkgs.bashInteractive - pkgs.coreutils - pkgs.cacert - pkgs.tzdata - # python3 (stdlib sqlite3) for the borgmatic k8s-sqlite-dump helper, - # which runs `python3 -c "...sqlite3...backup..."` inside the pod. - # Same nixpkgs python mealie is built against, so ~no added closure. - pkgs.python3 - ]; - - config = { - Cmd = [ "${mealie-run}/bin/mealie-run" ]; - Env = [ - "DATA_DIR=/app/data" - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "PYTHONUNBUFFERED=1" - "PRODUCTION=true" - ]; - ExposedPorts = { - "9000/tcp" = { }; - }; - }; -} diff --git a/containers/miniflux/Dockerfile b/containers/miniflux/Dockerfile new file mode 100644 index 0000000..4e987cc --- /dev/null +++ b/containers/miniflux/Dockerfile @@ -0,0 +1,35 @@ +# Miniflux RSS feed reader +# Based on upstream packaging/docker/alpine/Dockerfile + +ARG CONTAINER_APP_VERSION=2.2.17 +ARG MINIFLUX_VERSION=${CONTAINER_APP_VERSION} + +FROM golang:alpine3.22 AS build + +ARG MINIFLUX_VERSION +RUN apk add --no-cache build-base git make + +# Clone specific version +RUN git clone --depth 1 --branch ${MINIFLUX_VERSION} \ + https://forge.ops.eblu.me/mirrors/miniflux.git /go/src/app + +WORKDIR /go/src/app +RUN make miniflux + +FROM alpine:3.22 + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Miniflux" +LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +EXPOSE 8080 +ENV LISTEN_ADDR=0.0.0.0:8080 + +RUN apk --no-cache add ca-certificates tzdata +COPY --from=build /go/src/app/miniflux /usr/bin/miniflux + +USER 65534 +CMD ["/usr/bin/miniflux"] diff --git a/containers/miniflux/container.py b/containers/miniflux/container.py deleted file mode 100644 index e25485c..0000000 --- a/containers/miniflux/container.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Miniflux RSS feed reader — native Dagger build. - -Two-stage build: Go (backend with PIE), Alpine (runtime). -Source cloned from forge mirror. -""" - -import dagger - -from blumeops.containers import ( - alpine_runtime, - clone_from_forge, - go_build, - oci_labels, -) - -VERSION = "2.2.19" - - -async def build(src: dagger.Directory) -> dagger.Container: - source = clone_from_forge("miniflux", VERSION) - - # Stage 1: Build Go backend (PIE mode, matching upstream Makefile) - backend = go_build( - source, - "/miniflux", - buildmode="pie", - ldflags=f"-s -w -X 'miniflux.app/v2/internal/version.Version={VERSION}'", - cgo_enabled=True, - ) - - # Stage 2: Runtime (uses Alpine's built-in nobody:65534) - runtime = alpine_runtime( - extra_apk=["ca-certificates", "tzdata"], - create_user=False, - ) - runtime = oci_labels( - runtime, - title="Miniflux", - description="Miniflux is a minimalist and opinionated feed reader", - version=VERSION, - ) - return ( - runtime.with_file("/usr/bin/miniflux", backend.file("/miniflux")) - .with_exposed_port(8080) - .with_env_variable("LISTEN_ADDR", "0.0.0.0:8080") - .with_user("65534") - .with_default_args(args=["/usr/bin/miniflux"]) - ) diff --git a/containers/navidrome/Dockerfile b/containers/navidrome/Dockerfile new file mode 100644 index 0000000..7f78d36 --- /dev/null +++ b/containers/navidrome/Dockerfile @@ -0,0 +1,57 @@ +# Navidrome music server +# Three-stage build: UI (Node), backend (Go+taglib), runtime (Alpine) + +ARG CONTAINER_APP_VERSION=v0.60.3 +ARG NAVIDROME_VERSION=${CONTAINER_APP_VERSION} + +FROM node:22-alpine AS ui-build + +ARG NAVIDROME_VERSION +RUN apk add --no-cache git + +RUN git clone --depth 1 --branch ${NAVIDROME_VERSION} \ + https://forge.ops.eblu.me/mirrors/navidrome.git /app + +WORKDIR /app/ui +RUN npm ci +RUN npm run build + +FROM golang:alpine3.22 AS build + +ARG NAVIDROME_VERSION +RUN apk add --no-cache build-base git taglib-dev zlib-dev + +RUN git clone --depth 1 --branch ${NAVIDROME_VERSION} \ + https://forge.ops.eblu.me/mirrors/navidrome.git /app + +WORKDIR /app + +# Copy pre-built UI assets +COPY --from=ui-build /app/ui/build /app/ui/build + +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS_ALLOW="--define-prefix" + +RUN go build -tags=netgo \ + -ldflags="-w -s -X github.com/navidrome/navidrome/consts.gitTag=${NAVIDROME_VERSION}" \ + -o /navidrome . + +FROM alpine:3.22 + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Navidrome" +LABEL org.opencontainers.image.description="Navidrome is a self-hosted music server and streamer" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +RUN apk add --no-cache ca-certificates tzdata taglib ffmpeg \ + && addgroup -g 1000 navidrome \ + && adduser -u 1000 -G navidrome -D navidrome + +COPY --from=build /navidrome /usr/bin/navidrome + +EXPOSE 4533 + +USER 1000 +CMD ["/usr/bin/navidrome"] diff --git a/containers/navidrome/container.py b/containers/navidrome/container.py deleted file mode 100644 index a50a71f..0000000 --- a/containers/navidrome/container.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Navidrome music server — native Dagger build. - -Three-stage build: Node (UI), Go (backend with taglib + FTS5), Alpine (runtime). -Source cloned from forge mirror. -""" - -import dagger - -from blumeops.containers import ( - alpine_runtime, - clone_from_forge, - go_build, - node_build, - oci_labels, -) - -VERSION = "v0.61.1" - - -async def build(src: dagger.Directory) -> dagger.Container: - source = clone_from_forge("navidrome", VERSION) - - # Stage 1: Build UI assets - ui = node_build(source, "ui") - - # Stage 2: Build Go backend with CGO (taglib) and FTS5 - backend = go_build( - source.with_directory("ui/build", ui.directory("/app/ui/build")), - "/navidrome", - tags="netgo,sqlite_fts5", - ldflags=f"-w -s -X github.com/navidrome/navidrome/consts.gitTag={VERSION}", - cgo_enabled=True, - extra_apk=["taglib-dev", "zlib-dev"], - ) - - # Stage 3: Runtime - runtime = alpine_runtime( - extra_apk=["ca-certificates", "tzdata", "taglib", "ffmpeg"], - uid=1000, - gid=1000, - username="navidrome", - ) - runtime = oci_labels( - runtime, - title="Navidrome", - description="Navidrome is a self-hosted music server and streamer", - version=VERSION, - ) - return ( - runtime.with_file("/usr/bin/navidrome", backend.file("/navidrome")) - .with_exposed_port(4533) - .with_user("1000") - .with_default_args(args=["/usr/bin/navidrome"]) - ) diff --git a/containers/paperless/default.nix b/containers/paperless/default.nix deleted file mode 100644 index 734d909..0000000 --- a/containers/paperless/default.nix +++ /dev/null @@ -1,77 +0,0 @@ -# Nix-built Paperless-ngx for ringtail (amd64). -# -# Replaces the from-source Dockerfile build (s6-overlay) with nixpkgs' -# paperless-ngx, which already bundles the full OCR/imaging closure -# (tesseract, ghostscript, imagemagick, qpdf, poppler, jbig2enc) and the -# NLTK data via wrappers — so the image stays lean. -# -# Unlike the upstream s6 image, this image does NOT run all processes -# itself. Paperless is multi-process; on ringtail it runs as four -# containers sharing this one image, each with a different command: -# web -> paperless-web (granian, the wrapper below) -# worker -> celery --app paperless worker -# beat -> celery --app paperless beat -# consumer -> paperless-ngx document_consumer -# plus a redis/valkey sidecar. The PYTHONPATH/granian invocation mirrors -# the nixpkgs paperless NixOS module's paperless-web service exactly. -# -# Self-pins nixos-unstable: stable nixpkgs lags at 2.19.6, while unstable -# carries 2.20.15 — a same-minor forward patch bump from the previous -# Dockerfile build (v2.20.13). The version assertion makes nix-build fail -# if a pin bump changes the version, forcing an explicit acknowledgment -# here and in service-versions.yaml (enforced by container-version-check). -let - nixpkgs = fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/331800de5053fcebacf6813adb5db9c9dca22a0c.tar.gz"; - sha256 = "1p54fm6dkbq62kpi55cr4wyx7b1nsajpsnjgs64cmp073fwi15f7"; - }; - pkgs = import nixpkgs { system = "x86_64-linux"; }; - - version = "2.20.15"; - - app = pkgs.paperless-ngx; - - # Mirror the NixOS module's paperless-web service: granian serving the - # ASGI app with the package's propagated deps + src on PYTHONPATH. - pythonPath = - "${app.python.pkgs.makePythonPath app.propagatedBuildInputs}:${app}/lib/paperless-ngx/src"; - - paperless-web = pkgs.writeShellScriptBin "paperless-web" '' - export PYTHONPATH="${pythonPath}" - export PAPERLESS_NLTK_DIR="${app.nltkDataDir}" - exec ${app.python.pkgs.granian}/bin/granian \ - --interface asginl --ws \ - --host 0.0.0.0 --port 8000 \ - "paperless.asgi:application" - ''; -in - -assert app.version == version; - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/paperless"; - - contents = [ - app - paperless-web - pkgs.bashInteractive - pkgs.coreutils - pkgs.cacert - pkgs.tzdata - ]; - - config = { - # Default command is the web server; worker/beat/consumer containers - # override `command` in their k8s manifests. - Cmd = [ "${paperless-web}/bin/paperless-web" ]; - Env = [ - "PAPERLESS_NLTK_DIR=${app.nltkDataDir}" - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "PYTHONUNBUFFERED=1" - "PNGX_CONTAINERIZED=1" - ]; - ExposedPorts = { - "8000/tcp" = { }; - }; - }; -} diff --git a/containers/prowler/Dockerfile b/containers/prowler/Dockerfile index c5157cb..7cafd17 100644 --- a/containers/prowler/Dockerfile +++ b/containers/prowler/Dockerfile @@ -1,7 +1,7 @@ # Prowler CIS scanner — slim build for Kubernetes, image, and IaC providers # Strips PowerShell (M365) and dashboard dependencies from upstream # Includes Trivy for image vulnerability and IaC scanning -ARG CONTAINER_APP_VERSION=5.23.0 +ARG CONTAINER_APP_VERSION=5.22.0 FROM python:3.12-slim-bookworm AS build @@ -44,28 +44,10 @@ RUN ARCH=$(dpkg --print-architecture) \ && apt-get update && apt-get install -y --no-install-recommends wget ca-certificates \ && wget -q "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_${TRIVY_ARCH}.tar.gz" -O /tmp/trivy.tar.gz \ && tar xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy \ - && mv /usr/local/bin/trivy /usr/local/bin/trivy.real \ - && chmod +x /usr/local/bin/trivy.real \ + && chmod +x /usr/local/bin/trivy \ && rm /tmp/trivy.tar.gz \ && apt-get purge -y wget && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* -# Shim: Prowler's IaC provider invokes `trivy fs` directly with no -# --ignorefile flag, so any TRIVY_IGNOREFILE the user sets is ignored. -# This wrapper injects --ignorefile when the env var points at a real -# file and the invocation is `trivy fs ...`. Other subcommands and -# global-only invocations (--version, --help) pass through unchanged. -# TODO(upstream): contribute --ignorefile plumbing to prowler-cloud/prowler -# iac_provider.py so this shim isn't necessary. -RUN printf '%s\n' \ - '#!/bin/sh' \ - 'if [ "${1:-}" = "fs" ] && [ -n "${TRIVY_IGNOREFILE:-}" ] && [ -f "${TRIVY_IGNOREFILE}" ]; then' \ - ' shift' \ - ' exec /usr/local/bin/trivy.real fs --ignorefile "${TRIVY_IGNOREFILE}" "$@"' \ - 'fi' \ - 'exec /usr/local/bin/trivy.real "$@"' \ - > /usr/local/bin/trivy \ - && chmod +x /usr/local/bin/trivy - RUN addgroup --gid 1000 prowler \ && adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler \ && mkdir -p /tmp/.cache/trivy && chown prowler:prowler /tmp/.cache/trivy diff --git a/containers/quartz/Dockerfile b/containers/quartz/Dockerfile new file mode 100644 index 0000000..8ffd44c --- /dev/null +++ b/containers/quartz/Dockerfile @@ -0,0 +1,31 @@ +# Quartz Static Site Server +# Downloads and serves a Quartz-built static site from a release bundle +# +# Configuration (via environment): +# DOCS_RELEASE_URL - URL to download the static site tarball +# +# The container downloads the tarball on startup, extracts it, and serves with nginx. + +ARG CONTAINER_APP_VERSION=1.28.2 +ARG NGINX_VERSION=${CONTAINER_APP_VERSION} + +FROM nginx:${NGINX_VERSION}-alpine + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Quartz" +LABEL org.opencontainers.image.description="Static site server for Quartz-built documentation" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +# Install curl for downloading release assets +RUN apk add --no-cache curl + +# Copy startup script and nginx config +COPY start.sh /start.sh +COPY default.conf /etc/nginx/conf.d/default.conf +RUN chmod +x /start.sh + +EXPOSE 80 + +CMD ["/start.sh"] diff --git a/containers/quartz/default.conf b/containers/quartz/default.conf new file mode 100644 index 0000000..64eec4e --- /dev/null +++ b/containers/quartz/default.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Static file serving — no SPA fallback. + # Quartz generates complete HTML for every page, so all valid URLs + # map to real files. Non-existent paths get 404.html (generated by + # Quartz's NotFoundPage plugin), preventing the spider-trap issue + # where crawlers would get index.html for fabricated URLs. + location / { + try_files $uri $uri/ $uri.html =404; + } + + error_page 404 /404.html; + + # Health check endpoint + location /healthz { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } +} diff --git a/containers/quartz/start.sh b/containers/quartz/start.sh new file mode 100644 index 0000000..778eeb1 --- /dev/null +++ b/containers/quartz/start.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +HTML_DIR="/usr/share/nginx/html" + +# Check for required environment variable +if [ -z "$DOCS_RELEASE_URL" ]; then + echo "Error: DOCS_RELEASE_URL environment variable is required" + echo "Set it to the URL of the static site tarball to serve" + exit 1 +fi + +echo "Downloading docs from: $DOCS_RELEASE_URL" + +# Download the tarball +if ! curl -fsSL "$DOCS_RELEASE_URL" -o /tmp/docs.tar.gz; then + echo "Error: Failed to download docs from $DOCS_RELEASE_URL" + exit 1 +fi + +# Clear existing content and extract +rm -rf "${HTML_DIR:?}"/* +echo "Extracting docs to $HTML_DIR" +tar -xzf /tmp/docs.tar.gz -C "$HTML_DIR" +rm /tmp/docs.tar.gz + +echo "Docs extracted successfully" +echo "Starting nginx..." + +# Start nginx in foreground +exec nginx -g "daemon off;" diff --git a/containers/runner-job-image/Dockerfile b/containers/runner-job-image/Dockerfile new file mode 100644 index 0000000..0018c64 --- /dev/null +++ b/containers/runner-job-image/Dockerfile @@ -0,0 +1,84 @@ +# Forgejo Actions Job Execution Image +# +# This image is used as the job execution environment for Forgejo Actions. +# The host runner daemon creates containers from this image to run workflow steps. +# +# Build logic (container images, docs site) runs inside Dagger containers, +# so this image only needs: git, Docker CLI, Dagger CLI, ArgoCD CLI, uv, yq, and basic tools. +# +# Usage: Configure runner with label like: +# docker:docker://registry.ops.eblu.me/blumeops/runner-job-image:latest + +ARG CONTAINER_APP_VERSION=0.20.1 + +FROM debian:bookworm-slim + +ARG TARGETARCH +ARG CONTAINER_APP_VERSION +ARG DAGGER_VERSION=${CONTAINER_APP_VERSION} + +LABEL org.opencontainers.image.title="Runner Job Image" +LABEL org.opencontainers.image.description="Forgejo Actions job execution environment" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +# Install base dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + gnupg \ + jq \ + tzdata \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js (required by actions/checkout and other JavaScript Actions) +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && node --version + +# Install Docker CLI (Dagger shells out to `docker` to provision its engine) +RUN install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \ + && chmod a+r /etc/apt/keyrings/docker.asc \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# Install uv (Python package runner for towncrier) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ + && mv /root/.local/bin/uv /usr/local/bin/uv \ + && mv /root/.local/bin/uvx /usr/local/bin/uvx + +# Install argocd CLI (for syncing apps from workflows) +RUN ARCH="${TARGETARCH:-$(dpkg --print-architecture)}" \ + && curl -fsSL -o /usr/local/bin/argocd \ + "https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-${ARCH}" \ + && chmod +x /usr/local/bin/argocd \ + && argocd version --client + +# Install Dagger CLI (for running Dagger CI pipelines) +RUN ARCH="${TARGETARCH:-$(dpkg --print-architecture)}" \ + && curl -fsSL -o /tmp/dagger.tar.gz \ + "https://dl.dagger.io/dagger/releases/${DAGGER_VERSION}/dagger_v${DAGGER_VERSION}_linux_${ARCH}.tar.gz" \ + && tar -xzf /tmp/dagger.tar.gz -C /usr/local/bin dagger \ + && rm /tmp/dagger.tar.gz \ + && dagger version + +# Install yq (for editing YAML files in workflows) +RUN ARCH="${TARGETARCH:-$(dpkg --print-architecture)}" \ + && curl -fsSL -o /usr/local/bin/yq \ + "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${ARCH}" \ + && chmod +x /usr/local/bin/yq \ + && yq --version + +# Install flyctl (for Fly.io cache purge after docs deploy) +RUN curl -L https://fly.io/install.sh | sh \ + && mv /root/.fly/bin/flyctl /usr/local/bin/fly \ + && rm -rf /root/.fly + +# Default to bash +CMD ["/bin/bash"] diff --git a/containers/runner-job-image/container.py b/containers/runner-job-image/container.py deleted file mode 100644 index c5710ff..0000000 --- a/containers/runner-job-image/container.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Forgejo Actions job execution image — native Dagger build. - -The forgejo-runner daemon creates containers from this image to run -workflow steps. Contains the tools workflows reach for: git, Docker CLI, -Node.js (for JavaScript Actions), Dagger CLI, ArgoCD CLI, uv, yq, flyctl. - -VERSION tracks the Dagger CLI version, the primary build tool. -""" - -import dagger - -from blumeops.containers import alpine_runtime, oci_labels - -VERSION = "0.20.6" - - -async def build(src: dagger.Directory) -> dagger.Container: - # Map `uname -m` to the arch suffix each upstream uses. - arch_setup = ( - 'ARCH_UNAME="$(uname -m)"; ' - 'case "$ARCH_UNAME" in ' - " x86_64) ARCH=amd64 ;; " - " aarch64) ARCH=arm64 ;; " - ' *) echo "unsupported arch: $ARCH_UNAME" >&2; exit 1 ;; ' - "esac; " - ) - - runtime = alpine_runtime( - extra_apk=[ - "bash", - "ca-certificates", - "curl", - "docker-cli", - "git", - "gnupg", - "jq", - "nodejs", - "npm", - "tzdata", - ], - create_user=False, - ) - runtime = oci_labels( - runtime, - title="Runner Job Image", - description="Forgejo Actions job execution environment", - version=VERSION, - ) - - install_tools = ( - arch_setup - + "set -eux; " - # Dagger CLI (pinned) - + f'curl -fsSL -o /tmp/dagger.tar.gz "https://dl.dagger.io/dagger/releases/{VERSION}/dagger_v{VERSION}_linux_${{ARCH}}.tar.gz"; ' - + "tar -xzf /tmp/dagger.tar.gz -C /usr/local/bin dagger; " - + "rm /tmp/dagger.tar.gz; " - + "dagger version; " - # ArgoCD CLI (latest — matches cluster server version over time) - + 'curl -fsSL -o /usr/local/bin/argocd "https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-${ARCH}"; ' - + "chmod +x /usr/local/bin/argocd; " - + "argocd version --client; " - # yq (latest) - + 'curl -fsSL -o /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${ARCH}"; ' - + "chmod +x /usr/local/bin/yq; " - + "yq --version; " - # uv / uvx (latest; musl target auto-selected by installer) - + "curl -LsSf https://astral.sh/uv/install.sh " - + '| env UV_INSTALL_DIR=/usr/local/bin UV_UNMANAGED_INSTALL="/usr/local/bin" sh; ' - + "uv --version; " - # flyctl (latest) - + "curl -L https://fly.io/install.sh | sh; " - + "mv /root/.fly/bin/flyctl /usr/local/bin/fly; " - + "rm -rf /root/.fly; " - + "fly version" - ) - - return runtime.with_exec(["sh", "-c", install_tools]).with_default_args( - args=["/bin/bash"] - ) diff --git a/containers/shower/default.nix b/containers/shower/default.nix deleted file mode 100644 index c5bd41e..0000000 --- a/containers/shower/default.nix +++ /dev/null @@ -1,278 +0,0 @@ -# Nix-built shower app container — Adelaide / Heidi / Addie baby shower. -# -# The app is published as a wheel to the Forgejo PyPI index at -# https://forge.ops.eblu.me/api/packages/eblume/pypi/ (tailnet-only — the -# public forge.eblu.me /api/packages/* surface is blocked at the Fly edge). -# We can't point pip at Forgejo's simple index even from the tailnet, -# because Forgejo's index returns absolute file URLs hardcoded to its -# public ROOT_URL (forge.eblu.me), which then 403s. So both the wheel and -# the sdist are pulled by direct `fetchurl` against forge.ops.eblu.me, and -# the wheel is then handed to `pip install` as a local path; transitive -# deps come from pypi.ops.eblu.me. Build runs on the nix-container-builder -# runner (ringtail, amd64) so the image is native. -# -# Going through pip-install-target rather than nixpkgs Python packages -# sidesteps two issues we hit going through `python.pkgs.buildPythonPackage`: -# 1. python314Packages.django still aliases to Django 4.2 LTS, which -# doesn't support Python 3.14 at all. -# 2. django-axes pulls selenium + browser fonts into its check phase -# and the nix sandbox can't provide those. -# -# To bump the version: -# 1. Update `version` below. -# 2. Set `outputHash` to `pkgs.lib.fakeHash`, run the build, copy the -# real hash out of the error, and commit it. -{ pkgs ? import { } }: - -let - version = "1.1.3"; - - python = pkgs.python314; - - # The repo's top-level static/ directory (vendored Sortable + cropper - # JS/CSS, prize placeholder SVG) isn't shipped in the wheel — hatchling - # only packages config/ and shower/, leaving the repo-root static/ - # behind. Pull the sdist (which contains the full source tree) and - # extract just the static/ subtree into the image as /app/static. - # local_settings adds it to STATICFILES_DIRS so collectstatic at boot - # picks it up alongside the Django admin's static files. - # - # Fetched from forge.ops.eblu.me (tailnet) because /api/packages/* is - # blocked at the fly edge — see fly/nginx.conf forge.eblu.me block. - # Hash is the upstream sha256 from forge PyPI's simple index. - showerSdist = pkgs.fetchurl { - name = "adelaide_baby_shower_app-${version}.tar.gz"; - url = "https://forge.ops.eblu.me/api/packages/eblume/pypi/files/adelaide-baby-shower-app/${version}/adelaide_baby_shower_app-${version}.tar.gz"; - hash = "sha256-a3rCwEdOB+rnYXqsWDifyltpyKUgkOj0ikWB+WGQYKE="; - }; - - # Wheel pulled from forge.ops.eblu.me (tailnet) for the same reason the - # sdist is: Forgejo's PyPI simple index would return forge.eblu.me URLs - # that the Fly edge 403s on /api/packages/*. We hand this path to pip - # below so it never touches the forge index at all. - showerWheel = pkgs.fetchurl { - name = "adelaide_baby_shower_app-${version}-py3-none-any.whl"; - url = "https://forge.ops.eblu.me/api/packages/eblume/pypi/files/adelaide-baby-shower-app/${version}/adelaide_baby_shower_app-${version}-py3-none-any.whl"; - hash = "sha256-a6j91gBigG4IzE2DVTBntnZ46Yrx9b5PgHn+Uro98Tk="; - }; - - staticAssets = pkgs.runCommand "shower-static-assets-${version}" { } '' - ${pkgs.gnutar}/bin/tar -xzf ${showerSdist} -C $TMPDIR - cp -r $TMPDIR/adelaide_baby_shower_app-${version}/static $out - ''; - - # Fixed-output derivation: pip-installs the app wheel + every transitive - # dep into a single target dir. FODs get network access in exchange for - # a pinned output hash, which means the whole dependency closure is - # immutable across rebuilds. - pyDepsFOD = pkgs.stdenv.mkDerivation { - pname = "shower-python-deps-fod"; - inherit version; - - dontUnpack = true; - - nativeBuildInputs = [ python pkgs.cacert pkgs.removeReferencesTo ]; - - buildPhase = '' - runHook preBuild - - export HOME=$TMPDIR - export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt - export PIP_DISABLE_PIP_VERSION_CHECK=1 - - ${python}/bin/python -m venv "$TMPDIR/venv" - "$TMPDIR/venv/bin/pip" install --upgrade pip - - # Nix store paths embed a 32-char hash prefix, which pip's wheel - # filename parser rejects ("Invalid wheel filename"). Copy to a - # clean filename in TMPDIR before installing. - cp ${showerWheel} "$TMPDIR/${showerWheel.name}" - - "$TMPDIR/venv/bin/pip" install \ - --no-cache-dir \ - --index-url=https://pypi.ops.eblu.me/root/pypi/+simple/ \ - "$TMPDIR/${showerWheel.name}" \ - gunicorn - - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - - mkdir -p $out/lib/python3.14 $out/bin - cp -r "$TMPDIR/venv/lib/python3.14/site-packages" $out/lib/python3.14/site-packages - - for script in "$TMPDIR/venv/bin/"*; do - [ -f "$script" ] || continue - name=$(basename "$script") - case "$name" in - python*|pip*|activate*) continue ;; - esac - cp "$script" "$out/bin/$name" - chmod +x "$out/bin/$name" - done - - # --- Strip Nix store references (FOD outputs must be self-contained) --- - # The wrapper derivation below restores them via autoPatchelfHook + a - # python wrapper that points pyc-less imports at the on-image python. - - # Strip bytecode entirely — pyc files embed compile-time paths. - find $out -type f -name '*.pyc' -delete - find $out -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true - - # Dynamically discover all nix store references and strip them. We - # don't have a static list because pip pulls in stdenv via Python's - # build env (gcc-lib, libstdc++, etc.) and the closure is opaque. - { find $out -type f -print0 \ - | xargs -0 grep -aohE '/nix/store/[a-z0-9]{32}-[^/"[:space:]]+' 2>/dev/null \ - || true; } | sort -u > $TMPDIR/store-refs.txt - echo "Found $(wc -l < $TMPDIR/store-refs.txt) unique store path references to strip" - - refs_args="" - while IFS= read -r ref; do - refs_args="$refs_args -t $ref" - done < $TMPDIR/store-refs.txt - - if [ -n "$refs_args" ]; then - find $out -type f -exec remove-references-to $refs_args {} + 2>/dev/null || true - fi - - remaining=$({ find $out -type f -print0 | xargs -0 grep -cl '/nix/store/' 2>/dev/null || true; } | wc -l) - echo "Files with remaining store references: $remaining" - - runHook postInstall - ''; - - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - # Pinned dep closure — reproducible until version bumps. To recompute, - # set to pkgs.lib.fakeHash and read the failure. - outputHash = "sha256-1xx2qWAIwherklHIPXo6IOKkKHML1KUrUx6pbkMxffc="; - - dontFixup = true; - }; - - # Non-FOD wrapper: re-applies RPATHs to pre-built .so files (pillow, - # scipy) so they find libstdc++ / libz / etc. at runtime. autoPatchelfHook - # discovers needed libraries from buildInputs. - pyDeps = pkgs.stdenv.mkDerivation { - pname = "shower-python-deps"; - inherit version; - - dontUnpack = true; - - nativeBuildInputs = [ pkgs.autoPatchelfHook ]; - - buildInputs = with pkgs; [ - python - stdenv.cc.cc.lib # libstdc++, libgcc_s - zlib - libjpeg - libwebp - libtiff - openjpeg - lcms2 - freetype - ]; - - installPhase = '' - cp -r ${pyDepsFOD} $out - chmod -R u+w $out - ''; - }; - - sitePackages = "${pyDeps}/lib/python3.14/site-packages"; - - # Settings shim — config/settings.py's `BASE_DIR = parent.parent` would - # otherwise resolve to site-packages, scattering db.sqlite3 / media / - # staticfiles into the venv. Pin them to /app/{data,media,data/staticfiles}. - localSettings = pkgs.writeText "local_settings.py" '' - from pathlib import Path - - from config.settings import * # noqa: F401,F403 - - DATABASES["default"]["NAME"] = "/app/data/db.sqlite3" - MEDIA_ROOT = "/app/media" - STATIC_ROOT = "/app/data/staticfiles" - # /app/static comes from the repo-root static/ subtree of the sdist - # (see default.nix staticAssets). Added because the wheel doesn't - # ship vendored Sortable/cropper assets. - STATICFILES_DIRS = [Path("/app/static")] - ''; - - # PYTHONPATH, DJANGO_SETTINGS_MODULE, PATH, and HOME live in the image's - # `Env` block below — that way `kubectl exec deploy/shower -- python -m - # django ` Just Works without an inline `env` ceremony. - # The entrypoint just changes directory and runs the boot sequence. - entrypoint = pkgs.writeShellScript "shower-entrypoint" '' - set -eu - - cd /app - - mkdir -p /app/data /app/media - - echo "shower: running migrations" - python -m django migrate --noinput - - echo "shower: collecting static files" - python -m django collectstatic --noinput --clear - - echo "shower: starting gunicorn" - exec gunicorn \ - --bind 0.0.0.0:8000 \ - --workers 2 \ - --forwarded-allow-ips='*' \ - config.wsgi:application - ''; -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/shower"; - contents = [ - python - pyDeps - pkgs.cacert - pkgs.tzdata - pkgs.bashInteractive - pkgs.coreutils - ]; - - extraCommands = '' - mkdir -p app/data app/media tmp - chmod 1777 tmp - cp ${localSettings} app/local_settings.py - cp -r ${staticAssets} app/static - chmod -R u+w app/static - ''; - - fakeRootCommands = '' - chown -R 1000:1000 app - ''; - enableFakechroot = true; - - config = { - Entrypoint = [ "${entrypoint}" ]; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - "TZ=America/Los_Angeles" - "TMPDIR=/tmp" - "LANG=C.UTF-8" - "LC_ALL=C.UTF-8" - "PYTHONDONTWRITEBYTECODE=1" - "HOME=/app/data" - "PATH=${pyDeps}/bin:${python}/bin:/bin" - # /app first so local_settings.py is importable; sitePackages second so - # django, gunicorn, etc. resolve. Inherited by entrypoint + any - # `kubectl exec` so manual django subcommands work without ceremony. - "PYTHONPATH=/app:${sitePackages}" - "DJANGO_SETTINGS_MODULE=local_settings" - ]; - ExposedPorts = { - "8000/tcp" = { }; - }; - User = "1000"; - WorkingDir = "/app"; - }; -} diff --git a/containers/tailscale/default.nix b/containers/tailscale/default.nix deleted file mode 100644 index 8e87f76..0000000 --- a/containers/tailscale/default.nix +++ /dev/null @@ -1,77 +0,0 @@ -# Nix-built tailscale container for ringtail's tailscale-operator ProxyClass -# Builds v1.94.2 from forge mirror; mirrors upstream Dockerfile contents. -# Built with dockerTools.buildLayeredImage on the ringtail nix-container-builder. -{ pkgs ? import { } }: - -let - version = "1.94.2"; - - src = pkgs.fetchgit { - url = "https://forge.ops.eblu.me/mirrors/tailscale.git"; - rev = "v${version}"; - hash = "sha256-qjWVB8xWVgIVUgrf27F6hwiFIE+4ERXWeHv26ugg/x4="; - }; - - tailscale = pkgs.buildGoModule { - inherit src version; - pname = "tailscale"; - vendorHash = "sha256-WeMTOkERj4hvdg4yPaZ1gRgKnhRIBXX55kUVbX/k/xM="; - - subPackages = [ - "cmd/tailscale" - "cmd/tailscaled" - "cmd/containerboot" - ]; - - ldflags = [ - "-s" - "-w" - "-X tailscale.com/version.longStamp=${version}" - "-X tailscale.com/version.shortStamp=${version}" - ]; - - doCheck = false; - - meta = with pkgs.lib; { - description = "The easiest, most secure way to use WireGuard"; - homepage = "https://tailscale.com"; - license = licenses.bsd3; - }; - }; -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/tailscale"; - tag = "v${version}"; - - contents = [ - tailscale - pkgs.cacert - pkgs.iptables - pkgs.iproute2 - pkgs.tzdata - pkgs.busybox - ]; - - # Match upstream Dockerfile: symlink iptables-legacy over iptables. - # Synology NAS and similar hosts don't support nftables. - # Also recreate the /tailscale/run.sh compat symlink. - extraCommands = '' - rm -f usr/sbin/iptables usr/sbin/ip6tables - ln -s ${pkgs.iptables}/bin/iptables-legacy usr/sbin/iptables || true - ln -s ${pkgs.iptables}/bin/ip6tables-legacy usr/sbin/ip6tables || true - mkdir -p tailscale - ln -s /bin/containerboot tailscale/run.sh - mkdir -p tmp - chmod 1777 tmp - ''; - - config = { - Entrypoint = [ "/bin/containerboot" ]; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - "PATH=/bin:/usr/bin:/usr/sbin" - ]; - }; -} diff --git a/containers/tempo/Dockerfile b/containers/tempo/Dockerfile deleted file mode 100644 index aeca55e..0000000 --- a/containers/tempo/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# Grafana Tempo distributed tracing backend -# Two-stage build: Go binary, Alpine runtime - -ARG CONTAINER_APP_VERSION=2.10.3 -ARG TEMPO_VERSION=v${CONTAINER_APP_VERSION} - -FROM golang:alpine3.22 AS build - -ARG TEMPO_VERSION -RUN apk add --no-cache build-base git - -RUN git clone --depth 1 --branch ${TEMPO_VERSION} \ - https://forge.ops.eblu.me/mirrors/tempo.git /go/src/app - -WORKDIR /go/src/app -ENV CGO_ENABLED=0 - -RUN go build -mod vendor \ - -ldflags="-w -s \ - -X main.Version=${TEMPO_VERSION} \ - -X main.Branch=HEAD \ - -X main.Revision=blumeops-build" \ - -o /bin/tempo ./cmd/tempo - -FROM alpine:3.22 - -ARG CONTAINER_APP_VERSION -LABEL org.opencontainers.image.title="Tempo" -LABEL org.opencontainers.image.description="Grafana Tempo distributed tracing backend" -LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" -LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" -LABEL org.opencontainers.image.vendor="blumeops" - -RUN apk add --no-cache ca-certificates tzdata -RUN mkdir -p /var/tempo && chown 10001:10001 /var/tempo - -USER 10001 -COPY --from=build /bin/tempo /usr/bin/tempo -EXPOSE 3200 4317 4318 9095 -ENTRYPOINT ["/usr/bin/tempo"] diff --git a/containers/teslamate/Dockerfile b/containers/teslamate/Dockerfile new file mode 100644 index 0000000..70c6d71 --- /dev/null +++ b/containers/teslamate/Dockerfile @@ -0,0 +1,84 @@ +# TeslaMate - Tesla data logger +# Based on upstream Dockerfile + +ARG CONTAINER_APP_VERSION=v3.0.0 +ARG TESLAMATE_VERSION=${CONTAINER_APP_VERSION} + +FROM elixir:1.19.5-otp-26 AS builder + +ARG TESLAMATE_VERSION + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update \ + && apt-get install -y ca-certificates curl gnupg git zstd brotli \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && NODE_MAJOR=22 \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" \ + | tee /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install nodejs -y \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN mix local.rebar --force && \ + mix local.hex --force + +# Clone specific version +RUN git clone --depth 1 --branch ${TESLAMATE_VERSION} \ + https://forge.ops.eblu.me/mirrors/teslamate.git /opt/app + +ENV MIX_ENV=prod +WORKDIR /opt/app + +RUN mix deps.get --only $MIX_ENV +RUN mix deps.compile + +RUN npm ci --prefix ./assets --progress=false --no-audit --loglevel=error +RUN mix assets.deploy + +RUN mix compile +RUN SKIP_LOCALE_DOWNLOAD=true mix release --path /opt/built + +# Runtime image +FROM debian:trixie-slim AS app + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="TeslaMate" +LABEL org.opencontainers.image.description="Tesla data logger and visualization" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +ENV LANG=C.UTF-8 \ + SRTM_CACHE=/opt/app/.srtm_cache \ + HOME=/opt/app + +WORKDIR $HOME + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libodbc2 \ + libsctp1 \ + libssl3t64 \ + libstdc++6 \ + netcat-openbsd \ + tini \ + tzdata \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid 10001 --system nonroot \ + && useradd --uid 10000 --system --gid nonroot --home-dir /home/nonroot --shell /sbin/nologin nonroot \ + && chown -R nonroot:nonroot . + +COPY --chown=nonroot:nonroot --chmod=555 entrypoint.sh / +COPY --from=builder --chown=nonroot:nonroot /opt/built . +RUN mkdir $SRTM_CACHE + +USER nonroot:nonroot + +EXPOSE 4000 + +ENTRYPOINT ["tini", "--", "/bin/dash", "/entrypoint.sh"] +CMD ["bin/teslamate", "start"] diff --git a/containers/teslamate/default.nix b/containers/teslamate/default.nix deleted file mode 100644 index e126561..0000000 --- a/containers/teslamate/default.nix +++ /dev/null @@ -1,122 +0,0 @@ -# Nix-built TeslaMate for ringtail (amd64). -# -# Replaces the Dagger container.py (Elixir+Node builder -> Debian slim). -# TeslaMate is NOT in nixpkgs, so this is a from-scratch beamPackages -# mixRelease: an Elixir/Phoenix release with npm-built assets. -# -# Pinned to the same nixos-unstable rev as paperless/mealie for a -# consistent toolchain. The BEAM combo is pinned to erlang_27 + elixir_1_18 -# (teslamate requires elixir ~> 1.17; upstream's image uses OTP 26, so we -# stay off the default OTP 28 which elixir 1.18 does not target). -# -# Source comes from the forge mirror (supply-chain control), pinned by the -# v3.0.0 tag's commit so builtins.fetchGit needs no hash. -let - nixpkgs = fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/331800de5053fcebacf6813adb5db9c9dca22a0c.tar.gz"; - sha256 = "1p54fm6dkbq62kpi55cr4wyx7b1nsajpsnjgs64cmp073fwi15f7"; - }; - pkgs = import nixpkgs { system = "x86_64-linux"; }; - lib = pkgs.lib; - - version = "3.0.0"; - - beamPackages = pkgs.beam.packages.erlang_27; - elixir = beamPackages.elixir_1_18; - - src = builtins.fetchGit { - url = "https://forge.ops.eblu.me/mirrors/teslamate.git"; - ref = "refs/tags/v${version}"; - rev = "3281154d42330786a182c1bbe094ecda0b1c5578"; - }; - - # ex_cldr downloads locale JSON from GitHub at compile time, which the - # build sandbox blocks. teslamate's cldr.ex reads the data dir from the - # LOCALES env var; point it at the pre-fetched elixir-cldr data so no - # download is attempted (with SKIP_LOCALE_DOWNLOAD=true disabling the - # forced refresh). CLDR data version matches the compile-time errors. - cldrData = pkgs.fetchFromGitHub { - owner = "elixir-cldr"; - repo = "cldr"; - rev = "v2.46.0"; - sha256 = "1iwzk9dc754l72vpf8vsisdjncnjx26pz509552b6vnm49xbxyji"; - }; - - teslamate = beamPackages.mixRelease { - pname = "teslamate"; - inherit version src elixir; - - # Keep the build-generated Erlang cookie in the release. mixRelease - # strips it by default (expecting RELEASE_COOKIE at runtime), but the - # start script reads releases/COOKIE. teslamate is single-node (no - # distributed Erlang exposed), so a baked-in cookie is fine. - removeCookie = false; - - mixFodDeps = beamPackages.fetchMixDeps { - pname = "mix-deps-teslamate"; - inherit src version elixir; - hash = "sha256-DDrREiM1BIMgD2qFPTK8QyjOYlnfE3XlnaH/jk7G2go="; - }; - - # Frontend assets. esbuild + sass are devDeps and the esbuild platform - # binary is an optional dep, so npm ci must include both. We run npm ci - # here (not a separate derivation) because assets/package.json has - # file:../deps/phoenix references that only resolve once mixFodDeps has - # populated deps/. npmConfigHook wires up the offline cache from npmDeps; - # then `node scripts/build.js` (custom esbuild) + `mix phx.digest`. - nativeBuildInputs = [ pkgs.nodejs pkgs.npmHooks.npmConfigHook ]; - npmDeps = pkgs.fetchNpmDeps { - name = "teslamate-npm-deps"; - src = src + "/assets"; - hash = "sha256-XyiaUkT/c4rZnNxmxhVLb+vEXnc64A1hjOrnR5fhaEk="; - }; - npmRoot = "assets"; - - preBuild = '' - export SKIP_LOCALE_DOWNLOAD=true - export LOCALES=${cldrData}/priv/cldr - ( cd assets && npm ci --include=dev --include=optional && node scripts/build.js ) - mix phx.digest --no-deps-check - ''; - }; -in - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/teslamate"; - - contents = [ - teslamate - pkgs.bashInteractive - pkgs.coreutils - pkgs.dash - pkgs.netcat-openbsd - pkgs.cacert - pkgs.tzdata - ]; - - config = { - # Mirror entrypoint.sh: wait for postgres, run migrations, then start. - Entrypoint = [ - "${pkgs.dash}/bin/dash" - "-c" - '' - : "''${DATABASE_HOST:=127.0.0.1}" - : "''${DATABASE_PORT:=5432}" - while ! ${pkgs.netcat-openbsd}/bin/nc -z "$DATABASE_HOST" "$DATABASE_PORT" 2>/dev/null; do - echo "waiting for postgres at $DATABASE_HOST:$DATABASE_PORT"; sleep 1 - done - ${teslamate}/bin/teslamate eval "TeslaMate.Release.migrate" - exec ${teslamate}/bin/teslamate start - '' - ]; - Env = [ - "HOME=/opt/app" - "SRTM_CACHE=/opt/app/.srtm_cache" - "LANG=C.UTF-8" - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - ]; - ExposedPorts = { - "4000/tcp" = { }; - }; - }; -} diff --git a/containers/teslamate/entrypoint.sh b/containers/teslamate/entrypoint.sh new file mode 100644 index 0000000..f66117e --- /dev/null +++ b/containers/teslamate/entrypoint.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env dash +set -e + +: "${DATABASE_HOST:="127.0.0.1"}" +: "${DATABASE_PORT:=5432}" +: "${ULIMIT_MAX_NOFILE:=65536}" + +# prevent memory bloat in some misconfigured versions of Docker/containerd +# where the nofiles limit is very large. 0 means don't set it. +if test "${ULIMIT_MAX_NOFILE}" != 0 && test "$(ulimit -n)" -gt "${ULIMIT_MAX_NOFILE}"; then + ulimit -n "${ULIMIT_MAX_NOFILE}" +fi + +# wait until Postgres is ready +while ! nc -z "${DATABASE_HOST}" "${DATABASE_PORT}" 2>/dev/null; do + echo waiting for postgres at "${DATABASE_HOST}":"${DATABASE_PORT}" + sleep 1s +done + +# apply migrations +bin/teslamate eval "TeslaMate.Release.migrate" + +exec "$@" diff --git a/containers/transmission-exporter/Dockerfile b/containers/transmission-exporter/Dockerfile new file mode 100644 index 0000000..c9d0655 --- /dev/null +++ b/containers/transmission-exporter/Dockerfile @@ -0,0 +1,25 @@ +# Transmission Prometheus exporter - collect-on-scrape, no polling loop +# uv run --script handles dependency resolution at runtime + +ARG CONTAINER_APP_VERSION=1.0.1 + +FROM python:3.13-alpine3.23 + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="Transmission Exporter" +LABEL org.opencontainers.image.description="Prometheus exporter for Transmission BitTorrent client" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +ENV PYTHONUNBUFFERED=1 +ENV UV_CACHE_DIR=/tmp/uv-cache + +WORKDIR /app +COPY exporter.py . + +EXPOSE 19091 +USER 65534:65534 +CMD ["uv", "run", "--script", "/app/exporter.py"] diff --git a/containers/transmission-exporter/container.py b/containers/transmission-exporter/container.py deleted file mode 100644 index e88fc70..0000000 --- a/containers/transmission-exporter/container.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Transmission Prometheus exporter — native Dagger build. - -Minimal collect-on-scrape exporter using uv to resolve deps at runtime. -""" - -import dagger -from dagger import dag - -from blumeops.containers import oci_labels - -VERSION = "1.0.1" - -PYTHON_BASE = "python:3.14-alpine3.23" -UV_IMAGE = "ghcr.io/astral-sh/uv:0.11.6" - - -async def build(src: dagger.Directory) -> dagger.Container: - ctr = ( - dag.container() - .from_(PYTHON_BASE) - .with_file("/usr/local/bin/uv", dag.container().from_(UV_IMAGE).file("/uv")) - .with_env_variable("PYTHONUNBUFFERED", "1") - .with_env_variable("UV_CACHE_DIR", "/tmp/uv-cache") - .with_workdir("/app") - .with_file( - "/app/exporter.py", src.file("containers/transmission-exporter/exporter.py") - ) - .with_exposed_port(19091) - .with_user("65534:65534") - .with_default_args(args=["uv", "run", "--script", "/app/exporter.py"]) - ) - return oci_labels( - ctr, - title="Transmission Exporter", - description="Prometheus exporter for Transmission BitTorrent client", - version=VERSION, - ) diff --git a/containers/transmission/Dockerfile b/containers/transmission/Dockerfile new file mode 100644 index 0000000..6d7bcab --- /dev/null +++ b/containers/transmission/Dockerfile @@ -0,0 +1,39 @@ +# Transmission BitTorrent daemon +# Simpler alternative to linuxserver image + +ARG CONTAINER_APP_VERSION=4.1.1-r1 + +FROM alpine:3.22 + +ARG CONTAINER_APP_VERSION +ARG TRANSMISSION_VERSION=${CONTAINER_APP_VERSION} + +LABEL org.opencontainers.image.title="Transmission" +LABEL org.opencontainers.image.description="Transmission BitTorrent daemon" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +# Transmission 4.1.x is only in edge; base OS stays on stable 3.22 +RUN apk add --no-cache \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \ + transmission-daemon=${TRANSMISSION_VERSION} \ + transmission-cli=${TRANSMISSION_VERSION} \ + transmission-remote=${TRANSMISSION_VERSION} \ + && apk add --no-cache \ + bash \ + curl \ + tzdata \ + su-exec + +# Create directories (user is created dynamically by start.sh based on PUID/PGID) +RUN mkdir -p /config /downloads/complete /downloads/incomplete + +COPY start.sh /start.sh +RUN chmod +x /start.sh + +EXPOSE 9091 51413/tcp 51413/udp + +VOLUME ["/config", "/downloads"] + +ENTRYPOINT ["/start.sh"] diff --git a/containers/transmission/container.py b/containers/transmission/container.py deleted file mode 100644 index c7989aa..0000000 --- a/containers/transmission/container.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Transmission BitTorrent daemon — native Dagger build. - -Alpine-based container with transmission-daemon from edge repo. -Includes start.sh for dynamic PUID/PGID user creation at runtime. -""" - -import dagger -from dagger import dag - -from blumeops.containers import oci_labels - -VERSION = "4.1.1-r1" - -ALPINE_BASE = "alpine:3.23" - - -async def build(src: dagger.Directory) -> dagger.Container: - ctr = ( - dag.container() - .from_(ALPINE_BASE) - # Transmission 4.1.x is only in edge community - .with_exec( - [ - "apk", - "add", - "--no-cache", - "--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community", - f"transmission-daemon={VERSION}", - f"transmission-cli={VERSION}", - f"transmission-remote={VERSION}", - ] - ) - .with_exec(["apk", "add", "--no-cache", "bash", "curl", "tzdata", "su-exec"]) - .with_exec( - ["mkdir", "-p", "/config", "/downloads/complete", "/downloads/incomplete"] - ) - .with_file("/start.sh", src.file("containers/transmission/start.sh")) - .with_exec(["chmod", "+x", "/start.sh"]) - .with_exposed_port(9091) - .with_exposed_port(51413, protocol=dagger.NetworkProtocol.TCP) - .with_exposed_port(51413, protocol=dagger.NetworkProtocol.UDP) - .with_default_args(args=["/start.sh"]) - ) - return oci_labels( - ctr, - title="Transmission", - description="Transmission BitTorrent daemon", - version=VERSION, - ) diff --git a/containers/unpoller/Dockerfile b/containers/unpoller/Dockerfile new file mode 100644 index 0000000..241b375 --- /dev/null +++ b/containers/unpoller/Dockerfile @@ -0,0 +1,43 @@ +# UnPoller — UniFi metrics exporter for Prometheus +# Two-stage build: Go compilation, then minimal Alpine runtime + +ARG CONTAINER_APP_VERSION=v2.34.0 + +FROM golang:alpine3.22 AS build + +ARG CONTAINER_APP_VERSION +RUN apk add --no-cache git + +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/unpoller.git /app + +WORKDIR /app + +ENV CGO_ENABLED=0 + +RUN go build -ldflags="-s -w \ + -X main.version=${CONTAINER_APP_VERSION} \ + -X main.builtBy=blumeops \ + -X golift.io/version.Version=${CONTAINER_APP_VERSION} \ + -X golift.io/version.Branch=HEAD \ + -X golift.io/version.BuildUser=blumeops \ + -X golift.io/version.Revision=blumeops-build" \ + -o /bin/unpoller . + +FROM alpine:3.22 + +ARG CONTAINER_APP_VERSION +LABEL org.opencontainers.image.title="UnPoller" +LABEL org.opencontainers.image.description="UniFi metrics exporter for Prometheus" +LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}" +LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops" +LABEL org.opencontainers.image.vendor="blumeops" + +RUN apk add --no-cache ca-certificates tzdata + +COPY --from=build /bin/unpoller /usr/bin/unpoller + +EXPOSE 9130 +USER 65534:65534 +ENTRYPOINT ["/usr/bin/unpoller"] +CMD ["--config", "/etc/unpoller/up.conf"] diff --git a/containers/unpoller/container.py b/containers/unpoller/container.py deleted file mode 100644 index bfc75ba..0000000 --- a/containers/unpoller/container.py +++ /dev/null @@ -1,53 +0,0 @@ -"""UnPoller — UniFi metrics exporter for Prometheus. - -Two-stage build: Go backend, Alpine runtime. -Source cloned from forge mirror. -""" - -import dagger - -from blumeops.containers import ( - alpine_runtime, - clone_from_forge, - go_build, - oci_labels, -) - -VERSION = "v3.2.0" - - -async def build(src: dagger.Directory) -> dagger.Container: - source = clone_from_forge("unpoller", VERSION) - - backend = go_build( - source, - "/unpoller", - ldflags=( - f"-s -w " - f"-X main.version={VERSION} " - f"-X main.builtBy=blumeops " - f"-X golift.io/version.Version={VERSION} " - f"-X golift.io/version.Branch=HEAD " - f"-X golift.io/version.BuildUser=blumeops " - f"-X golift.io/version.Revision=blumeops-build" - ), - ) - - runtime = alpine_runtime( - extra_apk=["ca-certificates", "tzdata"], - create_user=False, - ) - runtime = oci_labels( - runtime, - title="UnPoller", - description="UniFi metrics exporter for Prometheus", - version=VERSION, - ) - return ( - runtime.with_file("/usr/bin/unpoller", backend.file("/unpoller")) - .with_exposed_port(9130) - .with_user("65534") - .with_default_args( - args=["/usr/bin/unpoller", "--config", "/etc/unpoller/up.conf"] - ) - ) diff --git a/containers/valkey/container.py b/containers/valkey/container.py deleted file mode 100644 index 34e8524..0000000 --- a/containers/valkey/container.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Valkey — native Dagger build (arm64, indri). - -Alpine 3.22 base with the `valkey` apk package (8.1.x — Redis-compatible). -Used by paperless (sidecar) on indri. immich on ringtail uses the -nix-built amd64 variant from `default.nix` in this directory. -""" - -import dagger -from dagger import dag - -from blumeops.containers import oci_labels - -# Alpine 3.22 currently ships valkey 8.1.7-r0. Alpine 3.23 jumps to 9.0 — -# hold on 3.22 to keep this aligned with the 8.1 line. -VERSION = "8.1.7" -ALPINE_PIN = "8.1.7-r0" - -ALPINE_BASE = "alpine:3.22" - - -async def build(src: dagger.Directory) -> dagger.Container: - ctr = ( - dag.container() - .from_(ALPINE_BASE) - .with_exec(["apk", "add", "--no-cache", f"valkey={ALPINE_PIN}"]) - .with_exec(["mkdir", "-p", "/data"]) - .with_exec(["chown", "valkey:valkey", "/data"]) - .with_workdir("/data") - .with_exposed_port(6379) - .with_user("valkey") - .with_default_args( - args=[ - "valkey-server", - "--bind", - "0.0.0.0", - "--protected-mode", - "no", - "--dir", - "/data", - ] - ) - ) - return oci_labels( - ctr, - title="Valkey", - description="Valkey high-performance key/value datastore (Redis-compatible)", - version=VERSION, - ) diff --git a/containers/valkey/default.nix b/containers/valkey/default.nix deleted file mode 100644 index 9cb1713..0000000 --- a/containers/valkey/default.nix +++ /dev/null @@ -1,30 +0,0 @@ -# Nix-built Valkey for ringtail (amd64) -# Companion to container.py (Alpine 3.22, arm64 on indri). -# Used by immich-ringtail which needs an amd64 image; paperless on indri -# continues to use the Alpine container.py build. -# -# The version assertion ensures nix-build fails if a flake.lock update -# changes the Valkey version — forcing an explicit version acknowledgment -# here and in service-versions.yaml (enforced by container-version-check). -{ pkgs ? import { } }: - -let - version = "8.1.7"; -in - -assert pkgs.valkey.version == version; - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/valkey"; - contents = [ - pkgs.valkey - ]; - - config = { - Entrypoint = [ "${pkgs.valkey}/bin/valkey-server" ]; - Cmd = [ "--bind" "0.0.0.0" "--protected-mode" "no" "--dir" "/data" ]; - ExposedPorts = { - "6379/tcp" = { }; - }; - }; -} diff --git a/dagger.json b/dagger.json index 3309378..684aa80 100644 --- a/dagger.json +++ b/dagger.json @@ -1,7 +1,8 @@ { - "name": "blumeops", - "engineVersion": "v0.20.6", + "name": "blumeops-ci", + "engineVersion": "v0.20.1", "sdk": { "source": "python" - } + }, + "source": ".dagger" } diff --git a/docs/changelog.d/+branch-cleanup-preserve.misc.md b/docs/changelog.d/+branch-cleanup-preserve.misc.md new file mode 100644 index 0000000..425e8cc --- /dev/null +++ b/docs/changelog.d/+branch-cleanup-preserve.misc.md @@ -0,0 +1 @@ +Add `preserve/*` branch prefix exclusion to `branch-cleanup` task; document Pyroscope profiling work and blockers in observability reference. diff --git a/docs/changelog.d/+cv-doc-review.doc.md b/docs/changelog.d/+cv-doc-review.doc.md new file mode 100644 index 0000000..ecace7d --- /dev/null +++ b/docs/changelog.d/+cv-doc-review.doc.md @@ -0,0 +1 @@ +Review and fix CV service doc (correct URL, forge domain, container tag link) and add private forge repo review guidance to review-services process. diff --git a/docs/changelog.d/+external-secrets-main-sha-rebuild.infra.md b/docs/changelog.d/+external-secrets-main-sha-rebuild.infra.md deleted file mode 100644 index 2e931d4..0000000 --- a/docs/changelog.d/+external-secrets-main-sha-rebuild.infra.md +++ /dev/null @@ -1 +0,0 @@ -Rebuilt the locally-built external-secrets image from the `main` branch so the deployed tag (`v2.2.0-0e70a1b`) traces to a `main` commit rather than the now-merged feature branch, giving a stable provenance reference. diff --git a/docs/changelog.d/+external-secrets-stable-main-sha.infra.md b/docs/changelog.d/+external-secrets-stable-main-sha.infra.md deleted file mode 100644 index fbe3c21..0000000 --- a/docs/changelog.d/+external-secrets-stable-main-sha.infra.md +++ /dev/null @@ -1 +0,0 @@ -Rebuilt the external-secrets images off `main` and repointed both clusters to the stable main-sha tags (`v2.2.0-13895bb` arm64 / `v2.2.0-13895bb-nix` amd64), so the deployed images on indri and ringtail trace to the same `main` commit rather than earlier feature-branch builds. diff --git a/docs/changelog.d/+heph-hub-v1.2.1.infra.md b/docs/changelog.d/+heph-hub-v1.2.1.infra.md deleted file mode 100644 index c203323..0000000 --- a/docs/changelog.d/+heph-hub-v1.2.1.infra.md +++ /dev/null @@ -1 +0,0 @@ -Bumped the indri heph hub to v1.2.1, which adds the hub `GET /config` endpoint and ships the heph-pwa **Login with Authentik** flow (Authorization Code + PKCE). Pairs with the Authentik `heph` provider redirect URIs registered earlier. diff --git a/docs/changelog.d/+homepage-v1.11.0.infra.md b/docs/changelog.d/+homepage-v1.11.0.infra.md new file mode 100644 index 0000000..a35eaed --- /dev/null +++ b/docs/changelog.d/+homepage-v1.11.0.infra.md @@ -0,0 +1 @@ +Upgrade Homepage dashboard from v1.10.1 to v1.11.0 diff --git a/docs/changelog.d/+nvidia-device-plugin-v0.19.0.infra.md b/docs/changelog.d/+nvidia-device-plugin-v0.19.0.infra.md new file mode 100644 index 0000000..95abf25 --- /dev/null +++ b/docs/changelog.d/+nvidia-device-plugin-v0.19.0.infra.md @@ -0,0 +1 @@ +Upgrade nvidia-device-plugin from v0.18.2 to v0.19.0 diff --git a/docs/changelog.d/+podnotready-lookback.infra.md b/docs/changelog.d/+podnotready-lookback.infra.md new file mode 100644 index 0000000..fec02df --- /dev/null +++ b/docs/changelog.d/+podnotready-lookback.infra.md @@ -0,0 +1 @@ +Reduce PodNotReady alert lookback window from 5m to 60s to clear faster after rollouts. diff --git a/docs/changelog.d/+qart-tuner.feature.md b/docs/changelog.d/+qart-tuner.feature.md new file mode 100644 index 0000000..720774d --- /dev/null +++ b/docs/changelog.d/+qart-tuner.feature.md @@ -0,0 +1 @@ +Add QArt Tuner — a Go tool that generates QR codes whose data modules form a recognizable image, with an interactive web UI for parameter tuning. Based on the [QArt technique](https://research.swtch.com/qart) by Russ Cox. Lives in `utils/qart/`. diff --git a/docs/changelog.d/+review-tailscale-setup.doc.md b/docs/changelog.d/+review-tailscale-setup.doc.md new file mode 100644 index 0000000..e3395a0 --- /dev/null +++ b/docs/changelog.d/+review-tailscale-setup.doc.md @@ -0,0 +1 @@ +Review tailscale-setup tutorial: fix macOS install steps, add `--accept-routes` tip, correct tag name, add ACL apply instructions, add `[[tailscale-operator]]` cross-reference. diff --git a/docs/changelog.d/+ringtail-post-deploy-maintenance.infra.md b/docs/changelog.d/+ringtail-post-deploy-maintenance.infra.md new file mode 100644 index 0000000..c85a3da --- /dev/null +++ b/docs/changelog.d/+ringtail-post-deploy-maintenance.infra.md @@ -0,0 +1 @@ +Add post-deploy maintenance docs and generation pruning task for ringtail. diff --git a/docs/changelog.d/+tailscale-operator-mirror-tailnet-url.bugfix.md b/docs/changelog.d/+tailscale-operator-mirror-tailnet-url.bugfix.md deleted file mode 100644 index cc29cf7..0000000 --- a/docs/changelog.d/+tailscale-operator-mirror-tailnet-url.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Fixed the `tailscale-operator` and `tailscale-operator-ringtail` ArgoCD apps showing `Unknown` sync status. Their shared base kustomization fetched the upstream operator manifest from the public `forge.eblu.me/mirrors/...`, which the AI-scraper mitigation now black-holes (403). Pointed the remote resource at the tailnet host `forge.ops.eblu.me` instead, which the in-cluster repo-server can reach. diff --git a/docs/changelog.d/+tune-argocd-outofsync-alert.infra.md b/docs/changelog.d/+tune-argocd-outofsync-alert.infra.md new file mode 100644 index 0000000..cac4b46 --- /dev/null +++ b/docs/changelog.d/+tune-argocd-outofsync-alert.infra.md @@ -0,0 +1 @@ +Tighten ArgoCDAppOutOfSync alert: reduce pending duration from 30m to 5m and lookback window from 5m to 1m so alerts clear faster after sync. diff --git a/docs/changelog.d/+update-ringtail-flake.infra.md b/docs/changelog.d/+update-ringtail-flake.infra.md new file mode 100644 index 0000000..d2c1ce8 --- /dev/null +++ b/docs/changelog.d/+update-ringtail-flake.infra.md @@ -0,0 +1 @@ +Update ringtail flake inputs (nixpkgs, home-manager). diff --git a/docs/changelog.d/deploy-snowflake-proxy.feature.md b/docs/changelog.d/deploy-snowflake-proxy.feature.md new file mode 100644 index 0000000..e34af2b --- /dev/null +++ b/docs/changelog.d/deploy-snowflake-proxy.feature.md @@ -0,0 +1 @@ +Add Tor Snowflake proxy on ringtail as a systemd service to support anti-censorship efforts. diff --git a/docs/changelog.d/external-secrets-ringtail-nix.infra.md b/docs/changelog.d/external-secrets-ringtail-nix.infra.md deleted file mode 100644 index 9ce3f85..0000000 --- a/docs/changelog.d/external-secrets-ringtail-nix.infra.md +++ /dev/null @@ -1 +0,0 @@ -Completed the external-secrets localization for the ringtail (amd64) cluster. The indri Dagger build (`container.py`) only produces an arm64 image; added `containers/external-secrets/default.nix` to build the amd64 variant on ringtail's nix-container-builder, and gave `external-secrets-ringtail` a thin kustomize overlay that reuses the shared manifest and points at the `-nix` image. Both clusters now run the locally-built external-secrets binary on their native architecture. diff --git a/docs/changelog.d/feature-borgmatic-all-pg-backups.infra.md b/docs/changelog.d/feature-borgmatic-all-pg-backups.infra.md new file mode 100644 index 0000000..892ee65 --- /dev/null +++ b/docs/changelog.d/feature-borgmatic-all-pg-backups.infra.md @@ -0,0 +1 @@ +Add borgmatic pg_dump backups for authentik and immich databases. Authentik uses the existing blumeops-pg cluster on port 5432. Immich requires a new borgmatic role on the immich-pg cluster, a Tailscale service, and Caddy L4 proxy on port 5433. diff --git a/docs/changelog.d/heph-indri-hub.infra.md b/docs/changelog.d/heph-indri-hub.infra.md deleted file mode 100644 index 6761cb7..0000000 --- a/docs/changelog.d/heph-indri-hub.infra.md +++ /dev/null @@ -1 +0,0 @@ -Added the [[hephaestus]] (`heph`) sync hub to indri as a self-updating LaunchAgent managed by Ansible (`ansible/roles/heph`, tag `heph`). The hub runs `hephd --mode server` behind `heph.ops.eblu.me` (Caddy TLS), with self-update on a 10-minute interval and the heph-pwa mobile shell served from `--web-root`. Access is gated by a new Authentik device-code (RFC 8628) OIDC application. Indri is now the canonical hub; other devices (e.g. gilbert) attach as offline-capable spokes. The hub's store was seeded from gilbert via the data-safe Path A bring-up (copy store, reset `meta.origin`). diff --git a/docs/changelog.d/heph-offline-access.bugfix.md b/docs/changelog.d/heph-offline-access.bugfix.md deleted file mode 100644 index e9721bc..0000000 --- a/docs/changelog.d/heph-offline-access.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Granted the `offline_access` scope on the Authentik `heph` OAuth2 provider so hephaestus spokes receive a durable 30-day refresh token. Previously the refresh token was session-bound, so spoke sync would silently fail with a `400 Bad Request` on the `refresh_token` grant once the Authentik session lapsed. diff --git a/docs/changelog.d/heph-pwa-redirect-uris.infra.md b/docs/changelog.d/heph-pwa-redirect-uris.infra.md deleted file mode 100644 index f887eed..0000000 --- a/docs/changelog.d/heph-pwa-redirect-uris.infra.md +++ /dev/null @@ -1 +0,0 @@ -Registered the heph-pwa redirect URIs (`https://heph.ops.eblu.me/`, plus `http://localhost:8787/` for dev) on the Authentik `heph` OAuth2 provider, enabling the PWA's new Authorization Code + PKCE "Login with Authentik" flow (and the token-endpoint CORS it needs). Pairs with hephaestus PR #9. diff --git a/docs/changelog.d/immich-photos-backup.feature.md b/docs/changelog.d/immich-photos-backup.feature.md new file mode 100644 index 0000000..6391af5 --- /dev/null +++ b/docs/changelog.d/immich-photos-backup.feature.md @@ -0,0 +1 @@ +Add offsite backup for immich photo library to BorgBase, running daily at 4 AM from indri via sifaka SMB mount. diff --git a/docs/changelog.d/local-external-secrets.infra.md b/docs/changelog.d/local-external-secrets.infra.md deleted file mode 100644 index 13cbb05..0000000 --- a/docs/changelog.d/local-external-secrets.infra.md +++ /dev/null @@ -1 +0,0 @@ -Localized the external-secrets controller image. It now builds from the forge mirror via a native Dagger `container.py` (single `all_providers` static Go binary, faithful to upstream's `make build`) and is served from `registry.ops.eblu.me/blumeops/external-secrets` instead of `ghcr.io`, bringing another platform component under local supply-chain control. diff --git a/docs/changelog.d/reviews-jun4.doc.md b/docs/changelog.d/reviews-jun4.doc.md deleted file mode 100644 index f1aeaa8..0000000 --- a/docs/changelog.d/reviews-jun4.doc.md +++ /dev/null @@ -1 +0,0 @@ -Reviewed four never-reviewed reference cards (`cluster`, `ntfy`, `tempo`, `alloy`) and corrected drift: minikube is now Kubernetes v1.35.0; ntfy, tempo, and alloy-k8s images are now locally-built `registry.ops.eblu.me/blumeops/*` nix containers (v2.19.2, v2.10.3, v1.16.0) rather than upstream Docker Hub; the Fly.io alloy binary is v1.16.1; and the ringtail workload list reflects the in-progress minikube→k3s migration. diff --git a/docs/changelog.d/reviews-jun4.infra.md b/docs/changelog.d/reviews-jun4.infra.md deleted file mode 100644 index c128e70..0000000 --- a/docs/changelog.d/reviews-jun4.infra.md +++ /dev/null @@ -1 +0,0 @@ -Upgraded the nvidia-device-plugin on ringtail from v0.19.0 to v0.19.2 (upstream patch release: CDI/Tegra fixes and dependency bumps, no breaking changes for our manifest-based CDI + RuntimeClass setup). diff --git a/docs/changelog.d/upgrade-external-secrets-v2.infra.md b/docs/changelog.d/upgrade-external-secrets-v2.infra.md new file mode 100644 index 0000000..606a937 --- /dev/null +++ b/docs/changelog.d/upgrade-external-secrets-v2.infra.md @@ -0,0 +1 @@ +Upgrade External Secrets Operator from v1.3.2 to v2.2.0 and migrate from Helm chart to static kustomize manifests. diff --git a/docs/explanation/agent-change-process.md b/docs/explanation/agent-change-process.md index 5141950..38b5a26 100644 --- a/docs/explanation/agent-change-process.md +++ b/docs/explanation/agent-change-process.md @@ -58,7 +58,7 @@ A change with enough complexity or risk that a human should review it, but not s - **Workflows:** point workflow triggers at the branch if needed 8. After user review and successful deployment, the user merges the PR 9. **After merge:** reset ArgoCD revisions back to main, re-sync -10. **If the PR changed `containers/`:** trigger a rebuild with `mise run container-build-and-release `. Once it completes, commit a C0 updating the manifest to the new `[main]`-tagged image (see [[build-container-image#Squash-merge and container tags]]) +10. **If the PR changed `containers/`:** the merge triggers a rebuild from main automatically. Once it completes, commit a C0 updating the manifest to the new `[main]`-tagged image (see [[build-container-image#Squash-merge and container tags]]) ### Upgrading to C2 @@ -235,8 +235,8 @@ When starting a new session to continue C2 work: Mikado resets apply to branch code, not build artifacts. Container images in the registry are independent of branch lifecycle: - **Registry images** are build outputs cached in zot — tagged with commit SHAs, so each build is unique and traceable -- **Squash-merge orphans:** Images built during PR development reference branch SHAs that won't exist on main after merge. After merge, trigger a rebuild with `mise run container-build-and-release ` and commit a C0 to update manifests to the new `[main]`-tagged image. Use `mise run container-list ` to find it -- **All builds are manual** — use `mise run container-build-and-release ` to dispatch +- **Squash-merge orphans:** Images built during PR development reference branch SHAs that won't exist on main after merge. After merge, a rebuild triggers automatically; commit a C0 to update manifests to the new `[main]`-tagged image. Use `mise run container-list ` to find it +- **Automatic builds** trigger when container changes merge to main. Use `mise run container-build-and-release` for manual dispatch - **If a build succeeds but deployment fails**, the image is fine; the problem is elsewhere. Document what you learned and try again - **If a build fails in CI**, no image is pushed. Fix the nix/dockerfile and re-merge or re-dispatch diff --git a/docs/explanation/ai-scraper-mitigation.md b/docs/explanation/ai-scraper-mitigation.md deleted file mode 100644 index fe4ba3d..0000000 --- a/docs/explanation/ai-scraper-mitigation.md +++ /dev/null @@ -1,201 +0,0 @@ ---- -title: AI Scraper Mitigation -modified: 2026-06-01 -last-reviewed: 2026-06-01 -tags: - - explanation - - fly-io - - forgejo - - security - - networking ---- - -# AI Scraper Mitigation on the Public Proxy - -> **Note:** This article was drafted by AI and reviewed by Erich. I plan to rewrite all explanatory content in my own words — these serve as placeholders to establish the documentation structure. - -How BlumeOps keeps AI crawlers from running up the [[expose-service-publicly|Fly.io proxy]] egress bill and DoS-ing [[forgejo|Forgejo]] on [[indri]]. - -## The incident - -A $29.60 Fly.io invoice arrived, nearly all of it a single line: - -``` -Bandwidth: Egress (iad) — 958,524,714,138 bytes — $19.17 -``` - -The `iad` (Ashburn) region is a red herring: the proxy machine runs in `sjc`, -but Fly bills egress at the edge PoP nearest the *client*, so `iad` just means -"the traffic went to clients on the US East Coast." - -Tracing it through the nginx access logs (shipped to Loki via [[alloy|Alloy]]): - -| Signal | Value | -|--------|-------| -| Total proxy egress (30d) | ~1.25 TB | -| Share that was `forge.eblu.me` | **99.95%** | -| Share of forge egress that was `/mirrors/*` | **~71%** | -| Share that was declared AI bots | **~85%+** | -| Top offenders | Meta `meta-externalagent` (66% of bytes), OpenAI `GPTBot` (16%), Amazonbot, Bytespider | -| Forgejo `5xx` (upstream timeouts) | tens of thousands/day, spiking to 112k | - -The crawlers were walking [[forgejo|Forgejo]]'s git-history browse endpoints — -`src/commit/`, `commits/`, `blame/`, `raw/commit/`, plus `.patch`/`.diff` -and `?page=N` pagination. That URL space is effectively **infinite**: every -file × every commit × every page, multiplied across every mirrored repo. A -crawler that follows links never finishes, and every page is a cache `MISS` -that both tunnels to indri *and* bills as egress. - -Two distinct harms, not one: - -1. **Cost** — ~1.25 TB/mo of egress on a free-tier-ish proxy. -2. **Availability** — the crawl alone generates ~400–530k requests/day, - enough to time out Forgejo regardless of how much RAM [[indri]] has. Moving - egress elsewhere would *not* fix this; the crawl has to be throttled at the - source. - -`robots.txt` already `Disallow`s `/mirrors/`, `/user/`, and archive/download -paths — but **`meta-externalagent` and `GPTBot` ignore it.** For these agents, -`robots.txt` is a dead letter, which is why edge enforcement is required. - -## The tiered plan - -### Tier 1 — Black-hole `/mirrors/*` (shipped) - -The mirror repositories (`tailscale`, `prometheus`, `mealie`, `paperless-ngx`, -…) are mirrors of *already-public upstreams*, kept for supply-chain control -(see [[spork-strategy]] and the container/mirror story in [[why-gitops]]). They -are consumed by CI, gilbert, and other tailnet clients over -`forge.ops.eblu.me`. Their web UI on the public internet served **no -legitimate audience** — only scrapers. So the proxy now returns `403` for -anything under `/mirrors/`, pointing humans at the tailnet host: - -```nginx -location ^~ /mirrors/ { - return 403 "Mirror repositories are tailnet-only — use forge.ops.eblu.me.\n"; -} -``` - -The `^~` modifier matters: without it, the regex `location` blocks for static -assets (`*.css`, `*.js`, release downloads) would match first and leak content -under `/mirrors/`. `^~` tells nginx to stop at the prefix match and skip the -regex round. - -This is config, not bot-fighting — we simply stopped serving an infinite -tarpit to the world. It removes ~71% of forge egress and a large share of the -upstream timeouts, with zero impact on any human or tailnet consumer. It -mirrors the existing tailnet-only blocks for `/api/packages/` and `/swagger`. - -The `403` is also a small act of public shaming. Blocked requests are served a -"roll of dishonour" page (`fly/naughty.html`, status kept at `403` via -`error_page 403 /naughty.html`) that names the offending operators and their -share of the stolen bytes, and every response carries an `X-Naughty-Scrapers` -header: - -``` -X-Naughty-Scrapers: OpenAI/GPTBot, Meta/meta-externalagent, Amazonbot, ByteDance/Bytespider — robots.txt ignorers -``` - -Petty? A little. But it costs nothing, documents *why* the block exists for the -next person who hits it, and the page is a few KB versus the megabytes of git -HTML the crawlers were taking. - -**Trade-off accepted:** mirror release-artifact downloads over WAN now also -`403`. Legitimate consumers already pull these over the tailnet, and the public -exposure was the same crawl liability, so this is intentional. - -### Tier 2 — Defend the repos that *stay* public (planned) - -`/eblume/*` is intentionally public (a public profile is a feature). But the -same git-history endpoints are still a tarpit there, just lower-volume. Two -layers, in increasing order of effort and effectiveness: - -#### 2a. User-agent denylist (cheap, evadable) - -Block the declared AI crawlers at the edge regardless of path: - -```nginx -# Illustrative — not yet deployed. -map $http_user_agent $is_ai_bot { - default 0; - "~*meta-externalagent" 1; - "~*GPTBot" 1; - "~*ClaudeBot" 1; - "~*Amazonbot" 1; - "~*Bytespider" 1; - "~*SemrushBot" 1; -} -# in the forge.eblu.me server block: -if ($is_ai_bot) { return 403; } -``` - -This catches ~85% of *current* traffic for a few lines of config. It is -trivially evadable — a scraper need only spoof a browser UA — so it is a -speed-bump, not a wall. Keep `robots.txt` too: well-behaved crawlers -(Googlebot, Bingbot) do honor it, and it documents intent. - -#### 2b. Anubis proof-of-work gateway (the real wall) - -[Anubis](https://github.com/TecharoHQ/anubis) is a Go reverse proxy that -weighs each request with a browser-based proof-of-work challenge before passing -it upstream. It was written for *exactly this scenario* — its author built it -after Amazon's scraper took down their Git server — and is widely deployed in -front of Forgejo/Gitea (Codeberg, the UN, etc.). Headless scrapers that can't -run the challenge JS never reach the application; humans clear it once and -proceed. - -Why it fits BlumeOps better than the alternatives: - -- **It attacks cost *and* availability at once.** Bots receive a few-KB - challenge page instead of MB of git HTML (egress collapses) and never reach - Forgejo (timeouts collapse). No other single lever does both. -- **It stays in-house.** No third party terminates our TLS or sees our - traffic. - -Placement options: - -| Where | Pros | Cons | -|-------|------|------| -| On [[indri]], between [[caddy|Caddy]] and Forgejo | Protects every path and every entry (WAN *and* tailnet); one config | Adds a hop and a service to the indri critical path; the challenge page still tunnels back through Fly for WAN clients (small egress) | -| On the Fly proxy machine, in front of nginx | Challenge served at the edge — bots never even tunnel to indri | Fly VM is small (512 MB); another moving part in the boot sequence alongside `tailscaled`/nginx/`fail2ban`/Alloy | - -Leaning toward Caddy-side on indri for simplicity and uniform coverage, but -this is the open design question for Tier 2. Anubis is MIT-licensed and the -author has signalled a future move to an `equi-x`-based challenge, so pin a -version and track upstream. - -### Tier 3 — Move egress off Fly entirely (rejected) - -A [[#The incident|Cloudflare]] Tunnel (`cloudflared` on indri → Cloudflare -edge) would make this a non-problem on the cost axis: Cloudflare does not meter -proxied bandwidth, and it bundles free AI-bot mitigation (Bot Fight Mode, the -"block AI scrapers" toggle, Managed Challenge, AI Labyrinth). One move would -zero the egress bill and add bot defense. - -**We are not doing this, on principle.** Cloudflare is a solid platform and a -defensible engineering choice — but it already sits in front of an enormous -fraction of the modern web, and routing BlumeOps through it would add one more -site to the pile of the internet that one company can see and gate. BlumeOps -deliberately keeps its own backbone ([[expose-service-publicly|Fly + Tailscale -+ Caddy]], DNS at [[gandi|Gandi]] — see the "no Cloudflare dependency" line in -that doc). This is a values decision, not a technical one: we would rather pay -a few dollars and run our own mitigation than centralize on Cloudflare. - -It is also worth noting that **Tier 3 would not, by itself, fix the upstream -timeouts** — free egress just means we'd stop *caring* that bots crawl, while -they continued to hammer Forgejo. Crawl mitigation (Tier 1 + Tier 2) is -required regardless of where egress is billed. - -## Summary - -| Tier | Lever | Cost | Availability | Status | -|------|-------|------|--------------|--------| -| 1 | Black-hole `/mirrors/*` at edge | −~71% | big drop | **shipped** | -| 2a | UA denylist on remaining repos | −most of the rest | further drop | planned | -| 2b | Anubis PoW gateway | −near-total | near-total | planned | -| 3 | Cloudflare Tunnel | −total | needs 2b anyway | **rejected (principle)** | - -The guiding insight: the cheapest, lowest-risk mitigation is to **not serve an -infinite-URL surface that has no human audience.** Everything past Tier 1 is -about defending the surface we *do* want public, in-house, without ceding -control of our traffic to a third party. diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md index a99956f..4080b1e 100644 --- a/docs/explanation/architecture.md +++ b/docs/explanation/architecture.md @@ -59,9 +59,9 @@ Three layers of reverse proxying expose services at different scopes: **Tailscale** is the base layer — every service gets a MagicDNS hostname. The [[tailscale-operator]] gives Kubernetes services their own Tailscale Ingress endpoints. -**[[caddy]]** runs natively on [[indri]] and provides a unified `*.ops.eblu.me` wildcard with TLS (Let's Encrypt via DNS-01/Gandi). It proxies to both local services (Forgejo, Zot, Jellyfin) and Kubernetes services (via their Tailscale Ingress endpoints). Caddy serves both tailnet clients and public traffic (via the Fly proxy). +**[[caddy]]** runs natively on [[indri]] and provides a unified `*.ops.eblu.me` wildcard with TLS (Let's Encrypt via DNS-01/Gandi). It proxies to both local services (Forgejo, Zot, Jellyfin) and Kubernetes services (via their Tailscale Ingress endpoints). Access is restricted by Tailscale ACLs — only `tag:homelab` and `autogroup:admin` can reach Caddy. -**[[flyio-proxy]]** runs on Fly.io for select services that need public internet access. Traffic hits Fly.io's Anycast edge, terminates TLS, and tunnels back to Caddy on indri over a direct Tailscale WireGuard connection. The proxy uses `tag:flyio-target` ACLs — indri carries this tag so the proxy can reach Caddy, but cannot route to arbitrary services on the tailnet. +**[[flyio-proxy]]** runs on Fly.io for select services that need public internet access. Traffic hits Fly.io's Anycast edge, terminates TLS, and tunnels back to the homelab over Tailscale. Only services explicitly tagged `tag:flyio-target` are reachable — a compromised proxy cannot route to arbitrary services on the tailnet. See [[routing]] for the full service URL table and port map. diff --git a/docs/explanation/no-helm-policy.md b/docs/explanation/no-helm-policy.md deleted file mode 100644 index 760c234..0000000 --- a/docs/explanation/no-helm-policy.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: No Helm Policy -modified: 2026-04-06 -tags: - - explanation - - kubernetes ---- - -# No Helm Policy - -> **Note:** This article was drafted by AI and reviewed by Erich. I plan to rewrite all explanatory content in my own words - these serve as placeholders to establish the documentation structure. - -BlumeOps avoids Helm charts as a deployment mechanism. Plain kustomize manifests are the standard for all services. - -## Rationale - -Helm templates add a layer of abstraction that works against the simplicity of Kubernetes YAML manifests. Go templates embedded in YAML are hard to read, hard to diff, and hard to reason about. A manifest should be a manifest — not a program that generates one. - -Kustomize overlays preserve the readability of plain YAML while providing the composition and patching features needed for environment-specific configuration. Version bumps are a one-line `newTag` edit in `kustomization.yaml`, and `kubectl diff` shows exactly what will change. - -## Current State - -All services in blumeops use kustomize manifests. The last Helm dependency (1Password Connect) was migrated in 2026-04. - -## Migration History - -Services previously deployed via Helm that have been migrated to kustomize: - -| Service | Migrated | Notes | -|---------|----------|-------| -| Grafana | 2026-02 | Converted during v12.x upgrade | -| CloudNative-PG | 2026-02 | Switched to upstream release manifest via forge mirror | -| External Secrets | 2026-03 | Static manifests rendered from chart | -| Homepage | 2026-02 | Replaced chart with plain manifests | -| Immich | 2026-04 | Converted during v2.6.3 upgrade | -| 1Password Connect | 2026-04 | Rendered from chart v2.4.1, bumped to 1.8.2 | - -## Guidelines - -- **Do not introduce new Helm chart dependencies.** When deploying a new service, write kustomize manifests directly — even if the upstream project provides a Helm chart. The chart's `helm template` output is a fine starting point for writing those manifests. -- **When upgrading a Helm-based service**, consider whether it's a good time to migrate off Helm as part of the upgrade. -- **Upstream manifests** can be referenced directly in `kustomization.yaml` resources (like ArgoCD and Tailscale operator do) or applied via ArgoCD's `directory.include` (like CloudNative-PG). Both avoid Helm. - -## Related - -- [[review-services]] — Service review process -- [[architecture]] — Overall infrastructure design diff --git a/docs/explanation/spork-strategy.md b/docs/explanation/spork-strategy.md deleted file mode 100644 index f5ac4ea..0000000 --- a/docs/explanation/spork-strategy.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Spork Strategy -modified: 2026-03-28 -last-reviewed: 2026-03-28 -tags: - - explanation - - git - - forgejo ---- - -# Spork Strategy - -> **Note:** This article was drafted by AI and reviewed by Erich. I plan to rewrite all explanatory content in my own words - these serve as placeholders to establish the documentation structure. - -A "spork" is a floating-branch soft-fork strategy for maintaining local changes against upstream projects without creating a true fork. The name: a fork that's trying its hardest not to be one. - -## The problem - -We mirror upstream projects on forge for supply-chain control. Sometimes we need to carry local patches — workflow support, build tooling, bug fixes. A real fork diverges silently until merge day becomes a nightmare. A spork stays perpetually close to upstream with patches "floating" on top, rebased daily. - -## The trade-off - -A spork chooses "small frequent pain" (constant rebasing, shifting branch targets) over "rare catastrophic pain" (fork divergence). For a solo operator carrying a handful of patches, this is the right trade-off. The key property: `git log main..blumeops` always shows your complete delta from upstream. No mystery divergence. - -Long-lived work against a sporked repo must accept that there is no "safe" branch — everything is an ever-shifting target. Anyone with a local checkout needs to be comfortable with `git pull --rebase`. - -## Architecture - -Three remotes, five branch types, one daily sync workflow. The `blumeops` branch is the default — it looks just like upstream with local workflows overlaid. Feature branches come in two flavors: upstreamable (branched off `main`, clean for contribution) and non-upstreamable (branched off `blumeops`, local-only). A `deploy` branch merges everything together as a build artifact. - -Forgejo Actions checks `.forgejo/workflows/` first; if that directory exists, `.github/workflows/` is ignored. This protects the `blumeops` branch and `feature/local/*` branches (which inherit `.forgejo/` from `blumeops`). However, `main` and `feature/upstream/*` branches do NOT have `.forgejo/workflows/` — they're clean upstream code — so Forgejo falls back to `.github/workflows/` on those branches. See [[spork-strategy#Spork Attack]] for the security implications. - -## Spork Attack - -A "spork attack" is a supply-chain risk inherent to the spork strategy. Because `main` and `feature/upstream/*` branches carry upstream's `.github/workflows/`, those workflows are registered by Forgejo Actions. If an upstream project publishes a workflow targeting runner labels that match your infrastructure, it will execute on your runners. - -**Attack chain:** - -1. Upstream pushes a workflow (in `.github/workflows/` or `.forgejo/workflows/`) with `runs-on: ` -2. Mirror auto-syncs, mirror-sync fast-forwards `main` on your fork -3. The workflow triggers — via push event, PR event, cron schedule, or any other trigger mechanism -4. Workflow executes on your runner with access to `GITHUB_TOKEN` and the runner's environment - -Note that a cron-triggered workflow is especially dangerous: it requires no user interaction at all. As soon as `main` is updated with the malicious workflow, Forgejo schedules it automatically. - -**Current mitigations:** - -- **Runner label mismatch** — our runner uses `k8s`, upstream workflows typically use `ubuntu-24.04` / `macos-latest` / `windows-latest`. Jobs queue but never execute. This is effective but fragile — it depends on upstream never guessing our label. -- **Trust boundary** — we only spork projects we trust. Kingfisher is maintained by a MongoDB security engineer. -- **Mirror review** — mirror syncs are visible in Forgejo; malicious workflow changes would appear in the commit history. But this is not a real-time defense — the workflow may execute before anyone reviews. - -**What would fix this properly:** - -- A Forgejo per-repo setting to disable workflow discovery entirely on specific branches, or to require an explicit allow-list of workflow files. Neither exists today. -- Runner-level repo allow-lists could limit blast radius, but the workflow files still come from the sporked repo via upstream, so the runner would still execute them. - -**Recommendation:** Use non-standard runner labels (not `ubuntu-latest`, `linux`, etc.) and only spork projects you trust. Document which projects are sporked and review upstream workflow changes periodically. Consider this an open problem — there is no complete defense short of disabling Actions on the repo entirely (which breaks mirror-sync). - -## How-to guides - -- [[create-a-spork]] — initial setup with `mise run spork-create` -- [[manage-spork-branches]] — feature branches, the deploy branch, handling rebase conflicts -- [[build-spork-container]] — building reproducible containers from pinned SHAs - -## See also - -- [[manage-forgejo-mirrors]] — how upstream mirrors work -- [[kingfisher]] — first project using the spork strategy diff --git a/docs/how-to/configuration/build-spork-container.md b/docs/how-to/configuration/build-spork-container.md deleted file mode 100644 index cdb637a..0000000 --- a/docs/how-to/configuration/build-spork-container.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: Build a Spork Container -modified: 2026-03-29 -last-reviewed: 2026-03-29 -tags: - - how-to - - containers - - git ---- - -# Build a Spork Container - -How to build a container image from a [[spork-strategy|sporked]] project with fully-pinned, reproducible inputs. - -## Why not use the `deploy` branch directly? - -The `deploy` branch is force-pushed on every mirror-sync. Building from `deploy` is not reproducible — the same build a week later fetches different code (or fails because the old commit was garbage collected). - -Instead, spork containers use Nix to fetch upstream `main` at a pinned SHA and generate patches from feature branches at pinned SHAs. Both upstream and feature commits are on stable branches that are never force-pushed. - -## How the Nix build works - -The `default.nix` uses `builtins.fetchGit` (eval-time, network access) to fetch two source trees: - -1. **Upstream source** at the pinned `upstreamRev` on `main` -2. **Feature branch source** at the pinned `rev` for each feature - -Then a sandboxed `diff -ruN` generates a patch from upstream→feature for each feature branch. `buildRustPackage` applies the patches to the upstream source and builds. - -This means: -- The upstream rev persists forever (main only fast-forwards) -- Feature revs are on your branches (you control them) -- No dependency on the `deploy` branch -- Fully reproducible given the same revs - -## Prerequisites - -- Sporked project set up (see [[create-a-spork]]) -- Nix build runs on ringtail (`nix-container-builder` runner) - -## Get the SHAs - -```fish -cd ~/code/3rd/kingfisher -git fetch origin - -# Upstream SHA (main branch) -git rev-parse origin/main -# e.g., 1d37d2983cd4a58c12663dd8df0e79dfe89a5d75 - -# Feature branch SHAs -git rev-parse origin/feature/upstream/clone-url-base -# e.g., 677c7a5d5fc42b655d38fbf95dc8b814d89ceabb -``` - -## Update `default.nix` - -Edit `containers/kingfisher/default.nix`: - -- `version` — short upstream SHA (for container tag) -- `upstreamRev` — full upstream main SHA -- `features[].rev` — full feature branch SHA - -If dependencies changed, update `Cargo.lock` too: - -```fish -cd ~/code/3rd/kingfisher -git checkout origin/main -cargo update -cp Cargo.lock ~/code/personal/blumeops/containers/kingfisher/Cargo.lock -``` - -## Build and push - -The build is triggered via the standard container build workflow on ringtail's `nix-container-builder` runner, or manually: - -```fish -mise run container-build-and-release kingfisher -``` - -## Update the deployment - -1. Update `argocd/manifests/kingfisher/kustomization.yaml` with the new tag -2. Update `service-versions.yaml` if the upstream SHA changed -3. Sync the ArgoCD app - -## Note on `CONTAINER_APP_VERSION` - -The `default.nix` includes `version` which maps to `CONTAINER_APP_VERSION` for the `container-version-check` hook. For sporked containers this is a git SHA, not a release version. Don't confuse it with an upstream release number. - -## Reproducibility - -The upstream rev must be an ancestor of each feature rev. If you bump the upstream rev without rebasing your feature branches, the generated patch will conflict and the build fails — which is the correct behavior. - -The invariant: **feature revs are descendants of the upstream rev**. Mirror-sync maintains this automatically. You just need to update the revs in `default.nix` after an upgrade. - -## See also - -- [[create-a-spork]] — initial spork setup -- [[manage-spork-branches]] — feature branch workflow -- [[kingfisher]] — first sporked project diff --git a/docs/how-to/configuration/create-a-spork.md b/docs/how-to/configuration/create-a-spork.md deleted file mode 100644 index cd1a6f3..0000000 --- a/docs/how-to/configuration/create-a-spork.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Create a Spork -modified: 2026-03-28 -last-reviewed: 2026-03-28 -tags: - - how-to - - git - - forgejo ---- - -# Create a Spork - -How to set up a floating-branch soft-fork ("spork") of a mirrored upstream project using `mise run spork-create`. - -## Prerequisites - -- Mirror already exists at `mirrors/` on forge (see [[manage-forgejo-mirrors]]) -- 1Password CLI authenticated (`op` CLI) -- SSH access to `forge.ops.eblu.me:2222` - -## Create the spork - -```fish -mise run spork-create kingfisher -``` - -This will: - -1. Fork `mirrors/kingfisher` → `eblume/kingfisher` on forge -2. Create a `blumeops` branch from upstream's main branch -3. Remove any upstream `.forgejo/` directory (if present) -4. Add `.forgejo/workflows/mirror-sync.yaml` and commit it -5. Set `blumeops` as the default branch -6. Clone to `~/code/3rd/kingfisher` with three remotes: `origin`, `mirror`, `upstream` - -Options: - -```fish -mise run spork-create kingfisher --dry-run # preview only -mise run spork-create kingfisher --no-clone # skip local clone -mise run spork-create kingfisher --main-branch dev # override branch name -``` - -## Verify the setup - -```fish -cd ~/code/3rd/kingfisher -git remote -v -# origin ssh://forgejo@forge.ops.eblu.me:2222/eblume/kingfisher.git (fetch) -# mirror ssh://forgejo@forge.ops.eblu.me:2222/mirrors/kingfisher.git (fetch) -# upstream https://github.com/mongodb/kingfisher.git (fetch) - -git branch -a -# * blumeops -# remotes/origin/blumeops -# remotes/origin/main -``` - -## What happens next - -The mirror-sync workflow runs daily at 05:00 UTC and: - -- Fast-forwards `main` from the mirror -- Rebases `blumeops` on top of `main` -- Rebases any `feature/local/*` and `feature/upstream/*` branches -- Rebuilds the `deploy` branch (all features merged) - -See [[manage-spork-branches]] for working with feature branches. - -## Terminology - -| Term | Meaning | -|------|---------| -| `origin` | Your mutable fork at `eblume/` on forge | -| `mirror` | Read-only upstream mirror at `mirrors/` on forge | -| `upstream` | Canonical upstream repository (e.g., GitHub) | -| `main` | Clean upstream tracking branch (may be named `master`, `dev`, etc.) | -| `blumeops` | Default branch — upstream + local workflows/tooling | -| `deploy` | Build artifact branch — everything merged, used for deployments | - -## See also - -- [[manage-spork-branches]] — creating feature branches, upstreamable vs local -- [[manage-forgejo-mirrors]] — mirror setup and PAT rotation diff --git a/docs/how-to/configuration/gandi-operations.md b/docs/how-to/configuration/gandi-operations.md new file mode 100644 index 0000000..0be00dc --- /dev/null +++ b/docs/how-to/configuration/gandi-operations.md @@ -0,0 +1,90 @@ +--- +title: Gandi Operations +modified: 2026-02-17 +last-reviewed: 2026-02-17 +tags: + - how-to + - dns + - pulumi +--- + +# Gandi Operations + +How to manage DNS records and cycle the Gandi API token. + +## Prerequisites + +- Pulumi CLI installed (`brew install pulumi`) +- Access to 1Password blumeops vault (for PAT) +- On the tailnet (Pulumi resolves indri's IP via MagicDNS) + +## Preview and Apply DNS Changes + +```bash +# Preview changes (always do this first) +mise run dns-preview + +# Apply changes +mise run dns-up +``` + +Both tasks fetch the Gandi PAT from 1Password automatically. + +To run Pulumi directly: + +```bash +export GANDI_PERSONAL_ACCESS_TOKEN=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/mco6ka3dc3rmw7zkg2dhia5d2m/pat") +cd pulumi/gandi +pulumi preview +pulumi up --yes +``` + +## Cycle the Gandi PAT + +The Gandi Personal Access Token has a maximum lifetime of 90 days. Currently set to 30 days as a security compromise, though shorter may be appropriate given infrequent use. + +### 1. Create a new PAT + +Go to the [Gandi admin console](https://admin.gandi.net/organizations/1db8d76a-f729-11ed-b8d1-00163e94b645/account/pat) and create a new token: + +- **Name:** `blumeops-pulumi` (or similar) +- **Expiration:** 30 days (max 90; shorter is fine if you run this rarely) +- **Required permission:** Manage domain name technical configurations +- **Also enable:** See and renew domain names + +Copy the new PAT to your clipboard. + +### 2. Update 1Password + +With the new PAT on your clipboard: + +```bash +op item edit mco6ka3dc3rmw7zkg2dhia5d2m pat="$(pbpaste)" --vault vg6xf6vvfmoh5hqjjhlhbeoaie +``` + +### 3. Delete the old PAT + +Return to the Gandi admin console and delete the previous token. + +### 4. Verify + +```bash +mise run dns-preview +``` + +A successful preview confirms the new PAT is working. + +## Break-Glass Override + +If MagicDNS is unavailable and Pulumi can't resolve indri's IP, set the target IP manually. Find indri's current Tailscale IP via `tailscale status` or the admin console: + +```bash +export BLUMEOPS_REVERSE_PROXY_IP= +mise run dns-up +``` + +## Related + +- [[gandi]] - DNS configuration reference +- [[caddy]] - Reverse proxy (also uses a Gandi token for TLS) +- [[update-tailscale-acls]] - Similar Pulumi workflow for Tailscale diff --git a/docs/how-to/configuration/manage-eblu-me-dns.md b/docs/how-to/configuration/manage-eblu-me-dns.md deleted file mode 100644 index 4c37d4c..0000000 --- a/docs/how-to/configuration/manage-eblu-me-dns.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Manage eblu.me DNS Records -modified: 2026-04-27 -last-reviewed: 2026-04-27 -tags: - - how-to - - dns - - pulumi ---- - -# Manage eblu.me DNS Records - -How to add, change, and apply DNS records for `eblu.me` via [[pulumi]]. - -## Prerequisites - -- Pulumi CLI installed (`brew install pulumi`) -- 1Password access (`blumeops` vault) — Pulumi reads the Gandi PAT from there -- On the tailnet — Pulumi resolves [[indri]]'s IP via MagicDNS at apply time - -## Preview and apply - -```bash -mise run dns-preview # always do this first -mise run dns-up # apply -``` - -Both fetch the PAT from 1Password automatically. The Pulumi program is in `pulumi/gandi/`; stack is `eblu-me`. - -## Adding a record - -Edit `pulumi/gandi/__main__.py` and add a `gandi.livedns.Record(...)`. The stack config (`Pulumi.eblu-me.yaml`) only holds `domain` and `subdomain`; everything else is in the program. - -After editing, preview, then apply. - -## Break-glass: override the indri target IP - -The wildcard `*.ops.eblu.me` is computed from `indri.tail8d86e.ts.net` via MagicDNS at apply time. If MagicDNS is unavailable: - -```bash -export BLUMEOPS_REVERSE_PROXY_IP= -mise run dns-up -``` - -Find the IP via `tailscale status` or the Tailscale admin console. - -## Related - -- [[gandi]] — Gandi reference card -- [[rotate-gandi-pat]] — Rotate the PAT shared with [[caddy]] -- [[pulumi]] — Pulumi tooling reference -- [[routing]] — Service URLs and routing architecture diff --git a/docs/how-to/configuration/manage-forgejo-mirrors.md b/docs/how-to/configuration/manage-forgejo-mirrors.md index 5d150dc..7a1d36a 100644 --- a/docs/how-to/configuration/manage-forgejo-mirrors.md +++ b/docs/how-to/configuration/manage-forgejo-mirrors.md @@ -137,13 +137,11 @@ Return to [GitHub token settings](https://github.com/settings/tokens?type=beta) Trigger a manual sync on one mirror to confirm the new PAT works: -1. Go to any mirror repo's settings page on forge (e.g., `https://forge.eblu.me/mirrors/cloudnative-pg/settings`) -2. In the "Mirror settings" section, click "Synchronize now" +1. Go to any mirror repo on forge (e.g., `mirrors/cloudnative-pg`) +2. Click the sync button (circular arrows icon) next to the mirror status 3. Confirm the sync completes without errors ## Related - [[forgejo]] — Forgejo service reference -- [[rotate-gandi-pat]] — Similar PAT rotation workflow for Gandi DNS -- [[spork-strategy]] — floating-branch soft-fork strategy explanation -- [[create-a-spork]] — create a spork on top of a mirror +- [[gandi-operations]] — Similar PAT rotation workflow for Gandi DNS diff --git a/docs/how-to/configuration/manage-spork-branches.md b/docs/how-to/configuration/manage-spork-branches.md deleted file mode 100644 index 7bcf4fc..0000000 --- a/docs/how-to/configuration/manage-spork-branches.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: Manage Spork Branches -modified: 2026-03-28 -last-reviewed: 2026-03-28 -tags: - - how-to - - git - - forgejo ---- - -# Manage Spork Branches - -How to create, maintain, and reason about feature branches on a sporked repository. See [[create-a-spork]] for initial setup. - -## Branch types - -### Upstreamable features (`feature/upstream/*`) - -Changes intended to be contributed upstream. Branch off `main` so the diff is clean — no local tooling or workflows mixed in. - -```fish -cd ~/code/3rd/kingfisher -git fetch origin -git checkout -b feature/upstream/forgejo-support origin/main - -# Make changes, commit as normal -git push -u origin feature/upstream/forgejo-support -``` - -The mirror-sync workflow will automatically rebase this branch onto `main` each day. - -To see what the upstream contribution looks like: - -```fish -git log main..feature/upstream/forgejo-support --oneline -git diff main...feature/upstream/forgejo-support -``` - -To create a preview PR on forge (targets the mirror, not upstream): - -```fish -# From the eblume/ repo, PR targeting mirrors/:main -# This gives a public URL showing the diff without filing upstream -tea pr create --repo mirrors/kingfisher --head eblume/kingfisher:feature/upstream/forgejo-support --base main -``` - -When ready to contribute upstream, manually translate the branch to a GitHub PR. - -### Non-upstreamable features (`feature/local/*`) - -Local-only changes that will never go upstream. Branch off `blumeops` so you have access to all local tooling. - -```fish -cd ~/code/3rd/kingfisher -git fetch origin -git checkout -b feature/local/custom-rules origin/blumeops - -# Make changes, commit as normal -git push -u origin feature/local/custom-rules -``` - -The mirror-sync workflow will automatically rebase this branch onto `blumeops` each day. - -## The `deploy` branch - -The `deploy` branch is a build artifact — rebuilt fresh by mirror-sync daily. It contains everything merged together: `blumeops` + all `feature/local/*` + all `feature/upstream/*`. Use this branch for deployments (e.g., ArgoCD `targetRevision`). - -**Never commit to or work from `deploy`.** - -## Working with rebasing branches - -Because mirror-sync force-pushes rebased branches daily, local checkouts will diverge. Always pull with rebase: - -```fish -git pull --rebase origin feature/upstream/my-change -``` - -Or set it as default for the repo: - -```fish -git config pull.rebase true -``` - -This is the fundamental trade-off of the spork strategy: small frequent rebases instead of rare catastrophic merges. - -## When rebases fail - -If upstream changes conflict with a feature branch, mirror-sync will skip that branch and log an error. Recovery: - -```fish -cd ~/code/3rd/kingfisher -git fetch origin -git checkout feature/upstream/my-change -git rebase origin/main -# Resolve conflicts... -git push --force-with-lease origin feature/upstream/my-change -``` - -The next mirror-sync run will pick up the resolved branch and rebuild `deploy`. - -**TODO:** Workflow failures — whether from rebase conflicts or upstream history rewrites (force-push on main) — are currently only visible in the Forgejo Actions UI. Alerting via Grafana is planned but not yet implemented. - -## Future: `.spork.toml` - -For repos with multiple feature branches, a `.spork.toml` file on the `blumeops` branch could declare: - -- **Branch dependencies** (stacked branches — `bar` depends on `foo`) -- **Feature descriptions** (what the branch is for, in prose) -- **Upstream/local classification** (as an alternative to the naming convention) - -This is not yet implemented. For now, the `feature/upstream/*` vs `feature/local/*` naming convention is the source of truth. - -## See also - -- [[create-a-spork]] — initial setup -- [[manage-forgejo-mirrors]] — mirror setup and PAT rotation diff --git a/docs/how-to/configuration/rotate-fly-deploy-token.md b/docs/how-to/configuration/rotate-fly-deploy-token.md deleted file mode 100644 index 9abe5f0..0000000 --- a/docs/how-to/configuration/rotate-fly-deploy-token.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Rotate the Fly.io API Token -modified: 2026-05-04 -last-reviewed: 2026-05-04 -tags: - - how-to - - fly-io - - secrets ---- - -# Rotate the Fly.io API Token - -How to rotate the Fly.io API token used to deploy [[flyio-proxy]]. The token lives in 1Password at `op://blumeops/fly.io admin/add more/deploy-token` and is consumed by [`mise run fly-deploy`](../../../mise-tasks/fly-deploy) and the `deploy-fly` Forgejo workflow (via the `FLY_DEPLOY_TOKEN` secret). - -## When to rotate - -- Every 75 days (heph recurring task) -- After any compromise / accidental disclosure -- If `fly deploy` starts returning auth errors - -Fly.io tokens default to a 20-year expiry, but a short rotation cadence limits the blast radius of an undetected leak. Token expiry is set to **90 days** (longer than the rotation window), leaving a 15-day buffer if a rotation is delayed. - -## Scope - -Use **`fly tokens create org`**, not `deploy`. - -| Scope | What it grants | Practical blast radius (this org) | -|-------|---------------|-----------------------------------| -| `deploy` | Manage one app and its resources | Same single-app surface as `org` for current setup | -| `org` | Manage one org and its resources | Adds: ability to create new apps (billing abuse) and read org-level metadata | -| `readonly` | Read one org | Not enough to deploy | -| Personal access token | Full account | Excessive | - -The personal Fly org currently contains a single app (`blumeops-proxy`), so the marginal blast radius of `org` over `deploy` is small. The benefit of `org` is that `fly status` works without a `Metrics token unavailable: ... context canceled` warning. That warning happens because `fly status` always tries to fetch org-level metrics-token info, and an app-scoped `deploy` token can't query the org. The warning is benign but persistent and could mask a real future failure. - -If a second Fly app is ever added to this org, reconsider — at that point the marginal scope cost of `org` grows. - -## Procedure - -### 1. Authenticate flyctl with the current token - -```fish -fly auth login -``` - -(Browser-based. Required to mint a new token, since the existing deploy token can't create tokens.) - -### 2. Mint the new token and store it - -The token is shown only once at creation, so combine the mint and the 1Password write into a single command. Pick the form for your shell. - -`fish`: - -```fish -op item edit on5slfaygtdjrxmdwezyhfmqsq "add more.deploy-token=(fly tokens create org --org personal --name 'blumeops-proxy deploy '(date +%Y-%m-%d) --expiry 2160h)" --vault vg6xf6vvfmoh5hqjjhlhbeoaie -``` - -`bash` / `zsh`: - -```bash -op item edit on5slfaygtdjrxmdwezyhfmqsq "add more.deploy-token=$(fly tokens create org --org personal --name "blumeops-proxy deploy $(date +%Y-%m-%d)" --expiry 2160h)" --vault vg6xf6vvfmoh5hqjjhlhbeoaie -``` - -(`2160h` = 90 days, paired with the 75-day rotation cadence for a 15-day buffer.) - -If you'd rather paste manually: - -```fish -fly tokens create org --org personal --name "blumeops-proxy deploy $(date +%Y-%m-%d)" --expiry 2160h -op item edit on5slfaygtdjrxmdwezyhfmqsq 'add more.deploy-token=' --vault vg6xf6vvfmoh5hqjjhlhbeoaie -``` - -> **op validator gotcha:** If `op item edit` returns `Password item requires ps value`, the item's primary `password` field is empty. The 1Password CLI validator rejects edits to a Password-category item with no primary password, even when you're only touching a section field. Set a placeholder once and future rotations will work: -> -> ```fish -> op item edit on5slfaygtdjrxmdwezyhfmqsq 'password=unused - see deploy-token field' --vault vg6xf6vvfmoh5hqjjhlhbeoaie -> ``` - -### 3. Sync to Forgejo Actions - -The `deploy-fly` workflow reads the same token from a Forgejo Actions secret named `FLY_DEPLOY_TOKEN`, populated by the `forgejo_actions_secrets` ansible role: - -```fish -mise run provision-indri -- --tags forgejo_actions_secrets -``` - -### 4. Verify - -```fish -mise run fly-deploy -``` - -A successful deploy confirms the new token works locally. Watch for the metrics-token warning — it should be **absent** with an `org`-scoped token. If still present, the rotation produced a `deploy`-scoped token by mistake. - -Then trigger the CI workflow (push a no-op commit touching `fly/`, or dispatch manually) to confirm Forgejo Actions has the new secret. - -### 5. Revoke the old token - -```fish -fly tokens list -fly tokens revoke -``` - -## Debugging - -### `fly deploy` returns "unauthorized" - -Token is invalid (expired, revoked, or wrong scope). Repeat the procedure. - -### `Metrics token unavailable: ... context canceled` after rotation - -The new token was created with `deploy` scope, not `org`. Either accept it (cosmetic) or re-mint with `fly tokens create org`. - -### Forgejo Actions deploy fails but local works - -The Forgejo secret wasn't synced. Re-run `mise run provision-indri -- --tags forgejo_actions_secrets` and confirm the secret value in Forgejo matches 1Password. - -## Related - -- [[flyio-proxy]] — Service reference card -- [[manage-flyio-proxy]] — Day-to-day operations and Tailscale auth-key rotation (separate 90-day rotation) -- [[expose-service-publicly]] — Full setup architecture diff --git a/docs/how-to/configuration/rotate-gandi-pat.md b/docs/how-to/configuration/rotate-gandi-pat.md deleted file mode 100644 index 5ce6f81..0000000 --- a/docs/how-to/configuration/rotate-gandi-pat.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Rotate the Gandi PAT -modified: 2026-04-27 -last-reviewed: 2026-04-27 -tags: - - how-to - - dns - - secrets ---- - -# Rotate the Gandi PAT - -How to rotate the Gandi Personal Access Token. **One PAT** is shared by [[caddy]] (TLS via ACME DNS-01) and Pulumi (DNS records). It lives in 1Password at `op://blumeops/gandi - blumeops/pat`. - -## When to rotate - -- Every 60 days (heph recurring task) -- After any compromise / accidental disclosure -- Whenever Gandi starts rejecting the PAT (see [Debugging](#debugging)) - -Gandi caps PAT lifetime at 90 days; rotating at 60 leaves a 30-day buffer. - -## Prerequisites - -- Access to the [Gandi PAT admin console](https://admin.gandi.net/organizations/1db8d76a-f729-11ed-b8d1-00163e94b645/account/pat) -- 1Password (`blumeops` vault) -- Ability to run `mise run provision-indri` (ssh to [[indri]] + 1Password biometric) - -## Procedure - -### 1. Create a new PAT in Gandi - -In the [Gandi PAT console](https://admin.gandi.net/organizations/1db8d76a-f729-11ed-b8d1-00163e94b645/account/pat), create a token: - -- **Name:** `blumeops` -- **Expiration:** **90 days** (the max — paired with the 60-day rotation cadence) -- **Permissions:** - - Manage domain name technical configurations *(required — DNS records and ACME TXT writes)* - - See and renew domain names - -Other permissions are not used. - -Copy the new PAT to your clipboard. - -### 2. Update 1Password - -```bash -op item edit mco6ka3dc3rmw7zkg2dhia5d2m pat="$(pbpaste)" --vault vg6xf6vvfmoh5hqjjhlhbeoaie -``` - -### 3. Push to indri - -The PAT lives in two places: 1Password (read by Pulumi at runtime) and `~/.config/caddy/gandi-token` on indri (read by Caddy at startup). The 1Password edit only updates the first. - -```bash -mise run provision-indri --tags caddy -``` - -This re-fetches the PAT from 1Password, writes it to indri, and restarts Caddy. Caddy will renew any due certificates within minutes. - -### 4. Verify - -```bash -mise run dns-preview -``` - -A successful preview confirms Pulumi can use the PAT. - -```bash -ssh indri 'tail -50 ~/Library/Logs/mcquack.caddy.err.log' \ - | grep -E "obtained|renew|error" -``` - -Expect to see no `LiveDNS returned a 403` lines, and either no renewal activity (if no certs were due) or `certificate obtained successfully`. - -### 5. Delete the old PAT in Gandi - -Return to the Gandi PAT console and delete the previous token. - -### 6. Clean up orphan ACME records - -Each successful Caddy renewal leaves orphan `_acme-challenge.ops` TXT records in the zone (a bug in `libdns/gandi` v1.1.0 — see the script docstring). Cadence aligns with rotation: - -```bash -mise run dns-acme-cleanup --dry-run -mise run dns-acme-cleanup -``` - -## Debugging - -### Caddy logs `LiveDNS returned a 403` - -The PAT is invalid (expired, revoked, or insufficient scope). **Gandi returns 403 — not 401 — for an expired PAT**, which can read as a permissions issue. The most common cause is plain expiry. Rotate. - -### `mise run dns-preview` returns 403 - -Same root cause — Pulumi and Caddy share this PAT. - -### After a fresh PAT, Caddy still fails - -Check that the value on indri matches 1Password: - -```bash -diff <(ssh indri 'cat ~/.config/caddy/gandi-token') \ - <(op read 'op://blumeops/gandi - blumeops/pat') -``` - -If they differ, `mise run provision-indri --tags caddy` was skipped or failed. - -Confirm the new PAT works against Gandi directly: - -```bash -curl -s -o /dev/null -w "HTTP %{http_code}\n" \ - -H "Authorization: Bearer $(op read 'op://blumeops/gandi - blumeops/pat')" \ - https://api.gandi.net/v5/livedns/domains/eblu.me -``` - -`200` = healthy. `403` = scope or expiry. `401` = malformed token. - -## Related - -- [[gandi]] — Gandi reference card -- [[manage-eblu-me-dns]] — DNS records workflow (separate operation, same PAT) -- [[caddy]] — Reverse proxy that uses the PAT for TLS -- [[mise-tasks]] — `dns-acme-cleanup`, `provision-indri`, `dns-preview` reference diff --git a/docs/how-to/configuration/update-tooling-dependencies.md b/docs/how-to/configuration/update-tooling-dependencies.md index 2bfe887..8b09e6d 100644 --- a/docs/how-to/configuration/update-tooling-dependencies.md +++ b/docs/how-to/configuration/update-tooling-dependencies.md @@ -28,45 +28,33 @@ Out of scope: ArgoCD-deployed service images, Ansible role versions, NixOS flake ### 1. Check prek hook versions -For each repo in `prek.toml` with a `rev =` value, check the upstream GitHub releases page for a newer tag. Update each `rev` to the **commit SHA** of the latest release with a trailing `# vX.Y.Z` comment (matches the `additional_dependencies` and Forgejo workflow pinning style). Also check `additional_dependencies` entries for PyPI version bumps and pin them with `==`. +For each repo in `prek.toml` with a `rev =` value, check the upstream GitHub releases page for a newer tag. Update each `rev` to the latest release tag. Also check `additional_dependencies` entries for PyPI version bumps. + +Verify after updating: ```fish -git ls-remote --tags https://github.com//.git 'refs/tags/v*' | sort -t/ -k3 -V | tail -5 -``` - -Clear the prek cache before verifying — it can grow to several GiB (one venv per hook per version) and old cached environments can mask resolution failures or stale catalogs: - -```fish -prek clean prek run --all-files ``` ### 2. Check Fly.io Dockerfile pins -Review `fly/Dockerfile` for pinned image digests. Each `FROM` and `COPY --from=` uses `image@sha256:...` digest pinning with a comment line above documenting the human-readable version. +Review `fly/Dockerfile` for pinned image tags: - **nginx** — check [Docker Hub](https://hub.docker.com/_/nginx) for latest stable alpine tag - **grafana/alloy** — check [GitHub releases](https://github.com/grafana/alloy/releases) -- **tailscale/tailscale** — pinned to a known-good version. Do not bump to v1.96.5 or later (MagicDNS regression breaks the proxy boot) - -To resolve a tag to a digest: - -```fish -docker buildx imagetools inspect docker.io/: -# Use the top-level "Digest:" line (multi-arch index) — not the per-platform sub-digest -``` +- **tailscale/tailscale** — uses `stable` rolling tag, no action needed After updating, the deploy-fly workflow will build and deploy on merge to main. Verify with `fly status -a blumeops-proxy` after deploy. -### 3. Pin mise task dependencies +### 3. Normalize mise task dependency bounds -Mise tasks use `uv run --script` with inline PEP 723 dependency metadata. All packages are pinned with `==` (PEP 508 doesn't support hashes inline). Check that pinned versions are consistent across all scripts: +Mise tasks use `uv run --script` with inline PEP 723 dependency metadata. Check that lower bounds are consistent across all scripts: ```fish grep -r 'dependencies' mise-tasks/ | grep '# dependencies' ``` -For each package in use (`httpx`, `rich`, `typer`, `pyyaml`), pick the latest PyPI version and update every script in lockstep — divergence between scripts is the failure mode this catches. Bump everything together; don't leave one script behind. +Ensure all scripts using the same package agree on the minimum version. When a package has a new major or breaking minor release, bump the lower bound across all scripts at once. ### 4. Pin Forgejo workflow action versions diff --git a/docs/how-to/dagger/upgrade-dagger.md b/docs/how-to/dagger/upgrade-dagger.md index 99058e4..d41ea09 100644 --- a/docs/how-to/dagger/upgrade-dagger.md +++ b/docs/how-to/dagger/upgrade-dagger.md @@ -1,6 +1,6 @@ --- title: Upgrade Dagger -modified: 2026-04-11 +modified: 2026-03-06 last-reviewed: 2026-03-06 tags: - how-to @@ -26,7 +26,7 @@ Dagger versions are pinned in multiple places. The runner job image (which execu | `service-versions.yaml` | `runner-job-image` version and `last-reviewed` | 1 | | `mise.toml` | `dagger` tool version | 2 | | `dagger.json` | `engineVersion` | 2 | -| `uv.lock` | SDK dependency lock (regenerated automatically) | 2 | +| `.dagger/uv.lock` | SDK dependency lock (regenerated automatically) | 2 | | `docs/reference/tools/dagger.md` | Version references in documentation | 2 | | `argocd/manifests/forgejo-runner/deployment.yaml` | `RUNNER_LABELS` image tag | 2 | @@ -43,7 +43,7 @@ The runner job image contains the Dagger CLI binary. Upgrading it first means th 2. Update `service-versions.yaml` — bump `current-version` and `last-reviewed` for `runner-job-image`. -3. Commit and push to main. Trigger a build with `mise run container-build-and-release runner-job-image`. +3. Commit and push to main. The `Build Container` workflow triggers automatically (it watches `containers/**`), building and publishing the new runner-job-image with the updated Dagger CLI. 4. Verify the build succeeds — check the workflow run on Forgejo. Note the image tag from the build output (format: `v-`). @@ -63,7 +63,7 @@ Once the Phase 1 build completes, upgrade the module engine version and deploy t "engineVersion": "v" ``` -4. Regenerate the SDK lock file — run any `dagger call` command (e.g., `dagger call --help` or `dagger functions`). This updates `uv.lock` if SDK dependencies changed. +4. Regenerate the SDK lock file — run any `dagger call` command (e.g., `dagger call --help` or `dagger functions`). This updates `.dagger/uv.lock` if SDK dependencies changed. 5. Update `docs/reference/tools/dagger.md` — bump the version in the Quick Reference table and any version references in the body text. diff --git a/docs/how-to/deployment/build-container-image.md b/docs/how-to/deployment/build-container-image.md index 2f0a980..4b47b3f 100644 --- a/docs/how-to/deployment/build-container-image.md +++ b/docs/how-to/deployment/build-container-image.md @@ -1,6 +1,6 @@ --- title: Build Container Image -modified: 2026-04-11 +modified: 2026-02-24 last-reviewed: 2026-02-15 tags: - how-to @@ -14,8 +14,8 @@ How to create a custom container image in BlumeOps, build it locally, and releas ## Prerequisites -- [Dagger CLI](https://docs.dagger.io/install) installed locally -- A `container.py`, `Dockerfile`, and/or `default.nix` for the service +- [Dagger CLI](https://docs.dagger.io/install) installed locally (for Dockerfile builds) +- A `Dockerfile` and/or `default.nix` for the service ## 1. Create the container directory @@ -23,21 +23,16 @@ Add build files under `containers//`: ``` containers// -├── container.py (native Dagger pipeline — preferred for new containers) -├── Dockerfile (legacy — built via docker_build() fallback) +├── Dockerfile (built by Dagger on the k8s runner) ├── default.nix (built by nix-build on the ringtail runner) └── (optional scripts, configs) ``` -A container can have one or more build files. The directory name becomes the image name: `registry.ops.eblu.me/blumeops/`. - -**New containers for indri (k8s runner) should use `container.py`** — native Dagger pipelines surface full build errors per step, while `docker_build()` (used for Dockerfiles) swallows errors. See `containers/navidrome/container.py` for the reference pattern. Existing Dockerfile containers are migrated incrementally during [[review-services|service reviews]]. - -**Ringtail containers should continue using `default.nix`** — these are built by `nix-build` on the ringtail runner and don't benefit from the Dagger migration. +A container can have one or both build files. The directory name becomes the image name: `registry.ops.eblu.me/blumeops/`. ## 2. Build locally -**Any container** (native `container.py` or legacy Dockerfile) — test with Dagger: +**Dockerfile** — test with Dagger: ```bash dagger call build --src=. --container-name= @@ -57,9 +52,9 @@ nix-build containers//default.nix -o result ## 3. Release -Container builds are triggered manually. Shared Dagger helpers (`src/blumeops/`) affect all Dagger-built containers, making path-based auto-triggers unreliable. +Container builds trigger automatically when changes to `containers//` are merged to `main`. Both workflows fire and each skips if the relevant build file is absent. -To trigger a build: +To trigger a manual build (e.g. from a branch or to rebuild at a specific commit): ```bash mise run container-build-and-release @@ -68,21 +63,12 @@ mise run container-build-and-release --ref Use `--dry-run` to preview without dispatching. -After dispatching, verify the workflow succeeded with `runner-logs`: - -```bash -mise run runner-logs # find the new run number -mise run runner-logs # see jobs and their status -mise run runner-logs -j # fetch full logs (e.g. on failure) -``` - | Build file | Workflow | Runner | Registry tag | |------------|----------|--------|--------------| -| `container.py` | `build-container.yaml` | `k8s` (indri) | `:vX.Y.Z-` | | `Dockerfile` | `build-container.yaml` | `k8s` (indri) | `:vX.Y.Z-` | -| `default.nix` | `build-container.yaml` | `nix-container-builder` ([[ringtail]]) | `:vX.Y.Z--nix` | +| `default.nix` | `build-container-nix.yaml` | `nix-container-builder` ([[ringtail]]) | `:vX.Y.Z--nix` | -The version (`X.Y.Z`) is extracted from `VERSION` in `container.py` (via `dagger call container-version`), `ARG CONTAINER_APP_VERSION=` in Dockerfiles, or `version = "..."` in `default.nix`. The SHA is the short (7-char) commit hash. +The version (`X.Y.Z`) is extracted from `ARG CONTAINER_APP_VERSION=` in the Dockerfile or `version = "..."` in `default.nix`. The SHA is the short (7-char) commit hash. Check available images and tags with: @@ -106,8 +92,8 @@ Container image tags include the git commit SHA they were built from (e.g. `v3.9 **The rule:** Production manifests must reference images built from a commit on main. After merging a PR that changed `containers//`: -1. Trigger a rebuild: `mise run container-build-and-release ` -2. Wait for the workflow to complete — verify with `mise run runner-logs` (find the run, check status) +1. The merge to main automatically triggers a rebuild (the `build-container.yaml` / `build-container-nix.yaml` workflows fire on pushes to `main` that touch `containers/**`) +2. Wait for the workflow to complete — check at `https://forge.eblu.me/eblume/blumeops/actions` 3. Find the new main-SHA tag: ```bash mise run container-list @@ -126,36 +112,36 @@ Existing containers demonstrate several build approaches: | Pattern | Example | Notes | |---------|---------|-------| -| Native Dagger (Go + Node) | [[#navidrome]] | `container.py` with helper functions — preferred for new containers | -| Alpine package install | [[#transmission]] | Simplest Dockerfile — install from apk | -| Go from source | [[#miniflux]] | Dockerfile: clone upstream, `go build` | -| Native Dagger (Elixir + Node) | [[#teslamate]] | `container.py` with Debian runtime — Elixir release with Node assets | -| Runtime tarball download | [[#kiwix-serve]] | Dockerfile: download pre-built binary with arch detection | -| Nix `dockerTools` | [[#ntfy-nix]] | `buildLayeredImage` with nix-built app (ringtail runner) | - -### navidrome - -`containers/navidrome/container.py` — Native Dagger build. Three-stage pipeline using helper functions: `node_build()` for UI, `go_build()` with CGO/taglib/FTS5 for backend, `alpine_runtime()` with ffmpeg. This is the reference pattern for migrating Dockerfile containers to native Dagger builds. +| Alpine package install | [[#transmission]] | Simplest — install from apk | +| Go from source | [[#miniflux]] | Clone upstream, `go build` | +| Multi-stage with Node + Go | [[#navidrome]] | Separate UI and backend build stages | +| Multi-stage Elixir | [[#teslamate]] | Elixir release with Node assets | +| Runtime tarball download | [[#kiwix-serve]] | Download pre-built binary with arch detection | +| Nix `dockerTools` | [[#ntfy-nix]] | `buildLayeredImage` with nix-built app | ### transmission -`containers/transmission/Dockerfile` — Installs transmission-daemon directly from Alpine packages. Good starting point for services available in apk. (Legacy Dockerfile — migrate to `container.py` during review.) +`containers/transmission/Dockerfile` — Installs transmission-daemon directly from Alpine packages. Good starting point for services available in apk. ### miniflux -`containers/miniflux/Dockerfile` — Two-stage Go build. Clones upstream at a pinned version tag, runs `make`, copies the binary into a minimal Alpine runtime. (Legacy Dockerfile — migrate to `container.py` during review.) +`containers/miniflux/Dockerfile` — Two-stage Go build. Clones upstream at a pinned version tag, runs `make`, copies the binary into a minimal Alpine runtime. + +### navidrome + +`containers/navidrome/Dockerfile` — Three-stage build with separate Node.js UI compilation, Go backend build with CGO (taglib), and a minimal Alpine runtime with ffmpeg. ### teslamate -`containers/teslamate/container.py` — Native Dagger build. Two-stage pipeline: Elixir builder with Node.js for asset compilation, Debian slim runtime. Uses Debian-based images (not Alpine) due to Elixir/OTP dependencies. Includes entrypoint script for pg-wait and migrations. +`containers/teslamate/Dockerfile` — Two-stage Elixir build with Node.js asset compilation. Uses Debian-based images due to Elixir/OTP dependencies. ### kiwix-serve -`containers/kiwix-serve/Dockerfile` — Downloads a pre-built binary from upstream, with architecture detection for cross-platform support. (Legacy Dockerfile — migrate to `container.py` during review.) +`containers/kiwix-serve/Dockerfile` — Downloads a pre-built binary from upstream, with architecture detection for cross-platform support. ### ntfy (nix) -`containers/ntfy/default.nix` — Builds ntfy from source using `buildGoModule` and packages it with `dockerTools.buildLayeredImage`. Runs alongside the existing Dockerfile; the nix variant is tagged `:version-nix` in the registry. Nix containers should continue using `default.nix`. +`containers/ntfy/default.nix` — Builds ntfy from source using `buildGoModule` and packages it with `dockerTools.buildLayeredImage`. Runs alongside the existing Dockerfile; the nix variant is tagged `:version-nix` in the registry. ## Related diff --git a/docs/how-to/forgejo-runner/configure-k8s-runner.md b/docs/how-to/forgejo-runner/configure-k8s-runner.md deleted file mode 100644 index 3c095d0..0000000 --- a/docs/how-to/forgejo-runner/configure-k8s-runner.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Configure K8s Forgejo Runner -modified: 2026-04-20 -last-reviewed: 2026-04-20 -tags: - - how-to - - forgejo-runner - - ci ---- - -# Configure K8s Forgejo Runner - -Configure the Kubernetes Forgejo runner on [[indri]] using declarative `server.connections` config instead of first-boot `register`. - -## Why This Flow - -The older bootstrap pattern used `forgejo-runner register` on container start and persisted `/data/.runner` in an `emptyDir`. That works, but it depends on deprecated CLI flows and mutates runner identity at runtime. - -The preferred pattern is: - -- Create runner credentials once on the Forgejo host -- Store the runner UUID and token in 1Password -- Inject them into Kubernetes via [[external-secrets]] -- Render `server.connections` in `argocd/manifests/forgejo-runner/config.yaml` - -This keeps runner identity under secret management and makes pod restarts idempotent. - -## Create Runner Credentials - -On [[indri]], use Forgejo's local CLI instead of the web UI: - -```bash -ssh indri 'cd ~/code/3rd/forgejo && ./forgejo forgejo-cli actions register \ - --name k8s-runner \ - --scope instance \ - --secret "$(openssl rand -hex 32)"' -``` - -This returns a runner UUID. The generated secret becomes the runner token. Store both in 1Password under the "Forgejo Secrets" item as: - -- `runner_k8s_uuid` -- `runner_k8s_token` - -## Kubernetes Secret Wiring - -Expose those fields with `argocd/manifests/forgejo-runner/external-secret.yaml` and make them available to the runner container as environment variables. - -The deployment should not carry registration-only env vars like `FORGEJO_URL`, `RUNNER_NAME`, or `RUNNER_TOKEN`. - -## Runner Config - -Keep the runner configuration in `argocd/manifests/forgejo-runner/config.yaml`. The key change is adopting `server.connections`: - -```yaml -server: - connections: - forgejo: - url: https://forge.ops.eblu.me - uuid: ${FORGEJO_RUNNER_UUID} - token: ${FORGEJO_RUNNER_TOKEN} - labels: - - k8s:docker://registry.ops.eblu.me/blumeops/runner-job-image: -``` - -Other settings that still matter for this deployment: - -- `runner.capacity: 2` -- `runner.timeout: 3h` -- `runner.shutdown_timeout: 3h` -- `container.network: host` -- `container.docker_host: tcp://127.0.0.1:2375` - -We do not currently use cache configuration, extra volume mounts, or multiple Forgejo connections. - -## Deployment Shape - -The pod still runs two containers: - -1. `runner` — Forgejo runner daemon -2. `dind` — Docker-in-Docker sidecar - -The startup script only needs to wait for DinD and then launch the daemon. It should no longer call `forgejo-runner register` or depend on `/data/.runner`. - -## Upgrade Procedure - -When bumping the runner version: - -1. Update `VERSION` in `containers/forgejo-runner/container.py` -2. Review release notes for runner breaking changes -3. Confirm `config.yaml` is still compatible with the current runner defaults -4. Build and release the updated `forgejo-runner` image -5. Update `argocd/manifests/forgejo-runner/kustomization.yaml` to the new image tag -6. Validate workflows with [[validate-forgejo-workflows]] -7. Sync the `forgejo-runner` ArgoCD app and trigger a test workflow - -## Related - -- [[validate-forgejo-workflows]] — Validate workflow schema against the deployed runner line -- [[forgejo-runner]] — Service reference -- [[build-container-image]] — Build and release the runner image diff --git a/docs/how-to/forgejo-runner/review-runner-config-v12.md b/docs/how-to/forgejo-runner/review-runner-config-v12.md new file mode 100644 index 0000000..af50090 --- /dev/null +++ b/docs/how-to/forgejo-runner/review-runner-config-v12.md @@ -0,0 +1,39 @@ +--- +title: Review Runner Config for v12 +modified: 2026-02-27 +last-reviewed: 2026-02-27 +tags: + - how-to + - forgejo-runner + - ci +--- + +# Review Runner Config for v12 + +Compare the current runner ConfigMap against the v12.7.0 default config to identify new, changed, or deprecated keys. + +## Findings + +Compared `forgejo-runner generate-config` output from v6.3.1 and v12.7.0. Our config is minimal and remains valid for v12. + +### New sections in v12 (not adopted) + +- **`server.connections`** — multi-server polling. Not needed (single Forgejo instance). +- **`cache.secret_url`** — load cache secret from file URL. Not needed. +- **`runner.report_retry`** — retry config for log uploads. Defaults are fine. + +### Changed semantics + +- **`container.docker_host`** — v12 supports `unix://` and `ssh://` URLs. Our explicit `tcp://127.0.0.1:2375` still correct for DinD sidecar. +- **`cache`** section restructured with proxy/server split and better docs. We don't configure cache, so defaults apply. + +### Config update applied + +Added `shutdown_timeout: 3h` to allow graceful job completion on pod termination (v12 default, was missing from our v6 config). Added review date comment. + +`container.valid_volumes` and `container.options` left empty — our jobs use host networking and don't mount volumes. Can harden later if needed. + +## Related + +- [[upgrade-k8s-runner]] — Parent goal +- [[validate-workflows-against-v12]] — Sibling prerequisite diff --git a/docs/how-to/forgejo-runner/upgrade-k8s-runner.md b/docs/how-to/forgejo-runner/upgrade-k8s-runner.md new file mode 100644 index 0000000..3d285ac --- /dev/null +++ b/docs/how-to/forgejo-runner/upgrade-k8s-runner.md @@ -0,0 +1,52 @@ +--- +title: Upgrade K8s Forgejo Runner to v12 +modified: 2026-02-27 +last-reviewed: 2026-02-27 +tags: + - how-to + - forgejo-runner + - ci +--- + +# Upgrade K8s Forgejo Runner to v12 + +Upgrade the k8s forgejo-runner daemon from v6.3.1 to v12.7.0 (or latest v12.x at time of execution). + +## Background + +The k8s runner on indri (minikube) uses the upstream `code.forgejo.org/forgejo/runner` image, currently pinned to v6.3.1. The latest is v12.7.0. The runner is still in alpha and uses major version bumps for each breaking change, so v6→v12 crosses six major versions. The ringtail runner is already at ~v12.6.4 via nixpkgs and needs no work. + +Blast radius is low — if the upgrade breaks CI, revert the image tag in `argocd/manifests/forgejo-runner/deployment.yaml` and sync. + +## Breaking Changes Crossed + +| Version | Change | Impact | +|---------|--------|--------| +| v7.0 | CLI `--gitea-instance` → `--forgejo-instance`; `FORGEJO_*` env vars | Low — our registration doesn't use the old flag | +| v8.0 | Workflow schema validation; default image → `node:22-bookworm` | Workflows must pass validation | +| v9.0 | Stricter schema + actions validation; `forgejo-runner validate` added | Same — but now we have a tool | +| v10.0 | Cache isolation; skip v10.0.0 (regression) | Low | +| v11.0 | License MIT → GPLv3 | Non-technical | +| v12.0 | Git binary required; git worktrees for remote actions | Low — OCI image includes git | + +## Execution Steps + +Once prerequisites are met: + +1. Update `argocd/manifests/forgejo-runner/deployment.yaml`: + - Change runner image from `code.forgejo.org/forgejo/runner:6.3.1` to `code.forgejo.org/forgejo/runner:12.7.0` +2. Update `argocd/manifests/forgejo-runner/config.yaml` with any config changes from [[review-runner-config-v12]] +3. Push, sync ArgoCD: `argocd app sync forgejo-runner` +4. Verify runner registers and connects: check Forgejo admin → runners +5. Trigger a test workflow (manual dispatch of `build-container.yaml` or `branch-cleanup.yaml`) +6. Update `service-versions.yaml` to note the daemon version + +## Rollback + +Revert the image tag to `6.3.1` in `deployment.yaml`, push, and sync. + +## Related + +- [[forgejo]] — Forgejo service reference +- [[validate-workflows-against-v12]] — Pre-upgrade workflow validation +- [[review-runner-config-v12]] — Config format review diff --git a/docs/how-to/forgejo-runner/validate-forgejo-workflows.md b/docs/how-to/forgejo-runner/validate-workflows-against-v12.md similarity index 51% rename from docs/how-to/forgejo-runner/validate-forgejo-workflows.md rename to docs/how-to/forgejo-runner/validate-workflows-against-v12.md index ed21de7..1b6cdc0 100644 --- a/docs/how-to/forgejo-runner/validate-forgejo-workflows.md +++ b/docs/how-to/forgejo-runner/validate-workflows-against-v12.md @@ -1,20 +1,20 @@ --- -title: Validate Forgejo Workflows -modified: 2026-04-11 -last-reviewed: 2026-04-20 +title: Validate Workflows Against v12 +modified: 2026-02-27 +last-reviewed: 2026-02-27 tags: - how-to - forgejo-runner - ci --- -# Validate Forgejo Workflows +# Validate Workflows Against v12 -Run `forgejo-runner validate` against all workflow files to catch schema issues before upgrading the k8s runner daemon. +Run `forgejo-runner validate` (available from v9.0+) against all workflow files to catch schema issues before upgrading the k8s runner daemon. ## Result -All current workflows pass the validation step with no changes needed: +All 6 workflows pass v12.7.0 schema validation with no changes needed: - `branch-cleanup.yaml` — OK - `build-blumeops.yaml` — OK @@ -25,9 +25,9 @@ All current workflows pass the validation step with no changes needed: ## Deliverables -1. `validate_workflows` function added to `src/blumeops/main.py` (formerly `.dagger/src/blumeops_ci/main.py`) +1. `validate_workflows` function added to `.dagger/src/blumeops_ci/main.py` - Uses `forgejo-runner validate --directory .` inside the upstream runner container - - `runner_version` parameter pins validation to the deployed runner line + - `runner_version` parameter (default `12.7.0`) pins to deployed version 2. `mise run validate-workflows` task wired to `dagger call validate-workflows` 3. Pre-commit hook triggers on `.forgejo/workflows/` changes @@ -41,4 +41,5 @@ dagger call validate-workflows --src=. ## Related -- [[configure-k8s-runner]] — Runner configuration and upgrade flow +- [[upgrade-k8s-runner]] — Parent goal +- [[review-runner-config-v12]] — Sibling prerequisite diff --git a/docs/how-to/grafana/build-grafana-images.md b/docs/how-to/grafana/build-grafana-images.md index 8a5ca3c..0a5f6fd 100644 --- a/docs/how-to/grafana/build-grafana-images.md +++ b/docs/how-to/grafana/build-grafana-images.md @@ -34,22 +34,23 @@ mise run container-build-and-release grafana ## Grafana Sidecar -**Build:** `containers/grafana-sidecar/container.py` (native Dagger) +**Dockerfile:** `containers/grafana-sidecar/Dockerfile` **Image:** `registry.ops.eblu.me/blumeops/grafana-sidecar` -Clones the [kiwigrid/k8s-sidecar](https://github.com/kiwigrid/k8s-sidecar) source from the forge mirror, installs the Python package into a venv, and copies it into a Python Alpine runtime image. +Clones the [kiwigrid/k8s-sidecar](https://github.com/kiwigrid/k8s-sidecar) source from the forge mirror, installs Python dependencies into a venv, and copies the application into a minimal Alpine runtime image. ```fish -# Update VERSION in container.py +# Update version in Dockerfile +# ARG CONTAINER_APP_VERSION=1.28.0 mise run container-build-and-release grafana-sidecar ``` **Gotchas:** +- **Pinned to v1.28.0:** v2.x has a 135% memory regression ([#462](https://github.com/kiwigrid/k8s-sidecar/issues/462)) and `readOnlyRootFilesystem` crashloop ([#3936](https://github.com/grafana/helm-charts/issues/3936)). Upgrade separately after upstream fixes land. - **UID 65534:** Matches upstream's `nobody` user convention for non-root execution. - **Forge mirror name:** `mirrors/kiwigrid-grafana-sidecar` (not `k8s-sidecar`). -- **Health endpoint:** 2.x exposes `/healthz` on port 8080 (liveness + readiness probes configured in deployment). ## Related diff --git a/docs/how-to/immich/cnpg-on-ringtail.md b/docs/how-to/immich/cnpg-on-ringtail.md deleted file mode 100644 index 153e674..0000000 --- a/docs/how-to/immich/cnpg-on-ringtail.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: CNPG Operator on Ringtail -modified: 2026-05-13 -last-reviewed: 2026-05-13 -tags: - - how-to - - operations - - postgres - - ringtail ---- - -# CNPG Operator on Ringtail - -Bring up the `cloudnative-pg` operator on `k3s-ringtail`. Today the -operator only exists on `minikube-indri` (see -`argocd/apps/cloudnative-pg.yaml`, destination `kubernetes.default.svc`). - -Prerequisite of [[migrate-immich-to-ringtail]]; consumed by -[[immich-pg-on-ringtail]]. - -## What to do - -- Add a sibling `argocd/apps/cloudnative-pg-ringtail.yaml` pointing - at the same mirror (`mirrors/cloudnative-pg`, tag `v1.27.1`), - destination `https://ringtail.tail8d86e.ts.net:6443`, - namespace `cnpg-system`. -- Mirror the `ServerSideApply=true` and `CreateNamespace=true` sync - options (the CRDs exceed the annotation size limit). -- Sync `apps` then `cloudnative-pg-ringtail`. Verify the operator - pod is running on ringtail. - -## Verification - -```fish -kubectl --context=k3s-ringtail -n cnpg-system get pods -kubectl --context=k3s-ringtail get crd clusters.postgresql.cnpg.io -``` - -## Why a separate app - -Each ArgoCD app targets a single cluster via `destination.server`. -We could parameterize with ApplicationSets, but blumeops' convention -is to duplicate the manifest with a `-ringtail` suffix (see -`alloy-ringtail`, `external-secrets-ringtail`, etc.). Keep the -convention. - -## Out of scope - -- Postgres clusters themselves (`immich-pg`, etc.) — those come from - [[immich-pg-on-ringtail]]. -- Removing the minikube cnpg operator. That happens at the very end - of the indri-k8s decommission, not in this chain. diff --git a/docs/how-to/immich/immich-app-on-ringtail.md b/docs/how-to/immich/immich-app-on-ringtail.md deleted file mode 100644 index 51b619d..0000000 --- a/docs/how-to/immich/immich-app-on-ringtail.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Immich App on Ringtail -modified: 2026-05-13 -last-reviewed: 2026-05-13 -tags: - - how-to - - operations - - immich ---- - -# Immich App on Ringtail - -Bring up `immich-server`, `immich-machine-learning`, and -`immich-valkey` on ringtail. This card stands the stack up against -the *new* pg cluster — it does not move user traffic. Cutover lives -in [[immich-cutover-and-decommission]]. - -## What to do - -- New manifest dir `argocd/manifests/immich-ringtail/` (the suffix - matches the `-ringtail` convention used by other apps). Port from - `argocd/manifests/immich/`: - - `deployment-server.yaml` — point `DB_HOSTNAME` at the ringtail - pg service. - - `deployment-ml.yaml` — use `runtimeClassName: nvidia` + a - `resources.limits` for `nvidia.com/gpu: 1`. Use the `-cuda` tag - of the immich-ml image (set in kustomization). Ringtail is - single-node, so no node selector needed. See - `argocd/manifests/frigate/` for the existing GPU pod pattern. - - **GPU contention discovery:** ringtail's `nvidia-device-plugin` - is configured with `timeSlicing.replicas: 2`. Frigate + Ollama - already consume both virtual slices. Adding immich-ml requires - bumping the count to >= 3. Edit - `argocd/manifests/nvidia-device-plugin/configmap.yaml` (or - wherever the device-plugin config lives) and re-sync the - `nvidia-device-plugin` ArgoCD app. The plugin pod restarts and - the new advertised count appears as the node's - `nvidia.com/gpu` allocatable. - - `deployment-valkey.yaml` — straight port, BUT use the upstream - multi-arch `docker.io/valkey/valkey:` image — do NOT - use the `registry.ops.eblu.me/blumeops/valkey` rewrite in the - kustomization. That mirror was built on indri (arm64) and is - single-arch; pulling it on ringtail (amd64) gets `exec format - error` in CrashLoopBackOff. The mirror should eventually carry - a multi-arch tag, at which point the rewrite can return. - - `service*.yaml` — straight port. - - `pvc-ml-cache.yaml` — straight port (empty `local-path` PVC). - - `pv-nfs.yaml` + `pvc.yaml` — already covered by - [[sifaka-nfs-from-ringtail]] (may live in this dir or theirs). - - `ingress-tailscale.yaml` — ProxyGroup ingress, **must not** set - an explicit `host:` (or use `host: *`) per the lesson on - ProxyGroup VIP routing. - **Hostname collision warning:** the minikube ingress claims the - Tailscale device name `photos` (`tls.hosts: [photos]`). Two - devices on the tailnet cannot share that name. While the - ringtail deployment is being staged it must use a *different* - `tls.hosts` value (e.g. `photos-ringtail`) so it can coexist - with the running minikube one. The flip to `photos` happens at - cutover time, *after* the minikube ingress has been removed. - See [[immich-cutover-and-decommission#Cutover sequence]]. - - `kustomization.yaml` — same `images:` block (server, ML, valkey). -- New ArgoCD app `argocd/apps/immich-ringtail.yaml` targeting - ringtail, namespace `immich`. **Manual sync only** until the - cutover. -- Existing `argocd/apps/immich.yaml` (minikube) stays untouched - during this card — both apps exist briefly. - -## Bring it up against a copy of the DB - -Use the throwaway/test path from [[immich-pg-data-migration#Dry run -before real cutover]]: point the ringtail immich at the *test* pg -cluster first, verify the pod boots, the web UI loads (via -`kubectl port-forward`), assets list, ML embeddings query. Then -tear it down. - -## Verification - -- All three pods Ready. -- ML pod has a GPU attached: `nvidia-smi` inside the container shows - the 4080. -- `immich-server` connects to pg and valkey (no `ECONNREFUSED` in - logs). -- A `kubectl port-forward` to the server service shows the Immich - web UI. - -## Out of scope - -- Public/tailnet routing flip. Caddy still points at the minikube - Tailscale ingress until [[immich-cutover-and-decommission]]. -- Removing the minikube immich. Same. diff --git a/docs/how-to/immich/immich-cutover-and-decommission.md b/docs/how-to/immich/immich-cutover-and-decommission.md deleted file mode 100644 index b44fddd..0000000 --- a/docs/how-to/immich/immich-cutover-and-decommission.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Immich Cutover and Decommission -modified: 2026-05-13 -last-reviewed: 2026-05-13 -tags: - - how-to - - operations - - immich - - migration ---- - -# Immich Cutover and Decommission - -The user-visible flip. By the time this card opens, the ringtail -stack has been proven against a copy of the data. This card does the -real cutover. - -## Pre-cutover checklist - -- [[immich-pg-data-migration]] dry-run succeeded; method is chosen. -- Ringtail immich stack has been brought up against the test pg, - pods healthy, UI loaded ([[immich-app-on-ringtail#Verification]]). -- Borgmatic just ran successfully (a fresh nightly archive is a - belt-and-suspenders fallback, on top of the live source pg). -- User has been told to stop uploading from the iOS app for the - cutover window. - -## Cutover sequence - -1. **Quiesce source.** `kubectl --context=minikube-indri -n immich - scale deploy/immich-server --replicas=0` and same for ML. Leave - valkey + pg running. Confirm no client traffic on the source pg - via `pg_stat_activity`. -2. **Tear down the minikube Tailscale ingress.** The `photos` - Tailscale device name must be freed before ringtail's ingress can - claim it (Tailscale enforces uniqueness across the tailnet). - `kubectl --context=minikube-indri -n immich delete ingress - immich-tailscale` and wait for the corresponding `tailscale`-LB - StatefulSet pod to terminate. Verify the `photos` device is gone: - `tailscale status | grep -i photos` from any tailnet host. -3. **Final sync.** Per chosen method in - [[immich-pg-data-migration]]: - - Option A: promote the ringtail replica. - - Option B: take final `pg_dump`, restore to ringtail - `immich-pg`. -4. **Verify.** Run the row-count and schema-diff checks from - [[immich-pg-data-migration#Verification on the real run]]. -5. **Flip the ringtail ingress to `photos`.** Update - `argocd/manifests/immich-ringtail/ingress-tailscale.yaml`: - `tls.hosts: [photos]` (was `[photos-ringtail]` during staging per - [[immich-app-on-ringtail]]). Commit, `argocd app sync - immich-ringtail`. Wait for the `photos` device to register on the - tailnet again. -6. **Bring up ringtail immich** against the now-promoted pg - (`argocd app sync immich-ringtail`). Wait for Ready. -7. **Flip routing.** Update Caddy on indri - (`ansible/roles/caddy/defaults/main.yml`): `photos.ops.eblu.me` - upstream changes to the ringtail Tailscale ingress hostname - (`photos` — same MagicDNS name, now pointing to the ringtail - proxy). `mise run provision-indri -- --tags caddy`. -8. **Smoke test.** Open `photos.ops.eblu.me` in a browser. Sign in. - Scroll the timeline. Open an album. Trigger an ML search. -9. **Update borgmatic.** If the Tailscale hostname for pg changed, - update `borgmatic.cfg` on indri to point at the ringtail - `immich-pg-tailscale` service. Run a manual backup to verify. - -## After cutover - -- `argocd app set immich --revision ` is no longer relevant; - the minikube `immich` app gets deleted entirely. -- Delete `argocd/apps/immich.yaml`, `argocd/manifests/immich/`, and - the minikube `argocd/manifests/databases/immich-pg.yaml` + - `external-secret-immich-borgmatic.yaml` + - `service-immich-pg-tailscale.yaml`. -- Rename `immich-ringtail` back to `immich` (the `-ringtail` suffix - was scaffolding for the dual-cluster window; once minikube is - empty of immich, the unsuffixed name is clean). -- Confirm the minikube `immich-pg` PVC is no longer used, then - delete it (the PV with `Retain` policy will persist — clean that - up too). - -## Verification (definition of done) - -- `photos.ops.eblu.me` works for a real session, including ML search. -- Source minikube has no `immich` pods, no `immich-pg`, no PVCs. -- Memory pressure on minikube has dropped (≥1.5 GiB reclaimed). Check - `docker stats minikube` on indri. -- Nightly borgmatic run after the cutover completes successfully, - with the immich-pg archive showing the new source. - -## Rollback (within the cutover window) - -If smoke test fails: flip Caddy back, scale ringtail immich to 0, -scale source immich back up. Source pg was never destroyed. File a -plan reset on the relevant prerequisite card and try again next -session. - -## Out of scope - -- Decommissioning all of minikube. This chain just removes immich. - Other tenants migrate in their own chains as part of the broader - indri-k8s decommission. See [[migrate-immich-to-ringtail]] for - context. diff --git a/docs/how-to/immich/immich-pg-data-migration.md b/docs/how-to/immich/immich-pg-data-migration.md deleted file mode 100644 index fb87783..0000000 --- a/docs/how-to/immich/immich-pg-data-migration.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Immich Postgres Data Migration -modified: 2026-05-13 -last-reviewed: 2026-05-13 -tags: - - how-to - - operations - - postgres - - immich - - critical ---- - -# Immich Postgres Data Migration - -**This is the data-loss surface of the migration.** Pick a method, -prove it on a throwaway copy first, then run the real cutover. - -## Decision: pick one - -### Option A — CNPG `externalCluster` bootstrap (preferred) - -Stand the ringtail cluster up as a streaming replica of the minikube -cluster via `bootstrap.pg_basebackup.source`. Replica catches up -online; when ready, promote it and point Immich at it. This is -CNPG's documented PG-to-PG migration path and gives near-zero data -loss (the WAL position at promote == the position at app stop). - -Requires: network path from ringtail to minikube's pg over the -tailnet (the existing `immich-pg-tailscale` Service works), and a -superuser secret minikube-side exposed to ringtail's basebackup. - -Pitfall to plan around: the ringtail Cluster CR will need its -`bootstrap` block rewritten *after* promotion (CNPG doesn't -gracefully drop the externalCluster reference). Account for this in -[[immich-pg-on-ringtail]] — it may force a reset of that card. - -### Option B — pg_dump / pg_restore - -Stop immich, `pg_dump -Fc` from minikube, scp to ringtail, restore. -Simpler but full downtime for the whole dump+restore window -(measure on a copy first — VectorChord indexes are slow to rebuild). -Smaller blast radius; no streaming-replication moving parts. - -Use this if Option A hits any blocker. Data loss should still be -zero if the source is stopped first. - -### Option C — leave pg on minikube - -Rejected. See goal card [[migrate-immich-to-ringtail#Why postgres on -ringtail (not cross-cluster)]]. - -## Dry run before real cutover - -Whichever option wins: - -1. Snapshot the minikube `immich-pg` PVC or take a fresh `pg_dump` - into a scratch location. -2. Restore into a *separate* ringtail CNPG cluster (different name, - e.g. `immich-pg-test`) and point a scratch immich-server pod at - it. -3. Verify: pod boots, can list assets, ML embeddings query without - error, face thumbnails render. VectorChord-backed queries should - not error. -4. Tear the scratch cluster down before doing the real one. - -## Verification on the real run - -- Row counts match for `assets`, `albums`, `users`, `face`, - `asset_face`, `smart_search` (the embedding table) — script this. -- `pg_dump --schema-only --no-owner` diff between source and dest - should be empty modulo CNPG-managed roles. -- Immich `/api/server-info/version` and `/api/server-info/statistics` - return sane numbers. - -## Rollback - -If the cutover fails verification: stop the ringtail immich, repoint -ArgoCD `immich.destination` back to minikube, re-sync. Source pg was -never deleted. Document what failed and reset the chain. diff --git a/docs/how-to/immich/immich-pg-on-ringtail.md b/docs/how-to/immich/immich-pg-on-ringtail.md deleted file mode 100644 index 10c7072..0000000 --- a/docs/how-to/immich/immich-pg-on-ringtail.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Immich Postgres Cluster on Ringtail -modified: 2026-05-13 -last-reviewed: 2026-05-13 -tags: - - how-to - - operations - - postgres - - immich ---- - -# Immich Postgres Cluster on Ringtail - -Stand up a fresh `immich-pg` CNPG Cluster on ringtail, ready to receive -data. **No data import yet** — that's [[immich-pg-data-migration]]. - -## What to do - -- Create `argocd/manifests/databases-ringtail/` (or pick another - namespace name — verify what other ringtail pg clusters will use; - if none yet, `databases` is fine). -- Port these from the minikube side: - - `immich-pg.yaml` — CNPG Cluster CR. Same image - (`ghcr.io/tensorchord/cloudnative-vectorchord:17-0.5.0`), same - extensions, same managed `borgmatic` role. Bump `storage.size` if - the minikube 10 GiB looks tight (check actual usage first). - `storageClass: local-path` on ringtail (default). - - `external-secret-immich-borgmatic.yaml` — same 1Password item, - same field, but referencing the ringtail `ClusterSecretStore` - (`onepassword-blumeops` already exists per the - `external-secrets-ringtail` app). - - Service for in-cluster access (the operator creates `immich-pg-rw` - etc. automatically; verify the app deployment uses those names). - - A Tailscale Service if we want backups to keep working via the - same hostname during the transition — see "Borgmatic" below. -- New ArgoCD app `argocd/apps/databases-ringtail.yaml` pointing at - the new path, destination ringtail. - -## Verification - -- Cluster reaches `Ready`. -- `borgmatic` role exists, `rolcanlogin=t`, and is a member of - `pg_read_all_data` (via `managed.roles[].inRoles`). -- ExternalSecret `immich-pg-borgmatic` syncs from 1Password - (`Ready: True`) and the rendered Secret has `username=borgmatic`. -- The `vchord`, `vector`, `cube`, `earthdistance` extensions show - installed in the `postgres` database (`\dx` from - `psql -U postgres`). They are NOT installed in the `immich` - database at this point — `postInitSQL` in CNPG's `initdb` block - runs against the `postgres` superuser database. The Immich app - itself creates the extensions in its own `immich` database at - startup; do not be alarmed by their absence pre-immich-deploy. - The `vchord.so` library is preloaded via - `shared_preload_libraries` regardless, so `CREATE EXTENSION` at - app startup just registers it in the right database. - -## Borgmatic implications - -`borgmatic.cfg` on indri targets `immich-pg-tailscale` over the -tailnet. During migration both clusters will exist briefly. Decide -upfront: backup the *source* pg until cutover, then flip borgmatic -to the ringtail Tailscale service. Document the flip in -[[immich-cutover-and-decommission]]. - -## Out of scope - -- Importing data. That is [[immich-pg-data-migration]], which may - drive a reset on this card if the migration approach (e.g. CNPG - `externalCluster` bootstrap) requires changes to this Cluster CR. diff --git a/docs/how-to/immich/migrate-immich-to-ringtail.md b/docs/how-to/immich/migrate-immich-to-ringtail.md deleted file mode 100644 index e654b62..0000000 --- a/docs/how-to/immich/migrate-immich-to-ringtail.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Migrate Immich to Ringtail -modified: 2026-05-13 -last-reviewed: 2026-05-13 -tags: - - how-to - - operations - - immich - - migration ---- - -# Migrate Immich to Ringtail - -Move the entire Immich stack (server, ML, valkey, postgres) off -`minikube-indri` and onto `k3s-ringtail`. This is the first concrete -chain in the broader indri-k8s decommission: minikube is -memory-saturated (97% RAM, swapping), and Immich is the single -largest tenant (~1.5 GiB resident). - -## End state - -- Immich `server`, `machine-learning`, and `valkey` Deployments run on - ringtail k3s in the `immich` namespace. -- The `immich-machine-learning` pod uses ringtail's RTX 4080 via the - `nvidia-device-plugin` (performance win — currently CPU-only on - minikube). -- A CNPG `immich-pg` Cluster (PostgreSQL 17 + VectorChord) runs in a - `databases` namespace on ringtail, owned by the `cnpg-system` - operator on ringtail. -- The photo library still lives on [[sifaka]] at `/volume1/photos`, - mounted via NFS from ringtail pods (RWX). -- Routing: `photos.ops.eblu.me` (Caddy on indri) proxies to a - Tailscale ProxyGroup ingress on ringtail. No public surface today. -- The ArgoCD `immich` app's `destination.server` points at - `https://ringtail.tail8d86e.ts.net:6443`. The old minikube - manifests are removed. - -## Non-goals - -- Public exposure via Fly. Immich stays tailnet-only. -- Changing the immich version or runtime configuration. This is a - lift-and-shift; bumps come later. -- Backing up to a different target. [[borgmatic]] keeps running on - indri (it pulls via Tailscale and uses sifaka SMB for the library). - -## Critical constraint: no data loss - -Downtime is acceptable (Immich is a single-user system; we can take -it offline for the cutover). **Data loss is not.** Two surfaces matter: - -1. **Postgres** — face data, ML embeddings (vectors), album state, - sharing, etc. Re-derivable in theory; weeks of recompute in - practice. See [[immich-pg-data-migration]]. -2. **Library files** — `/volume1/photos`. Not moving, but the NFS - path must be verified accessible from ringtail before cutover. - See [[sifaka-nfs-from-ringtail]]. - -[[borgmatic]] backs both up to sifaka + BorgBase nightly; restore is -possible but slow. Treat it as a fallback, not a plan. - -## Why postgres on ringtail (not cross-cluster) - -`immich-pg` already has a Tailscale Service we could point ringtail -at, leaving the DB on minikube. We're not doing that because: - -- The whole goal is to retire minikube — keeping pg there blocks it. -- Immich is chatty against pg; tailnet round-trips would hurt. -- CNPG is the same operator on both sides — a Cluster CR on ringtail - is mechanically equivalent. - -## Approach - -This is a C2 Mikado chain. The prerequisite cards each represent a -distinct surface that has to work before cutover. See -[[agent-change-process#C2 — Mikado Chain]] for the discipline. - -## Workflow note: registering new ArgoCD apps during the chain - -This chain adds three new ArgoCD `Application` definitions in -`argocd/apps/`: `cloudnative-pg-ringtail`, `databases-ringtail`, -and (later) `immich-ringtail`. The usual C1/C2 pattern of -`argocd app set --revision && argocd app sync ` -does NOT work for the app-of-apps `apps` Application itself, because -`apps` self-manages: it re-reads `apps.yaml` (which declares -`targetRevision: main`) on every sync and reverts the override. As a -result, new app definitions added on a feature branch are never -visible to the cluster via `apps`. - -**Use `kubectl apply` to register each new Application directly:** - -```fish -kubectl --context=minikube-indri apply -f argocd/apps/.yaml -``` - -This creates the Application resource out-of-band, bypassing `apps`. - -For apps whose source lives in **this** repo (e.g. -`databases-ringtail`, `immich-ringtail` — manifest paths exist only -on the branch until merge), follow the apply with a branch override: - -```fish -argocd app set --revision mikado/migrate-immich-to-ringtail -argocd app sync -``` - -For apps whose source is an **external** repo at a pinned tag (e.g. -`cloudnative-pg-ringtail` → `mirrors/cloudnative-pg` `v1.27.1`), no -override is needed — the source revision is independent of this PR. - -After PR merge: - -```fish -argocd app set --revision main -argocd app sync -``` - -`apps` itself, on its next sync from `main`, will discover the new -Application definitions in `argocd/apps/` and adopt the already-running -resources without disruption — provided their in-cluster spec matches -the on-disk definitions (which it does because we applied the same -file). - -## Related - -- [[migrate-wave1-ringtail]] — the next chain in the indri-k8s - decommission: paperless, teslamate, and mealie -- [[shower-on-ringtail]] — a previous migration to ringtail (simpler: - no upstream cluster, SQLite, no GPU) -- [[connect-to-postgres]] — getting a psql session against CNPG -- [[ringtail]] — the target cluster -- [[cnpg-on-ringtail]], [[immich-pg-on-ringtail]], - [[immich-pg-data-migration]], [[sifaka-nfs-from-ringtail]], - [[immich-app-on-ringtail]], [[immich-cutover-and-decommission]] — - the prerequisite cards diff --git a/docs/how-to/immich/sifaka-nfs-from-ringtail.md b/docs/how-to/immich/sifaka-nfs-from-ringtail.md deleted file mode 100644 index 2c490c1..0000000 --- a/docs/how-to/immich/sifaka-nfs-from-ringtail.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Sifaka NFS Photos from Ringtail -modified: 2026-05-13 -last-reviewed: 2026-05-13 -tags: - - how-to - - operations - - storage - - nfs - - sifaka ---- - -# Sifaka NFS Photos from Ringtail - -The Immich library lives at `sifaka:/volume1/photos` and is mounted -into the pod via an NFS PV (see `argocd/manifests/immich/pv-nfs.yaml`). -That PV is currently scoped to indri. We need ringtail to mount the -same path with the same RWX semantics, without breaking the existing -indri mount during the transition. - -## What to verify / do - -- Check `sifaka` DSM NFS rules for the `photos` share. Per - [[shower-on-ringtail#NFS + SMB share on sifaka]] convention, rules - use `192.168.1.0/24` + `100.64.0.0/10` with - `all_squash`/`Map all users to admin`. The existing rule may - already cover ringtail (it's on `192.168.1.21` per the recent - static-IP pin). If so this card is a verification card. -- If the rule is locked to indri's IP: add an entry for ringtail - (192.168.1.21) or widen to the subnet pattern above. -- Test mount from a ringtail debug pod (busybox or alpine with - nfs-utils) against the `photos` share. Read a file. Write a temp - file. Delete it. -- Watch for the known sifaka NFS-over-Tailscale gotcha: sifaka's - Tailscale must be in TUN mode (not userspace) for NFS to work - reliably over the tailnet. The NFS path here goes over the LAN - (not tailnet), so this shouldn't bite, but worth confirming the - NFS traffic is on `192.168.1.x` not `100.x`. - -## PV + PVC on ringtail - -- New `pv-nfs.yaml` mirroring the minikube one (name can be shared - if the PV is cluster-scoped — but PVs are per-cluster, so just - duplicate). Same `server: sifaka`, same path, same - `accessModes: [ReadWriteMany]`, `persistentVolumeReclaimPolicy: - Retain`. -- New `pvc.yaml` in the ringtail `immich` namespace bound to it. -- The minikube PVC stays bound and active until cutover — both - clusters can have the share NFS-mounted simultaneously (NFS RWX - permits this). Immich itself must not be running on both sides - at once. - -## Verification - -- A pod on ringtail can `ls /mnt/photos/` and see the same files - as the indri pod. -- File written from ringtail pod is visible from indri pod and - vice versa (proves there's no caching surprise). - -## Out of scope - -- Migrating photo files. Nothing moves; this is just adding a second - NFS client. -- The `pvc-ml-cache.yaml` PVC (a separate ML model cache). That's - not on NFS — it's a regular PVC. Recreated empty on ringtail in - [[immich-app-on-ringtail]]; the first ML pod boot will repopulate - it. diff --git a/docs/how-to/knowledgebase/review-services.md b/docs/how-to/knowledgebase/review-services.md index 9969e4c..30b5833 100644 --- a/docs/how-to/knowledgebase/review-services.md +++ b/docs/how-to/knowledgebase/review-services.md @@ -1,7 +1,7 @@ --- title: Review Services -modified: 2026-04-12 -last-reviewed: 2026-04-12 +modified: 2026-03-24 +last-reviewed: 2026-03-07 tags: - how-to - maintenance @@ -47,7 +47,6 @@ For all service types, start by reading the service's reference card (`docs/refe 3. Review the upstream changelog for breaking changes 4. If the service uses a custom-built container, also check the base image for security updates and follow [[build-container-image]] to rebuild 5. If upgrading, update the manifest and follow [[deploy-k8s-service]] -6. If the container still uses a Dockerfile (no `container.py`), consider migrating to a native Dagger build — see the `containers/navidrome/container.py` pattern for reference ### Ansible Services (`type: ansible`) @@ -58,23 +57,9 @@ For all service types, start by reading the service's reference card (`docs/refe ### NixOS Services (`type: nixos`) -Versioned NixOS services (forgejo-runner, snowflake, k3s) are pinned via a `nixpkgs-services` overlay in `nixos/ringtail/flake.nix`. This prevents `nix flake update` from silently upgrading them — they only change when the `nixpkgs-services` input is deliberately updated. - 1. Check the upstream project for new releases -2. Check what version nixpkgs has: `ssh ringtail 'nix eval nixpkgs#.version'` -3. To upgrade, update the `nixpkgs-services` rev in `flake.nix` to a nixpkgs commit that includes the desired version, then run `nix flake update nixpkgs-services` from `nixos/ringtail/` -4. Deploy via `mise run provision-ringtail` -5. Update `service-versions.yaml` with the new version - -### Mise Tools (`type: mise`) - -Development tools managed via `mise.toml` with pinned versions. These are local CLI tools (dagger, pulumi, prek, ty, ansible-core) rather than deployed services. - -1. Check the upstream releases page for new versions -2. Review the changelog for breaking changes -3. Update the pinned version in `mise.toml` -4. Run `mise install` to verify the new version installs correctly -5. Update `service-versions.yaml` with the new version +2. Review the Nix derivation or flake input for version pins +3. If upgrading, update and deploy via `mise run provision-ringtail` ### Private Forge Repos (`upstream-source` under `forge.eblu.me/eblume/`) @@ -129,15 +114,9 @@ After reviewing, edit `service-versions.yaml` (repo root) and update the service Commit this change alongside any upgrades you make during the review. -## Deployment Policy - -BlumeOps uses kustomize manifests for all services. Helm charts should not be introduced for new services. See [[no-helm-policy]] for rationale and migration history. - ## Related -- [[no-helm-policy]] - Why blumeops avoids Helm charts - [[review-documentation]] - Periodically review documentation cards - [[deploy-k8s-service]] - Deploy changes to Kubernetes services - [[build-container-image]] - Build and release custom container images - [[add-ansible-role]] - Add or modify Ansible roles -- [[service-versions]] - Version tracking file reference diff --git a/docs/how-to/mealie/restore-from-borg.md b/docs/how-to/mealie/restore-from-borg.md deleted file mode 100644 index 7ff3625..0000000 --- a/docs/how-to/mealie/restore-from-borg.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: Restore Mealie from Borg -modified: 2026-04-24 -last-reviewed: 2026-04-24 -tags: - - how-to - - mealie - - backup ---- - -# Restore Mealie from Borg - -How to restore [[mealie]]'s SQLite database from a [[borgmatic]] archive when data has been lost (e.g. PVC wiped, accidental deletion, post-DR rebuild). - -## Prerequisites - -- SSH access to [[indri]] (where borgmatic runs and stores k8s SQLite dumps) -- Mealie deployment present in the cluster (the PVC `mealie-data` exists in namespace `mealie`) -- Know which borg archive predates the data loss - -## Procedure - -### 1. Identify a Pre-Loss Archive - -List archives and pick one before the incident: - -```bash -ssh indri 'BORG_PASSCOMMAND="cat /Users/erichblume/.borg/config.yaml" \ - /opt/homebrew/bin/borg list /Volumes/backups/borg | tail -30' -``` - -Compare dump sizes across archives if you're unsure when the loss happened — the daily borgmatic run captures `/Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db`. A sudden drop in size signals the wipe: - -```bash -ssh indri 'bash -c "BORG_PASSCOMMAND=\"cat /Users/erichblume/.borg/config.yaml\" \ - /opt/homebrew/bin/borg list /Volumes/backups/borg:: \ - --pattern=+Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db"' -``` - -### 2. Extract the Pre-Loss Dump - -```bash -ssh indri 'mkdir -p ~/tmp/mealie-restore && cd ~/tmp/mealie-restore && \ - BORG_PASSCOMMAND="cat /Users/erichblume/.borg/config.yaml" \ - /opt/homebrew/bin/borg extract /Volumes/backups/borg:: \ - Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db' -``` - -The file lands at `~/tmp/mealie-restore/Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db` (borg preserves the full path). - -### 3. Verify the Extracted DB - -```bash -ssh indri 'sqlite3 ~/tmp/mealie-restore/Users/erichblume/.local/share/borgmatic/k8s-dumps/mealie.db \ - "PRAGMA integrity_check; SELECT COUNT(*) FROM recipes; SELECT COUNT(*) FROM users;"' -``` - -Expect `ok` and non-zero recipe/user counts. - -### 4. Snapshot the Current (Wiped) DB - -Belt and suspenders — keep a copy of the live DB before overwriting, in case the restore goes wrong: - -```bash -ssh indri 'bash -c "kubectl --context=minikube -n mealie exec deploy/mealie -- \ - python3 -c \"import sqlite3; sqlite3.connect(\\\"/app/data/mealie.db\\\").backup(sqlite3.connect(\\\"/tmp/wiped-mealie.db\\\"))\" && \ - POD=\$(kubectl --context=minikube -n mealie get pod -l app=mealie -o jsonpath=\"{.items[0].metadata.name}\") && \ - kubectl --context=minikube cp mealie/\$POD:/tmp/wiped-mealie.db /Users/erichblume/tmp/mealie-restore/wiped-mealie.db"' -``` - -### 5. Scale Mealie Down - -The PVC is `ReadWriteOnce`, so the helper pod can't mount it while mealie is running: - -```bash -ssh indri 'kubectl --context=minikube -n mealie scale deploy/mealie --replicas=0 && \ - kubectl --context=minikube -n mealie wait --for=delete pod -l app=mealie --timeout=60s' -``` - -### 6. Start a Helper Pod on the PVC - -```bash -ssh indri 'bash -c "cat > /tmp/mealie-helper.yaml <.tar.gz`) | - -`docs_version` in `ansible/roles/docs/defaults/main.yml` is the blumeops release tag (e.g. `v1.16.0`). The role's download/extract is gated by an on-disk sentinel. - -## Deploy - -1. Run the `Build BlumeOps` Forgejo workflow → builds the tarball, creates a release, bumps `docs_version` in the ansible role, pushes to main -2. From gilbert: `mise run provision-indri -- --tags docs` -3. From gilbert: `fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"` - -The Caddy block uses `try_files {path} {path}/ {path}.html` and a `handle_errors 404 → /404.html` rewrite, matching the original nginx behavior so Quartz's clean URLs continue to work. - -## Verify - -```fish -ssh indri 'cat ~/blumeops/docs/.installed-version' -ssh indri 'ls ~/blumeops/docs/content/' -curl -fsSI https://docs.ops.eblu.me/ # private -curl -fsSI https://docs.eblu.me/ # public -curl -fsSI https://docs.eblu.me/explanation/agent-change-process # clean URL → .html fallback -curl -fsSI https://docs.eblu.me/no-such-path-exists/ # → /404.html -``` - -## Bumping the docs version - -Normally driven by the workflow. If you need to pin manually, edit `docs_version` in `ansible/roles/docs/defaults/main.yml` and re-run `mise run provision-indri -- --tags docs`. - -## Backup - -Content dir is not borgmatic-backed. Source is in this repo; release tarballs are on the forge. - -## Rollback - -Set `docs_version` back to the previous release tag in the role defaults and re-run. Older release tarballs remain available as Forgejo release assets. - -## Related - -- [[cv-on-indri]] — sibling service, simpler (no `try_html`) -- [[devpi-on-indri]] — pattern reference for indri-native services -- [[docs]] — service reference diff --git a/docs/how-to/operations/manage-flyio-proxy.md b/docs/how-to/operations/manage-flyio-proxy.md index d1a243d..519481f 100644 --- a/docs/how-to/operations/manage-flyio-proxy.md +++ b/docs/how-to/operations/manage-flyio-proxy.md @@ -1,7 +1,7 @@ --- title: Manage Fly.io Proxy -modified: 2026-04-18 -last-reviewed: 2026-04-18 +modified: 2026-02-08 +last-reviewed: 2026-03-07 tags: - how-to - fly-io @@ -76,20 +76,14 @@ The auth key expires every 90 days. To rotate: 2. Re-run setup to stage the new secret: `mise run fly-setup` 3. Deploy to pick up the new secret: `mise run fly-deploy` -## Rotate Fly.io API Token - -See [[rotate-fly-deploy-token]] for the full rotation procedure (75-day cadence, `org`-scoped). - ## Troubleshooting -**502 Bad Gateway on fresh deploy**: MagicDNS may not be ready when nginx starts. The `start.sh` script polls `nslookup` before launching nginx, but if it still fails, check that `tailscale status` is healthy inside the container. +**502 Bad Gateway**: Check `fly logs` for nginx upstream errors. Verify the backend Tailscale service is running (`tailscale status` from inside the container via `fly ssh console`). **Health check failing**: `fly ssh console -a blumeops-proxy` then `curl localhost:8080/healthz` to test locally. **TLS errors on custom domain**: Check cert status with `fly certs show -a blumeops-proxy`. Certs auto-provision via Let's Encrypt and may take a few minutes. -**High latency (>1s p50)**: Check if direct WireGuard peering is established: `fly ssh console -a blumeops-proxy -C "tailscale ping indri"`. If it shows `via DERP`, the tunnel is relayed and latency will be 10-30s. See [[tailscale#Direct Peering vs DERP Relay]] for diagnosis. - ## Related - [[flyio-proxy]] - Service reference card diff --git a/docs/how-to/operations/read-compliance-reports.md b/docs/how-to/operations/read-compliance-reports.md index e676ad5..1e1b993 100644 --- a/docs/how-to/operations/read-compliance-reports.md +++ b/docs/how-to/operations/read-compliance-reports.md @@ -1,7 +1,7 @@ --- title: Read Compliance Reports -modified: 2026-04-06 -last-reviewed: 2026-04-06 +modified: 2026-03-24 +last-reviewed: 2026-03-24 tags: - how-to - security @@ -12,14 +12,6 @@ tags: How to access and interpret compliance scan reports from [[prowler]] and other security scanners. -## Quick summary - -```fish -mise run review-compliance-reports -``` - -This fetches the latest Prowler report from sifaka, parses it (respecting muted status), compares against the previous week, and shows only actionable unmuted failures. Use `--show-muted` to also see muted findings, or `--full` for complete detail. - ## Accessing reports Reports are stored on sifaka at `/volume1/reports/`. Each scanner writes to its own subdirectory: @@ -80,11 +72,10 @@ Not all failures require action. Common expected failures in our minikube cluste 1. **Triage** — review new failures, distinguish real issues from expected noise 2. **Remediate** — fix what you can (pod security contexts, RBAC tightening) -3. **Mutelist** — suppress expected/accepted failures by adding a Resource entry under the matching Check in `argocd/manifests/prowler/mutelist/*.yaml` with a free-form `Description` explaining why +3. **Mutelist** — suppress expected/accepted failures via Prowler's `--mutelist-file` to reduce noise in future scans 4. **Track** — compare reports over time to spot regressions -## Related +## See also - [[security]] — security & compliance posture overview - [[deploy-prowler]] — Prowler deployment and ad-hoc scans -- [[kingfisher]] — secret detection scanner diff --git a/docs/how-to/operations/rebuild-minikube-cluster.md b/docs/how-to/operations/rebuild-minikube-cluster.md deleted file mode 100644 index 0d924e9..0000000 --- a/docs/how-to/operations/rebuild-minikube-cluster.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -title: Rebuild Minikube Cluster (DR) -modified: 2026-04-13 -last-reviewed: 2026-04-13 -tags: - - how-to - - operations - - disaster-recovery ---- - -# Rebuild Minikube Cluster (DR) - -How to rebuild the minikube cluster from scratch after data loss (e.g., accidental `minikube delete`). This is a DR procedure — for normal restarts, see [[restart-indri]]. - -> **This procedure was validated during a real DR event on 2026-04-13** after a power loss and accidental `minikube delete` destroyed all cluster state. - -## Prerequisites - -- SSH access to indri (dismiss the macOS tailscaled permission dialog first — see [[restart-indri#0. Dismiss macOS Permission Dialogs]]) -- Docker Desktop running on indri -- Tailscale connected -- 1Password CLI (`op`) authenticated - -## Before You Start - -### Clean Stale Tailscale Devices - -Before bringing up the Tailscale operator, **delete stale service devices from the Tailscale admin console** (admin.tailscale.com). Old devices from the destroyed cluster will cause name collisions (new devices get `-1`, `-2` suffixes). - -Look for offline tagged devices like: `pg`, `immich-pg`, `cnpg-metrics`, `ingress-0`, `ingress-1`, and any other `tag:k8s` devices that show "last seen" timestamps from before the rebuild. - -If you miss this step, you'll need to: delete stale devices from the console, delete the Tailscale state secrets in k8s (`kubectl delete secret -n tailscale `), and restart the affected pods. - -> **Watch out for cross-cluster name collisions.** Both indri (minikube) and ringtail (k3s) use a ProxyGroup named `ingress`, producing pods named `ingress-0`, `ingress-1`. Deleting the wrong device can break the other cluster. Check which IPs are active before deleting. This is tech debt — the ProxyGroups should eventually be renamed to `indri-ingress` / `ringtail-ingress`. - -## Phase 1: Start Minikube - -```bash -minikube start --driver=docker --container-runtime=docker \ - --cpus=6 --memory=11264 --disk-size=200g \ - --apiserver-names=k8s.tail8d86e.ts.net --apiserver-names=indri \ - --apiserver-port=6443 --listen-address=0.0.0.0 -``` - -Then run the ansible minikube role to configure Tailscale serve and registry mirrors: - -```bash -mise run provision-indri -- --tags minikube -``` - -## Phase 2: Bootstrap Tailscale Operator - -The Tailscale operator must be deployed before ArgoCD (ArgoCD uses Tailscale Ingress). - -```bash -# 1. Create namespace -kubectl --context=minikube-indri create namespace tailscale - -# 2. Create OAuth secret manually (ExternalSecrets isn't available yet) -CLIENT_ID=$(op read "op://blumeops/Tailscale K8s Operator OAuth/client-id") -CLIENT_SECRET=$(op read "op://blumeops/Tailscale K8s Operator OAuth/client-secret") -kubectl --context=minikube-indri create secret generic operator-oauth -n tailscale \ - --from-literal=client_id="$CLIENT_ID" \ - --from-literal=client_secret="$CLIENT_SECRET" - -# 3. Apply operator manifests -# NOTE: The kustomization fetches from forge.eblu.me which routes through -# Fly → Tailscale → k8s (not yet up). Use forge.ops.eblu.me or github.com/eblume/blumeops. -# Fetch the upstream manifest locally and build a temp kustomization: -curl -s "https://forge.ops.eblu.me/mirrors/tailscale/raw/tag/v1.94.2/cmd/k8s-operator/deploy/manifests/operator.yaml" \ - -o /tmp/ts-operator.yaml -# (create temp kustomization referencing local file — see memory/project_dr_lessons_2026_04.md for details) -kubectl --context=minikube-indri apply -k /tmp/ts-bootstrap/ - -# 4. Apply ProxyGroup for ingress -kubectl --context=minikube-indri apply -f argocd/manifests/tailscale-operator/proxygroup-ingress.yaml -``` - -## Phase 3: Bootstrap ArgoCD - -```bash -# 1. Create namespace -kubectl --context=minikube-indri create namespace argocd - -# 2. Apply ArgoCD (skip ExternalSecret resources — not available yet) -# Create a temp kustomization without external-secret-*.yaml resources. -# Use --server-side --force-conflicts for large CRDs (applicationsets). -kubectl --context=minikube-indri apply -k /tmp/argocd-bootstrap/ --server-side --force-conflicts - -# 3. Wait for ArgoCD -kubectl --context=minikube-indri wait --for=condition=available deployment/argocd-server -n argocd --timeout=300s - -# 4. Create forge SSH repo credentials -PRIV_KEY=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/csjncynh6htjvnh2l2da65y32q/private key?ssh-format=openssh")$'\n' -KNOWN_HOSTS=$(ssh-keyscan -p 2222 forge.ops.eblu.me 2>/dev/null | grep ssh-rsa) -kubectl --context=minikube-indri create secret generic repo-creds-forge -n argocd \ - --from-literal=type=git \ - --from-literal=url='ssh://forgejo@forge.ops.eblu.me:2222/' \ - --from-literal=insecure=false \ - --from-literal=sshPrivateKey="$PRIV_KEY" \ - --from-literal=sshKnownHosts="$KNOWN_HOSTS" -kubectl --context=minikube-indri label secret repo-creds-forge -n argocd argocd.argoproj.io/secret-type=repo-creds - -# 5. Apply app-of-apps -kubectl --context=minikube-indri apply -f argocd/apps/argocd.yaml -kubectl --context=minikube-indri apply -f argocd/apps/apps.yaml - -# 6. Login and sync apps -argocd login argocd.tail8d86e.ts.net --username admin \ - --password "$(kubectl --context=minikube-indri -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d)" \ - argocd app sync apps``` - -## Phase 4: Bootstrap 1Password Connect + External Secrets - -```bash -# 1. Sync foundation -argocd app sync external-secrets-crdsargocd app sync external-secretsargocd app sync 1password-connect -# 2. Create 1Password Connect secrets manually -CREDS_RAW=$(op read "op://blumeops/1Password Connect/credentials-file") -echo "$CREDS_RAW" | kubectl --context=minikube-indri create secret generic op-credentials -n 1password \ - --from-file=1password-credentials.json=/dev/stdin -TOKEN=$(op read "op://blumeops/1Password Connect/token") -kubectl --context=minikube-indri create secret generic onepassword-token -n 1password \ - --from-literal=token="$TOKEN" - -# 3. Wait for 1Password Connect to start, then restart External Secrets -kubectl --context=minikube-indri wait --for=condition=available deployment/onepassword-connect -n 1password --timeout=120s -kubectl --context=minikube-indri rollout restart deployment -n external-secrets external-secrets - -# 4. Verify ClusterSecretStore becomes Valid -kubectl --context=minikube-indri get clustersecretstores -``` - -## Phase 5: Sync Services (Dependency Order) - -```bash -# Foundation (CRDs, operators) -argocd app sync cloudnative-pg kube-state-metrics -# Databases -argocd app sync blumeops-pg -# Observability -argocd app sync loki prometheus tempo grafana grafana-config -# Register ringtail cluster (for authentik, ntfy, ollama, frigate) -ssh ringtail 'sudo cat /etc/rancher/k3s/k3s.yaml' | \ - sed 's|127.0.0.1|ringtail.tail8d86e.ts.net|' > /tmp/k3s-ringtail.yaml -KUBECONFIG=/tmp/k3s-ringtail.yaml argocd cluster add default --name k3s-ringtail --grpc-web -y - -# Authentik (critical — Zot OIDC depends on it, most image pulls depend on Zot) -argocd app sync authentik -# Everything else -argocd app sync tailscale-operator alloy-k8s# ... remaining apps -``` - -## Phase 6: Restore Databases from Borgmatic - -Databases come up empty. Restore from the latest borgmatic backup. - -```bash -# Extract dumps -ssh indri 'mkdir -p /tmp/borg-restore && borgmatic extract --repository /Volumes/backups/borg --archive latest --destination /tmp/borg-restore --path borgmatic/postgresql_databases' - -# Create databases that don't exist yet -kubectl --context=minikube-indri exec -n databases blumeops-pg-1 -c postgres -- \ - psql -U postgres -c "CREATE DATABASE teslamate OWNER teslamate;" -kubectl --context=minikube-indri exec -n databases blumeops-pg-1 -c postgres -- \ - psql -U postgres -c "CREATE DATABASE authentik OWNER authentik;" -# (repeat for other DBs as needed) - -# For teslamate: create extensions BEFORE restoring -kubectl --context=minikube-indri exec -n databases blumeops-pg-1 -c postgres -- \ - psql -U postgres -d teslamate -c "CREATE EXTENSION IF NOT EXISTS cube CASCADE; CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE;" - -# For immich: create extensions BEFORE restoring -kubectl --context=minikube-indri exec -n databases immich-pg-1 -c postgres -- \ - psql -U postgres -d immich -c "CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS vchord CASCADE; CREATE EXTENSION IF NOT EXISTS cube CASCADE; CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS unaccent; CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" - -# Restore (dumps are in custom format — use pg_restore, not psql) -scp indri:/tmp/borg-restore/borgmatic/postgresql_databases/pg.ops.eblu.me:5432/miniflux /tmp/miniflux.sql -kubectl --context=minikube-indri exec -i -n databases blumeops-pg-1 -c postgres -- \ - pg_restore -U postgres -d miniflux --no-owner --role=miniflux < /tmp/miniflux.sql -# (repeat for teslamate, authentik, immich) - -# Reset passwords to match current ExternalSecrets/CNPG-generated credentials -# The restored dumps contain OLD password hashes -PASS=$(kubectl --context=minikube-indri -n databases get secret blumeops-pg-app -o jsonpath='{.data.password}' | base64 -d) -kubectl --context=minikube-indri exec -n databases blumeops-pg-1 -c postgres -- \ - psql -U postgres -c "ALTER USER miniflux WITH PASSWORD '${PASS}';" -# (repeat for each user with the appropriate secret source) - -# Create manually-managed DB secrets -kubectl --context=minikube-indri create secret generic miniflux-db -n miniflux \ - --from-literal=url="$(kubectl --context=minikube-indri -n databases get secret blumeops-pg-app -o jsonpath='{.data.uri}' | base64 -d)" -kubectl --context=minikube-indri create secret generic immich-db -n immich \ - --from-literal=password="$(kubectl --context=minikube-indri -n databases get secret immich-pg-app -o jsonpath='{.data.password}' | base64 -d)" -``` - -## Phase 7: Manual Fixups - -### Forge Tailscale Ingress + Endpoints - -The forge-external Endpoints must be applied manually (ArgoCD excludes Endpoints resources): - -```bash -kubectl --context=minikube-indri apply -f argocd/manifests/tailscale-operator/svc-forge-external.yaml -kubectl --context=minikube-indri apply -f argocd/manifests/tailscale-operator/ingress-forge.yaml -kubectl --context=minikube-indri apply -f argocd/manifests/tailscale-operator/endpoints-forge.yaml -``` - -### Restart Fly.io Proxy - -After the Tailscale ingress ProxyGroup gets new VIPs, the Fly.io proxy's MagicDNS cache may be stale: - -```bash -FLY_API_TOKEN=$(op read "op://blumeops/fly.io admin/deploy-token") fly machine restart --app blumeops-proxy -``` - -### Grafana SQLite - -If Grafana crashes with migration errors (`no such column: help_flags1`), delete its PVC and resync — Grafana is fully stateless (all config provisioned via ConfigMaps). - -## Phase 8: Verify - -```bash -mise run services-check -``` - -## Known Circular Dependencies - -| Dependency | Breaks | Workaround | -|-----------|--------|------------| -| `forge.eblu.me` → Fly → Tailscale → k8s | tailscale-operator kustomization fetch | Fetch manifests from `forge.ops.eblu.me` or `github.com/eblume/blumeops` | -| Forgejo Actions secrets → Forgejo API → Caddy → k8s | Full ansible playbook | Use `--tags minikube` during bootstrap | -| Zot → Authentik OIDC | All container image pulls from Zot | Sync authentik early; Zot will crash-loop until OIDC is reachable | -| ArgoCD Endpoints exclusion → forge-external | Forge Tailscale ingress has no backend | Manual `kubectl apply` for Endpoints | - -## Post-Rebuild: Cold Cache Failures - -Devpi runs natively on indri (see [[devpi-on-indri]]) and is unaffected by minikube rebuilds, so the historical "devpi cold cache after rebuild" failure mode no longer applies. If devpi itself goes cold (fresh server-dir), the same lazy-cache race can still cause `404` on the first Dagger build under concurrent load — re-run the build to warm the cache, or pre-warm with `uv pip install --dry-run --index-url https://pypi.ops.eblu.me/root/pypi/+simple/ dagger-io`. - -## Related - -- [[restart-indri]] — Normal restart procedure (no data loss) -- [[disaster-recovery]] — DR overview -- [[borgmatic]] — Backup restoration -- [[cluster]] — Kubernetes cluster details diff --git a/docs/how-to/operations/restart-indri.md b/docs/how-to/operations/restart-indri.md index e92581e..768ec9a 100644 --- a/docs/how-to/operations/restart-indri.md +++ b/docs/how-to/operations/restart-indri.md @@ -37,11 +37,12 @@ ssh indri 'minikube status' Native services managed by launchd will stop automatically during macOS shutdown. However, if you want to stop them explicitly first: ```bash +# Forgejo (managed by brew services) +ssh indri 'brew services stop forgejo' + # LaunchAgent services -ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.caddy.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.zot.plist' -ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.devpi.plist' # see [[devpi-on-indri]] ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.jellyfin.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.alloy.plist' ssh indri 'launchctl unload ~/Library/LaunchAgents/mcquack.eblume.borgmatic.plist' @@ -67,16 +68,10 @@ Or if you're at the console, use the Apple menu. After indri boots, most services recover automatically. Only a few things need manual attention. -**What autostarts:** Docker Desktop and all mcquack LaunchAgent services (Forgejo, Caddy, Zot, Jellyfin, Alloy, Borgmatic, metrics collectors). +**What autostarts:** Docker Desktop, brew services (Forgejo), and all mcquack LaunchAgent services (Caddy, Zot, Jellyfin, Alloy, Borgmatic, metrics collectors). **What needs manual action:** Amphetamine, AutoMounter, and minikube (including its Tailscale serve port). -> **Warning:** Do NOT run `minikube delete` — it destroys all PersistentVolumes, etcd state, and requires a full DR rebuild. Use `minikube stop` / `minikube start` instead. If minikube is stuck, see [[#Troubleshooting CNI Conflict After Unclean Shutdown]]. For full cluster rebuild, see [[rebuild-minikube-cluster]]. - -### 0. Dismiss macOS Permission Dialogs - -After a cold boot, the **first inbound Tailscale SSH connection** to indri triggers a macOS GUI permission dialog from tailscaled. This blocks the SSH session (and anything downstream like ansible) until dismissed at the console. You must be logged in to indri (via Screen Sharing or physically) to approve it before running any remote commands. - ### 1. Log In and Start GUI Apps Log in to indri (via Screen Sharing or physically) and launch: @@ -110,8 +105,6 @@ Run the minikube ansible role to detect the new port and update Tailscale serve: mise run provision-indri -- --tags minikube ``` -> **Note:** Do NOT run the full `mise run provision-indri` without tags during startup — the `forgejo_actions_secrets` role will timeout because the Forgejo API routes through Caddy → k8s, which isn't up yet. Use `--tags minikube` (or `--tags minikube,minikube_metrics`) to target just the minikube role. - This will: - Start minikube if it hasn't started yet - Detect the current API server port diff --git a/docs/how-to/operations/run-1password-backup.md b/docs/how-to/operations/run-1password-backup.md index 0dc9ec9..b0807da 100644 --- a/docs/how-to/operations/run-1password-backup.md +++ b/docs/how-to/operations/run-1password-backup.md @@ -26,17 +26,19 @@ How to export and encrypt your 1Password vaults for inclusion in [[borgmatic]] b 1. Open the 1Password desktop app 2. **File > Export > All Vaults** 3. Choose **1PUX** format -4. Save to `~/Documents/` — 1Password names the file `1PasswordExport--.1pux` automatically; don't bother renaming it, pass the path to the task in the next step +4. Save to `~/Documents/1Password-export.1pux` ### 2. Run the Backup Task -Pass the exported file's path: - ```fish -mise run op-backup ~/Documents/1PasswordExport-*.1pux +mise run op-backup ``` -(If only one export exists in `~/Documents/`, the glob expands cleanly. Otherwise, paste the full path.) +Or, if you saved the export to a non-default location: + +```fish +mise run op-backup ~/path/to/export.1pux +``` The task will: diff --git a/docs/how-to/operations/shower-on-ringtail.md b/docs/how-to/operations/shower-on-ringtail.md deleted file mode 100644 index daf1046..0000000 --- a/docs/how-to/operations/shower-on-ringtail.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -title: Shower App on Ringtail -modified: 2026-05-10 -last-reviewed: 2026-05-10 -tags: - - how-to - - operations - - kubernetes - - django ---- - -# Shower App on Ringtail - -How the Adelaide / Heidi / Addie baby shower app is deployed. The app is a -Django project ([`adelaide-baby-shower-app`](https://forge.eblu.me/eblume/adelaide-baby-shower-app)) -released as a wheel to the Forgejo Packages PyPI index and run on -[[ringtail]]'s k3s cluster. Public landing page at `shower.eblu.me`, staff -console + admin UI at `shower.ops.eblu.me` (tailnet only). - -The contract this deploy implements is defined in the app repo's -`docs/how-to/hosting.md` — read that for the env-var contract, security -model, and storage requirements before changing anything here. - -## Routing - -``` -Internet → shower.eblu.me - │ (Fly.io nginx — public) - ▼ - Caddy on indri (shower.ops.eblu.me) - │ - ▼ - Tailscale ProxyGroup ingress (shower.tail8d86e.ts.net) - │ - ▼ - Service shower:8000 → Pod (Django + gunicorn) -``` - -| Hostname | Reachable from | Notes | -|---|---|---| -| `shower.eblu.me` | Public internet | Guest surface only — splash, `/prizes//`, `/static/`, `/media/`. Everything authenticated 403s with a tailnet pointer. | -| `shower.ops.eblu.me` | Tailnet | Full app surface — `/host/`, `/admin/`, the works | -| `shower.tail8d86e.ts.net` | Tailnet | Bare ProxyGroup endpoint Caddy proxies to | - -## Defense layers (public side) - -The public surface is guest-only, so the threat model collapses: there -is no credential-accepting endpoint reachable from WAN, and nothing on -WAN that requires authentication. - -1. **edge auth lockout** — fly nginx 403s `/admin/`, `/host/`, and - anything that would redirect into them. Anyone hitting an auth URL - on WAN gets a "tailnet only" message. -2. **fly nginx `limit_req zone=general`** — 10 r/s per Fly-Client-IP - cushion for the splash form. -3. **django-axes** — 5 fails / 1 hour lockout per `(username, ip_address)`, - running on the tailnet-side login. Provides the only credential - defense, since brute-force is only reachable to tailnet members. - -The QR codes that `/host/` (on tailnet) generates for guests embed -`https://shower.eblu.me/...` even though the QR view is served from -the tailnet host. The app's `PUBLIC_URL_BASE` setting (added in v1.0.1) -overrides Django's `request.build_absolute_uri()` for those URLs. - -## Persistent storage - -| Mount | PVC | Type | Why | -|---|---|---|---| -| `/app/media` | `shower-media` | NFS RWX on sifaka (`/volume1/shower`) | Prize photos survive pod rescheduling | -| `/app/data` | `shower-data` | k3s `local-path` RWO | SQLite DB; NFS file locking can't be trusted for WAL/journal | - -The container has the app + its Python deps baked in at nix build time -(`buildPythonPackage` against the wheel fetched from forge PyPI). The -entrypoint runs migrations, runs `collectstatic`, and `exec`s gunicorn — -no pip-at-boot. A `local_settings.py` shim overrides `DATABASES.NAME`, -`MEDIA_ROOT`, and `STATIC_ROOT` to absolute paths under `/app/`, -sidestepping the wheel's `BASE_DIR = parent.parent` of an -in-site-packages settings module. - -## Backups - -[[borgmatic]] (running on indri) captures both halves of the persistent -state on its daily 2 a.m. run: - -- **`/app/data/db.sqlite3`** — dumped via `kubectl exec`'s - `sqlite3.backup()` against the live pod (entry in - `borgmatic_k8s_sqlite_dumps`, context `k3s-ringtail`). The dumped - file lands in `borgmatic_k8s_dump_dir` on indri and is picked up by - the main source-directory sweep. -- **`/app/media`** — picked up via `/Volumes/shower`, the SMB mount of - `sifaka:/volume1/shower` on indri. The same Synology share is exposed - via SMB *and* NFS simultaneously; ringtail's pod uses the NFS export, - while indri reads the SMB side for the borgmatic source. - -Both archive to [[sifaka]] (`borg-backups`) and BorgBase offsite, with -retention `keep_daily=7 / keep_monthly=12 / keep_yearly=1000`. - -The SMB mount on indri is set up manually once via Finder (Cmd-K → -`smb://sifaka/shower`, save credentials, "Always log in" so it -reconnects after reboot). If `/Volumes/shower` is missing at backup -time borgmatic will fail loudly — `source_directories_must_exist: true` -applies to all entries. - -## One-time setup steps - -These steps are required the first time the service is deployed and are -not encoded in the manifests. - -### 1. NFS + SMB share on sifaka - -On the Synology DSM web UI: - -1. **Control Panel → Shared Folder → Create**. Name: `shower`, - Location: Volume 1. Leave the rest at default. -2. **Control Panel → File Services → NFS → NFS Rules** (on the - `shower` row's *Permissions* tab). Add a rule mirroring the other - shares' pattern: Hostname/IP=`192.168.1.0/24` and again for - `100.64.0.0/10`, Privilege=Read/Write, Squash=`Map all users to - admin` (= `all_squash`), and tick *Allow connections from - non-privileged ports*. (See [[sifaka#NFS Exports]] — the existing - `frigate`, `paperless`, etc. shares use this exact pattern.) -3. **Control Panel → File Services → SMB**: leave SMB enabled - globally. No per-share rule required — the share inherits the - default `eblume` access. -4. The directory ownership at `/volume1/shower` will end up - `root:root`, mode `0777` (DSM default) — which is fine because - `all_squash` rewrites every NFS write to `admin:users`, and the - `0777` lets pods read what other pods wrote. No `chown` needed. - -After the share exists, mount it on indri for borgmatic: - -- In Finder, **Cmd-K → `smb://sifaka/shower`**, sign in as `eblume`, - and tick **Remember in Keychain** + **Always log in** so it - reconnects on reboot. This produces `/Volumes/shower`, which the - borgmatic source-directory list points at. - -### 2. 1Password item - -Item name: **`Shower (blumeops)`** in the `blumeops` vault. -Required property: - -| Field | Value | -|---|---| -| `secret-key` | Output of `openssl rand -base64 48` | - -The `ExternalSecret` `shower-app-secrets` will sync this into the -`shower` namespace as a `Secret` and `envFrom` exposes it as -`DJANGO_SECRET_KEY` to the container. - -**Never reuse a key that has ever been in git history.** Per the app's -hosting.md, an early dev key was committed before being replaced with -the `django-insecure-...` placeholder; the production key must be -freshly generated. - -### 3. Container image - -Built by the `build-container` Forgejo Actions workflow on the -`nix-container-builder` runner (ringtail, amd64). The wheel is fetched -from forge PyPI at nix build time and baked into the image — no -pip-at-runtime. To bump the version, change `version` in -`containers/shower/default.nix` and update `wheelHash` (or set it to -`pkgs.lib.fakeHash` and let the next build print the correct one). - -Trigger with: - -```fish -mise run container-build-and-release shower -``` - -After the workflow finishes, update `images[].newTag` in -`argocd/manifests/shower/kustomization.yaml` to the resulting -`vX.Y.Z--nix` tag, then commit (C0). - -### 4. DNS - -`pulumi/gandi/__main__.py` declares the `shower-public` CNAME pointing -at `blumeops-proxy.fly.dev.`. Apply with: - -```fish -mise run dns-preview -mise run dns-up -``` - -### 5. Fly.io certificate - -```fish -fly certs add shower.eblu.me -a blumeops-proxy -``` - -(Add to `mise-tasks/fly-setup` so re-runs of the one-time setup pick -it up.) - -### 6. Caddy on indri - -`shower` is in `ansible/roles/caddy/defaults/main.yml`. Push with: - -```fish -mise run provision-indri -- --tags caddy -``` - -### 7. Create the admin user - -The container's entrypoint runs `migrate --noinput` + `collectstatic ---noinput --clear` before gunicorn, so a fresh `db.sqlite3` is schema- -ready as soon as the pod boots. It does *not* create a Django superuser -— that has to happen once, interactively, after the first pod is up: - -```fish -kubectl --context=k3s-ringtail -n shower exec -it deploy/shower -- \ - python -m django createsuperuser -``` - -Use `erich` / your usual email. The same account doubles as the -`@staff_member_required` login for `/host/`. Subsequent staff accounts -can be created from `/admin/auth/user/` once you're signed in. - -## Deploying a new version - -1. Bump the wheel version in the app repo (`adelaide-baby-shower-app`) - and release it to Forgejo PyPI. -2. Bump `appVersion` in `containers/shower/default.nix` to match. -3. `mise run container-build-and-release shower`. Verify the build - with `mise run runner-logs`. -4. Update the `newTag` in `argocd/manifests/shower/kustomization.yaml` - to the new `[main]` SHA tag. -5. Commit (C0 after PR merge — see [[build-container-image#Squash-merge and container tags]]). -6. `argocd app sync shower`. - -## Verifying after a deploy - -```fish -kubectl --context=k3s-ringtail -n shower get pods -kubectl --context=k3s-ringtail -n shower logs deploy/shower -curl -sf https://shower.ops.eblu.me/ # tailnet -curl -sf https://shower.eblu.me/ # public -curl -I https://shower.eblu.me/admin/users/ # expect 403 (edge block) -curl -I https://shower.ops.eblu.me/admin/ # expect 200 / 302 (login) -``` - -## Related - -- [[expose-service-publicly]] — Fly.io proxy + Tailscale pattern -- [[deploy-k8s-service]] — generic ArgoCD service onboarding -- [[ringtail]] — the cluster -- [`hosting.md`](https://forge.eblu.me/eblume/adelaide-baby-shower-app/src/branch/main/docs/how-to/hosting.md) — app's deployment contract diff --git a/docs/how-to/operations/troubleshoot-sifaka-nfs.md b/docs/how-to/operations/troubleshoot-sifaka-nfs.md deleted file mode 100644 index 85514d4..0000000 --- a/docs/how-to/operations/troubleshoot-sifaka-nfs.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Troubleshoot Sifaka NFS -modified: 2026-03-28 -last-reviewed: 2026-03-28 -tags: - - how-to - - storage - - nfs ---- - -# Troubleshoot Sifaka NFS - -How to diagnose and fix NFS permission failures on [[sifaka]]. - -## Symptom - -NFS mounts from ringtail (or any Tailscale client) to sifaka fail with "Permission denied". Frigate shows empty storage stats. The `frigate-storage` check in `mise run services-check` fails. Existing mounts go stale — even `ls` on the mount point returns EACCES. - -## Root Cause: Tailscale Userspace Networking - -Sifaka runs Tailscale via the Synology DSM package. On DSM 7, the package can run in two modes: - -| Mode | `TUN` flag | NFS sees source IP as | NFS result | -|------|-----------|----------------------|------------| -| **TUN (kernel)** | `True` | Client's Tailscale IP (e.g. `100.121.200.77`) | Works — matches `100.64.0.0/10` export rule | -| **Userspace** | `False` | `127.0.0.1` (loopback) | Fails — doesn't match any export rule | - -In userspace mode, Tailscale proxies connections through loopback. The NFS daemon sees `127.0.0.1` as the source IP, which doesn't match the `100.64.0.0/10` or `192.168.1.0/24` export rules, so it rejects the mount. - -## Diagnosis - -```bash -# Check Tailscale mode on sifaka -ssh sifaka '/var/packages/Tailscale/target/bin/tailscale status --json' | python3 -c "import sys,json; print('TUN:', json.load(sys.stdin).get('TUN'))" - -# If TUN: False, that's the problem - -# Confirm NFS lease failures on ringtail -ssh ringtail 'sudo dmesg | grep -i nfs | tail -5' -# Look for: "check lease failed on NFSv4 server sifaka with error 13" -``` - -## Fix - -The DSM Task Scheduler has a boot-up task ("Enable tailscale outbound TUN") that runs: - -```bash -/var/packages/Tailscale/target/bin/tailscale configure-host; -synosystemctl restart pkgctl-Tailscale.service -``` - -`configure-host` grants the Tailscale package permission to open `/dev/net/tun` (which is `crw-------` root-only by default on DSM 7). The service restart then picks up TUN mode. - -**To fix immediately:** In DSM, go to Control Panel > Task Scheduler, select "Enable tailscale outbound TUN", and click Run. - -**Note:** Running this task restarts Tailscale, which briefly drops all Tailscale connections to sifaka. SSH sessions over Tailscale will disconnect but reconnect within seconds. - -After Tailscale restarts, restart the affected pods to get fresh NFS mounts: - -```bash -kubectl --context=k3s-ringtail rollout restart deployment/frigate -n frigate -``` - -## Why It Recurs - -The "Update Tailscale" scheduled task runs nightly (`tailscale update --yes`). Package updates can reset the TUN device permissions, reverting to userspace mode. The boot-up task only runs at boot, not after updates. - -If this keeps recurring, consider adding `tailscale configure-host` to the update task as well, or running it on a schedule. - -## Related - -- [[sifaka]] — NAS reference card -- [[frigate]] — Primary NFS consumer affected by this issue diff --git a/docs/how-to/operations/troubleshooting.md b/docs/how-to/operations/troubleshooting.md index 84301c3..63dc79a 100644 --- a/docs/how-to/operations/troubleshooting.md +++ b/docs/how-to/operations/troubleshooting.md @@ -72,11 +72,6 @@ kubectl --context=minikube-indri -n get pods --field-selector=status **ArgoCD login expired:** ```bash -argocd login argocd.ops.eblu.me --sso -``` - -If Authentik itself is down, fall back to admin: -```bash argocd login argocd.ops.eblu.me --username admin --password "$(op read 'op://vg6xf6vvfmoh5hqjjhlhbeoaie/srogeebssulhtb6tnqd7ls6qey/password')" ``` diff --git a/docs/how-to/ringtail/migrate-wave1-ringtail.md b/docs/how-to/ringtail/migrate-wave1-ringtail.md deleted file mode 100644 index ffb8cdc..0000000 --- a/docs/how-to/ringtail/migrate-wave1-ringtail.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Migrate Wave 1 (paperless, teslamate, mealie) to Ringtail -modified: 2026-06-03 -last-reviewed: 2026-06-03 -tags: - - how-to - - operations - - ringtail - - migration ---- - -# Migrate Wave 1 to Ringtail - -Move paperless, teslamate, and mealie off `minikube-indri` and onto -`k3s-ringtail`. This is the load-shedding response to minikube going -OOM: the kernel OOM killer was thrashing the 8 GiB node — killing -`kube-apiserver`, `dockerd`, and the argocd application-controller — -which made every minikube-hosted service probe-flap at once. These -three app pods are ~1.1 GiB resident combined and are the heaviest -non-observability tenants left on minikube. Following -[[migrate-immich-to-ringtail]], the first chain in the indri-k8s -decommission. - -## End state - -- `paperless`, `teslamate`, and `mealie` run on ringtail k3s in their - own namespaces, off minikube entirely. -- A CNPG `blumeops-pg` Cluster runs in a `databases` namespace on - ringtail (PostgreSQL, owned by ringtail's `cnpg-system` operator), - holding the `paperless` and `teslamate` databases. Apps reach it - in-cluster via `blumeops-pg-rw.databases.svc.cluster.local`. -- mealie keeps its SQLite database; its 2 GiB `mealie-data` PVC is - copied to a ringtail PVC. -- paperless media still lives on [[sifaka]] via NFS (RWX, 500 GiB), - mounted from ringtail pods. teslamate has no file state. -- Routing: `paperless.ops.eblu.me`, `teslamate.ops.eblu.me`, and - `mealie.ops.eblu.me` (Caddy on indri) proxy to Tailscale - ProxyGroup ingresses on ringtail. Service names are unchanged. -- The minikube manifests and the `paperless`/`teslamate`/`mealie` - databases inside indri's `blumeops-pg` are removed only after - cutover is verified. - -## Non-goals - -- Migrating the rest of `blumeops-pg` (e.g. miniflux) — that is a - later wave. This chain moves only the paperless + teslamate - databases out; the source cluster on indri stays up for the others. -- Version bumps or config changes. Lift-and-shift only. -- Public (Fly) exposure changes. These stay tailnet-only. -- The observability stack (prometheus/loki/tempo/grafana) — deferred; - it carries 50 GiB of local TSDB and is the riskiest move. - -## Critical constraint: no data loss - -**Downtime is acceptable — data loss is not.** We can take each -service fully offline for its cutover, which removes the entire -class of streaming-replication and double-writer hazards. The cold -dump is taken from a *quiesced* source, so it is internally -consistent. - -Data surfaces: - -1. **paperless postgres** — document metadata, tags, correspondents, - the search index state. The document *files* are on NFS and never - move, but losing the DB means files-without-index. This is the - surface to protect most carefully. -2. **teslamate postgres** — drive/charge history. Re-derivable only - from Tesla's API for a limited window; treat as unrecoverable. -3. **mealie SQLite** — recipes, meal plans. On the `mealie-data` PVC. - -The source databases on indri are **never dropped until the ringtail -side is verified and serving**. Rollback is "repoint and scale back -up," not "restore from backup." [[borgmatic]] remains the backstop. - -## Why a fresh CNPG cluster (not cross-cluster pg) - -indri's `blumeops-pg` is already exposed tailnet-wide at -`pg.ops.eblu.me` (Caddy L4), so we *could* leave the DBs on indri and -just move the app pods. We are not, because: - -- The goal is to retire minikube — keeping pg there blocks it and - leaves a cross-host runtime dependency (ringtail apps SPOF on - indri's pg over the tailnet). -- CNPG is the same operator on both clusters; a Cluster CR on ringtail - is mechanically equivalent to the one on minikube. -- Naming the ringtail cluster `blumeops-pg` in `databases` lets apps - use the same in-cluster DNS they would on indri. - -## Cold-cutover procedure (per service) - -Do these one service at a time. paperless first (heaviest, highest -data-sensitivity), then teslamate, then mealie. - -### 0. Prerequisites (once, before any service) - -- Confirm ringtail's `cnpg-system` operator and `databases` namespace - are healthy (immich-pg already runs there). -- Confirm ringtail pods can reach indri's `pg.ops.eblu.me:5432` (used - only to pull the dump) and the sifaka NFS export for paperless - media. See [[sifaka-nfs-from-ringtail]]. -- Define the ringtail `blumeops-pg` CNPG Cluster manifest (model on - `databases-ringtail/immich-pg.yaml`) and its ExternalSecrets for - the per-app roles. Sync it; let it come up empty and healthy. - -### 1. Quiesce the source - -```fish -kubectl --context=minikube-indri -n scale deploy/ --replicas=0 -# confirm 0 running, DB now has no writers -``` - -### 2. Dump from indri, restore to ringtail (postgres apps) - -```fish -# dump the single app DB from the quiesced source -kubectl --context=minikube-indri -n databases exec blumeops-pg-1 -- \ - pg_dump -Fc -d > /tmp/.dump - -# restore into the ringtail cluster -kubectl --context=k3s-ringtail -n databases exec -i blumeops-pg-1 -- \ - pg_restore --no-owner --role= -d < /tmp/.dump -``` - -For **mealie** (SQLite) instead: copy the `mealie-data` PVC contents -to the ringtail PVC (e.g. a one-shot rsync pod mounting both, or -`kubectl cp` via a helper pod). Verify the `.db` file size and that -mealie boots read-only against it. - -### 3. Verify the restore (before any routing flips) - -- Row counts match source for the key tables, scripted: - - paperless: `documents_document`, `documents_tag`, - `documents_correspondent`, `auth_user`. - - teslamate: `cars`, `drives`, `charging_processes`, `positions`. -- `pg_dump --schema-only --no-owner` diff between source and dest is - empty modulo CNPG-managed roles. -- Boot the app against the ringtail DB on its tailnet name *before* - Caddy is flipped, and smoke-test (paperless: documents list + - search; teslamate: dashboard loads recent drives; mealie: recipes - list). - -### 4. Release the service name - -```fish -# delete the minikube tailscale ingress so ringtail can claim the name -kubectl --context=minikube-indri -n delete ingress -tailscale -``` - -### 5. Bring up on ringtail - -- Apply the ringtail manifests (new ArgoCD app `-ringtail`, - `destination.server` = `https://ringtail.tail8d86e.ts.net:6443`). - App points at `blumeops-pg-rw.databases.svc.cluster.local`. -- Sync; wait for healthy + the ProxyGroup ingress to get its name. - -### 6. Flip routing - -- Repoint the Caddy `.ops.eblu.me` upstream at the ringtail - ProxyGroup ingress (provision-indri, caddy role). -- `mise run services-check` — confirm the service flips from FIRING - to OK and no neighbours regressed. - -### 7. Decommission the source (only after verification) - -- Remove the minikube manifests for the app. -- Drop the app DB from indri's `blumeops-pg` (paperless/teslamate) - **last**, once the ringtail side has served real traffic. - -## Rollback - -If a cutover fails verification at any step before §7: - -- Re-create the minikube tailscale ingress (if §4 ran). -- Scale the minikube app back to `1`. -- Repoint Caddy back to the minikube ingress. -- The source DB was never modified or dropped. Document the failure. diff --git a/docs/how-to/zot/add-container-version-sync-check.md b/docs/how-to/zot/add-container-version-sync-check.md index 12d758b..ebf1056 100644 --- a/docs/how-to/zot/add-container-version-sync-check.md +++ b/docs/how-to/zot/add-container-version-sync-check.md @@ -1,6 +1,6 @@ --- title: Add Container Version Sync Check -modified: 2026-04-11 +modified: 2026-02-20 tags: - how-to - containers @@ -10,7 +10,7 @@ tags: # Add Container Version Sync Check -Add a prek check that validates version consistency across the places container versions are declared: `container.py` VERSION constants, Dockerfile ARGs, `service-versions.yaml`, and nix derivations. The check enforces they agree. +Add a prek check that validates version consistency across the three places container versions are declared: Dockerfile ARGs, `service-versions.yaml`, and nix derivations. No VERSION files needed — the existing sources are the source of truth, and the check enforces they agree. ## Context @@ -20,14 +20,13 @@ Discovered during analysis of [[adopt-commit-based-container-tags]]: the new com ### 1. Created `mise run container-version-check` task -A typer-based uv-script that iterates over `containers/*/` and validates six rules per container: +A typer-based uv-script that iterates over `containers/*/` and validates five rules per container: -1. Any `container.py` must declare `VERSION = ""` -2. Any Dockerfile must declare `ARG CONTAINER_APP_VERSION=` -3. Any `default.nix` must produce a version via `dagger call nix-version` -4. At least one build file must exist (`container.py`, Dockerfile, or `default.nix`) -5. A matching `service-versions.yaml` entry must exist with non-null `current-version` -6. All resolved versions from (1), (2), (3), and (5) must agree (v-prefix stripped for comparison) +1. Any Dockerfile must declare `ARG CONTAINER_APP_VERSION=` +2. Any `default.nix` must produce a version via `dagger call nix-version` +3. At least one build file must exist (Dockerfile or default.nix) +4. A matching `service-versions.yaml` entry must exist with non-null `current-version` +5. All resolved versions from (1), (2), and (4) must agree (v-prefix stripped for comparison) Scoping: by default only checks containers changed vs main. `--all-files` checks everything. If `service-versions.yaml` itself changed, all containers are checked. @@ -52,7 +51,7 @@ Filled in `current-version` for all hybrid services: navidrome (v0.60.3), minifl ### ntfy nix version skew (resolved) -The check discovered that ntfy's Dockerfile pinned a newer version than nixpkgs `ntfy-sh` provided. Resolved by replacing the nixpkgs reference in `containers/ntfy/default.nix` with a custom derivation built from the forge mirror. The version check now extracts the version from local nix files via regex, falling back to Dagger for unmodified nixpkgs packages. +The check discovered that ntfy's Dockerfile pins v2.17.0 but nixpkgs has ntfy-sh 2.15.0. This was resolved in [[fix-ntfy-nix-version]] by building a custom nix derivation from the forge mirror. The version check now extracts the version from local nix files via regex, falling back to Dagger for unmodified nixpkgs packages. ## Key Files @@ -68,11 +67,12 @@ The check discovered that ntfy's Dockerfile pinned a newer version than nixpkgs - [x] Intentionally changing a Dockerfile ARG without updating `service-versions.yaml` fails the check - [x] `service-versions.yaml` has `current-version` populated for all hybrid services - [x] Nix-only container versions (authentik) checked via Dagger -- [x] ntfy nix version resolved via custom derivation in `containers/ntfy/default.nix` +- [x] ntfy nix version resolved via [[fix-ntfy-nix-version]] ## Related - [[pin-container-versions]] — Prereq: containers need parseable version ARGs first - [[add-dagger-nix-build]] — Prereq: nix version extraction +- [[fix-ntfy-nix-version]] — Prereq: ntfy nix derivation version skew - [[adopt-commit-based-container-tags]] — Parent: CI uses the same version extraction at build time - [[harden-zot-registry]] — Root goal diff --git a/docs/how-to/zot/add-dagger-nix-build.md b/docs/how-to/zot/add-dagger-nix-build.md index c84661a..fa5f261 100644 --- a/docs/how-to/zot/add-dagger-nix-build.md +++ b/docs/how-to/zot/add-dagger-nix-build.md @@ -1,6 +1,6 @@ --- title: Add Dagger Nix Build Function -modified: 2026-04-11 +modified: 2026-02-20 tags: - how-to - containers @@ -23,7 +23,7 @@ Currently, nix containers can only be built on ringtail (the `nix-container-buil ### 1. Add `build_nix` Dagger function -A new function in `src/blumeops/main.py` (formerly `.dagger/src/blumeops_ci/main.py`) that builds a nix container inside a `nixos/nix` container: +A new function in `.dagger/src/blumeops_ci/main.py` that builds a nix container inside a `nixos/nix` container: ```python @function @@ -80,7 +80,7 @@ The `flake_lock` function already demonstrates running nix inside Dagger using ` | File | Change | |------|--------| -| `src/blumeops/main.py` | Add `build_nix`, `nix_version`, optionally `publish_nix` | +| `.dagger/src/blumeops_ci/main.py` | Add `build_nix`, `nix_version`, optionally `publish_nix` | ## Verification diff --git a/docs/how-to/zot/adopt-commit-based-container-tags.md b/docs/how-to/zot/adopt-commit-based-container-tags.md index 8344ce6..82c90fc 100644 --- a/docs/how-to/zot/adopt-commit-based-container-tags.md +++ b/docs/how-to/zot/adopt-commit-based-container-tags.md @@ -1,6 +1,6 @@ --- title: Adopt Commit-Based Container Tags -modified: 2026-04-11 +modified: 2026-02-20 tags: - how-to - containers @@ -25,11 +25,12 @@ Currently, container builds trigger on git tags matching `-vX.Y.Z`. T ### Triggers -All container builds are triggered manually via `mise run container-build-and-release ` (which dispatches the workflow). Accepts two inputs: -- `container` (required) — which container to build -- `ref` (optional, string) — the source commit SHA to build, defaulting to `GITHUB_SHA` +1. **Merged changes to main** — any push to `main` that modifies files under `containers//` triggers builds for that container +2. **Manual workflow dispatch** — for ad-hoc builds. Accepts two inputs: + - `container` (required) — which container to build + - `ref` (optional, string) — the source commit SHA to build, defaulting to `GITHUB_SHA` -The workflow classifies the container by build type and routes to the correct runner. +Both the Dockerfile and Nix workflows fire for each trigger, each bailing out if the container lacks the relevant build file (same as today). ### Version Source @@ -63,7 +64,7 @@ Where: |------|--------| | `.forgejo/workflows/build-container.yaml` | Replace tag trigger with path + dispatch triggers; compute version and SHA | | `.forgejo/workflows/build-container-nix.yaml` | Same trigger changes; add `-nix` suffix to new tag format | -| `src/blumeops/main.py` | Accept SHA parameter; publish with new tag format | +| `.dagger/src/blumeops_ci/main.py` | Accept SHA parameter; publish with new tag format | | `mise-tasks/container-build-and-release` | New task replacing `container-tag-and-release`; triggers workflow dispatch | | `mise-tasks/container-list` | Updated tag display for new format | | `docs/how-to/deployment/build-container-image.md` | Updated documentation | diff --git a/docs/how-to/zot/enforce-tag-immutability.md b/docs/how-to/zot/enforce-tag-immutability.md index c6f27d2..4d65d82 100644 --- a/docs/how-to/zot/enforce-tag-immutability.md +++ b/docs/how-to/zot/enforce-tag-immutability.md @@ -1,7 +1,6 @@ --- title: Enforce Tag Immutability modified: 2026-02-21 -last-reviewed: 2026-04-14 tags: - how-to - zot @@ -27,4 +26,3 @@ This approach requires authentication to be meaningful — without auth, everyon ## Related - [[harden-zot-registry]] — Parent goal (includes this requirement) -- [[zot]] — Zot registry service reference diff --git a/docs/how-to/zot/fix-ntfy-nix-version.md b/docs/how-to/zot/fix-ntfy-nix-version.md new file mode 100644 index 0000000..cd08efa --- /dev/null +++ b/docs/how-to/zot/fix-ntfy-nix-version.md @@ -0,0 +1,41 @@ +--- +title: Fix ntfy Nix Version +modified: 2026-02-20 +tags: + - how-to + - containers + - nix + - zot +--- + +# Fix ntfy Nix Version + +Override the nixpkgs ntfy-sh derivation to build v2.17.0 from the forge mirror, aligning the nix-built container with the Dockerfile version. + +## Context + +Discovered during [[add-container-version-sync-check]]: the ntfy container has both a Dockerfile and a `default.nix`. The Dockerfile builds v2.17.0 from `forge.ops.eblu.me/mirrors/ntfy.git`, but the nix derivation uses `pkgs.ntfy-sh` from nixpkgs which is pinned at 2.15.0. The version sync check currently excludes ntfy from nix version validation as a workaround. + +## What Was Done + +Replaced the nixpkgs `pkgs.ntfy-sh` reference in `containers/ntfy/default.nix` with a custom derivation that builds v2.17.0 from the forge mirror using `fetchgit`, `buildNpmPackage` (web UI), and `buildGoModule` (server). Docs are skipped (placeholder for `go:embed`, matching the Dockerfile approach). + +The `container-version-check` script was updated to extract versions from local nix files via regex (`version = "X.Y.Z"`) before falling back to the Dagger `nix-version` function for unmodified nixpkgs packages. This avoids the issue where `nix eval nixpkgs#ntfy-sh.version` returns the upstream 2.15.0 instead of our overridden 2.17.0. + +## Key Files + +| File | Change | +|------|--------| +| `containers/ntfy/default.nix` | Custom derivation building v2.17.0 from forge | +| `mise-tasks/container-version-check` | Regex-based local nix version extraction | + +## Verification + +- [x] `dagger call build-nix --src=. --container-name=ntfy` produces a working image +- [x] Version extractable from local `default.nix` via regex (2.17.0) +- [x] `mise run container-version-check --all-files` passes with ntfy included + +## Related + +- [[add-container-version-sync-check]] — Parent: needs ntfy in NIX_PACKAGE_MAP +- [[harden-zot-registry]] — Root goal diff --git a/docs/how-to/zot/harden-zot-registry.md b/docs/how-to/zot/harden-zot-registry.md index d74a5d0..47ca322 100644 --- a/docs/how-to/zot/harden-zot-registry.md +++ b/docs/how-to/zot/harden-zot-registry.md @@ -1,6 +1,6 @@ --- title: Harden Zot Registry -modified: 2026-04-11 +modified: 2026-02-21 tags: - how-to - zot @@ -32,7 +32,7 @@ Updated `ansible/roles/zot/templates/config.json.j2` with: | `ansible/roles/zot/templates/config.json.j2` | Zot config with auth + access control | | `ansible/roles/zot/defaults/main.yml` | OIDC issuer and external URL variables | | `ansible/roles/zot/templates/oidc-credentials.json.j2` | OIDC client credentials | -| `src/blumeops/main.py` | `publish()` with registry auth | +| `.dagger/src/blumeops_ci/main.py` | `publish()` with registry auth | | `.forgejo/workflows/build-container.yaml` | Dagger push with API key | | `.forgejo/workflows/build-container-nix.yaml` | Skopeo push with API key | diff --git a/docs/how-to/zot/install-dagger-on-nix-runner.md b/docs/how-to/zot/install-dagger-on-nix-runner.md new file mode 100644 index 0000000..7d5fda7 --- /dev/null +++ b/docs/how-to/zot/install-dagger-on-nix-runner.md @@ -0,0 +1,32 @@ +--- +title: Install Dagger on Nix Runner +modified: 2026-02-20 +tags: + - how-to + - ci + - zot +--- + +# Install Dagger on Nix Runner + +Use `nix eval` instead of `dagger call nix-version` for version extraction on the ringtail nix-container-builder runner. + +## Context + +The `build-container-nix.yaml` workflow extracts container versions in this order: + +1. `version = "..."` from `default.nix` (e.g. ntfy) +2. `ARG CONTAINER_APP_VERSION=` from Dockerfile (e.g. nettest) +3. Nixpkgs package version for packages without explicit versions (e.g. authentik) + +Step 3 originally used `dagger call nix-version`, but dagger can't run on the bare nix runner: + +- **Dagger is not in nixpkgs** — removed due to [trademark concerns](https://github.com/NixOS/nixpkgs/issues/260848). Available via `github:dagger/nix` flake. +- **Dagger needs a container runtime** — the CLI is just an API client; the engine runs as a container via Docker/containerd, which the nix runner doesn't have. + +The fix was to use `nix eval --raw "nixpkgs#.version"` directly, which is already available on the nix host and more appropriate. + +## Related + +- [[adopt-commit-based-container-tags]] — Parent card +- [[harden-zot-registry]] — Root goal diff --git a/docs/how-to/zot/pin-container-versions.md b/docs/how-to/zot/pin-container-versions.md index b592728..4d0a64c 100644 --- a/docs/how-to/zot/pin-container-versions.md +++ b/docs/how-to/zot/pin-container-versions.md @@ -1,6 +1,6 @@ --- title: Pin Container Versions -modified: 2026-04-11 +modified: 2026-02-20 tags: - how-to - containers @@ -18,15 +18,13 @@ Discovered during analysis of [[adopt-commit-based-container-tags]]: containers ## What Was Done -Every container Dockerfile declares `ARG CONTAINER_APP_VERSION=X.Y.Z` as its first ARG, providing a uniform parsing target. Containers that use the version in build commands chain it to a semantic ARG: +Every container Dockerfile now declares `ARG CONTAINER_APP_VERSION=X.Y.Z` as its first ARG, providing a uniform parsing target. Containers that use the version in build commands chain it to a semantic ARG: ```dockerfile ARG CONTAINER_APP_VERSION=v0.60.3 ARG NAVIDROME_VERSION=${CONTAINER_APP_VERSION} ``` -> **Note:** Containers migrated to native Dagger builds use `VERSION = "X.Y.Z"` in `container.py` instead. See `containers/navidrome/container.py` for the pattern. New containers should use `container.py` rather than Dockerfiles. - Specific changes: - **devpi**: Pinned devpi-server==6.19.1 and devpi-web==5.0.1 - **cv**: `CONTAINER_APP_VERSION=1.0.3` (matches latest Forgejo package release) diff --git a/docs/how-to/zot/register-zot-oidc-client.md b/docs/how-to/zot/register-zot-oidc-client.md index 744b81e..b3696d0 100644 --- a/docs/how-to/zot/register-zot-oidc-client.md +++ b/docs/how-to/zot/register-zot-oidc-client.md @@ -1,7 +1,6 @@ --- title: Register Zot OIDC Client modified: 2026-02-21 -last-reviewed: 2026-04-20 tags: - how-to - zot diff --git a/docs/how-to/zot/wire-ci-registry-auth.md b/docs/how-to/zot/wire-ci-registry-auth.md index e2507c9..cce0655 100644 --- a/docs/how-to/zot/wire-ci-registry-auth.md +++ b/docs/how-to/zot/wire-ci-registry-auth.md @@ -1,6 +1,6 @@ --- title: Wire CI Registry Auth -modified: 2026-04-11 +modified: 2026-02-21 tags: - how-to - zot @@ -36,7 +36,7 @@ Authentication uses a zot API key generated after the service account's first OI | File | Purpose | |------|---------| -| `src/blumeops/main.py` | `publish()` accepts optional `registry_password` | +| `.dagger/src/blumeops_ci/main.py` | `publish()` accepts optional `registry_password` | | `.forgejo/workflows/build-container.yaml` | Passes API key to Dagger | | `.forgejo/workflows/build-container-nix.yaml` | Passes API key to skopeo | | `ansible/playbooks/indri.yml` | Pre_task fetches API key from 1Password | diff --git a/docs/index.md b/docs/index.md index fb04c47..6da90a4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,6 @@ --- title: BlumeOps -modified: 2026-05-06 -last-reviewed: 2026-05-06 +modified: 2026-02-08 aliases: [] id: index tags: [] @@ -23,9 +22,8 @@ raft I built for myself as I went, and you can see it all from within your editor of choice. (I recommend vim.) These services run on my home [[hosts|infrastructure]], primarily an m1 mac -mini named [[indri]], a NixOS GPU host called [[ringtail]] running a k3s -cluster, and a Synology NAS called [[sifaka]]. The infrastructure is networked -via [[tailscale]], with the domain `eblu.me` hosted via [[gandi]], +mini named [[indri]] and a Synology NAS called [[sifaka]]. The infrastructure +is networked via [[tailscale]], with the domain `eblu.me` hosted via [[gandi]], [[caddy]] providing a private reverse proxy for tailnet devices, and [[flyio-proxy|Fly.io]] serving public-facing services like [this documentation site](https://docs.eblu.me). diff --git a/docs/reference/infrastructure/gandi.md b/docs/reference/infrastructure/gandi.md index 763bae3..c374b05 100644 --- a/docs/reference/infrastructure/gandi.md +++ b/docs/reference/infrastructure/gandi.md @@ -1,7 +1,6 @@ --- title: Gandi -modified: 2026-04-27 -last-reviewed: 2026-04-27 +modified: 2026-02-17 tags: - infrastructure - networking @@ -20,11 +19,12 @@ DNS hosting provider for the `eblu.me` domain, managed via Pulumi IaC. | **Provider** | Gandi LiveDNS | | **IaC** | `pulumi/gandi/` | | **Stack** | `eblu-me` | -| **PAT** | `op://blumeops/gandi - blumeops/pat` | ## What It Does -Gandi hosts the DNS records that make `*.ops.eblu.me` resolve to [[indri]]'s Tailscale IP. Since Tailscale IPs are not publicly routable, this gives services real DNS names while keeping them private to the tailnet. The target IP is resolved dynamically from `indri.tail8d86e.ts.net` at deploy time. +Gandi hosts the DNS records that make `*.ops.eblu.me` resolve to [[indri]]'s Tailscale IP (`indri.tail8d86e.ts.net`). Since Tailscale IPs are not publicly routable, this gives services real DNS names while keeping them private to the tailnet. + +The target IP is resolved dynamically from `indri.tail8d86e.ts.net` at deploy time, so if indri's Tailscale IP changes, re-running the deployment is sufficient. ## DNS Records @@ -43,27 +43,39 @@ Both records point to [[indri]], which runs [[caddy]] as the reverse proxy for a |--------|------|-------|-----| | `docs.eblu.me` | CNAME | `blumeops-proxy.fly.dev` | 300s | | `cv.eblu.me` | CNAME | `blumeops-proxy.fly.dev` | 300s | -| `forge.eblu.me` | CNAME | `blumeops-proxy.fly.dev` | 300s | -Public CNAMEs point to [[flyio-proxy]] on Fly.io. See [[expose-service-publicly]] for adding new public services. See [[routing]] for the full service URL map. +Public CNAMEs point to [[flyio-proxy]] on Fly.io. See [[expose-service-publicly]] for adding new public services. + +See [[routing]] for the full service URL map. + +## Pulumi Configuration + +The Pulumi program lives in `pulumi/gandi/`: + +- `__main__.py` - Creates the two A records via `pulumiverse_gandi` +- `Pulumi.eblu-me.yaml` - Stack config (domain, subdomain) + +Stack config values: + +| Key | Value | +|-----|-------| +| `blumeops-dns:domain` | `eblu.me` | +| `blumeops-dns:subdomain` | `ops` | + +A break-glass override is available via the `BLUMEOPS_REVERSE_PROXY_IP` environment variable, which bypasses dynamic IP resolution. ## TLS Integration -[[caddy]] uses this same Gandi PAT for ACME DNS-01 challenges to obtain a wildcard Let's Encrypt certificate for `*.ops.eblu.me`. Caddy reads the PAT from `~/.config/caddy/gandi-token` on [[indri]], populated by ansible from 1Password. +[[caddy]] uses Gandi's API separately (via `GANDI_BEARER_TOKEN`) for ACME DNS-01 challenges to obtain a wildcard Let's Encrypt certificate for `*.ops.eblu.me`. This is a different credential from the Pulumi PAT. ## Authentication -One Gandi Personal Access Token, shared by Pulumi and Caddy. Gandi caps PATs at 90 days; rotate every 60 days via [[rotate-gandi-pat]]. - -## ACME Challenge Cleanup - -Caddy's renewal flow leaves `_acme-challenge.ops` TXT orphans in the zone — a value-comparison bug in `libdns/gandi` v1.1.0 makes the cleanup phase a no-op. Run `mise run dns-acme-cleanup` periodically (alongside PAT rotation works well). +Gandi requires a Personal Access Token (PAT) for API access. PATs have a maximum lifetime of 90 days (currently set to 30). See [[gandi-operations]] for deployment and PAT cycling instructions. ## Related -- [[manage-eblu-me-dns]] — Add/change DNS records via Pulumi -- [[rotate-gandi-pat]] — Rotate the shared Gandi PAT -- [[routing]] — Service URLs and routing architecture -- [[caddy]] — Reverse proxy using this PAT for TLS -- [[tailscale]] — Tailnet networking -- [[indri]] — Server hosting Caddy (DNS target) +- [[gandi-operations]] - PAT cycling and deployment how-to +- [[routing]] - Service URLs and routing architecture +- [[caddy]] - Reverse proxy using Gandi for TLS +- [[tailscale]] - Tailnet networking +- [[indri]] - Server hosting Caddy (DNS target) diff --git a/docs/reference/infrastructure/hosts.md b/docs/reference/infrastructure/hosts.md index 439edf7..f8b07ff 100644 --- a/docs/reference/infrastructure/hosts.md +++ b/docs/reference/infrastructure/hosts.md @@ -1,9 +1,7 @@ --- title: Hosts -modified: 2026-04-11 -last-reviewed: 2026-04-11 +modified: 2026-02-18 tags: - - reference - infrastructure --- @@ -15,12 +13,12 @@ All devices connected via [Tailscale](https://login.tailscale.com/) tailnet `tai | Host | Description | Card | |------|-------------|------| -| **[[indri|Indri]]** | Mac Mini M1, 2020 - Primary server | [[indri|Details]] | -| **[[gilbert|Gilbert]]** | MacBook Air M4, 2025 - Workstation | [[gilbert|Details]] | +| **Indri** | Mac Mini M1, 2020 - Primary server | [[indri|Details]] | +| **Gilbert** | MacBook Air M4, 2025 - Workstation | [[gilbert|Details]] | | **[[sifaka|Sifaka]]** | Synology NAS - Storage & backups | [[sifaka|Details]] | | **[[ringtail|Ringtail]]** | Custom PC, NixOS - Service host & gaming | [[ringtail|Details]] | | **Mouse** | MacBook Air M2 - Allison's laptop | - | -| **[[unifi|UniFi]]** | UniFi Express 7 - Home WiFi | [[unifi|Details]] | +| **UniFi** | UniFi Express 7 - Home WiFi | [[unifi|Details]] | | **Dwarf** | iPad Air - Employer-provided, off tailnet | - | ## Related diff --git a/docs/reference/infrastructure/indri.md b/docs/reference/infrastructure/indri.md index 8364ba0..cbb2a0f 100644 --- a/docs/reference/infrastructure/indri.md +++ b/docs/reference/infrastructure/indri.md @@ -1,7 +1,6 @@ --- title: Indri -modified: 2026-05-27 -last-reviewed: 2026-05-27 +modified: 2026-02-19 tags: - infrastructure - host @@ -16,7 +15,6 @@ Primary BlumeOps server. Mac Mini M1 (2020). | Property | Value | |----------|-------| | **Model** | Mac mini M1, 2020 (Macmini9,1) | -| **CPU / RAM** | 8 cores / 16 GB | | **Storage** | 2TB internal SSD | | **macOS** | 15.7.3 (Sequoia) | | **Tailscale hostname** | `indri.tail8d86e.ts.net` | @@ -32,13 +30,9 @@ Primary BlumeOps server. Mac Mini M1 (2020). - [[borgmatic]] - Backup system - [[alloy|Alloy]] - Metrics/logs collector - [[caddy]] - Reverse proxy for `*.ops.eblu.me` -- [[devpi]] - PyPI mirror (LaunchAgent) -- [[hephaestus]] - heph task/context sync hub (LaunchAgent, self-updating) -- [[cv]] - Static CV site, served by Caddy -- [[docs]] - Quartz-built docs site, served by Caddy **Kubernetes (via minikube):** -- [[apps|Most k8s applications]]. A growing set of apps (Authentik, Frigate, ntfy, Immich, Homepage, Shower, Kingfisher, alloy-ringtail) now run on [[ringtail]]'s k3s instead. Long-term plan is to decommission indri's minikube entirely. +- [[apps|Most k8s applications]] (Frigate, ntfy migrated to [[ringtail]] k3s) **GUI Applications (manual start required):** - Docker Desktop - Container runtime for minikube diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index a4e6837..d5bbd91 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -25,19 +25,6 @@ Service host and gaming PC. Custom-built PC running NixOS. | **OS** | NixOS 25.11 (Sway/Wayland) | | **Tailscale hostname** | `ringtail.tail8d86e.ts.net` | -## Networking - -| Property | Value | -|----------|-------| -| **Interface (wired)** | `enp5s0` | -| **IP** | `192.168.1.21/24` (static, set by NixOS scripted networking) | -| **Gateway** | `192.168.1.1` (UX7) | -| **DNS** | `192.168.1.1`, `1.1.1.1` (used as Tailscale's upstream resolvers; `/etc/resolv.conf` is owned by Tailscale's MagicDNS at `100.100.100.100`) | -| **DHCP reservation** | UniFi "Fixed IP" tied to ringtail's MAC; belt-and-suspenders so the UX7 won't lease `192.168.1.21` to anyone else even though ringtail no longer asks for it | -| **Wireless** | `wlp6s0` still managed by NetworkManager as a fallback path | - -NetworkManager is enabled but explicitly excluded from managing `enp5s0` via `networking.networkmanager.unmanaged = [ "interface-name:enp5s0" ]`. The wired address is configured by a deterministic `network-addresses-enp5s0.service` oneshot — no daemon, no lease, no renewal. - ## Software Managed declaratively via `nixos/ringtail/configuration.nix`. Home-manager handles ringtail-specific sway/waybar config; chezmoi manages cross-platform dotfiles. @@ -121,10 +108,6 @@ A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd servi The runner resolves `` from the flake registry at build time. Container trust policy (`/etc/containers/policy.json`) and registry search order (`/etc/containers/registries.conf`) are configured minimally in `configuration.nix` for skopeo — no full `virtualisation.containers` module needed. -## Pinned Service Versions - -Versioned services (forgejo-runner, snowflake, k3s) are pinned via a `nixpkgs-services` overlay in `flake.nix`, separate from the rolling `nixpkgs` input. This prevents `nix flake update` from silently upgrading them. The Dagger `flake-update` pipeline excludes `nixpkgs-services` automatically. See [[review-services]] for the upgrade procedure. - ## Maintenance Notes **1Password:** Desktop app must be running for `op` CLI. Use `$mod+Shift+minus` to send to scratchpad. diff --git a/docs/reference/infrastructure/routing.md b/docs/reference/infrastructure/routing.md index 6708a92..c85dbb5 100644 --- a/docs/reference/infrastructure/routing.md +++ b/docs/reference/infrastructure/routing.md @@ -1,6 +1,6 @@ --- title: Routing -modified: 2026-04-18 +modified: 2026-03-03 tags: - infrastructure - networking @@ -41,17 +41,15 @@ DNS points to [[indri]]'s Tailscale IP. TLS via Let's Encrypt (ACME DNS-01 with | [[jellyfin]] | https://jellyfin.ops.eblu.me | Media server | | [[postgresql]] | pg.ops.eblu.me:5432 | Database | | [[mealie]] | https://meals.ops.eblu.me | Recipe manager | -| [[paperless]] | https://paperless.ops.eblu.me | Document management | | [[sifaka|Sifaka]] | https://nas.ops.eblu.me | NAS dashboard | ## Public Services (`*.eblu.me`) -DNS CNAMEs point to `blumeops-proxy.fly.dev`. TLS via Fly.io-managed Let's Encrypt. Traffic tunnels back to [[caddy]] on [[indri]] over a direct Tailscale WireGuard connection, then Caddy routes to the service. See [[flyio-proxy]] for details. +DNS CNAMEs point to `blumeops-proxy.fly.dev`. TLS via Fly.io-managed Let's Encrypt. Traffic tunnels back to the homelab over Tailscale. Only services tagged `tag:flyio-target` are reachable by the proxy — see [[flyio-proxy]] for details. | Service | URL | Description | |---------|-----|-------------| | [[docs]] | https://docs.eblu.me | Documentation site | -| [[cv]] | https://cv.eblu.me | CV / resume | | [[forgejo]] | https://forge.eblu.me | Git hosting (public) | ## Tailscale-Only Services diff --git a/docs/reference/infrastructure/tailscale.md b/docs/reference/infrastructure/tailscale.md index 9c15d83..e266a05 100644 --- a/docs/reference/infrastructure/tailscale.md +++ b/docs/reference/infrastructure/tailscale.md @@ -1,7 +1,7 @@ --- title: Tailscale -modified: 2026-04-18 -last-reviewed: 2026-04-18 +modified: 2026-03-22 +last-reviewed: 2026-03-22 tags: - infrastructure - networking @@ -33,10 +33,10 @@ ACLs managed via Pulumi in `pulumi/tailscale/policy.hujson`. | `tag:loki` | indri | Loki log aggregation | | `tag:k8s-api` | indri | Kubernetes API server (minikube) | | `tag:k8s-operator` | (operator pod) | Tailscale operator for k8s — see [[tailscale-operator]] | -| `tag:k8s` | (Ingress proxy pods) | Kubernetes Tailscale Ingress nodes; each also carries a per-service tag (`tag:grafana`, `tag:kiwix`, `tag:feed`, `tag:pg`) | +| `tag:k8s` | (Ingress proxy pods) | Kubernetes Tailscale Ingress nodes; each also carries a per-service tag (`tag:grafana`, `tag:kiwix`, `tag:devpi`, `tag:feed`, `tag:pg`) | | `tag:ci-gateway` | (ephemeral CI containers) | CI containers pushing images to registry | | `tag:flyio-proxy` | (Fly.io proxy container) | Public reverse proxy | -| `tag:flyio-target` | indri, designated Ingress endpoints | Endpoints reachable by the Fly.io proxy (indri for Caddy routing, Ingress pods for Alloy metrics/logs) | +| `tag:flyio-target` | (designated Ingress endpoints) | Endpoints reachable by the Fly.io proxy | **Important:** Don't tag user-owned devices (like gilbert) via Pulumi. Tagging converts them to "tagged devices" which lose user identity and break user-based SSH rules. Gilbert is referenced as `tag:workstation` in tagOwners for ownership purposes but remains user-owned so `blume.erich@gmail.com` identity is preserved. @@ -81,19 +81,6 @@ Pulumi uses OAuth client from 1Password (blumeops vault): - Scopes: acl, dns, devices, services - Auto-applies `tag:blumeops` to IaC-managed resources -## Direct Peering vs DERP Relay - -Just because Tailscale can route traffic does not mean it routes it efficiently. DERP relay servers are a fallback for when direct WireGuard connections cannot be established — they add significant latency (20+ seconds observed under load) because every packet bounces through a relay server. - -**Direct peering is critical for any production-like traffic path.** Check with `tailscale ping ` — it should say `via :`, not `via DERP()`. - -Common reasons direct peering fails: -- **k8s pods**: Tailscale Ingress pods behind pod-network NAT cannot hole-punch. Route through a host-level Tailscale node (e.g., Caddy on indri) instead. -- **Cloud VMs**: Some cloud providers block incoming UDP. Pin the WireGuard port (`tailscaled --port=41641`) and expose it as a UDP service if possible. -- **Double NAT / CGNAT**: Multiple NAT layers make hole punching unreliable. - -The [[flyio-proxy]] uses `--port=41641` pinning to enable direct peering with indri, and routes through [[caddy]] (host-level Tailscale) to avoid the DERP bottleneck of k8s-hosted Tailscale Ingress pods. - ## Related - [[routing|Routing]] - Service URLs diff --git a/docs/reference/kubernetes/apps.md b/docs/reference/kubernetes/apps.md index fd5c06f..02215fc 100644 --- a/docs/reference/kubernetes/apps.md +++ b/docs/reference/kubernetes/apps.md @@ -24,9 +24,9 @@ Registry of all applications deployed via [[argocd]]. | `blumeops-pg` | databases | `argocd/manifests/databases/` | [[postgresql]] | | `prometheus` | monitoring | `argocd/manifests/prometheus/` | [[prometheus]] | | `loki` | monitoring | `argocd/manifests/loki/` | [[loki]] | -| `grafana` | monitoring | `argocd/manifests/grafana/` | [[grafana]] | +| `grafana` | monitoring | Helm chart (forge mirror) | [[grafana]] | | `grafana-config` | monitoring | `argocd/manifests/grafana-config/` | [[grafana]] | -| `immich` | immich | `argocd/manifests/immich/` | [[immich]] | +| `immich` | immich | Helm chart | [[immich]] | | `tempo` | monitoring | `argocd/manifests/tempo/` | [[tempo]] | | `alloy-k8s` | alloy | `argocd/manifests/alloy-k8s/` | [[alloy|Alloy]] | | `alloy-tracing-ringtail` | alloy | `argocd/manifests/alloy-tracing-ringtail/` | [[alloy|Alloy]] (eBPF tracing) | @@ -40,8 +40,6 @@ Registry of all applications deployed via [[argocd]]. | `forgejo-runner` | forgejo-runner | `argocd/manifests/forgejo-runner/` | [[forgejo]] CI | | `ollama` | ollama | `argocd/manifests/ollama/` | [[ollama]] | | `mealie` | mealie | `argocd/manifests/mealie/` | [[mealie]] | -| `paperless` | paperless | `argocd/manifests/paperless/` | [[paperless]] | -| `shower` | shower | `argocd/manifests/shower/` | [[shower-app]] | | `prowler` | prowler | `argocd/manifests/prowler/` | [[prowler]] | ## Sync Policies diff --git a/docs/reference/kubernetes/cluster.md b/docs/reference/kubernetes/cluster.md index 07c14af..9b632bd 100644 --- a/docs/reference/kubernetes/cluster.md +++ b/docs/reference/kubernetes/cluster.md @@ -1,7 +1,6 @@ --- title: Cluster -modified: 2026-06-04 -last-reviewed: 2026-06-04 +modified: 2026-02-19 tags: - kubernetes --- @@ -16,7 +15,7 @@ BlumeOps runs two Kubernetes clusters: a Minikube cluster on [[indri]] (most ser |----------|-------| | **Driver** | docker | | **Container Runtime** | docker | -| **Kubernetes Version** | v1.35.0 | +| **Kubernetes Version** | v1.34.0 | | **CPUs** | 6 | | **Memory** | 11GB | | **Disk** | 200GB | @@ -42,9 +41,7 @@ Single-node k3s cluster for workloads requiring amd64 or GPU access. See [[ringt |----------|-------| | **Context** | `k3s-ringtail` | | **API Server** | `https://ringtail.tail8d86e.ts.net:6443` | -| **Workloads** | GPU workloads (Frigate, Ollama), notifications (ntfy, frigate-notify), [[authentik]], and services migrated off indri minikube (Immich, Mealie, Paperless, TeslaMate). See [[ringtail]] for the authoritative list. | - -Services are being progressively migrated from indri's minikube to ringtail's k3s; the split above reflects an in-progress state, not a fixed boundary. +| **Workloads** | Frigate (GPU), ntfy, frigate-notify, nvidia-device-plugin | ## Related diff --git a/docs/reference/operations/disaster-recovery.md b/docs/reference/operations/disaster-recovery.md index 1a498ba..b144aaf 100644 --- a/docs/reference/operations/disaster-recovery.md +++ b/docs/reference/operations/disaster-recovery.md @@ -14,9 +14,8 @@ Recovery procedures for BlumeOps infrastructure. | Scenario | Guide | |----------|-------| -| Indri reboot/power loss | [[restart-indri]] | -| Full minikube cluster rebuild | [[rebuild-minikube-cluster]] | | Lost 1Password access | [[restore-1password-backup]] | +| Indri reboot/power loss | [[restart-indri]] | ## Components diff --git a/docs/reference/operations/security.md b/docs/reference/operations/security.md index 11c4df9..d66efe1 100644 --- a/docs/reference/operations/security.md +++ b/docs/reference/operations/security.md @@ -24,7 +24,6 @@ Security posture and compliance scanning for BlumeOps infrastructure. - [[prowler]] — CIS Kubernetes Benchmark scanner (weekly CronJob) - [[deploy-prowler]] — deployment and ad-hoc scan how-to - [[read-compliance-reports]] — accessing and interpreting reports -- [[kingfisher]] — Secret detection and live validation for Forgejo repos (weekly CronJob + prek hook) ## Identity & access @@ -46,8 +45,6 @@ Security posture and compliance scanning for BlumeOps infrastructure. All compliance scan reports are stored on `sifaka:/volume1/reports/`. See [[read-compliance-reports]] for access and interpretation. -Suppressed findings are kept in Prowler mutelist YAML under `argocd/manifests/prowler/mutelist/`. Each entry's `Description` field explains why the finding is muted; entries are reviewed ad-hoc rather than on a scheduled cadence. - ## Known gaps - No SOC 2 compliance mapping for Kubernetes (Prowler only maps SOC 2 for AWS/Azure/GCP) diff --git a/docs/reference/operations/service-versions.md b/docs/reference/operations/service-versions.md deleted file mode 100644 index 23d23e1..0000000 --- a/docs/reference/operations/service-versions.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Service Versions -modified: 2026-04-12 -last-reviewed: 2026-04-12 -tags: - - reference - - maintenance - - services ---- - -# Service Versions - -`service-versions.yaml` (repo root) tracks version information for all deployed services and tools in blumeops. Each entry records the service name, deployment type, current version, upstream source, and when it was last reviewed. - -This file enables a regular update cadence via `mise run service-review`, which surfaces stale services sorted by review date. See [[review-services]] for the full review process. - -## Related - -- [[review-services]] — How to review services for version freshness diff --git a/docs/reference/services/1password.md b/docs/reference/services/1password.md index 5ad50da..4489194 100644 --- a/docs/reference/services/1password.md +++ b/docs/reference/services/1password.md @@ -1,7 +1,6 @@ --- title: 1Password -modified: 2026-05-22 -last-reviewed: 2026-05-22 +modified: 2026-02-10 tags: - service - secrets @@ -9,22 +8,15 @@ tags: # 1Password -Root credential store for all BlumeOps secrets. Kubernetes workloads read items via [[external-secrets|External Secrets Operator]]; humans and agents read via the `op` CLI. +Root credential store for all BlumeOps secrets, synced to Kubernetes via External Secrets Operator. -## Vaults - -| Vault | Purpose | -|-------|---------| -| `blumeops` | Infrastructure secrets — referenced by ExternalSecret manifests and scripts. | -| `Personal` | Human login credentials keyed by URL for autofill. Not consumed by infrastructure. | - -## Kubernetes Integration +## Architecture ``` 1Password Cloud | v -1Password Connect (namespace: 1password, deployed on both indri and ringtail) +1Password Connect (namespace: 1password) | v External Secrets Operator (namespace: external-secrets) @@ -33,15 +25,15 @@ External Secrets Operator (namespace: external-secrets) Native Kubernetes Secrets ``` -**ClusterSecretStore:** `onepassword-blumeops` (same name on both clusters). +## Vault -Services reference 1Password items via `ExternalSecret` manifests. Both `minikube-indri` and `k3s-ringtail` run their own `onepassword-connect` deployment talking to the same vault. +The `blumeops` vault contains all infrastructure credentials. -## Direct Access +## Kubernetes Integration -Prefer `op read "op://vault/item/field"` over `op item get --fields` in scripts and IaC — `op item get --fields` wraps multi-line values in quotes, corrupting them. `op item get` without flags is fine for exploring item metadata. +**ClusterSecretStore:** `onepassword-blumeops` -If an item name contains special characters (e.g. parentheses), use the item ID instead of the name in the `op://` path. +Services reference 1Password items via `ExternalSecret` manifests. ## Disaster Recovery Backup @@ -49,9 +41,8 @@ The `mise run op-backup` task encrypts a `.1pux` vault export and transfers it t ## Related -- [[external-secrets]] — Kubernetes operator that consumes ClusterSecretStore -- [[argocd]] — Uses secrets for git access -- [[postgresql]] — Database credentials -- [[run-1password-backup]] — Periodic backup procedure -- [[restore-1password-backup]] — Recovery from backup -- [[borgmatic]] — Backup system +- [[argocd]] - Uses secrets for git access +- [[postgresql]] - Database credentials +- [[run-1password-backup]] - Periodic backup procedure +- [[restore-1password-backup]] - Recovery from backup +- [[borgmatic]] - Backup system diff --git a/docs/reference/services/alloy.md b/docs/reference/services/alloy.md index 97d1e77..d781f2f 100644 --- a/docs/reference/services/alloy.md +++ b/docs/reference/services/alloy.md @@ -1,7 +1,6 @@ --- title: Alloy -modified: 2026-06-04 -last-reviewed: 2026-06-04 +modified: 2026-03-13 tags: - service - observability @@ -21,10 +20,10 @@ Unified observability collector for metrics and logs with three deployments: | **Indri Binary** | `~/.local/bin/alloy` | | **Indri Config** | `~/.config/grafana-alloy/config.alloy` | | **K8s Namespace** | `alloy` | -| **K8s Image** | `registry.ops.eblu.me/blumeops/alloy:v1.16.0-9564435` (locally built) | +| **K8s Image** | `grafana/alloy:v1.14.0` | | **ArgoCD App** | `alloy-k8s` | | **Fly.io Config** | `fly/alloy.river` | -| **Fly.io Image** | `grafana/alloy:v1.16.1` (binary copied into nginx container, sha-pinned) | +| **Fly.io Image** | `grafana/alloy:v1.5.1` (binary copied into nginx container) | ## Metrics Collected diff --git a/docs/reference/services/borgmatic.md b/docs/reference/services/borgmatic.md index 37f1a60..fea4551 100644 --- a/docs/reference/services/borgmatic.md +++ b/docs/reference/services/borgmatic.md @@ -25,7 +25,7 @@ Daily backup system using Borg backup, running on indri. ## What Gets Backed Up **Directories:** -- `~/code/personal/zk` - Zettelkasten (migrating into heph docs; see [hephaestus](https://github.com/eblume/hephaestus)) +- `~/code/personal/zk` - Zettelkasten - `/opt/homebrew/var/forgejo` - Git forge data - `~/.config/borgmatic` - Borgmatic config - `~/Documents` - Personal documents diff --git a/docs/reference/services/caddy.md b/docs/reference/services/caddy.md index 04861ec..daadcb0 100644 --- a/docs/reference/services/caddy.md +++ b/docs/reference/services/caddy.md @@ -1,6 +1,6 @@ --- title: Caddy -modified: 2026-04-18 +modified: 2026-03-15 tags: - service - networking @@ -83,9 +83,7 @@ The token is written to `~/.config/caddy/gandi-token` (chmod 0600) and sourced b ## Security Considerations -Caddy has no authentication layer — it is a plain reverse proxy. Access control relies entirely on Tailscale ACLs restricting which devices can reach indri on port 443. Currently `tag:homelab`, `autogroup:admin`, and `tag:flyio-proxy` (via `tag:flyio-target` on indri) can reach Caddy. - -The [[flyio-proxy]] routes all public traffic through Caddy. This is the path for `*.eblu.me` requests from the public internet. Caddy sees these as requests from the Fly VM with `Host: *.ops.eblu.me` headers — the same routes used by tailnet clients. +Caddy has no authentication layer — it is a plain reverse proxy. Access control relies entirely on Tailscale ACLs restricting which devices can reach indri on port 443. Currently `tag:homelab` and `autogroup:admin` can reach Caddy. The [[flyio-proxy]] no longer routes through Caddy — it pushes logs and metrics directly to [[loki]] and [[prometheus]] via their Tailscale Ingress endpoints. ## Custom Build diff --git a/docs/reference/services/cv.md b/docs/reference/services/cv.md index 1bc5f15..55805d6 100644 --- a/docs/reference/services/cv.md +++ b/docs/reference/services/cv.md @@ -1,7 +1,7 @@ --- title: CV -modified: 2026-04-29 -last-reviewed: 2026-04-29 +modified: 2026-03-27 +last-reviewed: 2026-03-27 tags: - service - resume @@ -15,36 +15,37 @@ Personal resume/CV served as a static HTML page with PDF download, built from YA | Property | Value | |----------|-------| -| **Public URL** | `cv.eblu.me` (via [[flyio-proxy]]) | -| **Private URL** | `cv.ops.eblu.me` (Caddy on indri) | -| **Deployment** | Ansible role `cv` on indri (no daemon — Caddy serves files directly) | -| **Content dir** | `~/blumeops/cv/content/` on indri | +| **URL** | `cv.eblu.me` (public, via [[flyio-proxy]]) | +| **Namespace** | `cv` | +| **Container** | `registry.ops.eblu.me/blumeops/cv` ([kustomization](https://forge.eblu.me/eblume/blumeops/src/branch/main/argocd/manifests/cv/kustomization.yaml)) | | **Source repo** | `forge.eblu.me/eblume/cv` (private, not mirrored to GitHub) | | **Content packages** | `forge.eblu.me/eblume/-/packages` (generic package `cv`) | - -Migrated from minikube to indri-native on 2026-04-29 (see [[cv-on-indri]]). +| **ArgoCD App** | `cv` | ## Architecture 1. **Source**: `resume.yaml` (content) + `template.html` (Jinja2) + `style.css` in the cv repo 2. **Build**: `render.py` (uv script runner) generates `index.html`; WeasyPrint generates `resume.pdf` 3. **Release**: Dagger `build` function packages `index.html`, `style.css`, `resume.pdf` into a tarball, uploaded to Forgejo generic packages -4. **Deploy**: ansible role downloads the tarball into `~/blumeops/cv/content/` on indri; Caddy serves the directory directly +4. **Deploy**: nginx container downloads the tarball at startup via `CV_RELEASE_URL` env var ## Endpoints | Path | Description | |------|-------------| | `/` | Resume HTML page | -| `/resume.pdf` | PDF download (Caddy adds `Content-Disposition: attachment`) | +| `/resume.pdf` | PDF download (Content-Disposition: attachment) | +| `/healthz` | Health check (200 OK) | ## Configuration **Key files (blumeops):** -- `ansible/roles/cv/defaults/main.yml` — pinned `cv_version` and tarball URL -- `ansible/roles/cv/tasks/main.yml` — sentinel-gated download + extract -- `ansible/roles/caddy/defaults/main.yml` — `cv` service entry (`kind: static`, `download_paths` for the PDF) +- `containers/cv/Dockerfile` — nginx:alpine container +- `containers/cv/start.sh` — tarball download + extraction +- `containers/cv/default.conf` — nginx config (gzip, caching, PDF headers) +- `argocd/manifests/cv/deployment.yaml` — `CV_RELEASE_URL` env var +- `argocd/apps/cv.yaml` — ArgoCD Application **Key files (cv repo):** @@ -55,15 +56,17 @@ Migrated from minikube to indri-native on 2026-04-29 (see [[cv-on-indri]]). - `src/cv_ci/main.py` — Dagger pipeline (alpine + uv + WeasyPrint) - `.forgejo/workflows/cv-release.yaml` — Release workflow -## Release flow +## Secrets -1. Release a new package from the cv repo (`Release CV` workflow) -2. Run the blumeops `Deploy CV` workflow → bumps `cv_version` in the ansible role and pushes -3. Run `mise run provision-indri -- --tags cv` from gilbert -4. Purge the Fly.io proxy cache so the new content is fetched +| Secret | Repo | Source | Description | +|--------|------|--------|-------------| +| `FORGE_TOKEN` | cv | 1Password (via Ansible) | Forgejo API token for package uploads | + +Provisioned via `forgejo_actions_secrets` Ansible role. See [[create-release-artifact-workflow]]. ## Related -- [[cv-on-indri]] — Operations how-to -- [[docs]] — Similar architecture (Caddy serving a tarball-extracted dir) +- [[docs]] — Similar architecture (nginx container + content tarball) - [[flyio-proxy]] — Exposes `cv.eblu.me` publicly via Tailscale tunnel +- [[create-release-artifact-workflow]] — How to set up release artifact workflows +- [[deploy-k8s-service]] — General k8s deployment guide diff --git a/docs/reference/services/devpi.md b/docs/reference/services/devpi.md index 589a802..c6493fe 100644 --- a/docs/reference/services/devpi.md +++ b/docs/reference/services/devpi.md @@ -1,7 +1,7 @@ --- title: Devpi -modified: 2026-04-29 -last-reviewed: 2026-04-29 +modified: 2026-03-23 +last-reviewed: 2026-03-23 tags: - service - python @@ -9,37 +9,31 @@ tags: # devpi (PyPI Proxy) -PyPI caching proxy and private package index. Runs natively on [[indri]] as a LaunchAgent (not in-cluster). See [[devpi-on-indri]] for deploy and operations. +PyPI caching proxy and private package index. ## Quick Reference | Property | Value | |----------|-------| -| **URL** | `https://pypi.ops.eblu.me` | -| **Listen** | `127.0.0.1:3141` (loopback only; reached via Caddy) | -| **Service** | LaunchAgent `mcquack.eblume.devpi` on indri | -| **Server-dir** | `/Users/erichblume/devpi/server-dir/` | -| **Runtime** | uv-managed venv at `/Users/erichblume/devpi/venv/` | -| **Ansible role** | `ansible/roles/devpi/` | -| **Versions** | Pinned in `ansible/roles/devpi/defaults/main.yml` (`devpi_server_version`, `devpi_web_version`) | +| **URL** | https://pypi.ops.eblu.me | +| **Namespace** | `devpi` | +| **ArgoCD App** | `devpi` | +| **Storage** | 50Gi PVC | +| **Image** | `registry.ops.eblu.me/blumeops/devpi` (see `argocd/manifests/devpi/kustomization.yaml` for current tag) | ## Indices | Index | Purpose | |-------|---------| -| `root/pypi` | PyPI mirror/cache (auto-created by `devpi-init`) | -| `eblume/dev` | Private packages (inherits from `root/pypi`) | +| `root/pypi` | PyPI mirror/cache (auto-created) | +| `eblume/dev` | Private packages (inherits from root/pypi) | ## Credentials -Root password stored in 1Password (`blumeops` vault, item `devpi`, field `root password`). Fetched via `op read` in the `ansible/playbooks/indri.yml` `pre_tasks` and passed to the role on first init. - -## Backup - -The server-dir is **not** backed up. The PyPI cache (`+files/`) is re-fetchable from upstream on first request. The local `eblume/dev` index metadata is small but also not critical to retain — packages can be republished from source. If retention becomes important, add `/Users/erichblume/devpi/server-dir/` to `borgmatic_source_directories`. +Root password stored in 1Password (blumeops vault), injected via ExternalSecret. ## Related -- [[devpi-on-indri]] — Deploy, verify, and version-bump procedures -- [[use-pypi-proxy]] — Client configuration and package uploads -- [[1password]] — Secrets management +- [[use-pypi-proxy]] - Client configuration and package uploads +- [[argocd]] - Deployment +- [[1password]] - Secrets management diff --git a/docs/reference/services/docs.md b/docs/reference/services/docs.md index 8ca8310..1361d02 100644 --- a/docs/reference/services/docs.md +++ b/docs/reference/services/docs.md @@ -1,7 +1,7 @@ --- title: Docs -modified: 2026-04-29 -last-reviewed: 2026-04-29 +modified: 2026-03-23 +last-reviewed: 2026-03-23 tags: - service - documentation @@ -9,42 +9,44 @@ tags: # Docs (Quartz) -Documentation site built with [Quartz](https://quartz.jzhao.xyz/). +Documentation site built with [Quartz](https://quartz.jzhao.xyz/) and served via nginx. ## Quick Reference | Property | Value | |----------|-------| -| **Public URL** | https://docs.eblu.me (via [[flyio-proxy]]) | -| **Private URL** | `docs.ops.eblu.me` (Caddy on indri) | -| **Deployment** | Ansible role `docs` on indri (no daemon — Caddy serves files directly) | -| **Content dir** | `~/blumeops/docs/content/` on indri | +| **Public URL** | https://docs.eblu.me | +| **Private URL** | `docs.ops.eblu.me` (tailnet only, via [[caddy]]) | +| **Namespace** | `docs` | +| **Image** | `registry.ops.eblu.me/blumeops/quartz` (see `argocd/manifests/docs/kustomization.yaml` for current tag) | | **Source** | `docs/` directory in blumeops repo | | **Build** | Forgejo workflow `build-blumeops.yaml` | - -Migrated from minikube to indri-native on 2026-04-29 (see [[docs-on-indri]]). +| **Public proxy** | [[flyio-proxy]] (Fly.io → Tailscale tunnel) | ## Architecture 1. **Source**: Markdown files in `docs/` with Obsidian-compatible wiki-links -2. **Build**: `Build BlumeOps` Forgejo workflow runs towncrier + Quartz, uploads tarball as a release asset, and bumps `docs_version` in the ansible role -3. **Deploy**: ansible role downloads the tarball into `~/blumeops/docs/content/` on indri; Caddy serves the directory directly with Quartz-style `try_files` (path → path/ → path.html → 404.html) +2. **Build**: Forgejo workflow builds Quartz static site on push to main +3. **Release**: Built assets published as Forgejo release attachments +4. **Deploy**: Container downloads release bundle on startup, serves via nginx + +## Release Process + +Documentation is built and released via the `build-blumeops` Forgejo workflow (manual dispatch): + +1. Quartz builds static HTML/CSS/JS +2. Assets uploaded as Forgejo release attachment +3. Workflow updates `DOCS_RELEASE_URL` in `argocd/manifests/docs/deployment.yaml` and commits to main +4. ArgoCD syncs the updated deployment; new pod downloads the release bundle at startup ## Configuration - **Quartz config**: `quartz.config.ts` - **Layout**: `quartz.layout.ts` -- **Ansible role**: `ansible/roles/docs/` -- **Caddy entry**: `ansible/roles/caddy/defaults/main.yml` (`kind: static`, `try_html: true`) - -## Release flow - -1. Run the `Build BlumeOps` workflow → builds tarball, creates release, bumps `docs_version` in the ansible role and pushes -2. Run `mise run provision-indri -- --tags docs` from gilbert -3. Purge the Fly.io proxy cache so the new content is fetched +- **ArgoCD app**: `argocd/apps/docs.yaml` +- **Manifests**: `argocd/manifests/docs/` ## Related -- [[docs-on-indri]] — Operations how-to -- [[cv]] — Similar architecture -- [[forgejo]] — Build workflows +- [[argocd]] - Deployment management +- [[forgejo]] - Build workflows diff --git a/docs/reference/services/flyio-proxy.md b/docs/reference/services/flyio-proxy.md index 182e80c..3c66d4e 100644 --- a/docs/reference/services/flyio-proxy.md +++ b/docs/reference/services/flyio-proxy.md @@ -1,6 +1,6 @@ --- title: Fly.io Proxy -modified: 2026-04-18 +modified: 2026-02-08 tags: - service - networking @@ -23,27 +23,13 @@ Public reverse proxy on [Fly.io](https://fly.io) that exposes selected BlumeOps ## Exposed Services -| Public domain | Backend (via Caddy) | Service | -|---------------|---------------------|---------| -| `docs.eblu.me` | `docs.ops.eblu.me` | [[docs]] | -| `cv.eblu.me` | `cv.ops.eblu.me` | [[cv]] | -| `forge.eblu.me` | `forge.ops.eblu.me` | [[forgejo]] | +| Public domain | Backend | Service | +|---------------|---------|---------| +| `docs.eblu.me` | `docs.tail8d86e.ts.net` | [[docs]] | ## Architecture -Internet traffic hits Fly.io's Anycast edge, terminates TLS with a Let's Encrypt certificate, and is proxied by nginx to [[caddy]] on [[indri]] over a direct Tailscale WireGuard tunnel. Caddy then routes to the actual service. See [[expose-service-publicly]] for the full architecture diagram. - -### Why Caddy, not per-service Tailscale Ingress? - -Previously, nginx connected directly to each service's `*.tail8d86e.ts.net` Tailscale Ingress endpoint. This caused **20+ second latency** because the Tailscale Ingress pods (running inside k8s) are behind pod-network NAT and can only reach the Fly VM via Tailscale DERP relay servers — not direct WireGuard peering. - -Routing through Caddy on indri solves this because indri's host-level Tailscale can establish direct WireGuard connections with the Fly VM (45ms round trip). This generalizes to all services regardless of where they run (native on indri, minikube, or ringtail k3s), since Caddy already routes to everything. - -### Direct WireGuard Peering - -The Fly VM pins its Tailscale WireGuard listener to port 41641 (`tailscaled --port=41641`). Combined with well-behaved NAT on both sides (`MappingVariesByDestIP: false`), this allows Tailscale to establish direct peer-to-peer connections via UDP hole punching — no dedicated IPv4 required. - -If direct peering fails (observable via `tailscale ping indri` showing "via DERP"), allocate a dedicated IPv4 ($2/month) with `fly ips allocate-v4` to provide a guaranteed inbound UDP path. +Internet traffic hits Fly.io's Anycast edge, terminates TLS with a Let's Encrypt certificate, and is proxied by nginx to the backend service over a Tailscale WireGuard tunnel. See [[expose-service-publicly]] for the full architecture diagram. ## Key Files @@ -53,7 +39,7 @@ If direct peering fails (observable via `tailscale ping indri` showing "via DERP | `fly/Dockerfile` | nginx + Tailscale + Alloy container | | `fly/nginx.conf` | Reverse proxy, caching, rate limiting, JSON logging | | `fly/alloy.river` | Alloy config: log tailing, metric extraction, remote_write | -| `fly/start.sh` | Entrypoint: start Tailscale, wait for MagicDNS, then nginx + Alloy | +| `fly/start.sh` | Entrypoint: start Tailscale, Alloy, then nginx | | `pulumi/tailscale/__main__.py` | Auth key (`tag:flyio-proxy`) | | `pulumi/tailscale/policy.hujson` | ACL grants for proxy | | `pulumi/gandi/__main__.py` | DNS CNAMEs | @@ -62,8 +48,6 @@ If direct peering fails (observable via `tailscale ping indri` showing "via DERP Fly.io runs Firecracker microVMs which support TUN devices natively. Tailscale runs with a real TUN interface (not userspace networking), so MagicDNS and direct Tailscale IP routing work normally. -The `tailscaled` process is started with `--port=41641` to pin the WireGuard listener to a fixed port. This is critical for direct peering — without it, hole punching is unreliable. A `[[services]]` block in `fly.toml` exposes this port as UDP, though it is only active when a dedicated IPv4 is allocated. - The Tailscale auth key is `preauthorized=True` to avoid device approval hangs on container restarts. ## Observability @@ -73,8 +57,7 @@ The Tailscale auth key is `preauthorized=True` to avoid device approval hangs on - **Logs**: nginx JSON access logs tailed and pushed to [[loki|Loki]] (`{instance="flyio-proxy", job="flyio-nginx"}`) - **Metrics**: Derived from access logs, pushed to [[prometheus|Prometheus]] via `remote_write` - `flyio_nginx_http_requests_total` — request rate by status/method/host - - `flyio_nginx_http_request_duration_seconds` — total request latency histogram (includes proxy overhead) - - `flyio_nginx_upstream_response_time_seconds` — backend response time histogram (Forgejo processing only) + - `flyio_nginx_http_request_duration_seconds` — latency histogram - `flyio_nginx_http_response_bytes_total` — response bandwidth - `flyio_nginx_cache_requests_total` — cache HIT/MISS/EXPIRED counts @@ -89,23 +72,11 @@ Alloy listens on `127.0.0.1:12345` for self-scraping its `/metrics` endpoint. Al ## Security Considerations -The `tag:flyio-proxy` ACL grants access only to `tag:flyio-target:443`. Indri carries this tag (for Caddy), and the k8s Tailscale Ingress pods for Loki and Prometheus also carry it so [[alloy|Alloy]] can push logs and metrics directly. A compromised proxy cannot route to arbitrary services on the tailnet — only `tag:flyio-target` endpoints on port 443. +The `tag:flyio-proxy` ACL grants access only to `tag:flyio-target:443`. Services must explicitly opt in by adding a `tailscale.com/tags: "tag:k8s,tag:flyio-target"` annotation to their Tailscale Ingress. This means the proxy can only reach endpoints that have been individually tagged — a compromised nginx config cannot route to arbitrary services on the tailnet. -### Crawler Mitigation +Currently tagged as `tag:flyio-target`: [[docs]], [[loki]], [[prometheus]]. Loki and Prometheus are tagged so that [[alloy|Alloy]] (running inside the container) can push logs and metrics directly via their Tailscale Ingress endpoints — the restricted ACL means Caddy on indri (`tag:homelab`) is not reachable from the proxy. -The proxy serves a `robots.txt` blocking crawlers from expensive endpoints: - -- `/mirrors/` — large mirrored repos -- `/user/` — auth endpoints (crawlers follow redirect loops) -- `/users/` — user profile pages -- `/*/archive/` — git bundle generation (DoS vector, see below) -- `/*/releases/download/` — release artifacts - -Archive requests (`///archive/*`) are 302-redirected to `forge.ops.eblu.me` (tailnet-only), preventing unauthenticated archive generation. This mitigates a known Forgejo DoS vector where crawlers requesting unique commit SHAs trigger unbounded git bundle generation. - -Release downloads are cached at the proxy layer (7-day TTL, keyed by URI) to absorb repeated downloads of the same artifact. - -To expose an additional service through the proxy, add a Caddy route for it and an nginx `server` block. See [[expose-service-publicly]] for the full workflow. +To expose an additional service through the proxy, add the `tag:flyio-target` annotation to its Tailscale Ingress. See [[expose-service-publicly]] for the full workflow. ## Spider Trap Mitigation diff --git a/docs/reference/services/forgejo-runner.md b/docs/reference/services/forgejo-runner.md deleted file mode 100644 index 612f20f..0000000 --- a/docs/reference/services/forgejo-runner.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Forgejo Runner -modified: 2026-04-20 -last-reviewed: 2026-04-20 -tags: - - service - - ci-cd ---- - -# Forgejo Runner - -Forgejo Actions runner daemon for CI/CD job execution. Runs as a Kubernetes pod on [[indri]] (minikube) with a Docker-in-Docker sidecar. - -## Quick Reference - -| Property | Value | -|----------|-------| -| **Namespace** | `forgejo-runner` | -| **ArgoCD App** | `forgejo-runner` | -| **Runner Name** | `k8s-runner` | -| **Labels** | `k8s` | -| **Capacity** | 2 concurrent jobs | -| **Timeout** | 3h | -| **Forgejo Instance** | https://forge.ops.eblu.me | -| **Image** | `registry.ops.eblu.me/blumeops/forgejo-runner` (see `argocd/manifests/forgejo-runner/kustomization.yaml` for current tag) | -| **DinD Sidecar** | `docker:27-dind` | - -## Architecture - -The pod runs two containers: - -1. **runner** - The Forgejo runner daemon. Loads a rendered `server.connections` config at startup, then polls for jobs. Talks to DinD via `tcp://localhost:2375`. -2. **dind** - Docker-in-Docker sidecar (privileged). Provides the Docker daemon for job container execution. Uses a registry mirror at `host.minikube.internal:5050` ([[zot]]). - -The runner daemon image is built from `containers/forgejo-runner/container.py`, not pulled directly from upstream. Credentials come from 1Password via [[external-secrets]], and the startup script renders the final config before launching the daemon. The `/data` volume remains for the runner home directory and job scratch space, not for `.runner` registration state. - -## Job Execution Image - -The actual container image used to run workflow steps is declared in `server.connections.labels` in the runner config. This image is tracked separately as `runner-job-image` in `service-versions.yaml`. See [[build-container-image]] for how it's built. - -## Network - -Jobs run with `network: "host"` to share the DinD network namespace. This gives job containers access to the same DNS and network as the pod, including cluster-internal services. - -## Credentials - -| Secret | Source | Purpose | -|--------|--------|---------| -| `FORGEJO_RUNNER_UUID` | 1Password ("Forgejo Secrets" → `runner_k8s_uuid`) | Static runner identity for `server.connections` | -| `FORGEJO_RUNNER_TOKEN` | 1Password ("Forgejo Secrets" → `runner_k8s_token`) | Static runner credential for `server.connections` | - -## Related - -- [[forgejo]] - The forge this runner connects to -- [[argocd]] - Deployment mechanism -- [[zot]] - Registry mirror for job image pulls -- [[build-container-image]] - How container images are built via this runner diff --git a/docs/reference/services/forgejo.md b/docs/reference/services/forgejo.md index 5b16b0e..e96c247 100644 --- a/docs/reference/services/forgejo.md +++ b/docs/reference/services/forgejo.md @@ -1,6 +1,6 @@ --- title: Forgejo -modified: 2026-04-17 +modified: 2026-03-03 tags: - service - git @@ -11,8 +11,6 @@ tags: Git forge and CI/CD platform. **Primary source of truth for blumeops** (mirrored to GitHub). -Built from source on indri, managed via Ansible + mcquack LaunchAgent. Source cloned from Codeberg with a forge mirror as secondary remote. - ## Quick Reference | Property | Value | @@ -22,39 +20,6 @@ Built from source on indri, managed via Ansible + mcquack LaunchAgent. Source cl | **SSH** | `ssh://forgejo@forge.ops.eblu.me:2222` | | **Local Ports** | 3001 (HTTP), 2200 (SSH) | | **Config** | `ansible/roles/forgejo/templates/app.ini.j2` | -| **Binary** | `~/code/3rd/forgejo/forgejo` (source-built) | -| **Data** | `~/forgejo` | -| **LaunchAgent** | `mcquack.eblume.forgejo` | -| **Source** | `~/code/3rd/forgejo` (cloned from Codeberg) | - -## Building from Source - -Forgejo is built from source on indri, matching the pattern used by [[zot]], [[caddy]], and [[alloy]]. - -**One-time setup:** - -```fish -# Clone from Codeberg (avoids circular dependency with forge) -ssh indri 'git clone https://codeberg.org/forgejo/forgejo.git ~/code/3rd/forgejo' - -# Add forge mirror as secondary remote -ssh indri 'cd ~/code/3rd/forgejo && git remote add forge https://forge.eblu.me/mirrors/forgejo.git' -``` - -**Building a specific version:** - -```fish -ssh indri 'cd ~/code/3rd/forgejo && git fetch --tags && git checkout v14.0.3' -ssh indri 'cd ~/code/3rd/forgejo && mise run build' -``` - -The `build` mise task (defined in the repo's `mise.toml`) runs `make build` with the correct tags and creates the `./forgejo` hardlink. It uses `go@1.25.8` and `node@24` as configured by `mise use`. - -**WARNING:** Do NOT use `make forgejo` directly — it rebuilds with empty TAGS, stripping SQLite support. Always use `mise run build` or pass TAGS explicitly to `make build` and `ln -f gitea forgejo` afterwards. - -Build tags: `bindata` (embed assets), `timetzdata` (embed timezone data), `sqlite sqlite_unlock_notify` (SQLite support). - -After building, run `mise run provision-indri -- --tags forgejo` to deploy the config and restart the service. ## Repositories @@ -63,6 +28,7 @@ After building, run `mise run provision-indri -- --tags forgejo` to deploy the c | `eblume/blumeops` | Infrastructure as code (primary) | | `eblume/alloy` | Grafana Alloy fork (CGO build) | | `eblume/tesla_auth` | Tesla OAuth helper | +| Helm chart mirrors | cloudnative-pg-charts, grafana-helm-charts | ## CI/CD (Forgejo Actions) @@ -85,7 +51,6 @@ Both container workflows trigger on the same tag pattern (`*-v[0-9]*`). Each che Server configuration secrets managed via 1Password → Ansible: - `lfs-jwt-secret`, `internal-token`, `oauth2-jwt-secret` - Forgejo server tokens - `runner_reg` - Runner registration token (also in k8s via [[external-secrets]]) -- `runner_k8s_uuid`, `runner_k8s_token` - Static credentials for the k8s runner `server.connections` flow ## Forgejo Actions Secrets @@ -149,31 +114,18 @@ The UI shows `forge.eblu.me` for HTTPS clone URLs and `forge.ops.eblu.me` for SS - **Rate limiting:** nginx rate limits login/signup/forgot-password endpoints (3r/s per client IP via `Fly-Client-IP` header) - **fail2ban:** Runs in the Fly.io container; bans IPs after 5 failed logins in 10 minutes via nginx deny list (ephemeral across deploys) - **Swagger:** Blocked at the proxy (`/swagger` returns 403); use forge.ops.eblu.me for API access -- **Archive redirect:** Archive endpoints (`/*/archive/*`) are 302-redirected to `forge.ops.eblu.me` — prevents unauthenticated crawlers from triggering unbounded git bundle generation (known DoS vector, see [[flyio-proxy#Crawler Mitigation]]) -- **robots.txt:** Blocks crawlers from `/mirrors/`, `/user/`, `/users/`, `/*/archive/`, `/*/releases/download/` - **OAuth dead-end:** "Sign in with Authentik" redirects to the (tailnet-only) Authentik URL — SSO only works from the tailnet ### Break-glass `mise run fly-shutoff` stops all public traffic immediately. forge.ops.eblu.me continues to work from the tailnet. See [[expose-service-publicly#Break-glass shutoff]]. -## Monitoring - -Forgejo exposes a Prometheus `/metrics` endpoint (enabled via `[metrics]` in `app.ini`). Alloy on indri scrapes it at `localhost:3001/metrics`. Metrics are mostly Go runtime stats and repo counters (no per-request latency histogram). - -Request latency is measured at the Fly.io proxy layer via the `flyio_nginx_upstream_response_time_seconds` histogram, visible on the Forgejo Grafana dashboard under "Forgejo: Upstream Response Time". - -### Archive Cleanup - -The `[cron.archive_cleanup]` section is enabled with `OLDER_THAN = 2h` and `RUN_AT_START = true`. This prevents the `repo-archive/` directory from growing unboundedly when crawlers or users trigger archive downloads. Without this, the directory grew to 54GB in 2 days during a crawler incident in April 2026. - ## Mirrors Forgejo hosts pull mirrors of external repositories (GitHub, etc.) for supply chain control. Mirrors live in the `mirrors/` org and sync on a configurable interval. See [[manage-forgejo-mirrors]] for operations. ## Related -- [[forgejo-runner]] - k8s CI/CD runner (minikube on indri) - [[argocd]] - Uses Forgejo as git source - [[authentik]] - OIDC identity provider - [[zot]] - Container registry for built images diff --git a/docs/reference/services/hephaestus.md b/docs/reference/services/hephaestus.md deleted file mode 100644 index 7abc35b..0000000 --- a/docs/reference/services/hephaestus.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: Hephaestus -modified: 2026-06-04 -last-reviewed: 2026-06-04 -tags: - - service - - hephaestus ---- - -# Hephaestus - -[hephaestus](https://github.com/eblume/hephaestus) (`heph`) is the user's -self-hosted task + context/knowledge system. It is **hub-and-spoke**: each device -runs a full local SQLite replica (`hephd --mode local`) and background-syncs -against one canonical **hub**. Indri runs that hub. - -## Quick Reference - -| Property | Value | -|----------|-------| -| **PWA URL** | https://heph.ops.eblu.me (browser PWA, Caddy TLS) | -| **Spoke sync URL** | http://indri.tail8d86e.ts.net:8787 (direct, tailnet) | -| **Local Port** | 8787 (`hephd --mode server`, bound `0.0.0.0`) | -| **Binary** | `~/.cargo/bin/hephd` (self-updating) | -| **Data** | `~/.local/share/heph/heph.db` | -| **PWA shell** | `~/.local/share/heph/web` | -| **Logs** | `~/Library/Logs/mcquack.heph.{out,err}.log` | -| **LaunchAgent** | `mcquack.eblume.heph` | -| **Ansible role** | `ansible/roles/heph` (tag `heph`) | - -## What runs on indri - -The launchagent runs the hub in server mode with three features enabled: - -``` -hephd --mode server --http-addr 0.0.0.0:8787 --db ~/.local/share/heph/heph.db - --web-root ~/.local/share/heph/web - --oidc-issuer https://authentik.ops.eblu.me/application/o/heph/ - --oidc-audience heph - --self-update --self-update-interval-secs 600 -``` - -- **Server mode** exposes the HTTP sync endpoint (`/rpc`, `/sync/*`) that spokes - reconcile their op-log against. -- **Self-update** (10-minute poll) rebuilds `hephd` from the forge when a newer - release tag appears (`cargo install --git https://forge.eblu.me/eblume/hephaestus.git`). - Indri's Rust toolchain (`~/.cargo/bin`) is on the agent's `PATH` for this, and - the plist pins `RUSTUP_TOOLCHAIN=stable` — the - launchagent runs without mise, so a bare `cargo` shim would otherwise fall back - to rustup's *default* toolchain, which can lag behind heph's `rust-version` floor - (1.89) and silently fail the build. -- **PWA** (`--web-root`) serves the [heph-pwa] mobile shell; Caddy terminates TLS - at `heph.ops.eblu.me` so the PWA runs in a secure context (service worker, - install-to-home-screen, voice capture). - -[heph-pwa]: https://github.com/eblume/hephaestus - -The hub binds `0.0.0.0` so tailnet spokes can also sync directly -(`http://indri.tail8d86e.ts.net:8787`); access is gated by Authentik OIDC either -way — tailnet reachability alone is not enough. - -## Authentication (Authentik OIDC, device-code) - -The hub verifies an OIDC bearer token on every sync. The `heph` application is a -**public** OAuth2 client using the **device-code flow** (RFC 8628), provisioned -in the [[authentik]] blueprint (`argocd/manifests/authentik/configmap-blueprint.yaml`): - -- Issuer: `https://authentik.ops.eblu.me/application/o/heph/` -- Audience / client id: `heph` -- Restricted to the `admins` group (single-owner, sensitive data). -- Scope mappings: `openid`, `email`, `profile`, **`offline_access`**. - -> **`offline_access` is required for durable sync.** The `heph` CLI requests -> `scope = "openid offline_access"`, and a refresh token is only issued for the -> 30-day refresh-token window when the provider actually grants `offline_access`. -> Without that scope mapping the refresh token is bound to the login **session**; -> once the session lapses, hephd's `refresh_token` grant returns `400 Bad -> Request`, the bearer can't be refreshed, and spoke sync silently degrades -> (`heph sync --status` → `auth_failure: true`). `heph auth login` papers over it -> until the next session expiry. Keep `offline_access` in the provider's -> `property_mappings`. - -Because no Authentik instance ships a device-code flow by default, the blueprint -also creates `default-device-code-flow` and binds it to the default brand's -`flow_device_code`. Devices obtain a token with `heph auth login`; the PWA -currently takes a pasted token (in-app device-code login is upstream follow-up). - -## Data seeding (Path A, one-time) - -The hub was seeded from the existing `gilbert` device so no task history was -lost. heph's data-safe bring-up ("Path A") has the hub **adopt the device's -identity** rather than rewriting the device: - -1. Quiesce the seed device: `heph daemon stop` (on gilbert). -2. Copy its store to indri: `scp ~/.local/share/heph/heph.db indri:~/.local/share/heph/heph.db`. -3. Give the hub its **own device origin** (keeps gilbert's `owner_id` + data; - `hephd` regenerates a fresh `origin` on next start when it is missing): - ```fish - ssh indri "sqlite3 ~/.local/share/heph/heph.db \"DELETE FROM meta WHERE key='origin';\"" - ``` -4. `mise run provision-indri -- --tags heph` (installs hephd, stages the PWA, - loads the launchagent → hub starts on the seeded store). - -Only `meta.origin` changes; `owner_id`, nodes, op-log, and links are copied -untouched. A clean `hephd --owner-id` / seed command is tracked upstream as -hephaestus follow-up — until then this manual reset is the documented path. - -## Connecting a spoke (e.g. gilbert) - -A device joins by running its local daemon with the hub URL + OIDC client and -logging in once: - -```bash -hephd --mode local --hub-url http://indri.tail8d86e.ts.net:8787 \ - --oidc-issuer https://authentik.ops.eblu.me/application/o/heph/ \ - --oidc-client-id heph -heph auth login --hub-url http://indri.tail8d86e.ts.net:8787 \ - --issuer https://authentik.ops.eblu.me/application/o/heph/ --client-id heph -``` - -> **Use the direct `http://…:8787` tailnet URL for sync, not the Caddy HTTPS -> URL.** hephd's sync client is plain-HTTP-only; pointing `--hub-url` at -> `https://heph.ops.eblu.me` fails with a confusing `error sending request` -> (the HTTP connector rejects the `https` scheme before connecting). Tailscale -> encrypts the transport, and the OIDC bearer token still gates every request. -> `heph.ops.eblu.me` (Caddy TLS) exists only for the browser PWA, which needs a -> secure context. The cached token is keyed by the exact `--hub-url`, so use the -> same value for `hephd` and `heph auth login`. - -> **Caveat:** `heph daemon` cannot yet bake hub/spoke flags into the generated -> launchd plist (upstream gap). On a spoke whose plist is managed by `heph -> daemon`, the hub/OIDC flags must be hand-added — and a later `heph daemon -> start/restart` will regenerate the plist and drop them. Avoid `heph daemon` -> subcommands on a configured spoke until that gap is closed; reload via -> `launchctl` instead. - -## Related - -- [[indri]] — host -- [[authentik]] — OIDC provider -- [[caddy]] — TLS termination for `heph.ops.eblu.me` diff --git a/docs/reference/services/immich.md b/docs/reference/services/immich.md index 063deac..740dfa4 100644 --- a/docs/reference/services/immich.md +++ b/docs/reference/services/immich.md @@ -1,6 +1,6 @@ --- title: Immich -modified: 2026-04-04 +modified: 2026-02-07 last-reviewed: 2026-03-23 tags: - service @@ -17,7 +17,7 @@ Self-hosted photo and video management. |----------|-------| | **URL** | https://photos.ops.eblu.me | | **Namespace** | `immich` | -| **Deployment** | Kustomize (k8s) | +| **Deployment** | Helm chart (k8s) | | **Database** | [[postgresql]] (CNPG) | | **Storage** | [[sifaka|Sifaka]] photos volume | diff --git a/docs/reference/services/kingfisher.md b/docs/reference/services/kingfisher.md deleted file mode 100644 index 7512d6b..0000000 --- a/docs/reference/services/kingfisher.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Kingfisher -modified: 2026-03-28 -last-reviewed: 2026-03-28 -tags: - - service - - security ---- - -# Kingfisher - -Secret detection and live validation scanner for Forgejo repositories, using MongoDB's open-source [Kingfisher](https://github.com/mongodb/kingfisher) tool. - -## Quick Reference - -| Property | Value | -|----------|-------| -| **Namespace** | `kingfisher` | -| **Image** | `registry.ops.eblu.me/blumeops/kingfisher` (see `argocd/manifests/kingfisher/kustomization.yaml` for current tag) | -| **Schedule** | Sunday 4am (after Prowler k8s scan at 3am) | -| **Reports** | `sifaka:/volume1/reports/kingfisher/` (NFS) | -| **Manifests** | `argocd/manifests/kingfisher/` | -| **Upstream** | `forge.eblu.me/mirrors/kingfisher` (GitHub mirror) | - -## What it does - -Runs as a weekly CronJob that scans all Forgejo repos (eblume + all orgs) for leaked secrets, API keys, and credentials. Produces timestamped HTML reports on the sifaka NFS share. Uses `--clone-url-base` to route git clones via the internal tailnet instead of the public Fly.io proxy. - -Uses the Forgejo/Gitea API to enumerate repos, then clones and scans each one. Validation is enabled (secrets are tested against their respective APIs to confirm they're live). Reports are HTML only. - -## Pre-commit hook - -Kingfisher also runs as a prek hook alongside TruffleHog for comparative secret detection coverage. The hook uses `--staged` mode (only checks staged files) with validation disabled for fast, offline-safe commits. - -## Known false positives - -- **Postgres URL with `op://` template** — 1Password External Secrets template references match the postgres connection string pattern. Not a real credential. -- **GitHub legacy secret key in `.git/`** — git commit SHAs are 40-char hex strings matching the old GitHub PAT format. Only appears in full-repo scans, not `--staged` mode. - -## Ad-hoc scan - -```fish -kubectl create job --from=cronjob/kingfisher kingfisher-manual -n kingfisher --context=minikube-indri -kubectl logs -f job/kingfisher-manual -n kingfisher --context=minikube-indri -``` - -## Limitations - -- Built from a [[spork-strategy|sporked]] fork with a local `--clone-url-base` patch. See [[build-spork-container]] for the build process. -- Only one output format per invocation. Currently producing HTML only. - -## See also - -- [[prowler]] — CIS Kubernetes, image, and IaC compliance scanning -- [[read-compliance-reports]] — how to access and interpret reports diff --git a/docs/reference/services/kiwix.md b/docs/reference/services/kiwix.md index 04fe0f6..6806a5e 100644 --- a/docs/reference/services/kiwix.md +++ b/docs/reference/services/kiwix.md @@ -1,7 +1,6 @@ --- title: Kiwix -modified: 2026-05-04 -last-reviewed: 2026-05-04 +modified: 2026-03-05 tags: - service - knowledge @@ -42,7 +41,7 @@ Full list: `argocd/manifests/kiwix/torrents.txt` ## Adding Archives -1. Edit `argocd/manifests/kiwix/torrents.txt` (rendered into a ConfigMap by `configMapGenerator`) +1. Edit `configmap-zim-torrents.yaml` 2. Add torrent URL from https://download.kiwix.org/zim/ 3. Sync: `argocd app sync kiwix` 4. Torrent-sync adds to [[transmission]] diff --git a/docs/reference/services/mealie.md b/docs/reference/services/mealie.md index fdd0260..c658046 100644 --- a/docs/reference/services/mealie.md +++ b/docs/reference/services/mealie.md @@ -46,8 +46,6 @@ OIDC via [[authentik]] using a confidential client. Client secret stored in 1Pas SQLite database backed up via [[borgmatic]]'s `before_backup` hook. Borgmatic runs `kubectl exec` to create a safe `.backup` copy (via Python's `sqlite3` module), then `kubectl cp` to the host. The dump lands in `~/.local/share/borgmatic/k8s-dumps/mealie.db` and is included in both local (sifaka) and offsite (BorgBase) backups. -To restore from a borg archive, see [[restore-from-borg]]. - ## Networking | Endpoint | Reachable from | diff --git a/docs/reference/services/navidrome.md b/docs/reference/services/navidrome.md index 68a2a21..5f93331 100644 --- a/docs/reference/services/navidrome.md +++ b/docs/reference/services/navidrome.md @@ -1,7 +1,6 @@ --- title: Navidrome -modified: 2026-04-18 -last-reviewed: 2026-04-18 +modified: 2026-02-21 tags: - service - media @@ -17,15 +16,8 @@ Self-hosted music streaming server. |----------|-------| | **URL** | https://dj.ops.eblu.me | | **Tailscale URL** | https://dj.tail8d86e.ts.net | -| **ArgoCD app** | `navidrome` | -| **Sync policy** | Manual | | **Namespace** | `navidrome` | | **Manifests** | `argocd/manifests/navidrome/` | -| **Image** | `registry.ops.eblu.me/blumeops/navidrome:v0.61.1-3ecd888` | -| **Tracked upstream version** | `v0.61.1` | - -Traffic reaches Navidrome through a Tailscale Ingress at `dj.tail8d86e.ts.net`, -with [[caddy]] proxying `dj.ops.eblu.me` to that tailnet endpoint. ## Storage @@ -40,30 +32,16 @@ The `/data` directory contains SQLite database, configuration, and cache. | Variable | Value | |----------|-------| -| `ND_SCANNER_SCHEDULE` | `@every 1h` | +| `ND_SCANSCHEDULE` | 1h | | `ND_LOGLEVEL` | info | | `ND_MUSICFOLDER` | /music | | `ND_DATAFOLDER` | /data | -## Runtime - -| Property | Value | -|----------|-------| -| **Replicas** | 1 | -| **Container port** | `4533` | -| **Requests** | `100m` CPU, `128Mi` memory | -| **Limits** | `500m` CPU, `512Mi` memory | -| **Security context** | Runs as uid/gid `1000`, `fsGroup: 1000`, `RuntimeDefault` seccomp | -| **Health checks** | Liveness/readiness probe on `GET /ping` | - ## Authentication Local accounts only. Authentik SSO integration was evaluated (Feb 2026) but not pursued — Navidrome lacks native OIDC support. The reverse proxy auth approach (`ND_EXTAUTH_*`) can pass a username header from Authentik, but cannot map Authentik groups to Navidrome admin status, making group-based admin delegation impossible. ## Related -- [[routing]] - URL and exposure model -- [[caddy]] - Reverse proxy from `dj.ops.eblu.me` to the tailnet ingress -- [[sifaka|Sifaka]] - Music storage - [[jellyfin]] - Video streaming -- [[service-versions]] - Tracked upstream version inventory +- [[sifaka|Sifaka]] - Music storage diff --git a/docs/reference/services/ntfy.md b/docs/reference/services/ntfy.md index 1bf45af..b549a6d 100644 --- a/docs/reference/services/ntfy.md +++ b/docs/reference/services/ntfy.md @@ -1,7 +1,6 @@ --- title: Ntfy -modified: 2026-06-04 -last-reviewed: 2026-06-04 +modified: 2026-02-17 tags: - service - notifications @@ -18,7 +17,7 @@ Self-hosted push notification service. Ntfy receives HTTP POST messages and deli | **URL** | https://ntfy.ops.eblu.me | | **Tailscale URL** | https://ntfy.tail8d86e.ts.net | | **Namespace** | `ntfy` | -| **Image** | `registry.ops.eblu.me/blumeops/ntfy:v2.19.2-fd0bebb-nix` (locally built) | +| **Image** | `binwiederhier/ntfy:v2.17.0` | | **Upstream** | https://github.com/binwiederhier/ntfy | | **Manifests** | `argocd/manifests/ntfy/` | diff --git a/docs/reference/services/ollama.md b/docs/reference/services/ollama.md index b749cf2..75480cb 100644 --- a/docs/reference/services/ollama.md +++ b/docs/reference/services/ollama.md @@ -1,7 +1,6 @@ --- title: Ollama -modified: 2026-05-01 -last-reviewed: 2026-05-01 +modified: 2026-03-04 tags: - service - ai @@ -19,7 +18,7 @@ LLM inference server with GPU acceleration. Runs on [[ringtail]] with declarativ | **Tailscale URL** | https://ollama.tail8d86e.ts.net | | **Namespace** | `ollama` | | **Cluster** | ringtail k3s | -| **Image** | `ollama/ollama:0.20.4` | +| **Image** | `ollama/ollama:0.17.5` | | **Upstream** | https://github.com/ollama/ollama | | **Manifests** | `argocd/manifests/ollama/` | | **API Port** | 11434 | @@ -51,8 +50,6 @@ Declared in `argocd/manifests/ollama/models.txt`. The model-sync sidecar pulls m | `deepseek-r1:14b` | 14B | | `phi4:14b` | 14B | | `gemma3:12b` | 12B | -| `qwen3.5:9b` | 9B | -| `qwen3.5:27b` | 27B | To add or remove models, edit `models.txt` and sync via ArgoCD. diff --git a/docs/reference/services/paperless.md b/docs/reference/services/paperless.md deleted file mode 100644 index c74543e..0000000 --- a/docs/reference/services/paperless.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Paperless-ngx -modified: 2026-04-08 -tags: - - service ---- - -# Paperless-ngx - -Self-hosted document management system with OCR, tagging, and full-text search. - -## Quick Reference - -| Property | Value | -|----------|-------| -| **URL** | https://paperless.ops.eblu.me | -| **Tailscale URL** | https://paperless.tail8d86e.ts.net | -| **Namespace** | `paperless` | -| **Image** | `registry.ops.eblu.me/blumeops/paperless` | -| **Manifests** | `argocd/manifests/paperless/` | -| **Container source** | `containers/paperless/Dockerfile` | -| **Upstream** | [paperless-ngx/paperless-ngx](https://github.com/paperless-ngx/paperless-ngx) | -| **Database** | `paperless` on [[postgresql|blumeops-pg]] | -| **Storage** | NFS on [[sifaka]] at `/volume1/paperless` | -| **Auth** | [[authentik]] OIDC + local admin | - -## Architecture - -- **Web server**: Granian (ASGI), port 8000 -- **Task queue**: Celery worker + beat (Redis sidecar) -- **OCR**: Tesseract (English) -- **Process supervisor**: s6-overlay - -## Secrets - -1Password item "Paperless (blumeops)" in vault `blumeops`: -- `secret-key`: Django SECRET_KEY -- `postgresql-password`: database credential -- `admin-password`: initial admin account password -- `socialaccount-providers`: OIDC provider JSON (includes Authentik client secret) - -## Related - -- [[adding-a-service]] — Deployment tutorial -- [[authentik]] — SSO provider diff --git a/docs/reference/services/postgresql.md b/docs/reference/services/postgresql.md index ef86418..9d08d08 100644 --- a/docs/reference/services/postgresql.md +++ b/docs/reference/services/postgresql.md @@ -1,7 +1,6 @@ --- title: PostgreSQL -modified: 2026-04-07 -last-reviewed: 2026-04-07 +modified: 2026-02-15 tags: - service - database @@ -27,21 +26,19 @@ Database clusters via CloudNativePG operator. |----------|---------|-------|---------| | miniflux | blumeops-pg | miniflux | [[miniflux]] feed data | | teslamate | blumeops-pg | teslamate | [[teslamate]] vehicle data | -| authentik | blumeops-pg | authentik | [[authentik]] identity provider | | immich | immich-pg | immich | [[immich]] photo management | The `immich-pg` cluster uses a custom image (`cloudnative-vectorchord`) with vector search extensions (vector, vchord, cube, earthdistance). ## Users -| User | Cluster | Role | Purpose | -|------|---------|------|---------| -| postgres | both | superuser | CNPG internal | -| miniflux | blumeops-pg | app owner | Owns miniflux database | -| teslamate | blumeops-pg | db owner | TeslaMate (owns extensions) | -| authentik | blumeops-pg | createdb | [[authentik]] identity provider | -| eblume | blumeops-pg | superuser | Admin access | -| borgmatic | both | pg_read_all_data | [[borgmatic|Backup]] access | +| User | Role | Purpose | +|------|------|---------| +| postgres | superuser | CNPG internal | +| miniflux | app owner | Owns miniflux database | +| teslamate | superuser | TeslaMate (needs extensions) | +| eblume | superuser | Admin access | +| borgmatic | pg_read_all_data | [[borgmatic|Backup]] access | ## Backup @@ -58,11 +55,9 @@ Backed up via [[borgmatic]] `postgresql_databases` hook. Streams `pg_dump` direc - `blumeops-pg-eblume` - eblume superuser - `blumeops-pg-borgmatic` - borgmatic backup user - `blumeops-pg-teslamate` - teslamate user -- `blumeops-pg-authentik` - authentik user **CNPG-managed secrets (immich-pg):** - `immich-pg-app` - immich user -- `immich-pg-borgmatic` - borgmatic backup user ## Related @@ -70,5 +65,4 @@ Backed up via [[borgmatic]] `postgresql_databases` hook. Streams `pg_dump` direc - [[miniflux]] - Feed reader database - [[teslamate]] - Vehicle data database - [[immich]] - Photo management database -- [[authentik]] - Identity provider database - [[borgmatic]] - Database backup diff --git a/docs/reference/services/shower-app.md b/docs/reference/services/shower-app.md deleted file mode 100644 index 26d1764..0000000 --- a/docs/reference/services/shower-app.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Shower App -modified: 2026-05-10 -last-reviewed: 2026-05-10 -tags: - - service - - django ---- - -# Shower App - -Django web app for Adelaide / Heidi / Addie's baby shower — guest splash with -a "what did you bring?" form, raffle picker, contest-prize ranking via -QR-coded `/prizes//` URLs, and an `/host/` operator console with -drag-rank assignment solving via scipy. - -## Quick Reference - -| Property | Value | -|----------|-------| -| **Public URL** | `shower.eblu.me` (guest surface only — via [[flyio-proxy]]) | -| **Private URL** | `shower.ops.eblu.me` (admin + `/host/` console — Caddy on indri) | -| **Cluster** | [[ringtail]] k3s, namespace `shower` | -| **Container** | `registry.ops.eblu.me/blumeops/shower` (built from `containers/shower/default.nix`) | -| **App source** | `forge.eblu.me/eblume/adelaide-baby-shower-app` (wheel on Forgejo PyPI) | -| **Database** | SQLite on a local-path PVC (`shower-data`, RWO 2 Gi) | -| **Media (prize photos)** | NFS RWX PVC `shower-media` → `sifaka:/volume1/shower` | -| **Secrets** | `Shower (blumeops)` 1Password item → `DJANGO_SECRET_KEY` | - -## Routing - -``` -Internet → shower.eblu.me (Fly nginx, guest-only 403s on /admin/ /host/) - │ - ▼ - Caddy on indri (shower.ops.eblu.me — full surface) - │ - ▼ - Tailscale ProxyGroup → k3s Service → Deployment -``` - -## Backups - -- **SQLite** dumped via `kubectl exec` to indri's `borgmatic_k8s_dump_dir` on every 2 a.m. run (mealie-pattern entry in `borgmatic_k8s_sqlite_dumps`) -- **Media** picked up via `/Volumes/shower` (sifaka SMB mount on indri) in the main `borgmatic_source_directories` list - -Both archive to sifaka + BorgBase. - -## Related - -- [[shower-on-ringtail]] — onboarding + day-of runbook -- [[expose-service-publicly]] — Fly proxy + tailnet pattern this rides on -- [[ringtail]] — host cluster -- [[sifaka#NFS Exports]] — NFS share table -- [[borgmatic]] — backup system diff --git a/docs/reference/services/tempo.md b/docs/reference/services/tempo.md index 5eb5d87..771b97f 100644 --- a/docs/reference/services/tempo.md +++ b/docs/reference/services/tempo.md @@ -1,7 +1,6 @@ --- title: Tempo -modified: 2026-06-04 -last-reviewed: 2026-06-04 +modified: 2026-03-05 tags: - service - observability @@ -19,7 +18,7 @@ Distributed tracing backend for BlumeOps infrastructure. Receives traces via OTL | **Tailscale URL** | https://tempo.tail8d86e.ts.net | | **OTLP Endpoint** | https://tempo-otlp.tail8d86e.ts.net | | **Namespace** | `monitoring` | -| **Image** | `registry.ops.eblu.me/blumeops/tempo:v2.10.3-75f9ba4` (locally built) | +| **Image** | `grafana/tempo:2.10.1` | | **Storage** | 10Gi PVC (local filesystem) | | **Retention** | 7 days | diff --git a/docs/reference/services/teslamate.md b/docs/reference/services/teslamate.md index a11ed0e..f02e979 100644 --- a/docs/reference/services/teslamate.md +++ b/docs/reference/services/teslamate.md @@ -1,6 +1,6 @@ --- title: TeslaMate -modified: 2026-04-07 +modified: 2026-03-23 last-reviewed: 2026-03-23 tags: - service @@ -39,19 +39,7 @@ Self-hosted Tesla data logger collecting vehicle telemetry from the Tesla API. - Drive Stats, Charging Stats, Projected Range - Timeline, Updates, Visited -Dashboards use PostgreSQL datasource (not Prometheus). The Grafana datasource connects as the `teslamate` database user. - -## Database Permissions - -The `teslamate` role was initially provisioned as superuser to allow extension creation (`cube`, `earthdistance`) during initial setup. Superuser has been removed — `teslamate` is now a plain database owner with extension ownership transferred so it can `ALTER EXTENSION ... UPDATE` without superuser. - -Note: `earthdistance` is not a trusted extension in PostgreSQL, so `CREATE EXTENSION earthdistance` still requires superuser. If a future TeslaMate migration does `DROP EXTENSION ... CASCADE` + re-create (as happened in the 2024 migration), it will fail. In that case, temporarily grant superuser for the migration and remove it afterward. - -Extension ownership persists across pod restarts and CNPG failovers, but a full cluster rebuild (major PG upgrade, fresh `initdb`) would re-create extensions as `postgres`. After any rebuild, transfer ownership back: - -```sql -UPDATE pg_extension SET extowner = (SELECT oid FROM pg_roles WHERE rolname = 'teslamate') WHERE extname IN ('cube', 'earthdistance'); -``` +Dashboards use PostgreSQL datasource (not Prometheus). ## Authentication diff --git a/docs/reference/services/transmission.md b/docs/reference/services/transmission.md index 89904ce..3676177 100644 --- a/docs/reference/services/transmission.md +++ b/docs/reference/services/transmission.md @@ -1,7 +1,6 @@ --- title: Transmission -modified: 2026-04-29 -last-reviewed: 2026-04-29 +modified: 2026-02-07 tags: - service - torrent @@ -23,13 +22,14 @@ BitTorrent daemon, primarily for downloading ZIM archives for [[kiwix]]. ## Storage Layout -| Path | Backing | Purpose | -|------|---------|---------| -| `/downloads/incomplete/` | NFS (`sifaka:/volume1/torrents`) | Active downloads | -| `/downloads/complete/` | NFS (`sifaka:/volume1/torrents`) | Completed downloads | -| `/config/` | `emptyDir` (ephemeral) | Transmission `settings.json`, regenerated on pod start | +NFS share on sifaka (`/volume1/torrents`): -The watch directory is disabled (`watch-dir-enabled: false`); torrents are added via RPC (see Kiwix integration below). +| Path | Purpose | +|------|---------| +| `/downloads/` | Active downloads and metadata | +| `/downloads/complete/` | Completed downloads | +| `/config/` | Transmission configuration | +| `/watch/` | Watch directory for .torrent files | [[kiwix]] reads from `/downloads/complete/` to serve ZIM archives. @@ -44,7 +44,7 @@ When downloads complete, the zim-watcher CronJob detects new ZIMs and restarts K ## Monitoring -A `transmission-exporter` sidecar (image `registry.ops.eblu.me/blumeops/transmission-exporter`) scrapes the local RPC and exposes Prometheus metrics on port 19091. Uptime is also covered by a blackbox probe in [[alloy|Alloy]] k8s (Services Health dashboard). +Basic uptime via blackbox probe in [[alloy|Alloy]] k8s (Services Health dashboard). Web UI shows: active/seeding/paused counts, speeds, disk usage. diff --git a/docs/reference/services/zot.md b/docs/reference/services/zot.md index b01a6ce..c309557 100644 --- a/docs/reference/services/zot.md +++ b/docs/reference/services/zot.md @@ -56,9 +56,8 @@ The `zot-ci` API key expires every **90 days**. To rotate: 5. Generate a new API key, copy it to clipboard 6. Update 1Password: ```fish - set -l NEWKEY (pbpaste); op item edit "Forgejo Secrets" --vault blumeops "zot-ci-api[password]=$NEWKEY"; set -e NEWKEY + pbpaste | op item edit "Forgejo Secrets" --vault blumeops "zot-ci-api[password]=-" ``` - The value is briefly visible to other `ps`-readers on this machine (single-user mac, acceptable tradeoff). The older `pbpaste | op item edit ... "field[password]=-"` stdin syntax was rejected by op 2.34 as "invalid JSON" — recent op versions treat piped input as a full JSON template. 7. Sync to Forgejo: `mise run provision-indri -- --tags forgejo_actions_secrets` ## Related @@ -67,4 +66,4 @@ The `zot-ci` API key expires every **90 days**. To rotate: - [[cluster|Cluster]] - Registry consumer - [[authentik]] - OIDC identity provider - [[harden-zot-registry]] - Security hardening guide -- [[service-versions]] - Version tracking for deployed services +- [[install-dagger-on-nix-runner]] - Why Dagger can't run on the Nix builder diff --git a/docs/reference/storage/backups.md b/docs/reference/storage/backups.md index 2dfbae4..9ca3bcb 100644 --- a/docs/reference/storage/backups.md +++ b/docs/reference/storage/backups.md @@ -22,7 +22,7 @@ Daily automated backups from [[indri]] to [[sifaka|Sifaka]] NAS. | Path | Description | Priority | |------|-------------|----------| -| `~/code/personal/zk` | Zettelkasten notes (migrating into heph docs) | Critical | +| `~/code/personal/zk` | Zettelkasten notes | Critical | | `/opt/homebrew/var/forgejo` | Git repositories | Critical | | `~/.config/borgmatic` | Backup config | High | | `~/Documents` | Personal documents (includes [[1password]] encrypted export) | High | @@ -62,7 +62,7 @@ Other data lives directly on [[sifaka]] (music via [[navidrome]], video via [[je | ZIM archives (`~/transmission/`) | Re-downloadable via torrent | | Prometheus metrics | Ephemeral, in k8s PVC | | Loki logs | Ephemeral, in k8s PVC | -| devpi cache (`~/devpi/server-dir/` on indri) | Re-fetchable from PyPI on first request | +| devpi cache | Re-fetchable from PyPI | ## Retention Policy diff --git a/docs/reference/storage/sifaka.md b/docs/reference/storage/sifaka.md index b3387c1..31fe90a 100644 --- a/docs/reference/storage/sifaka.md +++ b/docs/reference/storage/sifaka.md @@ -1,7 +1,7 @@ --- title: Sifaka -modified: 2026-03-28 -last-reviewed: 2026-03-28 +modified: 2026-02-09 +last-reviewed: 2026-03-23 tags: - storage --- @@ -28,19 +28,14 @@ Synology NAS providing network storage and backup target. | music | `/volume1/music` | Music library | [[navidrome]] | | allisonflix | `/volume1/allisonflix` | Video library | [[jellyfin]] | | photos | `/volume1/photos` | Photo library | [[immich]] | -| frigate | `/volume1/frigate` | NVR recordings, clips, models | [[frigate]] | ## NFS Exports -| Export | Allowed Clients | Squash | Purpose | -|--------|-----------------|--------|---------| -| `/volume1/torrents` | 192.168.1.0/24, 100.64.0.0/10 | Map all users to admin | k8s pods | -| `/volume1/music` | 192.168.1.0/24, 100.64.0.0/10 | Map all users to admin | k8s pods | -| `/volume1/photos` | 192.168.1.0/24, 100.64.0.0/10 | Map all users to admin | k8s pods | -| `/volume1/frigate` | 192.168.1.0/24, 100.64.0.0/10 | Map all users to admin | k8s pods on ringtail | -| `/volume1/reports` | 192.168.1.0/24, 100.64.0.0/10 | Map all users to admin | Prowler reports | - -All exports use `all_squash` mapping to uid 1024 (`admin`) / gid 100 (`users`). If NFS mounts fail with permission denied, see [[troubleshoot-sifaka-nfs]]. +| Export | Allowed Clients | Purpose | +|--------|-----------------|---------| +| `/volume1/torrents` | 192.168.1.0/24, 100.64.0.0/10 | k8s pods via Docker NAT | +| `/volume1/music` | 192.168.1.0/24, 100.64.0.0/10 | k8s pods via Docker NAT | +| `/volume1/photos` | 192.168.1.0/24, 100.64.0.0/10 | k8s pods via Docker NAT | ## Monitoring @@ -113,7 +108,6 @@ Synology uses `/dev/sata*` (e.g., `/dev/sata1` through `/dev/sata4`) instead of - Tag: `tag:nas` - ACL: `tag:homelab` can access for backups -- **Must run in TUN mode** for NFS — see [[troubleshoot-sifaka-nfs]] ## Backup @@ -123,10 +117,8 @@ Data protection for sifaka itself currently relies on the Synology RAID 5 config ## Related -- [[troubleshoot-sifaka-nfs]] - NFS permission denied troubleshooting - [[backups|Backups]] - Backup policy - [[borgmatic]] - Backup system -- [[frigate]] - NVR recordings consumer - [[immich]] - Photo consumer - [[jellyfin]] - Media consumer - [[navidrome]] - Music consumer diff --git a/docs/reference/tools/ansible.md b/docs/reference/tools/ansible.md index 7c0ebc9..2f21eb2 100644 --- a/docs/reference/tools/ansible.md +++ b/docs/reference/tools/ansible.md @@ -1,7 +1,6 @@ --- title: Ansible -modified: 2026-03-30 -last-reviewed: 2026-03-30 +modified: 2026-02-12 tags: - ansible - reference @@ -9,7 +8,7 @@ tags: # Ansible -Host-level configuration management — the layer between cloud infrastructure ([[pulumi]]) and containerized workloads ([[argocd]]). The primary playbook is `ansible/playbooks/indri.yml` (targets [[indri]]); separate playbooks exist for [[ringtail]] and [[sifaka]]. +Configuration management for native services on [[indri]]. The primary playbook is `ansible/playbooks/indri.yml`. ## CLI Patterns @@ -24,16 +23,6 @@ mise run provision-indri -- --tags caddy mise run provision-indri -- --check --diff ``` -Other hosts have their own playbooks: - -```bash -# Ringtail (NixOS, k3s) -mise run provision-ringtail - -# Sifaka (Synology NAS exporters) -mise run provision-sifaka -``` - ## Available Roles | Role | Purpose | Service | @@ -43,8 +32,6 @@ mise run provision-sifaka | **borgmatic_metrics** | Backup metrics exporter | [[borgmatic]] | | **caddy** | Reverse proxy & TLS | [[routing]] | | **forgejo** | Git forge | [[forgejo]] | -| **forgejo_actions_secrets** | CI/CD secrets for Forgejo Actions | [[forgejo]] | -| **forgejo_metrics** | Forge metrics exporter | [[forgejo]] | | **jellyfin** | Media server | [[jellyfin]] | | **jellyfin_metrics** | Media metrics exporter | [[jellyfin]] | | **minikube** | Kubernetes cluster | [[cluster]] | @@ -70,7 +57,5 @@ Roles that need secrets use 1Password via the playbook's `pre_tasks`. Secrets ar ## Related -- [[indri]] — Primary managed host -- [[ringtail]] — NixOS host managed by its own playbook -- [[sifaka]] — Synology NAS managed by its own playbook +- [[indri]] — Target host - [[observability]] — Metrics collection diff --git a/docs/reference/tools/argocd-cli.md b/docs/reference/tools/argocd-cli.md index a2aa223..67fb281 100644 --- a/docs/reference/tools/argocd-cli.md +++ b/docs/reference/tools/argocd-cli.md @@ -1,7 +1,6 @@ --- title: ArgoCD CLI modified: 2026-02-12 -last-reviewed: 2026-04-01 tags: - reference - gitops @@ -24,14 +23,6 @@ argocd app sync apps # Sync the app-of-apps (picks up new Application ## Login -Default (Authentik SSO, PKCE, opens browser): - -```bash -argocd login argocd.ops.eblu.me --sso -``` - -Break-glass admin login (only if Authentik is down): - ```bash argocd login argocd.ops.eblu.me \ --username admin \ diff --git a/docs/reference/tools/dagger.md b/docs/reference/tools/dagger.md index 81c5caf..b07ed78 100644 --- a/docs/reference/tools/dagger.md +++ b/docs/reference/tools/dagger.md @@ -1,6 +1,6 @@ --- title: Dagger -modified: 2026-04-11 +modified: 2026-03-06 tags: - reference - ci-cd @@ -15,18 +15,17 @@ Build engine for BlumeOps CI/CD pipelines. Replaces shell-based build scripts wi | Property | Value | |----------|-------| -| **Module** | `blumeops` | -| **Engine Version** | v0.20.6 | +| **Module** | `blumeops-ci` | +| **Engine Version** | v0.20.1 | | **SDK** | Python | -| **Source** | `src/blumeops/main.py` | -| **Config** | `dagger.json` (source: `.`) | +| **Source** | `.dagger/src/blumeops_ci/main.py` | +| **Config** | `dagger.json` | ## Functions | Function | Signature | Description | |----------|-----------|-------------| -| `build` | `(src, container_name) → Container` | Build a container — uses native pipeline (`container.py`) if available, falls back to `docker_build()` for Dockerfile containers | -| `container_version` | `(container_name) → str` | Return the `VERSION` from a container's `container.py` (empty string if no `container.py`) | +| `build` | `(src, container_name) → Container` | Build a container from `containers//Dockerfile` | | `publish` | `(src, container_name, version, registry?) → str` | Build and push to registry (default: `registry.ops.eblu.me`) | | `build_nix` | `(src, container_name) → File` | Build a nix container from `containers//default.nix`, return docker-archive tarball | | `nix_version` | `(package) → str` | Extract the version of a nixpkgs package | @@ -34,32 +33,20 @@ Build engine for BlumeOps CI/CD pipelines. Replaces shell-based build scripts wi | `flake_lock` | `(src, flake_path?) → File` | Resolve flake inputs, return updated `flake.lock` | | `flake_update` | `(src, flake_path?) → File` | Update all flake inputs to latest, return `flake.lock` | -## Container Build Types - -Containers can be built in three ways: - -| Build file | How it works | Error visibility | -|------------|-------------|-----------------| -| `container.py` | Native Dagger pipeline (preferred) | Full per-step output | -| `Dockerfile` | `docker_build()` fallback (legacy) | Opaque — errors swallowed | -| `default.nix` | `nix-build` on ringtail runner | Full nix output | - -New containers for indri (k8s runner) should use `container.py`. Ringtail containers should continue using `default.nix`. Existing Dockerfile containers are migrated incrementally during [[review-services|service reviews]]. See `containers/navidrome/container.py` for the reference pattern. - ## CLI Examples ```bash # Build a container -dagger call build --src=. --container-name=miniflux +dagger call build --src=. --container-name=devpi # Drop into container shell for inspection -dagger call build --src=. --container-name=miniflux terminal +dagger call build --src=. --container-name=devpi terminal # Debug a failure interactively -dagger call --interactive build --src=. --container-name=miniflux +dagger call --interactive build --src=. --container-name=devpi # Publish a container to zot -dagger call publish --src=. --container-name=miniflux --version=v1.1.0 +dagger call publish --src=. --container-name=devpi --version=v1.1.0 # Build a nix container (no local nix required) dagger call build-nix --src=. --container-name=ntfy export --path=./ntfy.tar.gz diff --git a/docs/reference/tools/mise-tasks.md b/docs/reference/tools/mise-tasks.md index b614cb1..ae92013 100644 --- a/docs/reference/tools/mise-tasks.md +++ b/docs/reference/tools/mise-tasks.md @@ -1,6 +1,6 @@ --- title: Mise Tasks -modified: 2026-04-11 +modified: 2026-02-24 tags: - reference - tools @@ -33,13 +33,11 @@ Run `mise tasks --sort name` for the live list with descriptions. | `provision-indri` | Run Ansible playbook for [[indri]] | | `provision-ringtail` | Run Ansible playbook for [[ringtail]] (NixOS) | | `provision-sifaka` | Run Ansible playbook for [[sifaka]] | -| `fly-deploy` | Deploy Fly.io public proxy (uses op for auth) | -| `fly-reload` | Reload nginx config, re-resolve upstream DNS (no redeploy) | +| `fly-deploy` | Deploy Fly.io public proxy | | `fly-setup` | One-time Fly.io secrets and certs setup | | `fly-shutoff` | Emergency shutoff: stop all Fly.io proxy machines | | `dns-preview` | Preview DNS changes with [[pulumi]] | | `dns-up` | Apply DNS changes with [[pulumi]] | -| `dns-acme-cleanup` | Delete orphaned `_acme-challenge.ops` TXT records (libdns/gandi v1.1.0 workaround) | | `tailnet-preview` | Preview Tailscale ACL changes with [[pulumi]] | | `tailnet-up` | Apply Tailscale ACL changes with [[pulumi]] | @@ -49,7 +47,7 @@ Run `mise tasks --sort name` for the live list with descriptions. |------|-------------| | `container-list` | List containers and their recent tags | | `container-build-and-release` | Trigger container build workflows via Forgejo API | -| `container-version-check` | Validate version consistency across container.py, Dockerfiles, nix, and manifests | +| `container-version-check` | Validate version consistency across Dockerfiles, nix, and manifests | | `mirror-create` | Create an upstream mirror in the `mirrors/` Forgejo org | | `mirror-update-pats` | Update GitHub PAT on all mirror repos on indri | @@ -59,7 +57,7 @@ Run `mise tasks --sort name` for the live list with descriptions. |------|-------------| | `branch-cleanup` | Delete merged branches (local and remote) | | `pr-comments` | List unresolved PR comments | -| `runner-logs` | List Forgejo Actions runs and fetch job logs (supports `--repo`, `--limit`) | +| `runner-logs` | View Forgejo Actions workflow logs | | `validate-workflows` | Validate workflow files against runner schema | | `mikado-branch-invariant-check` | Validate Mikado Branch Invariant on `mikado/*` branches | @@ -69,6 +67,7 @@ Run `mise tasks --sort name` for the live list with descriptions. |------|-------------| | `services-check` | Check all services are online and responding | | `service-review` | Review the most stale service for version freshness | +| `blumeops-tasks` | List tasks from Todoist sorted by priority | | `op-backup` | Encrypt 1Password export and send to indri for borgmatic | ## Infrastructure Setup diff --git a/docs/reference/tools/pulumi.md b/docs/reference/tools/pulumi.md index a716bb9..3af94cd 100644 --- a/docs/reference/tools/pulumi.md +++ b/docs/reference/tools/pulumi.md @@ -1,7 +1,6 @@ --- title: Pulumi -modified: 2026-04-02 -last-reviewed: 2026-04-02 +modified: 2026-02-12 tags: - reference - iac @@ -43,15 +42,12 @@ mise run tailnet-up # Apply ACL/tag changes ## Authentication -- **Gandi**: `GANDI_PERSONAL_ACCESS_TOKEN` (fetched from 1Password by the mise task) -- **Tailscale**: `TAILSCALE_OAUTH_CLIENT_ID` + `TAILSCALE_OAUTH_CLIENT_SECRET` (fetched from 1Password by the mise task) +- **Gandi**: `GANDI_PERSONAL_ACCESS_TOKEN` environment variable +- **Tailscale**: `TAILSCALE_API_KEY` environment variable - **Pulumi state**: Local backend (no Pulumi Cloud) ## Related -- [[manage-eblu-me-dns]] — DNS records workflow -- [[rotate-gandi-pat]] — Rotate the Gandi PAT -- [[update-tailscale-acls]] — ACL editing and Pulumi workflow - [[gandi]] — DNS hosting - [[tailscale]] — Tailnet configuration - [[routing]] — How DNS records map to services diff --git a/docs/tutorials/adding-a-service.md b/docs/tutorials/adding-a-service.md index 52d4965..5fdce11 100644 --- a/docs/tutorials/adding-a-service.md +++ b/docs/tutorials/adding-a-service.md @@ -1,7 +1,6 @@ --- title: Adding a Service -modified: 2026-04-08 -last-reviewed: 2026-04-08 +modified: 2026-02-07 tags: - tutorials - argocd @@ -27,8 +26,7 @@ Adding a service involves: 2. Creating an ArgoCD Application 3. Configuring Tailscale ingress 4. Adding Homepage dashboard entry -5. Creating a reference card -6. Setting up Grafana dashboards (optional) +5. Setting up Grafana dashboards (optional) ## Step 1: Create Manifests Directory @@ -36,34 +34,12 @@ Create a directory for your service's Kubernetes manifests: ``` argocd/manifests// -├── kustomization.yaml ├── deployment.yaml ├── service.yaml ├── ingress-tailscale.yaml └── configmap.yaml # if needed ``` -### Kustomization - -Every service needs a `kustomization.yaml` that lists its resources and pins the container image tag. ArgoCD uses kustomize to render manifests. - -```yaml -# argocd/manifests/myservice/kustomization.yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - deployment.yaml - - service.yaml - - ingress-tailscale.yaml - -images: - - name: registry.ops.eblu.me/myservice - newTag: v1.0.0 -``` - -Use the `:kustomized` sentinel tag in `deployment.yaml` — kustomize replaces it with the `newTag` from above. To deploy a new version, update `newTag` here (not in the deployment). - ### Example Deployment ```yaml @@ -85,7 +61,7 @@ spec: spec: containers: - name: myservice - image: registry.ops.eblu.me/myservice:kustomized + image: registry.ops.eblu.me/myservice:v1.0.0 ports: - containerPort: 8080 ``` @@ -120,11 +96,9 @@ metadata: namespace: myservice spec: ingressClassName: tailscale - tls: - - hosts: - - myservice rules: - - http: + - host: myservice + http: paths: - path: / pathType: Prefix @@ -169,7 +143,7 @@ metadata: spec: project: default source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git targetRevision: main path: argocd/manifests/myservice destination: @@ -180,42 +154,7 @@ spec: - CreateNamespace=true ``` -## Step 5: Create a Reference Card - -Add a reference card at `docs/reference/services/.md` so the service is discoverable in documentation. Keep it short — target a 30-second reading time or less. Include a Quick Reference table with URLs, namespace, and image, then link out to how-to cards or other docs for anything deeper. - -```yaml ---- -title: My Service -modified: 2026-04-08 -tags: - - service ---- -``` - -```markdown -# My Service - -One-sentence description of what the service does. - -## Quick Reference - -| Property | Value | -|----------|-------| -| **URL** | https://myservice.ops.eblu.me | -| **Tailscale URL** | https://myservice.tail8d86e.ts.net | -| **Namespace** | `myservice` | -| **Image** | `registry.ops.eblu.me/myservice` | -| **Manifests** | `argocd/manifests/myservice/` | - -## Related - -- [[adding-a-service]] - Deployment tutorial -``` - -See existing cards like [[navidrome]] or [[kiwix]] for examples. - -## Step 6: Add Caddy Route (Optional) +## Step 5: Add Caddy Route (Optional) If the service needs to be accessible from other pods or containers, add a Caddy route in `ansible/roles/caddy/defaults/main.yml`: @@ -230,7 +169,7 @@ Then run `mise run provision-indri -- --tags caddy` to apply. This enables access via `https://myservice.ops.eblu.me`. See [[routing]] for details on when this is needed. -## Step 7: Deploy +## Step 6: Deploy ### Testing on a Feature Branch @@ -260,7 +199,7 @@ argocd app set myservice --revision main argocd app sync myservice ``` -## Step 8: Add Observability (Optional) +## Step 7: Add Observability (Optional) ### Prometheus Metrics @@ -302,7 +241,6 @@ See [[grafana]] for dashboard provisioning details. - [ ] ArgoCD Application created in `argocd/apps/` - [ ] Tailscale Ingress configured - [ ] Homepage annotations added -- [ ] Reference card created in `docs/reference/services/` - [ ] Caddy route added (if needed for pod access) - [ ] Feature branch tested via ArgoCD - [ ] Metrics/dashboard configured (if applicable) diff --git a/docs/tutorials/ai-assistance-guide.md b/docs/tutorials/ai-assistance-guide.md index 4f0c595..9138526 100644 --- a/docs/tutorials/ai-assistance-guide.md +++ b/docs/tutorials/ai-assistance-guide.md @@ -10,7 +10,7 @@ tags: > **Audiences:** AI, Owner -This guide provides context for AI agents assisting with BlumeOps operations, and helps Erich understand how to work effectively with AI assistance. +This guide provides context for AI agents (like Claude Code) assisting with BlumeOps operations, and helps Erich understand how to work effectively with AI assistance. ## Critical Rules @@ -22,7 +22,7 @@ These are non-negotiable for AI agents working in this repo: 4. **Wait for user review before deploying** - Create PRs, don't auto-deploy 5. **Never merge PRs without explicit request** - The user merges after review -Full rules are in the repo's `AGENTS.md`. See [[agent-change-process]] for the C0/C1/C2 change classification methodology — C0 (direct to main), C1 (feature branch + PR), C2 (Mikado Branch Invariant). +Full rules are in the repo's `CLAUDE.md`. See [[agent-change-process]] for the C0/C1/C2 change classification methodology — C0 (direct to main), C1 (feature branch + PR), C2 (Mikado Branch Invariant). ## Workflow Conventions @@ -98,6 +98,7 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail | `provision-indri` | Deploy changes to [[indri]]-hosted services via Ansible | | `services-check` | After deployments - verify all services are healthy | | `pr-comments` | Check unresolved PR comments during review | +| `blumeops-tasks` | Find pending tasks from Todoist | | `container-list` | View available container images and tags | | `container-build-and-release` | Trigger container build workflows | | `dns-preview` | Preview DNS changes before applying | @@ -110,8 +111,6 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail | `docs-review` | Review the most stale doc by last-reviewed date | | `runner-logs` | View Forgejo workflow logs (indri or ringtail runner) | -For task discovery, BlumeOps tasks live in [hephaestus](https://github.com/eblume/hephaestus) (`heph`), not Todoist. List outstanding work with `heph list --project Blumeops --json`. - For ArgoCD operations, use the `argocd` CLI directly: - `argocd app diff ` - Preview changes - `argocd app sync ` - Deploy changes diff --git a/docs/tutorials/contributing.md b/docs/tutorials/contributing.md index 0d48e8f..cddafea 100644 --- a/docs/tutorials/contributing.md +++ b/docs/tutorials/contributing.md @@ -1,7 +1,6 @@ --- title: Contributing -modified: 2026-04-21 -last-reviewed: 2026-04-21 +modified: 2026-02-07 tags: - tutorials - contributing @@ -11,7 +10,7 @@ tags: > **Audiences:** Contributor -This tutorial walks through making your first contribution to BlumeOps - from understanding the codebase to submitting a pull request. +This tutorial walks through making your first contribution to BluemeOps - from understanding the codebase to submitting a pull request. ## Prerequisites @@ -38,14 +37,14 @@ brew bundle # installs tea, argocd, mise, etc. ### Using Mise (Optional) -Mise manages language toolchains, runs tasks, and pins tools like `prek`: +Mise manages language toolchains and runs tasks: ```bash -mise install # installs Python, Node.js, prek, etc. from mise.toml +mise install # installs Python, Node.js, etc. from mise.toml ``` ### Git Hooks (prek) -Git hooks validate changes on `git commit` (prek is pinned in `mise.toml`): +Git hooks validate changes on `git commit`: ```bash prek install prek run --all-files # verify setup @@ -105,7 +104,6 @@ Fragment types (file suffix): - `.bugfix.md` - Bug fixes - `.infra.md` - Infrastructure changes - `.doc.md` - Documentation -- `.ai.md` - AI-assisted changes - `.misc.md` - Other ### 4. Test Your Changes diff --git a/docs/tutorials/exploring-the-docs.md b/docs/tutorials/exploring-the-docs.md index 2fd5f66..83aec43 100644 --- a/docs/tutorials/exploring-the-docs.md +++ b/docs/tutorials/exploring-the-docs.md @@ -30,15 +30,15 @@ The docs follow the [Diataxis](https://diataxis.fr/) framework: You probably want quick access to operational details: - [How-to](/how-to/) guides for common operations (deploy, troubleshoot, update ACLs) - [Reference](/reference/) has service URLs, commands, and config locations -- [[ai-assistance-guide]] explains how to work effectively with AI agents +- [[ai-assistance-guide]] explains how to work effectively with Claude - Run `mise run ai-docs` to prime AI context with key documentation -### For AI Agents +### For Claude/AI Agents Context for effective assistance: - Read [[ai-assistance-guide]] for operational conventions - [Reference](/reference/) has the technical specifics you'll need -- The repo's `AGENTS.md` has critical rules (especially the kubectl context requirement) +- The repo's `CLAUDE.md` has critical rules (especially the kubectl context requirement) ### For External Readers @@ -81,7 +81,7 @@ The `ai-docs` mise task concatenates key documentation files for AI context: mise run ai-docs ``` -This outputs key documentation files and a full tree listing of all docs, providing an agent with essential context for BlumeOps operations. +This outputs key documentation files and a full tree listing of all docs, providing Claude with essential context for BlumeOps operations. ## Related diff --git a/docs/tutorials/expose-service-publicly.md b/docs/tutorials/expose-service-publicly.md index 65af611..b3fdda6 100644 --- a/docs/tutorials/expose-service-publicly.md +++ b/docs/tutorials/expose-service-publicly.md @@ -1,7 +1,7 @@ --- title: Expose a Service Publicly -modified: 2026-04-18 -last-reviewed: 2026-04-18 +modified: 2026-03-15 +last-reviewed: 2026-03-03 tags: - tutorials - fly-io @@ -27,21 +27,24 @@ Internet → .eblu.me Fly.io edge (Anycast, TLS via Let's Encrypt) │ Fly.io VM (nginx reverse proxy + Tailscale) - │ (direct WireGuard tunnel to indri) - Caddy on indri (*.ops.eblu.me routing) + │ (WireGuard tunnel) + tailnet (tail8d86e.ts.net) │ - backend service (k8s, native, or remote) + .tail8d86e.ts.net (Tailscale ingress) + │ + k8s Service → pod ``` +(The approach works similarly for non-k8s services via `tailscale serve` +service definitions, eg. [[forgejo]] and [[zot]]) A single Fly.io container serves as the public-facing proxy for all exposed -services. Nginx routes all traffic through [[caddy]] on [[indri]] via a -direct Tailscale WireGuard connection. Caddy already knows how to route -to every service (native, minikube, or ringtail k3s), so adding a new -public service only requires an nginx `server` block and a DNS CNAME. +services. Each service gets a `server` block in the nginx config and a DNS +CNAME. The container joins the tailnet via an ephemeral auth key and reaches +backend services through Tailscale ingress endpoints. -The `*.ops.eblu.me` routes continue to work in parallel for private tailnet -access — the Fly proxy sends `Host: .ops.eblu.me` headers that -match the same Caddy routes. +Existing `*.ops.eblu.me` services remain private behind Tailscale — this +approach does not touch [[caddy]], [[gandi]] DNS-01, or any other existing +infrastructure. They can continue to operate in parallel for private access. ## Key decisions @@ -58,18 +61,21 @@ match the same Caddy routes. ## TLS in this architecture -There are three independent TLS segments: +There are three independent TLS segments — none involve Caddy: 1. **Browser → Fly.io edge**: Fly.io auto-provisions a Let's Encrypt certificate for each custom domain (e.g., `docs.eblu.me`). Validated via TLS-ALPN challenge — no DNS API needed. -2. **nginx → Caddy on indri**: nginx proxies to `https://indri.tail8d86e.ts.net` - with `Host: .ops.eblu.me`. Caddy serves its `*.ops.eblu.me` - Let's Encrypt wildcard cert. nginx uses `proxy_ssl_verify off` since the - underlying WireGuard tunnel is already encrypted. +2. **nginx → Tailscale ingress**: nginx proxies to + `https://.tail8d86e.ts.net`. The Tailscale ingress serves a + Tailscale-issued cert. nginx uses `proxy_ssl_verify off` since the + underlying tunnel is already encrypted. 3. **WireGuard tunnel**: All Tailscale traffic is encrypted at the network layer regardless of application-level TLS. +Caddy continues to serve `*.ops.eblu.me` with its existing Gandi DNS-01 +certificates. The two TLS domains are completely independent. + ## External references - [Tailscale on Fly.io](https://tailscale.com/kb/1132/flydotio) — official guide for running Tailscale in a Fly.io container @@ -110,8 +116,8 @@ See the actual files in `fly/` for current configuration. Key design points: - **`fly.toml`** — uses bluegreen deploys so the old machine serves traffic until the new one passes health checks. `auto_stop_machines = "off"` keeps the proxy always-on. - **`Dockerfile`** — multi-stage build pulling nginx, Tailscale, and [[alloy]] binaries. Alloy runs as a sidecar inside the container for observability (see below). -- **`start.sh`** — starts `tailscaled --port=41641` first (pinned port enables direct WireGuard peering), waits for MagicDNS readiness (polls `nslookup` against `100.100.100.100`), then starts nginx, fail2ban, and Alloy, and blocks on the nginx process. The MagicDNS check is required because the `upstream` block resolves DNS at config load. -- **`nginx.conf`** — uses a single `upstream` block with `keepalive` pointing at Caddy on indri (`indri.tail8d86e.ts.net:443`). All services route through this upstream with `Host: .ops.eblu.me` headers for Caddy routing. Includes a JSON access log format that Alloy tails for log collection and metric extraction. A catch-all server block serves `/healthz` and rejects unknown hosts. +- **`start.sh`** — starts `tailscaled` first (MagicDNS must be available before nginx resolves upstreams), then nginx in the background, then Alloy, and blocks on the nginx process. +- **`nginx.conf`** — uses a `resolver 100.100.100.100` directive so upstream DNS resolution is deferred to request time (not config load time). Each service gets a `server` block with a `set $upstream` variable pattern. Includes a JSON access log format that Alloy tails for log collection and metric extraction. A catch-all server block serves `/healthz` and rejects unknown hosts. - **`error.html`** — shown via `proxy_intercept_errors` when upstreams are unreachable (indri offline, tunnel down, etc.). Cached responses still take priority via `proxy_cache_use_stale`. #### Observability sidecar @@ -168,47 +174,25 @@ ACL test: { "src": "tag:flyio-proxy", "accept": ["tag:flyio-target:443"], - "deny": ["tag:k8s:443", "tag:homelab:22", "tag:nas:445", "tag:registry:443"], + "deny": ["tag:k8s:443", "tag:homelab:443", "tag:homelab:22", "tag:nas:445", "tag:registry:443"], }, ``` -Indri carries `tag:flyio-target` so the Fly proxy can reach Caddy. No per-service tagging is needed — Caddy handles routing to all services. +Each service's Tailscale Ingress must be annotated with `tag:flyio-target` to be reachable by the proxy — see [[#7. Tag the Tailscale Ingress with tag:flyio-target]]. Deploy: `mise run tailnet-preview` then `mise run tailnet-up`. -After deploying, push the auth key to Fly.io. The simplest path is -`mise run fly-setup`, which reads the current value from Pulumi state -and stages it as a Fly.io secret: - -```bash -mise run fly-setup -``` - -Manual equivalent for reference: +After deploying, extract the auth key and set it as a Fly.io secret: ```bash +# Get the key from Pulumi state cd pulumi/tailscale && pulumi stack output flyio_authkey --show-secrets -# then in fly/: -fly secrets set TS_AUTHKEY="tskey-auth-..." -a blumeops-proxy --stage + +# Set it in Fly.io +fly secrets set TS_AUTHKEY="tskey-auth-..." -a blumeops-proxy ``` -**Pulumi state is the only source of truth for this key.** No other -process (mise tasks, ansible, scripts) reads it from anywhere else — -in particular, the key is not stored in 1Password. To rotate -(every 90 days, or after a compromise), force-replace the resource -and re-run `fly-setup`: - -```bash -mise run tailnet-up -- \ - --replace='urn:pulumi:tail8d86e::blumeops-tailnet::tailscale:index/tailnetKey:TailnetKey::flyio-proxy-key' -mise run fly-setup -mise run fly-deploy -``` - -Pulumi destroys the old key and mints a new 90-day one in a single -operation. Older fly machines that already authed against the old key -are unaffected (they don't need it after the initial join); only -*new* machine starts read the rotated value. +Store the auth key in 1Password as well for the `fly-setup` mise task. ### Step 4: Mise tasks @@ -230,17 +214,13 @@ The `FLY_DEPLOY_TOKEN` Forgejo Actions secret must be set via the [[forgejo]] AP To expose an additional service (example: `wiki.eblu.me`): -### 1. Ensure the service has a Caddy route +### 1. Add nginx server block -The service must be accessible via `.ops.eblu.me` through [[caddy]]. -Most services already have this. If not, add it to `ansible/roles/caddy/defaults/main.yml` -and deploy with `mise run provision-indri -- --tags caddy`. - -### 2. Add nginx server block - -Edit `fly/nginx.conf` — add a `server` block. All services use the shared -`indri_backend` upstream (Caddy on indri). Set `Host` and `proxy_ssl_name` -to the service's `*.ops.eblu.me` hostname so Caddy routes correctly. +Edit `fly/nginx.conf` — add a new `server` block. The configuration +differs significantly between static and dynamic services. See the +existing `docs.eblu.me` and `cv.eblu.me` blocks in `fly/nginx.conf` +for the current pattern (uses `set $upstream` variable for deferred +DNS resolution, `proxy_intercept_errors` for error pages, etc.). **Static site template** (simplified — adapt from existing blocks): @@ -259,16 +239,12 @@ server { } location / { - proxy_pass https://indri_backend$request_uri; + set $upstream_wiki https://wiki.tail8d86e.ts.net; + proxy_pass $upstream_wiki$request_uri; proxy_ssl_verify off; proxy_ssl_server_name on; - proxy_ssl_name wiki.ops.eblu.me; - proxy_set_header Host wiki.ops.eblu.me; proxy_intercept_errors on; - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_cache services; proxy_cache_valid 200 1d; proxy_cache_valid 404 1m; @@ -283,7 +259,78 @@ server { } ``` -**Dynamic service template** — see `fly/nginx.conf` for the live Forgejo configuration, which includes rate-limited auth endpoints, cached static assets and release downloads, archive endpoint redirects, robots.txt, and WebSocket support. +**Dynamic service template** (e.g., Forgejo — see `fly/nginx.conf` for the live configuration): + +```nginx +# --- forge.eblu.me (dynamic, authenticated) --- +server { + listen 8080; + server_name forge.eblu.me; + + # Higher rate limit — git operations, CI webhooks, and API calls + # can legitimately burst. Forgejo also has its own rate limiting, + # so this is a safety net, not the primary control. + limit_req zone=general burst=50 nodelay; + + # Git LFS and repo uploads can be large + client_max_body_size 512m; + + error_page 502 503 504 /error.html; + location = /error.html { + root /usr/share/nginx/html; + internal; + } + + location / { + set $upstream_forge https://forge.tail8d86e.ts.net; + proxy_pass $upstream_forge$request_uri; + proxy_ssl_verify off; + proxy_ssl_server_name on; + proxy_intercept_errors on; + + # NO proxy_cache — dynamic content with sessions. + # Caching would serve stale pages and break authentication. + + # Pass through headers needed for proper proxying + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support (Forgejo uses it for live updates) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Selectively cache static assets only + location ~* \.(css|js|png|jpg|svg|woff2?)$ { + set $upstream_forge_static https://forge.tail8d86e.ts.net; + proxy_pass $upstream_forge_static$request_uri; + proxy_ssl_verify off; + proxy_ssl_server_name on; + + proxy_cache services; + proxy_cache_valid 200 7d; + proxy_cache_key $host$uri; + + add_header X-Cache-Status $upstream_cache_status; + add_header X-Clacks-Overhead "GNU Terry Pratchett" always; + } +} +``` + +Key differences for dynamic services: +- **No blanket caching** — only static assets (CSS, JS, images) are cached +- **Respect `Set-Cookie`** — do not ignore session headers +- **Include query strings** in non-cached requests (default behavior when + `proxy_cache_key` is not overridden) +- **Higher rate limits** — legitimate usage patterns are burstier +- **Proxy headers** — pass `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` + so the backend sees the real client IP (important for Forgejo's audit logs + and its own rate limiting) +- **WebSocket support** — many modern web apps use WebSockets +- **Larger body size** — git pushes and file uploads need more than the default 1MB ### 2. Add Fly.io certificate @@ -341,9 +388,18 @@ curl -I https://wiki.eblu.me # Should return 200 with X-Cache-Status header ``` -### 7. Verify routing +### 7. Tag the Tailscale Ingress with `tag:flyio-target` -Since all traffic routes through Caddy on indri, no per-service Tailscale Ingress tagging is needed. As long as the service has a Caddy route (step 1), the Fly proxy can reach it. +The fly.io proxy can only reach endpoints tagged with `tag:flyio-target`. Add the annotation to the service's Tailscale Ingress: + +```yaml +annotations: + tailscale.com/tags: "tag:k8s,tag:flyio-target" +``` + +Include `tag:k8s` to preserve existing access rules for the Ingress proxy node. The `tag:flyio-target` tag opts this specific endpoint into being reachable by the fly.io proxy — no broad ACL changes needed. + +For non-k8s services (e.g., Forgejo on indri), create a k8s ExternalName Service pointing to the host, then a Tailscale Ingress with the same annotation. --- @@ -376,13 +432,6 @@ Mitigations for dynamic services: - fail2ban on indri (see below) can block IPs showing abuse patterns - The break-glass shutoff remains the last resort -The most acute version of this in practice has been **AI scrapers**, which -ignore `robots.txt` and crawl dynamic services (notably [[forgejo|Forgejo]]'s -infinite git-history URL space) into both a surprise egress bill and an -effective L7 DoS. See [[ai-scraper-mitigation]] for the incident, the tiered -defense (mirror black-hole, user-agent denylist, Anubis proof-of-work), and -why a Cloudflare Tunnel is *not* the chosen answer here. - If a publicly exposed dynamic service attracts targeted attacks or the home network bandwidth is impacted, consider migrating to Cloudflare Tunnel for enterprise-grade DDoS protection (requires DNS migration; diff --git a/docs/tutorials/replicating-blumeops.md b/docs/tutorials/replicating-blumeops.md index e54ecb2..f2ed8ca 100644 --- a/docs/tutorials/replicating-blumeops.md +++ b/docs/tutorials/replicating-blumeops.md @@ -1,7 +1,6 @@ --- title: Replicating BlumeOps -modified: 2026-05-11 -last-reviewed: 2026-05-11 +modified: 2026-02-07 tags: - tutorials - replication @@ -11,7 +10,7 @@ tags: > **Audiences:** Replicator -This tutorial provides a roadmap for building your own homelab GitOps environment inspired by BlumeOps. It links to detailed component tutorials for each major piece. +This tutorial provides a roadmap for building your own homelab GitOps environment inspired by BluemeOps. It links to detailed component tutorials for each major piece. ## What You'll Build @@ -24,7 +23,7 @@ By following this guide, you'll have: ## Hardware Requirements -BlumeOps runs on modest hardware. At minimum: +BluemeOps runs on modest hardware. At minimum: | Component | BlumeOps Uses | Minimum Alternative | |-----------|---------------|---------------------| @@ -95,7 +94,7 @@ Without observability, you're flying blind. ### Phase 6: Your First Services -With the foundation in place, deploy actual workloads. BlumeOps runs: +With the foundation in place, deploy actual workloads. BluemeOps runs: - [[miniflux]] - RSS reader - [[jellyfin]] - Media server - [[immich]] - Photo management @@ -119,7 +118,7 @@ Protect your data. ## Alternative Approaches -BlumeOps makes specific choices that may not suit everyone: +BluemeOps makes specific choices that may not suit everyone: | BlumeOps Choice | Alternative | |-----------------|-------------| diff --git a/docs/tutorials/replication/core-services.md b/docs/tutorials/replication/core-services.md index 12c79e9..6657eab 100644 --- a/docs/tutorials/replication/core-services.md +++ b/docs/tutorials/replication/core-services.md @@ -1,7 +1,6 @@ --- title: Core Services modified: 2026-02-07 -last-reviewed: 2026-04-05 tags: - tutorials - replication @@ -10,8 +9,6 @@ tags: # Core Services Setup -> **TODO:** This tutorial is light on specifics and should be expanded. In its current form it serves as a general sketch of how BlumeOps got started — every component mentioned here (Forgejo, Zot, CI runners, etc.) receives much deeper treatment elsewhere in the blumeops codebase and documentation. - > **Audiences:** Replicator > > **Prerequisites:** [[tailscale-setup|Tailscale Setup]] @@ -49,13 +46,11 @@ Key configuration points: ## Step 2: Configure SSH Access -Forgejo runs its own SSH server on a non-standard port (e.g., 2222) to avoid conflicting with the host's SSH daemon on port 22. Later, [[caddy]] with the L4 plugin can map port 22 to 2222 using a DNS name (e.g., `forge.ops.eblu.me`), so git clients don't need to specify a port. - Set up SSH for git operations: ```bash # Add your SSH key to Forgejo via the web UI -# Then test access (using the non-standard port directly): +# Then test access: ssh -T git@your-server.tailnet.ts.net -p 2222 ``` @@ -80,46 +75,13 @@ your-repo/ ## Step 4: Set Up CI/CD Runner (Optional) -Forgejo Actions runs workflows defined in `.forgejo/workflows/`. The simplest setup is a native runner that executes jobs directly on the host (no Docker required): +Forgejo Actions runs workflows defined in `.forgejo/workflows/`. To use it: -1. Download the `forgejo-runner` binary from [code.forgejo.org](https://code.forgejo.org/forgejo/runner/releases): +1. Register a runner on your server +2. Configure runner to access your build tools +3. Create workflow files for builds and deployments -```bash -# macOS ARM64 example -curl -L -o forgejo-runner \ - https://code.forgejo.org/forgejo/runner/releases/download/v6.3.1/forgejo-runner-6.3.1-darwin-arm64 -chmod +x forgejo-runner -``` - -2. Generate a registration token from the Forgejo admin UI (Site Administration → Actions → Runners) - -3. Register and start the runner using the `host` scheme (no container isolation): - -```bash -forgejo-runner register \ - --instance https://your-forge.tailnet.ts.net \ - --token \ - --name local-runner \ - --labels "native:host" \ - --no-interactive - -forgejo-runner daemon -``` - -4. Reference the label in your workflows: - -```yaml -# .forgejo/workflows/example.yaml -jobs: - build: - runs-on: native - steps: - - run: echo "Running on bare metal" -``` - -> **Note:** Host mode has no isolation — jobs run as whatever user runs `forgejo-runner`. This is fine for a personal setup with trusted repos. Use a `launchd` plist or `brew services` wrapper to keep it running. - -BlumeOps runs its Forgejo runner inside Kubernetes instead — see [[forgejo]] for that approach. +BlumeOps runs a Forgejo runner in Kubernetes - see [[forgejo]] for details. ## Step 5: Container Registry (Optional) diff --git a/docs/tutorials/replication/observability-stack.md b/docs/tutorials/replication/observability-stack.md index d62731e..db98683 100644 --- a/docs/tutorials/replication/observability-stack.md +++ b/docs/tutorials/replication/observability-stack.md @@ -1,7 +1,6 @@ --- title: Observability Stack -modified: 2026-04-06 -last-reviewed: 2026-04-06 +modified: 2026-02-07 tags: - tutorials - replication @@ -11,14 +10,12 @@ tags: # Building the Observability Stack > **Audiences:** Replicator -> -> **Prerequisites:** [[kubernetes-bootstrap|Kubernetes Bootstrap]], [[argocd-config|ArgoCD Config]] -This tutorial walks through deploying metrics, logs, and dashboards for your homelab — because you can't fix what you can't see. +This tutorial walks through deploying metrics, logs, and dashboards for your homelab - because you can't fix what you can't see. ## The Stack -A complete observability solution has three pillars plus a collection layer: +A complete observability solution has three pillars: | Component | Purpose | BlumeOps Uses | |-----------|---------|---------------| @@ -27,11 +24,9 @@ A complete observability solution has three pillars plus a collection layer: | **Dashboards** | Visualization and alerting | [[grafana]] | | **Collection** | Gathering and forwarding data | [[alloy]] | -BlumeOps deploys all of these as plain kustomize manifests managed by ArgoCD — no Helm charts. See [[no-helm-policy]] for the rationale and [[observability]] for the full reference. +For BlumeOps specifics, see [[observability|Observability Reference]]. -## Step 1: Create the Monitoring Namespace - -ArgoCD can create this automatically via `CreateNamespace=true` in the Application spec, but if you're bootstrapping manually: +## Step 1: Create Monitoring Namespace ```bash kubectl create namespace monitoring @@ -39,46 +34,20 @@ kubectl create namespace monitoring ## Step 2: Deploy Prometheus -Prometheus collects and stores metrics. BlumeOps runs it as a StatefulSet with local persistent storage. +Prometheus collects and stores metrics. -### Write the Manifests +### Using Helm -Create `argocd/manifests/prometheus/` with: - -- **`kustomization.yaml`** — references the manifests and patches the container image -- **`statefulset.yaml`** — a single-replica StatefulSet with a 20Gi PVC for `/prometheus` -- **`configmap.yaml`** — the `prometheus.yml` scrape configuration -- **`service.yaml`** — exposes port 9090 within the cluster - -Key StatefulSet settings: - -```yaml -args: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.retention.time=3650d" - - "--web.enable-remote-write-receiver" - - "--web.enable-lifecycle" +```bash +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm install prometheus prometheus-community/prometheus \ + --namespace monitoring \ + --set server.persistentVolume.size=10Gi ``` -The remote-write-receiver flag is important — it lets [[alloy]] push metrics into Prometheus from both the host and in-cluster collectors. - -### Tag the Image - -Use your local container registry and the `:kustomized` sentinel pattern: - -```yaml -# kustomization.yaml -images: - - name: registry.ops.eblu.me/blumeops/prometheus - newTag: v3.10.0-abcdef0 -``` - -See [[build-container-image]] for how to build and tag images. - -### Create the ArgoCD Application - -Add `argocd/apps/prometheus.yaml`: +### Or via ArgoCD +Create an Application pointing to a values file in your repo: ```yaml apiVersion: argoproj.io/v1alpha1 kind: Application @@ -88,15 +57,17 @@ metadata: spec: project: default source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - path: argocd/manifests/prometheus - targetRevision: main + repoURL: https://prometheus-community.github.io/helm-charts + chart: prometheus + targetRevision: 25.0.0 + helm: + values: | + server: + persistentVolume: + size: 10Gi destination: server: https://kubernetes.default.svc namespace: monitoring - syncPolicy: - syncOptions: - - CreateNamespace=true ``` ### Verify @@ -107,133 +78,155 @@ kubectl -n monitoring get pods -l app.kubernetes.io/name=prometheus ## Step 3: Deploy Loki -Loki aggregates logs — think Prometheus, but for log lines instead of metrics. +Loki aggregates logs (like Prometheus but for logs). -### Write the Manifests +```bash +helm repo add grafana https://grafana.github.io/helm-charts +helm install loki grafana/loki-stack \ + --namespace monitoring \ + --set loki.persistence.enabled=true \ + --set loki.persistence.size=10Gi +``` -Create `argocd/manifests/loki/` with a StatefulSet, ConfigMap, and Service similar to Prometheus. Loki listens on port 3100 (HTTP) and 9096 (gRPC). - -The config file (`loki-config.yaml`) defines storage, compaction, and retention. For a homelab, a simple single-binary mode with local filesystem storage works well — no need for S3 or distributed mode. - -### Create the ArgoCD Application - -Same pattern as Prometheus — point to `argocd/manifests/loki`, target `monitoring` namespace. +This also installs Promtail for log collection from pods. ## Step 4: Deploy Grafana -Grafana provides dashboards, visualization, and alerting. +Grafana provides dashboards and visualization. -### Write the Manifests - -Grafana has more moving parts than Prometheus or Loki: - -- **Deployment** with a PVC for `/var/lib/grafana` -- **ConfigMap** containing `grafana.ini`, `datasources.yaml`, and `alerting.yaml` -- **Dashboard ConfigMaps** labeled `grafana_dashboard: "1"` — a sidecar container watches for these and auto-loads them -- **ExternalSecret** for the admin password (from 1Password via [[external-secrets]]) - -Configure data sources declaratively in the ConfigMap: - -```yaml -# datasources.yaml -apiVersion: 1 -datasources: - - name: Prometheus - type: prometheus - url: http://prometheus.monitoring.svc:9090 - isDefault: true - - name: Loki - type: loki - url: http://loki.monitoring.svc:3100 +```bash +helm install grafana grafana/grafana \ + --namespace monitoring \ + --set persistence.enabled=true \ + --set persistence.size=1Gi \ + --set adminPassword=admin # Change this! ``` -### Secrets +### Configure Data Sources -Grafana's admin password and any OAuth credentials (for [[authentik]] SSO) should come from 1Password via ExternalSecret — never hardcode passwords in manifests. See [[external-secrets]] and [[security-model]]. - -### Expose via Caddy - -BlumeOps exposes Grafana at `grafana.ops.eblu.me` through [[caddy]] on [[indri]], which reverse-proxies to the Kubernetes service via its Tailscale Ingress endpoint. This is the standard pattern for all services — see [[routing]] for details. - -## Step 5: Deploy Alloy - -Grafana Alloy is a unified telemetry collector that replaces multiple agents (Promtail, node_exporter, etc.). BlumeOps runs Alloy in **two places** — it is not optional; it's the glue that connects everything. - -### In-Cluster (DaemonSet) - -Create `argocd/manifests/alloy-k8s/` with: - -- **DaemonSet** — runs on every node, mounts `/var/log` read-only for pod log access -- **ServiceAccount + RBAC** — needs pod list/watch for Kubernetes discovery -- **ConfigMap** — the `config.alloy` file defining: - - Kubernetes pod log discovery and collection - - Service health probes (blackbox-style checks for key services) - - Remote write to Prometheus (`/api/v1/write`) and Loki (`/loki/api/v1/push`) - -The DaemonSet goes in a dedicated `alloy` namespace, separate from `monitoring`. - -### On the Host (Ansible) - -For metrics and logs from native services (Forgejo, Zot, Caddy, Borgmatic), Alloy runs directly on [[indri]] as a macOS LaunchAgent, managed by [[ansible]]. - -The host Alloy collects: -- System metrics via `prometheus.exporter.unix` -- Logs from Homebrew services and LaunchAgents -- Optional: PostgreSQL metrics, container registry metrics - -It pushes to the same Prometheus and Loki endpoints via `*.ops.eblu.me`. - -## What You Now Have - -- **Prometheus** scraping metrics from all services -- **Loki** aggregating logs from all pods and host services -- **Grafana** with declarative dashboards and data sources -- **Alloy** collecting from both Kubernetes and the host -- A foundation for alerting via Grafana Unified Alerting - -## Adding Alerts - -BlumeOps uses Grafana Unified Alerting (not Prometheus Alertmanager). Alerts are defined declaratively in `alerting.yaml` within the Grafana ConfigMap. Notifications go to [[ntfy]] — a self-hosted push notification service. - -Example alert categories: -- Service probe failures (is Grafana/Prometheus/Loki reachable?) -- Pod readiness (are pods healthy?) -- Metrics freshness (is data still flowing?) -- Storage and resource thresholds - -See [[observability]] for the full alerting reference. - -## Adding Dashboards - -Import community dashboards or create custom ones. BlumeOps uses a sidecar pattern — any ConfigMap in the `monitoring` namespace with the label `grafana_dashboard: "1"` is automatically loaded by Grafana's sidecar container. - -Create dashboard ConfigMaps in `argocd/manifests/grafana-config/dashboards/`: +After installation, add data sources in Grafana UI or via ConfigMap: ```yaml apiVersion: v1 kind: ConfigMap metadata: - name: grafana-dashboard-my-service + name: grafana-datasources + namespace: monitoring labels: - grafana_dashboard: "1" + grafana_datasource: "1" data: - my-service.json: | - { ... dashboard JSON ... } + datasources.yaml: | + apiVersion: 1 + datasources: + - name: Prometheus + type: prometheus + url: http://prometheus-server.monitoring.svc:80 + isDefault: true + - name: Loki + type: loki + url: http://loki.monitoring.svc:3100 ``` +## Step 5: Access Grafana + +Expose via Tailscale: +```bash +kubectl -n monitoring port-forward svc/grafana 3000:80 & +tailscale serve --bg --https 3000 http://localhost:3000 +``` + +Or create an Ingress. + +Default credentials: `admin` / (password you set or retrieve from secret) + +## Step 6: Add Dashboards + +Import community dashboards from [grafana.com/grafana/dashboards](https://grafana.com/grafana/dashboards/): + +| Dashboard | ID | Shows | +|-----------|-----|-------| +| Node Exporter Full | 1860 | Host metrics | +| Kubernetes Cluster | 7249 | Cluster overview | +| Loki Logs | 13639 | Log exploration | + +In Grafana: Dashboards > Import > Enter ID + +## Step 7: Deploy Alloy (Optional) + +Grafana Alloy is a unified collector that replaces multiple agents (Promtail, node_exporter, etc.). + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: alloy + namespace: argocd +spec: + project: default + source: + repoURL: https://grafana.github.io/helm-charts + chart: alloy + targetRevision: 0.1.0 + helm: + values: | + alloy: + configMap: + content: | + // Alloy configuration here + destination: + server: https://kubernetes.default.svc + namespace: monitoring +``` + +BluemeOps uses Alloy on both [[indri]] (for host metrics, via [[ansible|Ansible role]]) and in the [[cluster]] (for pod logs and service probes). + +## What You Now Have + +- Metrics collection and storage (Prometheus) +- Log aggregation (Loki) +- Dashboards and visualization (Grafana) +- Foundation for alerting + +## Adding Alerts + +Configure alerting rules in Prometheus: + +```yaml +groups: +- name: example + rules: + - alert: HighMemoryUsage + expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.1 + for: 5m + labels: + severity: warning + annotations: + summary: "High memory usage detected" +``` + +And notification channels in Grafana (email, Slack, PagerDuty, etc.). + ## Next Steps -- Set up [[authentik]] SSO for Grafana login (see [[federated-login]]) - Create custom dashboards for your services -- Configure alerting rules and notification channels +- Set up alerting for critical conditions - Add service-specific metrics exporters -## Related +## BluemeOps Specifics -- [[observability]] — Full observability reference -- [[no-helm-policy]] — Why kustomize instead of Helm -- [[alloy]] — Alloy collector reference -- [[prometheus]] — Prometheus reference -- [[loki]] — Loki reference -- [[grafana]] — Grafana reference -- [[routing]] — Service routing and exposure +BlumeOps' observability setup includes: +- Prometheus scraping all services via annotations +- Loki collecting logs from all pods and [[indri]] services +- Custom dashboards for [[jellyfin]], [[teslamate]], and cluster health +- [[alloy]] running on both host and in-cluster + +See [[observability|Observability Reference]] for full details. + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| No metrics appearing | Check Prometheus targets (`/targets` endpoint) | +| No logs in Loki | Verify Promtail/Alloy is collecting (`/ready` endpoint) | +| Dashboard shows no data | Check data source configuration and time range | +| High storage usage | Adjust retention settings in Prometheus/Loki | diff --git a/fly/Dockerfile b/fly/Dockerfile index 406c849..3f866fa 100644 --- a/fly/Dockerfile +++ b/fly/Dockerfile @@ -1,10 +1,9 @@ -# nginx 1.30.1-alpine -FROM nginx@sha256:c819f83c54b0361f5557601bf5eb4943d09360e7a7fdf426afc466570f45874d +FROM nginx:1.29.6-alpine -# Copy tailscale binaries from official image (v1.94.2) -COPY --from=docker.io/tailscale/tailscale@sha256:95e528798bebe75f39b10e74e7051cf51188ee615934f232ba7ad06a3390ffa1 \ +# Copy tailscale binaries from official image +COPY --from=docker.io/tailscale/tailscale:stable \ /usr/local/bin/tailscaled /usr/local/bin/tailscaled -COPY --from=docker.io/tailscale/tailscale@sha256:95e528798bebe75f39b10e74e7051cf51188ee615934f232ba7ad06a3390ffa1 \ +COPY --from=docker.io/tailscale/tailscale:stable \ /usr/local/bin/tailscale /usr/local/bin/tailscale RUN mkdir -p /var/run/tailscale /var/lib/tailscale \ @@ -13,8 +12,8 @@ RUN mkdir -p /var/run/tailscale /var/lib/tailscale \ && apk add --no-cache fail2ban \ && rm -f /etc/fail2ban/jail.d/alpine-ssh.conf -# Copy Alloy binary from official image (v1.16.1, Ubuntu-based, needs libc6-compat) -COPY --from=docker.io/grafana/alloy@sha256:51aeb9d829239345070619dad3edd6873186f913c84f45b365b74574fcb38ec0 \ +# Copy Alloy binary from official image (Ubuntu-based, needs libc6-compat) +COPY --from=docker.io/grafana/alloy:v1.14.1 \ /bin/alloy /usr/local/bin/alloy RUN mkdir -p /var/log/nginx /etc/alloy /tmp/alloy-data @@ -25,7 +24,6 @@ COPY fail2ban/action.d/nginx-deny.conf /etc/fail2ban/action.d/nginx-deny.conf COPY nginx.conf /etc/nginx/nginx.conf COPY error.html /usr/share/nginx/html/error.html -COPY naughty.html /usr/share/nginx/html/naughty.html COPY alloy.river /etc/alloy/config.alloy COPY start.sh /start.sh RUN chmod +x /start.sh diff --git a/fly/alloy.river b/fly/alloy.river index 015583c..06ad977 100644 --- a/fly/alloy.river +++ b/fly/alloy.river @@ -61,16 +61,7 @@ loki.process "nginx" { name = "flyio_nginx_http_request_duration_seconds" description = "HTTP request latency in seconds." source = "request_time" - buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 30, 45, 60] - } - } - - stage.metrics { - metric.histogram { - name = "flyio_nginx_upstream_response_time_seconds" - description = "Upstream (Forgejo) response time in seconds, excluding proxy overhead." - source = "upstream_response_time" - buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 20, 30, 45, 60] + buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] } } diff --git a/fly/fail2ban/action.d/nginx-deny.conf b/fly/fail2ban/action.d/nginx-deny.conf index bab8abb..1d3737b 100644 --- a/fly/fail2ban/action.d/nginx-deny.conf +++ b/fly/fail2ban/action.d/nginx-deny.conf @@ -2,22 +2,13 @@ # Standard iptables banning won't work in Fly.io because $remote_addr # is Fly's internal proxy IP. Instead, we write banned IPs to a file # that nginx checks via a geo directive keyed on $http_fly_client_ip. -# -# The deny file is per-service: each jail sets `nginx_deny_file = ...` -# (see jail.d/*.conf) and a matching `geo $http_fly_client_ip $..._banned` -# block in nginx.conf includes the same path. [Definition] -actionban = echo " 1;" >> && nginx -s reload +actionban = echo " 1;" >> /etc/nginx/forge-deny.conf && nginx -s reload -actionunban = sed -i '/ 1;/d' && nginx -s reload +actionunban = sed -i '/ 1;/d' /etc/nginx/forge-deny.conf && nginx -s reload actionstart = actionstop = actioncheck = - -[Init] - -# Default for jails that don't override (preserves forge behaviour). -nginx_deny_file = /etc/nginx/forge-deny.conf diff --git a/fly/fly.toml b/fly/fly.toml index 6ccf29d..17e3de8 100644 --- a/fly/fly.toml +++ b/fly/fly.toml @@ -7,7 +7,7 @@ primary_region = "sjc" memory = "512mb" [deploy] -strategy = "immediate" +strategy = "bluegreen" [http_service] internal_port = 8080 @@ -22,12 +22,3 @@ interval = "10s" method = "GET" path = "/healthz" timeout = "5s" - -# Expose Tailscale's WireGuard port so direct peer-to-peer connections can -# establish instead of falling back to DERP relay. Requires a dedicated IPv4. -[[services]] -internal_port = 41641 -protocol = "udp" - -[[services.ports]] -port = 41641 diff --git a/fly/naughty.html b/fly/naughty.html deleted file mode 100644 index b6eada8..0000000 --- a/fly/naughty.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - 403 · Roll of Dishonour - - - -
-

🪤 403 — you walked into the scraper trap

-

These are mirror repositories. They are tailnet-only.

- -

- This path used to serve the web UI for mirrors of public upstream - projects. It exists for supply-chain control, not for crawling. A - robots.txt politely disallowed /mirrors/. - A pack of AI scrapers ignored it, walked the infinite git-history URL - space, and ran up ~1.25 TB of egress and a real - money bill in a single month — while timing out the server for everyone - else. -

- -

So /mirrors/ is closed at the edge now. Roll of dishonour, - by share of the bytes they stole:

- - - - - - - - - -
OperatorUser-Agent
Metameta-externalagent
OpenAIGPTBot
AmazonAmazonbot
ByteDanceBytespider
- -

- If you are a human who actually wanted these mirrors, they are reachable - from the tailnet at forge.ops.eblu.me. If you are a crawler: - read the robots.txt next time. We left you a header, too. -

-
- - diff --git a/fly/nginx.conf b/fly/nginx.conf index ec35774..992a5df 100644 --- a/fly/nginx.conf +++ b/fly/nginx.conf @@ -27,22 +27,13 @@ http { access_log /var/log/nginx/access.json.log json_log; # Rate limiting zones — define per-service zones as needed - limit_req_zone $http_fly_client_ip zone=general:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; # Forge-specific rate limit keyed on real client IP (Fly-Client-IP header). # $binary_remote_addr is Fly's internal proxy IP — all clients share one # bucket. $http_fly_client_ip has the actual client IP. limit_req_zone $http_fly_client_ip zone=forge_auth:10m rate=3r/s; - # Shower-specific zone: loose enough that ~30 guests sharing a single - # venue-wifi NAT'd public IP can all scan the QR and load the splash - # (HTML + a handful of static asset hits each) without anyone tripping - # the limit. 50r/s + burst=200 covers the simultaneous-load spike; - # exploit scanners still trip it (e.g. the .env-sweeping bot we saw - # fired ~30 req in 2s — that pattern stays caught). See the - # shower.eblu.me server block for the matching `limit_req`. - limit_req_zone $http_fly_client_ip zone=shower_general:10m rate=50r/s; - # fail2ban deny list — banned IPs are written here by fail2ban and # checked via the $forge_banned variable. The file is touched at # container start to ensure it exists. @@ -55,27 +46,12 @@ http { proxy_cache_path /tmp/cache levels=1:2 keys_zone=services:10m max_size=200m inactive=24h; - # WebSocket-aware Connection header. Only send "upgrade" when the client - # actually requests a protocol switch; otherwise empty string to preserve - # upstream keepalive connections. - map $http_upgrade $connection_upgrade { - default ""; - websocket upgrade; - } - - # --- Upstream --- - # DNS resolved via Tailscale MagicDNS at config load. + # MagicDNS resolver — using a variable in proxy_pass defers upstream DNS + # resolution to request time (not config time). Results are cached for + # 30s per worker to avoid per-request DNS lookups. resolver 100.100.100.100 valid=30s; resolver_timeout 5s; - # All services route through Caddy on indri. Indri's host-level Tailscale - # can establish direct WireGuard peering, avoiding the DERP relay - # bottleneck that k8s-hosted Tailscale Ingress pods cannot escape. - upstream indri_backend { - server indri.tail8d86e.ts.net:443; - keepalive 16; - } - # --- docs.eblu.me (static site) --- server { listen 8080; @@ -92,16 +68,12 @@ http { internal; } location / { - proxy_pass https://indri_backend$request_uri; + set $upstream_docs https://docs.tail8d86e.ts.net; + proxy_pass $upstream_docs$request_uri; proxy_ssl_verify off; proxy_ssl_server_name on; - proxy_ssl_name docs.ops.eblu.me; - proxy_set_header Host docs.ops.eblu.me; proxy_intercept_errors on; - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - # Cache aggressively — static site only. # Do NOT use these settings for dynamic services. proxy_cache services; @@ -136,16 +108,12 @@ http { } location / { - proxy_pass https://indri_backend$request_uri; + set $upstream_cv https://cv.tail8d86e.ts.net; + proxy_pass $upstream_cv$request_uri; proxy_ssl_verify off; proxy_ssl_server_name on; - proxy_ssl_name cv.ops.eblu.me; - proxy_set_header Host cv.ops.eblu.me; proxy_intercept_errors on; - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_cache services; proxy_cache_valid 200 1d; proxy_cache_valid 404 1m; @@ -187,128 +155,37 @@ http { internal; } - # Serve robots.txt directly — block crawlers from expensive endpoints - location = /robots.txt { - default_type text/plain; - return 200 "User-agent: *\nDisallow: /mirrors/\nDisallow: /user/\nDisallow: /users/\nDisallow: /*/archive/\nDisallow: /*/releases/download/\n"; - } - - # Block the package registry at the public edge. Forgejo's per-user - # visibility model treats packages as world-readable when the owner - # has Visibility=Public — which means anyone on the internet can - # enumerate and download every wheel/sdist/generic artifact, even - # for private-repo releases (the sdist contains full source). We - # like keeping eblume's profile public, so we close the hole here - # at the proxy instead: WAN sees 403, tailnet (forge.ops.eblu.me) - # stays open for legitimate consumers (CI workflows, gilbert). - # See docs/tutorials/expose-service-publicly.md for the broader - # threat model on this proxy. - location /api/packages/ { - return 403 "Package downloads are tailnet-only — use forge.ops.eblu.me.\n"; - } - location /api/v1/packages { - return 403 "Package enumeration is tailnet-only — use forge.ops.eblu.me.\n"; - } - # Block swagger API docs — use forge.ops.eblu.me from tailnet location /swagger { return 403 "API documentation is only available at forge.ops.eblu.me (tailnet).\n"; } - # Black-hole the mirror repositories on WAN. These are mirrors of - # already-public upstreams (tailscale, prometheus, mealie, …) kept - # for supply-chain control; CI, gilbert, and tailnet clients consume - # them via forge.ops.eblu.me. Their web UI served no public purpose - # but AI scrapers, which crawled the near-infinite git-history URL - # space (src/commit, commits, blame, raw) and drove ~70% of Fly - # egress (1.24 TB/30d → a surprise bill) plus enough upstream load to - # time out Forgejo. robots.txt already Disallows /mirrors/, but - # meta-externalagent and GPTBot ignore it — so enforce at the edge. - # `^~` makes this win over the regex locations below (e.g. *.css), so - # static assets under /mirrors/ can't leak through. We also name and - # shame: blocked requests get a "roll of dishonour" page (403 status - # preserved) and an X-Naughty-Scrapers header. See - # docs/explanation/ai-scraper-mitigation.md. - location ^~ /mirrors/ { - error_page 403 /naughty.html; - return 403; - } - - # Roll of dishonour — served on the /mirrors/ 403, status kept at 403. - location = /naughty.html { - internal; - root /usr/share/nginx/html; - add_header X-Naughty-Scrapers "OpenAI/GPTBot, Meta/meta-externalagent, Amazonbot, ByteDance/Bytespider — robots.txt ignorers" always; - add_header X-Clacks-Overhead "GNU Terry Pratchett" always; - } - - # Redirect archive endpoints to tailnet — archive requests generate full - # git bundles on demand. Unauthenticated crawlers hitting unique commit - # SHAs cause unbounded CPU and disk usage (DoS vector). Legitimate users - # can download via forge.ops.eblu.me on the tailnet. - location ~ ^/[^/]+/[^/]+/archive/ { - default_type text/html; - return 302 https://forge.ops.eblu.me$request_uri; - } - # Rate-limit authentication endpoints location ~ ^/user/(login|sign_up|forgot_password) { limit_req zone=forge_auth burst=5 nodelay; - proxy_pass https://indri_backend$request_uri; + set $upstream_forge https://forge.tail8d86e.ts.net; + proxy_pass $upstream_forge$request_uri; proxy_ssl_verify off; proxy_ssl_server_name on; - proxy_ssl_name forge.ops.eblu.me; proxy_intercept_errors on; - proxy_set_header Host forge.ops.eblu.me; + proxy_set_header Host $host; proxy_set_header X-Real-IP $http_fly_client_ip; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } - - # Cache release artifact downloads — immutable files keyed by tag+filename. - # Avoids hammering Forgejo when crawlers or users re-download the same asset. - location ~ ^/[^/]+/[^/]+/releases/download/ { - proxy_pass https://indri_backend$request_uri; - proxy_ssl_verify off; - proxy_ssl_server_name on; - proxy_ssl_name forge.ops.eblu.me; - - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - - proxy_cache services; - proxy_cache_valid 200 7d; - proxy_cache_key $host$uri; - - proxy_set_header Host forge.ops.eblu.me; - proxy_set_header X-Real-IP $http_fly_client_ip; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - add_header X-Cache-Status $upstream_cache_status; - add_header X-Clacks-Overhead "GNU Terry Pratchett" always; + proxy_set_header Connection "upgrade"; } # Selectively cache static assets only location ~* \.(css|js|png|jpg|svg|woff2?)$ { - proxy_pass https://indri_backend$request_uri; + set $upstream_forge_static https://forge.tail8d86e.ts.net; + proxy_pass $upstream_forge_static$request_uri; proxy_ssl_verify off; proxy_ssl_server_name on; - proxy_ssl_name forge.ops.eblu.me; - - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - - proxy_set_header Host forge.ops.eblu.me; - proxy_set_header X-Real-IP $http_fly_client_ip; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; proxy_cache services; proxy_cache_valid 200 7d; @@ -319,15 +196,15 @@ http { } location / { - proxy_pass https://indri_backend$request_uri; + set $upstream_forge https://forge.tail8d86e.ts.net; + proxy_pass $upstream_forge$request_uri; proxy_ssl_verify off; proxy_ssl_server_name on; - proxy_ssl_name forge.ops.eblu.me; proxy_intercept_errors on; # NO proxy_cache — dynamic content with sessions - proxy_set_header Host forge.ops.eblu.me; + proxy_set_header Host $host; proxy_set_header X-Real-IP $http_fly_client_ip; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; @@ -335,146 +212,12 @@ http { # WebSocket support (Forgejo uses it for live updates) proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; + proxy_set_header Connection "upgrade"; add_header X-Clacks-Overhead "GNU Terry Pratchett" always; } } - # --- shower.eblu.me (Adelaide baby shower — guest-only public surface) --- - # Only the guest paths (`/`, `/prizes//`, /static/, /media/) are - # exposed on WAN. /host/, /admin/, and Django's login views are blocked - # at the edge with a 403 pointing at the tailnet hostname — staff sign - # in on shower.ops.eblu.me, which is reachable from any device with - # Tailscale installed. Defense layers reduce to: general per-IP rate - # limit + django-axes (5 fails / 1h) on the tailnet-side login. No - # fail2ban needed here because the public surface no longer takes - # credentials of any kind. - server { - listen 8080; - server_name shower.eblu.me; - - # Per-IP rate limit. shower_general (50r/s, burst=200) instead of - # the global `general` zone because at the party, guests on the - # venue's wifi all NAT through a single Fly-Client-IP — 30 guests - # scanning the QR at once would each fetch HTML + a few static - # assets, easily clearing 20 burst on `general`. Exploit scanners - # still trip it (sustained ≫ 50r/s patterns). - limit_req zone=shower_general burst=200 nodelay; - - # Image uploads from /host/'s prize cropper are ~150-300 KiB JPEGs. - # The host page itself isn't reachable here, but /media/ reads can - # be larger than 1 MiB so set the cap to 5 MiB to match Django. - client_max_body_size 5m; - - # Security headers — HSTS matches Django's SECURE_HSTS_SECONDS. - add_header X-Frame-Options "DENY" always; - add_header X-Content-Type-Options "nosniff" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - add_header Referrer-Policy "same-origin" always; - # GNU Terry Pratchett — keep the name moving. - add_header X-Clacks-Overhead "GNU Terry Pratchett" always; - - error_page 502 503 504 /error.html; - location = /error.html { - root /usr/share/nginx/html; - internal; - } - - # Reject indexers — there's nothing here we want crawled. - location = /robots.txt { - default_type text/plain; - return 200 "User-agent: *\nDisallow: /\n"; - } - - # Admin surface: tailnet-only. Anything under /admin/ — login, - # logout, CRUD UI, password reset — returns 403 with a pointer to - # the tailnet host. Django's `staff_member_required` will redirect - # /host/ to /admin/login/, which lands on this 403 if a guest - # device wanders into it. Staff hit the tailnet host directly. - location /admin/ { - return 403 "Authentication is tailnet-only — visit shower.ops.eblu.me.\n"; - } - - # Operator console: tailnet-only. Same rationale as /admin/. - location /host/ { - return 403 "The host console is tailnet-only — visit shower.ops.eblu.me.\n"; - } - - # Static assets — WhiteNoise + CompressedManifestStaticFilesStorage - # gives content-hashed filenames, so cache aggressively. Hashed - # names make cache invalidation automatic on app upgrades. - location /static/ { - proxy_pass https://indri_backend$request_uri; - proxy_ssl_verify off; - proxy_ssl_server_name on; - proxy_ssl_name shower.ops.eblu.me; - - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host shower.ops.eblu.me; - proxy_set_header X-Real-IP $http_fly_client_ip; - proxy_set_header X-Forwarded-For $http_fly_client_ip; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_cache services; - proxy_cache_valid 200 1y; - proxy_cache_valid 404 1m; - proxy_cache_use_stale error timeout updating; - proxy_cache_lock on; - proxy_cache_key $host$uri; - proxy_ignore_headers Cache-Control Set-Cookie; - - add_header X-Cache-Status $upstream_cache_status; - } - - # Prize photo uploads. Shorter TTL than /static/ because filenames - # aren't content-hashed — operators can re-upload a prize photo - # and we want guests to see the new image within a day. - location /media/ { - proxy_pass https://indri_backend$request_uri; - proxy_ssl_verify off; - proxy_ssl_server_name on; - proxy_ssl_name shower.ops.eblu.me; - - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host shower.ops.eblu.me; - proxy_set_header X-Real-IP $http_fly_client_ip; - proxy_set_header X-Forwarded-For $http_fly_client_ip; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_cache services; - proxy_cache_valid 200 1d; - proxy_cache_valid 404 1m; - proxy_cache_use_stale error timeout updating; - proxy_cache_lock on; - proxy_cache_key $host$uri; - proxy_ignore_headers Cache-Control Set-Cookie; - - add_header X-Cache-Status $upstream_cache_status; - } - - location / { - proxy_pass https://indri_backend$request_uri; - proxy_ssl_verify off; - proxy_ssl_server_name on; - proxy_ssl_name shower.ops.eblu.me; - proxy_intercept_errors on; - - # No proxy_cache — dynamic content with sessions and CSRF. - - proxy_set_header Host shower.ops.eblu.me; - proxy_set_header X-Real-IP $http_fly_client_ip; - proxy_set_header X-Forwarded-For $http_fly_client_ip; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } - } - # Catch-all: reject unknown hosts, but serve health check server { listen 8080 default_server; diff --git a/fly/start.sh b/fly/start.sh index a924849..5b08490 100644 --- a/fly/start.sh +++ b/fly/start.sh @@ -5,25 +5,16 @@ set -e # With bluegreen deploys, the old machine serves traffic until this one is # fully ready. Fly.io runs Firecracker microVMs that support TUN devices # natively — no need for --tun=userspace-networking. -tailscaled --statedir=/var/lib/tailscale --port=41641 & +tailscaled --statedir=/var/lib/tailscale & sleep 2 tailscale up --authkey="${TS_AUTHKEY}" --hostname=flyio-proxy until tailscale status > /dev/null 2>&1; do sleep 1; done echo "Tailscale connected" -# Wait for MagicDNS to be ready — upstream blocks resolve DNS at config -# load, so nginx will fail to start if MagicDNS can't resolve yet. -echo "Waiting for MagicDNS..." -until nslookup forge.tail8d86e.ts.net 100.100.100.100 > /dev/null 2>&1; do - sleep 1 -done -echo "MagicDNS ready" - # Ensure fail2ban deny file exists before nginx starts -# (the geo directive's `include` fails if the file is missing). touch /etc/nginx/forge-deny.conf -# Start nginx — MagicDNS is available, upstreams resolved. +# Start nginx — MagicDNS is available, health check passes immediately. nginx -g "daemon off;" & NGINX_PID=$! echo "Nginx started" diff --git a/mise-tasks/blumeops-tasks b/mise-tasks/blumeops-tasks new file mode 100755 index 0000000..94daa51 --- /dev/null +++ b/mise-tasks/blumeops-tasks @@ -0,0 +1,164 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = ["httpx>=0.28.1", "rich>=14.0.0"] +# /// +#MISE description="List Blumeops tasks from Todoist sorted by priority" +"""Fetch and display Blumeops tasks from Todoist, sorted by priority. + +This script is specific to Erich Blume's personal development workflow and +is not intended for general use. It requires: + + - A 1Password CLI (`op`) configured with access to the author's vault + - A Todoist account with a project named "Blumeops" + +The script fetches tasks and displays them sorted by a custom priority order: +p1 (urgent), p2 (high), p4 (normal/default), p3 (backlog). The p3-last ordering +reflects a deliberate choice to treat p3 as "backlog" rather than moderate +priority. + +Usage: mise run blumeops-tasks +""" + +import subprocess +import sys +from datetime import date + +import httpx +from rich.console import Console +from rich.text import Text + +TODOIST_API_BASE = "https://api.todoist.com/api/v1" +PROJECT_NAME = "Blumeops" + +# Priority mapping: Todoist API uses 1=normal(p4), 2=moderate(p3), 3=high(p2), 4=urgent(p1) +# User wants order: p1, p2, p4, p3 (p3 is backlog, goes last) +PRIORITY_LABELS = {4: "p1", 3: "p2", 1: "p4", 2: "p3"} +PRIORITY_SORT_ORDER = {4: 1, 3: 2, 1: 3, 2: 4} # Lower = earlier + + +def get_todoist_token() -> str: + """Retrieve Todoist API token from 1Password.""" + result = subprocess.run( + ["op", "read", "op://vg6xf6vvfmoh5hqjjhlhbeoaie/c53h3xnmswhvexa5mntoyvhgpm/credential"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to get Todoist token from 1Password: {result.stderr}") + return result.stdout.strip() + + +def get_project_id(client: httpx.Client, project_name: str) -> str: + """Find project ID by name.""" + cursor = None + while True: + params = {} + if cursor: + params["cursor"] = cursor + response = client.get(f"{TODOIST_API_BASE}/projects", params=params) + response.raise_for_status() + data = response.json() + for project in data.get("results", data if isinstance(data, list) else []): + if project["name"] == project_name: + return project["id"] + cursor = data.get("next_cursor") if isinstance(data, dict) else None + if not cursor: + break + + raise RuntimeError(f"Project '{project_name}' not found in Todoist") + + +def get_tasks(client: httpx.Client, project_id: str) -> list[dict]: + """Get all tasks for a project.""" + tasks = [] + cursor = None + while True: + params = {"project_id": project_id} + if cursor: + params["cursor"] = cursor + response = client.get(f"{TODOIST_API_BASE}/tasks", params=params) + response.raise_for_status() + data = response.json() + tasks.extend(data.get("results", data if isinstance(data, list) else [])) + cursor = data.get("next_cursor") if isinstance(data, dict) else None + if not cursor: + break + return tasks + + +def is_due(task: dict) -> bool: + """Check if a task should be displayed based on its due date. + + Tasks without a due date are always shown. Tasks with a due date + are only shown when the date is today or in the past. + """ + due = task.get("due") + if due is None: + return True + due_date = date.fromisoformat(due["date"][:10]) + return due_date <= date.today() + + +def sort_tasks(tasks: list[dict]) -> list[dict]: + """Sort tasks by custom priority order: p1, p2, p4, p3.""" + return sorted(tasks, key=lambda t: PRIORITY_SORT_ORDER.get(t["priority"], 5)) + + +def main() -> int: + console = Console() + + # Get API token + try: + token = get_todoist_token() + except RuntimeError as e: + console.print(f"[red]Error:[/red] {e}") + return 1 + + # Create HTTP client with auth header + with httpx.Client(headers={"Authorization": f"Bearer {token}"}) as client: + # Find project + try: + project_id = get_project_id(client, PROJECT_NAME) + except RuntimeError as e: + console.print(f"[red]Error:[/red] {e}") + return 1 + + # Get, filter, and sort tasks + tasks = get_tasks(client, project_id) + tasks = [t for t in tasks if is_due(t)] + sorted_tasks = sort_tasks(tasks) + + if not sorted_tasks: + console.print("No tasks found in Blumeops project") + return 0 + + # Display tasks + console.print(f"[bold]Blumeops Tasks[/bold] ({len(sorted_tasks)} tasks)") + console.print("=" * 40) + console.print() + + for task in sorted_tasks: + priority = task["priority"] + label = PRIORITY_LABELS.get(priority, "p?") + content = task["content"] + description = task.get("description", "") + + # Header line with priority and content + header = Text() + header.append(f"[{label}]", style="bold") + header.append(f" {content}") + console.print(header) + + # Description indented + if description: + for line in description.split("\n"): + console.print(f" {line}", style="dim") + + console.print() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/mise-tasks/branch-cleanup b/mise-tasks/branch-cleanup index a538880..bd5ac66 100755 --- a/mise-tasks/branch-cleanup +++ b/mise-tasks/branch-cleanup @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["httpx==0.28.1", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["httpx>=0.28.1", "rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Delete branches that have been merged into main (local and remote)" #MISE alias="bc" diff --git a/mise-tasks/container-build-and-release b/mise-tasks/container-build-and-release index 85e6cb8..508d586 100755 --- a/mise-tasks/container-build-and-release +++ b/mise-tasks/container-build-and-release @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["typer==0.26.2", "httpx==0.28.1"] +# dependencies = ["typer>=0.24.0", "httpx>=0.28.1"] # /// #MISE description="Trigger container build workflows via Forgejo API" #USAGE arg "" help="Container name (directory under containers/)" @@ -15,7 +15,6 @@ Dockerfile and Nix builds in a single workflow. import subprocess import sys -import time from pathlib import Path import httpx @@ -49,60 +48,12 @@ def get_forge_token() -> str: return result.stdout.strip() -def max_run_number(headers: dict[str, str]) -> int: - """Return the highest current run_number for WORKFLOW, or 0 if none.""" - resp = httpx.get( - f"{FORGE_API}/repos/{REPO}/actions/tasks", - params={"limit": 50}, - headers=headers, - timeout=15, - ) - if resp.status_code != 200: - return 0 - runs = [ - t["run_number"] - for t in resp.json().get("workflow_runs", []) - if t.get("workflow_id") == WORKFLOW - ] - return max(runs, default=0) - - -def find_dispatched_run( - ref: str, floor: int, headers: dict[str, str], timeout_s: int = 20 -) -> int | None: - """Poll the tasks endpoint for the run triggered by our dispatch. - - Matches by head_sha + workflow + run_number > floor so we don't pick up - an older build of the same commit or a concurrent unrelated dispatch. - """ - deadline = time.monotonic() + timeout_s - while time.monotonic() < deadline: - resp = httpx.get( - f"{FORGE_API}/repos/{REPO}/actions/tasks", - params={"limit": 20}, - headers=headers, - timeout=15, - ) - if resp.status_code == 200: - for task in resp.json().get("workflow_runs", []): - if ( - task.get("head_sha") == ref - and task.get("workflow_id") == WORKFLOW - and task.get("run_number", 0) > floor - ): - return task["run_number"] - time.sleep(1) - return None - - def list_containers() -> None: typer.echo("Available containers:") for d in sorted(Path("containers").iterdir()): if not d.is_dir(): continue types = [] - if (d / "container.py").exists(): - types.append("dagger") if (d / "Dockerfile").exists(): types.append("dockerfile") if (d / "default.nix").exists(): @@ -119,12 +70,11 @@ def main( ) -> None: """Trigger container build workflows via Forgejo API dispatch.""" container_dir = Path("containers") / container - has_container_py = (container_dir / "container.py").exists() has_dockerfile = (container_dir / "Dockerfile").exists() has_nix = (container_dir / "default.nix").exists() - if not has_container_py and not has_dockerfile and not has_nix: - typer.echo(f"Error: No container.py, Dockerfile, or default.nix found in '{container_dir}'") + if not has_dockerfile and not has_nix: + typer.echo(f"Error: No Dockerfile or default.nix found in '{container_dir}'") typer.echo() list_containers() raise typer.Exit(1) @@ -136,16 +86,14 @@ def main( ref = git("rev-parse", ref) short_sha = ref[:7] - image = f"blumeops/{container}" # Show expected builds builds = [] - if has_container_py or has_dockerfile: - label = "dagger" if has_container_py else "dockerfile" - builds.append(f" {label:12s} -> {REGISTRY}/{image}:v-{short_sha}") + if has_dockerfile: + builds.append(f" dockerfile -> {REGISTRY}/{image}:v-{short_sha}") if has_nix: - builds.append(f" {'nix':12s} -> {REGISTRY}/{image}:v-{short_sha}-nix") + builds.append(f" nix -> {REGISTRY}/{image}:v-{short_sha}-nix") if dry_run: typer.echo("[dry-run mode]") @@ -159,8 +107,7 @@ def main( if dry_run: typer.echo(f"[dry-run] Would dispatch {WORKFLOW}") typer.echo() - typer.echo("Monitor builds with: mise run runner-logs") - typer.echo(f" or visit: {FORGE_ACTIONS}") + typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") return token = get_forge_token() @@ -169,21 +116,6 @@ def main( "Content-Type": "application/json", } - # Verify the commit has been pushed to the remote - resp = httpx.get( - f"{FORGE_API}/repos/{REPO}/git/commits/{ref}", - headers=headers, - timeout=15, - ) - if resp.status_code != 200: - typer.echo(f"Error: commit {short_sha} not found on remote") - typer.echo("Push your changes before triggering a build: git push origin main") - raise typer.Exit(1) - - # Snapshot the highest existing run_number so we can identify the one - # our dispatch creates. - floor = max_run_number(headers) - url = f"{FORGE_API}/repos/{REPO}/actions/workflows/{WORKFLOW}/dispatches" payload = { "ref": "main", @@ -200,12 +132,7 @@ def main( raise typer.Exit(1) typer.echo() - run_number = find_dispatched_run(ref, floor, headers) - if run_number is not None: - typer.echo(f"Monitor builds with: mise run runner-logs {run_number}") - else: - typer.echo("Monitor builds with: mise run runner-logs") - typer.echo(f" or visit: {FORGE_ACTIONS}") + typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") if __name__ == "__main__": diff --git a/mise-tasks/container-list b/mise-tasks/container-list index 7dad346..5c554b6 100755 --- a/mise-tasks/container-list +++ b/mise-tasks/container-list @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["httpx==0.28.1", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["httpx>=0.28.1", "rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="List available containers and their recent tags" #USAGE arg "[name]" help="Optional container name to filter output" @@ -78,14 +78,11 @@ def discover_containers() -> list[dict]: for d in sorted(CONTAINER_DIR.iterdir()): if not d.is_dir(): continue - has_container_py = (d / "container.py").exists() has_dockerfile = (d / "Dockerfile").exists() has_nix = (d / "default.nix").exists() - if not has_container_py and not has_dockerfile and not has_nix: + if not has_dockerfile and not has_nix: continue types = [] - if has_container_py: - types.append("dagger") if has_dockerfile: types.append("dockerfile") if has_nix: diff --git a/mise-tasks/container-version-check b/mise-tasks/container-version-check index 06f96ae..1df062f 100755 --- a/mise-tasks/container-version-check +++ b/mise-tasks/container-version-check @@ -1,19 +1,18 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml==6.0.3", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["pyyaml>=6.0.2", "rich>=14.0.0", "typer>=0.24.0"] # /// -#MISE description="Validate container version consistency across container.py, Dockerfiles, nix derivations, and service-versions.yaml" +#MISE description="Validate container version consistency across Dockerfiles, nix derivations, and service-versions.yaml" #USAGE flag "--all-files" help="Check all containers, not just changed ones" """Validate that container versions are consistent across all declaration sites. For each container directory under containers/, checks: -1. Any container.py must declare VERSION= -2. Any Dockerfile must declare ARG CONTAINER_APP_VERSION= -3. Any default.nix must produce a version (via dagger call nix-version) -4. At least one build file (container.py, Dockerfile, or default.nix) must exist -5. A matching entry in service-versions.yaml must exist with non-null current-version -6. All resolved versions from (1), (2), (3), and (5) must agree +1. Any Dockerfile must declare ARG CONTAINER_APP_VERSION= +2. Any default.nix must produce a version (via dagger call nix-version) +3. At least one build file (Dockerfile or default.nix) must exist +4. A matching entry in service-versions.yaml must exist with non-null current-version +5. All resolved versions from (1), (2), and (4) must agree By default, only checks containers whose files differ from main. Pass --all-files to check every container. @@ -42,6 +41,7 @@ BLACKLIST = {"kubectl"} # Container dir name → service-versions.yaml name (when they differ) CONTAINER_TO_SERVICE = { + "quartz": "docs", "kiwix-serve": "kiwix", } @@ -53,7 +53,6 @@ NIX_PACKAGE_MAP = { "authentik": "authentik", } -CONTAINER_PY_VERSION_PATTERN = re.compile(r'^VERSION\s*=\s*["\']([^"\']+)["\']', re.MULTILINE) VERSION_ARG_PATTERN = re.compile(r"^ARG\s+CONTAINER_APP_VERSION=(\S+)", re.MULTILINE) NIX_VERSION_PATTERN = re.compile(r'^\s*version\s*=\s*"([^"]+)"\s*;', re.MULTILINE) @@ -110,7 +109,7 @@ def get_nix_version(container_name: str, nix_file: Path) -> str | None: return None result = subprocess.run( - ["dagger", "call", "nix-version", f"--package={pkg}"], + ["dagger", "-m", ".dagger", "call", "nix-version", f"--package={pkg}"], capture_output=True, text=True, cwd=REPO_ROOT, @@ -149,37 +148,26 @@ def main( if scope is not None and name not in scope: continue - container_py = container_dir / "container.py" dockerfile = container_dir / "Dockerfile" nix_file = container_dir / "default.nix" - has_container_py = container_py.exists() has_dockerfile = dockerfile.exists() has_nix = nix_file.exists() versions: dict[str, str] = {} entry = { "name": name, - "has_container_py": has_container_py, "has_dockerfile": has_dockerfile, "has_nix": has_nix, "versions": versions, } results.append(entry) - # Rule 4: at least one build file - if not has_container_py and not has_dockerfile and not has_nix: - errors.append((name, "No container.py, Dockerfile, or default.nix found")) + # Rule 3: at least one build file + if not has_dockerfile and not has_nix: + errors.append((name, "No Dockerfile or default.nix found")) continue - # Rule 1: container.py must declare VERSION - if has_container_py: - match = CONTAINER_PY_VERSION_PATTERN.search(container_py.read_text()) - if match: - versions["container.py"] = match.group(1) - else: - errors.append((name, "container.py missing VERSION declaration")) - - # Rule 2: Dockerfile must declare CONTAINER_APP_VERSION + # Rule 1: Dockerfile must declare CONTAINER_APP_VERSION if has_dockerfile: match = VERSION_ARG_PATTERN.search(dockerfile.read_text()) if match: @@ -187,7 +175,7 @@ def main( else: errors.append((name, "Dockerfile missing ARG CONTAINER_APP_VERSION")) - # Rule 3: nix derivation must produce a version + # Rule 2: nix derivation must produce a version if has_nix: nix_ver = get_nix_version(name, nix_file) if nix_ver is not None: @@ -231,8 +219,6 @@ def main( for entry in results: name = entry["name"] build_parts = [] - if entry["has_container_py"]: - build_parts.append("dagger") if entry["has_dockerfile"]: build_parts.append("dockerfile") if entry["has_nix"]: diff --git a/mise-tasks/dns-acme-cleanup b/mise-tasks/dns-acme-cleanup deleted file mode 100755 index 3a53b11..0000000 --- a/mise-tasks/dns-acme-cleanup +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.12" -# dependencies = ["httpx==0.28.1", "rich==15.0.0", "typer==0.26.2"] -# /// -#MISE description="Delete orphaned ACME challenge TXT records in eblu.me" -#USAGE flag "--dry-run" help="List orphans without deleting" -"""Clean up orphaned _acme-challenge TXT records in the eblu.me zone. - -Workaround for libdns/gandi v1.1.0: its DeleteRecords compares unquoted -certmagic values to Gandi-quoted stored values, so cleanup is a silent -no-op. Without this script, the rrset grows by ~2 values per successful -Caddy renewal cycle. - -In healthy steady state these records should be absent. Run alongside -PAT rotation, or any time after Caddy ACME activity. -""" - -import os -import subprocess -from typing import Annotated - -import httpx -import typer -from rich.console import Console -from rich.table import Table - -DOMAIN = "eblu.me" -RRSET = "_acme-challenge.ops" -GANDI_API = "https://api.gandi.net/v5/livedns" -OP_PAT_REF = "op://blumeops/gandi - blumeops/pat" - - -def resolve_token(console: Console) -> str: - env_token = os.environ.get("GANDI_PERSONAL_ACCESS_TOKEN", "").strip() - if env_token: - return env_token - console.print("[dim]Reading Gandi PAT from 1Password...[/dim]") - try: - result = subprocess.run( - ["op", "read", OP_PAT_REF], - capture_output=True, - text=True, - check=True, - ) - return result.stdout.strip() - except (subprocess.CalledProcessError, FileNotFoundError) as e: - console.print(f"[red]Failed to read PAT from 1Password:[/red] {e}") - raise typer.Exit(1) - - -app = typer.Typer(add_completion=False) - - -@app.command() -def main( - dry_run: Annotated[ - bool, - typer.Option("--dry-run", help="List orphans without deleting"), - ] = False, -) -> None: - """Delete orphan _acme-challenge TXT records in eblu.me.""" - console = Console() - token = resolve_token(console) - - url = f"{GANDI_API}/domains/{DOMAIN}/records/{RRSET}/TXT" - headers = {"Authorization": f"Bearer {token}"} - - with httpx.Client(timeout=15, headers=headers) as client: - resp = client.get(url) - if resp.status_code == 404: - console.print( - f"[green]Clean — {RRSET}.{DOMAIN} TXT rrset is absent.[/green]" - ) - raise typer.Exit(0) - resp.raise_for_status() - values = resp.json().get("rrset_values", []) - - if not values: - console.print( - f"[green]Clean — {RRSET}.{DOMAIN} TXT rrset is empty.[/green]" - ) - raise typer.Exit(0) - - table = Table(title=f"Orphan ACME challenge values: {RRSET}.{DOMAIN}") - table.add_column("#", justify="right") - table.add_column("Value") - for i, v in enumerate(values, 1): - table.add_row(str(i), v) - console.print(table) - console.print(f"\n[bold]{len(values)}[/bold] orphan(s).") - - if dry_run: - console.print("\n[dim]Dry run — no records deleted.[/dim]") - raise typer.Exit(0) - - del_resp = client.delete(url) - if del_resp.status_code == 204: - console.print( - f"[green]Deleted {RRSET}.{DOMAIN} TXT " - f"({len(values)} values).[/green]" - ) - else: - console.print( - f"[red]Delete failed: HTTP {del_resp.status_code}[/red]\n" - f"{del_resp.text[:300]}" - ) - raise typer.Exit(1) - - -if __name__ == "__main__": - app() diff --git a/mise-tasks/docs-check-frontmatter b/mise-tasks/docs-check-frontmatter index 35e1879..11d1a49 100755 --- a/mise-tasks/docs-check-frontmatter +++ b/mise-tasks/docs-check-frontmatter @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["rich==15.0.0"] +# dependencies = ["rich>=14.0.0"] # /// #MISE description="Check that all docs have required frontmatter fields" """Validate that all documentation files have required YAML frontmatter. diff --git a/mise-tasks/docs-check-links b/mise-tasks/docs-check-links index 9974fc7..78e871a 100755 --- a/mise-tasks/docs-check-links +++ b/mise-tasks/docs-check-links @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["rich==15.0.0"] +# dependencies = ["rich>=14.0.0"] # /// #MISE description="Validate all wiki-links point to existing doc files" """Validate that all wiki-links in documentation point to existing files. diff --git a/mise-tasks/docs-mikado b/mise-tasks/docs-mikado index c632e46..0b37f51 100755 --- a/mise-tasks/docs-mikado +++ b/mise-tasks/docs-mikado @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["httpx==0.28.1", "pyyaml==6.0.3", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["httpx>=0.28.1", "pyyaml>=6.0.2", "rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="View active Mikado dependency chains for C2 changes" #USAGE arg "[card]" help="Card stem to show chain for" diff --git a/mise-tasks/docs-preview b/mise-tasks/docs-preview index 9e0bd16..f63b1d1 100755 --- a/mise-tasks/docs-preview +++ b/mise-tasks/docs-preview @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml==6.0.3", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["pyyaml>=6.0.2", "rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Build docs with Dagger and serve locally, opening to a specific card" #USAGE arg "" help="Card path relative to docs/, e.g. how-to/knowledgebase/review-documentation" diff --git a/mise-tasks/docs-review b/mise-tasks/docs-review index 12e301f..49cf4d0 100755 --- a/mise-tasks/docs-review +++ b/mise-tasks/docs-review @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml==6.0.3", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["pyyaml>=6.0.2", "rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Review the most stale documentation card by last-reviewed date" #USAGE flag "--limit " default="15" help="Number of docs to show in the table" diff --git a/mise-tasks/docs-review-stale b/mise-tasks/docs-review-stale index 0c5490e..facbf6b 100755 --- a/mise-tasks/docs-review-stale +++ b/mise-tasks/docs-review-stale @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["rich==15.0.0", "typer==0.26.2"] +# dependencies = ["rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Report docs by git-last-modified date, highlighting stale ones" #USAGE flag "--threshold " default="180" help="Days before a doc is considered stale" diff --git a/mise-tasks/docs-review-tags b/mise-tasks/docs-review-tags index 869e2f2..0e7f1d4 100755 --- a/mise-tasks/docs-review-tags +++ b/mise-tasks/docs-review-tags @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml==6.0.3", "rich==15.0.0"] +# dependencies = ["pyyaml>=6.0.2", "rich>=14.0.0"] # /// #MISE description="Print frontmatter tag inventory across all docs" """Print every frontmatter tag with usage count and file list. diff --git a/mise-tasks/fly-deploy b/mise-tasks/fly-deploy index 8d693fd..bb2b4f8 100755 --- a/mise-tasks/fly-deploy +++ b/mise-tasks/fly-deploy @@ -3,8 +3,5 @@ set -euo pipefail -export FLY_API_TOKEN -FLY_API_TOKEN="$(op read 'op://blumeops/fly.io admin/add more/deploy-token')" - cd "$(dirname "$0")/../fly" fly deploy "$@" diff --git a/mise-tasks/fly-reload b/mise-tasks/fly-reload deleted file mode 100755 index 34806c5..0000000 --- a/mise-tasks/fly-reload +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -#MISE description="Reload Fly.io proxy nginx config (re-resolves upstream DNS)" - -set -euo pipefail - -export FLY_API_TOKEN -FLY_API_TOKEN="$(op read 'op://blumeops/fly.io admin/add more/deploy-token')" - -# SSH into the Fly machine and send nginx a reload signal. -# This re-resolves upstream DNS without a full redeploy. -APP="blumeops-proxy" -MACHINE_ID=$(fly machines list -a "$APP" --json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])") - -echo "Reloading nginx on machine $MACHINE_ID..." -fly ssh console -a "$APP" -C "nginx -s reload" -echo "Done. Upstream DNS re-resolved." diff --git a/mise-tasks/fly-setup b/mise-tasks/fly-setup index be797e5..0c5cb56 100755 --- a/mise-tasks/fly-setup +++ b/mise-tasks/fly-setup @@ -23,7 +23,6 @@ echo "IPs allocated" fly certs add docs.eblu.me -a "$APP" 2>/dev/null || true fly certs add cv.eblu.me -a "$APP" 2>/dev/null || true fly certs add forge.eblu.me -a "$APP" 2>/dev/null || true -fly certs add shower.eblu.me -a "$APP" 2>/dev/null || true echo "Certificates configured" echo "Done. Run 'mise run fly-deploy' to deploy." diff --git a/mise-tasks/mikado-branch-invariant-check b/mise-tasks/mikado-branch-invariant-check index 3135bf2..8760a39 100755 --- a/mise-tasks/mikado-branch-invariant-check +++ b/mise-tasks/mikado-branch-invariant-check @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["rich==15.0.0", "typer==0.26.2"] +# dependencies = ["rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Validate Mikado Branch Invariant on mikado/* branches" #USAGE arg "[commit_msg_file]" help="Commit message file (passed by commit-msg hook)" @@ -294,7 +294,7 @@ def main( console.print(f" [red]✗[/red] {error}") console.print() console.print( - "[dim]See: docs/explanation/agent-change-process.md " + "[dim]See: docs/how-to/agent-change-process.md " "§ The Mikado Branch Invariant[/dim]" ) raise SystemExit(1) diff --git a/mise-tasks/op-backup b/mise-tasks/op-backup index 7db033b..6ffef14 100755 --- a/mise-tasks/op-backup +++ b/mise-tasks/op-backup @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["rich==15.0.0", "typer==0.26.2"] +# dependencies = ["rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Encrypt a 1Password .1pux export and send to indri for borgmatic" #USAGE arg "[export_path]" help="Path to .1pux export file (prompted if omitted)" diff --git a/mise-tasks/pr-comments b/mise-tasks/pr-comments index 39d7c9a..a44a430 100755 --- a/mise-tasks/pr-comments +++ b/mise-tasks/pr-comments @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["httpx==0.28.1", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["httpx>=0.28.1", "rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="List unresolved comments on a PR" #USAGE arg "" help="Pull request number" diff --git a/mise-tasks/prune-ringtail-generations b/mise-tasks/prune-ringtail-generations index 2ad8dc8..8066f8b 100755 --- a/mise-tasks/prune-ringtail-generations +++ b/mise-tasks/prune-ringtail-generations @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["rich==15.0.0", "typer==0.26.2"] +# dependencies = ["rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Prune old NixOS generations on ringtail, preserving rollback safety" #MISE alias="prg" diff --git a/mise-tasks/review-compliance-reports b/mise-tasks/review-compliance-reports deleted file mode 100755 index 24d2afc..0000000 --- a/mise-tasks/review-compliance-reports +++ /dev/null @@ -1,695 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.12" -# dependencies = ["rich==15.0.0", "typer==0.26.2", "pyyaml==6.0.3"] -# /// -#MISE description="Summarize the latest Prowler and Kingfisher compliance reports from sifaka" -#USAGE flag "--full" help="Show all unmuted failures, not just new ones" -#USAGE flag "--show-muted" help="Also show muted failures" -"""Fetch and summarize compliance reports from sifaka. - -Covers: - - Prowler K8s CIS (in-cluster): per-finding detail - - Prowler container image scans: grouped by check + resource - - Prowler IaC manifest scans: grouped by check + resource - - Kingfisher secret scanning: TODO — pending upstream JSON/CSV output - support (currently HTML-only; contribute from spork) - -For each Prowler scan, copies the two most recent CSV reports, parses -them, and displays: - 1. Overall status (pass/fail/manual/muted counts) - 2. Unmuted failures by severity - 3. Delta from the previous report (new vs resolved) - 4. Actionable unmuted failures (per-finding for in-cluster; grouped - by check ID and resource for image/IaC because they have far too - many findings to list individually) - -This is the primary tool for the weekly compliance report review. -""" - -import csv -import subprocess -import tempfile -from collections import Counter -from pathlib import Path -from typing import Annotated - -import typer -from rich.console import Console -from rich.panel import Panel -from rich.table import Table - -PROWLER_SCANS: list[tuple[str, str, bool]] = [ - # (label, sifaka base path, group_findings) - ("K8s CIS (In-Cluster)", "/volume1/reports/prowler", False), - ("Container Images", "/volume1/reports/prowler-images", True), - ("IaC (manifests)", "/volume1/reports/prowler-iac", True), -] - -console = Console() - - -def scp(remote: str, local: str) -> bool: - """Copy a file from sifaka (requires scp -O for Synology).""" - result = subprocess.run( - ["scp", "-O", remote, local], - capture_output=True, - text=True, - timeout=30, - ) - return result.returncode == 0 - - -def list_reports(base: str) -> list[str]: - """List Prowler CSV reports under `base` on sifaka, sorted by timestamp.""" - result = subprocess.run( - ["ssh", "sifaka", f"find {base}/ -name '*.csv' " - "-not -path '*/compliance/*' -not -name '@*'"], - capture_output=True, - text=True, - timeout=15, - ) - if result.returncode != 0: - console.print(f"[bold red]Failed to list reports under {base}[/bold red]") - return [] - - csvs = [p.strip() for p in result.stdout.strip().splitlines() if p.strip()] - # Sort by the timestamp embedded in the filename (e.g. 20260405030007) - import re - - def sort_key(path: str) -> str: - m = re.search(r"(\d{14})", Path(path).name) - return m.group(1) if m else Path(path).name - - return sorted(csvs, key=sort_key) - - -def load_csv(path: str) -> list[dict]: - """Load a Prowler CSV report.""" - with open(path) as f: - return list(csv.DictReader(f, delimiter=";")) - - -def parse_findings(rows: list[dict]) -> dict: - """Categorize findings from a report.""" - statuses = Counter(r["STATUS"] for r in rows) - - fails = [r for r in rows if r["STATUS"] == "FAIL"] - unmuted = [r for r in fails if r.get("MUTED", "") != "True"] - muted = [r for r in fails if r.get("MUTED", "") == "True"] - - return { - "total": len(rows), - "statuses": statuses, - "fails": fails, - "unmuted": unmuted, - "muted": muted, - } - - -def finding_key(r: dict) -> tuple[str, str]: - """Stable identity for a finding (check + resource name, not UID).""" - return (r["CHECK_ID"], r.get("RESOURCE_NAME", "")) - - -SEVERITY_ORDER = ["critical", "high", "medium", "low", "informational"] - - -def severity_sort(r: dict) -> int: - sev = r.get("SEVERITY", "").lower() - return SEVERITY_ORDER.index(sev) if sev in SEVERITY_ORDER else 99 - - -def _ssh_minikube(cmd: str, timeout: int = 15) -> subprocess.CompletedProcess: - """Run a command inside the minikube node via SSH.""" - return subprocess.run( - ["ssh", "indri", f"minikube ssh -- {cmd}"], - capture_output=True, - text=True, - timeout=timeout, - ) - - -def _kubectl(args: str, timeout: int = 15) -> subprocess.CompletedProcess: - """Run a kubectl command against minikube-indri.""" - return subprocess.run( - ["kubectl", "--context=minikube-indri"] + args.split(), - capture_output=True, - text=True, - timeout=timeout, - ) - - -def run_node_verification(console: Console) -> None: - """Verify node-level conditions that Prowler reports as MANUAL. - - Prowler runs inside a pod and can't evaluate kubelet file permissions, - kubelet config arguments, etcd CA separation, or cluster-admin RBAC - bindings. We SSH into the minikube node and check each condition here, - failing loudly if any deviates from expected values. - """ - checks: list[tuple[str, str, bool]] = [] # (name, detail, passed) - - # --- File ownership and permissions --- - file_expectations = [ - ("kubelet.conf ownership", "/etc/kubernetes/kubelet.conf", "root:root", None), - ("kubelet.conf permissions", "/etc/kubernetes/kubelet.conf", None, "600"), - ("config.yaml ownership", "/var/lib/kubelet/config.yaml", "root:root", None), - ("config.yaml permissions", "/var/lib/kubelet/config.yaml", None, "644"), - ("kubelet service ownership", "/etc/systemd/system/kubelet.service.d/10-kubeadm.conf", "root:root", None), - ("kubelet service permissions", "/etc/systemd/system/kubelet.service.d/10-kubeadm.conf", None, "644"), - ] - - for name, path, expected_owner, expected_perms in file_expectations: - if expected_owner: - result = _ssh_minikube(f'"sudo stat -c %U:%G {path}"') - else: - result = _ssh_minikube(f'"sudo stat -c %a {path}"') - - if result.returncode != 0: - checks.append((name, f"could not stat {path}", False)) - else: - actual = result.stdout.strip() - expected = expected_owner or expected_perms - passed = actual == expected - checks.append((name, f"{actual} (expected {expected})", passed)) - - # --- Kubelet config arguments --- - kubelet_result = _ssh_minikube('"sudo cat /var/lib/kubelet/config.yaml"') - if kubelet_result.returncode != 0: - checks.append(("kubelet config", "could not read config.yaml", False)) - else: - import yaml as _yaml - - try: - kubelet_cfg = _yaml.safe_load(kubelet_result.stdout) or {} - except Exception: - kubelet_cfg = {} - checks.append(("kubelet config parse", "failed to parse config.yaml", False)) - - # readOnlyPort: absent or 0 is safe - rop = kubelet_cfg.get("readOnlyPort") - checks.append(( - "readOnlyPort", - f"{rop!r} (absent or 0 is safe)", - rop is None or rop == 0, - )) - - # makeIPTablesUtilChains: absent (defaults true) or true - miu = kubelet_cfg.get("makeIPTablesUtilChains") - checks.append(( - "makeIPTablesUtilChains", - f"{miu!r} (absent or true is safe)", - miu is None or miu is True, - )) - - # eventRecordQPS: absent (defaults 5) or > 0 - erq = kubelet_cfg.get("eventRecordQPS") - checks.append(( - "eventRecordQPS", - f"{erq!r} (absent or > 0 is safe)", - erq is None or (isinstance(erq, (int, float)) and erq > 0), - )) - - # tlsCipherSuites: absent uses Go defaults (acceptable) - tcs = kubelet_cfg.get("tlsCipherSuites") - checks.append(( - "tlsCipherSuites", - "Go defaults" if tcs is None else f"{tcs!r}", - True, # Go defaults are acceptable; explicit suites also fine - )) - - # --- Etcd CA separation --- - etcd_fp = _ssh_minikube( - '"sudo openssl x509 -in /var/lib/minikube/certs/etcd/ca.crt -noout -fingerprint -sha256"' - ) - cluster_fp = _ssh_minikube( - '"sudo openssl x509 -in /var/lib/minikube/certs/ca.crt -noout -fingerprint -sha256"' - ) - if etcd_fp.returncode != 0 or cluster_fp.returncode != 0: - checks.append(("etcd CA separation", "could not read certificates", False)) - else: - etcd_hash = etcd_fp.stdout.strip() - cluster_hash = cluster_fp.stdout.strip() - different = etcd_hash != cluster_hash - checks.append(( - "etcd CA separation", - "different CAs" if different else "SAME CA (unexpected)", - different, - )) - - # --- RBAC cluster-admin bindings --- - expected_bindings = {"cluster-admin", "kubeadm:cluster-admins", "minikube-rbac"} - # Use a jsonpath that emits "name\troleRef" pairs to avoid N+1 queries - # Tab-separated because binding names can contain colons (e.g. kubeadm:cluster-admins) - rb_result = subprocess.run( - [ - "kubectl", "--context=minikube-indri", - "get", "clusterrolebindings", - "-o", "jsonpath={range .items[*]}{.metadata.name}{'\\t'}{.roleRef.name}{'\\n'}{end}", - ], - capture_output=True, - text=True, - timeout=15, - ) - if rb_result.returncode != 0: - checks.append(("cluster-admin bindings", "kubectl failed", False)) - else: - admin_bindings: set[str] = set() - for line in rb_result.stdout.strip().splitlines(): - if "\t" in line: - name, role = line.split("\t", 1) - if role == "cluster-admin": - admin_bindings.add(name) - - unexpected = admin_bindings - expected_bindings - if unexpected: - checks.append(( - "cluster-admin bindings", - f"unexpected: {', '.join(sorted(unexpected))}", - False, - )) - else: - checks.append(( - "cluster-admin bindings", - f"only expected: {', '.join(sorted(admin_bindings))}", - True, - )) - - # --- Display results --- - all_passed = all(passed for _, _, passed in checks) - table = Table( - show_header=True, - header_style="bold", - title="Node Verification (out-of-band checks for MANUAL findings)", - ) - table.add_column("Check") - table.add_column("Detail") - table.add_column("Result", justify="center") - - for name, detail, passed in checks: - status = "[green]PASS[/green]" if passed else "[bold red]FAIL[/bold red]" - table.add_row(name, detail, status) - - console.print(table) - console.print() - - if all_passed: - console.print( - Panel( - "[bold green]All node-level checks passed.[/bold green] " - "Muted MANUAL findings are verified.", - title="Node Verification Verdict", - border_style="green", - ) - ) - else: - failed = [(n, d) for n, d, p in checks if not p] - console.print( - Panel( - f"[bold red]{len(failed)} node-level check(s) FAILED.[/bold red]\n" - "Review the failures above — muted MANUAL findings may no longer " - "be valid.", - title="Node Verification Verdict", - border_style="red", - ) - ) - console.print() - - -SEVERITY_STYLE = { - "critical": "bold red", - "high": "red", - "medium": "yellow", -} - - -def _sev_style(sev: str) -> str: - return SEVERITY_STYLE.get(sev.lower(), "") - - -def summarize_report( - label: str, - base: str, - tmpdir: str, - *, - show_muted: bool = False, - group_findings: bool = False, -) -> None: - """Fetch and summarize the latest Prowler report under `base`. - - When `group_findings` is True, top-N CHECK_ID and RESOURCE_NAME tables - are shown instead of a per-finding detail table — appropriate for - image and IaC scans that produce thousands of findings. - """ - console.rule(f"[bold]{label}[/bold]") - csvs = list_reports(base) - if not csvs: - console.print( - f"[bold yellow]{label}: no Prowler CSV reports found " - f"under {base}[/bold yellow]" - ) - console.print() - return - - safe = "".join(c if c.isalnum() else "_" for c in label.lower()) - latest_remote = csvs[-1] - latest_local = Path(tmpdir) / f"{safe}_latest.csv" - - console.print(f"[dim]Fetching {latest_remote}...[/dim]") - if not scp(f"sifaka:{latest_remote}", str(latest_local)): - console.print(f"[bold red]Failed to copy {latest_remote}[/bold red]") - return - - prev_local: Path | None = None - if len(csvs) >= 2: - prev_remote = csvs[-2] - prev_path = Path(tmpdir) / f"{safe}_prev.csv" - console.print(f"[dim]Fetching {prev_remote}...[/dim]") - if scp(f"sifaka:{prev_remote}", str(prev_path)): - prev_local = prev_path - - latest = parse_findings(load_csv(str(latest_local))) - report_name = Path(latest_remote).stem - console.print() - - # --- Overall status --- - status_table = Table( - show_header=True, header_style="bold", title=f"Report: {report_name}" - ) - status_table.add_column("Status") - status_table.add_column("Count", justify="right") - - for status in ["PASS", "FAIL", "MANUAL"]: - count = latest["statuses"].get(status, 0) - style = "red" if status == "FAIL" and count > 0 else "" - status_table.add_row( - f"[{style}]{status}[/{style}]" if style else status, - f"[{style}]{count}[/{style}]" if style else str(count), - ) - - muted_count = len(latest["muted"]) - unmuted_count = len(latest["unmuted"]) - status_table.add_row("", "") - status_table.add_row("[dim]↳ muted[/dim]", f"[dim]{muted_count}[/dim]") - status_table.add_row( - "[bold]↳ unmuted (action needed)[/bold]", - f"[bold red]{unmuted_count}[/bold red]" - if unmuted_count > 0 - else "[bold green]0[/bold green]", - ) - status_table.add_row("", "") - status_table.add_row("[bold]Total[/bold]", f"[bold]{latest['total']}[/bold]") - - console.print(status_table) - console.print() - - # --- Unmuted failures by severity --- - if latest["unmuted"]: - sev_table = Table( - show_header=True, - header_style="bold", - title="Unmuted Failures by Severity", - ) - sev_table.add_column("Severity") - sev_table.add_column("Count", justify="right") - - for sev, count in sorted( - Counter(r["SEVERITY"] for r in latest["unmuted"]).items(), - key=lambda kv: severity_sort({"SEVERITY": kv[0]}), - ): - style = _sev_style(sev) - sev_table.add_row( - f"[{style}]{sev}[/{style}]" if style else sev, - f"[{style}]{count}[/{style}]" if style else str(count), - ) - - console.print(sev_table) - console.print() - - # --- Delta from previous report --- - if prev_local: - prev = parse_findings(load_csv(str(prev_local))) - - prev_keys = {finding_key(r): r for r in prev["unmuted"]} - curr_keys = {finding_key(r): r for r in latest["unmuted"]} - - new_keys = set(curr_keys.keys()) - set(prev_keys.keys()) - resolved_keys = set(prev_keys.keys()) - set(curr_keys.keys()) - - prev_name = Path(csvs[-2]).stem - delta_lines = [ - f"Compared against: [dim]{prev_name}[/dim]", - "", - f"Previous unmuted FAILs: {len(prev['unmuted'])}", - f"Current unmuted FAILs: {len(latest['unmuted'])}", - f"[green]Resolved: {len(resolved_keys)}[/green]", - f"[red]New: {len(new_keys)}[/red]" - if new_keys - else "[green]New: 0[/green]", - ] - - console.print( - Panel( - "\n".join(delta_lines), - title="[bold]Week-over-Week Delta (unmuted only)[/bold]", - border_style="cyan", - ) - ) - console.print() - - # For grouped scans the new/resolved listings are too noisy - # (potentially thousands of lines). Skip the listings; the count - # is in the panel above and detail is in the grouped tables. - if not group_findings: - if new_keys: - console.print("[bold red]New Unmuted Failures:[/bold red]") - for k in sorted(new_keys): - r = curr_keys[k] - console.print( - f" [{r['SEVERITY']}] {r['CHECK_ID']}: " - f"{r['STATUS_EXTENDED'][:120]}" - ) - console.print() - - if resolved_keys: - console.print("[bold green]Resolved:[/bold green]") - for k in sorted(resolved_keys): - r = prev_keys[k] - console.print( - f" [dim][{r['SEVERITY']}] {r['CHECK_ID']}: " - f"{r['STATUS_EXTENDED'][:120]}[/dim]" - ) - console.print() - - # --- Unmuted failure details (grouped or per-finding) --- - if latest["unmuted"]: - if group_findings: - _print_grouped_findings(latest["unmuted"]) - else: - _print_findings_detail(latest["unmuted"]) - - # --- Muted findings summary --- - if show_muted and latest["muted"]: - muted_table = Table( - show_header=True, - header_style="bold", - title="Muted Failures (for reference)", - ) - muted_table.add_column("Severity") - muted_table.add_column("Check") - muted_table.add_column("Count", justify="right") - - muted_groups: dict[tuple[str, str], int] = Counter() - for r in latest["muted"]: - muted_groups[(r["SEVERITY"], r["CHECK_ID"])] += 1 - - for (sev, check), count in sorted( - muted_groups.items(), - key=lambda x: severity_sort({"SEVERITY": x[0][0]}), - ): - muted_table.add_row( - f"[dim]{sev}[/dim]", - f"[dim]{check}[/dim]", - f"[dim]{count}[/dim]", - ) - - console.print(muted_table) - console.print() - - # --- Verdict --- - if not latest["unmuted"]: - console.print( - Panel( - "[bold green]All clear.[/bold green] No unmuted failures.", - title=f"{label} Verdict", - border_style="green", - ) - ) - else: - console.print( - Panel( - f"[bold yellow]{len(latest['unmuted'])} unmuted failure(s) " - f"need triage.[/bold yellow]\n\n" - "For each: remediate, or add a Resource entry to the " - "matching check in argocd/manifests/prowler/mutelist/.", - title=f"{label} Verdict", - border_style="yellow", - ) - ) - console.print() - - -def _print_findings_detail(unmuted: list[dict]) -> None: - """Per-finding detail table — appropriate when finding count is small.""" - detail_table = Table( - show_header=True, - header_style="bold", - title="Unmuted Failures — Action Needed", - ) - detail_table.add_column("Severity") - detail_table.add_column("Check") - detail_table.add_column("Resource") - detail_table.add_column("Detail", max_width=60) - - for r in sorted(unmuted, key=severity_sort): - sev = r["SEVERITY"] - style = _sev_style(sev) - detail_table.add_row( - f"[{style}]{sev}[/{style}]" if style else sev, - r["CHECK_ID"], - r.get("RESOURCE_NAME", ""), - r["STATUS_EXTENDED"][:60], - ) - - console.print(detail_table) - console.print() - - -def _worst_severity(rows: list[dict]) -> str: - """Return the most severe severity label across `rows`.""" - if not rows: - return "" - return min( - (r["SEVERITY"] for r in rows), - key=lambda s: severity_sort({"SEVERITY": s}), - ) - - -def _print_grouped_findings(unmuted: list[dict], top_n: int = 15) -> None: - """Top-N tables grouped by CHECK_ID and RESOURCE_NAME. - - Used for image and IaC scans where per-finding tables would be too - large to be useful. Shows count and worst severity for each group. - """ - by_check: dict[str, list[dict]] = {} - by_resource: dict[str, list[dict]] = {} - for r in unmuted: - by_check.setdefault(r["CHECK_ID"], []).append(r) - by_resource.setdefault(r.get("RESOURCE_NAME", "") or "(no resource)", []).append(r) - - check_table = Table( - show_header=True, - header_style="bold", - title=f"Top {top_n} Checks by Unmuted Finding Count", - ) - check_table.add_column("Worst Sev") - check_table.add_column("Check ID") - check_table.add_column("Count", justify="right") - - for check, rows in sorted( - by_check.items(), key=lambda kv: -len(kv[1]) - )[:top_n]: - worst = _worst_severity(rows) - style = _sev_style(worst) - check_table.add_row( - f"[{style}]{worst}[/{style}]" if style else worst, - check, - str(len(rows)), - ) - - console.print(check_table) - console.print() - - res_table = Table( - show_header=True, - header_style="bold", - title=f"Top {top_n} Resources by Unmuted Finding Count", - ) - res_table.add_column("Worst Sev") - res_table.add_column("Resource") - res_table.add_column("Count", justify="right") - - for resource, rows in sorted( - by_resource.items(), key=lambda kv: -len(kv[1]) - )[:top_n]: - worst = _worst_severity(rows) - style = _sev_style(worst) - res_table.add_row( - f"[{style}]{worst}[/{style}]" if style else worst, - resource[:80], - str(len(rows)), - ) - - console.print(res_table) - console.print() - - -def main( - full: Annotated[ - bool, typer.Option(help="(reserved) currently a no-op; all unmuted failures already shown") - ] = False, - show_muted: Annotated[ - bool, typer.Option(help="Also show muted failures") - ] = False, -) -> None: - del full # historical flag, kept for backwards compatibility - - with tempfile.TemporaryDirectory() as tmpdir: - for label, base, group in PROWLER_SCANS: - summarize_report( - label, - base, - tmpdir, - show_muted=show_muted, - group_findings=group, - ) - - # --- Node-level MANUAL check verification --- - # These checks verify conditions Prowler reports as MANUAL because it - # runs inside a pod and cannot evaluate them directly. - run_node_verification(console) - - # --- Kingfisher secret scanning --- - # TODO: Kingfisher currently only outputs HTML. Once JSON or CSV output - # is supported upstream (contribute from our spork), add parsing here: - # - # KINGFISHER_BASE = "/volume1/reports/kingfisher" - # - Fetch latest JSON/CSV from sifaka:{KINGFISHER_BASE}/ - # - Parse findings: active vs inactive vs skipped validations - # - Flag any "Active Credential" findings as critical - # - Compare against previous scan for delta - # - Show summary panel similar to Prowler - # - # For now, check that a recent report exists and warn if missing. - kf_check = subprocess.run( - ["ssh", "sifaka", "ls -1t /volume1/reports/kingfisher/ | head -1"], - capture_output=True, - text=True, - timeout=15, - ) - kf_latest = kf_check.stdout.strip() if kf_check.returncode == 0 else "" - if kf_latest and kf_latest.startswith("202"): - console.print( - f"[dim]Kingfisher: latest report directory is {kf_latest} " - f"(HTML only — JSON/CSV pending upstream)[/dim]" - ) - else: - console.print( - "[bold yellow]Warning: No recent Kingfisher report found on " - "sifaka. Check the CronJob on ringtail.[/bold yellow]" - ) - - -if __name__ == "__main__": - typer.run(main) diff --git a/mise-tasks/runner-logs b/mise-tasks/runner-logs index 0d3028b..ec51608 100755 --- a/mise-tasks/runner-logs +++ b/mise-tasks/runner-logs @@ -1,28 +1,23 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["httpx==0.28.1", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["httpx>=0.28.1", "rich>=14.0.0", "typer>=0.24.0"] # /// -#MISE description="List recent Forgejo Actions runs or fetch logs for a specific job" -#USAGE arg "[run_number]" help="Run number to show jobs for (omit to list recent runs)" -#USAGE flag "--job -j " help="Job index (0-based) to fetch logs for" -#USAGE flag "--runner -r " help="Filter listing by runner: indri, ringtail, or all" -#USAGE flag "--repo " help="Forge repo (owner/name), default: detected from git remote" -#USAGE flag "--limit -n " help="Max runs to display (0 for all)" -#USAGE flag "--token " help="Forgejo API token (default: read from 1Password)" -"""List recent Forgejo Actions runs and fetch job logs. +#MISE description="Get logs for a Forgejo Actions workflow run (indri or ringtail runner)" +#USAGE arg "" help="Runner filter: indri, ringtail, or all" +#USAGE arg "[run_id]" help="Run ID to fetch logs for (omit to list recent runs)" +"""Fetch Forgejo Actions workflow logs from indri's log storage. + +Both the indri k8s runner and ringtail nix-container-builder runner report +logs back to the Forgejo server on indri. This tool lists recent runs +(optionally filtered by runner) and fetches compressed logs by run ID. Usage: - mise run runner-logs # list recent runs (default 15) - mise run runner-logs -n 0 # list ALL runs - mise run runner-logs -r ringtail # list recent ringtail runs - mise run runner-logs --repo eblume/hermes # list runs for a different repo - mise run runner-logs 474 # show jobs in run 474 - mise run runner-logs 474 -j 1 # fetch logs for job 1 of run 474 + mise run runner-logs all # list recent runs from all runners + mise run runner-logs ringtail # list recent ringtail runs + mise run runner-logs all 337 # fetch logs for run 337 """ -import os -import re import subprocess import sys from typing import Annotated @@ -32,9 +27,9 @@ import typer from rich.console import Console from rich.table import Table -FORGE_URL = "https://forge.ops.eblu.me" -FORGE_API = f"{FORGE_URL}/api/v1" -OP_TOKEN_REF = "op://vg6xf6vvfmoh5hqjjhlhbeoaie/w3663ffnvkewbftncqxtcpeavy/api-token" +FORGE_API = "https://forge.eblu.me/api/v1" +REPO = "eblume/blumeops" +ACTIONS_LOG_DIR = "/opt/homebrew/var/forgejo/data/actions_log/eblume/blumeops" # Workflows using the ringtail nix-container-builder runner; everything else # runs on the indri k8s runner. @@ -43,287 +38,93 @@ RINGTAIL_WORKFLOWS = {"build-container-nix.yaml"} app = typer.Typer(add_completion=False) -def resolve_token(explicit_token: str | None, console: Console) -> str: - """Resolve Forgejo API token: explicit flag > FORGEJO_TOKEN env > 1Password.""" - if explicit_token: - return explicit_token - env_token = os.environ.get("FORGEJO_TOKEN", "").strip() - if env_token: - return env_token - console.print("[dim]Reading Forgejo API token from 1Password...[/dim]") - result = subprocess.run( - ["op", "read", OP_TOKEN_REF], - capture_output=True, - text=True, - check=True, - ) - return result.stdout.strip() - - -def detect_repo_from_git() -> str | None: - """Sniff owner/repo from the git remote 'origin' URL.""" - try: - result = subprocess.run( - ["git", "remote", "get-url", "origin"], - capture_output=True, - text=True, - check=True, - ) - except (subprocess.CalledProcessError, FileNotFoundError): - return None - url = result.stdout.strip() - # Match SSH (git@host:owner/repo.git) or HTTPS (https://host/owner/repo.git) - m = re.search(r"[/:]([^/]+/[^/]+?)(?:\.git)?$", url) - if not m: - return None - candidate = m.group(1) - # Only use it if the remote points at our forge - if "forge.ops.eblu.me" in url or "forge.eblu.me" in url: - return candidate - return None - - def runner_for_workflow(workflow_id: str) -> str: return "ringtail" if workflow_id in RINGTAIL_WORKFLOWS else "indri" -def auth_headers(token: str) -> dict[str, str]: - return {"Authorization": f"token {token}"} +def list_runs(runner: str, console: Console) -> None: + resp = httpx.get( + f"{FORGE_API}/repos/{REPO}/actions/tasks", + timeout=15, + ) + resp.raise_for_status() + runs = resp.json().get("workflow_runs", []) - -def fetch_tasks(repo: str, token: str) -> list[dict]: - """Fetch all tasks from the Forgejo API, paginating if needed.""" - tasks: list[dict] = [] - page = 1 - while True: - resp = httpx.get( - f"{FORGE_API}/repos/{repo}/actions/tasks", - params={"page": page, "limit": 50}, - headers=auth_headers(token), - timeout=15, - ) - resp.raise_for_status() - batch = resp.json().get("workflow_runs", []) - if not batch: - break - tasks.extend(batch) - page += 1 - return tasks - - -def list_runs(runner: str, repo: str, limit: int, token: str, console: Console) -> None: - """List recent workflow runs, grouped by run number.""" - tasks = fetch_tasks(repo, token) - - # Group tasks by run_number - runs: dict[int, list[dict]] = {} - for t in tasks: - rn = t["run_number"] - runs.setdefault(rn, []).append(t) - - table = Table(title=f"Recent runs — {repo} (filter: {runner})") - table.add_column("Run #", style="cyan", no_wrap=True) + table = Table(title=f"Recent runs (filter: {runner})") + table.add_column("ID", style="cyan", no_wrap=True) table.add_column("Status") table.add_column("Runner") - table.add_column("Jobs") + table.add_column("Name") table.add_column("Title") - table.add_column("Event") - shown = 0 - for rn in sorted(runs, reverse=True): - if limit > 0 and shown >= limit: - break - - jobs = sorted(runs[rn], key=lambda x: x["id"]) - workflow_id = jobs[0].get("workflow_id", "") - host = runner_for_workflow(workflow_id) + for run in runs[:20]: + host = runner_for_workflow(run.get("workflow_id", "")) if runner != "all" and host != runner: continue - - # Aggregate status: worst status wins - statuses = [j.get("status", "") for j in jobs] - if "failure" in statuses: - status, style = "failure", "red" - elif "running" in statuses or "waiting" in statuses: - status, style = "running", "yellow" - elif all(s == "success" for s in statuses): - status, style = "success", "green" - else: - status, style = statuses[0], "yellow" - - job_names = ", ".join(j.get("name", "?")[:30] for j in jobs) - title = (jobs[0].get("display_title") or "")[:40] - event = jobs[0].get("event", "") - - table.add_row( - str(rn), - f"[{style}]{status}[/{style}]", - host, - job_names, - title, - event, - ) - shown += 1 - - console.print(table) - console.print("\n[dim]Use: mise run runner-logs to see jobs in a run[/dim]") - console.print("[dim] mise run runner-logs -j N to fetch logs for job N[/dim]") - - -def show_jobs(run_number: int, repo: str, token: str, console: Console) -> None: - """Show the jobs within a specific run.""" - tasks = fetch_tasks(repo, token) - - jobs = sorted( - [t for t in tasks if t["run_number"] == run_number], - key=lambda x: x["id"], - ) - if not jobs: - typer.echo(f"Error: No jobs found for run #{run_number}", err=True) - raise typer.Exit(1) - - table = Table(title=f"Jobs in run #{run_number} — {repo}") - table.add_column("Job #", style="cyan", no_wrap=True) - table.add_column("Status") - table.add_column("Name") - table.add_column("Created") - - for i, job in enumerate(jobs): - status = job.get("status", "") + status = run.get("status", "") style = "green" if status == "success" else "red" if status == "failure" else "yellow" table.add_row( - str(i), + str(run["id"]), f"[{style}]{status}[/{style}]", - job.get("name", ""), - job.get("created_at", ""), + host, + (run.get("name") or "")[:40], + (run.get("display_title") or "")[:30], ) console.print(table) - console.print(f"\n[dim]Use: mise run runner-logs {run_number} -j N to fetch logs for job N[/dim]") -def fetch_log(run_number: int, job_index: int, repo: str, token: str) -> None: - """Fetch logs for a specific job via SSH to indri. - - Forgejo stores action logs as zstd-compressed files on disk at - ~/forgejo/data/actions_log/{owner}/{repo}/{hex_prefix}/{task_id}.log.zst - regardless of which runner executed the job. The web log endpoint doesn't - support API-token auth for private repos, so we read the files directly. - """ - tasks = fetch_tasks(repo, token) - jobs = sorted( - [t for t in tasks if t["run_number"] == run_number], - key=lambda x: x["id"], - ) - if not jobs: - typer.echo(f"Error: No jobs found for run #{run_number}", err=True) - raise typer.Exit(1) - if job_index < 0 or job_index >= len(jobs): - typer.echo( - f"Error: job index {job_index} out of range (run #{run_number} has {len(jobs)} jobs)", - err=True, - ) - raise typer.Exit(1) - - task_id = jobs[job_index]["id"] - hex_prefix = f"{task_id & 0xff:02x}" - log_path = f"~/forgejo/data/actions_log/{repo}/{hex_prefix}/{task_id}.log.zst" - - # indri's login shell (fish) silently swallows SSH exit codes, so we can't - # rely on returncode. zstdcat itself also exits 0 with a "can't stat ... - # -- ignored" stderr message when the file is missing. Detect missing logs - # by running `test -f` over SSH and parsing the marker line from stdout. - probe = subprocess.run( - ["ssh", "indri", f"test -f {log_path} && echo EXISTS || echo MISSING"], - capture_output=True, - text=True, - ) - marker = probe.stdout.strip().splitlines()[-1] if probe.stdout.strip() else "" - if marker != "EXISTS": - typer.echo( - f"Error: log not found for run #{run_number} job {job_index} (task {task_id})", - err=True, - ) - typer.echo(f"Path: indri:{log_path}", err=True) - typer.echo( - "The runner may have crashed before uploading its log buffer " - "(action_task.log_in_storage = 0).", - err=True, - ) - raise typer.Exit(1) +def fetch_log(run_id: int) -> None: + hex_subdir = f"{run_id:02x}" + log_file = f"{ACTIONS_LOG_DIR}/{hex_subdir}/{run_id}.log.zst" + # All logs live on indri (the Forgejo server) regardless of runner result = subprocess.run( - ["ssh", "indri", f"zstdcat {log_path}"], + ["ssh", "indri", f"test -f '{log_file}' && zstd -d -c '{log_file}'"], capture_output=True, text=True, ) - if result.returncode != 0 or not result.stdout: - typer.echo( - f"Error: could not read log for run #{run_number} job {job_index} (task {task_id})", - err=True, + + if result.returncode == 0: + sys.stdout.write(result.stdout) + else: + typer.echo(f"Error: Log file not found for run {run_id}", err=True) + typer.echo(f"Expected path: {log_file}", err=True) + typer.echo("", err=True) + typer.echo("Available logs:", err=True) + avail = subprocess.run( + [ + "ssh", + "indri", + f"find '{ACTIONS_LOG_DIR}' -name '*.log.zst' -exec basename {{}} .log.zst \\; | sort -n | tail -10", + ], + capture_output=True, + text=True, ) - typer.echo(f"Path: indri:{log_path}", err=True) - if result.stderr.strip(): - typer.echo(result.stderr.strip(), err=True) + typer.echo(avail.stdout, err=True) raise typer.Exit(1) - sys.stdout.write(result.stdout) @app.command() def main( - run_number: Annotated[ - int | None, - typer.Argument(help="Run number to show jobs for (omit to list recent runs)"), - ] = None, - job: Annotated[ - int | None, - typer.Option("--job", "-j", help="Job index (0-based) to fetch logs for"), - ] = None, runner: Annotated[ str, - typer.Option("--runner", "-r", help="Filter listing by runner: indri, ringtail, or all"), - ] = "all", - repo: Annotated[ - str | None, - typer.Option("--repo", help="Forge repo (owner/name), default: detected from git remote"), - ] = None, - limit: Annotated[ - int, - typer.Option("--limit", "-n", help="Max runs to display (0 for all)"), - ] = 15, - token: Annotated[ - str | None, - typer.Option("--token", help="Forgejo API token (default: read from 1Password)"), + typer.Argument(help="Runner filter: indri, ringtail, or all"), + ], + run_id: Annotated[ + int | None, + typer.Argument(help="Run ID to fetch logs for (omit to list recent runs)"), ] = None, ) -> None: - """List recent Forgejo Actions runs or fetch logs for a specific job.""" + """Get logs for a Forgejo Actions workflow run.""" if runner not in ("indri", "ringtail", "all"): typer.echo(f"Error: runner must be 'indri', 'ringtail', or 'all', got '{runner}'") raise typer.Exit(1) - console = Console() - - if repo is None: - repo = detect_repo_from_git() - if repo is None: - typer.echo( - "Error: could not detect repo from git remote; use --repo owner/name", - err=True, - ) - raise typer.Exit(1) - console.print(f"[dim]Detected repo: {repo}[/dim]") - - resolved_token = resolve_token(token, console) - - if run_number is None: - if job is not None: - typer.echo("Error: --job requires a run number", err=True) - raise typer.Exit(1) - list_runs(runner, repo, limit, resolved_token, console) - elif job is None: - show_jobs(run_number, repo, resolved_token, console) + if run_id is None: + list_runs(runner, Console()) else: - fetch_log(run_number, job, repo, resolved_token) + fetch_log(run_id) if __name__ == "__main__": diff --git a/mise-tasks/service-review b/mise-tasks/service-review index f83b104..1bc2ae4 100755 --- a/mise-tasks/service-review +++ b/mise-tasks/service-review @@ -1,11 +1,11 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml==6.0.3", "rich==15.0.0", "typer==0.26.2"] +# dependencies = ["pyyaml>=6.0.2", "rich>=14.0.0", "typer>=0.24.0"] # /// #MISE description="Review the most stale service for version freshness" #USAGE flag "--limit " default="15" help="Number of services to show in the table" -#USAGE flag "--type " help="Filter by service type (argocd, ansible, nixos, fly, mise)" +#USAGE flag "--type " help="Filter by service type (argocd, ansible, nixos)" """Review the most stale service for version freshness. Reads ``docs/reference/services/service-versions.yaml`` and sorts services @@ -21,6 +21,7 @@ After reviewing, update the service entry in the YAML file: Usage: mise run service-review [-- --limit 15] [-- --type argocd] """ +import sys from datetime import date from pathlib import Path from typing import Annotated @@ -165,25 +166,12 @@ def main( ] svc_type = top_svc.get("type", "") - container_dir = Path(__file__).parent.parent / "containers" / top_svc["name"] - has_dockerfile_only = ( - (container_dir / "Dockerfile").exists() - and not (container_dir / "container.py").exists() - ) - if svc_type == "argocd": checklist_parts += [ "\n[bold]ArgoCD Deployment:[/bold]\n", - "• Update image tag in argocd/manifests//kustomization.yaml\n", + "• Update image tag or Helm chart version in argocd/manifests/\n", f"• Verify sync status: argocd app get {top_svc['name']}\n", ] - if has_dockerfile_only: - checklist_parts += [ - "\n[bold yellow]Dagger Migration:[/bold yellow]\n", - "• This container still uses a Dockerfile (no container.py)\n", - "• Consider migrating to a native Dagger build for better error visibility\n", - f"• See containers/{top_svc['name']}/Dockerfile\n", - ] elif svc_type == "ansible": checklist_parts += [ "\n[bold]Ansible Deployment:[/bold]\n", @@ -197,13 +185,6 @@ def main( "• Update: dagger call flake-update --src=. export --path=nixos/ringtail/flake.lock\n", "• Deploy: mise run provision-ringtail\n", ] - elif svc_type == "mise": - checklist_parts += [ - "\n[bold]Mise Tool Update:[/bold]\n", - "• Update pinned version in mise.toml\n", - "• Run: mise install to verify\n", - "• Check for breaking changes in release notes\n", - ] checklist_parts += [ "\n[bold]Health Check:[/bold]\n", diff --git a/mise-tasks/services-check b/mise-tasks/services-check index 1e90f93..2417f74 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -91,7 +91,7 @@ for a in alerts: continue if '$filter_key' and a['labels'].get('$filter_key') != '$filter_value': continue - if a['state'] in ('Alerting', 'Pending') or a['state'].startswith('Alerting'): + if a['state'] in ('Alerting', 'Pending'): url = a.get('annotations', {}).get('runbook_url', '') summary = a.get('annotations', {}).get('summary', '') print(f'{summary}|{url}') @@ -100,17 +100,16 @@ for a in alerts: if [ -z "$firing" ]; then echo -e "${GREEN}OK${NC}" else + local summary runbook + summary=$(echo "$firing" | head -1 | cut -d'|' -f1) + runbook=$(echo "$firing" | head -1 | cut -d'|' -f2) echo -e "${RED}FIRING${NC}" - local runbook_printed=false - while IFS='|' read -r summary runbook; do - if [ -n "$summary" ]; then - echo -e " $summary" - fi - if [ -n "$runbook" ] && [ "$runbook_printed" = false ]; then - echo -e " Runbook: $runbook" - runbook_printed=true - fi - done <<< "$firing" + if [ -n "$summary" ]; then + echo -e " $summary" + fi + if [ -n "$runbook" ]; then + echo -e " Runbook: $runbook" + fi FAILED=1 fi } @@ -121,7 +120,7 @@ echo "" # Local services on indri (not yet covered by alerting) echo "Local services on indri:" -check_service "forgejo" "ssh indri 'launchctl list mcquack.eblume.forgejo | grep -v \"^-\"'" +check_service "forgejo (brew)" "ssh indri 'brew services list | grep forgejo | grep started'" check_service "alloy" "ssh indri 'launchctl list mcquack.eblume.alloy | grep -v \"^-\"'" check_service "borgmatic" "ssh indri 'launchctl list mcquack.eblume.borgmatic | grep -v \"^-\"'" check_service "borgmatic-metrics" "ssh indri 'launchctl list mcquack.borgmatic-metrics | grep -v \"^-\"'" diff --git a/mise-tasks/spork-create b/mise-tasks/spork-create deleted file mode 100755 index 3f18563..0000000 --- a/mise-tasks/spork-create +++ /dev/null @@ -1,453 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.12" -# dependencies = ["httpx==0.28.1", "rich==15.0.0", "typer==0.26.2"] -# /// -#MISE description="Create a spork (floating-branch soft-fork) of a mirrored upstream project" -#USAGE arg "" help="Repository name in the mirrors/ org on forge (e.g. kingfisher)" -#USAGE flag "--description " help="Repository description override" -#USAGE flag "--main-branch " help="Name of the upstream main branch (default: auto-detect)" -#USAGE flag "--dry-run" help="Show what would be done without creating" -#USAGE flag "--no-clone" help="Skip cloning to ~/code/3rd/" -"""Create a spork of a mirrored upstream project. - -A "spork" is a floating-branch soft-fork strategy. It creates a mutable -clone of a read-only mirror in the eblume/ org on Forge, sets up a -'blumeops' branch with a mirror-sync workflow, and optionally clones -locally with the three-remote setup (origin, mirror, upstream). - -Prerequisites: - - Mirror must already exist at mirrors/ on forge - - 1Password CLI authenticated (for Forge API token) - -See docs/explanation/spork-strategy.md for the full strategy. -""" - -import json -import subprocess -import sys -import textwrap -from pathlib import Path -from typing import Annotated, Optional - -import httpx -import typer -from rich.console import Console - -FORGE_URL = "https://forge.eblu.me" -FORGE_API = f"{FORGE_URL}/api/v1" -FORGE_SSH = "ssh://forgejo@forge.ops.eblu.me:2222" -MIRROR_ORG = "mirrors" -OWNER = "eblume" -LOCAL_BASE = Path.home() / "code" / "3rd" -OP_TOKEN_REF = "op://blumeops/w3663ffnvkewbftncqxtcpeavy/api-token" - -console = Console() -app = typer.Typer(add_completion=False) - - -def op_read(ref: str) -> str: - """Read a secret from 1Password.""" - result = subprocess.run( - ["op", "read", ref], capture_output=True, text=True, check=True - ) - return result.stdout.strip() - - -def forge_api( - client: httpx.Client, - method: str, - path: str, - token: str, - json_data: dict | None = None, -) -> httpx.Response: - """Make an authenticated Forge API request.""" - return client.request( - method, - f"{FORGE_API}{path}", - headers={"Authorization": f"token {token}"}, - json=json_data, - ) - - -def get_mirror_info(client: httpx.Client, token: str, name: str) -> dict: - """Get mirror repo info, or exit if it doesn't exist.""" - resp = forge_api(client, "GET", f"/repos/{MIRROR_ORG}/{name}", token) - if resp.status_code == 404: - console.print( - f"[red]Error:[/red] Mirror [bold]{MIRROR_ORG}/{name}[/bold] not found on forge." - ) - console.print("Create it first: [dim]mise run mirror-create [/dim]") - raise SystemExit(1) - resp.raise_for_status() - return resp.json() - - -def detect_main_branch(client: httpx.Client, token: str, name: str) -> str: - """Detect the default branch of the mirror.""" - info = get_mirror_info(client, token, name) - return info.get("default_branch", "main") - - -def repo_exists(client: httpx.Client, token: str, owner: str, name: str) -> bool: - """Check if a repo already exists.""" - resp = forge_api(client, "GET", f"/repos/{owner}/{name}", token) - return resp.status_code == 200 - - -def fork_mirror(client: httpx.Client, token: str, name: str) -> dict: - """Fork the mirror into the eblume org.""" - resp = forge_api( - client, - "POST", - f"/repos/{MIRROR_ORG}/{name}/forks", - token, - json_data={"name": name}, - ) - if resp.status_code == 409: - console.print(f"[yellow]Fork already exists:[/yellow] {OWNER}/{name}") - return forge_api(client, "GET", f"/repos/{OWNER}/{name}", token).json() - resp.raise_for_status() - return resp.json() - - -def mirror_sync_workflow(main_branch: str, repo_name: str) -> str: - """Generate the mirror-sync workflow YAML.""" - return textwrap.dedent(f"""\ - # Mirror Sync — Spork Strategy - # - # Keeps the '{main_branch}' branch tracking upstream (via mirror) and - # rebases the 'blumeops' branch on top. See docs/explanation/spork-strategy.md - # in the blumeops repo for the full strategy. - # - # On conflict: the workflow fails. Manual rebase resolution required. - - name: Mirror Sync - - on: - schedule: - - cron: '0 5 * * *' # Daily at 05:00 UTC - workflow_dispatch: - - jobs: - sync: - runs-on: k8s - steps: - - name: Checkout blumeops branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: blumeops - fetch-depth: 0 - - - name: Configure git - run: | - git config user.name "Forgejo Actions" - git config user.email "actions@forge.eblu.me" - - - name: Add mirror remote - run: | - git remote add mirror "${{{{ env.MIRROR_URL }}}}" || true - git fetch mirror - env: - MIRROR_URL: {FORGE_URL}/{MIRROR_ORG}/{repo_name}.git - - - name: Fast-forward {main_branch} from mirror - run: | - git checkout -B {main_branch} origin/{main_branch} - git merge --ff-only mirror/{main_branch} - git push origin {main_branch} - - - name: Rebase blumeops onto {main_branch} - run: | - git checkout blumeops - git rebase {main_branch} - git push --force-with-lease origin blumeops - - - name: Rebase feature branches - run: | - # Rebase feature/local/* onto blumeops - for branch in $(git branch -r --list 'origin/feature/local/*'); do - local_name="${{branch#origin/}}" - echo "Rebasing $local_name onto blumeops..." - git checkout -B "$local_name" "$branch" - git rebase blumeops || {{ - echo "::error::Rebase conflict on $local_name" - git rebase --abort - continue - }} - git push --force-with-lease origin "$local_name" - done - - # Rebase feature/upstream/* onto {main_branch} - for branch in $(git branch -r --list 'origin/feature/upstream/*'); do - local_name="${{branch#origin/}}" - echo "Rebasing $local_name onto {main_branch}..." - git checkout -B "$local_name" "$branch" - git rebase {main_branch} || {{ - echo "::error::Rebase conflict on $local_name" - git rebase --abort - continue - }} - git push --force-with-lease origin "$local_name" - done - - - name: Build deploy branch - run: | - git checkout -B deploy blumeops - - # Merge all feature branches into deploy - for branch in $(git branch -r --list 'origin/feature/local/*' 'origin/feature/upstream/*'); do - local_name="${{branch#origin/}}" - echo "Merging $local_name into deploy..." - git merge --no-ff "$local_name" -m "deploy: merge $local_name" || {{ - echo "::error::Merge conflict on $local_name into deploy" - git merge --abort - continue - }} - done - - git push --force-with-lease origin deploy - """) - - -def set_default_branch( - client: httpx.Client, token: str, owner: str, name: str, branch: str -) -> None: - """Set the default branch of a repo.""" - resp = forge_api( - client, - "PATCH", - f"/repos/{owner}/{name}", - token, - json_data={"default_branch": branch}, - ) - resp.raise_for_status() - - -def get_upstream_url(client: httpx.Client, token: str, name: str) -> str | None: - """Try to find the upstream URL from the mirror's config.""" - info = get_mirror_info(client, token, name) - return info.get("original_url") or info.get("clone_url") - - -@app.command() -def main( - repo_name: Annotated[str, typer.Argument(help="Repository name in mirrors/ org")], - description: Annotated[Optional[str], typer.Option(help="Description override")] = None, - main_branch: Annotated[Optional[str], typer.Option("--main-branch", help="Upstream main branch name")] = None, - dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview without creating")] = False, - no_clone: Annotated[bool, typer.Option("--no-clone", help="Skip local clone")] = False, -) -> None: - """Create a spork of a mirrored upstream project.""" - console.print(f"[bold]Sporking[/bold] {MIRROR_ORG}/{repo_name}") - console.print() - - # --- Preflight checks (fail fast before any mutations) --- - local_path = LOCAL_BASE / repo_name - if not no_clone and local_path.exists(): - console.print( - f"[red]Error:[/red] Local directory already exists: [bold]{local_path}[/bold]" - ) - console.print( - "Remove it first or use [dim]--no-clone[/dim] to skip local setup." - ) - raise SystemExit(1) - - token = op_read(OP_TOKEN_REF) - - with httpx.Client(timeout=30) as client: - # Verify mirror exists - mirror_info = get_mirror_info(client, token, repo_name) - detected_main = main_branch or mirror_info.get("default_branch", "main") - upstream_url = mirror_info.get("original_url", "unknown") - desc = description or mirror_info.get("description", "") - - # Verify fork doesn't already exist - if repo_exists(client, token, OWNER, repo_name): - console.print( - f"[red]Error:[/red] Fork already exists: [bold]{OWNER}/{repo_name}[/bold]" - ) - console.print( - "If re-sporking, delete the fork on forge first." - ) - raise SystemExit(1) - - # Verify upstream doesn't have branches that conflict with spork names - for reserved in ("blumeops", "deploy"): - resp = forge_api( - client, - "GET", - f"/repos/{MIRROR_ORG}/{repo_name}/branches/{reserved}", - token, - ) - if resp.status_code == 200: - console.print( - f"[red]Error:[/red] Upstream already has a branch named " - f"[bold]{reserved}[/bold] — this conflicts with the spork strategy." - ) - raise SystemExit(1) - - console.print(f" Mirror: {MIRROR_ORG}/{repo_name}") - console.print(f" Upstream: {upstream_url}") - console.print(f" Main branch: {detected_main}") - console.print(f" Description: {desc}") - console.print(f" Fork target: {OWNER}/{repo_name}") - if not no_clone: - console.print(f" Local clone: {local_path}") - console.print() - - if dry_run: - console.print("[dim][dry-run] Would perform the following:[/dim]") - console.print(f" 1. Fork {MIRROR_ORG}/{repo_name} → {OWNER}/{repo_name}") - console.print(f" 2. Create 'blumeops' branch from '{detected_main}'") - console.print(" 3. Add .forgejo/workflows/mirror-sync.yaml") - console.print(" 4. Set 'blumeops' as default branch") - if not no_clone: - console.print(f" 5. Clone to {local_path} with 3 remotes") - return - - # --- All checks passed, start mutating --- - console.print("Forking mirror...") - fork_mirror(client, token, repo_name) - console.print(f"[green]Created fork:[/green] {OWNER}/{repo_name}") - - # Enable Actions (forks from mirrors have it disabled by default) - console.print("Enabling Actions...") - resp = forge_api( - client, - "PATCH", - f"/repos/{OWNER}/{repo_name}", - token, - json_data={"has_actions": True}, - ) - resp.raise_for_status() - console.print("[green]Actions enabled[/green]") - - # 3. Clone to temp dir, create blumeops branch with workflow - console.print("Setting up blumeops branch...") - import tempfile - - with tempfile.TemporaryDirectory() as tmpdir: - clone_url = f"{FORGE_SSH}/{OWNER}/{repo_name}.git" - subprocess.run( - ["git", "clone", clone_url, tmpdir], - check=True, - capture_output=True, - ) - - # Create blumeops branch from detected main - subprocess.run( - ["git", "checkout", "-b", "blumeops", f"origin/{detected_main}"], - cwd=tmpdir, - check=True, - capture_output=True, - ) - - # Remove any upstream .forgejo/ directory (rare, but possible) - existing_forgejo = Path(tmpdir) / ".forgejo" - if existing_forgejo.exists(): - import shutil - shutil.rmtree(existing_forgejo) - console.print("[yellow]Removed upstream .forgejo/ directory[/yellow]") - - # Add mirror-sync workflow - workflow_dir = Path(tmpdir) / ".forgejo" / "workflows" - workflow_dir.mkdir(parents=True, exist_ok=True) - workflow_path = workflow_dir / "mirror-sync.yaml" - workflow_path.write_text(mirror_sync_workflow(detected_main, repo_name)) - - # Commit and push - subprocess.run( - ["git", "add", ".forgejo/"], - cwd=tmpdir, - check=True, - capture_output=True, - ) - subprocess.run( - ["git", "config", "user.name", "Erich Blume"], - cwd=tmpdir, - check=True, - capture_output=True, - ) - subprocess.run( - ["git", "config", "user.email", "blume.erich@gmail.com"], - cwd=tmpdir, - check=True, - capture_output=True, - ) - subprocess.run( - ["git", "commit", "-m", "spork: add mirror-sync workflow\n\nBootstrap the blumeops branch with the spork mirror-sync\nworkflow. See blumeops docs/explanation/spork-strategy.md."], - cwd=tmpdir, - check=True, - capture_output=True, - ) - subprocess.run( - ["git", "push", "-u", "origin", "blumeops"], - cwd=tmpdir, - check=True, - capture_output=True, - ) - console.print("[green]Created and pushed blumeops branch[/green]") - - # 4. Set default branch to blumeops - console.print("Setting default branch to blumeops...") - set_default_branch(client, token, OWNER, repo_name, "blumeops") - console.print("[green]Default branch set to blumeops[/green]") - - # 5. Local clone with three remotes - if no_clone: - console.print("[dim]Skipping local clone (--no-clone)[/dim]") - else: - console.print(f"Cloning to {local_path}...") - clone_url = f"{FORGE_SSH}/{OWNER}/{repo_name}.git" - subprocess.run( - ["git", "clone", clone_url, str(local_path)], - check=True, - capture_output=True, - ) - # Add mirror and upstream remotes - mirror_url = f"{FORGE_SSH}/{MIRROR_ORG}/{repo_name}.git" - subprocess.run( - ["git", "remote", "add", "mirror", mirror_url], - cwd=local_path, - check=True, - capture_output=True, - ) - if upstream_url and upstream_url != "unknown": - subprocess.run( - ["git", "remote", "add", "upstream", upstream_url], - cwd=local_path, - check=True, - capture_output=True, - ) - subprocess.run( - ["git", "fetch", "--all"], - cwd=local_path, - check=True, - capture_output=True, - ) - console.print(f"[green]Local clone ready at {local_path}[/green]") - - # Summary - console.print() - console.print("[bold green]Spork complete![/bold green]") - console.print() - console.print(" Remotes:") - console.print(f" origin → {FORGE_URL}/{OWNER}/{repo_name}") - console.print(f" mirror → {FORGE_URL}/{MIRROR_ORG}/{repo_name}") - console.print(f" upstream → {upstream_url}") - console.print() - console.print(" Branches:") - console.print(f" {detected_main:<12} — clean upstream tracking (never commit here)") - console.print(" blumeops — local infra + workflows (default branch)") - console.print() - console.print(" Next steps:") - console.print(" • Create feature branches:") - console.print(f" git checkout -b feature/upstream/my-change {detected_main}") - console.print(" git checkout -b feature/local/my-change blumeops") - console.print(" • Mirror-sync runs daily at 05:00 UTC") - console.print(" • See: docs/explanation/spork-strategy.md") - - -if __name__ == "__main__": - app() diff --git a/mise.toml b/mise.toml index 286c4e0..5bb2829 100644 --- a/mise.toml +++ b/mise.toml @@ -1,12 +1,5 @@ [tools] -# Versions set here are referenced against service-versions.yaml -# If you add a new tool, please: -# 1. pin a specific version (or even better, a SHA) -# 2. create a new entry in service-versions.yaml -# This will help ensure reviewed upgrades at a steady cadence -"pipx:ansible-core" = { version = "2.20.1", uvx = "true", uvx_args = "--with botocore --with boto3" } -"pipx:borgmatic" = "2.1.4" -prek = "0.3.4" -pulumi = "3.215.0" -dagger = "0.20.6" -"pipx:ty" = "0.0.29" +"pipx:ansible-core" = { version = "latest", uvx = "true", uvx_args = "--with botocore --with boto3" } +prek = "latest" +pulumi = "latest" +dagger = "0.20.1" diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index bc893d5..7d948a2 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -16,26 +16,8 @@ in systemd.tpm2.enable = false; # Networking - # Wired interface (enp5s0) uses a static IP configured by NixOS scripted - # networking; NetworkManager is left enabled for the wireless fallback only. networking.hostName = "ringtail"; - networking.networkmanager = { - enable = true; - unmanaged = [ "interface-name:enp5s0" ]; - }; - networking.useDHCP = false; - networking.interfaces.enp5s0.ipv4.addresses = [{ - address = "192.168.1.21"; - prefixLength = 24; - }]; - networking.defaultGateway = "192.168.1.1"; - networking.nameservers = [ "192.168.1.1" "1.1.1.1" ]; - - # K3s pod networking and Tailscale tunnel routing require IP forwarding. - # NixOS leaves this off by default; previously it was being enabled - # implicitly by NM/scripted-DHCP setup, but with static networking we - # have to set it explicitly. - boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + networking.networkmanager.enable = true; # Time zone time.timeZone = "America/Los_Angeles"; @@ -124,19 +106,6 @@ in # Fish shell programs.fish.enable = true; - # Firefox with 1Password extension - programs.firefox = { - enable = true; - policies = { - ExtensionSettings = { - "{d634138d-c276-4fc8-924b-40a0ea21d284}" = { - install_url = "https://addons.mozilla.org/firefox/downloads/latest/1password-x-password-manager/latest.xpi"; - installation_mode = "force_installed"; - }; - }; - }; - }; - # 1Password (modules handle CLI group/setgid and polkit for GUI integration) programs._1password.enable = true; programs._1password-gui = { @@ -144,6 +113,12 @@ in polkitPolicyOwners = [ "eblume" ]; }; + # Steam + programs.steam = { + enable = true; + dedicatedServer.openFirewall = true; + }; + # K3s single-node cluster services.k3s = { enable = true; @@ -171,15 +146,6 @@ in ''; }; - # Raise memlock rlimit for k3s so eBPF workloads (Beyla/Alloy tracing) can - # call setrlimit(RLIMIT_MEMLOCK, unlimited) inside privileged containers. - systemd.services.k3s.serviceConfig.LimitMEMLOCK = "infinity"; - - # Allow BPF in privileged containers (Beyla eBPF tracing). NixOS defaults - # to 2 (block BPF outside init namespace even with CAP_BPF). Value 1 allows - # BPF for processes with CAP_BPF/CAP_SYS_ADMIN in any namespace. - boot.kernel.sysctl."kernel.unprivileged_bpf_disabled" = 1; - # K3s containerd registry mirrors (pull through Zot on indri) environment.etc."rancher/k3s/registries.yaml".source = ./k3s-registries.yaml; @@ -266,15 +232,6 @@ in home-manager.users.eblume = { home.stateVersion = "25.11"; - xdg.mimeApps = { - enable = true; - defaultApplications = { - "x-scheme-handler/http" = [ "firefox.desktop" ]; - "x-scheme-handler/https" = [ "firefox.desktop" ]; - "text/html" = [ "firefox.desktop" ]; - }; - }; - wayland.windowManager.sway = { enable = true; checkConfig = false; @@ -296,7 +253,6 @@ in commands = [ { command = "inhibit_idle fullscreen"; criteria = { class = ".*"; }; } { command = "inhibit_idle fullscreen"; criteria = { app_id = ".*"; }; } - { command = "fullscreen enable"; criteria = { class = "steam_app_1174180"; }; } ]; }; colors = { @@ -337,25 +293,17 @@ in output = { "DP-1" = { mode = "2560x1440@165Hz"; - # VRR off: the OMEN 27i IPS pumps gamma/brightness when the panel - # refresh swings into its low VRR range (e.g. low-fps game - # cutscenes), producing a ~20Hz flicker that compounds over a long - # session until a reboot. Fixed refresh at 165Hz eliminates it. - # If you want VRR back, cap in-game fps so refresh never dips low. - adaptive_sync = "off"; + adaptive_sync = "on"; bg = "~/.config/sway/wallpaper.jpg fill"; }; }; - # Extend (not replace) the home-manager default sway keybindings. - # lib.mkForce is needed on keys whose defaults we want to override - # (same priority otherwise conflicts). Audio keys and Mod+d (wmenu-run - # vs the default menu binding) don't collide with defaults. - keybindings = let mod = "Mod4"; in lib.mkOptionDefault { - "${mod}+Return" = lib.mkForce "exec wezterm"; - "${mod}+d" = lib.mkForce "exec wmenu-run"; - "${mod}+space" = lib.mkForce "exec fuzzel"; - "${mod}+l" = lib.mkForce "exec swaylock -f"; - "${mod}+F1" = "exec grep '^bindsym' ~/.config/sway/config | fuzzel --dmenu"; + keybindings = let mod = "Mod4"; in { + "${mod}+Return" = "exec wezterm"; + "${mod}+Shift+q" = "kill"; + "${mod}+d" = "exec wmenu-run"; + "${mod}+space" = "exec fuzzel"; + "${mod}+Shift+c" = "reload"; + "${mod}+l" = "exec swaylock -f"; "--locked XF86AudioMute" = "exec pactl set-sink-mute @DEFAULT_SINK@ toggle"; "--locked XF86AudioLowerVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ -5%"; "--locked XF86AudioRaiseVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ +5%"; @@ -427,10 +375,8 @@ in width = 40; horizontal-pad = 16; vertical-pad = 8; - }; - border = { - radius = 8; - width = 2; + border-radius = 8; + border-width = 2; }; colors = { background = "24273add"; @@ -614,22 +560,6 @@ in AllowSuspendThenHibernate=no ''; - # Cap systemd-coredump. Wine/Proton games (Diablo IV, etc.) segfault - # regularly and dump multi-GB cores; with the stock (effectively unbounded) - # limits, systemd-coredump then spends minutes streaming and compressing the - # dump to disk — e.g. a single D4 crash produced a 4.6G core, read 13.7G and - # wrote 17.4G, pinning the CPU and locking up the desktop for ~3.5 minutes. - # Those cores are useless anyway: Nix .so files carry no build-id, so no - # backtrace can be generated. Capping uncompressed size at 1G makes oversized - # cores get logged-but-skipped (the kernel stops dumping once we stop reading) - # while real service cores (well under 1G) are still captured. MaxUse bounds - # the on-disk store so frequent game crashes can't accumulate (was at 8.6G). - systemd.coredump.extraConfig = '' - ProcessSizeMax=1G - ExternalSizeMax=1G - MaxUse=2G - ''; - # NixOS release system.stateVersion = "25.11"; } diff --git a/nixos/ringtail/flake.lock b/nixos/ringtail/flake.lock index bb60501..9195dbd 100644 --- a/nixos/ringtail/flake.lock +++ b/nixos/ringtail/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1780290312, - "narHash": "sha256-eTAlX0CwgB84Ts3GaBd944A3DRXVMzgA0EqroZBISUo=", + "lastModified": 1773889306, + "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", "owner": "nix-community", "repo": "disko", - "rev": "115e5211780054d8a890b41f0b7734cafad54dfe", + "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", "type": "github" }, "original": { @@ -27,11 +27,11 @@ ] }, "locked": { - "lastModified": 1779506708, - "narHash": "sha256-QOD/CNm196nCJRheux/URi4/HE66fthdOMqCJoPP1Y0=", + "lastModified": 1774559029, + "narHash": "sha256-deix7yg3j6AhjMPnFDCmWB3f83LsajaaULP5HH2j34k=", "owner": "nix-community", "repo": "home-manager", - "rev": "3ee51fbdac8c8bdfe1e7e1fcaba6520a563f394f", + "rev": "a0bb0d11514f92b639514220114ac8063c72d0a3", "type": "github" }, "original": { @@ -42,22 +42,6 @@ } }, "nixpkgs": { - "locked": { - "lastModified": 1779796641, - "narHash": "sha256-ZsIrKmhp4vbBXoXXmR/tBXA/UCsAQiJL9vsgZEduhVY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "25f538306313eae3927264466c70d7001dcea1df", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-services": { "locked": { "lastModified": 1774388614, "narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=", @@ -68,8 +52,8 @@ }, "original": { "owner": "NixOS", + "ref": "nixos-25.11", "repo": "nixpkgs", - "rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e", "type": "github" } }, @@ -77,8 +61,7 @@ "inputs": { "disko": "disko", "home-manager": "home-manager", - "nixpkgs": "nixpkgs", - "nixpkgs-services": "nixpkgs-services" + "nixpkgs": "nixpkgs" } } }, diff --git a/nixos/ringtail/flake.nix b/nixos/ringtail/flake.nix index 541bafa..70a1d73 100644 --- a/nixos/ringtail/flake.nix +++ b/nixos/ringtail/flake.nix @@ -3,12 +3,6 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; - - # Pinned nixpkgs for versioned services (forgejo-runner, snowflake, k3s). - # Update this deliberately during service reviews, not via `nix flake update`. - # Current versions: forgejo-runner 12.7.2, snowflake 2.11.0, k3s 1.34.5+k3s1 - nixpkgs-services.url = "github:NixOS/nixpkgs/1073dad219cb244572b74da2b20c7fe39cb3fa9e"; - disko = { url = "github:nix-community/disko"; inputs.nixpkgs.follows = "nixpkgs"; @@ -19,7 +13,7 @@ }; }; - outputs = { nixpkgs, nixpkgs-services, disko, home-manager, ... }: { + outputs = { nixpkgs, disko, home-manager, ... }: { nixosConfigurations.ringtail = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ @@ -28,19 +22,6 @@ ./disk-config.nix ./hardware-configuration.nix ./configuration.nix - ./gaming.nix - # Pin versioned services to nixpkgs-services instead of the rolling nixpkgs. - # This prevents `nix flake update nixpkgs` from silently upgrading them. - # Bump nixpkgs-services explicitly during service reviews. - ({ ... }: { - nixpkgs.overlays = [ - (final: prev: let svcPkgs = nixpkgs-services.legacyPackages.x86_64-linux; in { - forgejo-runner = svcPkgs.forgejo-runner; - snowflake = svcPkgs.snowflake; - k3s = svcPkgs.k3s; - }) - ]; - }) ]; }; }; diff --git a/nixos/ringtail/gaming.nix b/nixos/ringtail/gaming.nix deleted file mode 100644 index 7c00378..0000000 --- a/nixos/ringtail/gaming.nix +++ /dev/null @@ -1,39 +0,0 @@ -{ pkgs, ... }: - -{ - # Steam - programs.steam = { - enable = true; - dedicatedServer.openFirewall = true; - extraCompatPackages = [ pkgs.proton-ge-bin ]; - }; - - # Proton Experimental ships an accessibility bridge (xalia) that hangs during - # game launch when AT-SPI is not running on the host. This host has no AT-SPI, - # so disable xalia globally to avoid wedging iscriptevaluator.exe. - environment.sessionVariables.PROTON_USE_XALIA = "0"; - - # Subnautica 2 pre-launch wrapper. SN2 (UE5) writes Saved/running.dat as a - # "currently running" lockfile. If the prior session exited uncleanly (SIGKILL - # via Steam's Stop button, crash, etc.), the file persists and on next launch - # SN2 pops up an invisible (0x0-sized) Error dialog ("Your game might not have - # exited correctly last time...") that the GameThread blocks on forever — - # observable only as a black screen with a spinning loader. This wrapper - # removes the stale lockfiles before exec'ing the actual game command. - # Use as Steam launch option for Subnautica 2: - # sn2-prelaunch %command% - environment.systemPackages = [ - (pkgs.writeShellScriptBin "sn2-prelaunch" '' - saved="/mnt/games/SteamLibrary/steamapps/compatdata/1962700/pfx/drive_c/users/steamuser/AppData/Local/Subnautica2/Saved" - rm -f "$saved/running.dat" "$saved/beforelobby.dat" - exec "$@" - '') - ]; - - # Gamescope — micro-compositor for game fullscreen/resolution management. - # Use as Steam launch option: gamescope -W 2560 -H 1440 -f -- %command% - programs.gamescope = { - enable = true; - capSysNice = true; # Allow gamescope to set realtime scheduling - }; -} diff --git a/prek.toml b/prek.toml index 2c66b82..b679a6f 100644 --- a/prek.toml +++ b/prek.toml @@ -22,13 +22,13 @@ hooks = [ # check-yaml with --unsafe (builtin fast path doesn't support --unsafe yet) [[repos]] repo = "https://github.com/pre-commit/pre-commit-hooks" -rev = "3e8a8703264a2f4a69428a0aa4dcb512790b2c8c" # v6.0.0 +rev = "v6.0.0" hooks = [{ id = "check-yaml", args = ["--unsafe"] }] -# Secret detection (running both tools in parallel to compare coverage) +# Secret detection [[repos]] repo = "https://github.com/trufflesecurity/trufflehog" -rev = "37b77001d0174ebec2fcca2bd83ff83a6d45a3ab" # v3.95.3 +rev = "v3.94.0" hooks = [ { id = "trufflehog", entry = "trufflehog git file://. --since-commit HEAD --no-verification --fail", stages = [ "pre-commit", @@ -36,27 +36,10 @@ hooks = [ ] }, ] -[[repos]] -repo = "https://github.com/mongodb/kingfisher" -rev = "6f560103cc6ea082ef4b80a9098e3f3111afb8bc" # v1.101.0 -hooks = [ - { id = "kingfisher", args = [ - "scan", - ".", - "--staged", - "--quiet", - "--no-update-check", - "--no-validate", - ], stages = [ - "pre-commit", - "pre-push", - ] }, -] - # YAML linting [[repos]] repo = "https://github.com/adrienverge/yamllint" -rev = "cba56bcde1fdd01c1deb3f945e69764c291a6530" # v1.38.0 +rev = "v1.38.0" hooks = [{ id = "yamllint", args = ["-c", ".yamllint.yaml"] }] # Ansible linting @@ -69,53 +52,41 @@ name = "ansible-lint" entry = "env ANSIBLE_ROLES_PATH=ansible/roles ansible-lint" language = "python" files = "^ansible/" -additional_dependencies = ["ansible-lint==26.4.0", "ansible-core==2.21.0"] +additional_dependencies = ["ansible-lint>=26.3.0", "ansible-core>=2.18"] # Python - ruff for linting and formatting [[repos]] repo = "https://github.com/astral-sh/ruff-pre-commit" -rev = "0c7b6c989466a93942def1f84baf36ddfcd60c83" # v0.15.14 +rev = "v0.15.7" hooks = [{ id = "ruff", args = ["--fix"] }, { id = "ruff-format" }] -# Python - ty type checker -[[repos]] -repo = "local" - -[[repos.hooks]] -id = "ty-check" -name = "ty type check" -entry = "ty check" -language = "system" -types = ["python"] -pass_filenames = false - # Shell scripts - shellcheck and shfmt [[repos]] repo = "https://github.com/shellcheck-py/shellcheck-py" -rev = "745eface02aef23e168a8afb6b5737818efbea95" # v0.11.0.1 +rev = "v0.11.0.1" hooks = [{ id = "shellcheck", args = ["--severity=warning"] }] [[repos]] repo = "https://github.com/scop/pre-commit-shfmt" -rev = "05c1426671b9237fb5e1444dd63aa5731bec0dfb" # v3.13.1-1 +rev = "v3.13.0-1" hooks = [{ id = "shfmt", args = ["-i", "2", "-ci", "-bn"] }] # TOML - taplo [[repos]] repo = "https://github.com/ComPWA/taplo-pre-commit" -rev = "23eab0f0eedcbedebff420f5fdfb284744adc7b3" # v0.9.3 -hooks = [{ id = "taplo-format" }, { id = "taplo-lint", args = ["--no-schema"] }] +rev = "v0.9.3" +hooks = [{ id = "taplo-format" }, { id = "taplo-lint" }] # JSON formatting (prettier for consistent style) [[repos]] repo = "https://github.com/rbubley/mirrors-prettier" -rev = "515f543f5718ebfd6ce22e16708bb32c68ff96e1" # v3.8.3 +rev = "v3.8.1" hooks = [{ id = "prettier", types_or = ["json"], args = ["--tab-width", "2"] }] # GitHub/Forgejo Actions workflow linting [[repos]] repo = "https://github.com/rhysd/actionlint" -rev = "914e7df21a07ef503a81201c76d2b11c789d3fca" # v1.7.12 +rev = "v1.7.11" hooks = [ { id = "actionlint-system", args = [ "-config-file", diff --git a/pulumi/gandi/README.md b/pulumi/gandi/README.md index 70d2821..9d7b7aa 100644 --- a/pulumi/gandi/README.md +++ b/pulumi/gandi/README.md @@ -27,19 +27,50 @@ pulumi stack select eblu-me # or: pulumi stack init eblu-me ## Authentication -This project uses a Gandi Personal Access Token (PAT) shared with Caddy. See the [Gandi reference card](../../docs/reference/infrastructure/gandi.md) and [Rotate the Gandi PAT](../../docs/how-to/configuration/rotate-gandi-pat.md). +This project requires a Gandi Personal Access Token (PAT) with LiveDNS permissions. -The mise tasks handle fetching the PAT from 1Password: +**The PAT expires every 30 days and must be cycled manually.** + +### Cycling the PAT + +1. Go to [Gandi PAT Management](https://admin.gandi.net/organizations/1db8d76a-f729-11ed-b8d1-00163e94b645/account/pat) + +2. Create a new PAT: + - Name: `blumeops-pulumi` (or similar) + - Expiration: 30 days (maximum is 90; shorter is fine if used rarely) + - Permissions required: + - **Manage domain name technical configurations** (required for DNS records) + - See and renew domain names + - Optional permissions (enabled but not strictly required): + - See & download SSL certificates + - Manage Cloud resources + - See Cloud resources + - View Organization + - Deploy Web Hosting instances + - Manage Web Hosting instances + - See and renew Web Hosting instances + +3. Update 1Password: + ```bash + # Update the existing item with the new PAT value + op item edit mco6ka3dc3rmw7zkg2dhia5d2m pat="" --vault vg6xf6vvfmoh5hqjjhlhbeoaie + ``` + +4. Delete the old PAT from Gandi admin console + +### Running with Authentication + +The mise task handles fetching the PAT from 1Password: ```bash +mise run dns-up # Preview and apply changes mise run dns-preview # Preview only -mise run dns-up # Preview and apply ``` Or manually: ```bash -export GANDI_PERSONAL_ACCESS_TOKEN=$(op read "op://blumeops/gandi - blumeops/pat") +export GANDI_PERSONAL_ACCESS_TOKEN=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/mco6ka3dc3rmw7zkg2dhia5d2m/pat") pulumi up ``` diff --git a/pulumi/gandi/__main__.py b/pulumi/gandi/__main__.py index 25fd0f7..e448ed2 100644 --- a/pulumi/gandi/__main__.py +++ b/pulumi/gandi/__main__.py @@ -8,7 +8,7 @@ This program manages DNS records for blumeops infrastructure: Authentication: Set GANDI_PERSONAL_ACCESS_TOKEN environment variable. - See docs/how-to/configuration/rotate-gandi-pat.md for PAT management. + See docs/how-to/gandi-operations.md for PAT management instructions. """ import os @@ -85,15 +85,6 @@ forge_public = gandi.livedns.Record( values=["blumeops-proxy.fly.dev."], ) -shower_public = gandi.livedns.Record( - "shower-public", - zone=domain, - name="shower", - type="CNAME", - ttl=300, - values=["blumeops-proxy.fly.dev."], -) - # ============== Exports ============== pulumi.export("domain", domain) pulumi.export("wildcard_fqdn", f"*.{subdomain}.{domain}") @@ -102,4 +93,3 @@ pulumi.export("target_ip", tailscale_ip) pulumi.export("docs_public_fqdn", f"docs.{domain}") pulumi.export("cv_public_fqdn", f"cv.{domain}") pulumi.export("forge_public_fqdn", f"forge.{domain}") -pulumi.export("shower_public_fqdn", f"shower.{domain}") diff --git a/pulumi/tailscale/__main__.py b/pulumi/tailscale/__main__.py index 3acbb62..2bbecfd 100644 --- a/pulumi/tailscale/__main__.py +++ b/pulumi/tailscale/__main__.py @@ -37,7 +37,7 @@ acl = tailscale.Acl( # indri - Mac Mini M1, primary homelab server # Hosts forge, loki, zot registry, and the k8s control plane. -# Other services (grafana, kiwix, etc.) run in k8s with their own Tailscale devices. +# Other services (grafana, kiwix, devpi, etc.) run in k8s with their own Tailscale devices. indri = tailscale.get_device(name="indri.tail8d86e.ts.net") indri_tags = tailscale.DeviceTags( "indri-tags", @@ -50,7 +50,6 @@ indri_tags = tailscale.DeviceTags( "tag:loki", "tag:registry", # Zot container registry "tag:k8s-api", # Kubernetes API server (minikube) - "tag:flyio-target", # Fly proxy routes through Caddy on indri ], ) diff --git a/pulumi/tailscale/policy.hujson b/pulumi/tailscale/policy.hujson index 88408ef..e6ddb85 100644 --- a/pulumi/tailscale/policy.hujson +++ b/pulumi/tailscale/policy.hujson @@ -20,8 +20,7 @@ }, // --- Members: user-facing services only --- - // Kiwix, Forge, Miniflux, PostgreSQL - // (devpi moved off-cluster to indri; reachable via Caddy on tag:flyio-target) + // Kiwix, Forge, devpi, Miniflux, PostgreSQL { "src": ["autogroup:member"], "dst": ["tag:kiwix"], @@ -32,6 +31,11 @@ "dst": ["tag:forge"], "ip": ["tcp:443", "tcp:22"], }, + { + "src": ["autogroup:member"], + "dst": ["tag:devpi"], + "ip": ["tcp:443"], + }, { "src": ["autogroup:member"], "dst": ["tag:feed"], @@ -148,6 +152,7 @@ "tag:grafana": ["autogroup:admin", "tag:blumeops"], "tag:kiwix": ["autogroup:admin", "tag:blumeops"], "tag:forge": ["autogroup:admin", "tag:blumeops"], + "tag:devpi": ["autogroup:admin", "tag:blumeops"], "tag:loki": ["autogroup:admin", "tag:blumeops"], "tag:pg": ["autogroup:admin", "tag:blumeops"], "tag:feed": ["autogroup:admin", "tag:blumeops"], @@ -188,13 +193,11 @@ "src": "tag:ci-gateway", "accept": ["tag:registry:443"], }, - // Fly.io proxy can only reach flyio-target tagged endpoints, nothing else. - // indri has tag:flyio-target (Caddy) so tag:homelab:443 is reachable on - // indri specifically but not other homelab devices. + // Fly.io proxy can only reach flyio-target tagged endpoints, nothing else { "src": "tag:flyio-proxy", "accept": ["tag:flyio-target:443"], - "deny": ["tag:k8s:443", "tag:homelab:22", "tag:nas:445", "tag:registry:443"], + "deny": ["tag:k8s:443", "tag:homelab:443", "tag:homelab:22", "tag:nas:445", "tag:registry:443"], }, ], } diff --git a/service-versions.yaml b/service-versions.yaml index 866c687..6821488 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -1,11 +1,11 @@ -# Service / Tooling/ Application Version Tracking +# Service Version Tracking # # Tracks when each BlumeOps service was last reviewed for version freshness. # Used by `mise run service-review` to surface stale services. # # Fields: # name - kebab-case service identifier -# type - argocd | ansible | nixos | fly | mise +# type - argocd | ansible | nixos # last-reviewed - date (YYYY-MM-DD) or null # current-version - deployed version string or null # upstream-source - URL to upstream releases/changelog @@ -44,20 +44,10 @@ services: upstream-source: https://github.com/gethomepage/homepage/releases notes: Custom container, kustomize manifests - - name: shower - type: argocd - last-reviewed: 2026-05-15 - current-version: "1.1.3" - upstream-source: https://forge.eblu.me/eblume/adelaide-baby-shower-app - notes: | - Django app for Adelaide / Heidi / Addie's baby shower. Wheel - published to Forgejo Packages PyPI; runs on ringtail k3s. Public - at shower.eblu.me (fly proxy), tailnet admin at shower.ops.eblu.me. - - name: nvidia-device-plugin type: argocd - last-reviewed: 2026-06-04 - current-version: "v0.19.2" + last-reviewed: 2026-03-27 + current-version: "v0.19.0" upstream-source: https://github.com/NVIDIA/k8s-device-plugin/releases notes: DaemonSet + RuntimeClass on ringtail for GPU workloads @@ -69,35 +59,34 @@ services: - name: frigate-notify type: argocd - last-reviewed: 2026-03-28 + last-reviewed: 2026-02-22 current-version: "v0.5.4" upstream-source: https://github.com/0x2142/frigate-notify/releases - name: tempo type: argocd - last-reviewed: 2026-04-02 - current-version: "2.10.3" + last-reviewed: 2026-03-05 + current-version: "2.10.1" upstream-source: https://github.com/grafana/tempo/releases - notes: Home-built container from forge mirror - name: alloy-tracing-ringtail type: argocd - last-reviewed: 2026-04-30 - current-version: "v1.16.0" + last-reviewed: 2026-03-13 + current-version: "v1.14.0" upstream-source: https://github.com/grafana/alloy/releases notes: Privileged DaemonSet with Beyla eBPF for HTTP tracing on ringtail - name: alloy-ringtail type: argocd - last-reviewed: 2026-04-30 - current-version: "v1.16.0" + last-reviewed: 2026-03-13 + current-version: "v1.14.0" upstream-source: https://github.com/grafana/alloy/releases notes: DaemonSet on ringtail for host metrics and pod logs - name: alloy-k8s type: argocd - last-reviewed: 2026-04-30 - current-version: "v1.16.0" + last-reviewed: 2026-03-13 + current-version: "v1.14.0" upstream-source: https://github.com/grafana/alloy/releases - name: tailscale-operator @@ -106,92 +95,67 @@ services: current-version: "v1.94.2" upstream-source: https://github.com/tailscale/tailscale/releases - - name: tailscale - type: container - last-reviewed: 2026-05-10 - current-version: "1.94.2" - upstream-source: https://github.com/tailscale/tailscale/releases - notes: | - Locally mirrored tailscale image used by ringtail's tailscale-operator - ProxyClass. Built via containers/tailscale/default.nix. - - name: grafana type: argocd - last-reviewed: 2026-04-02 - current-version: "12.4.2" + last-reviewed: 2026-02-23 + current-version: "12.3.3" upstream-source: https://github.com/grafana/grafana/releases notes: Home-built container from Alpine; upgraded from Helm to Kustomize - name: grafana-sidecar type: argocd parent: grafana - last-reviewed: "2026-04-13" - current-version: "2.6.0" + last-reviewed: "2026-03-03" + current-version: "1.28.0" upstream-source: https://github.com/kiwigrid/k8s-sidecar/releases notes: Dashboard ConfigMap watcher sidecar in grafana deployment - name: cloudnative-pg type: argocd - last-reviewed: 2026-03-28 + last-reviewed: 2026-02-24 current-version: "v1.28.1" upstream-source: https://github.com/cloudnative-pg/cloudnative-pg/releases notes: Deployed via Helm chart (chart v0.27.1 from forge mirror) - name: immich type: argocd - last-reviewed: 2026-04-04 - current-version: "v2.6.3" + last-reviewed: 2026-02-25 + current-version: "v2.5.6" upstream-source: https://github.com/immich-app/immich/releases - notes: Kustomize manifests with upstream images - - - name: valkey - type: argocd - last-reviewed: 2026-05-28 - current-version: "8.1.7" - upstream-source: https://github.com/valkey-io/valkey/releases - notes: >- - Dual-build valkey image: container.py builds Alpine 3.22 + apk valkey - (arm64, indri) for paperless; default.nix builds via nixpkgs (amd64, - ringtail) for immich-ringtail. Both track upstream valkey 8.1.x; Alpine - 3.22 currently ships 8.1.7-r0 and nixpkgs valkey is 8.1.7. Alpine 3.23 - jumps to 9.0. Distinct from authentik-redis (nix-built Redis - 8.x) which has its own entry. + notes: Deployed via Helm chart - name: external-secrets type: argocd - last-reviewed: 2026-06-04 + last-reviewed: 2026-03-25 current-version: "v2.2.0" upstream-source: https://github.com/external-secrets/external-secrets/releases - notes: >- - Static kustomize manifests rendered from upstream Helm chart. Controller - image is locally built from the forge mirror via containers/external-secrets/container.py - (single all_providers static Go binary). + notes: Static kustomize manifests rendered from upstream Helm chart - name: 1password-connect type: argocd - last-reviewed: 2026-04-06 - current-version: "1.8.2" + last-reviewed: 2026-02-26 + current-version: "1.8.1" upstream-source: https://hub.docker.com/r/1password/connect-api/tags - notes: Kustomize manifests rendered from connect-helm-charts v2.4.1 + notes: Deployed via Helm chart (chart v2.3.0) - name: argocd type: argocd - last-reviewed: 2026-04-07 - current-version: "v3.3.6" + last-reviewed: 2026-02-26 + current-version: "v3.3.2" upstream-source: https://github.com/argoproj/argo-cd/releases notes: Kustomize-based install with ServerSideApply - name: blumeops-pg type: argocd - last-reviewed: 2026-03-28 + last-reviewed: 2026-02-27 current-version: "18.3" upstream-source: https://github.com/cloudnative-pg/cloudnative-pg/releases notes: CloudNativePG Cluster resource; pinned to PG minor version - name: authentik type: argocd - last-reviewed: "2026-04-08" - current-version: "2026.2.2" + last-reviewed: "2026-03-01" + current-version: "2026.2.0" upstream-source: https://github.com/goauthentik/authentik/releases - name: authentik-redis @@ -206,87 +170,72 @@ services: - name: ollama type: argocd - last-reviewed: "2026-04-09" - current-version: "0.20.4" + last-reviewed: "2026-03-02" + current-version: "0.17.5" upstream-source: https://github.com/ollama/ollama/releases notes: LLM inference server on ringtail (GPU); upstream container image - name: navidrome type: argocd - last-reviewed: 2026-04-11 - current-version: "v0.61.1" + last-reviewed: 2026-03-02 + current-version: "v0.60.3" upstream-source: https://github.com/navidrome/navidrome/releases - name: miniflux type: argocd - last-reviewed: 2026-04-12 - current-version: "2.2.19" + last-reviewed: 2026-03-02 + current-version: "2.2.17" upstream-source: https://github.com/miniflux/v2/releases - name: teslamate type: argocd - last-reviewed: "2026-06-03" + last-reviewed: 2026-03-03 current-version: "v3.0.0" upstream-source: https://github.com/teslamate-org/teslamate/releases - notes: >- - Tesla data logger. Container ported from Dagger (container.py) to Nix - (containers/teslamate/default.nix) — a from-scratch beamPackages - mixRelease (Elixir/Phoenix release with npm-built assets), since - teslamate is not in nixpkgs. Pins erlang_27 + elixir_1_18 from the - shared nixos-unstable rev; assets via in-release npm ci + esbuild; - ex_cldr locale data pre-fetched (LOCALES env) to avoid sandbox - downloads. Version unchanged (v3.0.0). Build verified on ringtail. - name: transmission type: argocd - last-reviewed: 2026-04-15 + last-reviewed: 2026-03-04 current-version: "4.1.1-r1" upstream-source: https://github.com/transmission/transmission/releases - name: transmission-exporter type: argocd - last-reviewed: 2026-04-15 + last-reviewed: 2026-03-05 current-version: "1.0.1" upstream-source: null notes: Homegrown Python exporter, no upstream - name: kiwix type: argocd - last-reviewed: 2026-04-17 + last-reviewed: 2026-03-05 current-version: "3.8.2" upstream-source: https://github.com/kiwix/kiwix-tools/releases - name: devpi - type: ansible - last-reviewed: 2026-04-29 - current-version: "6.19.3" + type: argocd + last-reviewed: 2026-03-06 + current-version: "6.19.1" upstream-source: https://github.com/devpi/devpi/releases - notes: Installed via uv into a venv on indri; version pinned in ansible/roles/devpi/defaults/main.yml - name: cv - type: ansible - last-reviewed: 2026-04-29 + type: argocd + last-reviewed: 2026-03-07 current-version: "1.0.3" upstream-source: https://forge.eblu.me/eblume/cv - notes: >- - Static tarball downloaded by ansible/roles/cv into ~/blumeops/cv/content on indri; - served directly by Caddy (kind=static). Migrated from minikube 2026-04-29. - Review build deps (WeasyPrint, Jinja2) in source repo on upstream review. + notes: Personal static site; review build deps (WeasyPrint, Jinja2) in source repo - name: docs - type: ansible - last-reviewed: 2026-04-29 - current-version: "v1.16.0" - upstream-source: https://forge.eblu.me/eblume/blumeops/releases - notes: >- - Quartz-built tarball downloaded by ansible/roles/docs into ~/blumeops/docs/content - on indri; served directly by Caddy (kind=static, try_html). current-version - tracks the blumeops docs release tag. + type: argocd + last-reviewed: 2026-03-07 + current-version: "1.28.2" + upstream-source: https://github.com/jackyzha0/quartz/releases + notes: Quartz static site generator; container version tracks nginx base - name: forgejo-runner type: argocd - last-reviewed: 2026-04-20 - current-version: "12.8.2" + last-reviewed: 2026-02-22 + current-version: "12.7.0" upstream-source: https://code.forgejo.org/forgejo/runner/releases notes: >- Runner daemon version (code.forgejo.org/forgejo/runner). Job execution @@ -294,8 +243,8 @@ services: - name: runner-job-image type: argocd - last-reviewed: 2026-04-21 - current-version: "0.20.6" + last-reviewed: 2026-03-06 + current-version: "0.20.1" upstream-source: https://github.com/dagger/dagger/releases notes: >- Forgejo Actions job execution image. CONTAINER_APP_VERSION tracks the @@ -303,140 +252,73 @@ services: - name: nix-container-builder type: nixos - last-reviewed: 2026-04-01 - current-version: "12.7.2" + last-reviewed: 2026-02-22 + current-version: "12.6.4" upstream-source: https://code.forgejo.org/forgejo/runner/releases - notes: >- - Forgejo runner on ringtail; pinned via nixpkgs-services overlay in flake.nix. - Update nixpkgs-services rev during service reviews, not via nix flake update. + notes: Forgejo runner on ringtail via nixpkgs; version tracks flake.lock - name: snowflake-proxy type: nixos - last-reviewed: 2026-04-01 + last-reviewed: 2026-03-24 current-version: "2.11.0" upstream-source: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/releases - notes: >- - Tor Snowflake proxy on ringtail; pinned via nixpkgs-services overlay in flake.nix. - Anti-censorship bridge, not an exit node. - - - name: k3s - type: nixos - last-reviewed: 2026-04-01 - current-version: "1.34.5+k3s1" - upstream-source: https://github.com/k3s-io/k3s/releases - notes: >- - Single-node k3s cluster on ringtail; pinned via nixpkgs-services overlay in flake.nix. - Update nixpkgs-services rev during service reviews. - - - name: minikube - type: ansible - last-reviewed: 2026-04-01 - current-version: "1.38.0" - upstream-source: https://github.com/kubernetes/minikube/releases - notes: >- - Single-node minikube on indri; installed via homebrew (not version-pinned). - Homebrew may silently upgrade on brew update/upgrade. + notes: Tor Snowflake proxy on ringtail; anti-censorship bridge, not an exit node - name: mealie type: argocd - last-reviewed: "2026-06-03" - current-version: "v3.16.0" + last-reviewed: 2026-03-16 + current-version: "v3.12.0" upstream-source: https://github.com/mealie-recipes/mealie/releases - notes: >- - Recipe manager. Container ported from Dockerfile to Nix - (containers/mealie/default.nix wraps nixpkgs mealie from a pinned - nixos-unstable; single gunicorn process, SQLite on the mealie-data - PVC). Bumped v3.12.0 -> v3.16.0 as part of the port (the deferred - upgrade). Breaking-change review v3.13-v3.16: no schema breaking - changes, SQLite auto-migrates forward via init_db; notable items are - minor (OIDC missing-claims log -> DEBUG, NLP parser uses user-defined - units, Nuxt 3->4 frontend, new Announcements feature, path-traversal - patches). Source PVC retained for rollback. Build verified on ringtail. - - - name: paperless - type: argocd - last-reviewed: "2026-06-03" - current-version: "v2.20.15" - upstream-source: https://github.com/paperless-ngx/paperless-ngx/releases - notes: >- - Document management. Container ported from Dockerfile to Nix - (containers/paperless/default.nix wraps nixpkgs paperless-ngx from a - pinned nixos-unstable). Runs as web/worker/beat/consumer containers on - ringtail (multi-process; no s6). Bumped v2.20.13 -> v2.20.15 (the - unstable package version, same-minor patch) as part of the port. + notes: Recipe manager; built from source via forge mirror - name: unpoller type: argocd - last-reviewed: 2026-05-28 - current-version: "v3.2.0" + last-reviewed: 2026-03-16 + current-version: "v2.34.0" upstream-source: https://github.com/unpoller/unpoller/releases notes: UniFi metrics exporter for Prometheus - name: prowler type: argocd - last-reviewed: 2026-04-14 - current-version: "5.23.0" + last-reviewed: 2026-03-24 + current-version: "5.22.0" upstream-source: https://github.com/prowler-cloud/prowler/releases notes: CIS Kubernetes Benchmark scanner; weekly CronJob on minikube-indri - - name: kingfisher - type: argocd - last-reviewed: 2026-03-29 - current-version: "165768b" - upstream-source: https://github.com/mongodb/kingfisher/releases - notes: Secret scanner; sporked from upstream with --clone-url-base patch. Version is upstream main SHA. - - name: forgejo type: ansible - last-reviewed: 2026-03-28 - current-version: "14.0.3" + last-reviewed: 2026-02-22 + current-version: "14.0.2" upstream-source: https://codeberg.org/forgejo/forgejo/releases - notes: Built from source on indri (~/code/3rd/forgejo) + notes: Installed via Homebrew on indri; plan to migrate to source build - name: alloy type: ansible - last-reviewed: 2026-04-30 - current-version: "v1.16.0" + last-reviewed: 2026-03-13 + current-version: "v1.14.0" upstream-source: https://github.com/grafana/alloy/releases notes: Built from source on indri - name: zot type: ansible - last-reviewed: 2026-05-04 - current-version: "v2.1.16" + last-reviewed: 2026-03-14 + current-version: "v2.1.15" upstream-source: https://github.com/project-zot/zot/releases notes: Built from source on indri - name: caddy type: ansible - last-reviewed: 2026-05-06 + last-reviewed: 2026-03-15 current-version: "v2.11.2" upstream-source: https://github.com/caddyserver/caddy/releases notes: Built from source with Gandi DNS and Layer 4 plugins - - name: heph - type: ansible - last-reviewed: 2026-06-05 - current-version: "v1.2.1" - upstream-source: https://forge.eblu.me/eblume/hephaestus/releases - notes: >- - hephaestus task/context sync hub on indri (server-mode launchagent, - ansible/roles/heph; cargo-built from the forge). SELF-UPDATING: hephd - polls the forge for newer releases every 10 min and rebuilds + restarts - itself, so the running version drifts AHEAD of the ansible heph_version - pin. current-version here is the last observed/deployed tag, not a hard - pin — verify the live version via `curl https://heph.ops.eblu.me/config` - is served (hub up) and the hub log's `current=` line. Reconciling this - self-update vs IaC-pin drift is tracked in the heph "Hephaestus" project: - "Reconcile hephd self-update with ansible-pinned version (drift on indri - hub)" (node 01KTBXWT6XTHNDH92CVJY88E5K). - - name: borgmatic type: ansible - last-reviewed: 2026-04-15 - current-version: "2.1.4" + last-reviewed: 2026-03-16 + current-version: "2.1.3" upstream-source: https://github.com/borgmatic-collective/borgmatic/releases - notes: Installed via mise (pipx); version pinned in ansible/roles/borgmatic/defaults/main.yml and mise.toml + notes: Installed via mise (pipx), not managed by Ansible role - name: jellyfin type: ansible @@ -450,62 +332,3 @@ services: current-version: "1.11.0" upstream-source: https://www.pixeleyes.co.nz/automounter/ notes: Mac App Store app, no Ansible role. Updates via App Store. - - - name: flyio-tailscale - type: fly - last-reviewed: "2026-04-10" - current-version: "v1.94.1" - upstream-source: https://github.com/tailscale/tailscale/releases - notes: >- - Pinned after v1.96.5 broke MagicDNS in containers. Test DNS resolution - inside Fly container before upgrading. COPY --from in fly/Dockerfile. - - - name: flyio-nginx - type: fly - last-reviewed: "2026-04-10" - current-version: "1.29.6-alpine" - upstream-source: https://hub.docker.com/_/nginx - notes: Base image for Fly proxy (fly/Dockerfile) - - - name: flyio-alloy - type: fly - parent: flyio-nginx - last-reviewed: "2026-04-10" - current-version: "v1.14.1" - upstream-source: https://github.com/grafana/alloy/releases - notes: COPY --from in fly/Dockerfile for log shipping and metrics - - - name: dagger - type: mise - last-reviewed: 2026-04-21 - current-version: "0.20.6" - upstream-source: https://github.com/dagger/dagger/releases - notes: Dagger CI/CD engine; pinned in mise.toml - - - name: ansible-core - type: mise - last-reviewed: 2026-04-12 - current-version: "2.20.1" - upstream-source: https://github.com/ansible/ansible/releases - notes: Installed via pipx/uvx with botocore and boto3 - - - name: prek - type: mise - last-reviewed: 2026-04-12 - current-version: "0.3.4" - upstream-source: https://github.com/j178/prek/releases - notes: Pre-commit hook runner (Rust reimplementation) - - - name: pulumi-cli - type: mise - last-reviewed: 2026-04-12 - current-version: "3.215.0" - upstream-source: https://github.com/pulumi/pulumi/releases - notes: IaC CLI for tailscale and gandi stacks - - - name: ty - type: mise - last-reviewed: 2026-04-12 - current-version: "0.0.29" - upstream-source: https://github.com/astral-sh/ty/releases - notes: Astral Python typechecker (beta); prek hook diff --git a/src/blumeops/__init__.py b/src/blumeops/__init__.py deleted file mode 100644 index 1d1b128..0000000 --- a/src/blumeops/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""BlumeOps — Dagger build functions for container images and CI.""" - -from .main import Blumeops as Blumeops diff --git a/src/blumeops/containers.py b/src/blumeops/containers.py deleted file mode 100644 index d63c127..0000000 --- a/src/blumeops/containers.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Container build discovery and reusable helpers. - -Discovers native Dagger builds from containers//container.py files. -Each container.py must define a top-level `build(src)` async function that -returns a dagger.Container, and a `VERSION` string constant. -""" - -import importlib.util -from pathlib import Path -from types import ModuleType - -import dagger -from dagger import dag - -FORGE_MIRROR = "https://forge.ops.eblu.me/mirrors" - - -# --- Discovery --- - - -def _discover_modules(containers_dir: Path) -> dict[str, Path]: - """Find all containers//container.py files.""" - result = {} - if not containers_dir.is_dir(): - return result - for child in sorted(containers_dir.iterdir()): - container_py = child / "container.py" - if child.is_dir() and container_py.exists(): - result[child.name] = container_py - return result - - -def _load_module(name: str, path: Path) -> ModuleType: - """Dynamically load a container.py as a Python module.""" - spec = importlib.util.spec_from_file_location(f"containers.{name}", path) - if spec is None or spec.loader is None: - msg = f"Cannot load {path}" - raise ImportError(msg) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -def discover(containers_dir: Path) -> dict[str, ModuleType]: - """Discover and load all container.py modules. - - Returns a dict mapping container name to loaded module. - Each module must define: - - VERSION: str — the upstream application version - - async def build(src: dagger.Directory) -> dagger.Container - """ - modules = {} - for name, path in _discover_modules(containers_dir).items(): - modules[name] = _load_module(name, path) - return modules - - -# --- Reusable Helpers --- - - -def clone_from_forge(mirror: str, tag: str) -> dagger.Directory: - """Git clone from forge mirror at a given tag. Returns the repo tree.""" - return dag.git(f"{FORGE_MIRROR}/{mirror}.git").tag(tag).tree() - - -def go_build( - source: dagger.Directory, - output: str, - *, - cmd_path: str = ".", - tags: str = "netgo", - ldflags: str = "-w -s", - buildmode: str | None = None, - cgo_enabled: bool = False, - extra_apk: list[str] | None = None, - extra_env: dict[str, str] | None = None, -) -> dagger.Container: - """Go build stage on golang:alpine3.23. - - Returns a container with the built binary at `output`. - """ - apk_packages = ["build-base", "git"] + (extra_apk or []) - ctr = ( - dag.container() - .from_("golang:alpine3.23") - .with_exec(["apk", "add", "--no-cache", *apk_packages]) - .with_directory("/app", source) - .with_workdir("/app") - .with_env_variable("CGO_ENABLED", "1" if cgo_enabled else "0") - ) - for key, val in (extra_env or {}).items(): - ctr = ctr.with_env_variable(key, val) - build_cmd = ["go", "build"] - if buildmode: - build_cmd.append(f"-buildmode={buildmode}") - build_cmd += [f"-tags={tags}", f"-ldflags={ldflags}", "-o", output, cmd_path] - return ctr.with_exec(build_cmd) - - -def node_build( - source: dagger.Directory, - workdir: str, - *, - install_cmd: list[str] | None = None, - build_cmd: list[str] | None = None, -) -> dagger.Container: - """Node.js build stage on node:22-alpine. - - Returns a container with built assets in the workdir. - """ - if install_cmd is None: - install_cmd = ["npm", "ci"] - if build_cmd is None: - build_cmd = ["npm", "run", "build"] - - return ( - dag.container() - .from_("node:22-alpine") - .with_directory("/app", source) - .with_workdir(f"/app/{workdir}" if workdir != "." else "/app") - .with_exec(install_cmd) - .with_exec(build_cmd) - ) - - -def alpine_runtime( - *, - extra_apk: list[str] | None = None, - uid: int = 65534, - gid: int = 65534, - username: str = "app", - create_user: bool = True, -) -> dagger.Container: - """Standard Alpine 3.23 runtime base. - - When create_user is True (default), creates a non-root user with the given - uid/gid/username. Set create_user=False to use an existing user (e.g. - Alpine's built-in nobody:65534). - """ - packages = extra_apk or [] - setup_cmds = [] - if packages: - setup_cmds.append(f"apk add --no-cache {' '.join(packages)}") - if create_user: - setup_cmds.append(f"addgroup -g {gid} {username}") - setup_cmds.append(f"adduser -u {uid} -G {username} -D {username}") - - ctr = dag.container().from_("alpine:3.23") - if setup_cmds: - ctr = ctr.with_exec(["sh", "-c", " && ".join(setup_cmds)]) - return ctr - - -def oci_labels( - ctr: dagger.Container, - *, - title: str, - description: str, - version: str, -) -> dagger.Container: - """Apply standard BlumeOps OCI labels.""" - return ( - ctr.with_label("org.opencontainers.image.title", title) - .with_label("org.opencontainers.image.description", description) - .with_label("org.opencontainers.image.version", version) - .with_label( - "org.opencontainers.image.source", - "https://forge.eblu.me/eblume/blumeops", - ) - .with_label("org.opencontainers.image.vendor", "blumeops") - ) diff --git a/update-loki-3.6.7.infra.md b/update-loki-3.6.7.infra.md new file mode 100644 index 0000000..c30a619 --- /dev/null +++ b/update-loki-3.6.7.infra.md @@ -0,0 +1 @@ +Update loki from 3.6.5 to 3.6.7 diff --git a/uv.lock b/uv.lock deleted file mode 100644 index b86a906..0000000 --- a/uv.lock +++ /dev/null @@ -1,806 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13" - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/334/b70e641fd2221/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/08b/310f9e24a9594/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708" }, -] - -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/d03/ceb89cb322a8f/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c64/7aa4a12dfbad9/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" }, -] - -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/03f/829f5bb192318/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/635/79f9a0628e062/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" }, -] - -[[package]] -name = "beartype" -version = "0.22.9" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/8f8/2b54aa723a284/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d16/c9bbc61ea1463/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2" }, -] - -[[package]] -name = "blumeops" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "dagger-io" }, -] - -[package.metadata] -requires-dist = [{ name = "dagger-io", editable = "sdk" }] - -[[package]] -name = "cattrs" -version = "26.1.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "attrs" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/fa2/39e0f0ec0715b/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d1e/0804c42639494/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096" }, -] - -[[package]] -name = "certifi" -version = "2026.2.25" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/e88/7ab5cee78ea81/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/027/692e4402ad994/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/ae8/9db9e5f98a11a/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f49/6c9c3cc022300/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0ea/948db76d31190/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a27/7ab8928b9f299/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3be/c022aec2c514d/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e04/4c39e41b92c84/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f49/5a1652cf3fbab/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e71/2b419df8ba5e4/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/780/4338df6fcc081/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/481/551899c856c70/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f59/099f9b66f0d71/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f59/ad4c0e8f6bba2/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3de/dcc22d73ec993/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/64f/02c6841d7d83f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/404/2d5c8f957e152/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/394/6fa46a0cf3e4c/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/80d/04837f55fc81d/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c36/c333c39be2dbc/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/1c2/aed2e5e41f24e/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/545/23e136b894806/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/715/479b9a2802eca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/bd6/c2a1c7573c647/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c45/e9440fb78f8dd/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/353/4e7dcbdcf757d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e8a/c484bf18ce697/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a5f/e03b42827c13c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2d6/eb928e13016ce/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e74/327fb75de8986/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d60/38d37043bced9/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/757/9e913a5339fb8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5b7/7459df20e0815/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/92a/0a01ead5e6684/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/67f/6279d125ca004/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/eff/c3f4497871172/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fbc/cdc05410c9ee2/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/733/784b6d6def852/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a89/c23ef8d2c6b27/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6c1/14670c45346af/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a18/0c5e59792af26/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3c9/a494bc5ec77d4/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/8d8/28b6667a32a72/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/cf1/493cd8607bec4/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0c9/6c3b819b5c3e9/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/752/a45dc4a693406/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/877/8f0c7a52e56f7/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ce3/412fbe1e31eb8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c03/a41a8784091e6/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/038/53ed82eeebbce/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c35/abb8bfff0185e/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3dc/e51d0f5e7951f/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d" }, -] - -[[package]] -name = "dagger-io" -version = "0.0.0" -source = { editable = "sdk" } -dependencies = [ - { name = "anyio" }, - { name = "beartype" }, - { name = "cattrs" }, - { name = "exceptiongroup" }, - { name = "gql", extra = ["httpx"] }, - { name = "httpcore" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-logging" }, - { name = "opentelemetry-sdk" }, - { name = "platformdirs" }, - { name = "rich" }, - { name = "typing-extensions" }, -] - -[package.metadata] -requires-dist = [ - { name = "anyio", specifier = ">=3.6.2" }, - { name = "beartype", specifier = ">=0.22.0" }, - { name = "cattrs", specifier = ">=25.1.0" }, - { name = "exceptiongroup", specifier = ">=1.3.0" }, - { name = "gql", extras = ["httpx"], specifier = ">=4.0" }, - { name = "httpcore", specifier = ">=1.0.8" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.23.0" }, - { name = "opentelemetry-instrumentation-logging", specifier = ">=0.54b1" }, - { name = "opentelemetry-sdk", specifier = ">=1.23.0" }, - { name = "platformdirs", specifier = ">=2.6.2" }, - { name = "rich", specifier = ">=10.11.0" }, - { name = "typing-extensions", specifier = ">=4.13.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "aiohttp", specifier = ">=3.9.3" }, - { name = "codegen", editable = "sdk/codegen" }, - { name = "mypy", specifier = ">=1.8.0" }, - { name = "pytest", specifier = ">=8.0.2" }, - { name = "pytest-httpx", specifier = ">=0.30.0" }, - { name = "pytest-mock", specifier = ">=3.12.0" }, - { name = "pytest-subprocess", specifier = ">=1.5.0" }, - { name = "ruff", specifier = ">=0.3.4" }, - { name = "sphinx", specifier = ">=7.2.6" }, - { name = "sphinx-rtd-theme", specifier = ">=2.0.0" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/8b4/12432c6055b0b/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a7a/39a3bd276781e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.74.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/579/71e4eeeba6aad/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/702/216f78610bb51/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5" }, -] - -[[package]] -name = "gql" -version = "4.0.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "anyio" }, - { name = "backoff" }, - { name = "graphql-core" }, - { name = "yarl" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/f22/980844eb6a7c0/gql-4.0.0.tar.gz", hash = "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f3b/eed7c531218eb/gql-4.0.0-py3-none-any.whl", hash = "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479" }, -] - -[package.optional-dependencies] -httpx = [ - { name = "httpx" }, -] - -[[package]] -name = "graphql-core" -version = "3.2.8" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/015/457da5d996c92/graphql_core-3.2.8.tar.gz", hash = "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/cbe/e07bee1b3ed5e/graphql_core-3.2.8-py3-none-any.whl", hash = "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/4e3/5b956cf45792e/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/63c/f8bbe7522de3b/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/6e3/4463af53fd2ab/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2d4/00746a40668fc/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/75e/98c5f16b0f35b/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d90/9fcccc110f8c7/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/795/dafcc9c04ed0c/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/771/a87f49d9defaf/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/49f/ef1ae6440c182/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5a1/f80bf1daa4894/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/cb0/a2b4aa34f932c/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/873/27c59b172c501/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/bb4/13d29f5eea38f/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/840/08a41e51615a4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" }, -] - -[[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/ec6/652a1bee61c53/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2b4/1f5fed0ed5636/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/84e/61e3af5463c19/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/935/434b9853c7c11/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/432/feb25a1cb67fe/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e82/d14e3c948952a/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/4cf/b48c6ea66c83b/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/1d5/40e51b7e8e170/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/273/d23f4b40f3dce/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/9d6/24335fd4fa1c0/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/12f/ad252f8b267cc/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/03e/de2a6ffbe8ef9/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/90e/fbcf47dbe33dc/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5c4/b9bfc148f5a91/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/401/c5a650f3add24/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/978/91f3b1b3ffbde/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e1c/5988359516095/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/960/c83bf01a95b12/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/563/fe25c678aaba3/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c76/c4bec1538375d/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/57b/46b24b5d5ebcc/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e95/4b24433c768ce/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3bd/231490fa7217c/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/253/282d70d67885a/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0b4/c48648d7649c9/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/98b/c624954ec4d2c/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/1b9/9af4d9eec0b49/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6aa/c4f16b472d5b7/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/21f/830fe223215df/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f5d/d81c45b05518b/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/eb3/04767bca2bb92/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c90/35dde0f916702/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/af9/59b9beeb66c82/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/41f/2952231456154/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/df9/f19c28adcb40b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d54/ecf9f301853f2/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5a3/7ca18e360377c/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/8f3/33ec9c5eb1b71/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a40/7f13c188f804c/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0e1/61ddf326db557/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/1e3/a8bb24342a820/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/972/31140a50f5d44/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6b1/0359683bd8806/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/283/ddac99f7ac25a/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/538/cec1e18c067d0/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/7ee/e46ccb30ff48a/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fa2/63a02f4f2dd2d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2e1/425e2f99ec5bd/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/497/394b3239fc6f0/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/233/b398c29d3f1b9/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/93b/1818e4a6e0930/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f33/dc2a3abe9249e/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3ab/8b9d8b75aef9d/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5e0/1429a929600e7/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/488/5cb0e817aef5d/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/045/8c978acd8e6ea/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c0a/bd12629b0af3c/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/145/25a5f61d7d0c9/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/173/07b22c217b4cf/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/7a7/e590ff876a3ea/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5fa/6a95dfee63893/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a05/43217a6a01769/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f99/fe611c312b3c1/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/900/4d8386d133b7e/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e62/8ef0e6859ffd8/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/841/189848ba629c3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ce1/bbd7d780bb5a0/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/b26/684587228afed/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/9f9/af11306994335/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/b49/38326284c4f12/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/986/55c737850c064/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/497/bde6223c212ba/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2bb/d113e0d4af5db/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/55d/97cc6dae627ef/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56" }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.41.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/942/1d911326ec12d/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0e7/7c806e6a89c9e/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.41.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "opentelemetry-proto" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/966/bbce537e9edb1/opentelemetry_exporter_otlp_proto_common-1.41.0.tar.gz", hash = "sha256:966bbce537e9edb166154779a7c4f8ab6b8654a03a28024aeaf1a3eacb07d6ee" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/7a9/9177bf61f85f4/opentelemetry_exporter_otlp_proto_common-1.41.0-py3-none-any.whl", hash = "sha256:7a99177bf61f85f4f9ed2072f54d676364719c066f6d11f515acc6c745c7acf0" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.41.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/dcd/6e0686f56277d/opentelemetry_exporter_otlp_proto_http-1.41.0.tar.gz", hash = "sha256:dcd6e0686f56277db4eecbadd5262124e8f2cc739cadbc3fae3d08a12c976cf5" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a9c/4ee69cce9c3f4/opentelemetry_exporter_otlp_proto_http-1.41.0-py3-none-any.whl", hash = "sha256:a9c4ee69cce9c3f4d7ee736ad1b44e3c9654002c0816900abbafd9f3cf289751" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.62b0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/aa1/b0b9ab2e1722c/opentelemetry_instrumentation-0.62b0.tar.gz", hash = "sha256:aa1b0b9ab2e1722c2a8a5384fb016fc28d30bba51826676c8036074790d2861e" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/30d/4e76486eae64f/opentelemetry_instrumentation-0.62b0-py3-none-any.whl", hash = "sha256:30d4e76486eae64fb095264a70c2c809c4bed17b73373e53091470661f7d477c" }, -] - -[[package]] -name = "opentelemetry-instrumentation-logging" -version = "0.62b0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/61f/23be960e04705/opentelemetry_instrumentation_logging-0.62b0.tar.gz", hash = "sha256:61f23be960e047054b3aa38998e7d1eb1fd9bef6f52097e28bc113af8b6f3bd8" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/9a2/7b6c3d419170d/opentelemetry_instrumentation_logging-0.62b0-py3-none-any.whl", hash = "sha256:9a27b6c3d419170d96dedcea7d38cc0418f0dd1054365f52499a0a1eb70b8faf" }, -] - -[[package]] -name = "opentelemetry-proto" -version = "1.41.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/95d/2e576f9fb1800/opentelemetry_proto-1.41.0.tar.gz", hash = "sha256:95d2e576f9fb1800473a3e4cfcca054295d06bdb869fda4dc9f4f779dc68f7b6" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/b97/0ab537309f9ee/opentelemetry_proto-1.41.0-py3-none-any.whl", hash = "sha256:b970ab537309f9eed296be482c3e7cca05d8aca8165346e929f658dbe153b247" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.41.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/7bd/df3961131b318/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a59/6f5687964a3e0/opentelemetry_sdk-1.41.0-py3-none-any.whl", hash = "sha256:a596f5687964a3e0d7f8edfdcf5b79cbca9c93c7025ebf5fb00f398a9443b0bd" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.62b0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/cbf/b3c8fc259575c/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0dd/ac1ce59eaf1a8/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489" }, -] - -[[package]] -name = "packaging" -version = "26.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/002/43ae351a25711/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/b36/f1fef9334a558/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.6" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/3bf/a75b0ad0db840/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e61/adb1d5e5cb344/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917" }, -] - -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/f48/107a8c637e803/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/43e/edf29202c0855/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d62/cdfcfd89ccb8d/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/cae/65ad55793da34/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/333/ddb9031d2704a/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fd0/858c20f078a32/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/678/ae89ebc632c5c/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d47/2aeb4fbf9865e/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/4d3/df5fa7e36b322/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ee1/7f18d2498f267/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/580/e97762b950f99/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/501/d20b891688eb8/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/9a0/bd56e5b100aef/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/bcc/9aaa5d80322bc/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/381/914df18634f54/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/887/3eb4460fd5533/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/92d/1935ee1f8d744/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/473/c61b39e1460d3/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c0e/f0aaafc66fbd8/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f95/393b4d66bfae9/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c07/fda85708bc485/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/af2/23b406d6d0008/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a78/372c932c90ee4/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/564/d9f0d4d9509e1/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/176/12831fda01380/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/41a/89040cb10bd34/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e35/b88984e7fa64a/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6f8/b465489f927b0/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2ad/890caa1d928c7/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f7e/e0e597f495cf4/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/929/d7cbe1f01bb7b/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3f7/124c9d820ba55/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c0d/4b719b7da3359/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/9f3/02f4783709a78/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c80/ee5802e3fb9ea/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ed5/a841e8bb29a55/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/55c/72fd6ea2da4c3/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/832/6e14434146040/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/060/b16ae65bc098d/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/89e/b3fa9524f7bec/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/dee/69d7015dc235f/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/555/8992a00dfd54c/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c9b/822a577f560fb/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ab4/c29b49d560fe4/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5a1/03c3eb905fcea/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/74c/1fb26515153e4/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/824/e908bce90fb27/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c2b/5e7db5328427c/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6f6/ff873ed40292c/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/49a/2dc67c154db2c/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/005/f08e6a0529984/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5c3/310452e0d3139/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/4c3/c70630930447f/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/8e5/7061305815dfc/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/521/a463429ef5414/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/120/c964da3fdc75e/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d8f/353eb14ee3441/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ab2/943be7c652f09/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/056/74a162469f313/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/990/f6b3e2a27d683/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ece/f2343af4cc68e/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/af2/a6052aeb6cf17/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237" }, -] - -[[package]] -name = "protobuf" -version = "6.33.6" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/a67/68d25248312c2/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/7d2/9d9b65f8afef1/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0cd/27b587afca21b/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/972/0e6961b251bde/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e2a/fbae9b8e1825e/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c96/c37eec15086b7/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e9d/b7e292e0ab79d/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/771/79e006c476e69/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/675/7cd03768053ff/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/81a/9e26dd42fd28a/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" }, -] - -[[package]] -name = "requests" -version = "2.33.1" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/188/17f8c57c62639/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/4e6/d1ef462f3626a/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/edd/07a4824c6b401/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/33b/d4ef74232fb73/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/0ce/a48d173cc12fa/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f0f/a19c6845758ab/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/1b6/2b6884944a57d/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/bf2/72323e553dfb2/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" }, -] - -[[package]] -name = "wrapt" -version = "2.1.2" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/399/6a67eecc2c68f/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/787/fd6f4d67befa6/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/4bd/f26e03e6d0da3/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/bba/c24d879aa2299/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/169/97dfb9d67addc/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/162/e4e2ba7542da9/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f29/c827a8d9936ac/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a9d/d9813825f7ecb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6f8/dbdd3719e5348/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5c3/5b5d82b16a3bc/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f8b/c1c264d8d1cf5/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3be/b22f674550d56/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/0fc/04bc8664a8bc4/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a9b/9d50c9af99887/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2d3/ff4f0024dd224/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/327/8c471f4468ad5/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a89/14c754d3134a3/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ff9/5d4264e55839b/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/764/05518ca4e1b76/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c0b/e8b5a74c5824e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f01/277d9a5fc1862/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/84c/e8f1c2104d2f6/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a93/cd767e37faedd/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/137/0e516598854e5/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6de/1a3851c27e0bd/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/de9/f1a2bbc5ac7f6/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/970/d57ed83fa040d/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/396/9c56e4563c375/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/57d/7c0c980abdc5f/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/776/867878e83130c/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fab/036efe5464ec3/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e6e/d62c82ddf58d0/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/467/e7c7631539033/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/da1/f00a557c66225/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/625/03ffbc2d3a698/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c7e/6cd120ef837d5/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/376/9a77df8e756d6/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a76/d61a2e8519961/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6f9/7edc9842cf215/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/400/6c351de6d5007/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a93/72fc3639a878c/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/314/4b027ff30cbd2/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/3b8/d15e52e195813/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/08f/fa54146a7559f/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/72a/aa9d0d8e4ed0e/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/b8f/d6fa2b2c4e762/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8" }, -] - -[[package]] -name = "yarl" -version = "1.23.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/53b/1ea6ca88ebd44/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/16c/6994ac35c3e74/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/4a4/2e651629dafb6/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/7c6/b9461a2a8b47c/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/256/9b67d616eab45/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e9d/9a4d06d3481ea/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f51/4f6474e04179d/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fda/207c815b253e3/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/34b/6cf500e61c90f/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d75/04f2b476d2165/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/578/110dd426f0d20/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/609/d3614d78d74eb/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/496/6242ec68afc74/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e0f/d068364a6759b/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/390/04f0ad156da43/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e57/23c01a56c5028/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/1b6/b572edd95b4fa/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/baa/f55442359053c/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fb4/948814a2a98e3/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/aec/fed0b41aa72b7/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a41/bcf68efd19073/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/cde/9a2ecd91668bc/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/502/3346c4ee7992f/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d10/09abedb49ae95/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a8d/00f29b42f534c/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/954/51e6ce06c3e10/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/531/ef597132086b6/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/88f/9fb0116fbfcef/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e7b/0460976dc75cb/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/115/136c4a426f9da/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ead/11956716a940c/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fe8/f8f5e70e6dbdf/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a0e/317df055958a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/6f0/fd84de0c957b2/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/93a/784271881035a/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/dd0/0607bffbf3025/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/ac0/9d42f48f80c9e/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/21d/1b7305a71a15b/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/856/10b4f27f69984/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/23f/371bd662cf44a/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c4a/80f77dc1acaaa/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/bd6/54fad46d8d9e8/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/682/bae25f0a0dd23/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a82/836cab5f197a0/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/1c5/7676bdedc94cd/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c7f/8dc16c498ff06/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/5ee/586fb17ff8f90/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/172/35362f5801497/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/079/3e2bd0cf14234/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/365/0dc2480f94f71/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/f40/e782d49630ad3/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/94f/8575fbdf81749/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/c8a/a34a5c864db10/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/63e/92247f383c85a/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/70e/fd20be968c76e/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/9a1/8d6f9359e4572/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/280/3ed8b21ca47a4/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/394/906945aa8b19f/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/71d/006bee8397a4a/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/626/94e275c93d54f/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a31/de1613658308e/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/fb1/e8b8d66c278b2/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/50f/9d8d531dfb767/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/575/aa4405a656e61/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/041/b1a4cefacf658/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d38/c1e8231722c4c/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/d53/834e23c015ee8/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/2e2/7c8841126e017/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/768/55800ac56f878/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/e09/fd068c2e169a7/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/733/09162a6a571d4/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/450/3053d296bc6e4/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/44b/b7bef4ea40938/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25" }, - { url = "https://pypi.ops.eblu.me/root/pypi/+f/a2d/f6afe50dea8ae/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f" }, -] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.ops.eblu.me/root/pypi/+simple/" } -sdist = { url = "https://pypi.ops.eblu.me/root/pypi/+f/a07/157588a12518c/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" } -wheels = [ - { url = "https://pypi.ops.eblu.me/root/pypi/+f/071/652d6115ed432/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e" }, -]