diff --git a/.changes/unreleased/NOTES-20260714-120419.yaml b/.changes/unreleased/NOTES-20260714-120419.yaml new file mode 100644 index 0000000..698db00 --- /dev/null +++ b/.changes/unreleased/NOTES-20260714-120419.yaml @@ -0,0 +1,3 @@ +kind: NOTES +body: For developers of tfctl, added CONTRIBUTING.md, developer setup automation, and `make help` +time: 2026-07-14T12:04:19.469961-06:00 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..da48540 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,42 @@ +version: 2 +# Update docker, github-actions, and gomod dependencies weekly with a semver- +# sensitive cooldown and maximum open PR limits. +updates: + - package-ecosystem: "docker" + open-pull-requests-limit: 1 + directory: "/" + cooldown: + semver-major-days: 14 + semver-minor-days: 7 + semver-patch-days: 3 + schedule: + interval: "weekly" + labels: + - dependencies + - automated + + - package-ecosystem: "github-actions" + open-pull-requests-limit: 3 + directory: "/" + cooldown: + semver-major-days: 14 + semver-minor-days: 7 + semver-patch-days: 3 + schedule: + interval: "weekly" + labels: + - dependencies + - automated + + - package-ecosystem: "gomod" + open-pull-requests-limit: 5 + directory: "/" + cooldown: + semver-major-days: 14 + semver-minor-days: 7 + semver-patch-days: 3 + schedule: + interval: "weekly" + labels: + - dependencies + - automated \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ce2316..23f443e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,14 @@ jobs: with: go-version-file: go.mod + - name: Read golangci-lint version + id: golangci-lint-version + run: echo "version=$(cat .golangci-lint-version | head -n 1)" >> $GITHUB_OUTPUT + - name: Run golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - version: v2.12 + version: v${{ steps.golangci-lint-version.outputs.version }} - name: Run go test - run: go test ./... -v + run: make check diff --git a/.golangci-lint-version b/.golangci-lint-version new file mode 100644 index 0000000..623eed0 --- /dev/null +++ b/.golangci-lint-version @@ -0,0 +1 @@ +2.12.2 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..728faad --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,430 @@ +# tfctl CLI — Contributing + +# Part 1: How to Contribute + +## 1. Development Setup + +**Prerequisites**: Go 1.26, git, bash, make + +```bash +git clone https://github.com/hashicorp/tfctl-cli +cd tfctl-cli +scripts/setup.sh # Install toolchain and dev tools +make bin # Build binary for your os/arch +make check # Verify everything passes before creating PR +``` + +## 2. Essential Requirements + +- [ ] Run `changie new` to prepare a new changelog entry for the next set of release notes. +- [ ] Ensure any command changes are sensitive to these global flags: + - `--json` — Force machine readable output to stdout. Does not apply to stderr. + - `--markdown` — Force markdown output to stdout. Does not apply to stderr. + - `--dry-run` — Don't make any actual writes or other mutations. Describe what would have changed to stderr. + - `--quiet` — Don't render output to stdout. +- [ ] Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations. +- [ ] Run `make gen/screenshot` if the root command output changes. +- [ ] Add the `Autocomplete` field to **positional arguments** and **flags** to assist shell autocomplete. + +## 3. Questions? + +Open an issue for questions about contributing. + +# Part 2: Architecture + +`tfctl` is a Go command-line tool for interacting with **HCP Terraform** and +**Terraform Enterprise (TFE)**. It provides three tiers of interaction: + +1. A **low-level API helper** (`tfctl api`) for raw HTTP calls against the + Terraform API. +2. **High-level commands** (runs, variables, workspaces, auth, profiles) that + wrap common workflows. +3. **Interactive / agent-oriented commands** (`tfctl harness`, prompt-based + auth) designed to support coding agents and humans at a terminal. + +- **Module:** `github.com/hashicorp/tfctl-cli` +- **Language / toolchain:** Go 1.26 +- **CLI framework:** `github.com/hashicorp/cli` (not cobra/urfave), wrapped by a + custom command abstraction. + +--- + +## 1. Design Principles + +These principles (codified in `AGENTS.md`) shape the architecture: + +- **Testability first (TDD).** Every command separates wiring (`NewCmdXxx`) from + behavior (`runXxx`), so behavior is tested by varying an options struct rather + than driving the whole CLI. +- **Uniform output.** All stdout rendering goes through a single `Displayer` / + `Outputter` system that supports `--json`, `--markdown`, and default pretty + output (plus table and agent formats). +- **Consistent styling.** All stderr/human styling goes through `ColorScheme`. +- **Safe by default.** A global `--dry-run` flag must suppress all mutations. +- **Observable.** Commands emit structured debug output through a + context-carried logger, and OpenTelemetry traces command/HTTP activity. + +--- + +## 2. High-Level Structure + +``` +cmd/tfctl/ Binary entry point (main.go only) +internal/ + commands/ Command implementations (one package per group) + root/ Root command wiring + api/ auth/ run/ variable/ create/ get/ + profile/ Profile management (+ profiles/ subcommands) + harness/ Coding-agent skills, context, session-scoped exec + cmdtest/ cmdutil/ Shared test + command helpers + pkg/ Reusable infrastructure + cmd/ Home-grown Command / Invocation framework + hashicorp/cli bridge + client/ HCP Terraform / TFE API client (wraps go-tfe/v2) + format/ Displayer / Outputter rendering (json/markdown/pretty/table/agent) + iostreams/ Terminal I/O abstraction + ColorScheme + logging/ hclog-based logging carried on context + telemetry/ OpenTelemetry tracing + profile/ HCL-backed profile + host cache config + openapi/ Embedded OpenAPI spec loading + execsession/ Session-scoped permission store (agent safety rail) + terraform/ Reads local Terraform Cloud config + flagvalue/ heredoc/ table/ ld/ git/ resource/ Utilities +version/ Name + Version constants +skills/ Embedded coding-agent skills +scripts/ Developer setup scripts (setup.sh) +Makefile Build/test/lint/tooling targets (make help) +``` + +**Layering rule:** `internal/commands/*` (what the CLI *does*) depends on +`internal/pkg/*` (the *infrastructure*). Cross-cutting concerns — I/O, color, +logging, telemetry, profile config — are threaded through a single `Invocation` +value and the shutdown `context.Context`. + +--- + +## 3. Lifecycle & Bootstrap + +The entry point is `cmd/tfctl/main.go` (`realMain`), which wires the process in +a fixed order: + +1. **Signal handling.** A root context is created with + `context.WithCancelCause`; a goroutine cancels it on `SIGINT`/`SIGTERM` so + long-running commands can abort cleanly. +2. **I/O.** `iostreams.System(ctx)` builds the terminal abstraction (stdin/out/ + err, TTY detection, color capability). Console state is restored on exit. +3. **Logging.** An initial log level is derived by scanning args for `--debug`, + a logger is created, and it is stored **on the context** + (`logging.WithLogger`) so every layer can retrieve it. +4. **Profile.** `profile.NewLoader()` loads the active profile, writing sane + defaults on first run (`loadActiveProfile`). Profile `no_color` is honored + here. +5. **Telemetry.** `telemetry.Init` starts OpenTelemetry (device ID, hostname, + version) and is stored on the context. +6. **Invocation.** A `cmd.Invocation` is assembled bundling `IO`, `Profile`, + `Output: format.New(io)`, and `ShutdownCtx`. +7. **Command tree.** `root.NewCmdRoot(inv)` builds the command tree; + `cmd.ToCommandMap` flattens it into the `map[string]cli.CommandFactory` that + `hashicorp/cli` expects. +8. **Run.** `cli.CLI.Run()` dispatches to the matched command; afterward + telemetry is shut down with the resulting exit status. + +``` +main → realMain + ├─ context + signal handling + ├─ iostreams.System + ├─ logging.NewLogger → WithLogger(ctx) + ├─ profile.Loader → active profile + ├─ telemetry.Init → WithTelemetry(ctx) + ├─ cmd.Invocation{IO, Profile, Output, ShutdownCtx} + ├─ root.NewCmdRoot(inv) → cmd.ToCommandMap + └─ cli.CLI.Run() → exit status +``` + +--- + +## 4. The Command Framework + +Rather than use `hashicorp/cli` directly, the project defines its own richer +command model in `internal/pkg/cmd/`. + +### `Command` (`command.go`) + +A declarative struct describing a single command or a nesting group: + +- Identity/help: `Name`, `Aliases`, `ShortHelp`/`LongHelp`, `Examples`. +- Arguments: `Args` (`PositionalArguments`). +- Flags: `Flags.Local` + `Flags.Persistent` (persistent flags flow to + descendants). +- Hooks: `PersistentPreRun`, and the command body `RunF`. +- Behavior flags: `Hidden`, `NoAuthRequired`. +- Tree building: `AddChild` composes groups; a group without a `RunF` acts as a + container. + +Control flow uses **sentinel errors** — `ErrDisplayHelp`, `ErrDisplayUsage`, +`ErrUnderlyingError` — and `ExitCodeError`/`NewExitError` to set specific exit +codes (e.g. auth failure → 3, not-found → 2). + +### `Invocation` (`invocation.go`) + +The shared context object passed into every `NewCmdXxx` constructor. It holds +`IO`, `Output`, `Profile`, `ShutdownCtx`, and parsed `GlobalFlags`. Key +responsibilities: + +- **`ConfigureRootCommand`** installs the global flags (`--profile`, `--json`, + `--markdown`, `--jq`, `--dry-run`, `--quiet`, `--no-color`, `--debug`, + `--version`) and registers the persistent pre-run hook. +- **`applyGlobalFlags`** (run in pre-run) reconciles flags: reloads the profile + if `--profile` was passed, resolves the output format (`--json`/`--markdown`/ + `--jq` with conflict checks), disables color, and sets quiet mode. +- **`NewAPIClient`** constructs the API client from the active profile's + hostname + token, injecting a `User-Agent` header. +- **`ResolveLogLevel` / `IsDryRun`** expose global state to commands. + +### `hashicorp/cli` Bridge (`compat.go`) + +`ToCommandMap` recursively flattens the `Command` tree into path-keyed factories +(e.g. `"run status"`), expanding aliases. `CompatibleCommand` implements the +`cli.Command`, `cli.CommandAutocomplete`, and `cli.CommandHelpTemplate` +interfaces, adapting the custom model to the framework. + +### Pre-Run Pipeline + +On every command, the persistent pre-run hook (in `ConfigureRootCommand`): + +1. Applies global flags. +2. Sets the log level and creates a per-command **named logger** on the context. +3. Starts a telemetry span describing the command. +4. Enforces authentication (`isAuthenticated`) unless the command is top-level + or marked `NoAuthRequired`. + +--- + +## 5. The `runXxx` / `XxxOpts` Command Pattern + +Every command follows the same two-part shape, which is the backbone of the +project's testing strategy: + +``` +NewCmdRunStart(inv) *cmd.Command // wiring: declares flags, args, help + └─ RunF: build StartOpts from parsed flags + invocation + → runStart(ctx, startOpts, runOpts) // behavior +``` + +- **`XxxOpts`** carries exactly what the behavior needs — typically `IO`, + `Output`, an API client, `Profile`, `DryRun`, and command-specific fields — + **not** the whole command context. +- **`runXxx(ctx, opts)`** contains all real logic and is unit-tested directly by + varying the opts. +- Test seams (e.g. injectable `Store`, `PID`, `Run` in `harness exec`) are + passed via opts so behavior can be exercised deterministically. + +Examples: `run/run_start.go` (`StartOpts`/`CreateOpts` → `runStart`), +`run/run_status.go` (`StatusOpts` → `runStatus`), `auth/login.go` (`LoginOpts` → +`loginRun`), `api/api.go` (`Opts`), `harness/harness_exec.go` (`ExecOpts`). + +--- + +## 6. Output Rendering (`internal/pkg/format`) + +All stdout output is unified through the `Displayer` / `Outputter` system, so a +single command definition produces pretty, table, JSON, markdown, or agent +output. + +- **`Format`** enum: `Unset`, `Pretty`, `Table`, `JSON`, `Markdown`, `Agent`. +- **`Displayer` interface:** `DefaultFormat()`, `Payload() any`, + `FieldTemplates() []Field`. A `Field` pairs a name with a `text/template` + value expression (e.g. `{{ .CloudProvider }}/{{ .Region }}`). +- **`Outputter`** (`format.New(io)`, stored on `Invocation.Output`): + `Display(d)` selects the format (the displayer's default unless a global flag + forces one) and dispatches to `outputPretty` / `outputTable` / `outputJSON` / + `outputMarkdown` / `outputAgent`. `--jq` runs a `gojq` filter over JSON. +- **Extension interfaces:** `TemplatedPayload` (alternate payload for templated + output) and `StringPayload` (pre-formatted pretty/markdown, e.g. the run + status summary emits ANSI for pretty and markdown syntax for `--markdown`). +- **Helpers:** `NewDisplayer[T]` and `DisplayFields`/`Show` infer fields from a + struct via reflection for simple cases. + +This is the mechanism that satisfies the "one displayer, three formats" rule in +`AGENTS.md`. + +--- + +## 7. Terminal I/O and Styling + +### `IOStreams` (`internal/pkg/iostreams`) + +Abstracts stdin/stdout/stderr, TTY detection, quiet mode, and color capability. +Provides interactive primitives: `PromptConfirm`, `ReadSecret` (no-echo), +`CanPrompt`. Tests use `iostreams.Test()` to capture buffers. + +### `ColorScheme` (`colorscheme.go`) + +Used for **stderr / human-facing** styling (per `AGENTS.md`). Built on +`muesli/termenv` with automatic degradation to terminal capability: + +- Chainable strings: `.Bold()`, `.Italic()`, `.Underline()`, `.Faint()`, + `.Color()`, `.CodeBlock(ext)`, etc. +- HashiCorp-branded named colors and `RGB(hex)`. +- Semantic helpers: `SuccessIcon()`, `FailureIcon()`, `WarningLabel()`, + `DryRunLabel()`, `ErrorLabel()`. +- A markdown mode emits markdown syntax instead of ANSI from the same API. + +Color is disabled by `--no-color`, profile `no_color`, or a non-TTY stream. + +--- + +## 8. API Client Layer (`internal/pkg/client`) + +Wraps **`github.com/hashicorp/go-tfe/v2`**, which is built on **Microsoft +Kiota**-generated clients against the Terraform OpenAPI spec. + +- **`Client`** wraps `*tfe.Client`, the Kiota request adapter, `BaseURL`, and + default headers. `client.New(ctx, address, token, headers)` configures retry + policy (server errors, rate limiting, max 5 retries, retry-hook logging). +- **Typed calls** use the generated fluent API, e.g. + `client.TFE.API.Workspaces().ByWorkspace_id(id).Get(ctx, nil)`. +- **Raw calls** (`Client.Do`) build a Kiota `RequestInformation`, convert to a + native `*http.Request`, and send it — this powers `tfctl api`. `ResolveURL` + handles relative/absolute paths while preserving encoded slashes. +- **`Resolver`** (`resolver.go`) resolves resources by name (workspace, variable + set, current run, etc.), honoring `createIfNotFound` and `dryRun`. +- **Transports:** HTTP is wrapped by a `loggingTransport` (debug request/ + response logging with timing) and a `telemetryTransport` (OTel spans with + status/size/duration), installed via `SetLogger`/`SetTelemetry`. + +--- + +## 9. Configuration & Profiles (`internal/pkg/profile`) + +Configuration is **HCL-backed** (`hashicorp/hcl/v2`). + +- A `Profile` holds `Name`, `Hostname`, `Token`, `DefaultOrganization`, + `NoColor`, `Telemetry`. +- On-disk: per-profile `.hcl`, an `active_profile.hcl` pointer, a + `device_id` (telemetry UUID), and a per-host cache. +- `Loader` provides `GetActiveProfile`, `LoadProfile`, `ListProfiles`, + `DefaultProfile`, `GetDeviceID`, and autocomplete prediction. +- Hostname helpers default to `app.terraform.io`, normalize via IDNA, and detect + HCP Terraform vs. TFE. +- The global `--profile` flag overrides the active profile at runtime. +- Local Terraform Cloud config is read via `terraform.FindCloudConfig` to + default the organization. + +--- + +## 10. Authentication (`internal/commands/auth`) + +- Tokens live on the profile (`Profile.Token`) or are resolved from the + environment / Terraform credentials via `Profile.GetToken()`. +- `tfctl auth login` (marked `NoAuthRequired`) reads a token from `--token`, + stdin, or interactively (opens the browser to the token settings page and + reads a secret with no echo), verifies it against the account endpoint, then + persists it — respecting `--dry-run`. +- Enforcement happens in the pre-run pipeline: non-top-level commands without + `NoAuthRequired` require a token, otherwise a helpful "run `tfctl auth login`" + error is returned. `ErrUnauthorized` maps to exit code 3, `ErrNotFound` to 2. + +--- + +## 11. Logging (`internal/pkg/logging`) + +- Built on `github.com/hashicorp/go-hclog`, writing to stderr with timestamps; + color only on a color-capable TTY. +- The logger is **carried on the context** (`WithLogger`/`FromContext`), + returning a null logger when absent. +- Verbosity is controlled by the counting `--debug` flag: + `>=2 → Trace`, `1 → Debug`, else `Error`. +- The pre-run hook names the logger per command path so log lines are + attributable; the API transports log at debug. Commands should use + `logging.FromContext(ctx)` for debug output (per `AGENTS.md`). + +--- + +## 12. Telemetry (`internal/pkg/telemetry`) + +OpenTelemetry tracing (`go.opentelemetry.io/otel`, OTLP + stdout exporters). +Initialized at startup and carried on the context. The pre-run hook opens a span +per command capturing command path, profile, and flag state; the HTTP transport +adds per-request spans. Telemetry can be disabled per profile and is shut down +with the final exit status. Errors are intentionally non-fatal. + +--- + +## 13. Interactive & Agent Support + +There is no full-screen TUI; interactivity is prompt-based and agent-oriented: + +- **Prompts:** `PromptConfirm` / `ReadSecret` guard interactive auth and + destructive `api` deletes. +- **`tfctl harness`:** installs coding-agent skills (`harness_install.go`) and + prints agent context (`harness_context.go`). +- **Session-scoped permissions** (`internal/pkg/execsession` + + `harness exec`): `tfctl harness exec --allow-delete=… -- ` grants a + temporary, auto-reverting permission so nested `tfctl` invocations can perform + noninteractive mutations — a safety rail for automated agents. + +--- + +## 14. Testing Strategy + +- **TDD** is mandated. Commands are tested by calling `runXxx` directly with + constructed `XxxOpts` (no full CLI dispatch), asserting on captured I/O. +- **HTTP harness** (`internal/commands/cmdtest`): `RouteMap` maps method+path to + handlers, `NewServer` spins up an `httptest.Server` returning JSON:API + responses, and `NewInvocation` builds a test `Invocation` pointed at it. +- **Table-driven subtests** with `t.Parallel()`; assertions via + `stretchr/testify`. +- **Golden-style tests** in `format/` verify pretty/markdown/json/table output. + +Commands: + +``` +go test ./... -run "" # focused test +go test ./... -race # regression / race check +golangci-lint run # lint +``` + +--- + +## 15. Key Dependencies + +| Area | Library | +|----------------------|----------------------------------------------------------| +| CLI framework | `github.com/hashicorp/cli` | +| Flags / autocomplete | `spf13/pflag`, `posener/complete` | +| Terminal styling | `muesli/termenv`, `muesli/reflow` | +| Terraform API | `hashicorp/go-tfe/v2` (Kiota-generated) | +| Kiota runtime | `microsoft/kiota-abstractions-go` (+ http/serialization) | +| OpenAPI | `getkin/kin-openapi`, `blugelabs/bluge` (schema search) | +| Config | `hashicorp/hcl/v2`, `zclconf/go-cty` | +| JSON query | `itchyny/gojq` (`--jq`) | +| Logging | `hashicorp/go-hclog` | +| Telemetry | `go.opentelemetry.io/otel` (+ OTLP/stdout exporters) | +| Browser | `cli/browser` (login) | +| Testing | `stretchr/testify` | + +--- + +## 16. Request Flow Summary + +A typical mutating command (e.g. `tfctl run start`) flows as: + +``` +CLI args + → hashicorp/cli dispatch (ToCommandMap) + → Command.PersistentPreRun + applyGlobalFlags · set log level · start telemetry span · auth check + → Command.RunF + build StartOpts (IO, Output, APIClient, Profile, DryRun) + → runStart(ctx, opts) + Resolver resolves workspace/run + client.TFE.API... performs API call (skipped/echoed under --dry-run) + Output.Display(displayer) → pretty | json | markdown | table + ColorScheme-styled status to stderr + → exit status → telemetry.Shutdown +``` + +This consistent path — global flags, context-carried logger/telemetry, an +`Invocation`-built client, an `XxxOpts` → `runXxx` split, and unified `Displayer` +output — is repeated across every command, which is what makes the CLI uniform +and heavily testable. diff --git a/Makefile b/Makefile index 778ac11..87a074a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ SHELL=/usr/bin/env bash NAME=tfctl BIN_PATH ?= dist/$(NAME) +ASSETS ?= assets ifeq ($(GOARCH), arm64) GOARCH = arm64 @@ -10,7 +11,7 @@ else GOARCH = amd64 endif -default: $(BIN_PATH) +default: help .PHONY: linux linux: @@ -32,8 +33,8 @@ clean: rm -rf $(CURDIR)/$(dir $(BIN_PATH)) .PHONY: gen/screenshot -gen/screenshot: go/install ## Create a screenshot of the tfctl CLI - @go run github.com/homeport/termshot/cmd/termshot@v0.6.1 -c -f assets/tfctl.png -- tfctl +gen/screenshot: go/install # Create a screenshot of the tfctl CLI + @go run github.com/homeport/termshot/cmd/termshot@v0.6.1 -c -f $(ASSETS)/tfctl.png -- tfctl .PHONY: go/build go/build: bin @@ -44,4 +45,55 @@ go/install: .PHONY: go/lint go/lint: - @golangci-lint run \ No newline at end of file + @golangci-lint run + +.PHONY: go/test +go/test: + @go test -v ./... + +# Format code +.PHONY: go/fmt +go/fmt: + @gofmt -s -w . + +# Check formatting +.PHONY: fmt-check +fmt-check: + @test -z "$$(gofmt -s -l . | tee /dev/stderr)" || (echo "Code is not formatted. Run 'make go/fmt'" && exit 1) + +# Install development tools +.PHONY: tools +tools: + @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v$$(cat .golangci-lint-version | head -n 1) + @command -v changie >/dev/null 2>&1 || { \ + echo "Installing changie..."; \ + if command -v brew >/dev/null 2>&1; then brew install changie; \ + else echo "Could not auto-install changie: https://changie.dev/guide/installation/" && exit 1; \ + fi; \ + } + +.PHONY: check +check: fmt-check go/lint go/test + +# Help (make usage) +.PHONY: help +help: + @echo "Available targets:" + @echo "" + @echo "Tools:" + @echo " tools Install development tools" + @echo " gen/screenshot Generate a screenshot of the CLI in $(ASSETS)/" + @echo "" + @echo "Build:" + @echo " go/install Install tfctl binary to GOPATH/bin" + @echo " bin Build a binary for tfctl ($(BIN_PATH))" + @echo " clean Clean build artifacts" + @echo " docker Build a docker image for tfctl" + @echo "" + @echo "Code:" + @echo " check Run all checks (formatting, linting, tests)" + @echo " go/test Run all tests" + @echo " go/lint Run golangci-lint" + @echo " go/fmt Format go code" + @echo " fmt-check Check go code formatting" + @echo "" \ No newline at end of file diff --git a/README.md b/README.md index 68110b2..2986f97 100644 --- a/README.md +++ b/README.md @@ -185,696 +185,29 @@ If you have not configured a particular option for the active profile, `tfctl` c The `tfctl` command can manage HCP Terraform runs and variables with the corresponding subcommands. It also provides direct access to the HCP Terraform API with the `api` subcommand. -Use the `--help` flag to print out detailed usage instructions. For example, `tfctl --help` to print out help for the tfctl CLI, and `tfctl run --help` for help with the `run` subcommand. - ### Global flags `tfctl` supports the following global flags. +- `--help`: Detailed usage instructions for any command. + - `--debug`: Enable debug output. - - Data type: Boolean flag - - Defaults to `false`. - `--dry-run`: Creates a preview of the proposed changes. - - Data type: Boolean flag - - Defaults to `false`. - `--json`: Sets the output format to JSON. - - Data type: Boolean flag - - Defaults to `false`. -- `--jq=`: A jq filter expression to apply to JSON output. Sets `--json` to `true`. - - Data type: String - - Optional parameter. +- `--jq=`: A jq filter expression to apply to JSON output. Implies `--json` -- `--markdown`: Sets the output format to Markdown. - - Data type: Boolean flag - - Defaults to `false`. +- `--markdown`: Sets the output format to Markdown - `--no-color`: Disables color output. - - Data type: Boolean flag - - Defaults to `false`. - `--profile=`: The profile to use. If omitted, the CLI uses the current profile. - - Data type: String - - Optional parameter. - `--quiet`: Minimizes output to stdout. - - Data type: Boolean flag - - Defaults to `false`. - `--version`: Print the version of `tfctl` CLI. - - Data type: Boolean flag - - Defaults to `false`. - -### `tfctl auth login` reference - -**Usage:** `tfctl auth login [options]` - -#### Description - -Authenticate the `tfctl` CLI with HCP Terraform or Terraform Enterprise. Opens a browser to the token creation page for the configured hostname and prompts you to paste the generated token. - -#### Examples - -Login interactively: - -```bash -$ tfctl auth login -``` - -Login with a token from stdin. Replace `abcd.atlasv1.1234` with a valid HCP Terraform or Terraform Enterprise token. - -```bash -$ echo "abcd.atlasv1.1234" | tfctl auth login --token -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl auth status` reference - -**Usage:** `tfctl auth status [options]` - -#### Description - -Check the status of the currently configured authentication token, including expiration date if available. - -#### Examples - -Check the status of the token for the configured host: - -```bash -$ tfctl auth status -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl harness` reference - -Install coding agent skills or print out coding agent context document. - -#### Subcommands - -- `context`: Print coding agent context for tfctl, suitable for AGENTS.md. -- `install AGENT`: Install coding agent skills for tfctl in your project directory. - - `--global`: Install skills in the global user directory instead of the current project directory. -- `exec [--allow-delete=CLASSES] -- COMMAND [args...]`: Run a child command (such as a coding agent) with a short-lived, session-scoped permission that lets nested `tfctl` invocations perform noninteractive deletes. The permission is tied to the lifetime of this process and auto-reverts to the safe default (deletes require interactive confirmation) as soon as the child exits. - - `--allow-delete CLASSES`: Resource classes that nested `tfctl` may delete noninteractively. Repeat the flag or pass a comma-separated list. The special tokens `reversible` and `all` cover any reversible class, but never cover the irreversible classes `organizations` and `projects` — those must always be named explicitly. - - This is a safety rail, not a security boundary: the child runs as the same OS user, so a true guarantee that an agent cannot delete must come from the API token scope server-side. - -Supported agents are: - -- `bob` -- `claude` -- `codex` -- `copilot` -- `gemini` -- `opencode` -- `pi` - -#### Examples - -Install `tfctl` skills for Bob in the current project directory. - -```bash -$ tfctl harness install bob -``` - -Install `tfctl` skills for Claude in your user directory for use with all projects. - -```bash -$ tfctl harness install --global claude -``` - -Print out agent context for `tfctl`, suitable for AGENTS.md. - -```bash -$ tfctl harness context -``` - -Run a coding agent for one session with permission to delete workspaces and runs noninteractively (the permission auto-reverts when the agent exits): - -```bash -$ tfctl harness exec --allow-delete=workspaces,runs -- opencode -``` - -### `tfctl api` reference - -**Usage:** `tfctl api PATH [options]` - -#### Description - -Perform any HCP Terraform API v2 request with the given path or URL. - -The HCP Terraform API typically requires a resource ID as part of the path for resource-specific requests. To support this, the `tfctl` CLI interpolates parameter values in the `` argument denoted by `{NAME}`. Whenever possible, `tfctl` infers these values from the path, the active profile, or local Terraform configuration. You can provide values for named parameters with the `--pathparam` argument. - -#### Arguments - -- `PATH`: API path relative to configured host, or URL. - - Required argument - -#### Options - -- `--all`: Fetch all records in multiple pages, up to 2000, and combine them into one page. - -- `--attribute`, `-a`: Set attribute in request body. Sets `--method` to `POST`. - - Repeatable - - Format as `=` - -- `--field`, `-f`: Add a query parameter to the request URL. - - Repeatable - - Format as `=` - -- `--header`, `-H`: Add an HTTP header to the request. - - Repeatable - - Format as `=` - -- `--input`, `-i`: Specify the entire JSON request body. - - Repeatable - - Format as JSON:API data envelope, or `-` to read from stdin. - -- `--method`, `-X`: HTTP method to use (GET, POST, etc) - -- `--page-number`, `-n`: Page number to return. Ignored if `--all` is set. - - Default: 1 - -- `--page-size`, `-s`: Limit the number of records to return. Ignored if `--all` is set. - - Default: Varies by resource - -- `--pathparam`, `-p`: Provide a hint for path parameter resolution. - - Repeatable - - Format as `=` - - Useful for resolving unique resource names, such as project, to an ID that can be used in the PATH. - - Example: `tfctl api /projects/{name} -p 'name=Default Project'` - -- `--type`, `-t`: Resource type for --attribute JSON:API request bodies. - - This value is inferred from the path whenever possible. - -### `tfctl api schema search` reference - -**Usage:** `tfctl api schema search QUERY` - -#### Description - -Search all platform operations that can be invoked using `tfctl api` - -#### Arguments - -- `QUERY`: Any phrase - - Required argument - -#### Example - -```bash -$ tfctl api schema search 'run tasks' -``` - -### `tfctl api schema get` reference - -**Usage:** `tfctl api schema get OPERATION` - -#### Description - -Get the OpenAPI specification for any single operation, including any schema components in use. - -#### Arguments - -- `OPERATION`: The operation ID of any search result - - Required argument - -#### Example - -```bash -$ tfctl api schema get getTask -``` - -### `tfctl get` reference - -**Usage:** `tfctl get ID` - -#### Description - -Get or lists a resource. Acts as a simplified shortcut to using `tfctl api`. - -#### Arguments - -- `ID`: A flexible string argument that can be: - - A resource ID, like `proj-9arLMbi8daoo3hhG` or `ws-sRMALNbEeg1aT93F` - - A resource type prefix, like `proj`, `ws`, or `varset` - - A resource type name, like `projects`, `organizations`, or `agent-pools` - -### `tfctl create` reference - -**Usage:** `tfctl create TYPE` - -#### Description - -Creates a resource. Acts as a simplified shortcut to using `tfctl api`. - -#### Arguments - -- `TYPE`: A resource type name to create, like `projects`, `organizations`, or `agent-pools` - -#### Flags - -`--attribute`, `-a`: Attribute for the JSON:API request body - - Repeatable - - Format as `=` - -- `--input`, `-i`: Specify the entire JSON request body. - - Repeatable - - Format as JSON:API data envelope, or `-` to read from stdin. - -- `--organization`, `-o`: Organization name - - Default: Profile `default_organization` or terraform cloud config context - -### `tfctl profile display` reference - -**Usage:** `tfctl profile display [options]` - -#### Description - -Print out configuration for a profile. - -#### Examples - -Print out information about the active profile: - -```bash -$ tfctl profile display -``` - -Print out information about a profile named `my-profile`: - -```bash -$ tfctl profile display --profile="my-profile" -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile get` reference - -**Usage:** `tfctl profile get PROPERTY [options]` - -#### Description - -Get the value of the given configuration property for a profile. - -#### Arguments - -- `PROPERTY`: The configuration property name to retrieve. - - Required argument - - Data type: String - -#### Examples - -Get the organization for the active profile: - -```bash -$ tfctl profile get organization -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile set` reference - -**Usage:** `tfctl profile set PROPERTY VALUE [options]` - -#### Description - -Set the value of the given configuration property for a profile. - -#### Arguments - -- `PROPERTY`: The configuration property name to set. - - Required argument - - Data type: String - -- `VALUE`: The value to set for the property. - - Required argument - - Data type: String - -#### Examples - -Set the organization to `my-organization` for the active profile: - -```bash -$ tfctl profile set default_organization my-organization -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile unset` reference - -**Usage:** `tfctl profile unset PROPERTY [options]` - -#### Description - -Unset the value of the given configuration property for a profile. - -#### Arguments - -- `PROPERTY`: The configuration property name to unset. - - Required argument - - Data type: String - -#### Examples - -Unset the organization for the active profile: - -```bash -$ tfctl profile unset default_organization -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile profiles activate` reference - -**Usage:** `tfctl profile profiles activate NAME [options]` - -#### Description - -Activate an existing named profile. - -#### Arguments - -- `NAME`: The profile name to activate. - - Required argument - - Data type: String - -#### Examples - -Switch to a profile named `my-profile`: - -```bash -$ tfctl profile profiles activate my-profile -``` - -Switch back to the default profile: - -```bash -$ tfctl profile profiles activate default -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile profiles create` reference - -**Usage:** `tfctl profile profiles create NAME [options]` - -#### Description - -Create a new profile, and activate it automatically unless `--no-activate` is specified. - -#### Arguments - -- `NAME`: The profile name to create. - - Required argument - - Data type: String - -#### Flags - -- `--no-activate`: Don't automatically activate the new profile. - - Optional - - Data type: Boolean flag - - Default: `false` - -- `--hostname`: Set the hostname for the new profile. - - Optional - - Data type: String - -#### Examples - -Create and switch to a new profile named `my-profile` configured for a Terraform Enterprise instance hosted at `my-tfe-instance.example.com`: - -```bash -$ tfctl profile profiles create my-profile --hostname="my-tfe-instance.example.com" -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile profiles list` reference - -**Usage:** `tfctl profile profiles list [options]` - -#### Description - -List existing profiles. - -#### Examples - -List all profiles in JSON format: - -```bash -$ tfctl profile profiles list --json -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile profiles delete` reference - -**Usage:** `tfctl profile profiles delete NAME [ ...] [options]` - -#### Description - -Delete an existing named profile. - -#### Arguments - -- `NAME`: One or more profile names to delete. - - Required argument - - Data type: String - - Repeatable - -#### Examples - -Delete a profile named `old-profile`. - -```bash -$ tfctl profile profiles delete old-profile -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl profile profiles rename` reference - -**Usage:** `tfctl profile profiles rename NAME --new_name= [options]` - -#### Description - -Rename an existing named profile. - -#### Arguments - -- `NAME`: The current profile name. - - Required argument - - Data type: String - -- `--new_name`: Set the new profile name. - - Required - - Data type: String - -#### Examples - -Rename a profile named `old-name` to `new-name`: - -```bash -$ tfctl profile profiles rename old-name --new_name="new-name" -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl run start` reference - -**Usage:** `tfctl run start ID_OR_NAME [options]` - -#### Description - -Start a new run on the workspace specified by ID or name. - -#### Arguments - -- `ID_OR_NAME`: Workspace ID or name. - - Required argument - - Data type: String - -#### Flags - -- `--allow-empty-apply`: Allow the run to proceed even if the plan has no changes. - - Optional - - Data type: Boolean flag - - Default: `false` - -- `--debugging-mode`: Enables trace logging for this run by setting TF_LOG=trace in the Terraform environment for this run. - - Optional - - Data type: Boolean flag - - Default: `false` - -- `--message`: Attach a message to the run. - - Optional - - Data type: String - -- `--organization`: Organization name. This value overrides the organization name configured for the profile. - - Optional - - Data type: String - -#### Examples - -Start a new run on an existing workspace named `my-workspace`: - -```bash -$ tfctl run start my-workspace -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl run status` reference - -**Usage:** `tfctl run status ID [options]` - -#### Description - -Print out the status of a run. You can specify a run ID, workspace ID, or workspace name for the `ID` argument. - -- Run ID: Gets the specific run. Run IDs are prefixed with `run-`. -- Workspace ID: Gets the latest run on the workspace. Workspace IDs are prefixed with `ws-`. -- Workspace name: Gets the most recent run on the named workspace. - -#### Arguments - -- ``: Run ID, workspace ID, or workspace name. - - Required argument - - Data type: String - -#### Flags - -- `--organization`: Organization name. - - Optional - - Data type: String - - Default: Defaults to the profile organization or HCP Terraform configuration context. - -#### Examples - -Print out the status of a run with an ID of `run-1234abcd`: - -```bash -$ tfctl run status run-1234abcd -``` - -Print out the status of the latest run on a workspace named `my-workspace`: - -```bash -$ tfctl run status my-workspace -``` - -Print out the status of the latest run on a workspace with an ID of `ws-abc123xyz`: - -```bash -$ tfctl run status ws-abc123xyz -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) - -### `tfctl variable import` reference - -**Usage:** `tfctl variable import [TFVARS_FILE] [options]` - -#### Description - -Import Terraform variables from .tfvars files or environment variables from the `tfctl` process environment into a workspace or variable set. - -You must provide either a variable set or a workspace by name. Otherwise, `tfctl` scans the current working directory for Terraform configuration to attempt to determine the workspace name. - -#### Arguments - -- ``: The .tfvars file to import variables from. You can specify the path to the file if it's in another directory. The CLI automatically configures variables whose names indicate they may be sensitive as `sensitive`, such as those containing `secret`, `token`, or `private`. - - Optional argument - - Data type: String - -#### Options - -- `--env`, `-e`: Environment variable to import. `tfctl` configures all imported environment variables as sensitive. - - Repeatable - - Data type: String - -- `--organization`: Organization name. - - Optional - - Data type: String - - Default: Defaults to the profile configuration or HCP Terraform configuration context. - -- `--overwrite`: Update existing variables to match the imported variables. Otherwise, the CLI reports an error when imported variables don't match existing variables. - - Optional - - Data type: Boolean flag - - Default: `false` - -- `--variable-set-name`: Target variable set by name. - - Optional - - Data type: String - - Default: If omitted, the CLI sets workspace variables - -- `--workspace`: Workspace name override. - - Optional - - Data type: String - - Default: Defaults to HCP Terraform or Terraform Enterprise configuration context - -#### Examples - -Import Terraform variables from `terraform.tfvars` file to a workspace named `my-workspace`: - -```bash -$ tfctl variable import terraform.tfvars --workspace="my-workspace" -``` - -Import multiple environment variables into a variable set named `my-varset` and overwrite existing values: - -```bash -$ tfctl variable import --overwrite --variable-set-name="my-varset" --env="AWS_ACCESS_KEY_ID" --env="AWS_SECRET_ACCESS_KEY" -``` - -#### Related - -- [Configuration reference](#configuration-reference) -- [Global flags](#global-flags) ### Exit Codes @@ -892,7 +225,7 @@ $ tfctl variable import --overwrite --variable-set-name="my-varset" --env="AWS_A ### Telemetry -By default, HashiCorp collects anonymous trace telemetry for each command invocation, including the command name, exit status, network metrics, and basic process information. You can disable telemetry using any of these methods: Set `TFCTL_TELEMETRY` to `disabled`, Set `DO_NOT_TRACK` to `true`, or set telemetry to disabled in your profile: +By default, HashiCorp collects anonymous, redacted trace telemetry for each command invocation, including the command name, exit status, network metrics, and basic process information. You can disable telemetry using any of these methods: Set `TFCTL_TELEMETRY` to `disabled`, Set `DO_NOT_TRACK` to `true`, or set telemetry to disabled in your profile: ```bash $ tfctl profile set telemetry disabled @@ -904,6 +237,8 @@ You can view the telemetry that we transmit by setting profile telemetry to `log $ tfctl profile set telemetry log ``` +See [TELEMETRY.md](TELEMETRY.md) for more information, including full trace schema. + ### Uninstall/Clean up **Uninstall Shell Completions** diff --git a/TELEMETRY.md b/TELEMETRY.md new file mode 100644 index 0000000..4cb14c8 --- /dev/null +++ b/TELEMETRY.md @@ -0,0 +1,53 @@ +# Telemetry + +By default, HashiCorp collects anonymous trace telemetry for each command invocation, including the command name, exit status, network metrics, and basic process information. You can disable telemetry using any of these methods: Set `TFCTL_TELEMETRY` to `disabled`, Set `DO_NOT_TRACK` to `true`, or set telemetry to disabled in your profile: + +```bash +$ tfctl profile set telemetry disabled +``` + +You can view the telemetry that we transmit by setting profile telemetry to `log`: + +```bash +$ tfctl profile set telemetry log +``` + +## Telemetric Schema + +HashiCorp does not collect or transmit your data and takes steps to redact and obscure data that could contain your data. The following schema reflects exactly which telemetric data is collected. + +### Resource Schema (Common to all spans) +| Key | Example Value | Source | +|-----------------|----------------|-------------------------------------------| +| device_id | (string uuid) | Randomly generated once per install | +| service.name | "tfctl" | Constant | +| service.version | "v0.3.0" | Build version | +| session_id | (SHA-256 hash) | Hash of the parent process ID + device_id | + +### Command Span Schema (One span per execution) + +| Key | Example Value | Source | +|------------------|---------------------|----------------------------------------------------------------------| +| command | "api schema search" | Which subcommand was invoked (not the full command arguments) | +| dry_run_flag | false | Global flag state | +| debug_flag | false | Global flag state | +| json_flag | false | Global flag state | +| os | “darwin” | GOOS value | +| arch | “arm64” | GOARCH value | +| is_ci | false | “CI” env detection | +| is_tty | false | Terminal environment detection | +| is_named_profile | false | Config detection- default profile was loaded, or some named profile? | +| hostname | (SHA-256 hash) | Hashed hostname from config | +| exit_status | 0 | Process exit code | +| agent | "claude" | Process environment detection (CLAUDECODE == “1” ?) | + +### Network Request Span (One span per network request) + +| Key | Example Value | Source | +|---------------------|-------------------|-------------------------------------| +| http.path | “/workspaces/:id” | API request path, with IDs redacted | +| http.method | "GET" | API request verb | +| http.status_code | 200 | HTTP response status | +| http.content_length | 121070 | Response byte size | +| http.duration_ms | 855 | Request latency | + diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..3b1b1d4 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,20 @@ +#! /usr/bin/env bash + +set -e + +GREEN='\033[0;32m' +RED='\033[0;31m' +RESET='\033[0m' + +echo "Running \"make tools\" and \"make go/install\"..." +make tools +make go/install + +# Verify installation +if command -v tfctl >/dev/null 2>&1; then + echo -e "${GREEN}✓${RESET} tfctl CLI installed: $(tfctl --version 2>&1)" +else + echo -e "${RED}✗${RESET} tfctl installed to $(go env GOPATH)/bin/ but that's not on PATH" + echo " Add to shell config: export PATH=\"\$(go env GOPATH)/bin:\$PATH\"" + exit 1 +fi \ No newline at end of file