Skip to content

Restructure as Cargo workspace with typed Cloud API library#114

Merged
sdairs merged 58 commits into
mainfrom
monorepo-structure
May 5, 2026
Merged

Restructure as Cargo workspace with typed Cloud API library#114
sdairs merged 58 commits into
mainfrom
monorepo-structure

Conversation

@sdairs
Copy link
Copy Markdown
Collaborator

@sdairs sdairs commented Apr 15, 2026

Summary

  • Cargo workspace with two crates: clickhousectl (CLI binary) and clickhouse-cloud-api (typed Rust client library for the ClickHouse Cloud API)
  • Typed API client library generated from the OpenAPI spec — covers all 87 operations, 264 schemas, with full field optionality validation
  • Daily OpenAPI drift detection via CI workflow that auto-files GitHub issues when the live spec diverges from the library, including snapshot staleness checks
  • Scripts for resolving field requirements and updating model optionality from the spec
  • Bug fixes from code review: version fallback picking wrong patch release, --json output missing for delete operations, panic on auth-mode mismatch, BackupBucket deserialization misrouting, and more

Changes

New: crates/clickhouse-cloud-api/

  • client.rs — async client with methods for every API endpoint
  • models.rs — request/response types matching the OpenAPI spec
  • error.rs — error types (Http, Json, Api, AuthMismatch)
  • Full test suite: unit tests, integration tests, spec coverage tests, model deserialization tests

Migrated: crates/clickhousectl/

  • CLI now depends on the library for all cloud API calls
  • Removed ~3k lines of hand-rolled HTTP client and types from the CLI
  • Added CloudClient wrapper for CLI-specific concerns (auth, org resolution, error hints)
  • All cloud commands support --json output consistently (including delete operations)

CI & tooling

  • .github/workflows/openapi-drift.yml — daily spec drift checker
  • .github/workflows/test-cloud-api.yml — library test workflow
  • scripts/check-openapi-drift.py — drift detection with snapshot staleness reporting
  • scripts/resolve-field-requirements.py — OpenAPI field requirement resolution
  • scripts/update-models-optionality.py — automated model optionality updates

Bug fixes

  • Version fallback now picks the semantically highest patch release, not the last one returned by the API
  • --json flag works for query-endpoint delete, member remove, invitation delete, and key delete
  • set_bearer_token returns an error instead of panicking on auth-mode mismatch
  • BackupBucket deserialization correctly routes GCP/Azure variants
  • Prometheus endpoint error paths propagate body-read errors
  • local init --json outputs JSON instead of human-readable text
  • Path traversal vulnerability in server name handling fixed

Test plan

  • cargo test — all 410 tests pass (204 CLI + 206 library)
  • cargo clippy — clean
  • python3 scripts/check-openapi-drift.py --dry-run — no drift, library fully covers live spec
  • Manual: cargo run -p clickhousectl -- local install stable
  • Manual: cargo run -p clickhousectl -- cloud service list --json

🤖 Generated with Claude Code

sdairs and others added 30 commits April 14, 2026 15:27
Move the CLI into crates/clickhousectl/ and add the ClickHouse Cloud
API client library as crates/clickhouse-cloud-api/. The library is
standalone and not yet integrated into the CLI.

- Root Cargo.toml is now a workspace definition
- CI workflow paths updated for new crate locations
- CLAUDE.md updated to reflect workspace structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align the library with the CLI's reqwest version and TLS backend.
Uses default-features = false with rustls to support cross-compilation
to aarch64-linux without OpenSSL. Added query feature needed for
RequestBuilder::query() in reqwest 0.13.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runs build, clippy, and tests on PRs touching the library crate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a Python script and GitHub Actions workflow that compares the live
ClickHouse Cloud OpenAPI spec against the Rust library code (client.rs
and models.rs). Creates a GitHub issue with full spec JSON when the
live API has operations or schemas the library doesn't cover yet.

Runs daily at 08:00 UTC and can be triggered manually via workflow_dispatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If the live OpenAPI spec URL is unreachable (transient outage, DNS
failure, etc.), exit cleanly with a warning instead of failing the
workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Bearer token auth support (Auth enum, with_bearer_token constructor,
  set_bearer_token for token refresh)
- Add Unknown(String) catch-all variant with #[serde(untagged)] to all 217
  enums for forward-compatible deserialization of new API values
- Add Display impl to all enums
- Change usage_cost_get to accept multiple filters (&[&str]) instead of
  single Option<&str>
- Add clickhouse-cloud-api as a dependency of the clickhousectl CLI crate

This is the foundation for Phase 0 of the cloud API library integration.
No CLI behavior changes — the library dependency is added but not yet
consumed by any CLI code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewire org_list, org_get, org_update, org_prometheus, and org_usage to
delegate through the clickhouse-cloud-api library instead of the raw
reqwest client. This completes Phase 0 Step 5 of the integration plan.

- CloudClient org methods now call library client and return library types
- Remove 10 org-specific types from CLI types.rs (Organization,
  UpdateOrgRequest, UsageCost, etc.) — replaced by library models
- Update command handlers and parse helpers to use library types directly
- Remove org URL builders and their tests from client.rs
- Remove dead_code annotations on adapter helpers (api, unwrap_response,
  convert_error) now that they are actively used
- Fix error parsing in library prometheus endpoints to extract message
  from JSON error responses instead of returning raw body

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace CLI-local Activity types and URL builder with the
clickhouse-cloud-api library equivalents. Removes ~130 lines of
duplicate type definitions (ActivityType, ActivityActorType,
ActivityKeyUpdateType, Activity struct) and the hand-rolled
activities_url helper, delegating to the library's activity_get_list
and activity_get methods instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 5 hand-written CloudClient API key methods with library-delegating
versions (list, create, get, update, delete). Update command handlers and
request builders to use library types (ApiKeyPostRequest, ApiKeyPatchRequest,
ApiKeyHashData, IpAccessListEntry). Remove 6 CLI types from cloud/types.rs
that are now fully replaced. Add migration design docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate list_services, list_services_filtered, get_service, and
get_service_prometheus from direct HTTP calls to the clickhouse-cloud-api
library client. Update all consuming handlers (resolve_service,
service_list, service_get, service_client, service_delete) to work with
the library's Option-wrapped Service type. Also update instance_get_list
in the library to accept multiple filters (&[&str]).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrates change_service_state and delete_service from direct HTTP calls
to the clickhouse-cloud-api library, following the same delegation pattern
established in Phase 3a.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate create, update, scale, and reset-password operations from direct
HTTP calls to the clickhouse-cloud-api library client, completing the
service write command migration.

- Replace parse_enum (FromStr) with parse_serde_enum that validates
  against known values then constructs library enums via serde
- Migrate 7 helper functions to return library types (IpAccessListEntry,
  ResourceTagsV1, LibIpAccessListPatch, etc.)
- Migrate 3 builder functions to return library request types
  (ServicePostRequest, ServicePatchRequest, ServicePasswordPatchRequest)
- Update 4 client.rs methods to delegate to library client with
  convert_error/unwrap_response pattern
- Update 4 command handlers for all-optional library response fields
- Parse backup_id as uuid::Uuid for ServicePostRequest compatibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate query endpoints, private endpoints, and backup config from
direct HTTP calls to the clickhouse-cloud-api library client, completing
the service command group migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All service commands now use library types from clickhouse-cloud-api.
Remove ~580 lines of CLI-local service types/enums/structs from types.rs,
eliminate Lib* import aliases in commands.rs, replace known_values() calls
with static constants, and use ServiceStatePatchRequestCommand directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All CLI commands now delegate to the clickhouse-cloud-api library client.
Remove the old direct-HTTP pipeline: 10 private methods, reqwest Client
and base_url struct fields, basic_auth helper, AuthMode string payloads,
string_enum/flexible_string_enum macros, ApiResponse/ApiError types,
and the base64 dependency. CloudClient now has just 2 fields (lib_client,
auth_mode) and zero compiler warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the CLI smoke test (cloud_cli) that provisioned real cloud
resources to test CLI wiring. The library integration test now covers
the full API lifecycle, and CLI-specific concerns (arg parsing, JSON
output, request building) are already covered by unit tests in
commands.rs.

- Delete crates/clickhousectl/tests/cloud_cli/ (test + support)
- Add library integration test and support module
- Update cloud-integration.yml to run library test, scope triggers
  to library crate only
- Add test-cli.yml for CLI build/clippy/unit tests on PRs
- Update README to reference the library integration test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update CLAUDE.md to reflect CLI now depends on the library
- Add chrono dev-dependency for integration test support
- Expand client and model unit tests for full API coverage
- Update cloud-api-integration.md with phases 6-8 plans
- Add phase planning docs (phases 3, 5, 6, 7, 8)
- Add AGENTS.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The library unit tests and CLI tests are already run by their own
dedicated workflows (test-cloud-api.yml, test-cli.yml) which trigger
on the same paths. The integration workflow only needs to run the
live integration test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The OpenAPI spec uses two conventions for field optionality: newer schemas
have a standard `required` array, while legacy GA schemas rely on an
"Optional" prefix in field descriptions. Previously all fields were
`Option<T>`, losing this distinction.

- Add scripts to resolve field requirements from the spec and update
  models.rs accordingly (600 fields changed from Option<T> to T)
- Add field_optionality_matches_spec test to catch mismatches
- Extend drift detection script with field-level optionality checking
- Fix pre-existing bug: ClickPipePatchMongoDBSource.readPreference was
  non-Option despite being nullable in the spec
- Add library crate README with development guide
- Document the conventions and future-proofing steps in CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The clickhouse-cloud-api library now correctly marks required fields as
bare T instead of Option<T>. This updates the CLI crate to remove all
the Option-unwrapping patterns that are no longer needed, fixing 239
compilation errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server names were used directly in path joins without validation, allowing
names containing `/` or `..` to escape .clickhouse/servers/ and cause
start, stop, or remove to operate on arbitrary paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move progress messages in init() and create_project_scaffold() from
println! to eprintln! so stdout contains only valid JSON when --json
is used. Matches the existing pattern used by install and other commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A custom config file redirects where ClickHouse stores data, breaking
the managed server lifecycle (list, stop, remove, dotenv all depend on
the data directory living under .clickhouse/servers/<name>/). Individual
--setting=value flags remain supported. Users needing a fully custom
config are directed to run `clickhouse server` directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
resolve_exact was silently defaulting to Channel::Stable on any failure
from find_exact_channel (missing tag, GitHub outage, rate limiting),
which could produce a download URL for the wrong artifact. Propagate
the error instead so exact version installs fail clearly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… variant

The serde untagged enum tried AWS first, and because all fields had
#[serde(default)] with an Unknown(String) catch-all on bucketProvider,
every payload succeeded as AwsBackupBucket — silently dropping
provider-specific data. Replace derived Deserialize with custom impls
that dispatch on the bucketProvider discriminator field for all four
BackupBucket oneOf enums.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This endpoint returned raw response text on errors instead of extracting
the error message from the JSON ApiResponse envelope like every other
client method. Align the error handling and update the test to verify
the message is extracted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
field_optionality_matches_spec silently skipped OpenAPI properties that
had no corresponding Rust struct field, so new or renamed spec fields
could disappear from models.rs without breaking CI. Add a dedicated
struct_fields_cover_every_spec_property test and a matching
check_missing_fields check in the daily drift script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The spec marks dataWarehouseId as required on ServicePostRequest, but
the API returns 400 when it receives an empty string. Make the field
Option<String> so it is omitted from the JSON when not provided.

Add an OPTIONALITY_EXEMPTIONS mechanism to spec_coverage_test so we can
override individual fields without disabling the drift guard. The test
logs each exemption and fails if any become stale (spec fixed upstream).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sdairs sdairs had a problem deploying to cloud-integration April 15, 2026 18:57 — with GitHub Actions Failure
Same pattern as dataWarehouseId: the spec marks byocId as required on
ServicePostRequest but the API returns 400 for empty strings. Make it
Option<String> so it is omitted when not provided.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sdairs sdairs had a problem deploying to cloud-integration April 15, 2026 19:01 — with GitHub Actions Failure
The OpenAPI spec marks most fields as required via the description
heuristic, but the API rejects zero-value defaults for nearly all of
them. Only name, provider, region, and ipAccessList are truly required.

All other fields are now Option<T> with skip_serializing_if so they are
omitted when not set, and covered by OPTIONALITY_EXEMPTIONS in the spec
coverage test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sdairs sdairs temporarily deployed to cloud-integration April 15, 2026 19:25 — with GitHub Actions Inactive
sdairs and others added 13 commits April 16, 2026 15:49
Check for CLICKHOUSE_CLOUD_API_KEY and CLICKHOUSE_CLOUD_API_SECRET
environment variables and display their status (Active, Incomplete,
or Not configured) in the auth status table.

Closes #109

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes #48. Credentials can come from CLI flags, `.clickhouse/credentials.json`,
env vars, or OAuth tokens, and it was hard to tell which one actually won
precedence when debugging. `--debug` now prints the resolved source and API
URL to stderr, so it works equally well with and without `--json`.

`cloud auth status` also gains an `Active` column that marks the winning
source, reusing the same resolution logic.
Plumbs all 13 ClickHouse Cloud managed Postgres operations into the CLI
under `clickhousectl cloud postgres ...` — CRUD, lifecycle (restart/
promote/switchover), CA certs, runtime config (get/replace/patch with
--set key=value overrides), password reset, read replica creation, and
PITR restore. Lives in its own src/cloud/postgres.rs module with 33 new
unit + parse tests and the full write-classification coverage.

Closes #116

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the ClickHouse service lifecycle test (create, wait-running, list,
certs, config get, PATCH tags, password reset, restart, delete) against the
Postgres endpoints, wired into the scheduled Cloud Integration workflow.

Password step treats a successful 200 as the pass condition: per the OpenAPI
spec, PostgresServicePasswordResource.password is only populated when the
request omits `password`, so the supplied-password path returns empty by
design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tables now render using only ASCII `|` and `-` instead of the
non-standard rounded box-drawing characters, so output is readable in
minimal terminals and log aggregators and pasteable into issues/PRs.

Closes #126
Closes #133. Uses the is-ai-agent crate to detect when the CLI is invoked under a known agent (Claude Code, Cursor, Gemini CLI, Codex, Goose, Devin, etc.) and appends an agent=<id> query param to outbound requests to ClickHouse-owned hosts (builds.clickhouse.com, packages.clickhouse.com, api.clickhouse.cloud). GitHub and other third-party hosts are not annotated. The cloud library gains a generic Client::with_extra_query_params builder so the CLI can attach the tag to every request.
Three of the four credential branches in CloudClient::new differed only in which (key, secret) pair they pulled and which AuthSource label to attach. Introduce ResolvedAuth + resolve_auth() to walk the precedence ladder once, then build the lib client and tag it with the agent param at a single site. resolve_active_auth_source becomes a thin wrapper that preserves its lenient half-set CLI-flag behavior for cloud auth status.
The helper is only called from cloud auth status, which never has --api-key/--api-secret to pass (the subcommand doesn't accept them). The half-set lenient branch and its test were protecting a contract no production caller exercises. Inlining the only sensible call removes the dead parameters and the dead branch, leaving a one-line wrapper that documents its actual purpose: peek at credential precedence without erroring on the empty case (which auth status needs to render no-creds-configured correctly).
The local AgentId -> kebab-case match arm was duplicating a mapping the crate already maintains for its AGENT= env var parser. is-ai-agent 0.2.1 exposes AgentId::as_str returning the canonical kebab-case id (claude-code, gemini-cli, etc.) — the inverse of the parser. Switch to it. Replaces our 12-arm match plus exhaustive variant test with a single delegation and a contract test against the upstream lookup. New agents added upstream automatically flow through without code changes here.
…arams

Rather than thread an agent search param through every clickhouse.com request (with a URL-domain gate, two helpers, and a generic extra-query-params feature on the cloud library), fold the signal into the User-Agent header that every outbound request already carries: clickhousectl/0.1.18 (agent=claude-code). RFC 7231 allows parenthesised comments in User-Agent, and this matches conventional shapes (Mozilla/5.0 etc). Detection still uses is-ai-agent. The change deletes the agent_signal module, the cloud library extra_query_params API + tests, the URL-host parser, and the per-call-site wrappers in version_manager — net -146 lines vs the previous implementation. Every reqwest::Client::builder() in the codebase already calls user_agent::user_agent(), so the new attribution flows through with zero per-call-site wiring.
It's now a one-line implementation detail of an analytics signal — not user-facing functionality, not configurable, not surprising for a future reader to understand from the user_agent.rs source. Doesn't earn a documentation entry.
Pull in upstream security fixes flagged by Dependabot. Both are transitive dependencies; lockfile-only update, no API or behaviour changes. Build and full workspace tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sdairs added 6 commits May 5, 2026 18:02
Tag clickhouse.com requests with detected AI agent
Add --debug flag to report winning cloud credential source
Show env var auth status in cloud auth status
@sdairs sdairs had a problem deploying to cloud-integration May 5, 2026 17:07 — with GitHub Actions Failure
@sdairs sdairs temporarily deployed to cloud-integration May 5, 2026 17:38 — with GitHub Actions Inactive
@sdairs sdairs merged commit 684ef97 into main May 5, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants