Skip to content
Consuming a service: CLI, Terraform, and clients from one contract

Consuming a service: CLI, Terraform, and clients from one contract

A devedge service declares one contract and every way of consuming it — a Go client, a TypeScript client, a command-line interface, a Terraform provider — is a generated projection of that contract. You do not hand-write four integrations that drift apart; you annotate the API once and regenerate the surfaces. This page explains the model, the four surfaces, and how a single field annotation shows up correctly in each.

One contract, four surfaces

A service defines its API in proto. The build produces three coordinated artifacts from that one source:

  • a gRPC server,
  • a REST gateway, and
  • an enriched OpenAPI v3 document.

The OpenAPI document is the single interchange every consumer surface reads. It is enriched because it carries the API’s semantics, not just its wire shape: alongside native required / readOnly / writeOnly / enum, it emits x-aip-resource (the resource type, pattern, and key), x-aip-method (which standard AIP method each operation is), x-aip-pagination, x-aip-references, and x-aip-field-behavior (REQUIRED / OUTPUT_ONLY / IMMUTABLE / INPUT_ONLY). A generator reads this one file and knows enough to build a correct surface — which fields are inputs, which are outputs, which are set-once, and which are secrets.

Two kinds of surface come out of that contract:

  • Clients are emitted and (optionally) published — a Go module or an npm package generated from the spec, versioned with the API.
  • A CLI or a Terraform provider is scaffolded once as its own repo, then extended in place: de cli add / de terraform add generate the commands or the resource from a contract into the existing repo. A CLI shell then re-derives its command roster from every domain you have added, so domains accumulate into one binary.

Both kinds read the contract directly; nothing is transcribed by hand.

The four surfaces

SurfaceRuntime repoCommandConsumed as
Go clientapxapx client generate --generator go — or de api publish --client --client-generator goa typed Go module (go get)
TypeScript clientapx (orchestrates ng-openapi-gen)apx client generate — or de api publish --clientan npm package @<scope>/<svc>-client (GitHub Packages)
CLIdevedge-cli-sdk (clikit)de cli new <name>, then de cli add --input <spec> --domain <name>your own rebrandable CLI binary
Terraform providerdevedge-terraform-sdk (tfkit)de terraform new <name>, then de terraform add --input <spec> --resource <name>a terraform-provider-<name> on the Terraform Registry

de api publish drives apx to emit clients; de cli / de terraform drive the cligen / tfgen generators to scaffold and grow the CLI and provider. In every case de orchestrates and the SDK supplies the mechanism — the same division of labor described in How devedge fits together.

Publishing is deliberately gated, not automatic: the TypeScript client publishes to GitHub Packages only with --publish-client (and a write:packages token), and a Terraform provider reaches the registry through a tag-triggered, GPG-signed GoReleaser workflow. Generating a client or scaffolding a CLI, by contrast, only reads the contract locally.

The CLI is a rebrandable shell (the CLI mirror of a devedge micro-frontend shell): a git/kubectl-style root command whose subcommands are the domains you add. It logs in with a generic OIDC device grant by default. The Terraform provider is shaped for the Terraform Registry from the start — a HashiCorp-style GoReleaser config, a registry manifest, and a release workflow.

How field_behavior projects onto each surface

The whole point of enriching the contract is that one annotation lands correctly everywhere. A field marked REQUIRED becomes a required CLI flag and a required Terraform attribute and a required client field, from the same source:

field_behaviorClient typeCLI flag (clikit)Terraform schema (tfkit)
REQUIREDrequired fieldrequired flag (MarkFlagRequired, on create)Required
OUTPUT_ONLYresponse-only (readOnly)no input flag — appears only in outputComputed + UseStateForUnknown
IMMUTABLEset at createcreate-only flag (dropped from update)RequiresReplace()
INPUT_ONLY (secret)write-only (writeOnly)--<flag>-stdin, read from stdin, never echoedSensitive
enumtyped / validated valueflag help lists the allowed valuesenum validator

Because these projections are derived, the surfaces cannot disagree with the contract or with each other. Change the annotation, regenerate, and every surface moves together.

Running a surface in local development

In local development a devedge-sdk service runs the dev authorizer, which reads the caller’s identity from request metadata — the account-id, groups, and subject headers — not from a bearer token. A generated CLI or Terraform provider sends a bearer token by default, so to complete a round-trip against a freshly scaffolded service you also pass that identity metadata.

For the CLI, combine --dev (a static stub token) with a --header for each identity value:

ib --dev --header account-id=t1 --header groups=admin --server http://localhost:8080 zone list

For Terraform, set the provider’s headers attribute:

provider "zoned" {
  endpoint = "http://localhost:8080"
  headers  = { "account-id" = "t1", "groups" = "admin" }
}

For a micro-frontend consuming a generated TypeScript client, the Angular glue package ships the same seam. A de ufe new scaffold registers devAuthInterceptor alongside the bearer interceptor and reads the identity headers from environment.devAuthHeaders:

// src/environments/environment.ts (dev only)
export const environment = {
  production: false,
  devAuthHeaders: { 'account-id': 't1', groups: 'admin' },
};

The interceptor stamps those headers on same-origin API calls; environment.prod.ts leaves devAuthHeaders empty, so it is a no-op in production.

Each surface carries the metadata the dev authorizer reads; the dev authorizer ignores the bearer token. In production you replace the dev authorizer and its PrincipalFunc with a real policy and a verified-token principal, and drop the --header / headers / devAuthHeaders values.

Open core, private overlay

The runtimes and generators are open source under infobloxopen: devedge-cli-sdk (clikit + cligen) and devedge-terraform-sdk (tfkit + tfgen) are mechanism, not policy. Product-specific pieces — Infoblox’s Okta / PDS authentication, branding, extra commands — bind privately through an overlay, without forking the open SDK.

The seam is the same one devedge uses everywhere. A scaffolded CLI shell owns a small session.go that constructs its auth provider; the open core ships a generic OIDC device grant there, and an internal preset replaces it:

de cli new ib --preset-dir ../devedge-cli-sdk-internal/preset/infoblox-cli

This is the CLI analog of the backend’s opaauthzauthz.Authorizer binding (see How devedge fits together): the public interface is defined in the open SDK, and the private, product-specific implementation binds at scaffold time. The public repositories ship no proprietary preset; a missing or malformed preset fails loudly.

The catalog is the roster

apx maintains a catalog — the discovery index of every published service’s OpenAPI spec. It answers “which services exist, and where is each one’s contract?” That index is what makes a multi-domain CLI tractable: you compose one CLI by adding each service’s contract to it.

The CLI makes this concrete. de cli add generates the domain under gen/<name> and then re-derives the shell’s wiring from every domain present under gen/ — so adding a second service’s contract accumulates it alongside the first, and one shell binary carries every domain you have added:

de cli new shop --module example.com/shop
de cli add --input widgets.openapi.yaml --domain widgets
de cli add --input orders.openapi.yaml  --domain orders
# `shop --help` now lists both `widgets` and `orders`

This is the CLI mirror of composing a suite from many services: the contract is the unit of composition, and the catalog is the roster of contracts to draw from.

The Terraform provider composes the same way: de terraform add re-derives the provider’s resource registration from every resource generated under internal/provider/, so adding each service’s contract accumulates its resources into one terraform-provider-<app> — a single provider covering many domains.

The repositories

RepositoryRole
infobloxopen/devedgethe de CLI that orchestrates every command above
infobloxopen/devedge-sdkthe Go service framework that emits the enriched OpenAPI contract
infobloxopen/apxthe API lifecycle tool: catalog, client generation, publishing
infobloxopen/devedge-cli-sdkthe CLI runtime (clikit) and generator (cligen)
infobloxopen/devedge-terraform-sdkthe Terraform runtime (tfkit) and generator (tfgen)

To see the backend/frontend half of the same idea — one API contract feeding a typed client into a micro-frontend — walk Ship a full-stack feature.