Puppet calls /usr/local/bin/certmanager from generate() with combine: true, so the CLI must take the common name positionally and keep stderr clean or the catalog compile dies on a parsejson error. - Flatten the CLI: drop the pki subcommand, add -a/-i/-e/-c/--json - Emit ca_certificate so vault.pp stops writing undef to that file - Read output_path from under vault:, where Puppet writes it - Bound every Vault request with a timeout; exit non-zero on failure - Gate TLS verification on vault.tls_skip_verify (default true) - Restrict auth to approle and kubernetes, no fallback between them - Remove the SSH half; add Woodpecker CI, nfpm RPM and completions
4.2 KiB
AGENTS.md
Project Overview
certmanager issues Vault PKI certificates for Puppet-managed nodes. It is a
drop-in replacement for the Python certmanager script that
profiles::helpers::certmanager used to render into a venv.
The contract
profiles::pki::vault (puppet-prod) calls the binary during catalog
compilation:
/usr/local/bin/certmanager <fqdn> --alt-names "host,fqdn" --ip-sans "127.0.0.1,<ip>" --expiry-days 90 --json
generate() runs with failonfail: true, combine: true. combine: true dups
stderr onto stdout at spawn, so anything on stderr is spliced into the JSON
stream and parsejson() fails — breaking the compile with a parse error rather
than a clean failure. Two rules follow:
- the success path writes the JSON document to stdout and nothing to stderr
(
TestRunSuccessWritesNothingToStderrguards this); - every failure exits non-zero, with the message on stderr and stdout empty.
Structure
cmd/certmanager/main.go # CLI (flattened: the positional arg is the command)
cmd/certmanager/main_test.go # end-to-end tests against an httptest Vault stub
internal/config/ # YAML config: parsing, defaults, validation
internal/pki/ # issue request/response + the JSON output shape
internal/vault/ # authenticated HTTP client (approle, kubernetes)
Makefile # build / test / lint / completions / rpm / version bump
packaging/nfpm.yaml # nfpm spec (envsubst-templated) for the RPM
scripts/build-rpm.sh # generates completions + packages the RPM
.woodpecker/ # CI: build, test, pre-commit (PR) + release (tag)
dist/ # build output: binary, completions, RPM (not committed)
Code Patterns
run(args, stdout, stderr) intis the testable entry point;main()only wires it to the real streams andos.Exit. Cobra runs withSilenceUsageandSilenceErrorsso nothing reaches stderr unless the run failed.- Flat CLI: the root command takes
common_namepositionally (cobra.ExactArgs(1)). Onlycompletionandhelpare subcommands. parseListsplits comma lists, trims whitespace, drops empty tokens.config.Loadapplies defaults (auth_method: approle,jwt_path) thenValidate(), which names the offending key and rejects an unknownauth_methodbefore any network call.vault.Newauthenticates once and never falls back between auth methods. Every request is bounded byvault.timeout(default 30s) — the Python set no timeout, so a black-holed Vault hung the whole catalog compile.config.defaultPathis baked in at build time via-X ...internal/config.defaultPath=(make build CONFIG_PATH=...).
Deliberate deviations from the Python script
- Emits
ca_certificate; the Python did not, so Puppet's/etc/pki/tls/vault/ca_certificate.crtwas silently never populated. output_pathis read from undervault:where it is actually written. The Python declared it at the top level but read it from the unwrappedvaultdict, so it never resolved.- HTTP timeouts on every request.
tls_skip_verifyis a config key defaulting totrue, preserving the Python'sverify=Falsewhile letting it be flipped without a code change.- Non-zero exit on every failure path.
ldapandtokenauth methods are gone; onlyapproleandkubernetesexist.
Testing
make test # go test -v -race ./...
Vault is stubbed with httptest.NewTLSServer — no live Vault is required.
Coverage: the golden --json stdout document, stderr emptiness on success,
rejected logins for both auth methods, missing/empty JWT, unknown
auth_method, a non-200 issue response, request timeout, TLS verification when
enabled, file and zip output, and the comma-list parsing table.
Gotchas
- Changing the JSON key set or key order breaks
parsejson()inprofiles::pki::vault;TestRunJSONGoldenpins the exact bytes. alt_namesandip_sansare always sent, as empty strings when unset, to match what the Python posted.- The RPM owns
/usr/local/bin/certmanageras a symlink; the Puppet class that manages that path must be retired in the same change that ships the RPM.