tfcapi-lint CLI¶
tfcapi-lint checks a Terraform or OpenTofu module, or the OCI image built from it, against the CAPTF module contract and image contract. It reads the module’s files and the image’s layers. It never runs init, plan or apply, and it needs neither terraform nor tofu.
This page is the reference: every command, flag, exit code and check. For a walkthrough, registry credentials and CI patterns, see tfcapi-lint.
Install¶
Each provider release attaches one static binary per platform and a checksum file: tfcapi-lint-linux-amd64, tfcapi-lint-linux-arm64, tfcapi-lint-darwin-amd64, tfcapi-lint-darwin-arm64, tfcapi-lint-windows-amd64.exe and tfcapi-lint-checksums.txt (SHA-256). The binary has the same version as the provider release. There is no Windows ARM64 build, and go install does not work because the repository is a Go workspace.
curl -fsSLO "https://github.com/captf-io/cluster-api-provider-terraform/releases/download/<version>/tfcapi-lint-<os>-<arch>"
install -m 0755 "tfcapi-lint-<os>-<arch>" /usr/local/bin/tfcapi-lint
tfcapi-lint version
<version>is the provider release, for examplev0.1.0.<os>and<arch>pick a binary from the list above.
The tfcapi-lint guide shows how to verify the checksum.
Synopsis¶
tfcapi-lint image --role=<role> [flags] <image-ref>
tfcapi-lint module --role=<role> [flags] <module-dir>
tfcapi-lint version [--json]
tfcapi-lint --version[=raw]
<role> is cluster, machine or machinepool. A bare tfcapi-lint prints its usage and exits 3.
Global flags¶
| Flag | Type | Default | Description |
|---|---|---|---|
--version | version | false | Print version information and exit. --version=raw prints the full build information. --version=vX.Y.Z sets the reported version. |
module¶
Lints a module directory against the contract. It reads the .tf, .tf.json, .tofu and .tofu.json files in <module-dir> and in the local modules it calls, and runs the module checks for the role.
| Flag | Type | Default | Description |
|---|---|---|---|
--role | string | The module role: cluster, machine or machinepool. Required. | |
--contract | string | v1alpha1 | The contract version to lint against. |
--json | bool | false | Print a JSON report instead of text. |
--strict | bool | false | Count warnings as errors for the exit code. |
--allow-warning | stringSlice | Downgrade this check ID’s warnings to info. Repeatable, or a comma-separated list. Errors cannot be allowed. |
image¶
Lints a built source image against the image contract. It pulls the manifest and layers without running the image, runs the image checks, and runs the module checks on the module in /captf/module.
<image-ref> is a registry reference such as registry.example.com/acme/machine:v1.0.0, or oci:<dir> for a local OCI image layout. It authenticates with the same credential files as docker and podman.
| Flag | Type | Default | Description |
|---|---|---|---|
--role | string | The module role. Required. | |
--contract | string | v1alpha1 | The contract version to lint against. |
--json | bool | false | Print a JSON report instead of text. |
--strict | bool | false | Count warnings as errors for the exit code. |
--allow-warning | stringSlice | Downgrade this check ID’s warnings to info. Repeatable. Errors cannot be allowed. | |
--platform | string | linux/amd64 | The platform to check in a multi-platform image. |
--all-platforms | bool | false | Check every platform the image publishes. |
--insecure | bool | false | Allow a plain-HTTP registry. |
Lint a local image without a registry by saving it as an OCI layout first:
podman save --format oci-dir -o ./image <image>
tfcapi-lint image --role machine --strict oci:./image
<image>is the local image name, for examplelocalhost/acme/machine:dev.
If you set --platform and the image is for a different platform, the result is an image/platform error. An image with no manifest for the requested platform gets the same error.
Output¶
The text format prints one line per finding, then a summary:
error input/required -:0 contract input bootstrap_data is not declared as a variable
0 error(s), 1 warning(s), 0 info (read terraform files)
A finding that concerns the whole module or image shows - as its file. With --json, the report has a findings array and a summary. Each finding has id, severity (error, warning or info), file, line and message. summary counts error, warning and info. An image report also describes the image it checked.
Findings are ordered by file, line and ID, so output is stable between runs. --allow-warning changes a warning to info and says so in its message.
version¶
Prints the program name and version, then exits 0.
| Flag | Type | Default | Description |
|---|---|---|---|
--json | bool | false | Print JSON with version, commit, date and contract, the list of contract versions this build can lint against. |
Exit codes¶
| Code | Name | Meaning |
|---|---|---|
0 | ExitOK | No errors, and with --strict, no warnings. Info findings never fail a run. |
1 | ExitFindings | At least one error, or a warning under --strict. |
2 | ExitUnparsable | The module could not be read or parsed, or the image could not be pulled or extracted. |
3 | ExitUsage | A bad command line: a missing --role, an unknown role or contract version, a wrong number of arguments, or an invalid image reference. |
In CI, treat any non-zero code as a failed step. Use --strict to fail on warnings, which is the recommended default.
Checks¶
Every check has an ID of the form <group>/<name>, a severity and a fix. Run --json to see IDs in a report. A finding’s severity decides the exit code:
| Severity | Effect |
|---|---|
error | Fails the run. Cannot be allowed. |
warning | Fails the run only with --strict. --allow-warning <id> downgrades it to info. |
info | Never fails the run. |
The module command runs the module checks. The image command runs the image checks and the module checks on the image’s module. All module checks apply to every role unless a row says otherwise.
Module checks¶
Inputs¶
The contract’s inputs are the variables the controller passes to your module. See the common contract and the role pages for the full lists.
| ID | Severity | What it checks | How to fix |
|---|---|---|---|
input/required | error | A contract input is not declared as a variable. | Declare a variable for it. |
input/type | error | A contract input’s type does not accept what the generated root passes. A missing type, or a type the linter cannot read, is a warning. | Declare a type that accepts the contract’s type. |
input/default | warning | An input the controller always sets to a non-null value has a default, which would hide a controller mistake. Nullable inputs may default to null. | Remove the default. |
input/sensitive | warning | bootstrap_data is declared without sensitive = true, though it carries the bootstrap payload. | Add sensitive = true. |
input/reserved | error | A variable uses the reserved captf_ prefix but is not a contract input. | Rename the variable. |
input/tags-declared | error | The module does not declare captf_tags, the common input every module must accept. | Declare captf_tags. |
input/tags-unused | warning | captf_tags is declared but never used, directly or through a local module that uses it. | Apply var.captf_tags to every resource that can carry tags. Allow the warning if the provider cannot tag anything. |
input/user-variable-default | warning | A variable outside the contract has no default, so an object that does not set it in spec.variables or variablesFrom fails to apply. | Give it a default. |
Outputs¶
| ID | Severity | What it checks | How to fix |
|---|---|---|---|
output/required | error | A contract output is not declared. | Declare an output for it. |
output/health | error | The health output is not declared. Every module must report its health. | Declare health as the contract describes. |
output/reserved | warning | An output uses the reserved captf_ prefix. | Rename the output. |
output/endpoint-never-set | warning | Cluster role only. control_plane_endpoint is a literal null, so a KubeadmControlPlane cluster with no user-set endpoint waits forever. | Output the endpoint your infrastructure creates. |
output/provider-id-list-shape | error or warning | Machinepool role only. It is an error when provider_id_list is not declared. It is a warning when the expression filters on health or state, or is not sorted with sort(). | Declare it, list every non-terminated member, and wrap the list in sort(). |
pool/autoscaling-ignore-changes | warning | Machinepool role only. The module uses var.autoscaling, but no resource ignores changes to its desired capacity, so each apply resets the cloud autoscaler. | Add the desired-capacity argument to lifecycle { ignore_changes }. |
Module source¶
CAPTF owns the backend and the credentials of a run, so a module must not configure them.
| ID | Severity | What it checks | How to fix |
|---|---|---|---|
module/backend | error | The root or a nested module declares a terraform { backend } block. | Remove it. The generated root owns the backend. |
module/cloud | error | The root or a nested module declares a terraform { cloud } block. | Remove it, for the same reason. |
module/provider-config | warning | A provider block sets a credential argument, such as access_key, token or password, to a string literal. A reference such as var.token is fine. | Take credentials from the identity, not from the module source. |
module/source-escape | error | A local module call resolves outside the root module directory, directly or through a symlink, or does not resolve. A module file that is a symlink out of the root is also reported. Such code is never linted. | Keep every local module inside the root directory. |
module/tofu-shadow | warning | A .tofu file shadows a .tf file, and OpenTofu loads different declarations than Terraform does. | Make the two files agree, or ship only one. |
module/version | info | The module declares no required_version. | Declare the runtime versions you tested with. |
Image checks¶
The image checks read the image’s files and config. They apply to every role. The image contract describes the paths and labels they enforce.
Content¶
| ID | Severity | What it checks | How to fix |
|---|---|---|---|
image/module-present | error | /captf/module has no .tf, .tf.json, .tofu or .tofu.json file at its top level. | Copy the module into /captf/module. |
image/module-link | error | A link under /captf/module does not end at a regular file inside it. | Replace the link with the file. |
image/module-readable | warning | With a numeric non-root user, a path under the module or provider mirror is not readable or traversable. The finding lists up to five paths. | Fix the file modes or ownership. |
image/runtime-present | error | /captf/runtime is missing, is not a regular file, or is not executable by the image user. | Install the runtime there, executable. |
image/runtime-version | warning | The runtime’s file or link name looks like a different runtime than io.captf.runtime says. | Correct the label or the runtime. |
image/reserved-paths | error | Files exist under /captf/work, /captf/bin, /captf/config or /var/run/captf/credentials, which the Job mounts over. | Remove them. |
image/entrypoint | info | ENTRYPOINT or CMD is set. The runner replaces both. | None needed. |
image/platform | error | The image is not for the requested platform, or the index has no manifest for it. | Build for the platform, or pass --platform. |
Provider mirror¶
The mirror at /captf/providers is optional. Without it, init needs registry access when the Job runs.
| ID | Severity | What it checks | How to fix |
|---|---|---|---|
image/providers-absent | info | The image has no provider mirror. | Add a mirror to run without registry access. |
image/providers-layout | error | An entry is neither a packed (HOST/NS/TYPE/*.zip with *.json) nor an unpacked (HOST/NS/TYPE/VERSION/TARGET/) mirror entry. | Use the mirror layout. |
image/providers-complete | error | A mirror link is broken, or the mirror has no package for the checked platform for a provider the module requires. Built-in providers are exempt. | Mirror every required provider for the platform. |
Labels and user¶
| ID | Severity | What it checks | How to fix |
|---|---|---|---|
image/label-role | info or error | io.captf.role is not set (info), or names a different role than --role (error). | Set the label to the role. |
image/label-contract | info or warning | io.captf.contract is not set (info), or names a different version than --contract (warning). | Set the label to the contract version. |
image/label-capacity | info or error | Machine role. io.captf.capacity and io.captf.node-info do not parse. Missing labels are info: no scale-from-zero capacity. Other roles get info only. | Make the labels valid JSON of the documented shape. |
image/user-root | warning | The image runs as root, so the Job cannot run under the restricted Pod Security Standard. | Set a numeric non-root USER. |
image/user-unresolved | warning | The image user is a name the linter cannot resolve, so permission checks are approximated. | Use a numeric uid. |
Allow a warning only with a reason
--allow-warning hides a warning from --strict but not from the report, where it shows as info. Pass one flag per ID you have decided to accept, and keep the list in the CI file so a reviewer sees each exception.