Skip to content

Commit bab4bdd

Browse files
feat: adding cli for tools (#123)
* feat: adding cli for tools * fix: hashes * feat: update cli and docs * chore: update docs * fix: per copilot and make json output prettyified * fix: per copilot recommendations * fix: docs
1 parent 0383313 commit bab4bdd

34 files changed

Lines changed: 4706 additions & 14 deletions

.ai/AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This file provides guidance to AI coding assistants when working with code in th
66

77
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.
88

9+
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.
10+
911
## Development Setup
1012

1113
```bash
@@ -35,9 +37,10 @@ The Dockerfile uses `python:3.14-slim` as base, installs dependencies via pipenv
3537
## Architecture
3638

3739
- **`app/main.py`** — FastAPI app entry point. Defines all API routes, global exception handler, health/version endpoints. Routes redirect `/` to `/docs`.
38-
- **`app/schemas/schemas.py`** — Pydantic request/response models for all endpoints.
40+
- **`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.
3941
- **`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`.
4042
- **`app/templates/captain_manifests/`** — Jinja2 templates (`.yaml.j2`) for generating Kubernetes manifests (Namespace, AppProject, ApplicationSet).
43+
- **`cli/`** — Go CLI binary. See [`cli/.ai/AGENTS.md`](../cli/.ai/AGENTS.md).
4144

4245
All routes are defined directly in `main.py` (no router separation). Each route delegates to a corresponding util module.
4346

@@ -52,4 +55,5 @@ GitHub workflow endpoints (`github.py`) dispatch workflows via the GitHub API an
5255

5356
## CI/CD
5457

55-
GitHub Actions workflow (`.github/workflows/container_image.yaml`) builds and pushes Docker images to GHCR on any push.
58+
- **`.github/workflows/container_image.yaml`** — Builds and pushes Docker images to GHCR on any push.
59+
- **`.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.

.github/workflows/cli_release.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CLI Release
2+
3+
on:
4+
push:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
build-and-release:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
16+
17+
- name: Set build variables
18+
id: vars
19+
run: |
20+
echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
21+
echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
22+
echo "build_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT
23+
24+
- name: Build CLI binaries
25+
working-directory: cli
26+
run: |
27+
make build-all \
28+
VERSION=${{ steps.vars.outputs.version }} \
29+
COMMIT_SHA=${{ github.sha }} \
30+
SHORT_SHA=${{ steps.vars.outputs.short_sha }} \
31+
BUILD_TIMESTAMP=${{ steps.vars.outputs.build_timestamp }} \
32+
GIT_REF=${{ github.ref_name }}
33+
34+
- name: Upload workflow artifacts
35+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
36+
with:
37+
name: tools-cli-binaries
38+
path: |
39+
cli/tools-linux-amd64
40+
cli/tools-linux-arm64
41+
cli/tools-darwin-amd64
42+
cli/tools-darwin-arm64
43+
44+
- name: Delete existing release for this ref
45+
run: gh release delete "${{ github.ref_name }}" --yes 2>/dev/null || true
46+
env:
47+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48+
49+
- name: Create GitHub Release
50+
uses: softprops/action-gh-release@1853d73993c8ca1b2c9c1a7fede39682d0ab5c2a # v2.5.3
51+
with:
52+
tag_name: ${{ github.ref_name }}
53+
name: ${{ github.ref_name }}
54+
prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') }}
55+
files: |
56+
cli/tools-linux-amd64
57+
cli/tools-linux-arm64
58+
cli/tools-darwin-amd64
59+
cli/tools-darwin-arm64
60+
body: |
61+
CLI binaries built from `${{ github.sha }}` on `${{ github.ref_name }}`.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,8 @@ cython_debug/
163163

164164
# ignore devbox
165165
.devbox
166+
167+
# Go CLI build artifacts
168+
cli/tools
169+
cli/tools-linux-*
170+
cli/tools-darwin-*

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
# tools-api
22
This FastAPI app has various utilities to help streamline local development for Platform Engineers that are working on the GlueOps Platform. This tool is not meant for customers or any end users of the GlueOps Platform.
33

4-
## Setup
4+
## CLI
5+
6+
The `tools` CLI lets you interact with the API from the command line. Download the latest binary for your platform from [GitHub Releases](https://github.com/GlueOps/tools-api/releases).
7+
8+
```bash
9+
# Authenticate
10+
tools login
11+
12+
# Example: create storage buckets
13+
tools storage-buckets create --captain-domain nonprod.foobar.onglueops.rocks
14+
15+
# See all commands
16+
tools --help
17+
```
18+
19+
The CLI self-updates automatically when the API version changes. See [`cli/`](cli/) for development details.
20+
21+
## API Setup
522

623
1. Launch codespace
724
2. Configure environment variables (see below)

app/main.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pydantic import BaseModel, Field
66
from contextlib import asynccontextmanager
77
import os, glueops.setup_logging, traceback, base64, yaml, tempfile, json
8-
from schemas.schemas import Message, AwsCredentialsRequest, StorageBucketsRequest, AwsNukeAccountRequest, CaptainDomainNukeDataAndBackupsRequest, ChiselNodesRequest, ChiselNodesDeleteRequest, ResetGitHubOrganizationRequest, OpsgenieAlertsManifestRequest, CaptainManifestsRequest, GitHubWorkflowRunStatusRequest
8+
from schemas.schemas import Message, AwsCredentialsRequest, StorageBucketsRequest, AwsNukeAccountRequest, CaptainDomainNukeDataAndBackupsRequest, ChiselNodesRequest, ChiselNodesDeleteRequest, ResetGitHubOrganizationRequest, OpsgenieAlertsManifestRequest, CaptainManifestsRequest, GitHubWorkflowRunStatusRequest, VersionResponse
99
from util import storage, aws_setup_test_account_credentials, github, hetzner, opsgenie, captain_manifests
1010
from fastapi.responses import RedirectResponse
1111

@@ -53,7 +53,7 @@ async def hello(request: StorageBucketsRequest):
5353
return storage.create_all_buckets(request.captain_domain)
5454

5555

56-
@app.post("/v1/setup-aws-account-credentials", response_class=PlainTextResponse, summary="Wether 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.")
56+
@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.")
5757
async def create_credentials_for_aws_captain_account(request: AwsCredentialsRequest):
5858
"""
5959
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.
@@ -88,7 +88,7 @@ async def reset_github_organization(request: ResetGitHubOrganizationRequest):
8888
8989
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.
9090
91-
WARNING: By default delete_all_existing_repos = True. Please set it to False or make a manual backup if you are concerned about any dataloss within your tenant org (e.g. github.com/development-tenant-*)
91+
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-*)
9292
9393
"""
9494
return github.reset_tenant_github_organization(request.captain_domain, request.delete_all_existing_repos, request.custom_domain, request.enable_custom_domain)
@@ -153,12 +153,12 @@ async def health():
153153
return {"status": "healthy"}
154154

155155

156-
@app.get("/version", summary="Contains version information about this tools-api")
156+
@app.get("/version", response_model=VersionResponse, summary="Contains version information about this tools-api")
157157
async def version():
158-
return {
159-
"version": os.getenv("VERSION", "UNKNOWN"),
160-
"commit_sha": os.getenv("COMMIT_SHA", "UNKNOWN"),
161-
"short_sha": os.getenv("SHORT_SHA", "UNKNOWN"),
162-
"build_timestamp": os.getenv("BUILD_TIMESTAMP", "UNKNOWN"),
163-
"git_ref": os.getenv("GIT_REF", "UNKNOWN")
164-
}
158+
return VersionResponse(
159+
version=os.getenv("VERSION", "UNKNOWN"),
160+
commit_sha=os.getenv("COMMIT_SHA", "UNKNOWN"),
161+
short_sha=os.getenv("SHORT_SHA", "UNKNOWN"),
162+
build_timestamp=os.getenv("BUILD_TIMESTAMP", "UNKNOWN"),
163+
git_ref=os.getenv("GIT_REF", "UNKNOWN"),
164+
)

app/schemas/schemas.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
class Message(BaseModel):
55
message: str = Field(...,example = 'Success')
66

7+
class VersionResponse(BaseModel):
8+
version: str = Field(..., example='v1.0.0')
9+
commit_sha: str = Field(..., example='abc1234567890def1234567890abcdef12345678')
10+
short_sha: str = Field(..., example='abc1234')
11+
build_timestamp: str = Field(..., example='2026-01-01T00:00:00Z')
12+
git_ref: str = Field(..., example='main')
13+
714
class ChiselNodesRequest(BaseModel):
815
captain_domain: str = Field(..., example='nonprod.foobar.onglueops.rocks')
916
node_count: int = Field(

cli/.ai/AGENTS.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# AGENTS.md — CLI
2+
3+
This file provides guidance to AI coding assistants when working with the `tools` CLI.
4+
5+
## Overview
6+
7+
`tools` is a Go CLI that wraps the GlueOps Tools API. It authenticates via Dex device code flow through oauth2-proxy and self-updates when the API version changes.
8+
9+
All Go builds use Docker (`golang:1.24-alpine`, pinned by digest in the Makefile) — no local Go toolchain is required.
10+
11+
## Build
12+
13+
```bash
14+
cd cli
15+
16+
# Build for current platform
17+
make build
18+
19+
# Build for all release platforms (linux/darwin × amd64/arm64)
20+
make build-all
21+
22+
# Regenerate OpenAPI client after API changes
23+
make generate
24+
25+
# Clean build artifacts
26+
make clean
27+
```
28+
29+
## Regenerating the API Client
30+
31+
When the API changes (new endpoints, schema changes, updated descriptions/examples), run:
32+
33+
```bash
34+
cd cli
35+
make generate
36+
```
37+
38+
This does three things:
39+
1. Builds the tools-api Docker image and exports the OpenAPI spec to `openapi.json`
40+
2. Runs `oapi-codegen` (pinned to v2.6.0) via Docker to regenerate `api/generated.go`
41+
3. Copies `openapi.json` to `internal/spec/openapi.json` for embedding
42+
43+
The generated client (`api/generated.go`) and both copies of `openapi.json` are committed to the repo.
44+
45+
## Architecture
46+
47+
### File Structure
48+
49+
```
50+
cli/
51+
├── main.go # Entry point
52+
├── go.mod / go.sum # Go module (github.com/GlueOps/tools-api/cli)
53+
├── Makefile # Docker-based build targets
54+
├── openapi.json # Exported OpenAPI spec from FastAPI
55+
├── oapi-codegen.yaml # oapi-codegen config (generates types + client)
56+
├── api/
57+
│ └── generated.go # Auto-generated typed client — DO NOT EDIT
58+
├── cmd/
59+
│ ├── root.go # Root command, persistent flags, auth/update pre-run
60+
│ ├── client.go # Authenticated API client helper + response handler (pretty-prints JSON)
61+
│ ├── version.go # tools version
62+
│ ├── login.go # tools login (device code flow)
63+
│ ├── logout.go # tools logout
64+
│ ├── storage_buckets.go # tools storage-buckets create
65+
│ ├── aws.go # tools aws setup-credentials, aws nuke-account
66+
│ ├── nuke.go # tools nuke captain-domain-data
67+
│ ├── github.go # tools github reset-org, github workflow-status
68+
│ ├── chisel.go # tools chisel create, chisel delete
69+
│ ├── opsgenie.go # tools opsgenie create
70+
│ └── captain_manifests.go # tools captain-manifests generate
71+
└── internal/
72+
├── auth/
73+
│ ├── device_flow.go # Dex device code flow (issuer: dex.toolshosted.com)
74+
│ └── token.go # Token storage/refresh (~/.config/glueops/tools-cli/tokens.json)
75+
├── config/
76+
│ └── config.go # Config dir management (~/.config/glueops/tools-cli/)
77+
├── spec/
78+
│ ├── spec.go # Embedded OpenAPI spec parser (examples, summaries, descriptions)
79+
│ └── openapi.json # Embedded copy of OpenAPI spec (go:embed)
80+
├── updater/
81+
│ └── updater.go # Self-update from GitHub releases when API version changes
82+
└── version/
83+
└── version.go # Build-time injected version vars (ldflags)
84+
```
85+
86+
### Key Design Decisions
87+
88+
- **OpenAPI as single source of truth** — CLI flag descriptions, command summaries, and long descriptions are all read from the embedded `openapi.json` at compile time via `internal/spec`. When API docstrings or schema examples change, `make generate` + rebuild picks them up automatically without editing Go code.
89+
- **Auto-generated API client**`api/generated.go` is produced by `oapi-codegen` from the OpenAPI spec. Each command file in `cmd/` constructs requests using generated types and calls generated client methods.
90+
- **Auth via PersistentPreRunE**`root.go` checks for a valid token before every command except `login`, `logout`, `version`, `completion`, `help`, and the root command itself (so `tools --help` works without login). Expired tokens are automatically refreshed.
91+
- **Self-update** — On every invocation, the CLI checks `GET /version` on the API. If the version differs (and isn't a placeholder like `UNKNOWN` or `dev`), it downloads the matching binary from GitHub releases and replaces itself.
92+
- **Config directory**`~/.config/glueops/tools-cli/` stores `tokens.json`.
93+
94+
### Adding a New Command
95+
96+
1. Add the endpoint to `app/main.py` and schema to `app/schemas/schemas.py`
97+
2. Run `cd cli && make generate` to update the client and spec
98+
3. Create `cli/cmd/<command>.go`:
99+
- Use `spec.Summary()` and `spec.Description()` for `Short`/`Long`
100+
- Use `spec.FlagDesc()` for flag descriptions
101+
- Use `newClient()` from `client.go` to get an authenticated client
102+
- Use `handleResponse()` to print the response
103+
4. Register the command with `rootCmd` in `init()`
104+
105+
### Key Dependencies
106+
107+
- **`github.com/spf13/cobra`** — CLI framework
108+
- **`github.com/oapi-codegen/runtime`** — Runtime helpers for the generated client
109+
110+
### Auth Details
111+
112+
- Dex issuer: `https://dex.toolshosted.com`
113+
- Client ID: `tools-cli`
114+
- Scopes: `openid email profile offline_access`
115+
- Default API URL: `https://tools.toolshosted.rocks` (overridable via `--api-url`)
116+
117+
## CI/CD
118+
119+
`.github/workflows/cli_release.yaml` builds CLI binaries on every push:
120+
- Cross-compiles for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 via Docker
121+
- Uploads binaries as workflow artifacts
122+
- Creates a GitHub Release tagged with `github.ref_name` (rolling `main` release for branch pushes, versioned releases for tag pushes)
123+
- Version is injected via ldflags from the git ref

cli/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# CLAUDE.md
2+
3+
See [.ai/AGENTS.md](.ai/AGENTS.md) for AI coding assistant guidance for this CLI.

cli/Makefile

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
GO_IMAGE := golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191
2+
TOOLS_API_IMAGE := tools-api-spec
3+
MODULE := github.com/GlueOps/tools-api/cli
4+
VERSION ?= dev
5+
COMMIT_SHA ?= $(shell git rev-parse HEAD 2>/dev/null || echo unknown)
6+
SHORT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
7+
BUILD_TIMESTAMP ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
8+
GIT_REF ?= $(shell git describe --tags --always 2>/dev/null || echo unknown)
9+
10+
LDFLAGS := -s -w \
11+
-X $(MODULE)/internal/version.Version=$(VERSION) \
12+
-X $(MODULE)/internal/version.CommitSHA=$(COMMIT_SHA) \
13+
-X $(MODULE)/internal/version.ShortSHA=$(SHORT_SHA) \
14+
-X $(MODULE)/internal/version.BuildTimestamp=$(BUILD_TIMESTAMP) \
15+
-X $(MODULE)/internal/version.GitRef=$(GIT_REF)
16+
17+
.PHONY: generate build build-all clean
18+
19+
# Export OpenAPI spec from FastAPI and regenerate Go client
20+
generate:
21+
docker build -t $(TOOLS_API_IMAGE) ..
22+
docker run --rm $(TOOLS_API_IMAGE) python -c \
23+
"import sys; sys.path.insert(0, 'app'); from main import app; import json; print(json.dumps(app.openapi(), indent=2))" \
24+
> openapi.json
25+
docker run --rm -v "$$(pwd):/app" -w /app $(GO_IMAGE) sh -c \
26+
"go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.6.0 && oapi-codegen -config oapi-codegen.yaml openapi.json"
27+
cp openapi.json internal/spec/openapi.json
28+
29+
# Build for current platform
30+
build:
31+
docker run --rm -v "$$(pwd):/app" -w /app \
32+
-e CGO_ENABLED=0 \
33+
$(GO_IMAGE) go build -ldflags '$(LDFLAGS)' -o tools .
34+
35+
# Build for all release platforms
36+
build-all:
37+
@for platform in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do \
38+
os=$${platform%/*}; arch=$${platform#*/}; \
39+
echo "Building tools-$$os-$$arch..."; \
40+
docker run --rm -v "$$(pwd):/app" -w /app \
41+
-e CGO_ENABLED=0 -e GOOS=$$os -e GOARCH=$$arch \
42+
$(GO_IMAGE) go build -ldflags '$(LDFLAGS)' -o "tools-$$os-$$arch" .; \
43+
done
44+
45+
clean:
46+
rm -f tools tools-linux-amd64 tools-linux-arm64 tools-darwin-amd64 tools-darwin-arm64

0 commit comments

Comments
 (0)