Yet Another Java Atlantis Client: Lightweight API for Atlantis IntegrationAtlantis is a popular tool that automates Terraform pull request workflows: it detects Terraform changes in PRs, plans and applies them (when approved), and posts results back to the PR. For teams using Java-based tooling or building integration layers around Terraform workflows, a compact, focused Java client can simplify interaction with Atlantis’ HTTP API, embed Atlantis-driven automation in existing services, and reduce boilerplate.
This article describes the design goals, core features, typical usage patterns, integration examples, and extension points for a hypothetical library called “Yet Another Java Atlantis Client” (YAJAC). The client emphasizes a lightweight footprint, idiomatic Java APIs, testability, and simple extension for custom authentication or request semantics.
Why a lightweight Java client?
- Reduce boilerplate: HTTP interactions with Atlantis usually require repetitive request/response handling, JSON (de)serialization, error mapping, and retry logic. A client centralizes these concerns.
- Fit into Java ecosystems: Many companies run CI/CD orchestrations, webhooks processors, or internal tooling written in Java/Kotlin. A native Java client avoids glue code and simplifies dependency management compared to shelling out to CLI tools or calling curl.
- Testability: A small client with clear interfaces makes unit testing and mocking straightforward, enabling reliable CI tests for automation workflows.
- Extensibility: Teams often need custom auth, custom fields, or special retry/backoff strategies for their environments. A lightweight design makes such extensions easy.
Core design goals
-
Minimal external dependencies
Keep the dependency graph small — use a single HTTP client (e.g., Java 11 HttpClient or OkHttp) and a single JSON binder (e.g., Jackson or Gson). Avoid large frameworks so the library is easy to embed in microservices. -
Clear, idiomatic API
Provide simple POJOs for request/response models, builder-style request constructors, and synchronous/asynchronous execution options (CompletableFuture). Favor immutable value objects where appropriate. -
Friendly error handling
Map HTTP errors to typed exceptions (e.g., AtlantisClientException with subtypes for 4xx/5xx/timeouts). Include the request ID, HTTP status, and response body where useful. -
Testability and mocking
Expose an interface (AtlantisClient) with a default implementation (AtlantisClientImpl). Allow injection of HttpClient and Clock for deterministic tests. Provide in-memory or mocked responses for unit tests. -
Extensible authentication and middleware
Support pluggable authentication (Basic, Token, OAuth) and interceptors for logging, tracing, metrics, or custom headers.
Typical features and API surface
Below is a conceptual API surface for YAJAC that balances simplicity and coverage of Atlantis’ common endpoints:
-
AtlantisClient (interface)
- planWorkspace(prNumber, repoFullName, workspace, options): PlanResult
- applyWorkspace(prNumber, repoFullName, workspace, options): ApplyResult
- getPlanStatus(planId): PlanStatus
- listProjects(prNumber, repoFullName): List
- sendComment(repoFullName, prNumber, comment): CommentResult
- registerWebhook(uri, secret): WebhookRegistrationResult
- healthCheck(): HealthStatus
-
Builders and models
- PlanOptions (builder: targets, varFiles, envVars)
- ApplyOptions (autoApprove, lockRetry)
- PlanResult / ApplyResult (id, status, summary, logs)
- Project (name, path, workspace)
- AtlantisException hierarchy
-
Async variants
- planWorkspaceAsync(…): CompletableFuture
- applyWorkspaceAsync(…): CompletableFuture
- planWorkspaceAsync(…): CompletableFuture
Example usage
Synchronous example (conceptual):
AtlantisClient client = AtlantisClient.builder() .baseUrl("https://atlantis.example.com") .token("s3cr3t-token") .build(); PlanOptions options = PlanOptions.builder() .workspace("default") .targets(List.of("module.app")) .build(); PlanResult result = client.planWorkspace(42, "org/repo", "default", options); if (result.isSuccess()) { System.out.println("Plan succeeded: " + result.getSummary()); } else { System.err.println("Plan failed: " + result.getLogs()); }
Asynchronous example:
client.planWorkspaceAsync(42, "org/repo", "default", options) .thenCompose(plan -> { if (plan.isSuccess()) { return client.applyWorkspaceAsync(42, "org/repo", "default", ApplyOptions.autoApprove()); } else { return CompletableFuture.failedFuture(new IllegalStateException("Plan failed")); } }) .thenAccept(apply -> System.out.println("Apply finished: " + apply.getStatus())) .exceptionally(ex -> { ex.printStackTrace(); return null; });
Integration scenarios
- CI/CD orchestrator: A Java-based pipeline orchestrator can call YAJAC to trigger Atlantis plans for PRs before merging, gate merges on successful plan/apply, or implement custom retry logic for transient provider errors.
- ChatOps: Integrate Atlantis actions into chatbots (Slack/MS Teams). The bot receives a slash command and calls YAJAC to trigger plan/apply, then posts results back to chat.
- Internal tooling: Expose a web dashboard that shows Atlantis runs, project drift, and recent plans; the dashboard backend uses YAJAC to query Atlantis and aggregate results.
- Webhook processor: A microservice that listens to GitHub/GitLab PR events and conditionally triggers Atlantis actions (e.g., replan on terraform file changes). YAJAC simplifies the Atlantis-side interactions.
Error handling and retries
Design the client to classify errors and expose retry hooks:
- Network/timeout errors: transient — safe to retry with exponential backoff.
- 429 Too Many Requests: respect Retry-After header; expose callback to integrate rate limiting.
- 4xx client errors (400, 401, 403): typically fatal for the request — return typed exceptions.
- 5xx server errors: transient; retry with jittered backoff.
Provide a RetryPolicy interface so users can plug backoff strategies (fixed, exponential, or bucketed).
Authentication and security
- Token-based: Accept a Bearer token in the client builder; store securely and only inject into Authorization headers.
- Basic auth: Support username/password for on-prem Atlantis installations that use basic auth.
- Custom header provider: Allow users to supply a function that adds headers per-request (useful for mTLS or ephemeral tokens).
- TLS: Allow custom SSLContext/TrustManager configuration for private Atlantis instances.
Ensure sensitive data (tokens, passwords) are not included in exception messages or logs by default. Provide a configurable logging policy.
Extensibility and customization
- Pluggable HTTP transport: Allow using Java 11 HttpClient, OkHttp, or Apache HttpClient via a transport interface.
- Interceptors: Request/response interceptors for tracing (OpenTelemetry), logging, and metrics injection.
- Event hooks: Callbacks for lifecycle events (onRequest, onResponse, onError) to integrate with app instrumentation.
- Custom serializers: Allow registering custom Jackson modules for org-specific types or date formats.
Testing and local development
- Mock server: Provide a small in-memory mock server or a WireMock configuration that mimics Atlantis behavior for tests.
- Fixtures: Include JSON fixtures for typical Atlantis responses (plan start, plan finished, apply started, apply finished).
- Integration test tips:
- Run an ephemeral Atlantis in a container for end-to-end tests.
- Use recorded HTTP interactions (VCR-like) for CI if container-based testing is not feasible.
Performance and footprint
- Keep the client stateless where possible; do not cache large responses in memory.
- Support streaming of logs from Atlantis to avoid loading long logs fully into memory.
- Make JSON parsing lazy for large responses (streaming parser) when listing many projects.
Comparison with alternatives
Aspect | YAJAC (lightweight) | Full-featured SDK | Direct HTTP calls |
---|---|---|---|
Dependencies | Low | Higher | None (but you write them) |
Ease of use | High | Medium | Low |
Extensibility | High | High | Medium |
Testability | High | Medium | Low |
Runtime footprint | Small | Larger | Depends on client code |
Example extension: custom retry interceptor
public class ExponentialRetryInterceptor implements RequestInterceptor { private final RetryPolicy policy; public ExponentialRetryInterceptor(RetryPolicy policy) { this.policy = policy; } @Override public HttpResponse intercept(Request request, Chain chain) throws IOException { int attempts = 0; while (true) { try { return chain.proceed(request); } catch (IOException ex) { if (!policy.shouldRetry(++attempts, ex)) throw ex; long wait = policy.nextBackoffMillis(attempts); Thread.sleep(wait); } } } }
Roadmap and advanced features
- First milestone: stable core client with plan/apply/status endpoints, token auth, and basic retry policies.
- Second milestone: async-first APIs, streaming logs, webhook management, and metrics integration.
- Third milestone: official examples for Spring Boot, Micronaut, and Kotlin coroutines; publish to Maven Central with semantic versioning and good documentation.
Conclusion
Yet Another Java Atlantis Client aims to be a compact, idiomatic Java wrapper around Atlantis’ API that reduces repetitive code, increases testability, and fits naturally into Java-based tooling. By focusing on minimal dependencies, clear error handling, pluggable transports, and extensibility points for auth and retries, YAJAC can simplify building reliable infrastructure automation and integrations around Atlantis.
If you want, I can generate a starter Maven/Gradle project skeleton, provide detailed class definitions for the API interfaces and models, or produce sample unit tests and a mocked WireMock configuration.
Leave a Reply