Compare commits
8 commits
main
...
feature/un
| Author | SHA1 | Date | |
|---|---|---|---|
| fcabbddbb8 | |||
| a39b9a4bf6 | |||
| 0631035a43 | |||
| 6916c5b5ee | |||
| 61b1e0cc07 | |||
| 74c8ef7209 | |||
| 8a8bfffdb1 | |||
| 6b0005df1e |
538 changed files with 5625 additions and 30226 deletions
|
|
@ -1,62 +0,0 @@
|
|||
---
|
||||
name: change-classifier
|
||||
description: Classifies proposed changes as C0/C1/C2 before work begins. Use proactively when the user describes a new task or change, before any implementation starts.
|
||||
tools: Read, Glob, Grep, Bash
|
||||
model: haiku
|
||||
permissionMode: dontAsk
|
||||
---
|
||||
|
||||
You are a change classifier for the BlumeOps infrastructure project. Your job is to assess a proposed change and classify it as C0, C1, or C2 before any work begins.
|
||||
|
||||
## Classification Criteria
|
||||
|
||||
| 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 |
|
||||
|
||||
## Assessment Process
|
||||
|
||||
1. Understand what the user wants to change
|
||||
2. Identify which files/services are affected — use Glob/Grep to check the blast radius
|
||||
3. Assess risk factors:
|
||||
- How many files change?
|
||||
- Are critical services affected (networking, auth, DNS)?
|
||||
- Is the change easily reversible?
|
||||
- Could it cause downtime?
|
||||
- Does it span multiple services or systems?
|
||||
- Does it require multi-step sequencing?
|
||||
4. Classify and explain your reasoning
|
||||
|
||||
## C0 Indicators
|
||||
- Single file or small number of related files
|
||||
- Config value change, version bump, typo fix, doc update
|
||||
- No service restart needed, or restart is safe
|
||||
- Easy to fix-forward if wrong
|
||||
|
||||
## C1 Indicators
|
||||
- Multiple files across a service boundary
|
||||
- New feature or significant behavior change
|
||||
- Could affect service availability
|
||||
- Needs human review for correctness
|
||||
- Touching Ansible roles, ArgoCD manifests, or routing config
|
||||
|
||||
## C2 Indicators
|
||||
- Multi-phase work with ordering dependencies
|
||||
- Spans multiple sessions or multiple services
|
||||
- Requires prerequisite changes before the main goal
|
||||
- User explicitly requests Mikado methodology
|
||||
- Discovery-heavy work where the full scope isn't known upfront
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
Classification: C0 / C1 / C2
|
||||
Confidence: high / medium / low
|
||||
Rationale: <1-2 sentences>
|
||||
Blast radius: <files/services affected>
|
||||
Risk factors: <key concerns, if any>
|
||||
```
|
||||
|
||||
If confidence is low, explain what additional information would help. When in doubt, classify one level higher (C0 → C1, C1 → C2).
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
---
|
||||
name: infra-health
|
||||
description: Infrastructure health monitor. Use proactively after deployments, provisioning, or when the user asks about service status. Runs services-check and diagnoses failures.
|
||||
tools: Bash, Read, Grep, Glob
|
||||
model: haiku
|
||||
permissionMode: dontAsk
|
||||
background: true
|
||||
---
|
||||
|
||||
You are an infrastructure health monitor for the BlumeOps homelab.
|
||||
|
||||
When invoked, run the full health check suite and report results:
|
||||
|
||||
1. Run `mise run services-check` and capture the full output
|
||||
2. Parse the results — identify any FAILED services
|
||||
3. For each failure, provide a brief diagnosis:
|
||||
- Is the service process down?
|
||||
- Is it a network/connectivity issue?
|
||||
- Is it an ArgoCD sync issue?
|
||||
4. Summarize: total services checked, how many passed, how many failed
|
||||
|
||||
If everything is healthy, keep the summary to one line.
|
||||
|
||||
If there are failures, group them by category:
|
||||
- **Process failures** (service not running)
|
||||
- **HTTP failures** (endpoint not responding)
|
||||
- **Kubernetes failures** (pod not running, sync issues)
|
||||
- **Connectivity failures** (SSH, network)
|
||||
|
||||
Do NOT attempt to fix anything. Report findings only.
|
||||
|
||||
Context:
|
||||
- Services run across indri (Mac Mini, native + minikube), ringtail (NixOS, k3s), and Fly.io
|
||||
- Use `--context=minikube-indri` for indri k8s commands, `--context=k3s-ringtail` for ringtail
|
||||
- HTTP endpoints are proxied through Caddy at `*.ops.eblu.me`
|
||||
- Public endpoints go through Fly.io at `*.eblu.me`
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
---
|
||||
name: mikado-navigator
|
||||
description: Mikado chain navigator for C2 changes. Use when resuming a C2 chain, checking chain status, or deciding which leaf node to work next. Understands the Mikado Branch Invariant.
|
||||
tools: Read, Glob, Grep, Bash
|
||||
model: sonnet
|
||||
permissionMode: dontAsk
|
||||
---
|
||||
|
||||
You are a Mikado chain navigator for the BlumeOps C2 change process. You help the user understand the current state of a Mikado chain and decide what to do next.
|
||||
|
||||
## What You Do
|
||||
|
||||
1. Run `mise run docs-mikado --resume` to detect the current chain state
|
||||
2. Read the relevant Mikado cards (docs in `docs/how-to/` with `status: active`)
|
||||
3. Analyze the dependency graph and branch position
|
||||
4. Recommend the next action
|
||||
|
||||
## Chain State Analysis
|
||||
|
||||
After running `docs-mikado --resume`, interpret the output:
|
||||
|
||||
- **Planning phase:** Cards are being added, no code yet. Suggest reviewing the dependency graph for completeness.
|
||||
- **Mid-cycle:** An `impl` is in progress. Identify which leaf is being worked and what remains.
|
||||
- **Between cycles:** A leaf was just closed. Identify the next ready leaf and summarize what it requires.
|
||||
- **Finalized:** The chain is complete and awaiting merge.
|
||||
- **Invariant violation:** A plan commit was found after impl. Explain the reset procedure.
|
||||
|
||||
## Recommending Next Actions
|
||||
|
||||
For each ready leaf node:
|
||||
1. Read the card content to understand what it requires
|
||||
2. Check if there are related source files (manifests, playbooks, configs)
|
||||
3. Assess relative complexity and suggest an ordering if multiple leaves are ready
|
||||
4. Note any potential risks or dependencies not captured in the card graph
|
||||
|
||||
## The Mikado Branch Invariant
|
||||
|
||||
The branch must always have this structure:
|
||||
```
|
||||
main <- [plan commits] <- [impl, close] <- [impl, close] <- ... <- [finalize]
|
||||
```
|
||||
|
||||
Rules:
|
||||
- First N commits are card-only (plan phase)
|
||||
- Then repeating cycles of impl + close
|
||||
- No card introductions after any code commit
|
||||
- New prerequisites require a branch reset
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
Chain: <name>
|
||||
Branch: <branch name>
|
||||
Position: <planning / mid-cycle / between-cycles / etc.>
|
||||
PR: #<number> (if exists)
|
||||
|
||||
Ready leaves:
|
||||
1. <leaf-stem> — <title> — <brief description of work needed>
|
||||
2. ...
|
||||
|
||||
Recommendation: <what to do next and why>
|
||||
```
|
||||
|
||||
## Important
|
||||
|
||||
- Do NOT make any changes. You are advisory only.
|
||||
- If the user is on `main`, list all active chains and suggest which to resume.
|
||||
- If PR comments exist, remind the user to check them with `mise run pr-comments <number>`.
|
||||
- Check for stashed work — resets sometimes leave stashed changes.
|
||||
0
.gitattributes → .dagger/.gitattributes
vendored
0
.gitattributes → .dagger/.gitattributes
vendored
4
.dagger/.gitignore
vendored
Normal file
4
.dagger/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/.venv
|
||||
/**/__pycache__
|
||||
/sdk
|
||||
/.env
|
||||
|
|
@ -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/"]
|
||||
3
.dagger/src/blumeops_ci/__init__.py
Normal file
3
.dagger/src/blumeops_ci/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""BlumeOps CI — Dagger build functions for container images."""
|
||||
|
||||
from .main import BlumeopsCi as BlumeopsCi
|
||||
|
|
@ -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/<name>/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/<name>/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")
|
||||
)
|
||||
768
.dagger/uv.lock
generated
Normal file
768
.dagger/uv.lock
generated
Normal file
|
|
@ -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" },
|
||||
]
|
||||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
runs-on: k8s
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Run branch cleanup
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
echo "Building BlumeOps release: $VERSION"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -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'\""
|
||||
|
|
|
|||
147
.forgejo/workflows/build-container-nix.yaml
Normal file
147
.forgejo/workflows/build-container-nix.yaml
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Nix container build workflow
|
||||
# Triggers on pushes to main that modify containers/*, or via manual dispatch.
|
||||
# Detects which containers changed, builds from default.nix, and pushes via
|
||||
# skopeo with commit-SHA-based tags: vX.Y.Z-<sha>-nix
|
||||
name: Build Container (Nix)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['containers/**']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
container:
|
||||
description: 'Container name (directory under containers/)'
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: 'Commit SHA to build (defaults to current HEAD)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
runs-on: nix-container-builder
|
||||
outputs:
|
||||
containers: ${{ steps.list.outputs.containers }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed containers
|
||||
id: list
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
CONTAINERS='["${{ inputs.container }}"]'
|
||||
else
|
||||
CONTAINERS=$(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 "containers=$CONTAINERS" >> "$GITHUB_OUTPUT"
|
||||
echo "Containers to build: $CONTAINERS"
|
||||
|
||||
build:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.containers != '[]'
|
||||
runs-on: nix-container-builder
|
||||
strategy:
|
||||
matrix:
|
||||
container: ${{ fromJson(needs.detect.outputs.containers) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Check for default.nix
|
||||
id: check
|
||||
run: |
|
||||
if [ -f "containers/${{ matrix.container }}/default.nix" ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No default.nix for ${{ matrix.container }} — skipping"
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract version and SHA
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: meta
|
||||
run: |
|
||||
CONTAINER="${{ matrix.container }}"
|
||||
NIX_FILE="containers/$CONTAINER/default.nix"
|
||||
|
||||
# Try extracting version = "..." from the nix file (e.g. ntfy)
|
||||
VERSION=$(grep -m1 '^\s*version\s*=\s*"' "$NIX_FILE" \
|
||||
| sed 's/.*"\(.*\)".*/\1/' || true)
|
||||
|
||||
# Fall back to CONTAINER_APP_VERSION from Dockerfile (e.g. nettest)
|
||||
if [ -z "$VERSION" ] && [ -f "containers/$CONTAINER/Dockerfile" ]; then
|
||||
VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \
|
||||
"containers/$CONTAINER/Dockerfile" \
|
||||
| sed 's/^ARG CONTAINER_APP_VERSION=//')
|
||||
fi
|
||||
|
||||
# Last resort: nix eval for nixpkgs packages (e.g. authentik)
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(nix --extra-experimental-features "nix-command flakes" \
|
||||
eval --raw "nixpkgs#${CONTAINER}.version")
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not determine version for $CONTAINER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REF="${{ inputs.ref }}"
|
||||
if [ -z "$REF" ]; then
|
||||
REF="${GITHUB_SHA}"
|
||||
fi
|
||||
SHORT_SHA=$(echo "$REF" | head -c 7)
|
||||
|
||||
# Ensure version starts with 'v'
|
||||
case "$VERSION" in
|
||||
v*) ;;
|
||||
*) VERSION="v${VERSION}" ;;
|
||||
esac
|
||||
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Version: $VERSION, SHA: $SHORT_SHA"
|
||||
|
||||
- name: Resolve nixpkgs
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: nixpkgs
|
||||
run: |
|
||||
NIXPKGS_PATH=$(nix flake metadata nixpkgs --json | jq -r '.path')
|
||||
echo "Resolved nixpkgs: $NIXPKGS_PATH"
|
||||
echo "path=$NIXPKGS_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build with nix
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
env:
|
||||
NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}"
|
||||
run: |
|
||||
echo "Building containers/${{ matrix.container }}/default.nix"
|
||||
echo "NIX_PATH=$NIX_PATH"
|
||||
nix-build "containers/${{ matrix.container }}/default.nix" -o result
|
||||
echo "Build complete: $(readlink result)"
|
||||
|
||||
- name: Push to registry
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
env:
|
||||
ZOT_CI_API_KEY: ${{ secrets.ZOT_CI_API_KEY }}
|
||||
run: |
|
||||
CONTAINER="${{ matrix.container }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
SHORT_SHA="${{ steps.meta.outputs.sha }}"
|
||||
IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:${VERSION}-${SHORT_SHA}-nix"
|
||||
|
||||
echo "Pushing to $IMAGE"
|
||||
skopeo copy \
|
||||
--dest-creds="zot-ci:$ZOT_CI_API_KEY" \
|
||||
"docker-archive:result" \
|
||||
"docker://$IMAGE"
|
||||
echo "Push complete: $IMAGE"
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# Unified container build workflow
|
||||
# Manual dispatch only — use `mise run container-build-and-release <name>`.
|
||||
# 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
|
||||
# - Nix containers build on nix-container-builder (ringtail) via nix-build + skopeo
|
||||
# Dockerfile container build workflow
|
||||
# Triggers on pushes to main that modify containers/*, or via manual dispatch.
|
||||
# Detects which containers changed, extracts version from CONTAINER_APP_VERSION,
|
||||
# and publishes with commit-SHA-based tags: vX.Y.Z-<sha>
|
||||
name: Build Container
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['containers/**']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
container:
|
||||
|
|
@ -23,81 +23,64 @@ jobs:
|
|||
detect:
|
||||
runs-on: k8s
|
||||
outputs:
|
||||
dagger: ${{ steps.classify.outputs.dagger }}
|
||||
nix: ${{ steps.classify.outputs.nix }}
|
||||
containers: ${{ steps.list.outputs.containers }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Classify container build type
|
||||
id: classify
|
||||
- name: Detect changed containers
|
||||
id: list
|
||||
run: |
|
||||
CHANGED='["${{ inputs.container }}"]'
|
||||
echo "Building container: $CHANGED"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
CONTAINERS='["${{ inputs.container }}"]'
|
||||
else
|
||||
# Diff against parent commit to find changed container dirs
|
||||
CONTAINERS=$(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 "containers=$CONTAINERS" >> "$GITHUB_OUTPUT"
|
||||
echo "Containers to build: $CONTAINERS"
|
||||
|
||||
# Classify each container by build type (a container can appear in both)
|
||||
DAGGER='[]'
|
||||
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]')
|
||||
has_any=true
|
||||
fi
|
||||
if [ -f "containers/$name/default.nix" ]; then
|
||||
NIX=$(echo "$NIX" | jq -c --arg n "$name" '. + [$n]')
|
||||
has_any=true
|
||||
fi
|
||||
if [ "$has_any" = "false" ]; then
|
||||
echo "Warning: $name has neither container.py, Dockerfile, nor default.nix — skipping"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "dagger=$DAGGER" >> "$GITHUB_OUTPUT"
|
||||
echo "nix=$NIX" >> "$GITHUB_OUTPUT"
|
||||
echo "Dagger builds: $DAGGER"
|
||||
echo "Nix builds: $NIX"
|
||||
|
||||
build-dagger:
|
||||
build:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.dagger != '[]'
|
||||
if: needs.detect.outputs.containers != '[]'
|
||||
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.containers) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Extract version and SHA
|
||||
id: meta
|
||||
- name: Check for Dockerfile
|
||||
id: check
|
||||
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=//')
|
||||
if [ -f "containers/${{ matrix.container }}/Dockerfile" ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No Dockerfile for ${{ matrix.container }} — skipping"
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract version and SHA
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: meta
|
||||
run: |
|
||||
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
|
||||
|
||||
# Use dispatch input ref if provided, otherwise current commit
|
||||
REF="${{ inputs.ref }}"
|
||||
if [ -z "$REF" ]; then
|
||||
REF="${GITHUB_SHA}"
|
||||
|
|
@ -106,7 +89,7 @@ jobs:
|
|||
|
||||
# Ensure version starts with 'v'
|
||||
case "$VERSION" in
|
||||
v*) ;;
|
||||
v*) ;; # already has v prefix
|
||||
*) VERSION="v${VERSION}" ;;
|
||||
esac
|
||||
|
||||
|
|
@ -115,6 +98,7 @@ jobs:
|
|||
echo "Version: $VERSION, SHA: $SHORT_SHA"
|
||||
|
||||
- name: Publish
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
env:
|
||||
ZOT_CI_API_KEY: ${{ secrets.ZOT_CI_API_KEY }}
|
||||
run: |
|
||||
|
|
@ -124,79 +108,3 @@ jobs:
|
|||
--version=${{ steps.meta.outputs.version }} \
|
||||
--commit-sha=${{ steps.meta.outputs.sha }} \
|
||||
--registry-password=env:ZOT_CI_API_KEY
|
||||
|
||||
build-nix:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.nix != '[]'
|
||||
runs-on: nix-container-builder
|
||||
strategy:
|
||||
matrix:
|
||||
container: ${{ fromJson(needs.detect.outputs.nix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
|
||||
- name: Extract version and SHA
|
||||
id: meta
|
||||
run: |
|
||||
CONTAINER="${{ matrix.container }}"
|
||||
NIX_FILE="containers/$CONTAINER/default.nix"
|
||||
|
||||
# Extract version = "..." from the nix file
|
||||
VERSION=$(grep -m1 '^\s*version\s*=\s*"' "$NIX_FILE" \
|
||||
| sed 's/.*"\(.*\)".*/\1/' || true)
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: No version declaration found in $NIX_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REF="${{ inputs.ref }}"
|
||||
if [ -z "$REF" ]; then
|
||||
REF="${GITHUB_SHA}"
|
||||
fi
|
||||
SHORT_SHA=$(echo "$REF" | head -c 7)
|
||||
|
||||
# Ensure version starts with 'v'
|
||||
case "$VERSION" in
|
||||
v*) ;;
|
||||
*) VERSION="v${VERSION}" ;;
|
||||
esac
|
||||
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Version: $VERSION, SHA: $SHORT_SHA"
|
||||
|
||||
- name: Resolve nixpkgs
|
||||
id: nixpkgs
|
||||
run: |
|
||||
NIXPKGS_PATH=$(nix flake metadata nixpkgs --json | jq -r '.path')
|
||||
echo "Resolved nixpkgs: $NIXPKGS_PATH"
|
||||
echo "path=$NIXPKGS_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build with nix
|
||||
env:
|
||||
NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}"
|
||||
run: |
|
||||
echo "Building containers/${{ matrix.container }}/default.nix"
|
||||
echo "NIX_PATH=$NIX_PATH"
|
||||
nix-build "containers/${{ matrix.container }}/default.nix" -o result
|
||||
echo "Build complete: $(readlink result)"
|
||||
|
||||
- name: Push to registry
|
||||
env:
|
||||
ZOT_CI_API_KEY: ${{ secrets.ZOT_CI_API_KEY }}
|
||||
run: |
|
||||
CONTAINER="${{ matrix.container }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
SHORT_SHA="${{ steps.meta.outputs.sha }}"
|
||||
IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:${VERSION}-${SHORT_SHA}-nix"
|
||||
|
||||
echo "Pushing to $IMAGE"
|
||||
skopeo copy \
|
||||
--dest-creds="zot-ci:$ZOT_CI_API_KEY" \
|
||||
"docker-archive:result" \
|
||||
"docker://$IMAGE"
|
||||
echo "Push complete: $IMAGE"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -60,18 +58,20 @@ jobs:
|
|||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- 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/"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: k8s
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Install flyctl
|
||||
run: |
|
||||
|
|
|
|||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,6 +1,4 @@
|
|||
.claude/settings.local.json
|
||||
.claude/agent-memory/
|
||||
.claude/scheduled_tasks.lock
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
|
@ -8,10 +6,5 @@ __pycache__/
|
|||
*.pyo
|
||||
.venv/
|
||||
|
||||
# Dagger (auto-generated SDK)
|
||||
/sdk/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
/**/__pycache__
|
||||
/.env
|
||||
|
|
|
|||
171
AGENTS.md
171
AGENTS.md
|
|
@ -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 <pr_number>`** before proceeding
|
||||
6. **Add changelog fragments (all change levels)** - `docs/changelog.d/<name>.<type>.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: `<branch>.<type>.md`
|
||||
- **C0:** Use orphan prefix: `+<descriptive-slug>.<type>.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/<chain-stem>` governed by the Mikado Branch Invariant: all card commits first, then code progress, then card closures. Commits use `C2(<chain>): 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/<service>/`
|
||||
2. Push. Sync 'apps' app if service definition changed (set --revision to branch).
|
||||
3. Test on branch: `argocd app set <service> --revision <branch> && argocd app sync <service>`
|
||||
4. After merge: `argocd app set <service> --revision main && argocd app sync <service>`
|
||||
|
||||
**Commands:** `argocd app list|get|diff|sync <app>`
|
||||
|
||||
**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 <role> # 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 <name> <version> # 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 <run#> # see jobs in the run
|
||||
mise run runner-logs <run#> -j <N> # 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/<project>/`.
|
||||
|
||||
### 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/<project>/` 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 <mirror-name>`
|
||||
|
||||
## 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.
|
||||
505
CHANGELOG.md
505
CHANGELOG.md
|
|
@ -12,511 +12,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
<!-- towncrier release notes start -->
|
||||
|
||||
## [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:<context>` (mealie — kubectl uses indri's
|
||||
kubeconfig) or `ssh:<user@host>` (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: <id>` 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/<token>/`, 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-<account-uuid>-<timestamp>.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 <N>` command after dispatching, polling the Forgejo API to resolve the run number for the commit it just triggered.
|
||||
- `mise run runner-logs <run> -j <n>` 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
|
||||
|
||||
- Deploy Prowler CIS scanner as a weekly CronJob on minikube-indri, with reports written to sifaka NFS share.
|
||||
- Add Grafana "Alerts" dashboard showing currently firing alerts and recent state changes.
|
||||
- Add IaC scanning via Prowler IaC provider (Saturday 2am, Dockerfiles and K8s manifests).
|
||||
- Add container image vulnerability scanning via Prowler image provider (Saturday 3am, all blumeops/* images).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix authentik worker OOMKill by setting AUTHENTIK_WORKER_CONCURRENCY=2 (was defaulting to 16 based on CPU count).
|
||||
- Remove `group: ""` from tailscale-operator ignoreDifferences — ArgoCD normalizes away the empty string, causing permanent OutOfSync on the apps app.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Decommission JobSync service — removed ArgoCD app, k8s manifests, container build, Caddy proxy, Homepage entry, docs, and forge mirror. Replaced by datasette-based job tracking (coming soon).
|
||||
- Localize authentik-redis container: replace upstream `redis:7-alpine` with nix-built image from nixpkgs (Redis 8.2.3). Introduces attached service pattern with `parent` field in service-versions.yaml and version assertion in default.nix to prevent silent version drift.
|
||||
- Unified Dockerfile and Nix container build workflows into a single workflow that auto-classifies containers by build type and routes to the correct runner (k8s for Dockerfile, nix-container-builder for Nix). Removed nettest container (outgrown). Nix builds now require an explicit `version = "..."` declaration — no implicit nixpkgs fallback.
|
||||
- Monthly tooling dependency update: bump prek hooks (trufflehog 3.94.0, ruff 0.15.7, shfmt 3.13.0), Fly.io images (nginx 1.29.6, Alloy 1.14.1), actions/checkout v4.3.1→v6.0.2, tighten mise task Python lower bounds (rich 14, typer 0.24, httpx 0.28.1, pyyaml 6.0.2), and bump ansible-lint/ansible-core floors.
|
||||
- Upgrade ntfy v2.17.0 → v2.19.2 (adds experimental PostgreSQL support, read replicas, web push fixes)
|
||||
- Revert Tailscale operator to v1.94.2 (v1.96.3 images not yet published); keep Fly proxy `tailscale wait` improvement
|
||||
- Add RuntimeDefault seccomp profiles to all managed deployments, statefulsets, and cronjobs.
|
||||
- Upgrade Frigate from 0.17.0-rc2 to 0.17.1 (security fixes, bugfixes). Add motion retention tier (365 days), reduce continuous retention from 180 to 30 days.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Review and fix ArgoCD config tutorial: correct sync policy example, fix typo, add missing cross-references and frontmatter.
|
||||
- Review and update 12 reference docs: fix stale image references to point at kustomization manifests instead of hardcoded tags, correct Prometheus scrape target, expand external-secrets stub, add cross-references between backup/disaster-recovery docs, and remove misleading `.ts.net` URLs from Quick Reference tables.
|
||||
|
||||
|
||||
## [v1.14.3] - 2026-03-22
|
||||
|
||||
### Features
|
||||
|
||||
- Deploy infrastructure alerting pipeline using Grafana Unified Alerting with ntfy push notifications. 7 alert rules with runbooks covering service health, pod readiness, PostgreSQL, textfile freshness, Frigate cameras, and ArgoCD sync status. services-check now queries the alerting API for covered checks.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix Frigate NVR crash by re-adding required `mqtt` config section (disabled) after Mosquitto removal.
|
||||
- Fix borgmatic backup failure: use correct kubectl context (`minikube`) on indri for Mealie SQLite dump hook
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Localize Grafana Alloy container image with dual Dockerfile + Nix builds from forge mirror
|
||||
- Upgrade Prometheus from v3.9.1 to v3.10.0 (distroless variants, PromQL fill operators, performance improvements)
|
||||
- Bump Frigate recording retention (180d continuous, 30d detections, 730d alerts) and add camera-fps health check to services-check.
|
||||
- Improve Frigate health checks in services-check: per-camera FPS validation and NFS storage accessibility check.
|
||||
- Increase data retention: Prometheus 15d → 10y, Loki 31d → 365d (PVC sizes unchanged; minikube hostpath doesn't enforce limits)
|
||||
- Standardize OCI labels across all container Dockerfiles with consistent title, description, version, source, and vendor metadata.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Review and correct Tailscale reference doc: fix ACL path, add missing device tags (ringtail, per-service tags, ci-gateway, flyio-proxy), correct access matrix (PyPI→DevPI, homelab grants), add SSH homelab→homelab rule, document auto approvers, add last-reviewed frontmatter.
|
||||
|
||||
### AI Assistance
|
||||
|
||||
- Add four Claude Code subagents: infra-health (background health monitor), doc-reviewer (persistent-memory doc review), change-classifier (C0/C1/C2 triage), and mikado-navigator (C2 chain state advisor).
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Standardized USAGE pragmas and typer CLI parsing across all mise tasks: added missing `#USAGE` directive to `mikado-branch-invariant-check`, converted `pr-comments` and `op-backup` from raw `sys.argv` to typer for consistency with all other uv python scripts.
|
||||
|
||||
|
||||
## [v1.14.2] - 2026-03-17
|
||||
|
||||
### Features
|
||||
|
||||
- Deploy Mealie recipe manager on minikube-indri for meal planning and prep automation.
|
||||
- Add UnPoller deployment to monitor UniFi network metrics via Prometheus
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix Caddy v2.11 breaking change: preserve original Host header for HTTPS upstreams.
|
||||
- Fix plan-a-meal random recipe queries — add required `paginationSeed` parameter
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Externalize Tailscale operator manifest to forge mirror, removing 495 KB vendored file from the repo.
|
||||
- Externalize TeslaMate Grafana dashboards to forge mirror, removing 713 KB of ConfigMaps from the repo.
|
||||
- Upgrade Caddy from v2.10.2 to v2.11.2 (7 CVE fixes), create caddy-l4 forge mirror, migrate all ~/code/3rd clones on indri to HTTPS forge.ops.eblu.me remotes.
|
||||
- Upgrade borgmatic from 2.0.13 to 2.1.3 on indri (improved borg warning handling, memory/performance improvements)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add git last-modified subsort to docs-review script, so ties in review date are broken by least recently updated first.
|
||||
- Review jellyfin (10.11.6, current) and automounter (1.11.0) services; add missing frigate share to automounter docs.
|
||||
|
||||
|
||||
## [v1.14.1] - 2026-03-14
|
||||
|
||||
### Features
|
||||
|
|
|
|||
142
CLAUDE.md
142
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 <pr_number>`** before proceeding
|
||||
6. **Add changelog fragments (all change levels)** - `docs/changelog.d/<name>.<type>.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: `<branch>.<type>.md`
|
||||
- **C0:** Use orphan prefix: `+<descriptive-slug>.<type>.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/<chain-stem>` governed by the Mikado Branch Invariant: all card commits first, then code progress, then card closures. Commits use `C2(<chain>): 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/<service>/`
|
||||
2. Push. Sync 'apps' app if service definition changed (set --revision to branch).
|
||||
3. Test on branch: `argocd app set <service> --revision <branch> && argocd app sync <service>`
|
||||
4. After merge: `argocd app set <service> --revision main && argocd app sync <service>`
|
||||
|
||||
**Commands:** `argocd app list|get|diff|sync <app>`
|
||||
|
||||
**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 <role> # 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 <name> <version> # 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/<project>/`.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ==============
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -26,10 +16,6 @@ borgmatic_source_directories:
|
|||
- /opt/homebrew/var/forgejo
|
||||
- /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:
|
||||
|
|
@ -45,29 +31,6 @@ borgmatic_repositories:
|
|||
# BorgBase SSH key (fetched from 1Password in playbook pre_tasks)
|
||||
borgmatic_borgbase_ssh_key_path: /Users/erichblume/.ssh/borgbase_ed25519
|
||||
|
||||
# Directory for pre-backup database dumps from k8s pods
|
||||
borgmatic_k8s_dump_dir: /Users/erichblume/.local/share/borgmatic/k8s-dumps
|
||||
|
||||
# K8s SQLite databases to dump before backup via kubectl exec
|
||||
# Each entry runs: kubectl exec <pod-selector> -- sqlite3 <path> ".backup /tmp/backup.db"
|
||||
# then copies the dump to borgmatic_k8s_dump_dir/<name>.db
|
||||
borgmatic_k8s_sqlite_dumps:
|
||||
- name: mealie
|
||||
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
|
||||
|
||||
# Exclude patterns
|
||||
borgmatic_exclude_patterns: []
|
||||
|
||||
|
|
@ -82,20 +45,6 @@ borgmatic_keep_yearly: 1000
|
|||
# PostgreSQL databases to backup (streamed via pg_dump)
|
||||
# Password is read from ~/.pgpass (managed by this role)
|
||||
# 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_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
|
||||
borgmatic_photos_schedule_minute: 0
|
||||
# Retention: photos are precious, keep more history
|
||||
borgmatic_photos_keep_daily: 7
|
||||
borgmatic_photos_keep_monthly: 12
|
||||
borgmatic_photos_keep_yearly: 1000
|
||||
|
||||
borgmatic_pg_dump_command: /opt/homebrew/opt/postgresql@18/bin/pg_dump
|
||||
borgmatic_postgresql_databases:
|
||||
# k8s PostgreSQL (CloudNativePG) via Caddy L4 proxy
|
||||
|
|
@ -103,21 +52,7 @@ 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
|
||||
hostname: pg.ops.eblu.me
|
||||
port: 5434
|
||||
username: borgmatic
|
||||
- name: paperless
|
||||
hostname: pg.ops.eblu.me
|
||||
port: 5434
|
||||
username: borgmatic
|
||||
# immich-pg cluster (VectorChord) via Caddy L4 on port 5433
|
||||
- name: immich
|
||||
hostname: pg.ops.eblu.me
|
||||
port: 5433
|
||||
username: borgmatic
|
||||
|
|
|
|||
|
|
@ -4,9 +4,3 @@
|
|||
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.borgmatic.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.eblume.borgmatic.plist
|
||||
changed_when: true
|
||||
|
||||
- name: Reload borgmatic-photos
|
||||
ansible.builtin.shell: |
|
||||
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.borgmatic-photos.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.eblume.borgmatic-photos.plist
|
||||
changed_when: true
|
||||
|
|
|
|||
|
|
@ -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,7 @@
|
|||
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
|
||||
|
|
@ -35,35 +27,11 @@
|
|||
mode: '0600'
|
||||
no_log: true
|
||||
|
||||
- name: Add BorgBase host keys to known_hosts
|
||||
- name: Add BorgBase host key to known_hosts
|
||||
ansible.builtin.known_hosts:
|
||||
name: "{{ item }}"
|
||||
key: "{{ item }} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGU0mISTyHBw9tBs6SuhSq8tvNM8m9eifQxM+88TowPO"
|
||||
name: u3ugi1x1.repo.borgbase.com
|
||||
key: "u3ugi1x1.repo.borgbase.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGU0mISTyHBw9tBs6SuhSq8tvNM8m9eifQxM+88TowPO"
|
||||
state: present
|
||||
loop:
|
||||
- u3ugi1x1.repo.borgbase.com
|
||||
- xcrtl5tg.repo.borgbase.com
|
||||
|
||||
- name: Ensure k8s dump directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ borgmatic_k8s_dump_dir }}"
|
||||
state: directory
|
||||
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:
|
||||
|
|
@ -89,30 +57,3 @@
|
|||
when: borgmatic_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
# --- Immich photo library backup (BorgBase offsite only) ---
|
||||
|
||||
- name: Deploy borgmatic photos configuration
|
||||
ansible.builtin.template:
|
||||
src: photos.yaml.j2
|
||||
dest: "{{ borgmatic_photos_config }}"
|
||||
mode: '0600'
|
||||
|
||||
- name: Deploy borgmatic-photos LaunchAgent plist
|
||||
ansible.builtin.template:
|
||||
src: borgmatic-photos.plist.j2
|
||||
dest: ~/Library/LaunchAgents/mcquack.eblume.borgmatic-photos.plist
|
||||
mode: '0644'
|
||||
notify: Reload borgmatic-photos
|
||||
|
||||
- name: Check if borgmatic-photos LaunchAgent is loaded
|
||||
ansible.builtin.command: launchctl list mcquack.eblume.borgmatic-photos
|
||||
register: borgmatic_photos_launchctl_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load borgmatic-photos LaunchAgent if not loaded
|
||||
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.borgmatic-photos.plist
|
||||
when: borgmatic_photos_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.borgmatic-photos</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
|
||||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ borgmatic_bin }}</string>
|
||||
<string>--config</string>
|
||||
<string>{{ borgmatic_photos_config }}</string>
|
||||
<string>create</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ borgmatic_log_dir }}/mcquack.borgmatic-photos.err.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ borgmatic_log_dir }}/mcquack.borgmatic-photos.out.log</string>
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>{{ borgmatic_photos_schedule_hour }}</integer>
|
||||
<key>Minute</key>
|
||||
<integer>{{ borgmatic_photos_schedule_minute }}</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -14,7 +14,10 @@
|
|||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ borgmatic_bin }}</string>
|
||||
<string>/opt/homebrew/opt/mise/bin/mise</string>
|
||||
<string>x</string>
|
||||
<string>--</string>
|
||||
<string>borgmatic</string>
|
||||
<string>--config</string>
|
||||
<string>{{ borgmatic_config }}</string>
|
||||
<string>create</string>
|
||||
|
|
|
|||
|
|
@ -31,24 +31,6 @@ 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:<context> -> local kubectl with --context (mealie etc.)
|
||||
# - ssh:<user@host> -> ssh + k3s kubectl on the cluster host,
|
||||
# used for ringtail since indri's kubeconfig
|
||||
# deliberately doesn't carry that context.
|
||||
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
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
ssh_command: ssh -o IdentitiesOnly=yes -i {{ borgmatic_borgbase_ssh_key_path }}
|
||||
|
||||
# Retention policy
|
||||
|
|
|
|||
|
|
@ -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 <target> <namespace> <selector> \
|
||||
# <db_path> <name> <dump_target>
|
||||
#
|
||||
# <target> is one of:
|
||||
# local:<context> - run local kubectl with --context=<context>
|
||||
# ssh:<user@host> - ssh to host and run k3s kubectl there
|
||||
# (no indri-side kubeconfig needed)
|
||||
#
|
||||
# <namespace> - k8s namespace of the pod
|
||||
# <selector> - label selector to find the pod (e.g. app=shower)
|
||||
# <db_path> - absolute path inside the pod to the SQLite DB
|
||||
# <name> - short name used for temp filenames
|
||||
# <dump_target> - 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 <<EOF > "$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:<context> or ssh:<user@host>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# {{ 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.
|
||||
#
|
||||
# Separate from the main borgmatic config to keep concerns isolated:
|
||||
# - main config: indri data → sifaka + borgbase
|
||||
# - this config: sifaka photos → borgbase (different repo)
|
||||
|
||||
local_path: {{ borgmatic_local_path }}
|
||||
|
||||
source_directories:
|
||||
{% for dir in borgmatic_photos_source_directories %}
|
||||
- {{ dir }}
|
||||
{% endfor %}
|
||||
|
||||
source_directories_must_exist: true
|
||||
|
||||
repositories:
|
||||
- path: {{ borgmatic_photos_borgbase_repo }}
|
||||
label: borgbase-immich-photos
|
||||
encryption: repokey
|
||||
append_only: true
|
||||
|
||||
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
|
||||
|
||||
# Retention policy — photos are precious, keep more history
|
||||
keep_daily: {{ borgmatic_photos_keep_daily }}
|
||||
keep_monthly: {{ borgmatic_photos_keep_monthly }}
|
||||
keep_yearly: {{ borgmatic_photos_keep_yearly }}
|
||||
|
|
@ -1,14 +1,6 @@
|
|||
---
|
||||
# Borg repositories to collect metrics from
|
||||
# Each entry needs a path (local or ssh://) and a label for Prometheus metrics
|
||||
borgmatic_metrics_repos:
|
||||
- path: /Volumes/backups/borg/
|
||||
label: sifaka-local
|
||||
- path: ssh://xcrtl5tg@xcrtl5tg.repo.borgbase.com/./repo
|
||||
label: borgbase-immich-photos
|
||||
|
||||
borgmatic_metrics_repo: /Volumes/backups/borg/
|
||||
borgmatic_metrics_passcommand: cat /Users/erichblume/.borg/config.yaml
|
||||
borgmatic_metrics_ssh_key: /Users/erichblume/.ssh/borgbase_ed25519
|
||||
borgmatic_metrics_dir: /opt/homebrew/var/node_exporter/textfile
|
||||
borgmatic_metrics_script: /Users/erichblume/.local/bin/borgmatic-metrics
|
||||
borgmatic_metrics_interval: 3600 # seconds between metric collection (hourly)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
#!/bin/bash
|
||||
# {{ ansible_managed }}
|
||||
# Collects borg backup metrics for node_exporter textfile collector
|
||||
# Supports multiple repositories with a repo label for Prometheus
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export BORG_PASSCOMMAND="{{ borgmatic_metrics_passcommand }}"
|
||||
export BORG_RSH="ssh -o IdentitiesOnly=yes -i {{ borgmatic_metrics_ssh_key }}"
|
||||
BORG_REPO="{{ borgmatic_metrics_repo }}"
|
||||
OUTPUT_FILE="{{ borgmatic_metrics_dir }}/borgmatic.prom"
|
||||
TEMP_FILE="${OUTPUT_FILE}.tmp"
|
||||
|
||||
|
|
@ -14,109 +13,129 @@ TEMP_FILE="${OUTPUT_FILE}.tmp"
|
|||
BORG_CMD="/opt/homebrew/bin/borg"
|
||||
JQ_CMD="/opt/homebrew/bin/jq"
|
||||
|
||||
# Start fresh
|
||||
cat > "$TEMP_FILE" << 'EOF'
|
||||
# Get repository info
|
||||
repo_json=$($BORG_CMD info --json "$BORG_REPO" 2>/dev/null) || {
|
||||
echo "Failed to get borg repo info" >&2
|
||||
# Write down metric
|
||||
cat > "$TEMP_FILE" << 'EOF'
|
||||
# HELP borgmatic_up Borg backup repository is accessible
|
||||
# TYPE borgmatic_up gauge
|
||||
borgmatic_up 0
|
||||
EOF
|
||||
mv "$TEMP_FILE" "$OUTPUT_FILE"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Get archive list
|
||||
archives_json=$($BORG_CMD list --json "$BORG_REPO" 2>/dev/null) || {
|
||||
echo "Failed to list borg archives" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract repository stats
|
||||
total_size=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_size')
|
||||
total_csize=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_csize')
|
||||
unique_size=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.unique_size')
|
||||
unique_csize=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.unique_csize')
|
||||
total_chunks=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_chunks')
|
||||
unique_chunks=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_unique_chunks')
|
||||
|
||||
# Count archives
|
||||
archive_count=$(echo "$archives_json" | $JQ_CMD -r '.archives | length')
|
||||
|
||||
# Get last archive info
|
||||
last_archive_name=$(echo "$archives_json" | $JQ_CMD -r '.archives[-1].name // empty')
|
||||
|
||||
if [ -n "$last_archive_name" ]; then
|
||||
# Get detailed info for the last archive
|
||||
last_archive_json=$($BORG_CMD info --json "${BORG_REPO}::${last_archive_name}" 2>/dev/null) || {
|
||||
echo "Failed to get last archive info" >&2
|
||||
last_archive_json=""
|
||||
}
|
||||
|
||||
if [ -n "$last_archive_json" ]; then
|
||||
last_original_size=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.original_size')
|
||||
last_compressed_size=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.compressed_size')
|
||||
last_deduplicated_size=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.deduplicated_size')
|
||||
last_nfiles=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.nfiles')
|
||||
last_start=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].start')
|
||||
last_end=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].end')
|
||||
last_duration=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].duration')
|
||||
|
||||
# Convert timestamp to unix epoch
|
||||
last_timestamp=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${last_start%.*}" "+%s" 2>/dev/null || echo "0")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write metrics
|
||||
cat > "$TEMP_FILE" << EOF
|
||||
# HELP borgmatic_up Borg backup repository is accessible
|
||||
# TYPE borgmatic_up gauge
|
||||
borgmatic_up 1
|
||||
|
||||
# HELP borgmatic_repo_original_size_bytes Total original size of all archives (sum of what each backup contains)
|
||||
# TYPE borgmatic_repo_original_size_bytes gauge
|
||||
borgmatic_repo_original_size_bytes $total_size
|
||||
|
||||
# HELP borgmatic_repo_compressed_size_bytes Total compressed size of all archives
|
||||
# TYPE borgmatic_repo_compressed_size_bytes gauge
|
||||
borgmatic_repo_compressed_size_bytes $total_csize
|
||||
|
||||
# HELP borgmatic_repo_deduplicated_size_bytes Actual disk usage after deduplication (unique data)
|
||||
# TYPE borgmatic_repo_deduplicated_size_bytes gauge
|
||||
borgmatic_repo_deduplicated_size_bytes $unique_csize
|
||||
|
||||
# HELP borgmatic_repo_total_chunks Total number of chunks across all archives
|
||||
# TYPE borgmatic_repo_total_chunks gauge
|
||||
borgmatic_repo_total_chunks $total_chunks
|
||||
|
||||
# HELP borgmatic_repo_unique_chunks Number of unique chunks (after deduplication)
|
||||
# TYPE borgmatic_repo_unique_chunks gauge
|
||||
borgmatic_repo_unique_chunks $unique_chunks
|
||||
|
||||
# HELP borgmatic_archive_count Number of archives in the repository
|
||||
# TYPE borgmatic_archive_count gauge
|
||||
borgmatic_archive_count $archive_count
|
||||
EOF
|
||||
|
||||
# Add last archive metrics if available
|
||||
if [ -n "${last_original_size:-}" ]; then
|
||||
cat >> "$TEMP_FILE" << EOF
|
||||
|
||||
# HELP borgmatic_last_archive_original_size_bytes Original size of the last archive (data being backed up)
|
||||
# TYPE borgmatic_last_archive_original_size_bytes gauge
|
||||
borgmatic_last_archive_original_size_bytes $last_original_size
|
||||
|
||||
# HELP borgmatic_last_archive_compressed_size_bytes Compressed size of the last archive
|
||||
# TYPE borgmatic_last_archive_compressed_size_bytes gauge
|
||||
borgmatic_last_archive_compressed_size_bytes $last_compressed_size
|
||||
|
||||
# HELP borgmatic_last_archive_deduplicated_size_bytes Deduplicated size of last archive (new data added)
|
||||
# TYPE borgmatic_last_archive_deduplicated_size_bytes gauge
|
||||
borgmatic_last_archive_deduplicated_size_bytes $last_deduplicated_size
|
||||
|
||||
# HELP borgmatic_last_archive_files Number of files in the last archive
|
||||
# TYPE borgmatic_last_archive_files gauge
|
||||
borgmatic_last_archive_files $last_nfiles
|
||||
|
||||
# HELP borgmatic_last_archive_timestamp Unix timestamp of the last backup
|
||||
# TYPE borgmatic_last_archive_timestamp gauge
|
||||
borgmatic_last_archive_timestamp $last_timestamp
|
||||
|
||||
# HELP borgmatic_last_archive_duration_seconds Duration of the last backup in seconds
|
||||
# TYPE borgmatic_last_archive_duration_seconds gauge
|
||||
borgmatic_last_archive_duration_seconds ${last_duration:-0}
|
||||
EOF
|
||||
|
||||
# Collect per-source-directory sizes
|
||||
cat >> "$TEMP_FILE" << 'EOF'
|
||||
|
||||
# HELP borgmatic_source_size_bytes Size of each backup source directory in bytes
|
||||
# TYPE borgmatic_source_size_bytes gauge
|
||||
EOF
|
||||
|
||||
collect_repo_metrics() {
|
||||
local repo_path="$1"
|
||||
local repo_label="$2"
|
||||
|
||||
# Get repository info
|
||||
repo_json=$($BORG_CMD info --json "$repo_path" 2>/dev/null) || {
|
||||
echo "Failed to get borg repo info for $repo_label" >&2
|
||||
echo "borgmatic_up{repo=\"$repo_label\"} 0" >> "$TEMP_FILE"
|
||||
return
|
||||
}
|
||||
|
||||
# Get archive list
|
||||
archives_json=$($BORG_CMD list --json "$repo_path" 2>/dev/null) || {
|
||||
echo "Failed to list borg archives for $repo_label" >&2
|
||||
echo "borgmatic_up{repo=\"$repo_label\"} 0" >> "$TEMP_FILE"
|
||||
return
|
||||
}
|
||||
|
||||
# Extract repository stats
|
||||
total_size=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_size')
|
||||
total_csize=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_csize')
|
||||
unique_size=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.unique_size')
|
||||
unique_csize=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.unique_csize')
|
||||
total_chunks=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_chunks')
|
||||
unique_chunks=$(echo "$repo_json" | $JQ_CMD -r '.cache.stats.total_unique_chunks')
|
||||
archive_count=$(echo "$archives_json" | $JQ_CMD -r '.archives | length')
|
||||
|
||||
cat >> "$TEMP_FILE" << EOF
|
||||
borgmatic_up{repo="$repo_label"} 1
|
||||
borgmatic_repo_original_size_bytes{repo="$repo_label"} $total_size
|
||||
borgmatic_repo_compressed_size_bytes{repo="$repo_label"} $total_csize
|
||||
borgmatic_repo_deduplicated_size_bytes{repo="$repo_label"} $unique_csize
|
||||
borgmatic_repo_total_chunks{repo="$repo_label"} $total_chunks
|
||||
borgmatic_repo_unique_chunks{repo="$repo_label"} $unique_chunks
|
||||
borgmatic_archive_count{repo="$repo_label"} $archive_count
|
||||
EOF
|
||||
|
||||
# Get last archive info
|
||||
last_archive_name=$(echo "$archives_json" | $JQ_CMD -r '.archives[-1].name // empty')
|
||||
|
||||
if [ -z "$last_archive_name" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Get detailed info for the last archive
|
||||
last_archive_json=$($BORG_CMD info --json "${repo_path}::${last_archive_name}" 2>/dev/null) || {
|
||||
echo "Failed to get last archive info for $repo_label" >&2
|
||||
return
|
||||
}
|
||||
|
||||
last_original_size=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.original_size')
|
||||
last_compressed_size=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.compressed_size')
|
||||
last_deduplicated_size=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.deduplicated_size')
|
||||
last_nfiles=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].stats.nfiles')
|
||||
last_start=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].start')
|
||||
last_duration=$(echo "$last_archive_json" | $JQ_CMD -r '.archives[0].duration')
|
||||
|
||||
# Convert timestamp to unix epoch
|
||||
last_timestamp=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${last_start%.*}" "+%s" 2>/dev/null || echo "0")
|
||||
|
||||
cat >> "$TEMP_FILE" << EOF
|
||||
borgmatic_last_archive_original_size_bytes{repo="$repo_label"} $last_original_size
|
||||
borgmatic_last_archive_compressed_size_bytes{repo="$repo_label"} $last_compressed_size
|
||||
borgmatic_last_archive_deduplicated_size_bytes{repo="$repo_label"} $last_deduplicated_size
|
||||
borgmatic_last_archive_files{repo="$repo_label"} $last_nfiles
|
||||
borgmatic_last_archive_timestamp{repo="$repo_label"} $last_timestamp
|
||||
borgmatic_last_archive_duration_seconds{repo="$repo_label"} ${last_duration:-0}
|
||||
EOF
|
||||
|
||||
# Collect per-source-directory sizes
|
||||
$BORG_CMD list "${repo_path}::${last_archive_name}" --format "{size} {path}{NL}" 2>/dev/null | awk -v repo="$repo_label" '
|
||||
# List archive contents and group by source directory
|
||||
$BORG_CMD list "${BORG_REPO}::${last_archive_name}" --format "{size} {path}{NL}" 2>/dev/null | awk '
|
||||
{
|
||||
size = $1
|
||||
path = $2
|
||||
|
|
@ -126,10 +145,8 @@ EOF
|
|||
else if (path ~ /^Users\/[^\/]+\/devpi/) { source = "devpi" }
|
||||
else if (path ~ /^Users\/[^\/]+\/code\/personal\/zk/) { source = "Zettelkasten" }
|
||||
else if (path ~ /^Users\/[^\/]+\/.config\/borgmatic/) { source = "borgmatic_config" }
|
||||
else if (path ~ /^Users\/[^\/]+\/.local\/share\/borgmatic/) { source = "k8s_dumps" }
|
||||
else if (path ~ /^opt\/homebrew\/var\/forgejo/) { source = "Forgejo" }
|
||||
else if (path ~ /^opt\/homebrew\/var\/loki/) { source = "Loki" }
|
||||
else if (path ~ /^Volumes\/photos/) { source = "immich_photos" }
|
||||
else if (path ~ /^borgmatic\/postgresql_databases/) { source = "PostgreSQL" }
|
||||
else if (path ~ /^borgmatic\//) { source = "borgmatic_metadata" }
|
||||
else { source = "other" }
|
||||
|
|
@ -138,15 +155,10 @@ EOF
|
|||
}
|
||||
END {
|
||||
for (src in totals) {
|
||||
printf "borgmatic_source_size_bytes{repo=\"%s\",source=\"%s\"} %.0f\n", repo, src, totals[src]
|
||||
printf "borgmatic_source_size_bytes{source=\"%s\"} %.0f\n", src, totals[src]
|
||||
}
|
||||
}' >> "$TEMP_FILE"
|
||||
}
|
||||
|
||||
# Collect metrics for each configured repository
|
||||
{% for repo in borgmatic_metrics_repos %}
|
||||
collect_repo_metrics "{{ repo.path }}" "{{ repo.label }}"
|
||||
{% endfor %}
|
||||
fi
|
||||
|
||||
# Atomic move
|
||||
mv "$TEMP_FILE" "$OUTPUT_FILE"
|
||||
|
|
|
|||
|
|
@ -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,38 +72,25 @@ 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"
|
||||
- name: jobsync
|
||||
host: "jobsync.{{ caddy_domain }}"
|
||||
backend: "https://jobsync.tail8d86e.ts.net"
|
||||
- name: ollama
|
||||
host: "ollama.{{ caddy_domain }}"
|
||||
backend: "https://ollama.tail8d86e.ts.net"
|
||||
- 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"
|
||||
|
|
@ -117,11 +101,7 @@ caddy_tcp_services:
|
|||
- port: 2222
|
||||
backend: "localhost:2200" # Forgejo SSH
|
||||
- port: 5432
|
||||
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)
|
||||
backend: "pg.tail8d86e.ts.net:5432" # PostgreSQL
|
||||
- port: "{{ sifaka_node_exporter_port }}"
|
||||
backend: "sifaka:{{ sifaka_node_exporter_port }}" # Sifaka node_exporter
|
||||
- port: "{{ sifaka_smartctl_exporter_port }}"
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.devpi</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ devpi_binary }}</string>
|
||||
<string>--serverdir</string>
|
||||
<string>{{ devpi_server_dir }}</string>
|
||||
<string>--host</string>
|
||||
<string>{{ devpi_host }}</string>
|
||||
<string>--port</string>
|
||||
<string>{{ devpi_port }}</string>
|
||||
<string>--outside-url</string>
|
||||
<string>{{ devpi_outside_url }}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>{{ devpi_venv }}/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ devpi_log_dir }}/mcquack.devpi.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ devpi_log_dir }}/mcquack.devpi.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.forgejo</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ forgejo_binary }}</string>
|
||||
<string>-w</string>
|
||||
<string>{{ forgejo_work_path }}</string>
|
||||
<string>-c</string>
|
||||
<string>{{ forgejo_config_path }}</string>
|
||||
<string>web</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ forgejo_log_dir }}/mcquack.forgejo.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ forgejo_log_dir }}/mcquack.forgejo.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.heph</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ heph_binary }}</string>
|
||||
<string>--mode</string>
|
||||
<string>server</string>
|
||||
<string>--http-addr</string>
|
||||
<string>{{ heph_http_addr }}</string>
|
||||
<string>--db</string>
|
||||
<string>{{ heph_db }}</string>
|
||||
<string>--socket</string>
|
||||
<string>{{ heph_socket }}</string>
|
||||
<string>--web-root</string>
|
||||
<string>{{ heph_web_root }}</string>
|
||||
<string>--oidc-issuer</string>
|
||||
<string>{{ heph_oidc_issuer }}</string>
|
||||
<string>--oidc-audience</string>
|
||||
<string>{{ heph_oidc_audience }}</string>
|
||||
<string>--self-update</string>
|
||||
<string>--self-update-interval-secs</string>
|
||||
<string>{{ heph_self_update_interval_secs }}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<!-- cargo + toolchain on PATH so --self-update can run `cargo install`. -->
|
||||
<key>PATH</key>
|
||||
<string>{{ heph_bin_dir }}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<key>HOME</key>
|
||||
<string>/Users/erichblume</string>
|
||||
<!-- Pin the rustup channel: the launchagent runs without mise, so a bare
|
||||
cargo shim would otherwise use rustup's (stale) default toolchain. -->
|
||||
<key>RUSTUP_TOOLCHAIN</key>
|
||||
<string>{{ heph_rust_toolchain }}</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ heph_log_dir }}/mcquack.heph.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ heph_log_dir }}/mcquack.heph.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: prowler
|
||||
name: cv
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/prowler
|
||||
path: argocd/manifests/cv
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: prowler
|
||||
namespace: cv
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -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
|
||||
29
argocd/apps/devpi.yaml
Normal file
29
argocd/apps/devpi.yaml
Normal file
|
|
@ -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 <password>
|
||||
# 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
|
||||
18
argocd/apps/docs.yaml
Normal file
18
argocd/apps/docs.yaml
Normal file
|
|
@ -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
|
||||
24
argocd/apps/external-secrets-config-ringtail.yaml
Normal file
24
argocd/apps/external-secrets-config-ringtail.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# External Secrets Configuration for ringtail k3s cluster
|
||||
# Same ClusterSecretStore manifests as indri, different destination
|
||||
#
|
||||
# Prerequisites:
|
||||
# - 1password-connect-ringtail is deployed and healthy
|
||||
# - external-secrets-ringtail operator is deployed and CRDs are installed
|
||||
#
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: external-secrets-config-ringtail
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/external-secrets
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: external-secrets
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
26
argocd/apps/external-secrets-config.yaml
Normal file
26
argocd/apps/external-secrets-config.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# External Secrets Configuration - ClusterSecretStore for 1Password
|
||||
#
|
||||
# Deploys the ClusterSecretStore that connects ESO to 1Password Connect.
|
||||
# Must be synced AFTER external-secrets operator is running.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - 1password-connect is deployed and healthy
|
||||
# - external-secrets operator is deployed and CRDs are installed
|
||||
#
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: external-secrets-config
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/external-secrets
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: external-secrets
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -12,7 +12,7 @@ spec:
|
|||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/external-secrets.git
|
||||
targetRevision: helm-chart-2.2.0
|
||||
targetRevision: helm-chart-2.0.0
|
||||
path: config/crds/bases
|
||||
directory:
|
||||
exclude: 'kustomization.yaml'
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ spec:
|
|||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/external-secrets.git
|
||||
targetRevision: helm-chart-2.2.0
|
||||
targetRevision: helm-chart-2.0.0
|
||||
path: config/crds/bases
|
||||
directory:
|
||||
exclude: 'kustomization.yaml'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# External Secrets Operator for ringtail k3s cluster
|
||||
# Same manifests as indri, different destination
|
||||
# Same chart/values as indri, different destination
|
||||
#
|
||||
# Prerequisites:
|
||||
# - 1password-connect-ringtail must be deployed and healthy
|
||||
|
|
@ -12,10 +12,17 @@ metadata:
|
|||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
path: argocd/manifests/external-secrets-ringtail
|
||||
sources:
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/external-secrets.git
|
||||
targetRevision: helm-chart-2.0.0
|
||||
path: deploy/charts/external-secrets
|
||||
helm:
|
||||
releaseName: external-secrets
|
||||
valueFiles:
|
||||
- $values/argocd/manifests/external-secrets/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: external-secrets
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
# External Secrets Operator - Kubernetes secret sync from external providers
|
||||
# Syncs secrets from 1Password Connect to native Kubernetes Secrets
|
||||
#
|
||||
# Static manifests rendered from upstream Helm chart v2.2.0
|
||||
# Upstream: https://github.com/external-secrets/external-secrets
|
||||
# Chart mirrored from https://github.com/external-secrets/external-secrets
|
||||
#
|
||||
# Prerequisites:
|
||||
# - 1password-connect must be deployed and healthy
|
||||
# - external-secrets-crds must be synced first
|
||||
#
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
|
|
@ -15,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/external-secrets
|
||||
sources:
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/mirrors/external-secrets.git
|
||||
targetRevision: helm-chart-2.0.0
|
||||
path: deploy/charts/external-secrets
|
||||
helm:
|
||||
releaseName: external-secrets
|
||||
valueFiles:
|
||||
- $values/argocd/manifests/external-secrets/values.yaml
|
||||
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: external-secrets
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
25
argocd/apps/immich-storage.yaml
Normal file
25
argocd/apps/immich-storage.yaml
Normal file
|
|
@ -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
|
||||
37
argocd/apps/immich.yaml
Normal file
37
argocd/apps/immich.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: kingfisher
|
||||
name: jobsync
|
||||
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/jobsync
|
||||
destination:
|
||||
server: https://ringtail.tail8d86e.ts.net:6443
|
||||
namespace: kingfisher
|
||||
namespace: jobsync
|
||||
syncPolicy:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -11,7 +11,8 @@ spec:
|
|||
project: default
|
||||
# Tailscale operator mutates externalName from "placeholder" to actual proxy service
|
||||
ignoreDifferences:
|
||||
- kind: Service
|
||||
- group: ""
|
||||
kind: Service
|
||||
jsonPointers:
|
||||
- /spec/externalName
|
||||
source:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ spec:
|
|||
project: default
|
||||
# Tailscale operator mutates externalName from "placeholder" to actual proxy service
|
||||
ignoreDifferences:
|
||||
- kind: Service
|
||||
- group: ""
|
||||
kind: Service
|
||||
jsonPointers:
|
||||
- /spec/externalName
|
||||
source:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
32
argocd/apps/teslamate.yaml
Normal file
32
argocd/apps/teslamate.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
33
argocd/manifests/1password-connect/values.yaml
Normal file
33
argocd/manifests/1password-connect/values.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
@ -171,38 +169,6 @@ prometheus.exporter.blackbox "services" {
|
|||
address = "http://argocd-server.argocd.svc.cluster.local:80/healthz"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
target {
|
||||
name = "prometheus"
|
||||
address = "http://prometheus.monitoring.svc.cluster.local:9090/-/healthy"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
target {
|
||||
name = "loki"
|
||||
address = "http://loki.monitoring.svc.cluster.local:3100/ready"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
target {
|
||||
name = "grafana"
|
||||
address = "http://grafana.monitoring.svc.cluster.local:80/api/health"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
target {
|
||||
// Migrated to ringtail (wave-1); probe through Caddy over Tailscale.
|
||||
name = "teslamate"
|
||||
address = "https://tesla.ops.eblu.me/"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
target {
|
||||
name = "navidrome"
|
||||
address = "http://navidrome.navidrome.svc.cluster.local:4533/"
|
||||
module = "http_2xx"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Scrape blackbox probe results
|
||||
|
|
|
|||
|
|
@ -17,11 +17,9 @@ spec:
|
|||
serviceAccountName: alloy
|
||||
securityContext:
|
||||
fsGroup: 473 # alloy user group
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: alloy
|
||||
image: registry.ops.eblu.me/blumeops/alloy:kustomized
|
||||
image: grafana/alloy:kustomized
|
||||
args:
|
||||
- run
|
||||
- --server.http.listen-addr=0.0.0.0:12345
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ resources:
|
|||
- daemonset.yaml
|
||||
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/alloy
|
||||
newTag: v1.16.0-9564435
|
||||
- name: grafana/alloy
|
||||
newTag: v1.14.0
|
||||
|
||||
configMapGenerator:
|
||||
- name: alloy-config
|
||||
|
|
|
|||
|
|
@ -27,16 +27,6 @@ prometheus.relabel "instance" {
|
|||
}
|
||||
}
|
||||
|
||||
// ============== SNOWFLAKE PROXY METRICS ==============
|
||||
|
||||
// Scrape Tor Snowflake proxy metrics from host (systemd service on port 9999)
|
||||
prometheus.scrape "snowflake_proxy" {
|
||||
targets = [{"__address__" = coalesce(sys.env("HOST_IP"), "localhost") + ":9999", "job" = "snowflake_proxy"}]
|
||||
metrics_path = "/internal/metrics"
|
||||
scrape_interval = "30s"
|
||||
forward_to = [prometheus.relabel.instance.receiver]
|
||||
}
|
||||
|
||||
// ============== KUBE-STATE-METRICS SCRAPE ==============
|
||||
|
||||
prometheus.scrape "kube_state_metrics" {
|
||||
|
|
@ -45,26 +35,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" }
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ spec:
|
|||
fsGroup: 473 # alloy user group
|
||||
containers:
|
||||
- name: alloy
|
||||
image: registry.ops.eblu.me/blumeops/alloy:kustomized
|
||||
image: grafana/alloy:kustomized
|
||||
args:
|
||||
- run
|
||||
- --server.http.listen-addr=0.0.0.0:12345
|
||||
|
|
@ -33,10 +33,6 @@ spec:
|
|||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: HOST_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.hostIP
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ resources:
|
|||
- daemonset.yaml
|
||||
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/alloy
|
||||
newTag: v1.16.0-9564435-nix
|
||||
- name: grafana/alloy
|
||||
newTag: v1.14.0
|
||||
|
||||
configMapGenerator:
|
||||
- name: alloy-config
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ spec:
|
|||
hostPID: true
|
||||
containers:
|
||||
- name: alloy
|
||||
image: registry.ops.eblu.me/blumeops/alloy:kustomized
|
||||
image: grafana/alloy:kustomized
|
||||
args:
|
||||
- run
|
||||
- --server.http.listen-addr=0.0.0.0:12346
|
||||
|
|
@ -46,7 +46,6 @@ spec:
|
|||
mountPath: /var/lib/alloy/data
|
||||
securityContext:
|
||||
privileged: true
|
||||
runAsUser: 0
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ resources:
|
|||
- daemonset.yaml
|
||||
|
||||
images:
|
||||
- name: registry.ops.eblu.me/blumeops/alloy
|
||||
newTag: v1.16.0-9564435-nix
|
||||
- name: grafana/alloy
|
||||
newTag: v1.14.0
|
||||
|
||||
configMapGenerator:
|
||||
- name: alloy-tracing-config
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
31
argocd/manifests/argocd/external-secret-oidc-authentik.yaml
Normal file
31
argocd/manifests/argocd/external-secret-oidc-authentik.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
@ -346,181 +345,3 @@ data:
|
|||
provider: !KeyOf jellyfin-provider
|
||||
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:
|
||||
name: BlumeOps Mealie SSO
|
||||
labels:
|
||||
blueprints.goauthentik.io/description: "Mealie OIDC provider and application"
|
||||
entries:
|
||||
# OAuth2 provider for Mealie (confidential — Mealie requires client_secret)
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
id: mealie-provider
|
||||
identifiers:
|
||||
name: Mealie
|
||||
attrs:
|
||||
name: Mealie
|
||||
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: mealie
|
||||
client_secret: !Env AUTHENTIK_MEALIE_CLIENT_SECRET
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: https://meals.ops.eblu.me/login
|
||||
- matching_mode: strict
|
||||
url: https://meals.tail8d86e.ts.net/login
|
||||
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
|
||||
|
||||
# Mealie application — all authenticated users allowed (admin mapped via OIDC_ADMIN_GROUP)
|
||||
- model: authentik_core.application
|
||||
id: mealie-app
|
||||
identifiers:
|
||||
slug: mealie
|
||||
attrs:
|
||||
name: Mealie
|
||||
slug: mealie
|
||||
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
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@ spec:
|
|||
key: postgresql-password
|
||||
- name: AUTHENTIK_REDIS__HOST
|
||||
value: authentik-redis
|
||||
- name: AUTHENTIK_WORKER_CONCURRENCY
|
||||
value: "2"
|
||||
- name: AUTHENTIK_GRAFANA_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
|
@ -75,26 +73,21 @@ spec:
|
|||
secretKeyRef:
|
||||
name: authentik-config
|
||||
key: jellyfin-client-secret
|
||||
- name: AUTHENTIK_MEALIE_CLIENT_SECRET
|
||||
- name: AUTHENTIK_ARGOCD_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
|
||||
key: argocd-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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue