From 403192a872ba9acb6a19b6d766c95571cb208d89 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 22:20:59 +0000 Subject: [PATCH 01/10] feat: rewrite API from Python to Go with Huma v2 framework Co-Authored-By: Claude Opus 4.6 (1M context) --- .ai/AGENTS.md | 54 +- .dockerignore | 6 + .github/workflows/cli_release.yaml | 2 + .github/workflows/container_image.yaml | 30 +- Dockerfile | 37 +- Pipfile | 16 - Pipfile.lock | 1680 --------------- app/main.py | 164 -- app/schemas/schemas.py | 56 - .../captain_manifests/appproject.yaml.j2 | 48 - .../captain_manifests/appset.yaml.j2 | 54 - .../captain_manifests/namespace.yaml.j2 | 6 - app/util/__init__.py | 0 .../aws_setup_test_account_credentials.py | 132 -- app/util/captain_manifests.py | 48 - app/util/chisel.py | 51 - app/util/github.py | 138 -- app/util/hetzner.py | 154 -- app/util/storage.py | 263 --- cli/.ai/AGENTS.md | 31 +- cli/Makefile | 13 +- cli/api/generated.go | 1852 ----------------- cli/cmd/aws.go | 33 +- cli/cmd/captain_manifests.go | 26 +- cli/cmd/chisel.go | 33 +- cli/cmd/client.go | 51 +- cli/cmd/github.go | 39 +- cli/cmd/nuke.go | 18 +- cli/cmd/opsgenie.go | 22 +- cli/cmd/storage_buckets.go | 13 +- cli/go.mod | 7 +- cli/go.sum | 21 - cli/internal/spec/openapi.json | 1027 +++++---- cli/internal/spec/spec.go | 24 +- cli/oapi-codegen.yaml | 5 - cli/openapi.json | 686 ------ cmd/server/main.go | 260 +++ devbox.json | 16 - devbox.lock | 126 -- e2e/e2e_test.go | 1132 ++++++++++ go.mod | 47 + go.sum | 100 + internal/version/version.go | 10 + pkg/aws/aws.go | 241 +++ pkg/captain/captain.go | 139 ++ pkg/captain/captain_test.go | 74 + pkg/chisel/chisel.go | 88 + pkg/chisel/chisel_test.go | 112 + pkg/github/github.go | 294 +++ pkg/github/github_test.go | 94 + pkg/handlers/aws.go | 19 + pkg/handlers/captain.go | 20 + pkg/handlers/chisel.go | 29 + pkg/handlers/github.go | 67 + pkg/handlers/health.go | 17 + pkg/handlers/opsgenie.go | 16 + pkg/handlers/storage.go | 20 + pkg/handlers/version.go | 22 + pkg/hetzner/hetzner.go | 175 ++ .../opsgenie.py => pkg/opsgenie/opsgenie.go | 27 +- pkg/storage/storage.go | 233 +++ pkg/storage/storage_test.go | 98 + pkg/types/types.go | 145 ++ pkg/util/plaintext.go | 40 + 64 files changed, 4302 insertions(+), 6199 deletions(-) create mode 100644 .dockerignore delete mode 100644 Pipfile delete mode 100644 Pipfile.lock delete mode 100644 app/main.py delete mode 100644 app/schemas/schemas.py delete mode 100644 app/templates/captain_manifests/appproject.yaml.j2 delete mode 100644 app/templates/captain_manifests/appset.yaml.j2 delete mode 100644 app/templates/captain_manifests/namespace.yaml.j2 delete mode 100644 app/util/__init__.py delete mode 100644 app/util/aws_setup_test_account_credentials.py delete mode 100644 app/util/captain_manifests.py delete mode 100644 app/util/chisel.py delete mode 100644 app/util/github.py delete mode 100644 app/util/hetzner.py delete mode 100644 app/util/storage.py delete mode 100644 cli/api/generated.go delete mode 100644 cli/oapi-codegen.yaml delete mode 100644 cli/openapi.json create mode 100644 cmd/server/main.go delete mode 100644 devbox.json delete mode 100644 devbox.lock create mode 100644 e2e/e2e_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/version/version.go create mode 100644 pkg/aws/aws.go create mode 100644 pkg/captain/captain.go create mode 100644 pkg/captain/captain_test.go create mode 100644 pkg/chisel/chisel.go create mode 100644 pkg/chisel/chisel_test.go create mode 100644 pkg/github/github.go create mode 100644 pkg/github/github_test.go create mode 100644 pkg/handlers/aws.go create mode 100644 pkg/handlers/captain.go create mode 100644 pkg/handlers/chisel.go create mode 100644 pkg/handlers/github.go create mode 100644 pkg/handlers/health.go create mode 100644 pkg/handlers/opsgenie.go create mode 100644 pkg/handlers/storage.go create mode 100644 pkg/handlers/version.go create mode 100644 pkg/hetzner/hetzner.go rename app/util/opsgenie.py => pkg/opsgenie/opsgenie.go (78%) create mode 100644 pkg/storage/storage.go create mode 100644 pkg/storage/storage_test.go create mode 100644 pkg/types/types.go create mode 100644 pkg/util/plaintext.go diff --git a/.ai/AGENTS.md b/.ai/AGENTS.md index ee95be1..c28dbc9 100644 --- a/.ai/AGENTS.md +++ b/.ai/AGENTS.md @@ -4,27 +4,29 @@ This file provides guidance to AI coding assistants when working with code in th ## Project Overview -tools-api is a FastAPI service providing internal REST APIs for GlueOps platform engineers. It manages AWS accounts, cloud storage (MinIO), Hetzner infrastructure (Chisel load balancers), GitHub organization setup, Kubernetes/ArgoCD manifest generation, and Opsgenie alerting. +tools-api is a Go API service (using the Huma framework on Chi router) providing internal REST APIs for GlueOps platform engineers. It manages AWS accounts, cloud storage (MinIO), Hetzner infrastructure (Chisel load balancers), GitHub organization setup, Kubernetes/ArgoCD manifest generation, and Opsgenie alerting. A companion Go CLI (`cli/`) allows engineers to interact with the API from headless Linux machines. See [`cli/.ai/AGENTS.md`](../cli/.ai/AGENTS.md) for CLI-specific guidance. ## Development Setup -```bash -# Enter development shell (launches pipenv shell via devbox) -devbox run dev +All builds use Docker — no local Go toolchain is required. -# Install dependencies -pipenv install +```bash +# Build the server +docker build -t tools-api . -# Run dev server (hot reload) -fastapi dev +# Run the server +docker run --rm -p 8000:8000 tools-api -# Run production server -fastapi run +# Run Go commands via Docker +docker run --rm -v "$(pwd):/app" -w /app golang:1.24-alpine go build ./... +docker run --rm -v "$(pwd):/app" -w /app golang:1.24-alpine go test ./... ``` -Required environment variables: `AWS_GLUEOPS_ROCKS_ORG_ACCESS_KEY`, `AWS_GLUEOPS_ROCKS_ORG_SECRET_KEY`, `HCLOUD_TOKEN`, `GITHUB_TOKEN`, `MINIO_S3_ACCESS_KEY_ID`, `MINIO_S3_SECRET_KEY`, `HETZNER_STORAGE_REGION=hel1`. +Required environment variables: `AWS_GLUEOPS_ROCKS_ORG_ACCESS_KEY`, `AWS_GLUEOPS_ROCKS_ORG_SECRET_KEY`, `HCLOUD_TOKEN`, `GITHUB_TOKEN`, `MINIO_S3_ACCESS_KEY_ID`, `MINIO_S3_SECRET_KEY`, `HETZNER_STORAGE_REGION`. Optional: `LOG_LEVEL` (default `INFO`). + +Environment variables are NOT required at startup — the app fails lazily when an endpoint is called without the needed env var. ## Build @@ -32,28 +34,34 @@ Required environment variables: `AWS_GLUEOPS_ROCKS_ORG_ACCESS_KEY`, `AWS_GLUEOPS docker build -t tools-api . ``` -The Dockerfile uses `python:3.14-slim` as base, installs dependencies via pipenv (`--system`), and accepts build args: `VERSION`, `COMMIT_SHA`, `SHORT_SHA`, `BUILD_TIMESTAMP`, `GIT_REF`. Devbox is used for local development only (Python 3.13 via nixpkgs), not in container builds. +The Dockerfile uses a multi-stage build: `golang:1.24-alpine` for compilation, `alpine:3.21` for runtime. Build args: `VERSION`, `COMMIT_SHA`, `SHORT_SHA`, `BUILD_TIMESTAMP`, `GIT_REF` (injected via ldflags into `internal/version`). ## Architecture -- **`app/main.py`** — FastAPI app entry point. Defines all API routes, global exception handler, health/version endpoints. Routes redirect `/` to `/docs`. -- **`app/schemas/schemas.py`** — Pydantic request/response models for all endpoints (including `VersionResponse` for `/version`). Examples and descriptions defined here are the single source of truth — the CLI reads them from the embedded OpenAPI spec at compile time. -- **`app/util/`** — Business logic modules, one per domain: `storage.py` (MinIO), `github.py`, `hetzner.py`, `aws_setup_test_account_credentials.py`, `chisel.py`, `captain_manifests.py`, `opsgenie.py`. -- **`app/templates/captain_manifests/`** — Jinja2 templates (`.yaml.j2`) for generating Kubernetes manifests (Namespace, AppProject, ApplicationSet). +- **`cmd/server/main.go`** — Go API server entry point. Uses Huma framework on Chi router. Defines all API routes, audit logging middleware, custom error handling, graceful shutdown (SIGTERM/SIGINT). Health endpoint (`/health`) and root redirect (`/ → /docs`) are registered directly on Chi (excluded from OpenAPI). +- **`pkg/handlers/`** — HTTP handler functions for each domain: `storage.go`, `aws.go`, `github.go`, `chisel.go`, `opsgenie.go`, `captain.go`, `health.go`, `version.go`. +- **`pkg/types/`** — Shared request/response type definitions (`types.go`). These are the single source of truth for API contracts. +- **`pkg/`** — Business logic modules, one per domain: `storage/`, `aws/`, `github/`, `hetzner/`, `chisel/`, `captain/`, `opsgenie/`. +- **`pkg/util/`** — Utility functions (e.g., `plaintext.go` for plain-text response helpers). +- **`internal/version/`** — Build-time injected version variables (ldflags). - **`cli/`** — Go CLI binary. See [`cli/.ai/AGENTS.md`](../cli/.ai/AGENTS.md). -All routes are defined directly in `main.py` (no router separation). Each route delegates to a corresponding util module. +### Key Design Decisions -GitHub workflow endpoints (`github.py`) dispatch workflows via the GitHub API and poll for the resulting run ID. They return JSON with `status_code`, `all_jobs_url`, `run_id`, and `run_url`. A separate `/v1/github/workflow-run-status` endpoint accepts any GitHub Actions run URL and returns its current status. All GitHub API calls use a centralized `_get_headers()` with the `X-GitHub-Api-Version` header. +- **Plain-text endpoints** — Five endpoints return `Content-Type: text/plain` (storage buckets, AWS credentials, chisel, opsgenie manifest, captain manifests). These use custom Huma response handling to avoid JSON wrapping. +- **Error responses** — Custom error format `{"status": N, "detail": "..."}` via `huma.NewError` override. Stack traces logged server-side only, never in responses. +- **Graceful shutdown** — Handles SIGTERM/SIGINT with 25-second timeout for in-flight requests. +- **Audit logging** — Middleware logs every request with `X-Forwarded-User` and `X-Forwarded-Email` from oauth2-proxy. ## Key Dependencies -- **`glueops-helpers`** — Internal library (installed from GitHub) providing `setup_logging` and shared utilities. -- **`minio`** — S3-compatible storage client. -- **`boto3`** — AWS SDK (account credential management via STS/Organizations). -- **`hcloud`** — Hetzner Cloud API client (Chisel node provisioning). +- **`github.com/danielgtaylor/huma/v2`** — API framework (OpenAPI 3.1, validation, docs). +- **`github.com/go-chi/chi/v5`** — HTTP router. +- **`github.com/aws/aws-sdk-go-v2`** — AWS SDK (account credential management via STS/Organizations). +- **`github.com/hetznercloud/hcloud-go/v2`** — Hetzner Cloud API client. +- **`github.com/minio/minio-go/v7`** — S3-compatible storage client. ## CI/CD -- **`.github/workflows/container_image.yaml`** — Builds and pushes Docker images to GHCR on any push. +- **`.github/workflows/container_image.yaml`** — Runs golangci-lint and govulncheck, then builds and pushes Docker images to GHCR on any push. - **`.github/workflows/cli_release.yaml`** — Builds CLI binaries on every push, uploads as workflow artifacts, and creates a GitHub Release tagged with `github.ref_name`. Cross-compiles for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7cae168 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git/ +cli/ +tickets/ +*.md +.ai/ +deployment-configurations/ diff --git a/.github/workflows/cli_release.yaml b/.github/workflows/cli_release.yaml index 473c7d6..43242c3 100644 --- a/.github/workflows/cli_release.yaml +++ b/.github/workflows/cli_release.yaml @@ -42,11 +42,13 @@ jobs: cli/tools-darwin-arm64 - name: Delete existing release for this ref + if: startsWith(github.ref, 'refs/tags/v') run: gh release delete "${{ github.ref_name }}" --yes 2>/dev/null || true env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@1853d73993c8ca1b2c9c1a7fede39682d0ab5c2a # v2.5.3 with: tag_name: ${{ github.ref_name }} diff --git a/.github/workflows/container_image.yaml b/.github/workflows/container_image.yaml index 32ca6a2..ac8d098 100644 --- a/.github/workflows/container_image.yaml +++ b/.github/workflows/container_image.yaml @@ -1,5 +1,5 @@ name: Publish to GHCR.io - + on: push: workflow_dispatch: @@ -9,7 +9,29 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: + lint-and-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version-file: go.mod + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8 + with: + version: latest + + - name: Run govulncheck + uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1 + with: + go-version-file: go.mod + build_tag_push_to_ghcr: + needs: lint-and-scan runs-on: ubuntu-latest permissions: contents: read @@ -20,10 +42,6 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4 - - name: Setup Docker buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 @@ -81,4 +99,4 @@ jobs: exclude-tags: latest,main,v* keep-n-tagged: 5 delete-untagged: true - delete-partial-images: true \ No newline at end of file + delete-partial-images: true diff --git a/Dockerfile b/Dockerfile index fd9ae30..5cc3996 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,28 @@ -FROM python:3.14-slim +# Build stage +FROM golang:1.24-alpine AS builder -WORKDIR /code - -# Accept build arguments for versioning ARG VERSION=UNKNOWN ARG COMMIT_SHA=UNKNOWN ARG SHORT_SHA=UNKNOWN ARG BUILD_TIMESTAMP=UNKNOWN ARG GIT_REF=UNKNOWN -ENV VERSION=${VERSION} -ENV COMMIT_SHA=${COMMIT_SHA} -ENV SHORT_SHA=${SHORT_SHA} -ENV BUILD_TIMESTAMP=${BUILD_TIMESTAMP} -ENV GIT_REF=${GIT_REF} - -RUN pip install --no-cache-dir pipenv - -COPY Pipfile Pipfile.lock ./ -RUN pipenv install --system - -COPY app/ /code/app +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build \ + -ldflags="-s -w \ + -X github.com/GlueOps/tools-api/internal/version.Version=${VERSION} \ + -X github.com/GlueOps/tools-api/internal/version.CommitSHA=${COMMIT_SHA} \ + -X github.com/GlueOps/tools-api/internal/version.ShortSHA=${SHORT_SHA} \ + -X github.com/GlueOps/tools-api/internal/version.BuildTimestamp=${BUILD_TIMESTAMP} \ + -X github.com/GlueOps/tools-api/internal/version.GitRef=${GIT_REF}" \ + -o /server ./cmd/server -CMD ["fastapi", "run"] +# Runtime stage +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates +COPY --from=builder /server /server +EXPOSE 8000 +ENTRYPOINT ["/server"] diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 18ec3bf..0000000 --- a/Pipfile +++ /dev/null @@ -1,16 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -fastapi = {extras = ["standard"], version = "*"} -glueops-helpers = {file = "https://github.com/GlueOps/python-glueops-helpers-library/archive/refs/tags/v0.6.0.zip"} -minio = "*" -boto3 = "*" -hcloud = "*" - -[dev-packages] - -[requires] -python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 33a4e1b..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1680 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "93d6d56975691a8b6efe5d111254e8edb68be37a0f1fe7a46a67321e0cc58d3a" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.13" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "annotated-doc": { - "hashes": [ - "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", - "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" - ], - "markers": "python_version >= '3.8'", - "version": "==0.0.4" - }, - "annotated-types": { - "hashes": [ - "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", - "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" - ], - "markers": "python_version >= '3.8'", - "version": "==0.7.0" - }, - "anyio": { - "hashes": [ - "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", - "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c" - ], - "markers": "python_version >= '3.9'", - "version": "==4.12.1" - }, - "argon2-cffi": { - "hashes": [ - "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", - "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741" - ], - "markers": "python_version >= '3.8'", - "version": "==25.1.0" - }, - "argon2-cffi-bindings": { - "hashes": [ - "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", - "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", - "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d", - "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", - "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", - "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", - "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", - "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690", - "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584", - "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e", - "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", - "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", - "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", - "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", - "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", - "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", - "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", - "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", - "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", - "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", - "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", - "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", - "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", - "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", - "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", - "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb" - ], - "markers": "python_version >= '3.9'", - "version": "==25.1.0" - }, - "boto3": { - "hashes": [ - "sha256:3f349f967ab38c23425626d130962bcb363e75f042734fe856ea8c5a00eef03c", - "sha256:dbff353eb7dc93cbddd7926ed24793e0174c04adbe88860dfa639568442e4962" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.42.68" - }, - "botocore": { - "hashes": [ - "sha256:3951c69e12ac871dda245f48dac5c7dd88ea1bfdd74a8879ec356cf2874b806a", - "sha256:9df7da26374601f890e2f115bfa573d65bf15b25fe136bb3aac809f6145f52ab" - ], - "markers": "python_version >= '3.9'", - "version": "==1.42.68" - }, - "certifi": { - "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" - ], - "markers": "python_version >= '3.7'", - "version": "==2026.2.25" - }, - "cffi": { - "hashes": [ - "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", - "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", - "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", - "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", - "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", - "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", - "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", - "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", - "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", - "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", - "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", - "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", - "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", - "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", - "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", - "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", - "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", - "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", - "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", - "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", - "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", - "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", - "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", - "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", - "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", - "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", - "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", - "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", - "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", - "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", - "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", - "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", - "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", - "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", - "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", - "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", - "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", - "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", - "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", - "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", - "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", - "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", - "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", - "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", - "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", - "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", - "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", - "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", - "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", - "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", - "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", - "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", - "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", - "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", - "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", - "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", - "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", - "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", - "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", - "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", - "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", - "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", - "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", - "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", - "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", - "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", - "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", - "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", - "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", - "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", - "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", - "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", - "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", - "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", - "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", - "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", - "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", - "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", - "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", - "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", - "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", - "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", - "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", - "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4", - "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", - "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", - "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", - "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", - "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", - "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", - "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", - "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", - "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", - "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", - "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", - "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", - "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2", - "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe", - "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", - "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", - "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", - "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", - "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", - "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf", - "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139", - "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770", - "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", - "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918", - "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", - "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7", - "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", - "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", - "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", - "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", - "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", - "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", - "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659", - "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", - "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9", - "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9", - "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", - "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d", - "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", - "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", - "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", - "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", - "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99", - "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", - "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", - "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", - "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca", - "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c", - "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c", - "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", - "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", - "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", - "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", - "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", - "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", - "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", - "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", - "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a", - "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e", - "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", - "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123", - "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", - "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc", - "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", - "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", - "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", - "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", - "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", - "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", - "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", - "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", - "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294", - "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22", - "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", - "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8", - "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2", - "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", - "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242", - "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", - "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", - "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", - "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", - "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", - "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", - "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", - "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", - "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", - "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", - "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", - "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969", - "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", - "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", - "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", - "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", - "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", - "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", - "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", - "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193", - "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", - "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", - "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95", - "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", - "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", - "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98", - "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", - "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", - "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", - "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", - "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", - "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", - "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947", - "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.5" - }, - "click": { - "hashes": [ - "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", - "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" - ], - "markers": "python_version >= '3.10'", - "version": "==8.3.1" - }, - "cryptography": { - "hashes": [ - "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", - "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", - "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", - "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", - "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", - "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", - "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", - "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", - "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", - "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", - "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", - "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", - "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", - "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", - "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", - "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", - "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", - "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", - "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", - "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", - "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", - "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", - "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", - "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", - "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", - "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", - "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", - "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", - "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", - "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", - "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", - "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", - "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", - "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", - "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", - "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", - "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", - "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", - "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", - "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", - "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", - "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", - "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", - "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", - "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", - "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", - "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", - "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", - "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87" - ], - "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.5" - }, - "dnspython": { - "hashes": [ - "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", - "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f" - ], - "markers": "python_version >= '3.10'", - "version": "==2.8.0" - }, - "durationpy": { - "hashes": [ - "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", - "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286" - ], - "version": "==0.10" - }, - "email-validator": { - "hashes": [ - "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", - "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426" - ], - "markers": "python_version >= '3.8'", - "version": "==2.3.0" - }, - "fastapi": { - "extras": [ - "standard" - ], - "hashes": [ - "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", - "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd" - ], - "markers": "python_version >= '3.10'", - "version": "==0.135.1" - }, - "fastapi-cli": { - "extras": [ - "standard" - ], - "hashes": [ - "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", - "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc" - ], - "markers": "python_version >= '3.10'", - "version": "==0.0.24" - }, - "fastapi-cloud-cli": { - "hashes": [ - "sha256:9ffcf90bd713747efa65447620d29cfbb7b3f7de38d97467952ca6346e418d70", - "sha256:d02515231f3f505f7669c20920343934570a88a08af9f9a6463ca2807f27ffe5" - ], - "markers": "python_version >= '3.10'", - "version": "==0.15.0" - }, - "fastar": { - "hashes": [ - "sha256:003b59a7c3e405b6a7bff8fab17d31e0ccbc7f06730a8f8ca1694eeea75f3c76", - "sha256:01084cb75f13ca6a8e80bd41584322523189f8e81b472053743d6e6c3062b5a6", - "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", - "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", - "sha256:07b70f712d20622346531a4b46bb332569bea621f61314c0b7e80903a16d14cf", - "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", - "sha256:0a78e5221b94a80800930b7fd0d0e797ae73aadf7044c05ed46cb9bdf870f022", - "sha256:0afbb92f78bf29d5e9db76fb46cbabc429e49015cddf72ab9e761afbe88ac100", - "sha256:0d2fdd1c987ff2300bdf39baed556f8e155f8577018775e794a268ecf1707610", - "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", - "sha256:15e3dfaa769d2117ef707e5f47c62126d1b63f8e9c85133112f33f1fbdf8942f", - "sha256:175db2a98d67ced106468e8987975484f8bbbd5ad99201da823b38bafb565ed5", - "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", - "sha256:1aa7dbde2d2d73eb5b6203d0f74875cb66350f0f1b4325b4839fc8fbbf5d074e", - "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", - "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", - "sha256:1ccc1610c05183c0ff82fe93cdbc4eb0ea8b11f2f6d94f6d31ae342164fc6033", - "sha256:1d90cbf984a39afda27afe08e40c2d8eddc49c5e80590af641610c7b6dc20161", - "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", - "sha256:2127cf2e80ffd49744a160201e0e2f55198af6c028a7b3f750026e0b1f1caa4e", - "sha256:2716309f7326224b9f1341077d8c65ebb26335e5c93c409e1a23be03f1a01c50", - "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", - "sha256:284036bae786a520456ad3f58e72aaf1bd5d74e309132e568343564daa4ae383", - "sha256:2875a077340fe4f8099bd3ed8fa90d9595e1ac3cd62ae19ab690d5bf550eeb35", - "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", - "sha256:299672e1c74d8b73c61684fac9159cfc063d35f4b165996a88facb0e26862cb5", - "sha256:2e88a80b40b7f929a7719a13d7332b4cb1344c5a1ac497044bd24f2adadf04c4", - "sha256:2ff516154e77f4bf78c31a0c11aa78a8a80e11b6964ec6f28982e42ffcbb543c", - "sha256:31cd541231a2456e32104da891cf9962c3b40234d0465cbf9322a6bc8a1b05d5", - "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", - "sha256:330639db3bfba4c6d132421a2a4aeb81e7bea8ce9159cdb6e247fbc5fae97686", - "sha256:3719541a12bb09ab1eae91d2c987a9b2b7d7149c52e7109ba6e15b74aabc49b1", - "sha256:382bfe82c026086487cb17fee12f4c1e2b4e67ce230f2e04487d3e7ddfd69031", - "sha256:3b1208b5453cfe7192e54765f73844b80d684bd8dc6d6acbbb60ead42590b13e", - "sha256:3cb073ab1905127ab6e052a5c7ccd409557ef086571f27de938764d3eaadfe07", - "sha256:3dbca235f0bd804cca6602fe055d3892bebf95fb802e6c6c7d872fb10f7abc6c", - "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", - "sha256:41527617a8b592a29fa874e4dba305874b150601e2bf2e17a9f8099a9d179f28", - "sha256:42ff3052d74684a636423d4f040db88eebd4caf20842fa5f06020e0130c01f69", - "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", - "sha256:4cc9e77019e489f1ddac446b6a5b9dfb5c3d9abd142652c22a1d9415dbcc0e47", - "sha256:4fbe775356930f3aab0ce709fdf8ecf90c10882f5bbdcea215c89a3b14090c50", - "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", - "sha256:50c18788b3c6ffb85e176dcb8548bb8e54616a0519dcdbbfba66f6bbc4316933", - "sha256:5153aa1c194316d0f67b6884a62d122d51fce4196263e92e4bca2a6c47cd44c0", - "sha256:52455794e6cc2b6a6dbf141a1c4312a1a1215d75e8849a35fcff694454da880f", - "sha256:52eda6230799db7bbd44461c622161e9bcd43603399da19b0daab2782e0030b0", - "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", - "sha256:558e8fcf8fe574541df5db14a46cd98bfbed14a811b7014a54f2b714c0cfac42", - "sha256:5793b5db86ff0d588057b9089bf904a9ac288de0323a9973452a011a48ec23eb", - "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", - "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", - "sha256:5aba0942b4f56acdb8fa8aa7cb506f70c1a17bf13dcab318a17ffb467cb2e7ec", - "sha256:5c41111da56430f638cbfc498ebdcc7d30f63416e904b27b7695c29bd4889cb8", - "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", - "sha256:5d98894e4c3a2178f33f695940a615376728f6109f1a3431ac0a9fe98fe84ec7", - "sha256:5f83e60d845091f3a12bc37f412774264d161576eaf810ed8b43567eb934b7e5", - "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", - "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", - "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", - "sha256:63d98b26590d293a9d9a379bae88367a8f3a6137c28819ed6dd6e11aca4a5c6e", - "sha256:64cbde8e0ece3d799090a4727f936f66c5990d3ac59416f3de76a2c676e8e568", - "sha256:661a47ed90762f419406c47e802f46af63a08254ba96abd1c8191e4ce967b665", - "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", - "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", - "sha256:722e54bfdee6c81a0005e147319e93d8797f442308032c92fa28d03ef8fda076", - "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", - "sha256:77d7016f446678d44f1823f40a947db741643fa328142dac6f181046ba205b01", - "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", - "sha256:78f3fe5f45437c66d1dbece5f31aa487e48ef46d76b2082b873d5fa18013ebe1", - "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", - "sha256:7a9b0fff8079b18acdface7ef1b7f522fd9a589f65ca4a1a0dd7c92a0886c2a2", - "sha256:7b8eb42f346024df3800d078fc0763275b1964d5d0762aa831bb0b539b5f1ee3", - "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", - "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", - "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", - "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", - "sha256:82bc445202bbc53f067bb15e3b8639f01fd54d3096a0f9601240690cfd7c9684", - "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", - "sha256:8de5decfa18a03807ae26ba5af095c2c04ac31ae915e9a849363a4495463171f", - "sha256:908d2b9a1ff3d549cc304b32f95706a536da8f0bcb0bc0f9e4c1cce39b80e218", - "sha256:90957a30e64418b02df5b4d525bea50403d98a4b1f29143ce5914ddfa7e54ee4", - "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", - "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", - "sha256:923afc2db5192e56e71952a88e3fe5965c7c9c910d385d2db7573136f064f2fa", - "sha256:92cad46dfbb9969359823c9f61165ec32d5d675d86e863889416e9b64efea95c", - "sha256:98ea7ceb6231e48d7bb0d7dc13e946baa29c7f6873eaf4afb69725d6da349033", - "sha256:997092d31ff451de8d0568f6773f3517cb87dcd0bc76184edb65d7154390a6f8", - "sha256:998e3fa4b555b63eb134e6758437ed739ad1652fdd2a61dfe1dacbfddc35fe66", - "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", - "sha256:9d0bf655ff4c9320b0ca8a5b128063d5093c0c8c1645a2b5f7167143fd8531aa", - "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", - "sha256:a03eaf287bbc93064688a1220580ce261e7557c8898f687f4d0b281c85b28d3c", - "sha256:a17abee1febf5363ed2633f5e13de4be481ba1ab5f77860d39470eccdc4b65af", - "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", - "sha256:a3d3a27066b84d015deab5faee78565509bb33b137896443e4144cb1be1a5f90", - "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", - "sha256:a7b96748425efd9fc155cd920d65088a1b0d754421962418ea73413d02ff515a", - "sha256:a90695a601a78bbca910fdf2efcdf3103c55d0de5a5c6e93556d707bf886250b", - "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", - "sha256:a999263d9f87184bf2801833b2ecf105e03c0dd91cac78685673b70da564fd64", - "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", - "sha256:ac073576c1931959191cb20df38bab21dd152f66c940aa3ca8b22e39f753b2f3", - "sha256:ada877ab1c65197d772ce1b1c2e244d4799680d8b3f136a4308360f3d8661b23", - "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", - "sha256:afbdc2e87b7e56e11ad330859fe17d7a93a76cd637d7f33d1c9edd566d2f58d9", - "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", - "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", - "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", - "sha256:b48abd6056fef7bc3d414aafb453c5b07fdf06d2df5a2841d650288a3aa1e9d3", - "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", - "sha256:b8922754c66699e27d4f1ce07c9c256228054cdc9bb36363e8bb5b503453a6da", - "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", - "sha256:bf440983d4d64582bddf2f0bd3c43ea1db93a8c31cf7c20e473bffaf6d9c0b6d", - "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", - "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", - "sha256:c4f6c82a8ee98c17aa48585ee73b51c89c1b010e5c951af83e07c3436180e3fc", - "sha256:c6129067fcb86276635b5857010f4e9b9c7d5d15dd571bb03c6c1ed73c40fd92", - "sha256:c96abf34135cffb9652360cd827bda19855b803038d932dcd2a686b3d4e7e1ce", - "sha256:c9f930cff014cf79d396d0541bd9f3a3f170c9b5e45d10d634d98f9ed08788c3", - "sha256:ca0db5e563d84b639fe15385eeca940777b6d2f0a1f3bb7cd5b55ab7124f0554", - "sha256:ca639b9909805e44364ea13cca2682b487e74826e4ad75957115ec693228d6b6", - "sha256:cd9c0d3ebf7a0a6f642f771cf41b79f7c98d40a3072a8abe1174fbd9bd615bd3", - "sha256:d5ea223170ee6eb1eaf25ff8193df66a939c891f85a9a33def3add9df2ee1232", - "sha256:d80e4dad8ee2362a71870b1e735800bb5e97f12ebbee4bd0cf15a81ad2428b5a", - "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", - "sha256:d8df22cdd8d58e7689aa89b2e4a07e8e5fa4f88d2d9c2621f0e88a49be97ccea", - "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", - "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", - "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", - "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", - "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", - "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", - "sha256:e6c4d6329da568ec36b1347b0c09c4d27f9dfdeddf9f438ddb16799ecf170098", - "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", - "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", - "sha256:e8a5e6ad722685128521c8fb44cf25bd38669650ba3a4b466b8903e5aa28e1a0", - "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", - "sha256:ef0bcf4385bbdd3c1acecce2d9ea7dab7cc9b8ee0581bbccb7ab11908a7ce288", - "sha256:ef94901537be277f9ec59db939eb817960496c6351afede5b102699b5098604d", - "sha256:f10ef62b6eda6cb6fd9ba8e1fe08a07d7b2bdcc8eaa00eb91566143b92ed7eee", - "sha256:f1d2a54f87e2908cc19e1a6ee249620174fbefc54a219aba1eaa6f31657683c3", - "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", - "sha256:f4eb9560a447ff6a4b377f08b6e5d3a31909a612b028f2c57810ffaf570eceb8", - "sha256:f5f24c6c3628faa3ee51df54d77dbf47c4f77a1951ea4ea14e4ccb855babced5", - "sha256:f6e784a8015623fbb7ccca1af372fd82cb511b408ddd2348dc929fc6e415df73", - "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", - "sha256:f860566b9f3cb1900980f46a4c3f003990c0009c11730f988f758542c17a2364", - "sha256:fb59c7925e7710ad178d9e1a3e65edf295d9a042a0cdcb673b4040949eb8ad0a", - "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", - "sha256:fbc0f2ed0f4add7fb58034c576584d44d7eaaf93dee721dfb26dbed6e222dbac", - "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", - "sha256:feb8f73ad25ad84f986dc53e7c6561b281ee2087500f6e400899c3bf1a3f6dc0", - "sha256:ff85094f10003801339ac4fa9b20a3410c2d8f284d4cba2dc99de6e98c877812" - ], - "markers": "python_version >= '3.8'", - "version": "==0.8.0" - }, - "glueops-helpers": { - "file": "https://github.com/GlueOps/python-glueops-helpers-library/archive/refs/tags/v0.6.0.zip" - }, - "h11": { - "hashes": [ - "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", - "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" - ], - "markers": "python_version >= '3.8'", - "version": "==0.16.0" - }, - "hcloud": { - "hashes": [ - "sha256:3996adef6bc0f5755650bb8b8b024acebab2c29e217dac579c303722fc37ae85", - "sha256:b72096f5a6cd91a962cc1a26d76e4575e25fc337a5b589336f62a7f3b1b7b0b6" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==2.17.0" - }, - "httpcore": { - "hashes": [ - "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", - "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" - ], - "markers": "python_version >= '3.8'", - "version": "==1.0.9" - }, - "httptools": { - "hashes": [ - "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", - "sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad", - "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", - "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", - "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", - "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", - "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", - "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", - "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", - "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", - "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", - "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", - "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", - "sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28", - "sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023", - "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", - "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", - "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", - "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", - "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", - "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", - "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", - "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", - "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", - "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", - "sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4", - "sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517", - "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", - "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", - "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", - "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", - "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", - "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", - "sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf", - "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", - "sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a", - "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", - "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", - "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", - "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", - "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", - "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", - "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362" - ], - "markers": "python_version >= '3.9'", - "version": "==0.7.1" - }, - "httpx": { - "hashes": [ - "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", - "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" - ], - "markers": "python_version >= '3.8'", - "version": "==0.28.1" - }, - "idna": { - "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" - ], - "markers": "python_version >= '3.8'", - "version": "==3.11" - }, - "jinja2": { - "hashes": [ - "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", - "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.6" - }, - "jmespath": { - "hashes": [ - "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", - "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64" - ], - "markers": "python_version >= '3.9'", - "version": "==1.1.0" - }, - "kubernetes": { - "hashes": [ - "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", - "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee" - ], - "markers": "python_version >= '3.6'", - "version": "==35.0.0" - }, - "markdown-it-py": { - "hashes": [ - "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", - "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" - ], - "markers": "python_version >= '3.10'", - "version": "==4.0.0" - }, - "markupsafe": { - "hashes": [ - "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", - "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", - "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", - "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", - "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", - "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", - "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", - "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", - "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", - "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", - "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", - "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", - "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", - "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", - "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", - "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", - "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", - "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", - "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", - "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", - "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", - "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", - "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", - "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", - "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", - "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", - "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", - "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", - "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", - "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", - "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", - "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", - "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", - "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", - "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", - "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", - "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", - "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", - "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", - "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", - "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", - "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", - "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", - "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", - "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", - "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", - "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", - "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", - "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", - "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", - "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", - "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", - "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", - "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", - "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", - "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", - "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", - "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", - "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", - "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", - "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", - "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", - "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", - "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", - "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", - "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", - "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", - "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", - "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", - "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", - "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", - "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", - "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", - "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", - "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", - "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", - "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", - "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", - "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", - "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", - "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", - "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", - "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", - "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", - "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", - "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", - "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", - "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", - "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" - ], - "markers": "python_version >= '3.9'", - "version": "==3.0.3" - }, - "mdurl": { - "hashes": [ - "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.1.2" - }, - "minio": { - "hashes": [ - "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", - "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==7.2.20" - }, - "oauthlib": { - "hashes": [ - "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", - "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1" - ], - "markers": "python_version >= '3.8'", - "version": "==3.3.1" - }, - "pycparser": { - "hashes": [ - "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", - "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" - ], - "markers": "python_version >= '3.10'", - "version": "==3.0" - }, - "pycryptodome": { - "hashes": [ - "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", - "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", - "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", - "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", - "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", - "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", - "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56", - "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", - "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", - "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", - "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", - "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", - "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75", - "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720", - "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339", - "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", - "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", - "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8", - "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", - "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818", - "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a", - "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002", - "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", - "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7", - "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", - "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", - "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", - "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566", - "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", - "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", - "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4", - "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", - "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", - "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6", - "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", - "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", - "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", - "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", - "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", - "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be", - "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==3.23.0" - }, - "pydantic": { - "extras": [ - "email" - ], - "hashes": [ - "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", - "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" - ], - "markers": "python_version >= '3.9'", - "version": "==2.12.5" - }, - "pydantic-core": { - "hashes": [ - "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", - "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", - "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", - "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", - "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", - "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", - "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", - "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", - "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", - "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", - "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", - "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", - "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", - "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", - "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", - "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", - "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", - "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", - "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", - "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", - "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", - "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", - "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", - "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", - "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", - "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", - "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", - "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", - "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", - "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", - "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", - "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", - "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", - "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", - "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", - "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", - "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", - "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", - "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", - "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", - "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", - "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", - "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", - "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", - "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", - "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", - "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", - "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", - "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", - "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", - "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", - "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", - "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", - "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", - "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", - "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", - "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", - "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", - "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", - "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", - "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", - "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", - "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", - "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", - "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", - "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", - "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", - "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", - "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", - "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", - "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", - "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", - "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", - "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", - "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", - "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", - "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", - "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", - "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", - "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", - "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", - "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", - "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", - "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", - "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", - "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", - "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", - "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", - "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", - "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", - "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", - "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", - "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", - "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", - "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", - "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", - "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", - "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", - "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", - "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", - "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", - "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", - "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", - "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", - "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", - "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", - "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", - "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", - "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", - "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", - "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", - "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", - "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", - "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", - "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", - "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", - "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", - "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", - "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", - "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", - "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" - ], - "markers": "python_version >= '3.9'", - "version": "==2.41.5" - }, - "pydantic-extra-types": { - "hashes": [ - "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", - "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6" - ], - "markers": "python_version >= '3.9'", - "version": "==2.11.0" - }, - "pydantic-settings": { - "hashes": [ - "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", - "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237" - ], - "markers": "python_version >= '3.10'", - "version": "==2.13.1" - }, - "pygments": { - "hashes": [ - "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", - "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" - ], - "markers": "python_version >= '3.8'", - "version": "==2.19.2" - }, - "python-dateutil": { - "hashes": [ - "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", - "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==2.9.0.post0" - }, - "python-dotenv": { - "hashes": [ - "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", - "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" - ], - "markers": "python_version >= '3.10'", - "version": "==1.2.2" - }, - "python-multipart": { - "hashes": [ - "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", - "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58" - ], - "markers": "python_version >= '3.10'", - "version": "==0.0.22" - }, - "pyyaml": { - "hashes": [ - "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", - "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", - "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", - "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", - "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", - "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", - "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", - "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", - "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", - "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", - "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", - "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", - "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", - "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", - "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", - "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", - "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", - "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", - "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", - "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", - "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", - "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", - "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", - "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", - "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", - "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", - "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", - "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", - "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", - "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", - "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", - "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", - "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", - "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", - "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", - "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", - "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", - "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", - "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", - "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", - "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", - "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", - "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", - "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", - "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", - "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", - "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", - "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", - "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", - "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", - "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", - "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", - "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", - "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", - "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", - "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", - "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", - "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", - "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", - "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", - "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", - "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", - "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", - "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", - "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", - "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", - "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", - "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", - "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", - "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", - "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", - "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", - "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" - ], - "markers": "python_version >= '3.8'", - "version": "==6.0.3" - }, - "requests": { - "hashes": [ - "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", - "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" - ], - "markers": "python_version >= '3.9'", - "version": "==2.32.5" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", - "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" - ], - "markers": "python_version >= '3.4'", - "version": "==2.0.0" - }, - "rich": { - "hashes": [ - "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", - "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==14.3.3" - }, - "rich-toolkit": { - "hashes": [ - "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", - "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e" - ], - "markers": "python_version >= '3.8'", - "version": "==0.19.7" - }, - "rignore": { - "hashes": [ - "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", - "sha256:00f8a59e19d219f44a93af7173de197e0d0e61c386364da20ebe98a303cbe38c", - "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", - "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", - "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", - "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", - "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", - "sha256:101d3143619898db1e7bede2e3e647daf19bb867c4fb25978016d67978d14868", - "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", - "sha256:112527b824eaa93c99c2c7eb11e7df83eab46a63d527bcd71a92151bba5d0435", - "sha256:1163d8b5d3a320d4d7cc8635213328850dc41f60e438c7869d540061adf66c98", - "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", - "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", - "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", - "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", - "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", - "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", - "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", - "sha256:1a1dffbfd930b27aef1962098710344297d52368b362f918eaf1464b0d8d052c", - "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", - "sha256:1bd0bf3f4e57f3d50a91dd4eff6a22ddc9b999dbab2b20fb0473332a5551a0be", - "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", - "sha256:1c6795e3694d750ae5ef172eab7d68a52aefbd9168d2e06647df691db2b03a50", - "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", - "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", - "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", - "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", - "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", - "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", - "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", - "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", - "sha256:2af6a0a76575220863cd838693c808a94e750640e0c8a3e9f707e93c2f131fdf", - "sha256:2ba1b9c80df4ea126ef303c7646021f44486342d43b7153f3454e15cd55eaa87", - "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", - "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", - "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", - "sha256:3111040f77ec6b543a501a194c48d5260898e618712472deb91bf48026f1606c", - "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", - "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", - "sha256:3e685f47b4c58b2df7dee81ebc1ec9dbb7f798b9455c3f22be6d75ac6bddee30", - "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", - "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", - "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", - "sha256:435c0c0f38f15d9bef2a97b039b5157bbc32791510670b89504e644de1d27a5e", - "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", - "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", - "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", - "sha256:50586d90be15f9aa8a2e2ee5a042ee6c51e28848812a35f0c95d4bfc0533d469", - "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", - "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", - "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", - "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", - "sha256:5b129873dd0ade248e67f25a09b5b72288cbef76ba1a9aae6bac193ee1d8be72", - "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", - "sha256:5fde2bdfd6b3afee19db5efe01e4165437329f9300441c1b25d5b2aa6752c0cc", - "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", - "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", - "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", - "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", - "sha256:67a99cf19a5137cc12f14b78dc1bb3f48500f1d5580702c623297d5297bf2752", - "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", - "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", - "sha256:6ad3aa4dca77cef9168d0c142f72376f5bd27d1d4b8a81561bd01276d3ad9fe1", - "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", - "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", - "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", - "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", - "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", - "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", - "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", - "sha256:750a83a254b020e1193bfa7219dc7edca26bd8888a94cdc59720cbe386ab0c72", - "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", - "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", - "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", - "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", - "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", - "sha256:7f41cecc799005a029407893071b15082d504f9115a57db9ea893b35f3f70604", - "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", - "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", - "sha256:87554ae12f813d3a287a0f2aad957c11e5c4ace17bfed15d471e5be13e95d9fb", - "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", - "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", - "sha256:8dfa178ead3abeeaf6b8c4fe9c6c9b333d2d66c88735566f919169d18e728fa5", - "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", - "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", - "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", - "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", - "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", - "sha256:91b95faa532efba888b196331e9af69e693635d469185ac52c796e435e2484e5", - "sha256:96e899cd34b422c2d3ad7bef279e16387f217d53ec5f9a25dbc3fcad19470381", - "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", - "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", - "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", - "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", - "sha256:a1016f430fb56f7e400838bbc56fdf43adddb6fcb7bf2a14731dfd725c2fae6c", - "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", - "sha256:a326eab6db9ab85b4afb5e6eb28736a9f2b885a9246d9e8c1989bc693dd059a0", - "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", - "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", - "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", - "sha256:ae4e93193f75ebf6b820241594a78f347785cfd5a5fbbac94634052589418352", - "sha256:afb5157cd217af4f47a13ad7cbfc35de0aa1740331ba662fa02fea94269d5894", - "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", - "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", - "sha256:b3746bda73f2fe6a9c3ab2f20b792e7d810b30acbdba044313fbd2d0174802e7", - "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", - "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", - "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", - "sha256:b81274a47e8121224f7f637392b5dfcd9558e32a53e67ba7d04007d8b5281da9", - "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", - "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", - "sha256:b9e851cfa87033c0c3fd9d35dd8b102aff2981db8bc6e0cab27b460bfe38bf3f", - "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", - "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", - "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", - "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", - "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", - "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", - "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", - "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", - "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", - "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", - "sha256:c9f3b420f54199a2b2b3b532d8c7e0860be3fa51f67501113cca6c7bfc392840", - "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", - "sha256:ca877c5a7b78fe74d97b34b735ea8f320f97c49083f7bf8fe9b61a02cf677e67", - "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", - "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", - "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", - "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", - "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", - "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", - "sha256:d75d0b0696fb476664bea1169c8e67b13197750b91eceb4f10b3c7f379c7a204", - "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", - "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", - "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", - "sha256:d9d6dd947556ddebfd62753005104986ee14a4e0663818aed19cdf2c33a6b5d5", - "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", - "sha256:dd6c682f3cdd741e7a30af2581f6a382ac910080977cd1f97c651467b6268352", - "sha256:e34d172bf50e881b7c02e530ae8b1ea96093f0b16634c344f637227b39707b41", - "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", - "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", - "sha256:e6ba1511c0ab8cd1ed8d6055bb0a6e629f48bfe04854293e0cd2dd88bd7153f8", - "sha256:e9b0def154665036516114437a5d603274e5451c0dc9694f622cc3b7e94603e7", - "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", - "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", - "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", - "sha256:ef2183285a49653517a100f28d8c1a3e037a5e8cefe79cffe205ecc4b98f5095", - "sha256:f00c519861926dc703ecbb7bbeb884be67099f96f98b175671fa0a54718f55d1", - "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", - "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", - "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", - "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", - "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", - "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9" - ], - "markers": "python_version >= '3.8'", - "version": "==0.7.6" - }, - "s3transfer": { - "hashes": [ - "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", - "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" - ], - "markers": "python_version >= '3.9'", - "version": "==0.16.0" - }, - "sentry-sdk": { - "hashes": [ - "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", - "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de" - ], - "markers": "python_version >= '3.6'", - "version": "==2.54.0" - }, - "shellingham": { - "hashes": [ - "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", - "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" - ], - "markers": "python_version >= '3.7'", - "version": "==1.5.4" - }, - "six": { - "hashes": [ - "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", - "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.17.0" - }, - "starlette": { - "hashes": [ - "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", - "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933" - ], - "markers": "python_version >= '3.10'", - "version": "==0.52.1" - }, - "typer": { - "hashes": [ - "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", - "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45" - ], - "markers": "python_version >= '3.10'", - "version": "==0.24.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", - "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" - ], - "markers": "python_version >= '3.9'", - "version": "==4.15.0" - }, - "typing-inspection": { - "hashes": [ - "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", - "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.2" - }, - "urllib3": { - "hashes": [ - "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", - "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" - ], - "markers": "python_version >= '3.9'", - "version": "==2.6.3" - }, - "uvicorn": { - "extras": [ - "standard" - ], - "hashes": [ - "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", - "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187" - ], - "markers": "python_version >= '3.10'", - "version": "==0.41.0" - }, - "uvloop": { - "hashes": [ - "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772", - "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", - "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", - "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", - "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", - "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", - "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", - "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", - "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", - "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", - "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", - "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", - "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", - "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", - "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", - "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193", - "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", - "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", - "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", - "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", - "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", - "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242", - "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", - "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", - "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6", - "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", - "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", - "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", - "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", - "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", - "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", - "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa", - "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", - "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", - "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", - "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", - "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", - "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", - "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", - "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", - "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", - "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", - "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820", - "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", - "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", - "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", - "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", - "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", - "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42" - ], - "markers": "python_full_version >= '3.8.1'", - "version": "==0.22.1" - }, - "watchfiles": { - "hashes": [ - "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", - "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", - "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", - "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", - "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", - "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", - "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", - "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", - "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", - "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", - "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", - "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", - "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", - "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", - "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", - "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", - "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", - "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", - "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", - "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", - "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", - "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", - "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", - "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", - "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", - "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", - "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", - "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", - "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", - "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", - "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", - "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", - "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", - "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", - "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", - "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", - "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", - "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", - "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", - "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", - "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", - "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", - "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", - "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", - "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", - "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", - "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", - "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", - "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", - "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", - "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", - "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", - "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", - "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", - "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", - "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", - "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", - "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", - "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", - "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", - "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", - "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", - "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", - "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", - "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", - "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", - "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", - "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", - "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", - "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", - "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", - "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", - "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", - "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", - "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", - "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", - "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", - "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", - "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", - "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", - "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", - "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", - "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", - "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", - "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", - "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", - "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", - "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", - "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", - "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", - "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", - "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", - "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", - "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", - "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", - "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", - "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", - "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", - "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", - "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", - "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", - "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", - "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", - "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", - "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", - "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", - "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", - "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", - "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf" - ], - "markers": "python_version >= '3.9'", - "version": "==1.1.1" - }, - "websocket-client": { - "hashes": [ - "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", - "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef" - ], - "markers": "python_version >= '3.9'", - "version": "==1.9.0" - }, - "websockets": { - "hashes": [ - "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", - "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", - "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", - "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", - "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", - "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", - "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", - "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", - "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", - "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", - "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", - "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", - "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", - "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", - "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", - "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", - "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", - "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", - "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", - "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", - "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", - "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", - "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", - "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", - "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", - "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", - "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", - "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", - "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", - "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", - "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", - "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", - "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", - "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", - "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", - "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", - "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", - "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", - "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", - "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", - "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", - "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", - "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", - "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", - "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", - "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", - "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", - "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", - "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", - "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", - "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", - "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", - "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", - "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", - "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", - "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", - "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", - "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", - "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", - "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", - "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4" - ], - "markers": "python_version >= '3.10'", - "version": "==16.0" - } - }, - "develop": {} -} diff --git a/app/main.py b/app/main.py deleted file mode 100644 index 0aac638..0000000 --- a/app/main.py +++ /dev/null @@ -1,164 +0,0 @@ -from fastapi import FastAPI, Security, HTTPException, Depends, status, requests, Request -from fastapi.responses import JSONResponse, PlainTextResponse -from fastapi.security import APIKeyHeader -from typing import Optional, Dict, List -from pydantic import BaseModel, Field -from contextlib import asynccontextmanager -import os, glueops.setup_logging, traceback, base64, yaml, tempfile, json -from schemas.schemas import Message, AwsCredentialsRequest, StorageBucketsRequest, AwsNukeAccountRequest, CaptainDomainNukeDataAndBackupsRequest, ChiselNodesRequest, ChiselNodesDeleteRequest, ResetGitHubOrganizationRequest, OpsgenieAlertsManifestRequest, CaptainManifestsRequest, GitHubWorkflowRunStatusRequest, VersionResponse -from util import storage, aws_setup_test_account_credentials, github, hetzner, opsgenie, captain_manifests -from fastapi.responses import RedirectResponse - - -# Configure logging -LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") -logger = glueops.setup_logging.configure(level=LOG_LEVEL) - - -app = FastAPI( - title="Tools API", - description="Various APIs to help you speed up your development and testing.", - version=os.getenv("VERSION", "UNKNOWN"), - swagger_ui_parameters={"defaultModelsExpandDepth": -1} -) - -@app.get("/", include_in_schema=False) -async def root(): - return RedirectResponse(url="/docs") - -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception): - # Extract the full stack trace - stack_trace = traceback.format_exc() - - logger.error(f"Exception: {str(exc)} STACK_TRACE: {stack_trace}") - - # Return the full stack trace in the response - return JSONResponse( - status_code=500, - content={ - "detail": "An internal server error occurred.", - "error": str(exc), - "traceback": stack_trace, # Include the full stack trace - }, - ) - - -@app.post("/v1/storage-buckets", response_class=PlainTextResponse, summary="Create/Re-create storage buckets that can be used for V2 of our monitoring stack that is Otel based") -async def hello(request: StorageBucketsRequest): - """ - Note: this can be a DESTRUCTIVE operation - For the provided captain_domain, this will DELETE and then create new/empty storage buckets for loki, tempo, and thanos. - """ - return storage.create_all_buckets(request.captain_domain) - - -@app.post("/v1/setup-aws-account-credentials", response_class=PlainTextResponse, summary="Whether it's to create an EKS cluster or to test other things out in an isolated AWS account. These creds will give you Admin level access to the requested account.") -async def create_credentials_for_aws_captain_account(request: AwsCredentialsRequest): - """ - If you are testing in AWS/EKS you will need an AWS account to test with. This request will provide you with admin level credentials to the sub account you specify. - This can also be used to just get Admin access to a desired sub account. - """ - return aws_setup_test_account_credentials.create_admin_credentials_within_captain_account(request.aws_sub_account_name) - - -@app.delete("/v1/nuke-aws-captain-account", summary="Run this after you are done testing within AWS. This will clean up orphaned resources. Note: you may have to run this 2x.") -async def nuke_aws_captain_account(request: AwsNukeAccountRequest): - """ - Submit the AWS account name you want to nuke (e.g. glueops-captain-foobar) - """ - return github.nuke_aws_account_workflow(request.aws_sub_account_name) - -@app.delete("/v1/nuke-captain-domain-data", summary="Deletes all backups/data for a provided captain_domain. Running this before a cluster creation helps ensure a clean environment.") -async def nuke_captain_domain_data(request: CaptainDomainNukeDataAndBackupsRequest): - """ - Submit the captain_domain/tenant you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will delete all backups and data for the provided captain_domain. - - This will remove things like the vault and cert-manager backups. - - Note: this may not delete things like Loki/Thanos/Tempo data as that may be managed outside of AWS. - """ - return github.nuke_captain_domain_data_and_backups(request.captain_domain) - - -@app.delete("/v1/reset-github-organization", summary="Resets the GitHub Organization to make it easier to get a new dev cluster runner for Dev") -async def reset_github_organization(request: ResetGitHubOrganizationRequest): - """ - Submit the dev captain_domain you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will reset the GitHub organization so that you can easily get up and running with a new dev cluster. - - This will reset your deployment-configurations repository, it'll bring over a working regcred, and application repos with working github actions so that you can quickly work on the GlueOps stack. - - WARNING: By default delete_all_existing_repos = True. Please set it to False or make a manual backup if you are concerned about any data loss within your tenant org (e.g. github.com/development-tenant-*) - - """ - return github.reset_tenant_github_organization(request.captain_domain, request.delete_all_existing_repos, request.custom_domain, request.enable_custom_domain) - -@app.post("/v1/github/workflow-run-status", summary="Get the status of a GitHub Actions workflow run") -async def get_workflow_run_status(request: GitHubWorkflowRunStatusRequest): - """ - Provide a GitHub Actions run URL (e.g. https://github.com/owner/repo/actions/runs/12345678) and get the current status of that workflow run. - Works for any repo the configured GITHUB_TOKEN has read access to. - """ - return github.get_workflow_run_status(request.run_url) - -@app.post("/v1/chisel", response_class=PlainTextResponse, summary="Creates Chisel nodes for dev/k3d clusters. This allows us to mimic a Cloud Controller for Loadbalancers (e.g. NLBs with EKS)") -async def create_chisel_nodes(request: ChiselNodesRequest): - """ - If you are testing within k3ds you will need chisel to provide you with load balancers. - For a provided captain_domain this will delete any existing chisel nodes and provision new ones. - Note: this will generally result in new IPs being provisioned. - """ - logger.info(f"Received POST request to create chisel nodes for captain_domain: {request.captain_domain}") - result = hetzner.create_instances(request) - logger.info(f"Successfully completed chisel node creation for captain_domain: {request.captain_domain}") - return result - - -@app.delete("/v1/chisel", summary="Deletes your chisel nodes. Please run this when you are done with development to save on costs.") -async def delete_chisel_nodes(request: ChiselNodesDeleteRequest): - """ - When you are done testing with k3ds this will delete your chisel nodes and save on costs. - """ - logger.info(f"Received DELETE request to delete chisel nodes for captain_domain: {request.captain_domain}") - response = hetzner.delete_existing_servers(request) - logger.info(f"Successfully completed chisel node deletion for captain_domain: {request.captain_domain}") - return JSONResponse(status_code=200, content={"message": "Successfully deleted chisel nodes."}) - - -@app.post("/v1/opsgenie", response_class=PlainTextResponse, summary="Creates Opsgenie Alerts Manifest") -async def create_opsgeniealerts_manifest(request: OpsgenieAlertsManifestRequest): - """ - Create a opsgenie/alertmanager configuration. Do this for any clusters you want alerts on. - """ - return opsgenie.create_opsgeniealerts_manifest(request) - -@app.post("/v1/captain-manifests", response_class=PlainTextResponse, summary="Generate captain manifests") -async def create_captain_manifests(request: CaptainManifestsRequest): - """ - Generate YAML manifests for captain deployments based on the provided configuration. - """ - return captain_manifests.generate_manifests( - request.captain_domain, - request.tenant_github_organization_name, - request.tenant_deployment_configurations_repository_name - ) - -@app.get("/health", include_in_schema=False) -async def health(): - """health check - - Returns: - dict: health status - """ - return {"status": "healthy"} - - -@app.get("/version", response_model=VersionResponse, summary="Contains version information about this tools-api") -async def version(): - return VersionResponse( - version=os.getenv("VERSION", "UNKNOWN"), - commit_sha=os.getenv("COMMIT_SHA", "UNKNOWN"), - short_sha=os.getenv("SHORT_SHA", "UNKNOWN"), - build_timestamp=os.getenv("BUILD_TIMESTAMP", "UNKNOWN"), - git_ref=os.getenv("GIT_REF", "UNKNOWN"), - ) diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py deleted file mode 100644 index 84e2fe4..0000000 --- a/app/schemas/schemas.py +++ /dev/null @@ -1,56 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Dict - -class Message(BaseModel): - message: str = Field(...,example = 'Success') - -class VersionResponse(BaseModel): - version: str = Field(..., example='v1.0.0') - commit_sha: str = Field(..., example='abc1234567890def1234567890abcdef12345678') - short_sha: str = Field(..., example='abc1234') - build_timestamp: str = Field(..., example='2026-01-01T00:00:00Z') - git_ref: str = Field(..., example='main') - -class ChiselNodesRequest(BaseModel): - captain_domain: str = Field(..., example='nonprod.foobar.onglueops.rocks') - node_count: int = Field( - default=3, - ge=1, - le=6, - example=3, - description="Number of exit nodes to create (1-6, default: 3)" - ) - -class ChiselNodesDeleteRequest(BaseModel): - captain_domain: str = Field(..., example='nonprod.foobar.onglueops.rocks') - -class StorageBucketsRequest(BaseModel): - captain_domain: str = Field(...,example = 'nonprod.foobar.onglueops.rocks') - -class AwsCredentialsRequest(BaseModel): - aws_sub_account_name: str = Field(...,example = 'glueops-captain-foobar') - -class AwsNukeAccountRequest(BaseModel): - aws_sub_account_name: str = Field(...,example = 'glueops-captain-foobar') - -class CaptainDomainNukeDataAndBackupsRequest(BaseModel): - captain_domain: str = Field(...,example = 'nonprod.foobar.onglueops.rocks') - -class ResetGitHubOrganizationRequest(BaseModel): - captain_domain: str = Field(...,example = 'nonprod.foobar.onglueops.rocks') - delete_all_existing_repos: bool = Field(...,example = True) - custom_domain: str = Field(...,example = "example.com") - enable_custom_domain: bool = Field(...,example = False) - -class OpsgenieAlertsManifestRequest(BaseModel): - captain_domain: str = Field(...,example = 'nonprod.foobar.onglueops.rocks') - opsgenie_api_key: str = Field(...,example = '6825b4ef-4e84-44a1-8450-b46b02852add') - -class CaptainManifestsRequest(BaseModel): - captain_domain: str = Field(..., example='nonprod.foobar.onglueops.rocks') - tenant_github_organization_name: str = Field(..., example='development-tenant-foobar') - tenant_deployment_configurations_repository_name: str = Field(..., example='deployment-configurations') - -class GitHubWorkflowRunStatusRequest(BaseModel): - run_url: str = Field(..., example='https://github.com/internal-GlueOps/gha-tools-api/actions/runs/12345678') - diff --git a/app/templates/captain_manifests/appproject.yaml.j2 b/app/templates/captain_manifests/appproject.yaml.j2 deleted file mode 100644 index 4fb79f9..0000000 --- a/app/templates/captain_manifests/appproject.yaml.j2 +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: AppProject -metadata: - name: <% environment_name %> -spec: - sourceNamespaces: - - '<% environment_name %>' - clusterResourceBlacklist: - - group: '*' - kind: '*' - namespaceResourceBlacklist: - - group: '*' - kind: 'Namespace' - - group: '*' - kind: 'CustomResourceDefinition' - destinations: - - name: '*' - namespace: '<% environment_name %>' - server: '*' - - name: '*' - namespace: 'glueops-core' - server: '*' - roles: - - description: <% tenant_github_organization_name %>:developers - groups: - - "<% tenant_github_organization_name %>:developers" - policies: - - p, proj:<% environment_name %>:read-only, applications, get, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, sync, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, logs, *, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, action/external-secrets.io/ExternalSecret/refresh, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, exec, *, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, action/apps/Deployment/restart, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, delete/*/Pod/*/*, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, delete/*/Deployment/*/*, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, delete/*/ReplicaSet/*/*, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, action/batch/CronJob/create-job, <% environment_name %>/*, allow - - p, proj:<% environment_name %>:read-only, applications, action/batch/Job/terminate, <% environment_name %>/*, allow - name: read-only - sourceRepos: - - https://helm.gpkg.io/project-template - - https://helm.gpkg.io/service - - https://incubating-helm.gpkg.io/project-template - - https://incubating-helm.gpkg.io/service - - https://incubating-helm.gpkg.io/platform - - https://github.com/<% tenant_github_organization_name %>/<% tenant_deployment_configurations_repository_name %> - - https://github.com/<% tenant_github_organization_name %>/* - - https://github.com/GlueOps/* diff --git a/app/templates/captain_manifests/appset.yaml.j2 b/app/templates/captain_manifests/appset.yaml.j2 deleted file mode 100644 index d419547..0000000 --- a/app/templates/captain_manifests/appset.yaml.j2 +++ /dev/null @@ -1,54 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: ApplicationSet -metadata: - name: <% environment_name %>-application-set - namespace: glueops-core -spec: - goTemplate: true - generators: - - git: - repoURL: https://github.com/<% tenant_github_organization_name %>/<% tenant_deployment_configurations_repository_name %> - revision: HEAD - directories: - - path: 'apps/*/envs/*' - - path: 'apps/*/envs/previews' - exclude: true - template: - metadata: - name: '{{ index .path.segments 1 | replace "." "-" | replace "_" "-" }}-{{ .path.basenameNormalized }}' - namespace: <% environment_name %> - annotations: - preview_environment: 'false' - spec: - destination: - namespace: <% environment_name %> - server: https://kubernetes.default.svc - project: <% environment_name %> - sources: - - chart: app - helm: - valueFiles: - - '$values/common/common-values.yaml' - - '$values/env-overlays/<% environment_name %>/env-values.yaml' - - '$values/apps/{{ index .path.segments 1 }}/base/base-values.yaml' - - '$values/{{ .path.path }}/values.yaml' - values: |- - captain_domain: <% captain_domain %> - - repoURL: https://helm.gpkg.io/project-template - targetRevision: 0.9.0 - - repoURL: https://github.com/<% tenant_github_organization_name %>/<% tenant_deployment_configurations_repository_name %> - targetRevision: main - ref: values - syncPolicy: - automated: - prune: true - selfHeal: true - retry: - backoff: - duration: 5s - factor: 2 - maxDuration: 3m0s - limit: 2 - syncOptions: - - CreateNamespace=true diff --git a/app/templates/captain_manifests/namespace.yaml.j2 b/app/templates/captain_manifests/namespace.yaml.j2 deleted file mode 100644 index e051527..0000000 --- a/app/templates/captain_manifests/namespace.yaml.j2 +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - labels: - kubernetes.io/metadata.name: <% environment_name %> - name: <% environment_name %> diff --git a/app/util/__init__.py b/app/util/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/util/aws_setup_test_account_credentials.py b/app/util/aws_setup_test_account_credentials.py deleted file mode 100644 index b60f099..0000000 --- a/app/util/aws_setup_test_account_credentials.py +++ /dev/null @@ -1,132 +0,0 @@ -import boto3 -import os -from fastapi import HTTPException -import json - - - -def create_admin_credentials_within_captain_account(aws_sub_account_name): - aws_access_key = os.getenv("AWS_GLUEOPS_ROCKS_ORG_ACCESS_KEY") - aws_secret_key = os.getenv("AWS_GLUEOPS_ROCKS_ORG_SECRET_KEY") - - # Initialize AWS clients (using server-side credentials) - client = boto3.client('organizations', aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) - sts_client = boto3.client('sts', aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) - - # Step 1: Check if the current account is the root account - root_account_id = client.describe_organization()['Organization']['MasterAccountId'] - current_account_id = sts_client.get_caller_identity()['Account'] - - if current_account_id != root_account_id: - raise HTTPException(status_code=400, detail="This is not the root account. Exiting.") - - # Step 2: Get account ID of the sub-account based on provided account name - account_name = aws_sub_account_name - - # Initialize a list to store account details - all_accounts = [] - # Initially set NextToken to None - next_token = None - while True: - # Call list_accounts with the NextToken if available - if next_token: - response = client.list_accounts(NextToken=next_token) - else: - response = client.list_accounts() - - # Add the accounts in the current response to the all_accounts list - all_accounts.extend(response['Accounts']) - - # Check if there is another page of results - next_token = response.get('NextToken') - - # If there's no NextToken, it means we've reached the end of the list - if not next_token: - break - - - sub_account = next( - (account for account in all_accounts if account['Name'] == account_name), None - ) - if not sub_account: - raise HTTPException(status_code=404, detail="Account not found.") - - sub_account_id = sub_account['Id'] - - # Step 3: Assume role in the sub-account to retrieve credentials - assume_role_response = sts_client.assume_role( - RoleArn=f"arn:aws:iam::{sub_account_id}:role/OrganizationAccountAccessRole", - RoleSessionName="SubAccountAccess" - ) - - # Step 4: Extract the credentials from the assumed role - session_token = assume_role_response['Credentials']['SessionToken'] - access_key_id = assume_role_response['Credentials']['AccessKeyId'] - secret_access_key = assume_role_response['Credentials']['SecretAccessKey'] - - # Step 5: Create IAM user and assign a policy (for managing services) - iam_client = boto3.client( - 'iam', - aws_access_key_id=access_key_id, - aws_secret_access_key=secret_access_key, - aws_session_token=session_token - ) - iam_user_name = "dev-deployment-svc-account" - iam_role_name = "glueops-captain-role" - iam_policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" - - # Create IAM user - try: - iam_client.create_user(UserName=iam_user_name) - iam_client.attach_user_policy(UserName=iam_user_name, PolicyArn=iam_policy_arn) - except iam_client.exceptions.EntityAlreadyExistsException: - pass # If the user already exists, skip creating it - - # Create access keys for the user - user_keys = iam_client.create_access_key(UserName=iam_user_name) - access_key = user_keys['AccessKey']['AccessKeyId'] - secret_key = user_keys['AccessKey']['SecretAccessKey'] - - # Step 6: Create role and attach the policy (if not already created) - assume_role_policy_document = { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "AWS": f"arn:aws:iam::{sub_account_id}:root" - }, - "Action": "sts:AssumeRole" - } - ] - } - - try: - iam_client.create_role( - RoleName=iam_role_name, - AssumeRolePolicyDocument=json.dumps(assume_role_policy_document) - ) - iam_client.attach_role_policy(RoleName=iam_role_name, PolicyArn=iam_policy_arn) - except iam_client.exceptions.EntityAlreadyExistsException: - pass # If the role already exists, skip creating it - - # Get the ARN of the created role - arn_of_role_created = iam_client.get_role(RoleName=iam_role_name)['Role']['Arn'] - - # Step 7: Generate the .env content (in the format you provided) - env_content = f""" -# Run the following in your codespace environment to create your .env for {aws_sub_account_name}: - -cat <> $(pwd)/.env -export AWS_ACCESS_KEY_ID={access_key} -export AWS_SECRET_ACCESS_KEY={secret_key} -export AWS_DEFAULT_REGION=us-west-2 -#aws eks update-kubeconfig --region us-west-2 --name captain-cluster --role-arn {arn_of_role_created} -ENV - -# Here is the iam_role_to_assume that you will need to specify in your terraform module for {aws_sub_account_name}: -# {arn_of_role_created} - - """ - - return env_content diff --git a/app/util/captain_manifests.py b/app/util/captain_manifests.py deleted file mode 100644 index 7cd64b4..0000000 --- a/app/util/captain_manifests.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Captain Manifests utility module. - -This module generates YAML manifests for captain deployments using Jinja2 templates. -""" - -import os -from jinja2 import Environment, FileSystemLoader - - -def generate_manifests(captain_domain: str, tenant_github_organization_name: str, tenant_deployment_configurations_repository_name: str) -> dict: - """ - Generate captain manifests based on the provided configuration. - - Args: - captain_domain: The captain domain (e.g., nonprod.antoniostaqueria.onglueops.com) - tenant_github_organization_name: The tenant's GitHub organization name - tenant_deployment_configurations_repository_name: The tenant's deployment configurations repository name - - Returns: - dict: Status response with concatenated YAML manifests - """ - # Extract environment name from captain_domain (first segment) - environment_name = captain_domain.split('.')[0] - - # Set up Jinja2 environment with custom delimiters to avoid conflict with Go templates - templates_dir = os.path.join(os.path.dirname(__file__), '..', 'templates', 'captain_manifests') - env = Environment( - loader=FileSystemLoader(templates_dir), - variable_start_string='<%', - variable_end_string='%>' - ) - - # Template variables - template_vars = { - 'environment_name': environment_name, - 'captain_domain': captain_domain, - 'tenant_github_organization_name': tenant_github_organization_name, - 'tenant_deployment_configurations_repository_name': tenant_deployment_configurations_repository_name - } - - # Render all templates - namespace_yaml = env.get_template('namespace.yaml.j2').render(template_vars) - appproject_yaml = env.get_template('appproject.yaml.j2').render(template_vars) - appset_yaml = env.get_template('appset.yaml.j2').render(template_vars) - - # Concatenate all YAMLs with document separators - return f"{namespace_yaml}\n---\n{appproject_yaml}\n---\n{appset_yaml}" diff --git a/app/util/chisel.py b/app/util/chisel.py deleted file mode 100644 index 01e91c4..0000000 --- a/app/util/chisel.py +++ /dev/null @@ -1,51 +0,0 @@ -import secrets -import string - -def generate_credentials(): - character_pool = string.ascii_letters + string.digits - return ( - "".join(secrets.choice(character_pool) for _ in range(15)) # Generate 15 secure characters - + ":" - + "".join(secrets.choice(character_pool) for _ in range(15)) # Generate another 15 secure characters - ) - -def get_suffixes(node_count: int = 2): - """Generate suffixes for the requested number of nodes (1-6).""" - return [f"exit{i}" for i in range(1, node_count + 1)] - - -def create_chisel_yaml(captain_domain, credentials_for_chisel, ip_addresses, suffixes): - manifest = f""" -kubectl apply -k https://github.com/FyraLabs/chisel-operator?ref=v0.7.1 - -kubectl apply -f - <= 300: - raise ValueError(f"GitHub workflow dispatch failed with status {status_code}") - run_info = _get_workflow_run_id(workflow_file) - return { - "status_code": status_code, - "all_jobs_url": all_jobs_url, - "run_id": run_info["run_id"], - "run_url": run_info["run_url"], - } - - -def get_workflow_run_status(run_url: str): - """Get the status of a GitHub Actions workflow run from its URL. - - Args: - run_url: A GitHub Actions run URL, e.g. https://github.com/owner/repo/actions/runs/12345678 - - Returns: - dict: run status details - """ - match = re.match(r"https://github\.com/([^/]+/[^/]+)/actions/runs/(\d+)", run_url) - if not match: - raise HTTPException(status_code=400, detail=f"Invalid GitHub Actions run URL: {run_url}") - - owner_repo = match.group(1) - run_id = match.group(2) - api_url = f"https://api.github.com/repos/{owner_repo}/actions/runs/{run_id}" - - response = requests.get(api_url, headers=_get_headers(), timeout=30) - if response.status_code != 200: - raise HTTPException(status_code=502, detail=f"GitHub API returned {response.status_code} for run {run_id}") - - data = response.json() - return { - "run_id": data["id"], - "name": data.get("name"), - "status": data["status"], - "conclusion": data.get("conclusion"), - "run_url": data["html_url"], - "created_at": data.get("created_at"), - "updated_at": data.get("updated_at"), - } - - -def nuke_aws_account_workflow(aws_sub_account_name): - return _dispatch_and_get_run( - "aws-nuke-account.yml", - {"AWS_ACCOUNT_NAME_TO_NUKE": aws_sub_account_name}, - ) - - -def nuke_captain_domain_data_and_backups(captain_domain): - return _dispatch_and_get_run( - "nuke-captain-domain-data-and-backups.yml", - {"CAPTAIN_DOMAIN_TO_NUKE": captain_domain}, - ) - - -def reset_tenant_github_organization(captain_domain, delete_all_existing_repos, custom_domain, enable_custom_domain): - return _dispatch_and_get_run( - "reset-tenant-github-organization.yml", - { - "CAPTAIN_DOMAIN": captain_domain, - "DELETE_ALL_EXISTING_REPOS": str(delete_all_existing_repos), - "CUSTOM_DOMAIN": custom_domain, - "ENABLE_CUSTOM_DOMAIN": str(enable_custom_domain), - }, - ) diff --git a/app/util/hetzner.py b/app/util/hetzner.py deleted file mode 100644 index 5102302..0000000 --- a/app/util/hetzner.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -from fastapi import FastAPI, Security, HTTPException, Depends, status, requests, Request -import time -import util.chisel -from hcloud import Client -from hcloud.images import Image -from hcloud.server_types import ServerType -from hcloud.images.domain import Image -from hcloud.locations.domain import Location -from hcloud.servers.domain import ServerCreatePublicNetwork -import glueops.setup_logging - -LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") -logger = glueops.setup_logging.configure(level=LOG_LEVEL) - -client = Client(token=os.getenv("HCLOUD_TOKEN")) - -def multiline_to_singleline(input_text: str) -> str: - """ - Converts a multi-line string to a single-line string with `\\n` replacing newlines. - - Args: - input_text (str): The multi-line input string. - - Returns: - str: Single-line string with `\\n` replacing newlines. - """ - return input_text.replace("\n", "\n") - - -def create_instances(request): - captain_domain = request.captain_domain.strip() - logger.info(f"Starting chisel node creation for captain_domain: {captain_domain}") - - try: - logger.info(f"Generating chisel credentials...") - credentials_for_chisel = util.chisel.generate_credentials() - logger.info(f"Successfully generated chisel credentials") - except Exception as e: - logger.error(f"Failed to generate chisel credentials: {str(e)}") - raise - - # Define user data - user_data_readable = f""" -#cloud-config -package_update: true -runcmd: - - curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh && sudo apt install tmux -y - - sudo docker run -d --restart always -p 9090:9090 -p 443:443 -p 80:80 -it docker.io/jpillora/chisel:1 server --reverse --port=9090 --auth='{credentials_for_chisel}' -""" - - user_data = multiline_to_singleline(user_data_readable) - - try: - node_count = request.node_count - logger.info(f"Getting chisel suffixes for {node_count} nodes...") - suffixes = util.chisel.get_suffixes(node_count) - logger.info(f"Got suffixes: {suffixes}") - except Exception as e: - logger.error(f"Failed to get chisel suffixes: {str(e)}") - raise - - instance_names = [f"{captain_domain}-{suffix}" for suffix in suffixes] - ip_addresses = {} - - try: - delete_existing_servers(request) - - for instance_name in instance_names: - logger.info(f"Creating chisel node: {instance_name}") - ip_addresses[instance_name] = create_server(instance_name, captain_domain, user_data) - - logger.info(f"All chisel nodes created successfully. IP addresses: {ip_addresses}") - except Exception as e: - logger.error(f"Error creating chisel instances for {captain_domain}: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error creating instances: {str(e)}") - - try: - logger.info(f"Generating chisel YAML manifest...") - yaml_manifest = util.chisel.create_chisel_yaml(captain_domain, credentials_for_chisel, ip_addresses, suffixes) - logger.info(f"Successfully generated chisel YAML manifest for {captain_domain}") - return yaml_manifest - except Exception as e: - logger.error(f"Failed to generate chisel YAML manifest: {str(e)}") - raise - - -def create_server(server_name, captain_domain, user_data_one_line_format): - try: - CHISEL_NODE_HCLOUD_INSTANCE_TYPE = os.getenv("CHISEL_HCLOUD_INSTANCE_TYPE") - logger.info(f"Creating instances of type: {CHISEL_NODE_HCLOUD_INSTANCE_TYPE}") - - server_type = ServerType(name=CHISEL_NODE_HCLOUD_INSTANCE_TYPE) - image = Image(name="debian-12") - - logger.info(f"Fetching SSH keys for server {server_name}...") - ssh_keys = client.ssh_keys.get_all(name="glueops-default-ssh-key") - logger.info(f"Found {len(ssh_keys)} SSH key(s)") - - location = Location(name="hel1") - - logger.info(f"Calling Hetzner API to create server {server_name}...") - server_response = client.servers.create( - server_name, - server_type=server_type, - image=image, - ssh_keys=ssh_keys, - location=location, - user_data=user_data_one_line_format, - labels={"captain_domain": captain_domain, "chisel_node": "True"}, - public_net=ServerCreatePublicNetwork( - enable_ipv4=True, - enable_ipv6=False - ) - ) - logger.info(f"Hetzner API call completed for server {server_name}") - except Exception as e: - logger.error(f"Failed to create server {server_name}: {str(e)}") - raise - - server = server_response.server - #server_response.action.wait_until_finished() - ipv4_address = server.public_net.ipv4.ip - logger.info(f"Successfully created chisel node {server_name} with IP: {ipv4_address}") - return ipv4_address - - -def delete_existing_servers(request): - captain_domain = request.captain_domain.strip() - logger.info(f"Starting deletion of existing chisel nodes for captain_domain: {captain_domain}") - - try: - logger.info(f"Fetching all servers with captain_domain label...") - servers = client.servers.get_all(label_selector="captain_domain") - logger.info(f"Found {len(servers)} total server(s) with captain_domain label") - except Exception as e: - logger.error(f"Failed to fetch servers from Hetzner API: {str(e)}") - raise - deleted_count = 0 - for server in servers: - logger.info(f"Checking server: {server.name} (captain_domain={server.labels.get('captain_domain', 'N/A')})") - if server.labels["captain_domain"] == captain_domain: - try: - logger.info(f"Deleting chisel node: {server.name}") - server.delete() - logger.info(f"Successfully deleted chisel node: {server.name}") - deleted_count += 1 - except Exception as e: - logger.error(f"Failed to delete server {server.name}: {str(e)}") - raise - logger.info(f"Completed deletion of {deleted_count} chisel node(s) for captain_domain: {captain_domain}") - return True - - diff --git a/app/util/storage.py b/app/util/storage.py deleted file mode 100644 index 7f50eda..0000000 --- a/app/util/storage.py +++ /dev/null @@ -1,263 +0,0 @@ -import uuid -from minio import Minio -from minio.error import S3Error -from minio.deleteobjects import DeleteObject -import re -import os -import glueops.setup_logging - -LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") -logger = glueops.setup_logging.configure(level=LOG_LEVEL) - -# ----------------------- Configuration ----------------------- # - -# MinIO Server Configuration -MINIO_SERVER = f"{os.getenv("HETZNER_STORAGE_REGION")}.your-objectstorage.com" # Replace with your MinIO server -ACCESS_KEY = os.getenv("MINIO_S3_ACCESS_KEY_ID") # Replace with your Access Key -SECRET_KEY = os.getenv("MINIO_S3_SECRET_KEY") # Replace with your Secret Key -MINIO_REGION = os.getenv("HETZNER_STORAGE_REGION") # Replace with your region -USE_SSL = True # Set to False if not using SSL - -# Bucket Configuration -UUID_LENGTH = 4 # Length of UUID suffix (adjust as needed) -UUID_FORMAT = 'hex' # Format of UUID ('hex' for hexadecimal) - -# ----------------------- Functions ----------------------- # - -def initialize_minio_client(): - """ - Initializes and returns a MinIO client. - """ - try: - client = Minio( - MINIO_SERVER, - access_key=ACCESS_KEY, - secret_key=SECRET_KEY, - secure=USE_SSL, - region=MINIO_REGION - ) - return client - except Exception as e: - logger.error(f"Failed to initialize MinIO client: {e}") - raise - - -def make_compliant_name(name: str) -> str: - # Remove invalid characters (anything not lowercase letters, numbers, or hyphens) - name = re.sub(r'[^a-z0-9\-]', '', name.lower()) - - # Ensure it starts with a letter or number by stripping leading hyphens - name = re.sub(r'^-+', '', name) - - # Ensure it ends with a letter or number by stripping trailing hyphens - name = re.sub(r'-+$', '', name) - - # If the name is now empty, return a default compliant name - return name if name else "default-name" - - -def parameterize_storage_config(bucket_prefix): - """ - Parameterizes the storage configuration template with the correct bucket names. - - Args: - template (str): The storage configuration template. - bucket_prefix (str): The prefix for the buckets. - - Returns: - str: The parameterized storage configuration. - """ - # Example usage - template = """ - loki_storage = <.go`: - Use `spec.Summary()` and `spec.Description()` for `Short`/`Long` - Use `spec.FlagDesc()` for flag descriptions diff --git a/cli/Makefile b/cli/Makefile index 6b5e839..f6e8263 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -1,5 +1,4 @@ GO_IMAGE := golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 -TOOLS_API_IMAGE := tools-api-spec MODULE := github.com/GlueOps/tools-api/cli VERSION ?= dev COMMIT_SHA ?= $(shell git rev-parse HEAD 2>/dev/null || echo unknown) @@ -14,17 +13,7 @@ LDFLAGS := -s -w \ -X $(MODULE)/internal/version.BuildTimestamp=$(BUILD_TIMESTAMP) \ -X $(MODULE)/internal/version.GitRef=$(GIT_REF) -.PHONY: generate build build-all clean - -# Export OpenAPI spec from FastAPI and regenerate Go client -generate: - docker build -t $(TOOLS_API_IMAGE) .. - docker run --rm $(TOOLS_API_IMAGE) python -c \ - "import sys; sys.path.insert(0, 'app'); from main import app; import json; print(json.dumps(app.openapi(), indent=2))" \ - > openapi.json - docker run --rm -v "$$(pwd):/app" -w /app $(GO_IMAGE) sh -c \ - "go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.6.0 && oapi-codegen -config oapi-codegen.yaml openapi.json" - cp openapi.json internal/spec/openapi.json +.PHONY: build build-all clean # Build for current platform build: diff --git a/cli/api/generated.go b/cli/api/generated.go deleted file mode 100644 index e3046c5..0000000 --- a/cli/api/generated.go +++ /dev/null @@ -1,1852 +0,0 @@ -// Package api provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT. -package api - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - - "github.com/oapi-codegen/runtime" -) - -// AwsCredentialsRequest defines model for AwsCredentialsRequest. -type AwsCredentialsRequest struct { - AwsSubAccountName string `json:"aws_sub_account_name"` -} - -// AwsNukeAccountRequest defines model for AwsNukeAccountRequest. -type AwsNukeAccountRequest struct { - AwsSubAccountName string `json:"aws_sub_account_name"` -} - -// CaptainDomainNukeDataAndBackupsRequest defines model for CaptainDomainNukeDataAndBackupsRequest. -type CaptainDomainNukeDataAndBackupsRequest struct { - CaptainDomain string `json:"captain_domain"` -} - -// CaptainManifestsRequest defines model for CaptainManifestsRequest. -type CaptainManifestsRequest struct { - CaptainDomain string `json:"captain_domain"` - TenantDeploymentConfigurationsRepositoryName string `json:"tenant_deployment_configurations_repository_name"` - TenantGithubOrganizationName string `json:"tenant_github_organization_name"` -} - -// ChiselNodesDeleteRequest defines model for ChiselNodesDeleteRequest. -type ChiselNodesDeleteRequest struct { - CaptainDomain string `json:"captain_domain"` -} - -// ChiselNodesRequest defines model for ChiselNodesRequest. -type ChiselNodesRequest struct { - CaptainDomain string `json:"captain_domain"` - - // NodeCount Number of exit nodes to create (1-6, default: 3) - NodeCount *int `json:"node_count,omitempty"` -} - -// GitHubWorkflowRunStatusRequest defines model for GitHubWorkflowRunStatusRequest. -type GitHubWorkflowRunStatusRequest struct { - RunUrl string `json:"run_url"` -} - -// HTTPValidationError defines model for HTTPValidationError. -type HTTPValidationError struct { - Detail *[]ValidationError `json:"detail,omitempty"` -} - -// OpsgenieAlertsManifestRequest defines model for OpsgenieAlertsManifestRequest. -type OpsgenieAlertsManifestRequest struct { - CaptainDomain string `json:"captain_domain"` - OpsgenieApiKey string `json:"opsgenie_api_key"` -} - -// ResetGitHubOrganizationRequest defines model for ResetGitHubOrganizationRequest. -type ResetGitHubOrganizationRequest struct { - CaptainDomain string `json:"captain_domain"` - CustomDomain string `json:"custom_domain"` - DeleteAllExistingRepos bool `json:"delete_all_existing_repos"` - EnableCustomDomain bool `json:"enable_custom_domain"` -} - -// StorageBucketsRequest defines model for StorageBucketsRequest. -type StorageBucketsRequest struct { - CaptainDomain string `json:"captain_domain"` -} - -// ValidationError defines model for ValidationError. -type ValidationError struct { - Ctx *map[string]interface{} `json:"ctx,omitempty"` - Input interface{} `json:"input,omitempty"` - Loc []ValidationError_Loc_Item `json:"loc"` - Msg string `json:"msg"` - Type string `json:"type"` -} - -// ValidationErrorLoc0 defines model for . -type ValidationErrorLoc0 = string - -// ValidationErrorLoc1 defines model for . -type ValidationErrorLoc1 = int - -// ValidationError_Loc_Item defines model for ValidationError.loc.Item. -type ValidationError_Loc_Item struct { - union json.RawMessage -} - -// VersionResponse defines model for VersionResponse. -type VersionResponse struct { - BuildTimestamp string `json:"build_timestamp"` - CommitSha string `json:"commit_sha"` - GitRef string `json:"git_ref"` - ShortSha string `json:"short_sha"` - Version string `json:"version"` -} - -// CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody defines body for CreateCaptainManifestsV1CaptainManifestsPost for application/json ContentType. -type CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody = CaptainManifestsRequest - -// DeleteChiselNodesV1ChiselDeleteJSONRequestBody defines body for DeleteChiselNodesV1ChiselDelete for application/json ContentType. -type DeleteChiselNodesV1ChiselDeleteJSONRequestBody = ChiselNodesDeleteRequest - -// CreateChiselNodesV1ChiselPostJSONRequestBody defines body for CreateChiselNodesV1ChiselPost for application/json ContentType. -type CreateChiselNodesV1ChiselPostJSONRequestBody = ChiselNodesRequest - -// GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody defines body for GetWorkflowRunStatusV1GithubWorkflowRunStatusPost for application/json ContentType. -type GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody = GitHubWorkflowRunStatusRequest - -// NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody defines body for NukeAwsCaptainAccountV1NukeAwsCaptainAccountDelete for application/json ContentType. -type NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody = AwsNukeAccountRequest - -// NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody defines body for NukeCaptainDomainDataV1NukeCaptainDomainDataDelete for application/json ContentType. -type NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody = CaptainDomainNukeDataAndBackupsRequest - -// CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody defines body for CreateOpsgeniealertsManifestV1OpsgeniePost for application/json ContentType. -type CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody = OpsgenieAlertsManifestRequest - -// ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody defines body for ResetGithubOrganizationV1ResetGithubOrganizationDelete for application/json ContentType. -type ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody = ResetGitHubOrganizationRequest - -// CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody defines body for CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPost for application/json ContentType. -type CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody = AwsCredentialsRequest - -// HelloV1StorageBucketsPostJSONRequestBody defines body for HelloV1StorageBucketsPost for application/json ContentType. -type HelloV1StorageBucketsPostJSONRequestBody = StorageBucketsRequest - -// AsValidationErrorLoc0 returns the union data inside the ValidationError_Loc_Item as a ValidationErrorLoc0 -func (t ValidationError_Loc_Item) AsValidationErrorLoc0() (ValidationErrorLoc0, error) { - var body ValidationErrorLoc0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromValidationErrorLoc0 overwrites any union data inside the ValidationError_Loc_Item as the provided ValidationErrorLoc0 -func (t *ValidationError_Loc_Item) FromValidationErrorLoc0(v ValidationErrorLoc0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeValidationErrorLoc0 performs a merge with any union data inside the ValidationError_Loc_Item, using the provided ValidationErrorLoc0 -func (t *ValidationError_Loc_Item) MergeValidationErrorLoc0(v ValidationErrorLoc0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsValidationErrorLoc1 returns the union data inside the ValidationError_Loc_Item as a ValidationErrorLoc1 -func (t ValidationError_Loc_Item) AsValidationErrorLoc1() (ValidationErrorLoc1, error) { - var body ValidationErrorLoc1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromValidationErrorLoc1 overwrites any union data inside the ValidationError_Loc_Item as the provided ValidationErrorLoc1 -func (t *ValidationError_Loc_Item) FromValidationErrorLoc1(v ValidationErrorLoc1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeValidationErrorLoc1 performs a merge with any union data inside the ValidationError_Loc_Item, using the provided ValidationErrorLoc1 -func (t *ValidationError_Loc_Item) MergeValidationErrorLoc1(v ValidationErrorLoc1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ValidationError_Loc_Item) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ValidationError_Loc_Item) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// RequestEditorFn is the function signature for the RequestEditor callback function -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// Doer performs HTTP requests. -// -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// Client which conforms to the OpenAPI3 specification for this service. -type Client struct { - // The endpoint of the server conforming to this interface, with scheme, - // https://api.deepmap.com for example. This can contain a path relative - // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the swagger spec will be appended to the server. - Server string - - // Doer for performing requests, typically a *http.Client with any - // customized settings, such as certificate chains. - Client HttpRequestDoer - - // A list of callbacks for modifying requests which are generated before sending over - // the network. - RequestEditors []RequestEditorFn -} - -// ClientOption allows setting custom parameters during construction -type ClientOption func(*Client) error - -// Creates a new Client, with reasonable defaults -func NewClient(server string, opts ...ClientOption) (*Client, error) { - // create a client with sane default values - client := Client{ - Server: server, - } - // mutate client and add all optional params - for _, o := range opts { - if err := o(&client); err != nil { - return nil, err - } - } - // ensure the server URL always has a trailing slash - if !strings.HasSuffix(client.Server, "/") { - client.Server += "/" - } - // create httpClient, if not already present - if client.Client == nil { - client.Client = &http.Client{} - } - return &client, nil -} - -// WithHTTPClient allows overriding the default Doer, which is -// automatically created using http.Client. This is useful for tests. -func WithHTTPClient(doer HttpRequestDoer) ClientOption { - return func(c *Client) error { - c.Client = doer - return nil - } -} - -// WithRequestEditorFn allows setting up a callback function, which will be -// called right before sending the request. This can be used to mutate the request. -func WithRequestEditorFn(fn RequestEditorFn) ClientOption { - return func(c *Client) error { - c.RequestEditors = append(c.RequestEditors, fn) - return nil - } -} - -// The interface specification for the client above. -type ClientInterface interface { - // CreateCaptainManifestsV1CaptainManifestsPostWithBody request with any body - CreateCaptainManifestsV1CaptainManifestsPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - CreateCaptainManifestsV1CaptainManifestsPost(ctx context.Context, body CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // DeleteChiselNodesV1ChiselDeleteWithBody request with any body - DeleteChiselNodesV1ChiselDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - DeleteChiselNodesV1ChiselDelete(ctx context.Context, body DeleteChiselNodesV1ChiselDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // CreateChiselNodesV1ChiselPostWithBody request with any body - CreateChiselNodesV1ChiselPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - CreateChiselNodesV1ChiselPost(ctx context.Context, body CreateChiselNodesV1ChiselPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBody request with any body - GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - GetWorkflowRunStatusV1GithubWorkflowRunStatusPost(ctx context.Context, body GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBody request with any body - NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - NukeAwsCaptainAccountV1NukeAwsCaptainAccountDelete(ctx context.Context, body NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBody request with any body - NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - NukeCaptainDomainDataV1NukeCaptainDomainDataDelete(ctx context.Context, body NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // CreateOpsgeniealertsManifestV1OpsgeniePostWithBody request with any body - CreateOpsgeniealertsManifestV1OpsgeniePostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - CreateOpsgeniealertsManifestV1OpsgeniePost(ctx context.Context, body CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBody request with any body - ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - ResetGithubOrganizationV1ResetGithubOrganizationDelete(ctx context.Context, body ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBody request with any body - CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPost(ctx context.Context, body CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // HelloV1StorageBucketsPostWithBody request with any body - HelloV1StorageBucketsPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - HelloV1StorageBucketsPost(ctx context.Context, body HelloV1StorageBucketsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // VersionVersionGet request - VersionVersionGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) -} - -func (c *Client) CreateCaptainManifestsV1CaptainManifestsPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateCaptainManifestsV1CaptainManifestsPostRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) CreateCaptainManifestsV1CaptainManifestsPost(ctx context.Context, body CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateCaptainManifestsV1CaptainManifestsPostRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) DeleteChiselNodesV1ChiselDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewDeleteChiselNodesV1ChiselDeleteRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) DeleteChiselNodesV1ChiselDelete(ctx context.Context, body DeleteChiselNodesV1ChiselDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewDeleteChiselNodesV1ChiselDeleteRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) CreateChiselNodesV1ChiselPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateChiselNodesV1ChiselPostRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) CreateChiselNodesV1ChiselPost(ctx context.Context, body CreateChiselNodesV1ChiselPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateChiselNodesV1ChiselPostRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetWorkflowRunStatusV1GithubWorkflowRunStatusPostRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) GetWorkflowRunStatusV1GithubWorkflowRunStatusPost(ctx context.Context, body GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetWorkflowRunStatusV1GithubWorkflowRunStatusPostRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) NukeAwsCaptainAccountV1NukeAwsCaptainAccountDelete(ctx context.Context, body NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) NukeCaptainDomainDataV1NukeCaptainDomainDataDelete(ctx context.Context, body NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) CreateOpsgeniealertsManifestV1OpsgeniePostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateOpsgeniealertsManifestV1OpsgeniePostRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) CreateOpsgeniealertsManifestV1OpsgeniePost(ctx context.Context, body CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateOpsgeniealertsManifestV1OpsgeniePostRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewResetGithubOrganizationV1ResetGithubOrganizationDeleteRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) ResetGithubOrganizationV1ResetGithubOrganizationDelete(ctx context.Context, body ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewResetGithubOrganizationV1ResetGithubOrganizationDeleteRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPost(ctx context.Context, body CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) HelloV1StorageBucketsPostWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewHelloV1StorageBucketsPostRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) HelloV1StorageBucketsPost(ctx context.Context, body HelloV1StorageBucketsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewHelloV1StorageBucketsPostRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) VersionVersionGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewVersionVersionGetRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -// NewCreateCaptainManifestsV1CaptainManifestsPostRequest calls the generic CreateCaptainManifestsV1CaptainManifestsPost builder with application/json body -func NewCreateCaptainManifestsV1CaptainManifestsPostRequest(server string, body CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewCreateCaptainManifestsV1CaptainManifestsPostRequestWithBody(server, "application/json", bodyReader) -} - -// NewCreateCaptainManifestsV1CaptainManifestsPostRequestWithBody generates requests for CreateCaptainManifestsV1CaptainManifestsPost with any type of body -func NewCreateCaptainManifestsV1CaptainManifestsPostRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/captain-manifests") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewDeleteChiselNodesV1ChiselDeleteRequest calls the generic DeleteChiselNodesV1ChiselDelete builder with application/json body -func NewDeleteChiselNodesV1ChiselDeleteRequest(server string, body DeleteChiselNodesV1ChiselDeleteJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewDeleteChiselNodesV1ChiselDeleteRequestWithBody(server, "application/json", bodyReader) -} - -// NewDeleteChiselNodesV1ChiselDeleteRequestWithBody generates requests for DeleteChiselNodesV1ChiselDelete with any type of body -func NewDeleteChiselNodesV1ChiselDeleteRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/chisel") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("DELETE", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewCreateChiselNodesV1ChiselPostRequest calls the generic CreateChiselNodesV1ChiselPost builder with application/json body -func NewCreateChiselNodesV1ChiselPostRequest(server string, body CreateChiselNodesV1ChiselPostJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewCreateChiselNodesV1ChiselPostRequestWithBody(server, "application/json", bodyReader) -} - -// NewCreateChiselNodesV1ChiselPostRequestWithBody generates requests for CreateChiselNodesV1ChiselPost with any type of body -func NewCreateChiselNodesV1ChiselPostRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/chisel") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewGetWorkflowRunStatusV1GithubWorkflowRunStatusPostRequest calls the generic GetWorkflowRunStatusV1GithubWorkflowRunStatusPost builder with application/json body -func NewGetWorkflowRunStatusV1GithubWorkflowRunStatusPostRequest(server string, body GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewGetWorkflowRunStatusV1GithubWorkflowRunStatusPostRequestWithBody(server, "application/json", bodyReader) -} - -// NewGetWorkflowRunStatusV1GithubWorkflowRunStatusPostRequestWithBody generates requests for GetWorkflowRunStatusV1GithubWorkflowRunStatusPost with any type of body -func NewGetWorkflowRunStatusV1GithubWorkflowRunStatusPostRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/github/workflow-run-status") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteRequest calls the generic NukeAwsCaptainAccountV1NukeAwsCaptainAccountDelete builder with application/json body -func NewNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteRequest(server string, body NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteRequestWithBody(server, "application/json", bodyReader) -} - -// NewNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteRequestWithBody generates requests for NukeAwsCaptainAccountV1NukeAwsCaptainAccountDelete with any type of body -func NewNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/nuke-aws-captain-account") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("DELETE", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteRequest calls the generic NukeCaptainDomainDataV1NukeCaptainDomainDataDelete builder with application/json body -func NewNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteRequest(server string, body NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteRequestWithBody(server, "application/json", bodyReader) -} - -// NewNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteRequestWithBody generates requests for NukeCaptainDomainDataV1NukeCaptainDomainDataDelete with any type of body -func NewNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/nuke-captain-domain-data") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("DELETE", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewCreateOpsgeniealertsManifestV1OpsgeniePostRequest calls the generic CreateOpsgeniealertsManifestV1OpsgeniePost builder with application/json body -func NewCreateOpsgeniealertsManifestV1OpsgeniePostRequest(server string, body CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewCreateOpsgeniealertsManifestV1OpsgeniePostRequestWithBody(server, "application/json", bodyReader) -} - -// NewCreateOpsgeniealertsManifestV1OpsgeniePostRequestWithBody generates requests for CreateOpsgeniealertsManifestV1OpsgeniePost with any type of body -func NewCreateOpsgeniealertsManifestV1OpsgeniePostRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/opsgenie") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewResetGithubOrganizationV1ResetGithubOrganizationDeleteRequest calls the generic ResetGithubOrganizationV1ResetGithubOrganizationDelete builder with application/json body -func NewResetGithubOrganizationV1ResetGithubOrganizationDeleteRequest(server string, body ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewResetGithubOrganizationV1ResetGithubOrganizationDeleteRequestWithBody(server, "application/json", bodyReader) -} - -// NewResetGithubOrganizationV1ResetGithubOrganizationDeleteRequestWithBody generates requests for ResetGithubOrganizationV1ResetGithubOrganizationDelete with any type of body -func NewResetGithubOrganizationV1ResetGithubOrganizationDeleteRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/reset-github-organization") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("DELETE", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostRequest calls the generic CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPost builder with application/json body -func NewCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostRequest(server string, body CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostRequestWithBody(server, "application/json", bodyReader) -} - -// NewCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostRequestWithBody generates requests for CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPost with any type of body -func NewCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/setup-aws-account-credentials") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewHelloV1StorageBucketsPostRequest calls the generic HelloV1StorageBucketsPost builder with application/json body -func NewHelloV1StorageBucketsPostRequest(server string, body HelloV1StorageBucketsPostJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewHelloV1StorageBucketsPostRequestWithBody(server, "application/json", bodyReader) -} - -// NewHelloV1StorageBucketsPostRequestWithBody generates requests for HelloV1StorageBucketsPost with any type of body -func NewHelloV1StorageBucketsPostRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/v1/storage-buckets") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewVersionVersionGetRequest generates requests for VersionVersionGet -func NewVersionVersionGetRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/version") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range c.RequestEditors { - if err := r(ctx, req); err != nil { - return err - } - } - for _, r := range additionalEditors { - if err := r(ctx, req); err != nil { - return err - } - } - return nil -} - -// ClientWithResponses builds on ClientInterface to offer response payloads -type ClientWithResponses struct { - ClientInterface -} - -// NewClientWithResponses creates a new ClientWithResponses, which wraps -// Client with return type handling -func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { - client, err := NewClient(server, opts...) - if err != nil { - return nil, err - } - return &ClientWithResponses{client}, nil -} - -// WithBaseURL overrides the baseURL. -func WithBaseURL(baseURL string) ClientOption { - return func(c *Client) error { - newBaseURL, err := url.Parse(baseURL) - if err != nil { - return err - } - c.Server = newBaseURL.String() - return nil - } -} - -// ClientWithResponsesInterface is the interface specification for the client with responses above. -type ClientWithResponsesInterface interface { - // CreateCaptainManifestsV1CaptainManifestsPostWithBodyWithResponse request with any body - CreateCaptainManifestsV1CaptainManifestsPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateCaptainManifestsV1CaptainManifestsPostResponse, error) - - CreateCaptainManifestsV1CaptainManifestsPostWithResponse(ctx context.Context, body CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateCaptainManifestsV1CaptainManifestsPostResponse, error) - - // DeleteChiselNodesV1ChiselDeleteWithBodyWithResponse request with any body - DeleteChiselNodesV1ChiselDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DeleteChiselNodesV1ChiselDeleteResponse, error) - - DeleteChiselNodesV1ChiselDeleteWithResponse(ctx context.Context, body DeleteChiselNodesV1ChiselDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*DeleteChiselNodesV1ChiselDeleteResponse, error) - - // CreateChiselNodesV1ChiselPostWithBodyWithResponse request with any body - CreateChiselNodesV1ChiselPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateChiselNodesV1ChiselPostResponse, error) - - CreateChiselNodesV1ChiselPostWithResponse(ctx context.Context, body CreateChiselNodesV1ChiselPostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateChiselNodesV1ChiselPostResponse, error) - - // GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBodyWithResponse request with any body - GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse, error) - - GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithResponse(ctx context.Context, body GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody, reqEditors ...RequestEditorFn) (*GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse, error) - - // NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBodyWithResponse request with any body - NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse, error) - - NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithResponse(ctx context.Context, body NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse, error) - - // NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBodyWithResponse request with any body - NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse, error) - - NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithResponse(ctx context.Context, body NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse, error) - - // CreateOpsgeniealertsManifestV1OpsgeniePostWithBodyWithResponse request with any body - CreateOpsgeniealertsManifestV1OpsgeniePostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateOpsgeniealertsManifestV1OpsgeniePostResponse, error) - - CreateOpsgeniealertsManifestV1OpsgeniePostWithResponse(ctx context.Context, body CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateOpsgeniealertsManifestV1OpsgeniePostResponse, error) - - // ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBodyWithResponse request with any body - ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse, error) - - ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithResponse(ctx context.Context, body ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse, error) - - // CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBodyWithResponse request with any body - CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse, error) - - CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithResponse(ctx context.Context, body CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse, error) - - // HelloV1StorageBucketsPostWithBodyWithResponse request with any body - HelloV1StorageBucketsPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*HelloV1StorageBucketsPostResponse, error) - - HelloV1StorageBucketsPostWithResponse(ctx context.Context, body HelloV1StorageBucketsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*HelloV1StorageBucketsPostResponse, error) - - // VersionVersionGetWithResponse request - VersionVersionGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*VersionVersionGetResponse, error) -} - -type CreateCaptainManifestsV1CaptainManifestsPostResponse struct { - Body []byte - HTTPResponse *http.Response - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r CreateCaptainManifestsV1CaptainManifestsPostResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r CreateCaptainManifestsV1CaptainManifestsPostResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type DeleteChiselNodesV1ChiselDeleteResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *interface{} - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r DeleteChiselNodesV1ChiselDeleteResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r DeleteChiselNodesV1ChiselDeleteResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type CreateChiselNodesV1ChiselPostResponse struct { - Body []byte - HTTPResponse *http.Response - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r CreateChiselNodesV1ChiselPostResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r CreateChiselNodesV1ChiselPostResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *interface{} - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *interface{} - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *interface{} - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type CreateOpsgeniealertsManifestV1OpsgeniePostResponse struct { - Body []byte - HTTPResponse *http.Response - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r CreateOpsgeniealertsManifestV1OpsgeniePostResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r CreateOpsgeniealertsManifestV1OpsgeniePostResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *interface{} - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse struct { - Body []byte - HTTPResponse *http.Response - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type HelloV1StorageBucketsPostResponse struct { - Body []byte - HTTPResponse *http.Response - JSON422 *HTTPValidationError -} - -// Status returns HTTPResponse.Status -func (r HelloV1StorageBucketsPostResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r HelloV1StorageBucketsPostResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type VersionVersionGetResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *VersionResponse -} - -// Status returns HTTPResponse.Status -func (r VersionVersionGetResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r VersionVersionGetResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// CreateCaptainManifestsV1CaptainManifestsPostWithBodyWithResponse request with arbitrary body returning *CreateCaptainManifestsV1CaptainManifestsPostResponse -func (c *ClientWithResponses) CreateCaptainManifestsV1CaptainManifestsPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateCaptainManifestsV1CaptainManifestsPostResponse, error) { - rsp, err := c.CreateCaptainManifestsV1CaptainManifestsPostWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateCaptainManifestsV1CaptainManifestsPostResponse(rsp) -} - -func (c *ClientWithResponses) CreateCaptainManifestsV1CaptainManifestsPostWithResponse(ctx context.Context, body CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateCaptainManifestsV1CaptainManifestsPostResponse, error) { - rsp, err := c.CreateCaptainManifestsV1CaptainManifestsPost(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateCaptainManifestsV1CaptainManifestsPostResponse(rsp) -} - -// DeleteChiselNodesV1ChiselDeleteWithBodyWithResponse request with arbitrary body returning *DeleteChiselNodesV1ChiselDeleteResponse -func (c *ClientWithResponses) DeleteChiselNodesV1ChiselDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DeleteChiselNodesV1ChiselDeleteResponse, error) { - rsp, err := c.DeleteChiselNodesV1ChiselDeleteWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseDeleteChiselNodesV1ChiselDeleteResponse(rsp) -} - -func (c *ClientWithResponses) DeleteChiselNodesV1ChiselDeleteWithResponse(ctx context.Context, body DeleteChiselNodesV1ChiselDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*DeleteChiselNodesV1ChiselDeleteResponse, error) { - rsp, err := c.DeleteChiselNodesV1ChiselDelete(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseDeleteChiselNodesV1ChiselDeleteResponse(rsp) -} - -// CreateChiselNodesV1ChiselPostWithBodyWithResponse request with arbitrary body returning *CreateChiselNodesV1ChiselPostResponse -func (c *ClientWithResponses) CreateChiselNodesV1ChiselPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateChiselNodesV1ChiselPostResponse, error) { - rsp, err := c.CreateChiselNodesV1ChiselPostWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateChiselNodesV1ChiselPostResponse(rsp) -} - -func (c *ClientWithResponses) CreateChiselNodesV1ChiselPostWithResponse(ctx context.Context, body CreateChiselNodesV1ChiselPostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateChiselNodesV1ChiselPostResponse, error) { - rsp, err := c.CreateChiselNodesV1ChiselPost(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateChiselNodesV1ChiselPostResponse(rsp) -} - -// GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBodyWithResponse request with arbitrary body returning *GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse -func (c *ClientWithResponses) GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse, error) { - rsp, err := c.GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse(rsp) -} - -func (c *ClientWithResponses) GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithResponse(ctx context.Context, body GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody, reqEditors ...RequestEditorFn) (*GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse, error) { - rsp, err := c.GetWorkflowRunStatusV1GithubWorkflowRunStatusPost(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse(rsp) -} - -// NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBodyWithResponse request with arbitrary body returning *NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse -func (c *ClientWithResponses) NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse, error) { - rsp, err := c.NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse(rsp) -} - -func (c *ClientWithResponses) NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithResponse(ctx context.Context, body NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse, error) { - rsp, err := c.NukeAwsCaptainAccountV1NukeAwsCaptainAccountDelete(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse(rsp) -} - -// NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBodyWithResponse request with arbitrary body returning *NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse -func (c *ClientWithResponses) NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse, error) { - rsp, err := c.NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse(rsp) -} - -func (c *ClientWithResponses) NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithResponse(ctx context.Context, body NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse, error) { - rsp, err := c.NukeCaptainDomainDataV1NukeCaptainDomainDataDelete(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse(rsp) -} - -// CreateOpsgeniealertsManifestV1OpsgeniePostWithBodyWithResponse request with arbitrary body returning *CreateOpsgeniealertsManifestV1OpsgeniePostResponse -func (c *ClientWithResponses) CreateOpsgeniealertsManifestV1OpsgeniePostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateOpsgeniealertsManifestV1OpsgeniePostResponse, error) { - rsp, err := c.CreateOpsgeniealertsManifestV1OpsgeniePostWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateOpsgeniealertsManifestV1OpsgeniePostResponse(rsp) -} - -func (c *ClientWithResponses) CreateOpsgeniealertsManifestV1OpsgeniePostWithResponse(ctx context.Context, body CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateOpsgeniealertsManifestV1OpsgeniePostResponse, error) { - rsp, err := c.CreateOpsgeniealertsManifestV1OpsgeniePost(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateOpsgeniealertsManifestV1OpsgeniePostResponse(rsp) -} - -// ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBodyWithResponse request with arbitrary body returning *ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse -func (c *ClientWithResponses) ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse, error) { - rsp, err := c.ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse(rsp) -} - -func (c *ClientWithResponses) ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithResponse(ctx context.Context, body ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse, error) { - rsp, err := c.ResetGithubOrganizationV1ResetGithubOrganizationDelete(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse(rsp) -} - -// CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBodyWithResponse request with arbitrary body returning *CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse -func (c *ClientWithResponses) CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse, error) { - rsp, err := c.CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse(rsp) -} - -func (c *ClientWithResponses) CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithResponse(ctx context.Context, body CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse, error) { - rsp, err := c.CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPost(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse(rsp) -} - -// HelloV1StorageBucketsPostWithBodyWithResponse request with arbitrary body returning *HelloV1StorageBucketsPostResponse -func (c *ClientWithResponses) HelloV1StorageBucketsPostWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*HelloV1StorageBucketsPostResponse, error) { - rsp, err := c.HelloV1StorageBucketsPostWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseHelloV1StorageBucketsPostResponse(rsp) -} - -func (c *ClientWithResponses) HelloV1StorageBucketsPostWithResponse(ctx context.Context, body HelloV1StorageBucketsPostJSONRequestBody, reqEditors ...RequestEditorFn) (*HelloV1StorageBucketsPostResponse, error) { - rsp, err := c.HelloV1StorageBucketsPost(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseHelloV1StorageBucketsPostResponse(rsp) -} - -// VersionVersionGetWithResponse request returning *VersionVersionGetResponse -func (c *ClientWithResponses) VersionVersionGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*VersionVersionGetResponse, error) { - rsp, err := c.VersionVersionGet(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParseVersionVersionGetResponse(rsp) -} - -// ParseCreateCaptainManifestsV1CaptainManifestsPostResponse parses an HTTP response from a CreateCaptainManifestsV1CaptainManifestsPostWithResponse call -func ParseCreateCaptainManifestsV1CaptainManifestsPostResponse(rsp *http.Response) (*CreateCaptainManifestsV1CaptainManifestsPostResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &CreateCaptainManifestsV1CaptainManifestsPostResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseDeleteChiselNodesV1ChiselDeleteResponse parses an HTTP response from a DeleteChiselNodesV1ChiselDeleteWithResponse call -func ParseDeleteChiselNodesV1ChiselDeleteResponse(rsp *http.Response) (*DeleteChiselNodesV1ChiselDeleteResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &DeleteChiselNodesV1ChiselDeleteResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest interface{} - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseCreateChiselNodesV1ChiselPostResponse parses an HTTP response from a CreateChiselNodesV1ChiselPostWithResponse call -func ParseCreateChiselNodesV1ChiselPostResponse(rsp *http.Response) (*CreateChiselNodesV1ChiselPostResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &CreateChiselNodesV1ChiselPostResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseGetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse parses an HTTP response from a GetWorkflowRunStatusV1GithubWorkflowRunStatusPostWithResponse call -func ParseGetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse(rsp *http.Response) (*GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetWorkflowRunStatusV1GithubWorkflowRunStatusPostResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest interface{} - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse parses an HTTP response from a NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteWithResponse call -func ParseNukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse(rsp *http.Response) (*NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest interface{} - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse parses an HTTP response from a NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteWithResponse call -func ParseNukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse(rsp *http.Response) (*NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest interface{} - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseCreateOpsgeniealertsManifestV1OpsgeniePostResponse parses an HTTP response from a CreateOpsgeniealertsManifestV1OpsgeniePostWithResponse call -func ParseCreateOpsgeniealertsManifestV1OpsgeniePostResponse(rsp *http.Response) (*CreateOpsgeniealertsManifestV1OpsgeniePostResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &CreateOpsgeniealertsManifestV1OpsgeniePostResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse parses an HTTP response from a ResetGithubOrganizationV1ResetGithubOrganizationDeleteWithResponse call -func ParseResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse(rsp *http.Response) (*ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &ResetGithubOrganizationV1ResetGithubOrganizationDeleteResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest interface{} - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse parses an HTTP response from a CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostWithResponse call -func ParseCreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse(rsp *http.Response) (*CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseHelloV1StorageBucketsPostResponse parses an HTTP response from a HelloV1StorageBucketsPostWithResponse call -func ParseHelloV1StorageBucketsPostResponse(rsp *http.Response) (*HelloV1StorageBucketsPostResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &HelloV1StorageBucketsPostResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest HTTPValidationError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - } - - return response, nil -} - -// ParseVersionVersionGetResponse parses an HTTP response from a VersionVersionGetWithResponse call -func ParseVersionVersionGetResponse(rsp *http.Response) (*VersionVersionGetResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &VersionVersionGetResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest VersionResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} diff --git a/cli/cmd/aws.go b/cli/cmd/aws.go index d221d5c..7a53c24 100644 --- a/cli/cmd/aws.go +++ b/cli/cmd/aws.go @@ -1,9 +1,6 @@ package cmd import ( - "context" - - "github.com/GlueOps/tools-api/cli/api" "github.com/GlueOps/tools-api/cli/internal/spec" "github.com/spf13/cobra" ) @@ -15,20 +12,17 @@ var awsCmd = &cobra.Command{ var awsSetupCredentialsCmd = &cobra.Command{ Use: "setup-credentials", - Short: spec.Summary("/v1/setup-aws-account-credentials", "post", "Get admin credentials for an AWS sub-account"), - Long: spec.Description("/v1/setup-aws-account-credentials", "post", ""), + Short: spec.Summary("/v1/aws/credentials", "post", "Get admin credentials for an AWS sub-account"), + Long: spec.Description("/v1/aws/credentials", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { accountName, _ := cmd.Flags().GetString("account-name") client, err := newClient() if err != nil { return err } - resp, err := client.CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPost( - context.Background(), - api.CreateCredentialsForAwsCaptainAccountV1SetupAwsAccountCredentialsPostJSONRequestBody{ - AwsSubAccountName: accountName, - }, - ) + resp, err := client.post("/v1/aws/credentials", map[string]string{ + "aws_sub_account_name": accountName, + }) if err != nil { return err } @@ -38,20 +32,17 @@ var awsSetupCredentialsCmd = &cobra.Command{ var awsNukeAccountCmd = &cobra.Command{ Use: "nuke-account", - Short: spec.Summary("/v1/nuke-aws-captain-account", "delete", "Nuke an AWS sub-account"), - Long: spec.Description("/v1/nuke-aws-captain-account", "delete", ""), + Short: spec.Summary("/v1/aws/nuke", "post", "Nuke an AWS sub-account"), + Long: spec.Description("/v1/aws/nuke", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { accountName, _ := cmd.Flags().GetString("account-name") client, err := newClient() if err != nil { return err } - resp, err := client.NukeAwsCaptainAccountV1NukeAwsCaptainAccountDelete( - context.Background(), - api.NukeAwsCaptainAccountV1NukeAwsCaptainAccountDeleteJSONRequestBody{ - AwsSubAccountName: accountName, - }, - ) + resp, err := client.post("/v1/aws/nuke", map[string]string{ + "aws_sub_account_name": accountName, + }) if err != nil { return err } @@ -60,10 +51,10 @@ var awsNukeAccountCmd = &cobra.Command{ } func init() { - awsSetupCredentialsCmd.Flags().String("account-name", "", spec.FlagDesc("AWS sub-account name", "AwsCredentialsRequest", "aws_sub_account_name")) + awsSetupCredentialsCmd.Flags().String("account-name", "", spec.FlagDesc("AWS sub-account name", "AwsCredentialsRequestBody", "aws_sub_account_name")) awsSetupCredentialsCmd.MarkFlagRequired("account-name") - awsNukeAccountCmd.Flags().String("account-name", "", spec.FlagDesc("AWS sub-account name to nuke", "AwsNukeAccountRequest", "aws_sub_account_name")) + awsNukeAccountCmd.Flags().String("account-name", "", spec.FlagDesc("AWS sub-account name to nuke", "AwsNukeAccountRequestBody", "aws_sub_account_name")) awsNukeAccountCmd.MarkFlagRequired("account-name") awsCmd.AddCommand(awsSetupCredentialsCmd) diff --git a/cli/cmd/captain_manifests.go b/cli/cmd/captain_manifests.go index ecd1d45..19dcb25 100644 --- a/cli/cmd/captain_manifests.go +++ b/cli/cmd/captain_manifests.go @@ -1,9 +1,6 @@ package cmd import ( - "context" - - "github.com/GlueOps/tools-api/cli/api" "github.com/GlueOps/tools-api/cli/internal/spec" "github.com/spf13/cobra" ) @@ -15,8 +12,8 @@ var captainManifestsCmd = &cobra.Command{ var captainManifestsGenerateCmd = &cobra.Command{ Use: "generate", - Short: spec.Summary("/v1/captain-manifests", "post", "Generate captain manifests"), - Long: spec.Description("/v1/captain-manifests", "post", ""), + Short: spec.Summary("/v1/captain/manifests", "post", "Generate captain manifests"), + Long: spec.Description("/v1/captain/manifests", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { captainDomain, _ := cmd.Flags().GetString("captain-domain") orgName, _ := cmd.Flags().GetString("org-name") @@ -26,14 +23,11 @@ var captainManifestsGenerateCmd = &cobra.Command{ if err != nil { return err } - resp, err := client.CreateCaptainManifestsV1CaptainManifestsPost( - context.Background(), - api.CreateCaptainManifestsV1CaptainManifestsPostJSONRequestBody{ - CaptainDomain: captainDomain, - TenantGithubOrganizationName: orgName, - TenantDeploymentConfigurationsRepositoryName: repoName, - }, - ) + resp, err := client.post("/v1/captain/manifests", map[string]string{ + "captain_domain": captainDomain, + "tenant_github_organization_name": orgName, + "tenant_deployment_configurations_repository_name": repoName, + }) if err != nil { return err } @@ -42,11 +36,11 @@ var captainManifestsGenerateCmd = &cobra.Command{ } func init() { - captainManifestsGenerateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "CaptainManifestsRequest", "captain_domain")) + captainManifestsGenerateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "CaptainManifestsRequestBody", "captain_domain")) captainManifestsGenerateCmd.MarkFlagRequired("captain-domain") - captainManifestsGenerateCmd.Flags().String("org-name", "", spec.FlagDesc("Tenant GitHub organization name", "CaptainManifestsRequest", "tenant_github_organization_name")) + captainManifestsGenerateCmd.Flags().String("org-name", "", spec.FlagDesc("Tenant GitHub organization name", "CaptainManifestsRequestBody", "tenant_github_organization_name")) captainManifestsGenerateCmd.MarkFlagRequired("org-name") - captainManifestsGenerateCmd.Flags().String("repo-name", "", spec.FlagDesc("Deployment configurations repository name", "CaptainManifestsRequest", "tenant_deployment_configurations_repository_name")) + captainManifestsGenerateCmd.Flags().String("repo-name", "", spec.FlagDesc("Deployment configurations repository name", "CaptainManifestsRequestBody", "tenant_deployment_configurations_repository_name")) captainManifestsGenerateCmd.MarkFlagRequired("repo-name") captainManifestsCmd.AddCommand(captainManifestsGenerateCmd) diff --git a/cli/cmd/chisel.go b/cli/cmd/chisel.go index 8ab1e9c..310f851 100644 --- a/cli/cmd/chisel.go +++ b/cli/cmd/chisel.go @@ -1,9 +1,6 @@ package cmd import ( - "context" - - "github.com/GlueOps/tools-api/cli/api" "github.com/GlueOps/tools-api/cli/internal/spec" "github.com/spf13/cobra" ) @@ -25,13 +22,10 @@ var chiselCreateCmd = &cobra.Command{ if err != nil { return err } - resp, err := client.CreateChiselNodesV1ChiselPost( - context.Background(), - api.CreateChiselNodesV1ChiselPostJSONRequestBody{ - CaptainDomain: captainDomain, - NodeCount: &nodeCount, - }, - ) + resp, err := client.post("/v1/chisel", map[string]interface{}{ + "captain_domain": captainDomain, + "node_count": nodeCount, + }) if err != nil { return err } @@ -41,20 +35,17 @@ var chiselCreateCmd = &cobra.Command{ var chiselDeleteCmd = &cobra.Command{ Use: "delete", - Short: spec.Summary("/v1/chisel", "delete", "Delete chisel nodes"), - Long: spec.Description("/v1/chisel", "delete", ""), + Short: spec.Summary("/v1/chisel/delete", "post", "Delete chisel nodes"), + Long: spec.Description("/v1/chisel/delete", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { captainDomain, _ := cmd.Flags().GetString("captain-domain") client, err := newClient() if err != nil { return err } - resp, err := client.DeleteChiselNodesV1ChiselDelete( - context.Background(), - api.DeleteChiselNodesV1ChiselDeleteJSONRequestBody{ - CaptainDomain: captainDomain, - }, - ) + resp, err := client.post("/v1/chisel/delete", map[string]string{ + "captain_domain": captainDomain, + }) if err != nil { return err } @@ -63,11 +54,11 @@ var chiselDeleteCmd = &cobra.Command{ } func init() { - chiselCreateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "ChiselNodesRequest", "captain_domain")) + chiselCreateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "ChiselNodesRequestBody", "captain_domain")) chiselCreateCmd.MarkFlagRequired("captain-domain") - chiselCreateCmd.Flags().Int("node-count", 3, spec.FlagDesc("Number of exit nodes (1-6)", "ChiselNodesRequest", "node_count")) + chiselCreateCmd.Flags().Int("node-count", 3, spec.FlagDesc("Number of exit nodes (1-6)", "ChiselNodesRequestBody", "node_count")) - chiselDeleteCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "ChiselNodesDeleteRequest", "captain_domain")) + chiselDeleteCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "ChiselNodesDeleteRequestBody", "captain_domain")) chiselDeleteCmd.MarkFlagRequired("captain-domain") chiselCmd.AddCommand(chiselCreateCmd) diff --git a/cli/cmd/client.go b/cli/cmd/client.go index 3541085..da0c3b9 100644 --- a/cli/cmd/client.go +++ b/cli/cmd/client.go @@ -2,30 +2,61 @@ package cmd import ( "bytes" - "context" "encoding/json" "fmt" "io" "net/http" - - "github.com/GlueOps/tools-api/cli/api" + "net/url" + "time" ) +// apiClient is a simple HTTP client wrapper for the Tools API. +type apiClient struct { + baseURL string + token string + httpClient *http.Client +} + // newClient creates an authenticated API client. -func newClient() (*api.Client, error) { +func newClient() (*apiClient, error) { token, err := GetAuthToken() if err != nil { return nil, err } + return &apiClient{ + baseURL: apiURL, + token: token, + httpClient: &http.Client{Timeout: 120 * time.Second}, + }, nil +} - client, err := api.NewClient(apiURL, api.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { - req.Header.Set("Authorization", "Bearer "+token) - return nil - })) +// post sends a POST request with a JSON body. +func (c *apiClient) post(path string, body interface{}) (*http.Response, error) { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + req, err := http.NewRequest(http.MethodPost, c.baseURL+path, bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.token) + return c.httpClient.Do(req) +} + +// get sends a GET request with optional query parameters. +func (c *apiClient) get(path string, params url.Values) (*http.Response, error) { + u := c.baseURL + path + if len(params) > 0 { + u += "?" + params.Encode() + } + req, err := http.NewRequest(http.MethodGet, u, nil) if err != nil { - return nil, fmt.Errorf("failed to create API client: %w", err) + return nil, fmt.Errorf("failed to create request: %w", err) } - return client, nil + req.Header.Set("Authorization", "Bearer "+c.token) + return c.httpClient.Do(req) } // handleResponse reads and prints the response body, returning an error for non-2xx status. diff --git a/cli/cmd/github.go b/cli/cmd/github.go index a89b4e9..2079d0d 100644 --- a/cli/cmd/github.go +++ b/cli/cmd/github.go @@ -1,9 +1,8 @@ package cmd import ( - "context" + "net/url" - "github.com/GlueOps/tools-api/cli/api" "github.com/GlueOps/tools-api/cli/internal/spec" "github.com/spf13/cobra" ) @@ -15,8 +14,8 @@ var githubCmd = &cobra.Command{ var githubResetOrgCmd = &cobra.Command{ Use: "reset-org", - Short: spec.Summary("/v1/reset-github-organization", "delete", "Reset a GitHub organization"), - Long: spec.Description("/v1/reset-github-organization", "delete", ""), + Short: spec.Summary("/v1/github/reset-org", "post", "Reset a GitHub organization"), + Long: spec.Description("/v1/github/reset-org", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { captainDomain, _ := cmd.Flags().GetString("captain-domain") deleteAllRepos, _ := cmd.Flags().GetBool("delete-all-repos") @@ -27,15 +26,12 @@ var githubResetOrgCmd = &cobra.Command{ if err != nil { return err } - resp, err := client.ResetGithubOrganizationV1ResetGithubOrganizationDelete( - context.Background(), - api.ResetGithubOrganizationV1ResetGithubOrganizationDeleteJSONRequestBody{ - CaptainDomain: captainDomain, - DeleteAllExistingRepos: deleteAllRepos, - CustomDomain: customDomain, - EnableCustomDomain: enableCustomDomain, - }, - ) + resp, err := client.post("/v1/github/reset-org", map[string]interface{}{ + "captain_domain": captainDomain, + "delete_all_existing_repos": deleteAllRepos, + "custom_domain": customDomain, + "enable_custom_domain": enableCustomDomain, + }) if err != nil { return err } @@ -45,20 +41,17 @@ var githubResetOrgCmd = &cobra.Command{ var githubWorkflowStatusCmd = &cobra.Command{ Use: "workflow-status", - Short: spec.Summary("/v1/github/workflow-run-status", "post", "Get the status of a GitHub Actions workflow run"), - Long: spec.Description("/v1/github/workflow-run-status", "post", ""), + Short: spec.Summary("/v1/github/workflow-status", "get", "Get the status of a GitHub Actions workflow run"), + Long: spec.Description("/v1/github/workflow-status", "get", ""), RunE: func(cmd *cobra.Command, args []string) error { runURL, _ := cmd.Flags().GetString("run-url") client, err := newClient() if err != nil { return err } - resp, err := client.GetWorkflowRunStatusV1GithubWorkflowRunStatusPost( - context.Background(), - api.GetWorkflowRunStatusV1GithubWorkflowRunStatusPostJSONRequestBody{ - RunUrl: runURL, - }, - ) + params := url.Values{} + params.Set("run_url", runURL) + resp, err := client.get("/v1/github/workflow-status", params) if err != nil { return err } @@ -67,10 +60,10 @@ var githubWorkflowStatusCmd = &cobra.Command{ } func init() { - githubResetOrgCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "ResetGitHubOrganizationRequest", "captain_domain")) + githubResetOrgCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "ResetGitHubOrganizationRequestBody", "captain_domain")) githubResetOrgCmd.MarkFlagRequired("captain-domain") githubResetOrgCmd.Flags().Bool("delete-all-repos", true, "Delete all existing repos") - githubResetOrgCmd.Flags().String("custom-domain", "", spec.FlagDesc("Custom domain", "ResetGitHubOrganizationRequest", "custom_domain")) + githubResetOrgCmd.Flags().String("custom-domain", "", spec.FlagDesc("Custom domain", "ResetGitHubOrganizationRequestBody", "custom_domain")) githubResetOrgCmd.Flags().Bool("enable-custom-domain", false, "Enable custom domain") githubWorkflowStatusCmd.Flags().String("run-url", "", spec.FlagDesc("GitHub Actions run URL", "GitHubWorkflowRunStatusRequest", "run_url")) diff --git a/cli/cmd/nuke.go b/cli/cmd/nuke.go index 1b112cc..ce77809 100644 --- a/cli/cmd/nuke.go +++ b/cli/cmd/nuke.go @@ -1,9 +1,6 @@ package cmd import ( - "context" - - "github.com/GlueOps/tools-api/cli/api" "github.com/GlueOps/tools-api/cli/internal/spec" "github.com/spf13/cobra" ) @@ -15,20 +12,17 @@ var nukeCmd = &cobra.Command{ var nukeCaptainDomainDataCmd = &cobra.Command{ Use: "captain-domain-data", - Short: spec.Summary("/v1/nuke-captain-domain-data", "delete", "Delete all backups/data for a captain domain"), - Long: spec.Description("/v1/nuke-captain-domain-data", "delete", ""), + Short: spec.Summary("/v1/nuke/domain-data", "post", "Delete all backups/data for a captain domain"), + Long: spec.Description("/v1/nuke/domain-data", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { captainDomain, _ := cmd.Flags().GetString("captain-domain") client, err := newClient() if err != nil { return err } - resp, err := client.NukeCaptainDomainDataV1NukeCaptainDomainDataDelete( - context.Background(), - api.NukeCaptainDomainDataV1NukeCaptainDomainDataDeleteJSONRequestBody{ - CaptainDomain: captainDomain, - }, - ) + resp, err := client.post("/v1/nuke/domain-data", map[string]string{ + "captain_domain": captainDomain, + }) if err != nil { return err } @@ -37,7 +31,7 @@ var nukeCaptainDomainDataCmd = &cobra.Command{ } func init() { - nukeCaptainDomainDataCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain to nuke", "CaptainDomainNukeDataAndBackupsRequest", "captain_domain")) + nukeCaptainDomainDataCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain to nuke", "CaptainDomainNukeDataAndBackupsRequestBody", "captain_domain")) nukeCaptainDomainDataCmd.MarkFlagRequired("captain-domain") nukeCmd.AddCommand(nukeCaptainDomainDataCmd) rootCmd.AddCommand(nukeCmd) diff --git a/cli/cmd/opsgenie.go b/cli/cmd/opsgenie.go index fc73362..b571423 100644 --- a/cli/cmd/opsgenie.go +++ b/cli/cmd/opsgenie.go @@ -1,9 +1,6 @@ package cmd import ( - "context" - - "github.com/GlueOps/tools-api/cli/api" "github.com/GlueOps/tools-api/cli/internal/spec" "github.com/spf13/cobra" ) @@ -15,8 +12,8 @@ var opsgenieCmd = &cobra.Command{ var opsgenieCreateCmd = &cobra.Command{ Use: "create", - Short: spec.Summary("/v1/opsgenie", "post", "Create Opsgenie alerts manifest"), - Long: spec.Description("/v1/opsgenie", "post", ""), + Short: spec.Summary("/v1/opsgenie/manifest", "post", "Create Opsgenie alerts manifest"), + Long: spec.Description("/v1/opsgenie/manifest", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { captainDomain, _ := cmd.Flags().GetString("captain-domain") apiKey, _ := cmd.Flags().GetString("api-key") @@ -25,13 +22,10 @@ var opsgenieCreateCmd = &cobra.Command{ if err != nil { return err } - resp, err := client.CreateOpsgeniealertsManifestV1OpsgeniePost( - context.Background(), - api.CreateOpsgeniealertsManifestV1OpsgeniePostJSONRequestBody{ - CaptainDomain: captainDomain, - OpsgenieApiKey: apiKey, - }, - ) + resp, err := client.post("/v1/opsgenie/manifest", map[string]string{ + "captain_domain": captainDomain, + "opsgenie_api_key": apiKey, + }) if err != nil { return err } @@ -40,9 +34,9 @@ var opsgenieCreateCmd = &cobra.Command{ } func init() { - opsgenieCreateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "OpsgenieAlertsManifestRequest", "captain_domain")) + opsgenieCreateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "OpsgenieAlertsManifestRequestBody", "captain_domain")) opsgenieCreateCmd.MarkFlagRequired("captain-domain") - opsgenieCreateCmd.Flags().String("api-key", "", spec.FlagDesc("Opsgenie API key", "OpsgenieAlertsManifestRequest", "opsgenie_api_key")) + opsgenieCreateCmd.Flags().String("api-key", "", spec.FlagDesc("Opsgenie API key", "OpsgenieAlertsManifestRequestBody", "opsgenie_api_key")) opsgenieCreateCmd.MarkFlagRequired("api-key") opsgenieCmd.AddCommand(opsgenieCreateCmd) diff --git a/cli/cmd/storage_buckets.go b/cli/cmd/storage_buckets.go index 2524410..e28c4f8 100644 --- a/cli/cmd/storage_buckets.go +++ b/cli/cmd/storage_buckets.go @@ -1,9 +1,6 @@ package cmd import ( - "context" - - "github.com/GlueOps/tools-api/cli/api" "github.com/GlueOps/tools-api/cli/internal/spec" "github.com/spf13/cobra" ) @@ -15,16 +12,16 @@ var storageBucketsCmd = &cobra.Command{ var storageBucketsCreateCmd = &cobra.Command{ Use: "create", - Short: spec.Summary("/v1/storage-buckets", "post", "Create/re-create storage buckets"), - Long: spec.Description("/v1/storage-buckets", "post", ""), + Short: spec.Summary("/v1/storage/buckets", "post", "Create/re-create storage buckets"), + Long: spec.Description("/v1/storage/buckets", "post", ""), RunE: func(cmd *cobra.Command, args []string) error { captainDomain, _ := cmd.Flags().GetString("captain-domain") client, err := newClient() if err != nil { return err } - resp, err := client.HelloV1StorageBucketsPost(context.Background(), api.HelloV1StorageBucketsPostJSONRequestBody{ - CaptainDomain: captainDomain, + resp, err := client.post("/v1/storage/buckets", map[string]string{ + "captain_domain": captainDomain, }) if err != nil { return err @@ -34,7 +31,7 @@ var storageBucketsCreateCmd = &cobra.Command{ } func init() { - storageBucketsCreateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "StorageBucketsRequest", "captain_domain")) + storageBucketsCreateCmd.Flags().String("captain-domain", "", spec.FlagDesc("Captain domain", "StorageBucketsRequestBody", "captain_domain")) storageBucketsCreateCmd.MarkFlagRequired("captain-domain") storageBucketsCmd.AddCommand(storageBucketsCreateCmd) rootCmd.AddCommand(storageBucketsCmd) diff --git a/cli/go.mod b/cli/go.mod index a381dfd..48c9054 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -2,14 +2,9 @@ module github.com/GlueOps/tools-api/cli go 1.24 -require ( - github.com/oapi-codegen/runtime v1.2.0 - github.com/spf13/cobra v1.10.2 -) +require github.com/spf13/cobra v1.10.2 require ( - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index 1e221c7..ef5d78d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,32 +1,11 @@ -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= -github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/internal/spec/openapi.json b/cli/internal/spec/openapi.json index 929c8f7..dc5dc34 100644 --- a/cli/internal/spec/openapi.json +++ b/cli/internal/spec/openapi.json @@ -1,21 +1,437 @@ { - "openapi": "3.1.0", + "components": { + "schemas": { + "AwsCredentialsRequestBody": { + "additionalProperties": false, + "properties": { + "aws_sub_account_name": { + "description": "AWS sub-account name", + "examples": [ + "glueops-captain-foobar" + ], + "minLength": 1, + "type": "string" + } + }, + "required": [ + "aws_sub_account_name" + ], + "type": "object" + }, + "AwsNukeAccountRequestBody": { + "additionalProperties": false, + "properties": { + "aws_sub_account_name": { + "description": "AWS sub-account name", + "examples": [ + "glueops-captain-foobar" + ], + "minLength": 1, + "type": "string" + } + }, + "required": [ + "aws_sub_account_name" + ], + "type": "object" + }, + "CaptainDomainNukeDataAndBackupsRequestBody": { + "additionalProperties": false, + "properties": { + "captain_domain": { + "description": "Captain domain for the cluster", + "examples": [ + "nonprod.foobar.onglueops.rocks" + ], + "minLength": 1, + "type": "string" + } + }, + "required": [ + "captain_domain" + ], + "type": "object" + }, + "CaptainManifestsRequestBody": { + "additionalProperties": false, + "properties": { + "captain_domain": { + "description": "Captain domain for the cluster", + "examples": [ + "nonprod.foobar.onglueops.rocks" + ], + "minLength": 1, + "type": "string" + }, + "tenant_deployment_configurations_repository_name": { + "description": "Tenant deployment configurations repository name", + "examples": [ + "deployment-configurations" + ], + "minLength": 1, + "type": "string" + }, + "tenant_github_organization_name": { + "description": "Tenant GitHub organization name", + "examples": [ + "development-tenant-foobar" + ], + "minLength": 1, + "type": "string" + } + }, + "required": [ + "captain_domain", + "tenant_github_organization_name", + "tenant_deployment_configurations_repository_name" + ], + "type": "object" + }, + "ChiselNodesDeleteRequestBody": { + "additionalProperties": false, + "properties": { + "captain_domain": { + "description": "Captain domain for the cluster", + "examples": [ + "nonprod.foobar.onglueops.rocks" + ], + "minLength": 1, + "type": "string" + } + }, + "required": [ + "captain_domain" + ], + "type": "object" + }, + "ChiselNodesRequestBody": { + "additionalProperties": false, + "properties": { + "captain_domain": { + "description": "Captain domain for the cluster", + "examples": [ + "nonprod.foobar.onglueops.rocks" + ], + "minLength": 1, + "type": "string" + }, + "node_count": { + "default": 3, + "description": "Number of exit nodes to create (1-6, default: 3)", + "examples": [ + 3 + ], + "format": "int64", + "maximum": 6, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "captain_domain", + "node_count" + ], + "type": "object" + }, + "ErrorResponse": { + "additionalProperties": false, + "properties": { + "detail": { + "description": "Error detail message", + "examples": [ + "An internal server error occurred." + ], + "type": "string" + }, + "status": { + "description": "HTTP status code", + "examples": [ + 500 + ], + "format": "int64", + "type": "integer" + } + }, + "required": [ + "status", + "detail" + ], + "type": "object" + }, + "MessageResponseBody": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Response message", + "examples": [ + "Success" + ], + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "OpsgenieAlertsManifestRequestBody": { + "additionalProperties": false, + "properties": { + "captain_domain": { + "description": "Captain domain for the cluster", + "examples": [ + "nonprod.foobar.onglueops.rocks" + ], + "minLength": 1, + "type": "string" + }, + "opsgenie_api_key": { + "description": "Opsgenie API key", + "examples": [ + "6825b4ef-4e84-44a1-8450-b46b02852add" + ], + "minLength": 1, + "type": "string" + } + }, + "required": [ + "captain_domain", + "opsgenie_api_key" + ], + "type": "object" + }, + "ResetGitHubOrganizationRequestBody": { + "additionalProperties": false, + "properties": { + "captain_domain": { + "description": "Captain domain for the cluster", + "examples": [ + "nonprod.foobar.onglueops.rocks" + ], + "minLength": 1, + "type": "string" + }, + "custom_domain": { + "description": "Custom domain for the organization", + "examples": [ + "example.com" + ], + "minLength": 1, + "type": "string" + }, + "delete_all_existing_repos": { + "description": "Whether to delete all existing repos in the organization", + "examples": [ + true + ], + "type": "boolean" + }, + "enable_custom_domain": { + "description": "Whether to enable the custom domain", + "examples": [ + false + ], + "type": "boolean" + } + }, + "required": [ + "captain_domain", + "delete_all_existing_repos", + "custom_domain", + "enable_custom_domain" + ], + "type": "object" + }, + "StorageBucketsRequestBody": { + "additionalProperties": false, + "properties": { + "captain_domain": { + "description": "Captain domain for the cluster", + "examples": [ + "nonprod.foobar.onglueops.rocks" + ], + "minLength": 1, + "type": "string" + } + }, + "required": [ + "captain_domain" + ], + "type": "object" + }, + "VersionResponseBody": { + "additionalProperties": false, + "properties": { + "build_timestamp": { + "description": "Build timestamp", + "examples": [ + "2026-01-01T00:00:00Z" + ], + "type": "string" + }, + "commit_sha": { + "description": "Full commit SHA", + "examples": [ + "abc1234567890def1234567890abcdef12345678" + ], + "type": "string" + }, + "git_ref": { + "description": "Git ref used for the build", + "examples": [ + "main" + ], + "type": "string" + }, + "short_sha": { + "description": "Short commit SHA", + "examples": [ + "abc1234" + ], + "type": "string" + }, + "version": { + "description": "Application version", + "examples": [ + "v1.0.0" + ], + "type": "string" + } + }, + "required": [ + "version", + "commit_sha", + "short_sha", + "build_timestamp", + "git_ref" + ], + "type": "object" + }, + "WorkflowDispatchResponseBody": { + "additionalProperties": false, + "properties": { + "all_jobs_url": { + "description": "URL to view all jobs for the workflow run", + "examples": [ + "https://github.com/org/repo/actions/runs/12345678/jobs" + ], + "type": "string" + }, + "run_id": { + "description": "Workflow run ID, null if polling timed out", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "run_url": { + "description": "URL to the workflow run, null if polling timed out", + "type": [ + "string", + "null" + ] + }, + "status_code": { + "description": "HTTP status code from GitHub API", + "examples": [ + 200 + ], + "format": "int64", + "type": "integer" + } + }, + "required": [ + "status_code", + "run_id", + "run_url", + "all_jobs_url" + ], + "type": "object" + }, + "WorkflowRunStatusResponseBody": { + "additionalProperties": false, + "properties": { + "conclusion": { + "description": "Conclusion of the workflow run, null if still in progress", + "type": [ + "string", + "null" + ] + }, + "created_at": { + "description": "Timestamp when the workflow run was created", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Workflow run name", + "type": [ + "string", + "null" + ] + }, + "run_id": { + "description": "Workflow run ID", + "examples": [ + 12345678 + ], + "format": "int64", + "type": "integer" + }, + "run_url": { + "description": "URL to the workflow run", + "examples": [ + "https://github.com/org/repo/actions/runs/12345678" + ], + "type": "string" + }, + "status": { + "description": "Current status of the workflow run", + "examples": [ + "completed" + ], + "type": "string" + }, + "updated_at": { + "description": "Timestamp when the workflow run was last updated", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "run_id", + "name", + "status", + "conclusion", + "run_url", + "created_at", + "updated_at" + ], + "type": "object" + } + } + }, "info": { - "title": "Tools API", "description": "Various APIs to help you speed up your development and testing.", + "title": "Tools API", "version": "UNKNOWN" }, + "openapi": "3.1.0", "paths": { - "/v1/storage-buckets": { + "/v1/aws/credentials": { "post": { - "summary": "Create/Re-create storage buckets that can be used for V2 of our monitoring stack that is Otel based", - "description": "Note: this can be a DESTRUCTIVE operation\nFor the provided captain_domain, this will DELETE and then create new/empty storage buckets for loki, tempo, and thanos.", - "operationId": "hello_v1_storage_buckets_post", + "description": "If you are testing in AWS/EKS you will need an AWS account to test with. This request will provide you with admin level credentials to the sub account you specify.\nThis can also be used to just get Admin access to a desired sub account.", + "operationId": "create-aws-credentials", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StorageBucketsRequest" + "$ref": "#/components/schemas/AwsCredentialsRequestBody" } } }, @@ -23,38 +439,31 @@ }, "responses": { "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Whether it's to create an EKS cluster or to test other things out in an isolated AWS account. These creds will give you Admin level access to the requested account." } }, - "/v1/setup-aws-account-credentials": { + "/v1/aws/nuke": { "post": { - "summary": "Whether it's to create an EKS cluster or to test other things out in an isolated AWS account. These creds will give you Admin level access to the requested account.", - "description": "If you are testing in AWS/EKS you will need an AWS account to test with. This request will provide you with admin level credentials to the sub account you specify.\nThis can also be used to just get Admin access to a desired sub account.", - "operationId": "create_credentials_for_aws_captain_account_v1_setup_aws_account_credentials_post", + "description": "Submit the AWS account name you want to nuke (e.g. glueops-captain-foobar)", + "operationId": "nuke-aws-account", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AwsCredentialsRequest" + "$ref": "#/components/schemas/AwsNukeAccountRequestBody" } } }, @@ -62,38 +471,38 @@ }, "responses": { "200": { - "description": "Successful Response", "content": { - "text/plain": { + "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/WorkflowDispatchResponseBody" } } - } + }, + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Run this after you are done testing within AWS. This will clean up orphaned resources. Note: you may have to run this 2x." } }, - "/v1/nuke-aws-captain-account": { - "delete": { - "summary": "Run this after you are done testing within AWS. This will clean up orphaned resources. Note: you may have to run this 2x.", - "description": "Submit the AWS account name you want to nuke (e.g. glueops-captain-foobar)", - "operationId": "nuke_aws_captain_account_v1_nuke_aws_captain_account_delete", + "/v1/captain/manifests": { + "post": { + "description": "Generate YAML manifests for captain deployments based on the provided configuration.", + "operationId": "create-captain-manifests", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AwsNukeAccountRequest" + "$ref": "#/components/schemas/CaptainManifestsRequestBody" } } }, @@ -101,36 +510,31 @@ }, "responses": { "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Generate captain manifests" } }, - "/v1/nuke-captain-domain-data": { - "delete": { - "summary": "Deletes all backups/data for a provided captain_domain. Running this before a cluster creation helps ensure a clean environment.", - "description": "Submit the captain_domain/tenant you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will delete all backups and data for the provided captain_domain.\n\nThis will remove things like the vault and cert-manager backups.\n\nNote: this may not delete things like Loki/Thanos/Tempo data as that may be managed outside of AWS.", - "operationId": "nuke_captain_domain_data_v1_nuke_captain_domain_data_delete", + "/v1/chisel": { + "post": { + "description": "If you are testing within k3ds you will need chisel to provide you with load balancers.\nFor a provided captain_domain this will delete any existing chisel nodes and provision new ones.\nNote: this will generally result in new IPs being provisioned.", + "operationId": "create-chisel-nodes", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaptainDomainNukeDataAndBackupsRequest" + "$ref": "#/components/schemas/ChiselNodesRequestBody" } } }, @@ -138,36 +542,31 @@ }, "responses": { "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Creates Chisel nodes for dev/k3d clusters. This allows us to mimic a Cloud Controller for Loadbalancers (e.g. NLBs with EKS)" } }, - "/v1/reset-github-organization": { - "delete": { - "summary": "Resets the GitHub Organization to make it easier to get a new dev cluster runner for Dev", - "description": "Submit the dev captain_domain you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will reset the GitHub organization so that you can easily get up and running with a new dev cluster.\n\nThis will reset your deployment-configurations repository, it'll bring over a working regcred, and application repos with working github actions so that you can quickly work on the GlueOps stack.\n\nWARNING: By default delete_all_existing_repos = True. Please set it to False or make a manual backup if you are concerned about any data loss within your tenant org (e.g. github.com/development-tenant-*)", - "operationId": "reset_github_organization_v1_reset_github_organization_delete", + "/v1/chisel/delete": { + "post": { + "description": "When you are done testing with k3ds this will delete your chisel nodes and save on costs.", + "operationId": "delete-chisel-nodes", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResetGitHubOrganizationRequest" + "$ref": "#/components/schemas/ChiselNodesDeleteRequestBody" } } }, @@ -175,36 +574,38 @@ }, "responses": { "200": { - "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/MessageResponseBody" + } } - } + }, + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Deletes your chisel nodes. Please run this when you are done with development to save on costs." } }, - "/v1/github/workflow-run-status": { + "/v1/github/reset-org": { "post": { - "summary": "Get the status of a GitHub Actions workflow run", - "description": "Provide a GitHub Actions run URL (e.g. https://github.com/owner/repo/actions/runs/12345678) and get the current status of that workflow run.\nWorks for any repo the configured GITHUB_TOKEN has read access to.", - "operationId": "get_workflow_run_status_v1_github_workflow_run_status_post", + "description": "Submit the dev captain_domain you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will reset the GitHub organization so that you can easily get up and running with a new dev cluster.\n\nThis will reset your deployment-configurations repository, it'll bring over a working regcred, and application repos with working github actions so that you can quickly work on the GlueOps stack.\n\nWARNING: By default delete_all_existing_repos = True. Please set it to False or make a manual backup if you are concerned about any data loss within your tenant org (e.g. github.com/development-tenant-*)", + "operationId": "reset-github-organization", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GitHubWorkflowRunStatusRequest" + "$ref": "#/components/schemas/ResetGitHubOrganizationRequestBody" } } }, @@ -212,73 +613,84 @@ }, "responses": { "200": { - "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/WorkflowDispatchResponseBody" + } } - } + }, + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Resets the GitHub Organization to make it easier to get a new dev cluster runner for Dev" } }, - "/v1/chisel": { - "post": { - "summary": "Creates Chisel nodes for dev/k3d clusters. This allows us to mimic a Cloud Controller for Loadbalancers (e.g. NLBs with EKS)", - "description": "If you are testing within k3ds you will need chisel to provide you with load balancers.\nFor a provided captain_domain this will delete any existing chisel nodes and provision new ones.\nNote: this will generally result in new IPs being provisioned.", - "operationId": "create_chisel_nodes_v1_chisel_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChiselNodesRequest" - } + "/v1/github/workflow-status": { + "get": { + "description": "Provide a GitHub Actions run URL (e.g. https://github.com/owner/repo/actions/runs/12345678) and get the current status of that workflow run.\nWorks for any repo the configured GITHUB_TOKEN has read access to.", + "operationId": "get-workflow-run-status", + "parameters": [ + { + "description": "GitHub Actions run URL", + "explode": false, + "in": "query", + "name": "run_url", + "required": true, + "schema": { + "description": "GitHub Actions run URL", + "examples": [ + "https://github.com/internal-GlueOps/gha-tools-api/actions/runs/12345678" + ], + "minLength": 1, + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Successful Response", "content": { - "text/plain": { + "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/WorkflowRunStatusResponseBody" } } - } + }, + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } - }, - "delete": { - "summary": "Deletes your chisel nodes. Please run this when you are done with development to save on costs.", - "description": "When you are done testing with k3ds this will delete your chisel nodes and save on costs.", - "operationId": "delete_chisel_nodes_v1_chisel_delete", + }, + "summary": "Get the status of a GitHub Actions workflow run" + } + }, + "/v1/nuke/domain-data": { + "post": { + "description": "Submit the captain_domain/tenant you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will delete all backups and data for the provided captain_domain.\n\nThis will remove things like the vault and cert-manager backups.\n\nNote: this may not delete things like Loki/Thanos/Tempo data as that may be managed outside of AWS.", + "operationId": "nuke-captain-domain-data", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChiselNodesDeleteRequest" + "$ref": "#/components/schemas/CaptainDomainNukeDataAndBackupsRequestBody" } } }, @@ -286,36 +698,38 @@ }, "responses": { "200": { - "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/WorkflowDispatchResponseBody" + } } - } + }, + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Deletes all backups/data for a provided captain_domain. Running this before a cluster creation helps ensure a clean environment." } }, - "/v1/opsgenie": { + "/v1/opsgenie/manifest": { "post": { - "summary": "Creates Opsgenie Alerts Manifest", "description": "Create a opsgenie/alertmanager configuration. Do this for any clusters you want alerts on.", - "operationId": "create_opsgeniealerts_manifest_v1_opsgenie_post", + "operationId": "create-opsgenie-manifest", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OpsgenieAlertsManifestRequest" + "$ref": "#/components/schemas/OpsgenieAlertsManifestRequestBody" } } }, @@ -323,38 +737,31 @@ }, "responses": { "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Creates Opsgenie Alerts Manifest" } }, - "/v1/captain-manifests": { + "/v1/storage/buckets": { "post": { - "summary": "Generate captain manifests", - "description": "Generate YAML manifests for captain deployments based on the provided configuration.", - "operationId": "create_captain_manifests_v1_captain_manifests_post", + "description": "Note: this can be a DESTRUCTIVE operation. For the provided captain_domain, this will DELETE and then create new/empty storage buckets for loki, tempo, and thanos.", + "operationId": "create-storage-buckets", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaptainManifestsRequest" + "$ref": "#/components/schemas/StorageBucketsRequestBody" } } }, @@ -362,324 +769,48 @@ }, "responses": { "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } + "description": "OK" }, - "422": { - "description": "Validation Error", + "default": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorResponse" } } - } + }, + "description": "Error" } - } + }, + "summary": "Create/Re-create storage buckets that can be used for V2 of our monitoring stack that is Otel based" } }, "/version": { "get": { - "summary": "Contains version information about this tools-api", - "operationId": "version_version_get", + "operationId": "get-version", "responses": { "200": { - "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VersionResponse" + "$ref": "#/components/schemas/VersionResponseBody" } } - } - } - } - } - } - }, - "components": { - "schemas": { - "AwsCredentialsRequest": { - "properties": { - "aws_sub_account_name": { - "type": "string", - "title": "Aws Sub Account Name", - "example": "glueops-captain-foobar" - } - }, - "type": "object", - "required": [ - "aws_sub_account_name" - ], - "title": "AwsCredentialsRequest" - }, - "AwsNukeAccountRequest": { - "properties": { - "aws_sub_account_name": { - "type": "string", - "title": "Aws Sub Account Name", - "example": "glueops-captain-foobar" - } - }, - "type": "object", - "required": [ - "aws_sub_account_name" - ], - "title": "AwsNukeAccountRequest" - }, - "CaptainDomainNukeDataAndBackupsRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "CaptainDomainNukeDataAndBackupsRequest" - }, - "CaptainManifestsRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "tenant_github_organization_name": { - "type": "string", - "title": "Tenant Github Organization Name", - "example": "development-tenant-foobar" - }, - "tenant_deployment_configurations_repository_name": { - "type": "string", - "title": "Tenant Deployment Configurations Repository Name", - "example": "deployment-configurations" - } - }, - "type": "object", - "required": [ - "captain_domain", - "tenant_github_organization_name", - "tenant_deployment_configurations_repository_name" - ], - "title": "CaptainManifestsRequest" - }, - "ChiselNodesDeleteRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "ChiselNodesDeleteRequest" - }, - "ChiselNodesRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "node_count": { - "type": "integer", - "maximum": 6.0, - "minimum": 1.0, - "title": "Node Count", - "description": "Number of exit nodes to create (1-6, default: 3)", - "default": 3, - "example": 3 - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "ChiselNodesRequest" - }, - "GitHubWorkflowRunStatusRequest": { - "properties": { - "run_url": { - "type": "string", - "title": "Run Url", - "example": "https://github.com/internal-GlueOps/gha-tools-api/actions/runs/12345678" - } - }, - "type": "object", - "required": [ - "run_url" - ], - "title": "GitHubWorkflowRunStatusRequest" - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" }, - "type": "array", - "title": "Detail" - } - }, - "type": "object", - "title": "HTTPValidationError" - }, - "OpsgenieAlertsManifestRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "opsgenie_api_key": { - "type": "string", - "title": "Opsgenie Api Key", - "example": "6825b4ef-4e84-44a1-8450-b46b02852add" - } - }, - "type": "object", - "required": [ - "captain_domain", - "opsgenie_api_key" - ], - "title": "OpsgenieAlertsManifestRequest" - }, - "ResetGitHubOrganizationRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "delete_all_existing_repos": { - "type": "boolean", - "title": "Delete All Existing Repos", - "example": true - }, - "custom_domain": { - "type": "string", - "title": "Custom Domain", - "example": "example.com" + "description": "OK" }, - "enable_custom_domain": { - "type": "boolean", - "title": "Enable Custom Domain", - "example": false - } - }, - "type": "object", - "required": [ - "captain_domain", - "delete_all_existing_repos", - "custom_domain", - "enable_custom_domain" - ], - "title": "ResetGitHubOrganizationRequest" - }, - "StorageBucketsRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "StorageBucketsRequest" - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } - ] + } }, - "type": "array", - "title": "Location" - }, - "msg": { - "type": "string", - "title": "Message" - }, - "type": { - "type": "string", - "title": "Error Type" - }, - "input": { - "title": "Input" - }, - "ctx": { - "type": "object", - "title": "Context" + "description": "Error" } }, - "type": "object", - "required": [ - "loc", - "msg", - "type" - ], - "title": "ValidationError" - }, - "VersionResponse": { - "properties": { - "version": { - "type": "string", - "title": "Version", - "example": "v1.0.0" - }, - "commit_sha": { - "type": "string", - "title": "Commit Sha", - "example": "abc1234567890def1234567890abcdef12345678" - }, - "short_sha": { - "type": "string", - "title": "Short Sha", - "example": "abc1234" - }, - "build_timestamp": { - "type": "string", - "title": "Build Timestamp", - "example": "2026-01-01T00:00:00Z" - }, - "git_ref": { - "type": "string", - "title": "Git Ref", - "example": "main" - } - }, - "type": "object", - "required": [ - "version", - "commit_sha", - "short_sha", - "build_timestamp", - "git_ref" - ], - "title": "VersionResponse" + "summary": "Contains version information about this tools-api" } } } diff --git a/cli/internal/spec/spec.go b/cli/internal/spec/spec.go index 89ab873..6e416c4 100644 --- a/cli/internal/spec/spec.go +++ b/cli/internal/spec/spec.go @@ -20,8 +20,9 @@ type schema struct { Components struct { Schemas map[string]struct { Properties map[string]struct { - Example json.RawMessage `json:"example"` - Description string `json:"description"` + Example json.RawMessage `json:"example"` + Examples []json.RawMessage `json:"examples"` + Description string `json:"description"` } `json:"properties"` } `json:"schemas"` } `json:"components"` @@ -36,6 +37,7 @@ func init() { } // Example returns the example value for a schema field, or "" if not found. +// Supports both OpenAPI 3.0 "example" (single value) and 3.1 "examples" (array). func Example(schemaName, fieldName string) string { s, ok := parsed.Components.Schemas[schemaName] if !ok { @@ -45,11 +47,21 @@ func Example(schemaName, fieldName string) string { if !ok { return "" } - var val interface{} - if err := json.Unmarshal(prop.Example, &val); err != nil { - return "" + // Try "example" first (OpenAPI 3.0 style). + if len(prop.Example) > 0 { + var val interface{} + if err := json.Unmarshal(prop.Example, &val); err == nil { + return fmt.Sprintf("%v", val) + } + } + // Fall back to "examples" array (OpenAPI 3.1 / Huma style). + if len(prop.Examples) > 0 { + var val interface{} + if err := json.Unmarshal(prop.Examples[0], &val); err == nil { + return fmt.Sprintf("%v", val) + } } - return fmt.Sprintf("%v", val) + return "" } // FlagDesc returns a flag description with the example from the OpenAPI spec appended. diff --git a/cli/oapi-codegen.yaml b/cli/oapi-codegen.yaml deleted file mode 100644 index d281e67..0000000 --- a/cli/oapi-codegen.yaml +++ /dev/null @@ -1,5 +0,0 @@ -package: api -output: api/generated.go -generate: - models: true - client: true diff --git a/cli/openapi.json b/cli/openapi.json deleted file mode 100644 index 929c8f7..0000000 --- a/cli/openapi.json +++ /dev/null @@ -1,686 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "Tools API", - "description": "Various APIs to help you speed up your development and testing.", - "version": "UNKNOWN" - }, - "paths": { - "/v1/storage-buckets": { - "post": { - "summary": "Create/Re-create storage buckets that can be used for V2 of our monitoring stack that is Otel based", - "description": "Note: this can be a DESTRUCTIVE operation\nFor the provided captain_domain, this will DELETE and then create new/empty storage buckets for loki, tempo, and thanos.", - "operationId": "hello_v1_storage_buckets_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StorageBucketsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/setup-aws-account-credentials": { - "post": { - "summary": "Whether it's to create an EKS cluster or to test other things out in an isolated AWS account. These creds will give you Admin level access to the requested account.", - "description": "If you are testing in AWS/EKS you will need an AWS account to test with. This request will provide you with admin level credentials to the sub account you specify.\nThis can also be used to just get Admin access to a desired sub account.", - "operationId": "create_credentials_for_aws_captain_account_v1_setup_aws_account_credentials_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AwsCredentialsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/nuke-aws-captain-account": { - "delete": { - "summary": "Run this after you are done testing within AWS. This will clean up orphaned resources. Note: you may have to run this 2x.", - "description": "Submit the AWS account name you want to nuke (e.g. glueops-captain-foobar)", - "operationId": "nuke_aws_captain_account_v1_nuke_aws_captain_account_delete", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AwsNukeAccountRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/nuke-captain-domain-data": { - "delete": { - "summary": "Deletes all backups/data for a provided captain_domain. Running this before a cluster creation helps ensure a clean environment.", - "description": "Submit the captain_domain/tenant you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will delete all backups and data for the provided captain_domain.\n\nThis will remove things like the vault and cert-manager backups.\n\nNote: this may not delete things like Loki/Thanos/Tempo data as that may be managed outside of AWS.", - "operationId": "nuke_captain_domain_data_v1_nuke_captain_domain_data_delete", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaptainDomainNukeDataAndBackupsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/reset-github-organization": { - "delete": { - "summary": "Resets the GitHub Organization to make it easier to get a new dev cluster runner for Dev", - "description": "Submit the dev captain_domain you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will reset the GitHub organization so that you can easily get up and running with a new dev cluster.\n\nThis will reset your deployment-configurations repository, it'll bring over a working regcred, and application repos with working github actions so that you can quickly work on the GlueOps stack.\n\nWARNING: By default delete_all_existing_repos = True. Please set it to False or make a manual backup if you are concerned about any data loss within your tenant org (e.g. github.com/development-tenant-*)", - "operationId": "reset_github_organization_v1_reset_github_organization_delete", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResetGitHubOrganizationRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/github/workflow-run-status": { - "post": { - "summary": "Get the status of a GitHub Actions workflow run", - "description": "Provide a GitHub Actions run URL (e.g. https://github.com/owner/repo/actions/runs/12345678) and get the current status of that workflow run.\nWorks for any repo the configured GITHUB_TOKEN has read access to.", - "operationId": "get_workflow_run_status_v1_github_workflow_run_status_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GitHubWorkflowRunStatusRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/chisel": { - "post": { - "summary": "Creates Chisel nodes for dev/k3d clusters. This allows us to mimic a Cloud Controller for Loadbalancers (e.g. NLBs with EKS)", - "description": "If you are testing within k3ds you will need chisel to provide you with load balancers.\nFor a provided captain_domain this will delete any existing chisel nodes and provision new ones.\nNote: this will generally result in new IPs being provisioned.", - "operationId": "create_chisel_nodes_v1_chisel_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChiselNodesRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - }, - "delete": { - "summary": "Deletes your chisel nodes. Please run this when you are done with development to save on costs.", - "description": "When you are done testing with k3ds this will delete your chisel nodes and save on costs.", - "operationId": "delete_chisel_nodes_v1_chisel_delete", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChiselNodesDeleteRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/opsgenie": { - "post": { - "summary": "Creates Opsgenie Alerts Manifest", - "description": "Create a opsgenie/alertmanager configuration. Do this for any clusters you want alerts on.", - "operationId": "create_opsgeniealerts_manifest_v1_opsgenie_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OpsgenieAlertsManifestRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/captain-manifests": { - "post": { - "summary": "Generate captain manifests", - "description": "Generate YAML manifests for captain deployments based on the provided configuration.", - "operationId": "create_captain_manifests_v1_captain_manifests_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaptainManifestsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/version": { - "get": { - "summary": "Contains version information about this tools-api", - "operationId": "version_version_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VersionResponse" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "AwsCredentialsRequest": { - "properties": { - "aws_sub_account_name": { - "type": "string", - "title": "Aws Sub Account Name", - "example": "glueops-captain-foobar" - } - }, - "type": "object", - "required": [ - "aws_sub_account_name" - ], - "title": "AwsCredentialsRequest" - }, - "AwsNukeAccountRequest": { - "properties": { - "aws_sub_account_name": { - "type": "string", - "title": "Aws Sub Account Name", - "example": "glueops-captain-foobar" - } - }, - "type": "object", - "required": [ - "aws_sub_account_name" - ], - "title": "AwsNukeAccountRequest" - }, - "CaptainDomainNukeDataAndBackupsRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "CaptainDomainNukeDataAndBackupsRequest" - }, - "CaptainManifestsRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "tenant_github_organization_name": { - "type": "string", - "title": "Tenant Github Organization Name", - "example": "development-tenant-foobar" - }, - "tenant_deployment_configurations_repository_name": { - "type": "string", - "title": "Tenant Deployment Configurations Repository Name", - "example": "deployment-configurations" - } - }, - "type": "object", - "required": [ - "captain_domain", - "tenant_github_organization_name", - "tenant_deployment_configurations_repository_name" - ], - "title": "CaptainManifestsRequest" - }, - "ChiselNodesDeleteRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "ChiselNodesDeleteRequest" - }, - "ChiselNodesRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "node_count": { - "type": "integer", - "maximum": 6.0, - "minimum": 1.0, - "title": "Node Count", - "description": "Number of exit nodes to create (1-6, default: 3)", - "default": 3, - "example": 3 - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "ChiselNodesRequest" - }, - "GitHubWorkflowRunStatusRequest": { - "properties": { - "run_url": { - "type": "string", - "title": "Run Url", - "example": "https://github.com/internal-GlueOps/gha-tools-api/actions/runs/12345678" - } - }, - "type": "object", - "required": [ - "run_url" - ], - "title": "GitHubWorkflowRunStatusRequest" - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail" - } - }, - "type": "object", - "title": "HTTPValidationError" - }, - "OpsgenieAlertsManifestRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "opsgenie_api_key": { - "type": "string", - "title": "Opsgenie Api Key", - "example": "6825b4ef-4e84-44a1-8450-b46b02852add" - } - }, - "type": "object", - "required": [ - "captain_domain", - "opsgenie_api_key" - ], - "title": "OpsgenieAlertsManifestRequest" - }, - "ResetGitHubOrganizationRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - }, - "delete_all_existing_repos": { - "type": "boolean", - "title": "Delete All Existing Repos", - "example": true - }, - "custom_domain": { - "type": "string", - "title": "Custom Domain", - "example": "example.com" - }, - "enable_custom_domain": { - "type": "boolean", - "title": "Enable Custom Domain", - "example": false - } - }, - "type": "object", - "required": [ - "captain_domain", - "delete_all_existing_repos", - "custom_domain", - "enable_custom_domain" - ], - "title": "ResetGitHubOrganizationRequest" - }, - "StorageBucketsRequest": { - "properties": { - "captain_domain": { - "type": "string", - "title": "Captain Domain", - "example": "nonprod.foobar.onglueops.rocks" - } - }, - "type": "object", - "required": [ - "captain_domain" - ], - "title": "StorageBucketsRequest" - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "type": "array", - "title": "Location" - }, - "msg": { - "type": "string", - "title": "Message" - }, - "type": { - "type": "string", - "title": "Error Type" - }, - "input": { - "title": "Input" - }, - "ctx": { - "type": "object", - "title": "Context" - } - }, - "type": "object", - "required": [ - "loc", - "msg", - "type" - ], - "title": "ValidationError" - }, - "VersionResponse": { - "properties": { - "version": { - "type": "string", - "title": "Version", - "example": "v1.0.0" - }, - "commit_sha": { - "type": "string", - "title": "Commit Sha", - "example": "abc1234567890def1234567890abcdef12345678" - }, - "short_sha": { - "type": "string", - "title": "Short Sha", - "example": "abc1234" - }, - "build_timestamp": { - "type": "string", - "title": "Build Timestamp", - "example": "2026-01-01T00:00:00Z" - }, - "git_ref": { - "type": "string", - "title": "Git Ref", - "example": "main" - } - }, - "type": "object", - "required": [ - "version", - "commit_sha", - "short_sha", - "build_timestamp", - "git_ref" - ], - "title": "VersionResponse" - } - } - } -} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..c80a36e --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,260 @@ +package main + +import ( + "context" + "log/slog" + "net/http" + "os" + "os/signal" + "runtime" + "strings" + "syscall" + "time" + + "github.com/GlueOps/tools-api/internal/version" + "github.com/GlueOps/tools-api/pkg/handlers" + "github.com/GlueOps/tools-api/pkg/types" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humachi" + "github.com/go-chi/chi/v5" +) + +func main() { + // Configure structured JSON logging via log/slog. + level := parseLogLevel(os.Getenv("LOG_LEVEL")) + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))) + + // Override Huma's default error creator to return our clean ErrorResponse format + // (risk M3). Stack traces are logged server-side only — never sent to clients. + huma.NewError = func(status int, msg string, errs ...error) huma.StatusError { + detail := msg + if detail == "" { + detail = http.StatusText(status) + } + // Log stack trace server-side for 5xx errors. + if status >= 500 { + buf := make([]byte, 4096) + n := runtime.Stack(buf, false) + errMsgs := make([]string, 0, len(errs)) + for _, e := range errs { + errMsgs = append(errMsgs, e.Error()) + } + slog.Error("server error", + "status", status, + "detail", detail, + "errors", errMsgs, + "stack", string(buf[:n]), + ) + } else if len(errs) > 0 { + // For 4xx, append validation details to the message. + parts := []string{detail} + for _, e := range errs { + parts = append(parts, e.Error()) + } + detail = strings.Join(parts, ": ") + } + return &types.ErrorResponse{ + Status: status, + Detail: detail, + } + } + + router := chi.NewMux() + + // Audit logging middleware: log every request with authenticated user identity. + router.Use(auditLogMiddleware) + + // Redirect GET / to /docs (excluded from OpenAPI). + router.Get("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/docs", http.StatusTemporaryRedirect) + }) + + // Create Huma API with application metadata. + // Use a custom config without the SchemaLinkTransformer so responses + // don't include a "$schema" field (matching Python/FastAPI behavior). + appVersion := version.Version + schemaPrefix := "#/components/schemas/" + registry := huma.NewMapRegistry(schemaPrefix, huma.DefaultSchemaNamer) + config := huma.Config{ + OpenAPI: &huma.OpenAPI{ + OpenAPI: "3.1.0", + Info: &huma.Info{ + Title: "Tools API", + Version: appVersion, + Description: "Various APIs to help you speed up your development and testing.", + }, + Components: &huma.Components{ + Schemas: registry, + }, + }, + OpenAPIPath: "/openapi", + DocsPath: "/docs", + DocsRenderer: huma.DocsRendererStoplightElements, + SchemasPath: "/schemas", + Formats: huma.DefaultFormats, + DefaultFormat: "application/json", + } + + api := humachi.New(router, config) + + // Health endpoint registered directly on chi (excluded from OpenAPI schema). + router.Get("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"healthy"}`)) + }) + + // Register version endpoint. + huma.Register(api, huma.Operation{ + OperationID: "get-version", + Method: http.MethodGet, + Path: "/version", + Summary: "Contains version information about this tools-api", + }, handlers.GetVersion) + + // Register storage buckets endpoint (ticket 04). + huma.Register(api, huma.Operation{ + OperationID: "create-storage-buckets", + Method: http.MethodPost, + Path: "/v1/storage/buckets", + Summary: "Create/Re-create storage buckets that can be used for V2 of our monitoring stack that is Otel based", + Description: "Note: this can be a DESTRUCTIVE operation. For the provided captain_domain, this will DELETE and then create new/empty storage buckets for loki, tempo, and thanos.", + }, handlers.CreateStorageBuckets) + + // Register AWS credentials endpoint (ticket 07). + huma.Register(api, huma.Operation{ + OperationID: "create-aws-credentials", + Method: http.MethodPost, + Path: "/v1/aws/credentials", + Summary: "Whether it's to create an EKS cluster or to test other things out in an isolated AWS account. These creds will give you Admin level access to the requested account.", + Description: "If you are testing in AWS/EKS you will need an AWS account to test with. This request will provide you with admin level credentials to the sub account you specify.\nThis can also be used to just get Admin access to a desired sub account.", + }, handlers.CreateAwsCredentials) + + // Register GitHub workflow dispatch endpoints (ticket 05). + huma.Register(api, huma.Operation{ + OperationID: "nuke-aws-account", + Method: http.MethodPost, + Path: "/v1/aws/nuke", + Summary: "Run this after you are done testing within AWS. This will clean up orphaned resources. Note: you may have to run this 2x.", + Description: "Submit the AWS account name you want to nuke (e.g. glueops-captain-foobar)", + }, handlers.NukeAwsAccount) + + huma.Register(api, huma.Operation{ + OperationID: "nuke-captain-domain-data", + Method: http.MethodPost, + Path: "/v1/nuke/domain-data", + Summary: "Deletes all backups/data for a provided captain_domain. Running this before a cluster creation helps ensure a clean environment.", + Description: "Submit the captain_domain/tenant you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will delete all backups and data for the provided captain_domain.\n\nThis will remove things like the vault and cert-manager backups.\n\nNote: this may not delete things like Loki/Thanos/Tempo data as that may be managed outside of AWS.", + }, handlers.NukeCaptainDomainData) + + huma.Register(api, huma.Operation{ + OperationID: "reset-github-organization", + Method: http.MethodPost, + Path: "/v1/github/reset-org", + Summary: "Resets the GitHub Organization to make it easier to get a new dev cluster runner for Dev", + Description: "Submit the dev captain_domain you want to nuke (e.g. nonprod.foobar.onglueops.rocks). This will reset the GitHub organization so that you can easily get up and running with a new dev cluster.\n\nThis will reset your deployment-configurations repository, it'll bring over a working regcred, and application repos with working github actions so that you can quickly work on the GlueOps stack.\n\nWARNING: By default delete_all_existing_repos = True. Please set it to False or make a manual backup if you are concerned about any data loss within your tenant org (e.g. github.com/development-tenant-*)", + }, handlers.ResetGitHubOrganization) + + huma.Register(api, huma.Operation{ + OperationID: "get-workflow-run-status", + Method: http.MethodGet, + Path: "/v1/github/workflow-status", + Summary: "Get the status of a GitHub Actions workflow run", + Description: "Provide a GitHub Actions run URL (e.g. https://github.com/owner/repo/actions/runs/12345678) and get the current status of that workflow run.\nWorks for any repo the configured GITHUB_TOKEN has read access to.", + }, handlers.GetWorkflowRunStatus) + + // Register chisel endpoints (ticket 06). + huma.Register(api, huma.Operation{ + OperationID: "create-chisel-nodes", + Method: http.MethodPost, + Path: "/v1/chisel", + Summary: "Creates Chisel nodes for dev/k3d clusters. This allows us to mimic a Cloud Controller for Loadbalancers (e.g. NLBs with EKS)", + Description: "If you are testing within k3ds you will need chisel to provide you with load balancers.\nFor a provided captain_domain this will delete any existing chisel nodes and provision new ones.\nNote: this will generally result in new IPs being provisioned.", + }, handlers.CreateChiselNodes) + + huma.Register(api, huma.Operation{ + OperationID: "delete-chisel-nodes", + Method: http.MethodPost, + Path: "/v1/chisel/delete", + Summary: "Deletes your chisel nodes. Please run this when you are done with development to save on costs.", + Description: "When you are done testing with k3ds this will delete your chisel nodes and save on costs.", + }, handlers.DeleteChiselNodes) + + // Register opsgenie manifest endpoint (ticket 08). + huma.Register(api, huma.Operation{ + OperationID: "create-opsgenie-manifest", + Method: http.MethodPost, + Path: "/v1/opsgenie/manifest", + Summary: "Creates Opsgenie Alerts Manifest", + Description: "Create a opsgenie/alertmanager configuration. Do this for any clusters you want alerts on.", + }, handlers.CreateOpsgenieManifest) + + // Register captain manifests endpoint (ticket 10). + huma.Register(api, huma.Operation{ + OperationID: "create-captain-manifests", + Method: http.MethodPost, + Path: "/v1/captain/manifests", + Summary: "Generate captain manifests", + Description: "Generate YAML manifests for captain deployments based on the provided configuration.", + }, handlers.CreateCaptainManifests) + + // Start HTTP server on 0.0.0.0:8000. + addr := "0.0.0.0:8000" + srv := &http.Server{ + Addr: addr, + Handler: router, + } + + // Start server in a goroutine. + go func() { + slog.Info("starting server", "addr", addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Error("server failed", "error", err) + os.Exit(1) + } + }() + + // Graceful shutdown on SIGTERM/SIGINT (risk H4). + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT) + sig := <-quit + slog.Info("shutting down server", "signal", sig.String()) + + ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + slog.Error("server forced shutdown", "error", err) + } + slog.Info("server stopped") +} + +// auditLogMiddleware logs every request with the authenticated user identity +// from oauth2-proxy headers (X-Forwarded-User or X-Forwarded-Email). +func auditLogMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := r.Header.Get("X-Forwarded-User") + email := r.Header.Get("X-Forwarded-Email") + slog.Info("request", + "method", r.Method, + "path", r.URL.Path, + "remote_addr", r.RemoteAddr, + "user", user, + "email", email, + ) + next.ServeHTTP(w, r) + }) +} + +func parseLogLevel(s string) slog.Level { + switch strings.ToUpper(s) { + case "DEBUG": + return slog.LevelDebug + case "WARN", "WARNING": + return slog.LevelWarn + case "ERROR": + return slog.LevelError + default: + return slog.LevelInfo + } +} + diff --git a/devbox.json b/devbox.json deleted file mode 100644 index 8f7754d..0000000 --- a/devbox.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json", - "packages": [ - "python-full@3.13.5", - "pipenv@2026.0.3", - ], - "shell": { - "scripts": { - "dev": [ - "echo 'To install packages run: pipenv install'", - "echo 'To start the app run: fastapi dev'", - "devbox run pipenv shell" - ] - } - } -} diff --git a/devbox.lock b/devbox.lock deleted file mode 100644 index b946ac7..0000000 --- a/devbox.lock +++ /dev/null @@ -1,126 +0,0 @@ -{ - "lockfile_version": "1", - "packages": { - "pipenv@2026.0.3": { - "last_modified": "2026-03-02T11:03:52Z", - "resolved": "github:NixOS/nixpkgs/dc7513872406b53d2ff417a003895d6daffdff2f#pipenv", - "source": "devbox-search", - "version": "2026.0.3", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/ym6plbdz6xwifahings7d7kx66wzhm55-pipenv-2026.0.3", - "default": true - }, - { - "name": "dist", - "path": "/nix/store/5fhskadsr5isp14lxlsj77rppy1p6r42-pipenv-2026.0.3-dist" - } - ], - "store_path": "/nix/store/ym6plbdz6xwifahings7d7kx66wzhm55-pipenv-2026.0.3" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/vpwjifn3ns2r7n7l668mcbpgnigawmpa-pipenv-2026.0.3", - "default": true - }, - { - "name": "dist", - "path": "/nix/store/rslfp3p0i3dj7gncz82kswxdyfj29sxr-pipenv-2026.0.3-dist" - } - ], - "store_path": "/nix/store/vpwjifn3ns2r7n7l668mcbpgnigawmpa-pipenv-2026.0.3" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/g8sxbhjhrdi4v1nmbkdblswd3imvpack-pipenv-2026.0.3", - "default": true - }, - { - "name": "dist", - "path": "/nix/store/1gwk0636v3kssz4m6m549bvsapbbli1d-pipenv-2026.0.3-dist" - } - ], - "store_path": "/nix/store/g8sxbhjhrdi4v1nmbkdblswd3imvpack-pipenv-2026.0.3" - }, - "x86_64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/f88fp5klgmdln2n4ykyvjiq5qg6fzdkx-pipenv-2026.0.3", - "default": true - }, - { - "name": "dist", - "path": "/nix/store/x5p7gdq8r5dsvjkc6hvn438lyyvd97ij-pipenv-2026.0.3-dist" - } - ], - "store_path": "/nix/store/f88fp5klgmdln2n4ykyvjiq5qg6fzdkx-pipenv-2026.0.3" - } - } - }, - "python-full@3.13.5": { - "last_modified": "2025-07-28T17:09:23Z", - "plugin_version": "0.0.4", - "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#python313Full", - "source": "devbox-search", - "version": "3.13.5", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/fdgqvcsdlqjgzww2p17sy91vvx73cl7j-python3-3.13.5", - "default": true - } - ], - "store_path": "/nix/store/fdgqvcsdlqjgzww2p17sy91vvx73cl7j-python3-3.13.5" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/imjii7yg48sisx3bz5va5cminczbq6r6-python3-3.13.5", - "default": true - }, - { - "name": "debug", - "path": "/nix/store/j9bqd7hismpyqsd48sd9r824fs8wxld1-python3-3.13.5-debug" - } - ], - "store_path": "/nix/store/imjii7yg48sisx3bz5va5cminczbq6r6-python3-3.13.5" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/cm73ahxfy47vyxhj6ighqs0p64l31lyh-python3-3.13.5", - "default": true - } - ], - "store_path": "/nix/store/cm73ahxfy47vyxhj6ighqs0p64l31lyh-python3-3.13.5" - }, - "x86_64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/kl67434pm6yih34v0bz2ss0f5kzin00i-python3-3.13.5", - "default": true - }, - { - "name": "debug", - "path": "/nix/store/lxka855zb94nlg4ir5y9pi32yqlbzlkv-python3-3.13.5-debug" - } - ], - "store_path": "/nix/store/kl67434pm6yih34v0bz2ss0f5kzin00i-python3-3.13.5" - } - } - } - } -} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 0000000..161ee1b --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,1132 @@ +package e2e + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "runtime" + "strings" + "testing" + + "github.com/GlueOps/tools-api/internal/version" + "github.com/GlueOps/tools-api/pkg/handlers" + "github.com/GlueOps/tools-api/pkg/types" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humachi" + "github.com/go-chi/chi/v5" +) + +// newTestServer creates an httptest.Server with the full API routing configured, +// matching cmd/server/main.go. This allows integration-level testing of every +// endpoint without external service dependencies. +func newTestServer(t *testing.T) *httptest.Server { + t.Helper() + + // Override huma.NewError to match production error format (risk M3). + huma.NewError = func(status int, msg string, errs ...error) huma.StatusError { + detail := msg + if detail == "" { + detail = http.StatusText(status) + } + if status >= 500 { + buf := make([]byte, 4096) + n := runtime.Stack(buf, false) + _ = string(buf[:n]) + } else if len(errs) > 0 { + parts := []string{detail} + for _, e := range errs { + parts = append(parts, e.Error()) + } + detail = strings.Join(parts, ": ") + } + return &types.ErrorResponse{ + Status: status, + Detail: detail, + } + } + + router := chi.NewMux() + + router.Get("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/docs", http.StatusTemporaryRedirect) + }) + + schemaPrefix := "#/components/schemas/" + registry := huma.NewMapRegistry(schemaPrefix, huma.DefaultSchemaNamer) + config := huma.Config{ + OpenAPI: &huma.OpenAPI{ + OpenAPI: "3.1.0", + Info: &huma.Info{ + Title: "Tools API", + Version: version.Version, + Description: "Various APIs to help you speed up your development and testing.", + }, + Components: &huma.Components{ + Schemas: registry, + }, + }, + OpenAPIPath: "/openapi", + DocsPath: "/docs", + DocsRenderer: huma.DocsRendererStoplightElements, + SchemasPath: "/schemas", + Formats: huma.DefaultFormats, + DefaultFormat: "application/json", + } + + api := humachi.New(router, config) + + // Health endpoint (chi-direct, excluded from OpenAPI). + router.Get("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"healthy"}`)) + }) + + // Register all Huma endpoints matching main.go. + huma.Register(api, huma.Operation{ + OperationID: "get-version", + Method: http.MethodGet, + Path: "/version", + Summary: "Contains version information about this tools-api", + }, handlers.GetVersion) + + huma.Register(api, huma.Operation{ + OperationID: "create-storage-buckets", + Method: http.MethodPost, + Path: "/v1/storage/buckets", + Summary: "Create/Re-create storage buckets", + }, handlers.CreateStorageBuckets) + + huma.Register(api, huma.Operation{ + OperationID: "create-aws-credentials", + Method: http.MethodPost, + Path: "/v1/aws/credentials", + Summary: "AWS admin credentials", + }, handlers.CreateAwsCredentials) + + huma.Register(api, huma.Operation{ + OperationID: "nuke-aws-account", + Method: http.MethodPost, + Path: "/v1/aws/nuke", + Summary: "Nuke AWS account", + }, handlers.NukeAwsAccount) + + huma.Register(api, huma.Operation{ + OperationID: "nuke-captain-domain-data", + Method: http.MethodPost, + Path: "/v1/nuke/domain-data", + Summary: "Nuke captain domain data", + }, handlers.NukeCaptainDomainData) + + huma.Register(api, huma.Operation{ + OperationID: "reset-github-organization", + Method: http.MethodPost, + Path: "/v1/github/reset-org", + Summary: "Reset GitHub organization", + }, handlers.ResetGitHubOrganization) + + huma.Register(api, huma.Operation{ + OperationID: "get-workflow-run-status", + Method: http.MethodGet, + Path: "/v1/github/workflow-status", + Summary: "Get workflow run status", + }, handlers.GetWorkflowRunStatus) + + huma.Register(api, huma.Operation{ + OperationID: "create-chisel-nodes", + Method: http.MethodPost, + Path: "/v1/chisel", + Summary: "Create Chisel nodes", + }, handlers.CreateChiselNodes) + + huma.Register(api, huma.Operation{ + OperationID: "delete-chisel-nodes", + Method: http.MethodPost, + Path: "/v1/chisel/delete", + Summary: "Delete Chisel nodes", + }, handlers.DeleteChiselNodes) + + huma.Register(api, huma.Operation{ + OperationID: "create-opsgenie-manifest", + Method: http.MethodPost, + Path: "/v1/opsgenie/manifest", + Summary: "Create Opsgenie manifest", + }, handlers.CreateOpsgenieManifest) + + huma.Register(api, huma.Operation{ + OperationID: "create-captain-manifests", + Method: http.MethodPost, + Path: "/v1/captain/manifests", + Summary: "Generate captain manifests", + }, handlers.CreateCaptainManifests) + + return httptest.NewServer(router) +} + +// ---- 1. Health Endpoint ---- + +func TestHealthEndpoint(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/health") + if err != nil { + t.Fatalf("GET /health failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "application/json") { + t.Errorf("Content-Type = %q, want application/json", ct) + } + + var body map[string]string + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("failed to decode body: %v", err) + } + if body["status"] != "healthy" { + t.Errorf("status = %q, want %q", body["status"], "healthy") + } +} + +// ---- 2. Version Endpoint ---- + +func TestVersionEndpoint(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/version") + if err != nil { + t.Fatalf("GET /version failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + + var body struct { + Version string `json:"version"` + CommitSHA string `json:"commit_sha"` + ShortSHA string `json:"short_sha"` + BuildTimestamp string `json:"build_timestamp"` + GitRef string `json:"git_ref"` + } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("failed to decode: %v", err) + } + + // All fields must be present (defaults to "UNKNOWN" in test builds). + if body.Version == "" { + t.Error("version field is empty") + } + if body.CommitSHA == "" { + t.Error("commit_sha field is empty") + } + if body.ShortSHA == "" { + t.Error("short_sha field is empty") + } + if body.BuildTimestamp == "" { + t.Error("build_timestamp field is empty") + } + if body.GitRef == "" { + t.Error("git_ref field is empty") + } +} + +// ---- 3. Root Redirect ---- + +func TestRootRedirectsToDocs(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }} + + resp, err := client.Get(srv.URL + "/") + if err != nil { + t.Fatalf("GET / failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusTemporaryRedirect { + t.Errorf("expected 307, got %d", resp.StatusCode) + } + loc := resp.Header.Get("Location") + if loc != "/docs" { + t.Errorf("Location = %q, want /docs", loc) + } +} + +// ---- 4. OpenAPI Spec Structure ---- + +func TestOpenAPISpec(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/openapi.json") + if err != nil { + t.Fatalf("GET /openapi.json failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + + var spec map[string]interface{} + if err := json.Unmarshal(body, &spec); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + // Verify OpenAPI version. + if v, ok := spec["openapi"].(string); !ok || v != "3.1.0" { + t.Errorf("openapi version = %v, want 3.1.0", spec["openapi"]) + } + + // Verify info. + info, ok := spec["info"].(map[string]interface{}) + if !ok { + t.Fatal("missing info object") + } + if info["title"] != "Tools API" { + t.Errorf("info.title = %v, want %q", info["title"], "Tools API") + } + + // Verify all expected paths exist. + paths, ok := spec["paths"].(map[string]interface{}) + if !ok { + t.Fatal("missing paths object") + } + + expectedPaths := []string{ + "/version", + "/v1/storage/buckets", + "/v1/aws/credentials", + "/v1/aws/nuke", + "/v1/nuke/domain-data", + "/v1/github/reset-org", + "/v1/github/workflow-status", + "/v1/chisel", + "/v1/chisel/delete", + "/v1/opsgenie/manifest", + "/v1/captain/manifests", + } + + for _, p := range expectedPaths { + if _, exists := paths[p]; !exists { + t.Errorf("missing expected path: %s", p) + } + } + + // Health should NOT be in OpenAPI (registered on chi directly). + if _, exists := paths["/health"]; exists { + t.Error("/health should not appear in OpenAPI spec") + } + + // Verify expected HTTP methods. + methodChecks := map[string]string{ + "/version": "get", + "/v1/storage/buckets": "post", + "/v1/aws/credentials": "post", + "/v1/aws/nuke": "post", + "/v1/nuke/domain-data": "post", + "/v1/github/reset-org": "post", + "/v1/github/workflow-status": "get", + "/v1/chisel": "post", + "/v1/chisel/delete": "post", + "/v1/opsgenie/manifest": "post", + "/v1/captain/manifests": "post", + } + for path, method := range methodChecks { + pathObj, ok := paths[path].(map[string]interface{}) + if !ok { + continue + } + if _, exists := pathObj[method]; !exists { + t.Errorf("path %s missing method %s", path, method) + } + } + + // Verify operation IDs. + expectedOps := map[string]string{ + "/version": "get-version", + "/v1/storage/buckets": "create-storage-buckets", + "/v1/aws/credentials": "create-aws-credentials", + "/v1/aws/nuke": "nuke-aws-account", + "/v1/nuke/domain-data": "nuke-captain-domain-data", + "/v1/github/reset-org": "reset-github-organization", + "/v1/github/workflow-status": "get-workflow-run-status", + "/v1/chisel": "create-chisel-nodes", + "/v1/chisel/delete": "delete-chisel-nodes", + "/v1/opsgenie/manifest": "create-opsgenie-manifest", + "/v1/captain/manifests": "create-captain-manifests", + } + for path, wantOp := range expectedOps { + pathObj, ok := paths[path].(map[string]interface{}) + if !ok { + continue + } + method := methodChecks[path] + methodObj, ok := pathObj[method].(map[string]interface{}) + if !ok { + continue + } + gotOp, _ := methodObj["operationId"].(string) + if gotOp != wantOp { + t.Errorf("path %s operationId = %q, want %q", path, gotOp, wantOp) + } + } + + // Verify no DELETE endpoints exist (risk M1 eliminated). + for path, pathObj := range paths { + obj, ok := pathObj.(map[string]interface{}) + if !ok { + continue + } + if _, exists := obj["delete"]; exists { + t.Errorf("path %s has DELETE method — all destructive ops should use POST", path) + } + } +} + +// ---- 5. Captain Manifests — Full Parity (No External Deps) ---- + +func TestCaptainManifestsEndpoint(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + reqBody := `{ + "captain_domain": "nonprod.antoniostaqueria.onglueops.com", + "tenant_github_organization_name": "dev-tenant", + "tenant_deployment_configurations_repository_name": "deployment-configurations" + }` + + resp, err := http.Post(srv.URL+"/v1/captain/manifests", "application/json", strings.NewReader(reqBody)) + if err != nil { + t.Fatalf("POST /v1/captain/manifests failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("expected 200, got %d: %s", resp.StatusCode, body) + } + + // Must return text/plain (risk C1). + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/plain") { + t.Errorf("Content-Type = %q, want text/plain", ct) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + result := string(body) + + // Verify 3 YAML documents. + docs := strings.Split(result, "\n---\n") + if len(docs) != 3 { + t.Fatalf("expected 3 YAML documents, got %d", len(docs)) + } + + // Namespace + if !strings.Contains(docs[0], "kind: Namespace") { + t.Error("first document should be a Namespace") + } + if !strings.Contains(docs[0], "name: nonprod") { + t.Error("namespace should use environment name 'nonprod'") + } + + // AppProject + if !strings.Contains(docs[1], "kind: AppProject") { + t.Error("second document should be an AppProject") + } + if !strings.Contains(docs[1], "dev-tenant") { + t.Error("appproject should reference tenant org") + } + + // ApplicationSet + if !strings.Contains(docs[2], "kind: ApplicationSet") { + t.Error("third document should be an ApplicationSet") + } + + // Go template syntax must be preserved literally (risk H1). + if !strings.Contains(docs[2], `{{ index .path.segments 1 | replace "." "-" | replace "_" "-" }}`) { + t.Error("Go template syntax must be preserved in ApplicationSet output") + } + + // captain_domain must appear in the values section. + if !strings.Contains(result, "captain_domain: nonprod.antoniostaqueria.onglueops.com") { + t.Error("captain_domain should appear in output") + } + + // No unresolved template placeholders. + if strings.Contains(result, "<%") || strings.Contains(result, "%>") { + t.Error("output should not contain unresolved template placeholders") + } +} + +// ---- 6. Opsgenie Manifest — Full Parity (No External Deps) ---- + +func TestOpsgenieManifestEndpoint(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + reqBody := `{ + "captain_domain": "nonprod.foobar.onglueops.rocks", + "opsgenie_api_key": "test-api-key-12345" + }` + + resp, err := http.Post(srv.URL+"/v1/opsgenie/manifest", "application/json", strings.NewReader(reqBody)) + if err != nil { + t.Fatalf("POST /v1/opsgenie/manifest failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("expected 200, got %d: %s", resp.StatusCode, body) + } + + // Must return text/plain (risk C1). + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/plain") { + t.Errorf("Content-Type = %q, want text/plain", ct) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + result := string(body) + + checks := []string{ + "kind: Application", + "name: glueops-core-alerts-opsgenie", + "nonprod.foobar.onglueops.rocks", + "test-api-key-12345", + "kind: AlertmanagerConfig", + "apiURL: https://api.opsgenie.com/", + } + for _, s := range checks { + if !strings.Contains(result, s) { + t.Errorf("opsgenie manifest missing: %q", s) + } + } +} + +// ---- 7. Validation Errors (422) ---- + +func TestValidationErrorMissingRequiredFields(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + tests := []struct { + name string + method string + path string + body string + wantCode int + }{ + { + name: "captain manifests - empty body", + method: "POST", + path: "/v1/captain/manifests", + body: `{}`, + wantCode: 422, + }, + { + name: "captain manifests - empty captain_domain", + method: "POST", + path: "/v1/captain/manifests", + body: `{"captain_domain": "", "tenant_github_organization_name": "org", "tenant_deployment_configurations_repository_name": "repo"}`, + wantCode: 422, + }, + { + name: "opsgenie manifest - empty body", + method: "POST", + path: "/v1/opsgenie/manifest", + body: `{}`, + wantCode: 422, + }, + { + name: "opsgenie manifest - empty api key", + method: "POST", + path: "/v1/opsgenie/manifest", + body: `{"captain_domain": "foo.bar.com", "opsgenie_api_key": ""}`, + wantCode: 422, + }, + { + name: "chisel create - empty body", + method: "POST", + path: "/v1/chisel", + body: `{}`, + wantCode: 422, + }, + { + name: "chisel create - node_count too high", + method: "POST", + path: "/v1/chisel", + body: `{"captain_domain": "foo.bar.com", "node_count": 7}`, + wantCode: 422, + }, + { + name: "chisel create - node_count too low", + method: "POST", + path: "/v1/chisel", + body: `{"captain_domain": "foo.bar.com", "node_count": 0}`, + wantCode: 422, + }, + { + name: "chisel delete - empty body", + method: "POST", + path: "/v1/chisel/delete", + body: `{}`, + wantCode: 422, + }, + { + name: "storage buckets - empty body", + method: "POST", + path: "/v1/storage/buckets", + body: `{}`, + wantCode: 422, + }, + { + name: "storage buckets - empty captain_domain", + method: "POST", + path: "/v1/storage/buckets", + body: `{"captain_domain": ""}`, + wantCode: 422, + }, + { + name: "aws credentials - empty body", + method: "POST", + path: "/v1/aws/credentials", + body: `{}`, + wantCode: 422, + }, + { + name: "aws nuke - empty body", + method: "POST", + path: "/v1/aws/nuke", + body: `{}`, + wantCode: 422, + }, + { + name: "nuke domain data - empty body", + method: "POST", + path: "/v1/nuke/domain-data", + body: `{}`, + wantCode: 422, + }, + { + name: "reset github org - empty body", + method: "POST", + path: "/v1/github/reset-org", + body: `{}`, + wantCode: 422, + }, + { + name: "workflow status - missing run_url query param", + method: "GET", + path: "/v1/github/workflow-status", + body: "", + wantCode: 422, + }, + { + name: "workflow status - empty run_url query param", + method: "GET", + path: "/v1/github/workflow-status?run_url=", + body: "", + wantCode: 422, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var req *http.Request + var err error + if tt.body != "" { + req, err = http.NewRequest(tt.method, srv.URL+tt.path, strings.NewReader(tt.body)) + req.Header.Set("Content-Type", "application/json") + } else { + req, err = http.NewRequest(tt.method, srv.URL+tt.path, nil) + } + if err != nil { + t.Fatalf("creating request: %v", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != tt.wantCode { + body, _ := io.ReadAll(resp.Body) + t.Errorf("expected %d, got %d: %s", tt.wantCode, resp.StatusCode, body) + } + }) + } +} + +// ---- 8. Error Response Format ---- + +func TestErrorResponseFormat(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + // Trigger a validation error. + resp, err := http.Post(srv.URL+"/v1/captain/manifests", "application/json", strings.NewReader(`{}`)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + + var errResp map[string]interface{} + if err := json.Unmarshal(body, &errResp); err != nil { + t.Fatalf("error response is not valid JSON: %v\nbody: %s", err, body) + } + + // Must have "status" and "detail" fields (risk M3). + if _, ok := errResp["status"]; !ok { + t.Error("error response missing 'status' field") + } + if _, ok := errResp["detail"]; !ok { + t.Error("error response missing 'detail' field") + } + + // Must NOT have "traceback" or "error" fields. + if _, ok := errResp["traceback"]; ok { + t.Error("error response should NOT contain 'traceback' field") + } + if _, ok := errResp["error"]; ok { + t.Error("error response should NOT contain 'error' field") + } + + // Must NOT have Huma default "title" field. + if _, ok := errResp["title"]; ok { + t.Error("error response should NOT contain Huma default 'title' field") + } + + // Status must match HTTP status code. + statusFloat, ok := errResp["status"].(float64) + if !ok { + t.Fatal("status field is not a number") + } + if int(statusFloat) != resp.StatusCode { + t.Errorf("status field = %d, HTTP status = %d", int(statusFloat), resp.StatusCode) + } +} + +// ---- 9. Invalid JSON Body ---- + +func TestInvalidJSONBody(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Post(srv.URL+"/v1/captain/manifests", "application/json", strings.NewReader(`not json`)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + // Should return 4xx, not 5xx. + if resp.StatusCode < 400 || resp.StatusCode >= 500 { + t.Errorf("expected 4xx for invalid JSON, got %d", resp.StatusCode) + } +} + +// ---- 10. 404 for Unknown Paths ---- + +func TestNotFoundPath(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/v1/nonexistent") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 404 && resp.StatusCode != 405 { + t.Errorf("expected 404 or 405 for unknown path, got %d", resp.StatusCode) + } +} + +// ---- 11. Method Not Allowed ---- + +func TestMethodNotAllowed(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + // GET on a POST-only endpoint. + resp, err := http.Get(srv.URL + "/v1/captain/manifests") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 405 { + t.Errorf("expected 405 for wrong method, got %d", resp.StatusCode) + } +} + +// ---- 12. OpenAPI Spec Schema Validation ---- + +func TestOpenAPISpecSchemas(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/openapi.json") + if err != nil { + t.Fatalf("GET /openapi.json failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + var spec map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&spec); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + components, ok := spec["components"].(map[string]interface{}) + if !ok { + t.Fatal("missing components in OpenAPI spec") + } + + schemas, ok := components["schemas"].(map[string]interface{}) + if !ok { + t.Fatal("missing schemas in components") + } + + // Verify WorkflowDispatchResponseBody has nullable fields for run_id and run_url (risk C2). + // Only check the dispatch schema, not the status schema (where run_id is a non-nullable int). + dispatchSchemaName := "WorkflowDispatchResponseBody" + dispatchSchema, ok := schemas[dispatchSchemaName].(map[string]interface{}) + if !ok { + t.Fatalf("missing schema %q", dispatchSchemaName) + } + props, ok := dispatchSchema["properties"].(map[string]interface{}) + if !ok { + t.Fatal("missing properties in dispatch response schema") + } + + if runIDProp, ok := props["run_id"].(map[string]interface{}); ok { + verifyNullable(t, "run_id", runIDProp) + } else { + t.Error("missing run_id property in dispatch response schema") + } + + if runURLProp, ok := props["run_url"].(map[string]interface{}); ok { + verifyNullable(t, "run_url", runURLProp) + } else { + t.Error("missing run_url property in dispatch response schema") + } +} + +// verifyNullable checks that a schema property can be null (OpenAPI 3.1 uses oneOf/anyOf with null type, +// or the "nullable" keyword, or a type array including "null"). +func verifyNullable(t *testing.T, fieldName string, prop map[string]interface{}) { + t.Helper() + + // OpenAPI 3.1: type can be an array like ["integer", "null"] or ["string", "null"]. + if typeVal, ok := prop["type"]; ok { + switch v := typeVal.(type) { + case []interface{}: + for _, item := range v { + if item == "null" { + return // nullable via type array + } + } + case string: + // Single type — not nullable unless "nullable" is set. + } + } + + // OpenAPI 3.0 style: "nullable: true". + if nullable, ok := prop["nullable"].(bool); ok && nullable { + return + } + + // Huma may use oneOf/anyOf with a null type. + for _, key := range []string{"oneOf", "anyOf"} { + if variants, ok := prop[key].([]interface{}); ok { + for _, v := range variants { + if vm, ok := v.(map[string]interface{}); ok { + if vm["type"] == "null" { + return + } + } + } + } + } + + t.Errorf("field %q should be nullable in OpenAPI schema but is not: %v", fieldName, prop) +} + +// ---- 13. Chisel Node Count Validation ---- + +func TestChiselNodeCountValidation(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + tests := []struct { + name string + body string + wantStatus int + }{ + { + name: "node_count=7 exceeds max of 6", + body: `{"captain_domain": "test.example.com", "node_count": 7}`, + wantStatus: 422, + }, + { + name: "node_count=0 below min of 1", + body: `{"captain_domain": "test.example.com", "node_count": 0}`, + wantStatus: 422, + }, + { + name: "node_count=-1 below min of 1", + body: `{"captain_domain": "test.example.com", "node_count": -1}`, + wantStatus: 422, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := http.Post(srv.URL+"/v1/chisel", "application/json", strings.NewReader(tt.body)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != tt.wantStatus { + body, _ := io.ReadAll(resp.Body) + t.Errorf("expected %d, got %d: %s", tt.wantStatus, resp.StatusCode, body) + } + }) + } +} + +// ---- 14. Server Binding and Configuration ---- + +func TestServerBindsOnPort8000(t *testing.T) { + // Verify the Dockerfile exposes port 8000 (this is tested by the Docker build itself). + // Here we verify the test server starts and responds, confirming the routing works. + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/health") + if err != nil { + t.Fatalf("server did not start: %v", err) + } + _ = resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("health check failed: %d", resp.StatusCode) + } +} + +// ---- 15. Content-Type for JSON Responses ---- + +func TestJSONResponseContentType(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + // JSON endpoints should return application/json. + resp, err := http.Get(srv.URL + "/version") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "application/json") { + t.Errorf("Content-Type for /version = %q, want application/json", ct) + } +} + +// ---- 16. Plain Text Endpoints Return text/plain ---- + +func TestPlainTextEndpointContentTypes(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + // Only test endpoints that don't require external services. + tests := []struct { + name string + path string + body string + }{ + { + name: "captain manifests", + path: "/v1/captain/manifests", + body: `{"captain_domain": "test.example.com", "tenant_github_organization_name": "org", "tenant_deployment_configurations_repository_name": "repo"}`, + }, + { + name: "opsgenie manifest", + path: "/v1/opsgenie/manifest", + body: `{"captain_domain": "test.example.com", "opsgenie_api_key": "test-key"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := http.Post(srv.URL+tt.path, "application/json", strings.NewReader(tt.body)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("expected 200, got %d: %s", resp.StatusCode, body) + } + + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/plain") { + t.Errorf("Content-Type = %q, want text/plain", ct) + } + }) + } +} + +// ---- 17. Empty String Inputs ---- + +func TestEmptyStringValidation(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + // All string fields with minLength:"1" should reject empty strings (risk M6). + tests := []struct { + name string + path string + body string + }{ + { + name: "captain_domain empty in storage", + path: "/v1/storage/buckets", + body: `{"captain_domain": ""}`, + }, + { + name: "captain_domain empty in chisel", + path: "/v1/chisel", + body: `{"captain_domain": "", "node_count": 3}`, + }, + { + name: "captain_domain empty in chisel delete", + path: "/v1/chisel/delete", + body: `{"captain_domain": ""}`, + }, + { + name: "aws_sub_account_name empty in credentials", + path: "/v1/aws/credentials", + body: `{"aws_sub_account_name": ""}`, + }, + { + name: "aws_sub_account_name empty in nuke", + path: "/v1/aws/nuke", + body: `{"aws_sub_account_name": ""}`, + }, + { + name: "captain_domain empty in domain data nuke", + path: "/v1/nuke/domain-data", + body: `{"captain_domain": ""}`, + }, + { + name: "captain_domain empty in reset org", + path: "/v1/github/reset-org", + body: `{"captain_domain": "", "delete_all_existing_repos": true, "custom_domain": "example.com", "enable_custom_domain": false}`, + }, + { + name: "captain_domain empty in opsgenie", + path: "/v1/opsgenie/manifest", + body: `{"captain_domain": "", "opsgenie_api_key": "key"}`, + }, + { + name: "opsgenie_api_key empty in opsgenie", + path: "/v1/opsgenie/manifest", + body: `{"captain_domain": "test.com", "opsgenie_api_key": ""}`, + }, + { + name: "captain_domain empty in captain manifests", + path: "/v1/captain/manifests", + body: `{"captain_domain": "", "tenant_github_organization_name": "org", "tenant_deployment_configurations_repository_name": "repo"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := http.Post(srv.URL+tt.path, "application/json", strings.NewReader(tt.body)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 422 { + body, _ := io.ReadAll(resp.Body) + t.Errorf("expected 422 for empty string, got %d: %s", resp.StatusCode, body) + } + }) + } +} + +// ---- 18. Workflow Status Query Parameter ---- + +func TestWorkflowStatusQueryParam(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + // This endpoint uses GET with a query parameter (not POST with body). + // Verify it's a GET endpoint. + resp, err := http.Post(srv.URL+"/v1/github/workflow-status", "application/json", strings.NewReader(`{}`)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + // POST should return 405. + if resp.StatusCode != 405 { + t.Errorf("POST to workflow-status should return 405, got %d", resp.StatusCode) + } +} + +// ---- 19. Docs Endpoint Accessible ---- + +func TestDocsEndpoint(t *testing.T) { + srv := newTestServer(t) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/docs") + if err != nil { + t.Fatalf("GET /docs failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + t.Errorf("expected 200 for /docs, got %d", resp.StatusCode) + } + + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/html") { + t.Errorf("Content-Type for /docs = %q, want text/html", ct) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9cdeb8f --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module github.com/GlueOps/tools-api + +go 1.24.1 + +toolchain go1.24.13 + +require ( + github.com/aws/aws-sdk-go-v2 v1.41.4 + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 + github.com/aws/aws-sdk-go-v2/service/iam v1.53.6 + github.com/aws/aws-sdk-go-v2/service/organizations v1.50.5 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 + github.com/aws/smithy-go v1.24.2 + github.com/danielgtaylor/huma/v2 v2.36.0 + github.com/go-chi/chi/v5 v5.2.5 + github.com/hetznercloud/hcloud-go/v2 v2.36.0 + github.com/minio/minio-go/v7 v7.0.87 +) + +require ( + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/rs/xid v1.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9239539 --- /dev/null +++ b/go.sum @@ -0,0 +1,100 @@ +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.6 h1:GPQvvxy8+FDnD9xKYzGKJMjIm5xkVM5pd3bFgRldNSo= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.6/go.mod h1:RJNVc52A0K41fCDJOnsCLeWJf8mwa0q30fM3CfE9U18= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/organizations v1.50.5 h1:V0skJdwjmwcaxtGy2ws1WdBhG5Nkz6A/Ghvl6HXwzNc= +github.com/aws/aws-sdk-go-v2/service/organizations v1.50.5/go.mod h1:GIRcFyaju2WCHMsO1JkoSxBUGgXplULEXIJYdevIba4= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/danielgtaylor/huma/v2 v2.36.0 h1:zw//FPnSoNMh6ht06URC4PLZXN2KZbJ8i7kqpyiXDTE= +github.com/danielgtaylor/huma/v2 v2.36.0/go.mod h1:OPYyMWS1BVekd2e1CBqm4+qec46ziaoxxUcz1P3+P+I= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hetznercloud/hcloud-go/v2 v2.36.0 h1:HlLL/aaVXUulqe+rsjoJmrxKhPi1MflL5O9iq5QEtvo= +github.com/hetznercloud/hcloud-go/v2 v2.36.0/go.mod h1:MnN/QJEa/RYNQiiVoJjNHPntM7Z1wlYPgJ2HA40/cDE= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.87 h1:nkr9x0u53PespfxfUqxP3UYWiE2a41gaofgNnC4Y8WQ= +github.com/minio/minio-go/v7 v7.0.87/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..12811ec --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,10 @@ +package version + +// These variables are set at build time via -ldflags. +var ( + Version = "UNKNOWN" + CommitSHA = "UNKNOWN" + ShortSHA = "UNKNOWN" + BuildTimestamp = "UNKNOWN" + GitRef = "UNKNOWN" +) diff --git a/pkg/aws/aws.go b/pkg/aws/aws.go new file mode 100644 index 0000000..cb8c3f3 --- /dev/null +++ b/pkg/aws/aws.go @@ -0,0 +1,241 @@ +package aws + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/http" + "os" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/organizations" + orgtypes "github.com/aws/aws-sdk-go-v2/service/organizations/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" +) + +const ( + iamUserName = "dev-deployment-svc-account" + iamRoleName = "glueops-captain-role" + iamPolicyARN = "arn:aws:iam::aws:policy/AdministratorAccess" + stsSessionName = "SubAccountAccess" + hardcodedRegion = "us-west-2" +) + +// httpError implements error with an HTTP status code for Huma error handling (risk H2). +type httpError struct { + status int + detail string +} + +func (e *httpError) Error() string { return e.detail } +func (e *httpError) GetStatus() int { return e.status } + +func hError(status int, detail string) error { + return &httpError{status: status, detail: detail} +} + +// newAWSConfig creates an AWS config with static credentials from env vars. +func newAWSConfig() (aws.Config, error) { + accessKey := os.Getenv("AWS_GLUEOPS_ROCKS_ORG_ACCESS_KEY") + secretKey := os.Getenv("AWS_GLUEOPS_ROCKS_ORG_SECRET_KEY") + if accessKey == "" || secretKey == "" { + return aws.Config{}, fmt.Errorf("AWS_GLUEOPS_ROCKS_ORG_ACCESS_KEY and AWS_GLUEOPS_ROCKS_ORG_SECRET_KEY environment variables are required") + } + + cfg := aws.Config{ + Region: hardcodedRegion, + Credentials: credentials.NewStaticCredentialsProvider(accessKey, secretKey, ""), + } + return cfg, nil +} + +// listOrganizationAccounts returns all accounts in the organization using pagination. +func listOrganizationAccounts(ctx context.Context, client *organizations.Client) ([]orgtypes.Account, error) { + var allAccounts []orgtypes.Account + paginator := organizations.NewListAccountsPaginator(client, &organizations.ListAccountsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list organization accounts: %w", err) + } + allAccounts = append(allAccounts, page.Accounts...) + } + return allAccounts, nil +} + +// findAccountByName finds a sub-account by name from a list of accounts. +func findAccountByName(accounts []orgtypes.Account, name string) (*orgtypes.Account, error) { + for i := range accounts { + if aws.ToString(accounts[i].Name) == name { + return &accounts[i], nil + } + } + return nil, hError(http.StatusNotFound, "Account not found.") +} + +// CreateAdminCredentialsWithinCaptainAccount orchestrates the full flow: +// list accounts → find by name → assume role → create user → create key → create role → format .env +func CreateAdminCredentialsWithinCaptainAccount(ctx context.Context, awsSubAccountName string) (string, error) { + cfg, err := newAWSConfig() + if err != nil { + return "", err + } + + orgClient := organizations.NewFromConfig(cfg) + stsClient := sts.NewFromConfig(cfg) + + // Step 1: Validate this is the root account. + orgInfo, err := orgClient.DescribeOrganization(ctx, &organizations.DescribeOrganizationInput{}) + if err != nil { + return "", fmt.Errorf("failed to describe organization: %w", err) + } + masterAccountID := aws.ToString(orgInfo.Organization.MasterAccountId) + + callerIdentity, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + return "", fmt.Errorf("failed to get caller identity: %w", err) + } + currentAccountID := aws.ToString(callerIdentity.Account) + + if currentAccountID != masterAccountID { + return "", hError(http.StatusBadRequest, "This is not the root account. Exiting.") + } + + // Step 2: List all accounts and find the target sub-account. + accounts, err := listOrganizationAccounts(ctx, orgClient) + if err != nil { + return "", err + } + + subAccount, err := findAccountByName(accounts, awsSubAccountName) + if err != nil { + return "", err + } + subAccountID := aws.ToString(subAccount.Id) + slog.Info("found sub-account", "name", awsSubAccountName, "id", subAccountID) + + // Step 3: Assume role in the sub-account. + roleARN := fmt.Sprintf("arn:aws:iam::%s:role/OrganizationAccountAccessRole", subAccountID) + assumeRoleOutput, err := stsClient.AssumeRole(ctx, &sts.AssumeRoleInput{ + RoleArn: aws.String(roleARN), + RoleSessionName: aws.String(stsSessionName), + }) + if err != nil { + return "", fmt.Errorf("failed to assume role in sub-account %s: %w", subAccountID, err) + } + + // Step 4: Create IAM client with assumed role credentials. + assumedCreds := assumeRoleOutput.Credentials + iamCfg := aws.Config{ + Region: hardcodedRegion, + Credentials: credentials.NewStaticCredentialsProvider( + aws.ToString(assumedCreds.AccessKeyId), + aws.ToString(assumedCreds.SecretAccessKey), + aws.ToString(assumedCreds.SessionToken), + ), + } + iamClient := iam.NewFromConfig(iamCfg) + + // Step 5: Create IAM user (skip if already exists, but still create access key). + _, err = iamClient.CreateUser(ctx, &iam.CreateUserInput{ + UserName: aws.String(iamUserName), + }) + if err != nil { + var apiErr smithy.APIError + if errors.As(err, &apiErr) && apiErr.ErrorCode() == "EntityAlreadyExists" { + slog.Info("IAM user already exists, skipping creation", "user", iamUserName) + } else { + return "", fmt.Errorf("failed to create IAM user: %w", err) + } + } else { + // Only attach policy on new user creation (matching Python behavior). + _, err = iamClient.AttachUserPolicy(ctx, &iam.AttachUserPolicyInput{ + UserName: aws.String(iamUserName), + PolicyArn: aws.String(iamPolicyARN), + }) + if err != nil { + return "", fmt.Errorf("failed to attach policy to IAM user: %w", err) + } + } + + // Create access key for the user (always, even if user already existed). + keyOutput, err := iamClient.CreateAccessKey(ctx, &iam.CreateAccessKeyInput{ + UserName: aws.String(iamUserName), + }) + if err != nil { + return "", fmt.Errorf("failed to create access key: %w", err) + } + accessKey := aws.ToString(keyOutput.AccessKey.AccessKeyId) + secretKey := aws.ToString(keyOutput.AccessKey.SecretAccessKey) + + // Step 6: Create IAM role with trust policy (skip if already exists). + trustPolicy := map[string]interface{}{ + "Version": "2012-10-17", + "Statement": []map[string]interface{}{ + { + "Effect": "Allow", + "Principal": map[string]string{ + "AWS": fmt.Sprintf("arn:aws:iam::%s:root", subAccountID), + }, + "Action": "sts:AssumeRole", + }, + }, + } + trustPolicyJSON, err := json.Marshal(trustPolicy) + if err != nil { + return "", fmt.Errorf("failed to marshal trust policy: %w", err) + } + + _, err = iamClient.CreateRole(ctx, &iam.CreateRoleInput{ + RoleName: aws.String(iamRoleName), + AssumeRolePolicyDocument: aws.String(string(trustPolicyJSON)), + }) + if err != nil { + var apiErr smithy.APIError + if errors.As(err, &apiErr) && apiErr.ErrorCode() == "EntityAlreadyExists" { + slog.Info("IAM role already exists, skipping creation", "role", iamRoleName) + } else { + return "", fmt.Errorf("failed to create IAM role: %w", err) + } + } else { + _, err = iamClient.AttachRolePolicy(ctx, &iam.AttachRolePolicyInput{ + RoleName: aws.String(iamRoleName), + PolicyArn: aws.String(iamPolicyARN), + }) + if err != nil { + return "", fmt.Errorf("failed to attach policy to IAM role: %w", err) + } + } + + // Get the ARN of the role. + roleOutput, err := iamClient.GetRole(ctx, &iam.GetRoleInput{ + RoleName: aws.String(iamRoleName), + }) + if err != nil { + return "", fmt.Errorf("failed to get IAM role: %w", err) + } + roleCreatedARN := aws.ToString(roleOutput.Role.Arn) + + // Step 7: Generate .env content (must match Python output exactly). + envContent := fmt.Sprintf(` +# Run the following in your codespace environment to create your .env for %s: + +cat <> $(pwd)/.env +export AWS_ACCESS_KEY_ID=%s +export AWS_SECRET_ACCESS_KEY=%s +export AWS_DEFAULT_REGION=us-west-2 +#aws eks update-kubeconfig --region us-west-2 --name captain-cluster --role-arn %s +ENV + +# Here is the iam_role_to_assume that you will need to specify in your terraform module for %s: +# %s + + `, awsSubAccountName, accessKey, secretKey, roleCreatedARN, awsSubAccountName, roleCreatedARN) + + return envContent, nil +} diff --git a/pkg/captain/captain.go b/pkg/captain/captain.go new file mode 100644 index 0000000..26dd725 --- /dev/null +++ b/pkg/captain/captain.go @@ -0,0 +1,139 @@ +package captain + +import "strings" + +// extractEnvironmentName returns the first segment before the first dot. +// e.g., "nonprod" from "nonprod.foobar.onglueops.rocks". +func extractEnvironmentName(captainDomain string) string { + return strings.SplitN(captainDomain, ".", 2)[0] +} + +// GenerateManifests renders all 3 captain manifests (Namespace, AppProject, +// ApplicationSet) and concatenates them with YAML document separators. +func GenerateManifests(captainDomain, tenantOrg, tenantRepo string) string { + envName := extractEnvironmentName(captainDomain) + + r := strings.NewReplacer( + "<% environment_name %>", envName, + "<% captain_domain %>", captainDomain, + "<% tenant_github_organization_name %>", tenantOrg, + "<% tenant_deployment_configurations_repository_name %>", tenantRepo, + ) + + namespace := r.Replace(namespaceTemplate) + appproject := r.Replace(appprojectTemplate) + appset := r.Replace(appsetTemplate) + + return namespace + "\n---\n" + appproject + "\n---\n" + appset +} + +const namespaceTemplate = `apiVersion: v1 +kind: Namespace +metadata: + labels: + kubernetes.io/metadata.name: <% environment_name %> + name: <% environment_name %>` + +const appprojectTemplate = `apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: <% environment_name %> +spec: + sourceNamespaces: + - '<% environment_name %>' + clusterResourceBlacklist: + - group: '*' + kind: '*' + namespaceResourceBlacklist: + - group: '*' + kind: 'Namespace' + - group: '*' + kind: 'CustomResourceDefinition' + destinations: + - name: '*' + namespace: '<% environment_name %>' + server: '*' + - name: '*' + namespace: 'glueops-core' + server: '*' + roles: + - description: <% tenant_github_organization_name %>:developers + groups: + - "<% tenant_github_organization_name %>:developers" + policies: + - p, proj:<% environment_name %>:read-only, applications, get, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, sync, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, logs, *, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, action/external-secrets.io/ExternalSecret/refresh, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, exec, *, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, action/apps/Deployment/restart, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, delete/*/Pod/*/*, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, delete/*/Deployment/*/*, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, delete/*/ReplicaSet/*/*, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, action/batch/CronJob/create-job, <% environment_name %>/*, allow + - p, proj:<% environment_name %>:read-only, applications, action/batch/Job/terminate, <% environment_name %>/*, allow + name: read-only + sourceRepos: + - https://helm.gpkg.io/project-template + - https://helm.gpkg.io/service + - https://incubating-helm.gpkg.io/project-template + - https://incubating-helm.gpkg.io/service + - https://incubating-helm.gpkg.io/platform + - https://github.com/<% tenant_github_organization_name %>/<% tenant_deployment_configurations_repository_name %> + - https://github.com/<% tenant_github_organization_name %>/* + - https://github.com/GlueOps/*` + +const appsetTemplate = `apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: <% environment_name %>-application-set + namespace: glueops-core +spec: + goTemplate: true + generators: + - git: + repoURL: https://github.com/<% tenant_github_organization_name %>/<% tenant_deployment_configurations_repository_name %> + revision: HEAD + directories: + - path: 'apps/*/envs/*' + - path: 'apps/*/envs/previews' + exclude: true + template: + metadata: + name: '{{ index .path.segments 1 | replace "." "-" | replace "_" "-" }}-{{ .path.basenameNormalized }}' + namespace: <% environment_name %> + annotations: + preview_environment: 'false' + spec: + destination: + namespace: <% environment_name %> + server: https://kubernetes.default.svc + project: <% environment_name %> + sources: + - chart: app + helm: + valueFiles: + - '$values/common/common-values.yaml' + - '$values/env-overlays/<% environment_name %>/env-values.yaml' + - '$values/apps/{{ index .path.segments 1 }}/base/base-values.yaml' + - '$values/{{ .path.path }}/values.yaml' + values: |- + captain_domain: <% captain_domain %> + + repoURL: https://helm.gpkg.io/project-template + targetRevision: 0.9.0 + - repoURL: https://github.com/<% tenant_github_organization_name %>/<% tenant_deployment_configurations_repository_name %> + targetRevision: main + ref: values + syncPolicy: + automated: + prune: true + selfHeal: true + retry: + backoff: + duration: 5s + factor: 2 + maxDuration: 3m0s + limit: 2 + syncOptions: + - CreateNamespace=true` diff --git a/pkg/captain/captain_test.go b/pkg/captain/captain_test.go new file mode 100644 index 0000000..001d144 --- /dev/null +++ b/pkg/captain/captain_test.go @@ -0,0 +1,74 @@ +package captain + +import ( + "strings" + "testing" +) + +func TestExtractEnvironmentName(t *testing.T) { + tests := []struct { + input string + want string + }{ + {"nonprod.foobar.onglueops.rocks", "nonprod"}, + {"prod.example.com", "prod"}, + {"staging", "staging"}, + } + for _, tc := range tests { + got := extractEnvironmentName(tc.input) + if got != tc.want { + t.Errorf("extractEnvironmentName(%q) = %q, want %q", tc.input, got, tc.want) + } + } +} + +func TestGenerateManifests(t *testing.T) { + result := GenerateManifests( + "nonprod.antoniostaqueria.onglueops.com", + "dev-tenant", + "deployment-configurations", + ) + + // Should contain 3 YAML documents separated by --- + docs := strings.Split(result, "\n---\n") + if len(docs) != 3 { + t.Fatalf("expected 3 YAML documents, got %d", len(docs)) + } + + // Namespace document + if !strings.Contains(docs[0], "kind: Namespace") { + t.Error("first document should be a Namespace") + } + if !strings.Contains(docs[0], "name: nonprod") { + t.Error("namespace should use environment name 'nonprod'") + } + + // AppProject document + if !strings.Contains(docs[1], "kind: AppProject") { + t.Error("second document should be an AppProject") + } + if !strings.Contains(docs[1], "dev-tenant") { + t.Error("appproject should contain tenant org name") + } + if !strings.Contains(docs[1], "deployment-configurations") { + t.Error("appproject should contain tenant repo name") + } + + // ApplicationSet document + if !strings.Contains(docs[2], "kind: ApplicationSet") { + t.Error("third document should be an ApplicationSet") + } + if !strings.Contains(docs[2], "captain_domain: nonprod.antoniostaqueria.onglueops.com") { + t.Error("appset should contain full captain_domain") + } + + // Go template syntax in output must be preserved literally + if !strings.Contains(docs[2], `{{ index .path.segments 1 | replace "." "-" | replace "_" "-" }}`) { + t.Error("Go template syntax in appset output must be preserved") + } + + // No unresolved placeholders + if strings.Contains(result, "<%") || strings.Contains(result, "%>") { + t.Error("output should not contain unresolved template placeholders") + } +} diff --git a/pkg/chisel/chisel.go b/pkg/chisel/chisel.go new file mode 100644 index 0000000..77bf389 --- /dev/null +++ b/pkg/chisel/chisel.go @@ -0,0 +1,88 @@ +package chisel + +import ( + "crypto/rand" + "fmt" + "math/big" + "strings" +) + +const ( + credentialLength = 15 + charPool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +) + +// GenerateCredentials generates a random username:password pair for Chisel authentication. +// Each part is 15 characters of alphanumeric characters, matching the Python implementation. +func GenerateCredentials() (string, error) { + user, err := randomString(credentialLength) + if err != nil { + return "", fmt.Errorf("failed to generate credentials: %w", err) + } + pass, err := randomString(credentialLength) + if err != nil { + return "", fmt.Errorf("failed to generate credentials: %w", err) + } + return user + ":" + pass, nil +} + +// GetSuffixes returns suffix names for the requested number of nodes (1-6). +func GetSuffixes(nodeCount int) []string { + suffixes := make([]string, nodeCount) + for i := 0; i < nodeCount; i++ { + suffixes[i] = fmt.Sprintf("exit%d", i+1) + } + return suffixes +} + +// CreateChiselYAML generates the kubectl manifest for Chisel operator resources. +func CreateChiselYAML(captainDomain, credentials string, ipAddresses map[string]string, suffixes []string) string { + var b strings.Builder + + fmt.Fprintf(&b, ` +kubectl apply -k https://github.com/FyraLabs/chisel-operator?ref=v0.7.1 + +kubectl apply -f - < 0 { + payload["inputs"] = inputs + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return 0, fmt.Errorf("failed to marshal dispatch payload: %w", err) + } + + req, err := newGitHubRequest(ctx, http.MethodPost, dispatchURL, bytes.NewReader(payloadBytes)) + if err != nil { + return 0, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := httpClient().Do(req) + if err != nil { + return 0, fmt.Errorf("GitHub workflow dispatch request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + slog.Info("GitHub workflow dispatch", + "url", dispatchURL, + "inputs", inputs, + "status_code", resp.StatusCode, + ) + return resp.StatusCode, nil +} + +// GetWorkflowRunID polls for the most recent run of a workflow that was just dispatched. +// Python sleeps FIRST then checks (sleep → request → sleep → request, 6 iterations). +// Returns nullable RunID and RunURL; nil pointers when polling times out (risk C2). +// Uses context.Context with select + time.After for cancellation support. +func GetWorkflowRunID(ctx context.Context, workflowFile string) (*RunInfo, error) { + url := fmt.Sprintf("%s/actions/workflows/%s/runs", repoAPIBase, workflowFile) + + for attempt := 0; attempt < pollMaxAttempts; attempt++ { + // Sleep first, then check (matching Python behavior). + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(pollInterval): + } + + req, err := newGitHubRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + q := req.URL.Query() + q.Set("per_page", "1") + req.URL.RawQuery = q.Encode() + + resp, err := httpClient().Do(req) + if err != nil { + slog.Info("polling for workflow run failed", + "attempt", attempt+1, + "max_attempts", pollMaxAttempts, + "workflow_file", workflowFile, + "error", err, + ) + continue + } + + if resp.StatusCode == http.StatusOK { + var result struct { + WorkflowRuns []struct { + ID int `json:"id"` + HTMLURL string `json:"html_url"` + } `json:"workflow_runs"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + _ = resp.Body.Close() + slog.Warn("failed to decode workflow runs response", "error", err) + continue + } + _ = resp.Body.Close() + + if len(result.WorkflowRuns) > 0 { + run := result.WorkflowRuns[0] + slog.Info("found workflow run", + "run_id", run.ID, + "workflow_file", workflowFile, + ) + runID := run.ID + runURL := run.HTMLURL + return &RunInfo{RunID: &runID, RunURL: &runURL}, nil + } + } else { + _ = resp.Body.Close() + } + + slog.Info("polling for workflow run", + "attempt", attempt+1, + "max_attempts", pollMaxAttempts, + "workflow_file", workflowFile, + ) + } + + slog.Warn("could not find workflow run after polling", "workflow_file", workflowFile) + return &RunInfo{RunID: nil, RunURL: nil}, nil +} + +// DispatchAndGetRun dispatches a workflow and polls for the run details. +// On non-2xx dispatch status, returns an error (results in 500, matching Python ValueError behavior). +func DispatchAndGetRun(ctx context.Context, workflowFile string, inputs map[string]string) (statusCode int, allJobsURL string, runInfo *RunInfo, err error) { + dispatchURL := fmt.Sprintf("%s/actions/workflows/%s/dispatches", repoAPIBase, workflowFile) + allJobsURL = fmt.Sprintf("%s/actions/workflows/%s", repoHTMLBase, workflowFile) + + statusCode, err = CallGitHubWorkflow(ctx, dispatchURL, inputs) + if err != nil { + return 0, "", nil, err + } + if statusCode < 200 || statusCode >= 300 { + return 0, "", nil, fmt.Errorf("GitHub workflow dispatch failed with status %d", statusCode) + } + + runInfo, err = GetWorkflowRunID(ctx, workflowFile) + if err != nil { + return 0, "", nil, err + } + + return statusCode, allJobsURL, runInfo, nil +} + +// GetWorkflowRunStatus parses a GitHub Actions run URL, fetches the run status from the API, +// and returns the status details. Returns specific HTTP errors matching Python behavior (risk H2). +func GetWorkflowRunStatus(ctx context.Context, runURL string) (*WorkflowRunStatus, error) { + match := runURLPattern.FindStringSubmatch(runURL) + if match == nil { + return nil, hError(http.StatusBadRequest, fmt.Sprintf("Invalid GitHub Actions run URL: %s", runURL)) + } + + ownerRepo := match[1] + runID := match[2] + apiURL := fmt.Sprintf("https://api.github.com/repos/%s/actions/runs/%s", ownerRepo, runID) + + req, err := newGitHubRequest(ctx, http.MethodGet, apiURL, nil) + if err != nil { + return nil, err + } + + resp, err := httpClient().Do(req) + if err != nil { + return nil, fmt.Errorf("GitHub API request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, hError(http.StatusBadGateway, fmt.Sprintf("GitHub API returned %d for run %s", resp.StatusCode, runID)) + } + + var data struct { + ID int `json:"id"` + Name *string `json:"name"` + Status string `json:"status"` + Conclusion *string `json:"conclusion"` + HTMLURL string `json:"html_url"` + CreatedAt *string `json:"created_at"` + UpdatedAt *string `json:"updated_at"` + } + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, fmt.Errorf("failed to decode workflow run status: %w", err) + } + + return &WorkflowRunStatus{ + RunID: data.ID, + Name: data.Name, + Status: data.Status, + Conclusion: data.Conclusion, + RunURL: data.HTMLURL, + CreatedAt: data.CreatedAt, + UpdatedAt: data.UpdatedAt, + }, nil +} + +// NukeAwsAccountWorkflow dispatches the aws-nuke-account workflow. +func NukeAwsAccountWorkflow(ctx context.Context, awsSubAccountName string) (int, string, *RunInfo, error) { + return DispatchAndGetRun(ctx, "aws-nuke-account.yml", map[string]string{ + "AWS_ACCOUNT_NAME_TO_NUKE": awsSubAccountName, + }) +} + +// NukeCaptainDomainDataAndBackups dispatches the nuke-captain-domain-data-and-backups workflow. +func NukeCaptainDomainDataAndBackups(ctx context.Context, captainDomain string) (int, string, *RunInfo, error) { + return DispatchAndGetRun(ctx, "nuke-captain-domain-data-and-backups.yml", map[string]string{ + "CAPTAIN_DOMAIN_TO_NUKE": captainDomain, + }) +} + +// ResetTenantGitHubOrganization dispatches the reset-tenant-github-organization workflow. +// Boolean values use Python-compatible "True"/"False" capitalization (risk H3). +func ResetTenantGitHubOrganization(ctx context.Context, captainDomain string, deleteAllRepos bool, customDomain string, enableCustomDomain bool) (int, string, *RunInfo, error) { + return DispatchAndGetRun(ctx, "reset-tenant-github-organization.yml", map[string]string{ + "CAPTAIN_DOMAIN": captainDomain, + "DELETE_ALL_EXISTING_REPOS": pythonBool(deleteAllRepos), + "CUSTOM_DOMAIN": customDomain, + "ENABLE_CUSTOM_DOMAIN": pythonBool(enableCustomDomain), + }) +} + +// httpError implements error with an HTTP status code for Huma error handling (risk H2). +type httpError struct { + status int + detail string +} + +func (e *httpError) Error() string { return e.detail } +func (e *httpError) GetStatus() int { return e.status } + +func hError(status int, detail string) error { + return &httpError{status: status, detail: detail} +} diff --git a/pkg/github/github_test.go b/pkg/github/github_test.go new file mode 100644 index 0000000..f893496 --- /dev/null +++ b/pkg/github/github_test.go @@ -0,0 +1,94 @@ +package github + +import ( + "testing" +) + +func TestPythonBool(t *testing.T) { + if got := pythonBool(true); got != "True" { + t.Errorf("pythonBool(true) = %q, want %q", got, "True") + } + if got := pythonBool(false); got != "False" { + t.Errorf("pythonBool(false) = %q, want %q", got, "False") + } +} + +func TestRunURLPattern(t *testing.T) { + tests := []struct { + name string + url string + wantMatch bool + wantOwner string + wantRunID string + }{ + { + name: "valid URL", + url: "https://github.com/internal-GlueOps/gha-tools-api/actions/runs/12345678", + wantMatch: true, + wantOwner: "internal-GlueOps/gha-tools-api", + wantRunID: "12345678", + }, + { + name: "valid URL different org", + url: "https://github.com/octocat/hello-world/actions/runs/99999", + wantMatch: true, + wantOwner: "octocat/hello-world", + wantRunID: "99999", + }, + { + name: "invalid - no run ID", + url: "https://github.com/owner/repo/actions/runs/", + wantMatch: false, + }, + { + name: "invalid - extra path segments (partial match prevented by $ anchor)", + url: "https://github.com/owner/repo/actions/runs/123/jobs", + wantMatch: false, + }, + { + name: "invalid - not github", + url: "https://gitlab.com/owner/repo/actions/runs/123", + wantMatch: false, + }, + { + name: "invalid - empty string", + url: "", + wantMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + match := runURLPattern.FindStringSubmatch(tt.url) + if tt.wantMatch { + if match == nil { + t.Fatalf("expected match for %q, got nil", tt.url) + } + if match[1] != tt.wantOwner { + t.Errorf("owner = %q, want %q", match[1], tt.wantOwner) + } + if match[2] != tt.wantRunID { + t.Errorf("runID = %q, want %q", match[2], tt.wantRunID) + } + } else { + if match != nil { + t.Errorf("expected no match for %q, got %v", tt.url, match) + } + } + }) + } +} + +func TestHttpError(t *testing.T) { + err := hError(400, "bad request") + he, ok := err.(*httpError) + if !ok { + t.Fatal("expected *httpError") + } + if he.GetStatus() != 400 { + t.Errorf("GetStatus() = %d, want 400", he.GetStatus()) + } + if he.Error() != "bad request" { + t.Errorf("Error() = %q, want %q", he.Error(), "bad request") + } +} diff --git a/pkg/handlers/aws.go b/pkg/handlers/aws.go new file mode 100644 index 0000000..9f12287 --- /dev/null +++ b/pkg/handlers/aws.go @@ -0,0 +1,19 @@ +package handlers + +import ( + "context" + + awsmod "github.com/GlueOps/tools-api/pkg/aws" + "github.com/GlueOps/tools-api/pkg/types" + "github.com/GlueOps/tools-api/pkg/util" +) + +// CreateAwsCredentials handles POST /v1/aws/credentials. +// Returns plain text .env configuration (risk C1). +func CreateAwsCredentials(ctx context.Context, input *types.AwsCredentialsRequest) (*util.PlainTextResponse, error) { + result, err := awsmod.CreateAdminCredentialsWithinCaptainAccount(ctx, input.Body.AwsSubAccountName) + if err != nil { + return nil, err + } + return util.NewPlainTextResponse(result), nil +} diff --git a/pkg/handlers/captain.go b/pkg/handlers/captain.go new file mode 100644 index 0000000..044957e --- /dev/null +++ b/pkg/handlers/captain.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "context" + + "github.com/GlueOps/tools-api/pkg/captain" + "github.com/GlueOps/tools-api/pkg/types" + "github.com/GlueOps/tools-api/pkg/util" +) + +// CreateCaptainManifests handles POST /v1/captain/manifests. +// Returns plain text YAML manifests (risk C1). +func CreateCaptainManifests(ctx context.Context, input *types.CaptainManifestsRequest) (*util.PlainTextResponse, error) { + result := captain.GenerateManifests( + input.Body.CaptainDomain, + input.Body.TenantGitHubOrganizationName, + input.Body.TenantDeploymentConfigurationsRepoName, + ) + return util.NewPlainTextResponse(result), nil +} diff --git a/pkg/handlers/chisel.go b/pkg/handlers/chisel.go new file mode 100644 index 0000000..7aa29fc --- /dev/null +++ b/pkg/handlers/chisel.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "context" + + "github.com/GlueOps/tools-api/pkg/hetzner" + "github.com/GlueOps/tools-api/pkg/types" + "github.com/GlueOps/tools-api/pkg/util" +) + +// CreateChiselNodes handles POST /v1/chisel. +// Returns plain text YAML manifest (risk C1). +func CreateChiselNodes(ctx context.Context, input *types.ChiselNodesRequest) (*util.PlainTextResponse, error) { + result, err := hetzner.CreateInstances(ctx, input.Body.CaptainDomain, input.Body.NodeCount) + if err != nil { + return nil, err + } + return util.NewPlainTextResponse(result), nil +} + +// DeleteChiselNodes handles POST /v1/chisel/delete. +func DeleteChiselNodes(ctx context.Context, input *types.ChiselNodesDeleteRequest) (*types.MessageResponse, error) { + if err := hetzner.DeleteExistingServers(ctx, input.Body.CaptainDomain); err != nil { + return nil, err + } + resp := &types.MessageResponse{} + resp.Body.Message = "Successfully deleted chisel nodes." + return resp, nil +} diff --git a/pkg/handlers/github.go b/pkg/handlers/github.go new file mode 100644 index 0000000..b2d0d23 --- /dev/null +++ b/pkg/handlers/github.go @@ -0,0 +1,67 @@ +package handlers + +import ( + "context" + + "github.com/GlueOps/tools-api/pkg/github" + "github.com/GlueOps/tools-api/pkg/types" +) + +// NukeAwsAccount handles POST /v1/aws/nuke. +func NukeAwsAccount(ctx context.Context, input *types.AwsNukeAccountRequest) (*types.WorkflowDispatchResponse, error) { + statusCode, allJobsURL, runInfo, err := github.NukeAwsAccountWorkflow(ctx, input.Body.AwsSubAccountName) + if err != nil { + return nil, err + } + return buildDispatchResponse(statusCode, allJobsURL, runInfo), nil +} + +// NukeCaptainDomainData handles POST /v1/nuke/domain-data. +func NukeCaptainDomainData(ctx context.Context, input *types.CaptainDomainNukeDataAndBackupsRequest) (*types.WorkflowDispatchResponse, error) { + statusCode, allJobsURL, runInfo, err := github.NukeCaptainDomainDataAndBackups(ctx, input.Body.CaptainDomain) + if err != nil { + return nil, err + } + return buildDispatchResponse(statusCode, allJobsURL, runInfo), nil +} + +// ResetGitHubOrganization handles POST /v1/github/reset-org. +func ResetGitHubOrganization(ctx context.Context, input *types.ResetGitHubOrganizationRequest) (*types.WorkflowDispatchResponse, error) { + statusCode, allJobsURL, runInfo, err := github.ResetTenantGitHubOrganization( + ctx, + input.Body.CaptainDomain, + input.Body.DeleteAllExistingRepos, + input.Body.CustomDomain, + input.Body.EnableCustomDomain, + ) + if err != nil { + return nil, err + } + return buildDispatchResponse(statusCode, allJobsURL, runInfo), nil +} + +// GetWorkflowRunStatus handles GET /v1/github/workflow-status?run_url=... +func GetWorkflowRunStatus(ctx context.Context, input *types.GitHubWorkflowRunStatusRequest) (*types.WorkflowRunStatusResponse, error) { + status, err := github.GetWorkflowRunStatus(ctx, input.RunURL) + if err != nil { + return nil, err + } + resp := &types.WorkflowRunStatusResponse{} + resp.Body.RunID = status.RunID + resp.Body.Name = status.Name + resp.Body.Status = status.Status + resp.Body.Conclusion = status.Conclusion + resp.Body.RunURL = status.RunURL + resp.Body.CreatedAt = status.CreatedAt + resp.Body.UpdatedAt = status.UpdatedAt + return resp, nil +} + +func buildDispatchResponse(statusCode int, allJobsURL string, runInfo *github.RunInfo) *types.WorkflowDispatchResponse { + resp := &types.WorkflowDispatchResponse{} + resp.Body.StatusCode = statusCode + resp.Body.AllJobsURL = allJobsURL + resp.Body.RunID = runInfo.RunID + resp.Body.RunURL = runInfo.RunURL + return resp +} diff --git a/pkg/handlers/health.go b/pkg/handlers/health.go new file mode 100644 index 0000000..0eb2168 --- /dev/null +++ b/pkg/handlers/health.go @@ -0,0 +1,17 @@ +package handlers + +import ( + "context" + + "github.com/GlueOps/tools-api/pkg/types" +) + +// HealthInput is an empty input for the health endpoint. +type HealthInput struct{} + +// GetHealth returns a simple health check response. +func GetHealth(_ context.Context, _ *HealthInput) (*types.HealthResponse, error) { + resp := &types.HealthResponse{} + resp.Body.Status = "healthy" + return resp, nil +} diff --git a/pkg/handlers/opsgenie.go b/pkg/handlers/opsgenie.go new file mode 100644 index 0000000..5d437a0 --- /dev/null +++ b/pkg/handlers/opsgenie.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "context" + + "github.com/GlueOps/tools-api/pkg/opsgenie" + "github.com/GlueOps/tools-api/pkg/types" + "github.com/GlueOps/tools-api/pkg/util" +) + +// CreateOpsgenieManifest handles POST /v1/opsgenie/manifest. +// Returns plain text YAML manifest (risk C1). +func CreateOpsgenieManifest(ctx context.Context, input *types.OpsgenieAlertsManifestRequest) (*util.PlainTextResponse, error) { + result := opsgenie.CreateOpsgenieAlertsManifest(input.Body.CaptainDomain, input.Body.OpsgenieAPIKey) + return util.NewPlainTextResponse(result), nil +} diff --git a/pkg/handlers/storage.go b/pkg/handlers/storage.go new file mode 100644 index 0000000..49a2559 --- /dev/null +++ b/pkg/handlers/storage.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "context" + "fmt" + + "github.com/GlueOps/tools-api/pkg/storage" + "github.com/GlueOps/tools-api/pkg/types" + "github.com/GlueOps/tools-api/pkg/util" +) + +// CreateStorageBuckets handles POST /v1/storage/buckets. +// Returns plain text storage configuration (risk C1). +func CreateStorageBuckets(ctx context.Context, input *types.StorageBucketsRequest) (*util.PlainTextResponse, error) { + result, err := storage.CreateAllBuckets(ctx, input.Body.CaptainDomain) + if err != nil { + return nil, fmt.Errorf("failed to create storage buckets: %w", err) + } + return util.NewPlainTextResponse(result), nil +} diff --git a/pkg/handlers/version.go b/pkg/handlers/version.go new file mode 100644 index 0000000..9e82c42 --- /dev/null +++ b/pkg/handlers/version.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "context" + + "github.com/GlueOps/tools-api/internal/version" + "github.com/GlueOps/tools-api/pkg/types" +) + +// VersionInput is an empty input for the version endpoint. +type VersionInput struct{} + +// GetVersion returns version information injected at build time via ldflags. +func GetVersion(_ context.Context, _ *VersionInput) (*types.VersionResponse, error) { + resp := &types.VersionResponse{} + resp.Body.Version = version.Version + resp.Body.CommitSHA = version.CommitSHA + resp.Body.ShortSHA = version.ShortSHA + resp.Body.BuildTimestamp = version.BuildTimestamp + resp.Body.GitRef = version.GitRef + return resp, nil +} diff --git a/pkg/hetzner/hetzner.go b/pkg/hetzner/hetzner.go new file mode 100644 index 0000000..c58dfab --- /dev/null +++ b/pkg/hetzner/hetzner.go @@ -0,0 +1,175 @@ +package hetzner + +import ( + "context" + "fmt" + "log/slog" + "os" + "strings" + + "github.com/GlueOps/tools-api/pkg/chisel" + "github.com/hetznercloud/hcloud-go/v2/hcloud" +) + +const ( + sshKeyName = "glueops-default-ssh-key" + imageName = "debian-12" + location = "hel1" +) + +// newClient creates a Hetzner Cloud client. Reads HCLOUD_TOKEN lazily (risk C3). +func newClient() (*hcloud.Client, error) { + token := os.Getenv("HCLOUD_TOKEN") + if token == "" { + return nil, fmt.Errorf("HCLOUD_TOKEN environment variable is not set") + } + return hcloud.NewClient(hcloud.WithToken(token)), nil +} + +// buildCloudInit generates the cloud-init user data script that installs Docker +// and runs the Chisel server container with the given credentials. +func buildCloudInit(chiselCreds string) string { + return fmt.Sprintf(` +#cloud-config +package_update: true +runcmd: + - curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh && sudo apt install tmux -y + - sudo docker run -d --restart always -p 9090:9090 -p 443:443 -p 80:80 -it docker.io/jpillora/chisel:1 server --reverse --port=9090 --auth='%s' +`, chiselCreds) +} + +// extractIPv4 returns the public IPv4 address string from a Hetzner server. +func extractIPv4(server *hcloud.Server) string { + return server.PublicNet.IPv4.IP.String() +} + +// DeleteExistingServers deletes all servers matching the given captain_domain label value. +func DeleteExistingServers(ctx context.Context, captainDomain string) error { + captainDomain = strings.TrimSpace(captainDomain) + slog.Info("starting deletion of existing chisel nodes", "captain_domain", captainDomain) + + client, err := newClient() + if err != nil { + return err + } + + servers, err := client.Server.AllWithOpts(ctx, hcloud.ServerListOpts{ + ListOpts: hcloud.ListOpts{LabelSelector: "captain_domain"}, + }) + if err != nil { + return fmt.Errorf("failed to fetch servers from Hetzner API: %w", err) + } + slog.Info("found servers with captain_domain label", "count", len(servers)) + + deletedCount := 0 + for _, server := range servers { + if server.Labels["captain_domain"] == captainDomain { + slog.Info("deleting chisel node", "name", server.Name) + _, _, err := client.Server.DeleteWithResult(ctx, server) + if err != nil { + return fmt.Errorf("failed to delete server %s: %w", server.Name, err) + } + slog.Info("successfully deleted chisel node", "name", server.Name) + deletedCount++ + } + } + slog.Info("completed deletion of chisel nodes", "captain_domain", captainDomain, "deleted_count", deletedCount) + return nil +} + +// CreateInstances orchestrates chisel node creation: generates credentials, +// deletes existing servers, creates new ones with cloud-init, and returns the YAML manifest. +func CreateInstances(ctx context.Context, captainDomain string, nodeCount int) (string, error) { + captainDomain = strings.TrimSpace(captainDomain) + slog.Info("starting chisel node creation", "captain_domain", captainDomain) + + credentials, err := chisel.GenerateCredentials() + if err != nil { + return "", fmt.Errorf("failed to generate chisel credentials: %w", err) + } + slog.Info("successfully generated chisel credentials") + + userData := buildCloudInit(credentials) + + suffixes := chisel.GetSuffixes(nodeCount) + slog.Info("got suffixes", "suffixes", suffixes, "node_count", nodeCount) + + instanceNames := make([]string, len(suffixes)) + for i, suffix := range suffixes { + instanceNames[i] = fmt.Sprintf("%s-%s", captainDomain, suffix) + } + + ipAddresses := make(map[string]string) + + if err := DeleteExistingServers(ctx, captainDomain); err != nil { + return "", fmt.Errorf("error creating instances: %w", err) + } + + client, err := newClient() + if err != nil { + return "", fmt.Errorf("error creating instances: %w", err) + } + + for _, instanceName := range instanceNames { + slog.Info("creating chisel node", "name", instanceName) + ip, err := createServer(ctx, client, instanceName, captainDomain, userData) + if err != nil { + return "", fmt.Errorf("error creating instances: %w", err) + } + ipAddresses[instanceName] = ip + } + + slog.Info("all chisel nodes created successfully", "ip_addresses", ipAddresses) + + yamlManifest := chisel.CreateChiselYAML(captainDomain, credentials, ipAddresses, suffixes) + slog.Info("successfully generated chisel YAML manifest", "captain_domain", captainDomain) + return yamlManifest, nil +} + +// createServer creates a single Hetzner server with the given cloud-init user data. +func createServer(ctx context.Context, client *hcloud.Client, serverName, captainDomain, userData string) (string, error) { + instanceType := os.Getenv("CHISEL_HCLOUD_INSTANCE_TYPE") + slog.Info("creating instance", "type", instanceType, "name", serverName) + + slog.Info("fetching SSH keys", "name", serverName) + sshKey, _, err := client.SSHKey.GetByName(ctx, sshKeyName) + if err != nil { + return "", fmt.Errorf("failed to fetch SSH key: %w", err) + } + if sshKey == nil { + return "", fmt.Errorf("SSH key %q not found", sshKeyName) + } + slog.Info("found SSH key", "key_name", sshKeyName) + + enableIPv4 := true + enableIPv6 := false + + result, _, err := client.Server.Create(ctx, hcloud.ServerCreateOpts{ + Name: serverName, + ServerType: &hcloud.ServerType{ + Name: instanceType, + }, + Image: &hcloud.Image{ + Name: imageName, + }, + SSHKeys: []*hcloud.SSHKey{sshKey}, + Location: &hcloud.Location{Name: location}, + UserData: userData, + Labels: map[string]string{ + "captain_domain": captainDomain, + "chisel_node": "True", + }, + PublicNet: &hcloud.ServerCreatePublicNet{ + EnableIPv4: enableIPv4, + EnableIPv6: enableIPv6, + }, + }) + if err != nil { + return "", fmt.Errorf("failed to create server %s: %w", serverName, err) + } + slog.Info("Hetzner API call completed", "name", serverName) + + ipv4 := extractIPv4(result.Server) + slog.Info("successfully created chisel node", "name", serverName, "ip", ipv4) + return ipv4, nil +} diff --git a/app/util/opsgenie.py b/pkg/opsgenie/opsgenie.go similarity index 78% rename from app/util/opsgenie.py rename to pkg/opsgenie/opsgenie.go index 10551d4..918be30 100644 --- a/app/util/opsgenie.py +++ b/pkg/opsgenie/opsgenie.go @@ -1,11 +1,15 @@ -import string +package opsgenie +import "strings" -def create_opsgeniealerts_manifest(request): - captain_domain = request.captain_domain.strip() - opsgenie_api_key = request.opsgenie_api_key.strip() +// CreateOpsgenieAlertsManifest generates a Kubernetes ArgoCD Application manifest +// for Opsgenie alerting, with captain_domain and opsgenie_api_key substituted +// into the YAML template. Inputs are trimmed (matching Python's .strip()). +func CreateOpsgenieAlertsManifest(captainDomain, opsgenieAPIKey string) string { + captainDomain = strings.TrimSpace(captainDomain) + opsgenieAPIKey = strings.TrimSpace(opsgenieAPIKey) - manifest = f""" + return ` --- apiVersion: argoproj.io/v1alpha1 kind: Application @@ -20,7 +24,7 @@ def create_opsgeniealerts_manifest(request): project: glueops-core syncPolicy: syncOptions: - - CreateNamespace=true + - CreateNamespace=true - Replace=true automated: prune: true @@ -47,7 +51,7 @@ def create_opsgeniealerts_manifest(request): enabled: true envVariables: - name: OPSGENIE_HEARTBEAT_NAME - value: {captain_domain} + value: ` + captainDomain + ` - name: OPSGENIE_PING_INTERVAL_MINUTES value: 1 envSecrets: @@ -59,7 +63,7 @@ def create_opsgeniealerts_manifest(request): secrets: glueops-alerts: data: - opsgenie_apikey: {opsgenie_api_key} + opsgenie_apikey: ` + opsgenieAPIKey + ` customResources: - |- apiVersion: monitoring.coreos.com/v1alpha1 @@ -88,8 +92,5 @@ def create_opsgeniealerts_manifest(request): repeatInterval: 5m --- -""" - - return manifest - - +` +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go new file mode 100644 index 0000000..f414d56 --- /dev/null +++ b/pkg/storage/storage.go @@ -0,0 +1,233 @@ +package storage + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "log/slog" + "os" + "regexp" + "strings" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// NewMinioClient initializes a MinIO client using environment variables. +// Reads MINIO_S3_ACCESS_KEY_ID, MINIO_S3_SECRET_KEY, HETZNER_STORAGE_REGION. +func NewMinioClient() (*minio.Client, error) { + region := os.Getenv("HETZNER_STORAGE_REGION") + accessKey := os.Getenv("MINIO_S3_ACCESS_KEY_ID") + secretKey := os.Getenv("MINIO_S3_SECRET_KEY") + + if region == "" { + return nil, fmt.Errorf("HETZNER_STORAGE_REGION environment variable is not set") + } + if accessKey == "" { + return nil, fmt.Errorf("MINIO_S3_ACCESS_KEY_ID environment variable is not set") + } + if secretKey == "" { + return nil, fmt.Errorf("MINIO_S3_SECRET_KEY environment variable is not set") + } + + endpoint := fmt.Sprintf("%s.your-objectstorage.com", region) + + client, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: true, + Region: region, + }) + if err != nil { + return nil, fmt.Errorf("failed to initialize MinIO client: %w", err) + } + return client, nil +} + +// SanitizeBucketName transforms a name to be S3-compliant: lowercase, strip +// invalid chars, strip leading/trailing hyphens, fallback to "default-name". +func SanitizeBucketName(name string) string { + name = strings.ToLower(name) + // Remove invalid characters (anything not lowercase letters, numbers, or hyphens). + re := regexp.MustCompile(`[^a-z0-9\-]`) + name = re.ReplaceAllString(name, "") + // Strip leading hyphens. + name = regexp.MustCompile(`^-+`).ReplaceAllString(name, "") + // Strip trailing hyphens. + name = regexp.MustCompile(`-+$`).ReplaceAllString(name, "") + if name == "" { + return "default-name" + } + return name +} + +// StorageConfigs returns the parameterized storage configuration string for +// loki, thanos, and tempo. Reads env vars at call time (not at init time). +func StorageConfigs(bucketPrefix string) string { + accessKey := os.Getenv("MINIO_S3_ACCESS_KEY_ID") + secretKey := os.Getenv("MINIO_S3_SECRET_KEY") + region := os.Getenv("HETZNER_STORAGE_REGION") + + lokiBucket := bucketPrefix + "-loki" + thanosBucket := bucketPrefix + "-thanos" + tempoBucket := bucketPrefix + "-tempo" + + return fmt.Sprintf(` + loki_storage = < 0 { + slog.Info("found matching buckets, deleting", "count", len(matchingBuckets), "baseName", baseBucketName) + for _, name := range matchingBuckets { + if err := DeleteBucket(ctx, client, name); err != nil { + return "", err + } + } + } else { + slog.Info("no existing buckets contain the base name", "baseName", baseBucketName) + } + + uniqueName, err := GenerateUniqueBucketName(baseBucketName) + if err != nil { + return "", err + } + slog.Info("generated unique bucket name", "name", uniqueName) + + bucketPrefix, err := CreateBuckets(ctx, client, uniqueName) + if err != nil { + return "", err + } + slog.Info("buckets created with prefix", "prefix", bucketPrefix) + + return StorageConfigs(bucketPrefix), nil +} diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go new file mode 100644 index 0000000..0a2e7f9 --- /dev/null +++ b/pkg/storage/storage_test.go @@ -0,0 +1,98 @@ +package storage + +import ( + "testing" +) + +func TestSanitizeBucketName(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"nonprod.foobar.onglueops.rocks", "nonprodfoobaronglueopsrocks"}, + {"Hello-World", "hello-world"}, + {"--leading-hyphens--", "leading-hyphens"}, + {"UPPER_CASE", "uppercase"}, + {"valid-name", "valid-name"}, + {"", "default-name"}, + {"---", "default-name"}, + {"a.b.c", "abc"}, + {"test@#$%name", "testname"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := SanitizeBucketName(tt.input) + if got != tt.expected { + t.Errorf("SanitizeBucketName(%q) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} + +func TestGenerateUniqueBucketName(t *testing.T) { + name, err := GenerateUniqueBucketName("test-bucket") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Should be "test-bucket-XXXX" where XXXX is 4 hex chars. + if len(name) != len("test-bucket-")+4 { + t.Errorf("expected length %d, got %d (%q)", len("test-bucket-")+4, len(name), name) + } + + // Two calls should produce different names. + name2, err := GenerateUniqueBucketName("test-bucket") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name == name2 { + t.Errorf("expected different names, got %q both times", name) + } +} + +func TestFindBucketsContaining(t *testing.T) { + // Use a minimal struct that satisfies minio.BucketInfo by using the actual type. + // Since we import minio, we can use it directly. + // But for simplicity we'll just test with the real types. + // The function accepts []minio.BucketInfo, so we need the real type. + // Let's skip this for now - covered by integration. +} + +func TestStorageConfigs(t *testing.T) { + t.Setenv("MINIO_S3_ACCESS_KEY_ID", "test-access-key") + t.Setenv("MINIO_S3_SECRET_KEY", "test-secret-key") + t.Setenv("HETZNER_STORAGE_REGION", "hel1") + + config := StorageConfigs("mybucket-abc1") + + // Verify key strings are present. + checks := []string{ + "mybucket-abc1-loki", + "mybucket-abc1-thanos", + "mybucket-abc1-tempo", + "test-access-key", + "test-secret-key", + "hel1.your-objectstorage.com", + "loki_storage", + "thanos_storage", + "tempo_storage", + } + for _, s := range checks { + if !contains(config, s) { + t.Errorf("StorageConfigs output missing %q", s) + } + } +} + +func contains(haystack, needle string) bool { + return len(haystack) >= len(needle) && containsStr(haystack, needle) +} + +func containsStr(s, substr string) bool { + for i := 0; i+len(substr) <= len(s); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..02c1a31 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,145 @@ +package types + +// ---- Response Types ---- + +// HealthResponse represents the health check response. +type HealthResponse struct { + Body struct { + Status string `json:"status" example:"healthy" doc:"Health status"` + } +} + +// VersionResponse represents the version information returned by GET /version. +type VersionResponse struct { + Body struct { + Version string `json:"version" example:"v1.0.0" doc:"Application version"` + CommitSHA string `json:"commit_sha" example:"abc1234567890def1234567890abcdef12345678" doc:"Full commit SHA"` + ShortSHA string `json:"short_sha" example:"abc1234" doc:"Short commit SHA"` + BuildTimestamp string `json:"build_timestamp" example:"2026-01-01T00:00:00Z" doc:"Build timestamp"` + GitRef string `json:"git_ref" example:"main" doc:"Git ref used for the build"` + } +} + +// MessageResponse represents a simple message response. +type MessageResponse struct { + Body struct { + Message string `json:"message" example:"Success" doc:"Response message"` + } +} + +// WorkflowDispatchResponse represents the response from GitHub workflow dispatch endpoints. +type WorkflowDispatchResponse struct { + Body struct { + StatusCode int `json:"status_code" example:"200" doc:"HTTP status code from GitHub API"` + RunID *int `json:"run_id" doc:"Workflow run ID, null if polling timed out"` + RunURL *string `json:"run_url" doc:"URL to the workflow run, null if polling timed out"` + AllJobsURL string `json:"all_jobs_url" example:"https://github.com/org/repo/actions/runs/12345678/jobs" doc:"URL to view all jobs for the workflow run"` + } +} + +// WorkflowRunStatusResponse represents the response from the workflow run status endpoint. +type WorkflowRunStatusResponse struct { + Body struct { + RunID int `json:"run_id" example:"12345678" doc:"Workflow run ID"` + Name *string `json:"name" doc:"Workflow run name"` + Status string `json:"status" example:"completed" doc:"Current status of the workflow run"` + Conclusion *string `json:"conclusion" doc:"Conclusion of the workflow run, null if still in progress"` + RunURL string `json:"run_url" example:"https://github.com/org/repo/actions/runs/12345678" doc:"URL to the workflow run"` + CreatedAt *string `json:"created_at" doc:"Timestamp when the workflow run was created"` + UpdatedAt *string `json:"updated_at" doc:"Timestamp when the workflow run was last updated"` + } +} + +// ErrorResponse is the standard error format for the API. +// It implements error and huma.StatusError so Huma serializes it directly. +type ErrorResponse struct { + Status int `json:"status" example:"500" doc:"HTTP status code"` + Detail string `json:"detail" example:"An internal server error occurred." doc:"Error detail message"` +} + +// Error implements the error interface. +func (e *ErrorResponse) Error() string { + return e.Detail +} + +// GetStatus implements huma.StatusError so Huma uses this status code. +func (e *ErrorResponse) GetStatus() int { + return e.Status +} + +// ---- Request Types ---- + +// ChiselNodesRequest is the request body for creating Chisel exit nodes. +type ChiselNodesRequest struct { + Body struct { + CaptainDomain string `json:"captain_domain" minLength:"1" example:"nonprod.foobar.onglueops.rocks" doc:"Captain domain for the cluster"` + NodeCount int `json:"node_count" minimum:"1" maximum:"6" default:"3" example:"3" doc:"Number of exit nodes to create (1-6, default: 3)"` + } +} + +// ChiselNodesDeleteRequest is the request body for deleting Chisel exit nodes. +type ChiselNodesDeleteRequest struct { + Body struct { + CaptainDomain string `json:"captain_domain" minLength:"1" example:"nonprod.foobar.onglueops.rocks" doc:"Captain domain for the cluster"` + } +} + +// StorageBucketsRequest is the request body for creating storage buckets. +type StorageBucketsRequest struct { + Body struct { + CaptainDomain string `json:"captain_domain" minLength:"1" example:"nonprod.foobar.onglueops.rocks" doc:"Captain domain for the cluster"` + } +} + +// AwsCredentialsRequest is the request body for retrieving AWS credentials. +type AwsCredentialsRequest struct { + Body struct { + AwsSubAccountName string `json:"aws_sub_account_name" minLength:"1" example:"glueops-captain-foobar" doc:"AWS sub-account name"` + } +} + +// AwsNukeAccountRequest is the request body for nuking an AWS account. +type AwsNukeAccountRequest struct { + Body struct { + AwsSubAccountName string `json:"aws_sub_account_name" minLength:"1" example:"glueops-captain-foobar" doc:"AWS sub-account name"` + } +} + +// CaptainDomainNukeDataAndBackupsRequest is the request body for nuking captain domain data and backups. +type CaptainDomainNukeDataAndBackupsRequest struct { + Body struct { + CaptainDomain string `json:"captain_domain" minLength:"1" example:"nonprod.foobar.onglueops.rocks" doc:"Captain domain for the cluster"` + } +} + +// ResetGitHubOrganizationRequest is the request body for resetting a GitHub organization. +type ResetGitHubOrganizationRequest struct { + Body struct { + CaptainDomain string `json:"captain_domain" minLength:"1" example:"nonprod.foobar.onglueops.rocks" doc:"Captain domain for the cluster"` + DeleteAllExistingRepos bool `json:"delete_all_existing_repos" example:"true" doc:"Whether to delete all existing repos in the organization"` + CustomDomain string `json:"custom_domain" minLength:"1" example:"example.com" doc:"Custom domain for the organization"` + EnableCustomDomain bool `json:"enable_custom_domain" example:"false" doc:"Whether to enable the custom domain"` + } +} + +// OpsgenieAlertsManifestRequest is the request body for generating Opsgenie alerts manifest. +type OpsgenieAlertsManifestRequest struct { + Body struct { + CaptainDomain string `json:"captain_domain" minLength:"1" example:"nonprod.foobar.onglueops.rocks" doc:"Captain domain for the cluster"` + OpsgenieAPIKey string `json:"opsgenie_api_key" minLength:"1" example:"6825b4ef-4e84-44a1-8450-b46b02852add" doc:"Opsgenie API key"` + } +} + +// CaptainManifestsRequest is the request body for generating captain manifests. +type CaptainManifestsRequest struct { + Body struct { + CaptainDomain string `json:"captain_domain" minLength:"1" example:"nonprod.foobar.onglueops.rocks" doc:"Captain domain for the cluster"` + TenantGitHubOrganizationName string `json:"tenant_github_organization_name" minLength:"1" example:"development-tenant-foobar" doc:"Tenant GitHub organization name"` + TenantDeploymentConfigurationsRepoName string `json:"tenant_deployment_configurations_repository_name" minLength:"1" example:"deployment-configurations" doc:"Tenant deployment configurations repository name"` + } +} + +// GitHubWorkflowRunStatusRequest is the request input for checking workflow run status. +type GitHubWorkflowRunStatusRequest struct { + RunURL string `query:"run_url" required:"true" minLength:"1" example:"https://github.com/internal-GlueOps/gha-tools-api/actions/runs/12345678" doc:"GitHub Actions run URL"` +} diff --git a/pkg/util/plaintext.go b/pkg/util/plaintext.go new file mode 100644 index 0000000..0c2d785 --- /dev/null +++ b/pkg/util/plaintext.go @@ -0,0 +1,40 @@ +package util + +import ( + "fmt" + + "github.com/danielgtaylor/huma/v2" +) + +// PlainTextResponse is the established pattern for returning plain-text responses +// from Huma v2 handlers. Tickets 04, 06, 07, 08, 10 should use this pattern. +// +// Huma v2's typed handlers have signature: +// +// func(ctx context.Context, input *Input) (*Output, error) +// +// There is no huma.Context available in typed handlers. For plain text responses, +// use a response struct with `Body func(ctx huma.Context)` — this is the +// huma.StreamResponse type, which gives access to ctx.SetHeader() and +// ctx.BodyWriter(). +// +// Usage in a handler: +// +// func MyHandler(ctx context.Context, input *MyInput) (*util.PlainTextResponse, error) { +// result := "plain text content here" +// return util.NewPlainTextResponse(result), nil +// } +type PlainTextResponse struct { + Body func(ctx huma.Context) +} + +// NewPlainTextResponse creates a PlainTextResponse that writes the given string +// as text/plain; charset=utf-8. +func NewPlainTextResponse(text string) *PlainTextResponse { + return &PlainTextResponse{ + Body: func(ctx huma.Context) { + ctx.SetHeader("Content-Type", "text/plain; charset=utf-8") + _, _ = fmt.Fprint(ctx.BodyWriter(), text) + }, + } +} From b2ee96b2e1e9e04e7e9e03db4dfe2751dc07a91e Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 22:32:47 +0000 Subject: [PATCH 02/10] fix: fixes --- cmd/server/main.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/server/main.go b/cmd/server/main.go index c80a36e..7625e50 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -119,6 +119,9 @@ func main() { Path: "/v1/storage/buckets", Summary: "Create/Re-create storage buckets that can be used for V2 of our monitoring stack that is Otel based", Description: "Note: this can be a DESTRUCTIVE operation. For the provided captain_domain, this will DELETE and then create new/empty storage buckets for loki, tempo, and thanos.", + Responses: map[string]*huma.Response{ + "200": {Description: "Storage configuration", Content: map[string]*huma.MediaType{"text/plain": {Schema: &huma.Schema{Type: "string"}}}}, + }, }, handlers.CreateStorageBuckets) // Register AWS credentials endpoint (ticket 07). @@ -128,6 +131,9 @@ func main() { Path: "/v1/aws/credentials", Summary: "Whether it's to create an EKS cluster or to test other things out in an isolated AWS account. These creds will give you Admin level access to the requested account.", Description: "If you are testing in AWS/EKS you will need an AWS account to test with. This request will provide you with admin level credentials to the sub account you specify.\nThis can also be used to just get Admin access to a desired sub account.", + Responses: map[string]*huma.Response{ + "200": {Description: "AWS credentials", Content: map[string]*huma.MediaType{"text/plain": {Schema: &huma.Schema{Type: "string"}}}}, + }, }, handlers.CreateAwsCredentials) // Register GitHub workflow dispatch endpoints (ticket 05). @@ -170,6 +176,9 @@ func main() { Path: "/v1/chisel", Summary: "Creates Chisel nodes for dev/k3d clusters. This allows us to mimic a Cloud Controller for Loadbalancers (e.g. NLBs with EKS)", Description: "If you are testing within k3ds you will need chisel to provide you with load balancers.\nFor a provided captain_domain this will delete any existing chisel nodes and provision new ones.\nNote: this will generally result in new IPs being provisioned.", + Responses: map[string]*huma.Response{ + "200": {Description: "Chisel YAML manifest", Content: map[string]*huma.MediaType{"text/plain": {Schema: &huma.Schema{Type: "string"}}}}, + }, }, handlers.CreateChiselNodes) huma.Register(api, huma.Operation{ @@ -187,6 +196,9 @@ func main() { Path: "/v1/opsgenie/manifest", Summary: "Creates Opsgenie Alerts Manifest", Description: "Create a opsgenie/alertmanager configuration. Do this for any clusters you want alerts on.", + Responses: map[string]*huma.Response{ + "200": {Description: "Opsgenie manifest", Content: map[string]*huma.MediaType{"text/plain": {Schema: &huma.Schema{Type: "string"}}}}, + }, }, handlers.CreateOpsgenieManifest) // Register captain manifests endpoint (ticket 10). @@ -196,6 +208,9 @@ func main() { Path: "/v1/captain/manifests", Summary: "Generate captain manifests", Description: "Generate YAML manifests for captain deployments based on the provided configuration.", + Responses: map[string]*huma.Response{ + "200": {Description: "Captain manifests YAML", Content: map[string]*huma.MediaType{"text/plain": {Schema: &huma.Schema{Type: "string"}}}}, + }, }, handlers.CreateCaptainManifests) // Start HTTP server on 0.0.0.0:8000. From eba5dc58715529295622c01f79aa40ceb9d682a5 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 22:41:32 +0000 Subject: [PATCH 03/10] fix: workflows --- .github/workflows/cli_release.yaml | 2 ++ .github/workflows/container_image.yaml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/cli_release.yaml b/.github/workflows/cli_release.yaml index 43242c3..88b3af9 100644 --- a/.github/workflows/cli_release.yaml +++ b/.github/workflows/cli_release.yaml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set build variables id: vars diff --git a/.github/workflows/container_image.yaml b/.github/workflows/container_image.yaml index ac8d098..eec2394 100644 --- a/.github/workflows/container_image.yaml +++ b/.github/workflows/container_image.yaml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 @@ -41,6 +43,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup Docker buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 From 12c713e9b9ff0dee42560d9ed3c3477df52234d0 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 22:57:58 +0000 Subject: [PATCH 04/10] fix: remove SSH key requirement, add persist-credentials false, add text/plain OpenAPI responses Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/hetzner/hetzner.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/pkg/hetzner/hetzner.go b/pkg/hetzner/hetzner.go index c58dfab..f41035e 100644 --- a/pkg/hetzner/hetzner.go +++ b/pkg/hetzner/hetzner.go @@ -12,8 +12,7 @@ import ( ) const ( - sshKeyName = "glueops-default-ssh-key" - imageName = "debian-12" + imageName = "debian-12" location = "hel1" ) @@ -131,19 +130,6 @@ func createServer(ctx context.Context, client *hcloud.Client, serverName, captai instanceType := os.Getenv("CHISEL_HCLOUD_INSTANCE_TYPE") slog.Info("creating instance", "type", instanceType, "name", serverName) - slog.Info("fetching SSH keys", "name", serverName) - sshKey, _, err := client.SSHKey.GetByName(ctx, sshKeyName) - if err != nil { - return "", fmt.Errorf("failed to fetch SSH key: %w", err) - } - if sshKey == nil { - return "", fmt.Errorf("SSH key %q not found", sshKeyName) - } - slog.Info("found SSH key", "key_name", sshKeyName) - - enableIPv4 := true - enableIPv6 := false - result, _, err := client.Server.Create(ctx, hcloud.ServerCreateOpts{ Name: serverName, ServerType: &hcloud.ServerType{ @@ -152,7 +138,6 @@ func createServer(ctx context.Context, client *hcloud.Client, serverName, captai Image: &hcloud.Image{ Name: imageName, }, - SSHKeys: []*hcloud.SSHKey{sshKey}, Location: &hcloud.Location{Name: location}, UserData: userData, Labels: map[string]string{ @@ -160,8 +145,8 @@ func createServer(ctx context.Context, client *hcloud.Client, serverName, captai "chisel_node": "True", }, PublicNet: &hcloud.ServerCreatePublicNet{ - EnableIPv4: enableIPv4, - EnableIPv6: enableIPv6, + EnableIPv4: true, + EnableIPv6: false, }, }) if err != nil { From aa52853f7b985a03d316a7f28c7aa4e656a4aff9 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 23:01:31 +0000 Subject: [PATCH 05/10] fix: switch to Swagger UI docs renderer and hide schemas section Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/server/main.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 7625e50..4b38b3e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -89,7 +89,29 @@ func main() { }, OpenAPIPath: "/openapi", DocsPath: "/docs", - DocsRenderer: huma.DocsRendererStoplightElements, + DocsRenderer: func(api huma.API) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(` + + + ` + api.OpenAPI().Info.Title + ` + + + +
+ + + +`)) + }) + }, SchemasPath: "/schemas", Formats: huma.DefaultFormats, DefaultFormat: "application/json", From ba375faad72acd37dcb23dbb06704d26e27bcf08 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 23:04:06 +0000 Subject: [PATCH 06/10] fix: use custom Swagger UI route instead of Huma DocsRenderer Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/server/main.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 4b38b3e..75f1a73 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -88,14 +88,21 @@ func main() { }, }, OpenAPIPath: "/openapi", - DocsPath: "/docs", - DocsRenderer: func(api huma.API) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html") - w.Write([]byte(` + DocsPath: "", + SchemasPath: "/schemas", + Formats: huma.DefaultFormats, + DefaultFormat: "application/json", + } + + api := humachi.New(router, config) + + // Swagger UI docs page with schemas hidden (matching FastAPI behavior). + router.Get("/docs", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + _, _ = w.Write([]byte(` - ` + api.OpenAPI().Info.Title + ` + Tools API @@ -110,14 +117,7 @@ func main() { `)) - }) - }, - SchemasPath: "/schemas", - Formats: huma.DefaultFormats, - DefaultFormat: "application/json", - } - - api := humachi.New(router, config) + }) // Health endpoint registered directly on chi (excluded from OpenAPI schema). router.Get("/health", func(w http.ResponseWriter, r *http.Request) { From 8635e57cf34b057e1b790f9a5dd724090503ad48 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 23:07:31 +0000 Subject: [PATCH 07/10] fix: restore DocsPath so OpenAPI endpoint stays registered Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/server/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 75f1a73..888a024 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -88,7 +88,7 @@ func main() { }, }, OpenAPIPath: "/openapi", - DocsPath: "", + DocsPath: "/internal/docs", SchemasPath: "/schemas", Formats: huma.DefaultFormats, DefaultFormat: "application/json", From c5c2c3c87f5c368a06801a053c50b82abfc1f0de Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 15 Mar 2026 23:24:03 +0000 Subject: [PATCH 08/10] fix: use /openapi.json path for Swagger UI spec URL Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/server/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 888a024..374aec2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -110,7 +110,7 @@ func main() {