Add base class for offline cosign commands
What does this MR do and why?
This MR adds SupplyChain::Cosign::Command in lib/supply_chain/cosign/command.rb, a base class for running one offline cosign CLI command. It is the first of three MRs for https://gitlab.com/gitlab-org/gitlab/-/work_items/603914 (local CA signing, ADR 007).
Local CA signing for private projects, self-managed and Dedicated (ADR 007) needs several offline cosign commands: import-key-pair now, trusted-root create and signing-config create in follow-up issues. Each command repeats the same work: run cosign, pass environment and stdin, check the result, map errors. This base class does that work once, so each subclass only needs to declare its subcommand and arguments.
Behaviour:
- Subclasses define a
SUBCOMMANDconstant (for example%w[import-key-pair]), plusargumentsandoutput_path. Optional hooks:validate!,env,input,result.executeruns validation, then cosign, then returns the result (the output path by default). - The command is built as
['cosign', *SUBCOMMAND, *arguments]and run withGitlab::Popen.popen_with_detailas a token array, not through a shell. - The caller owns
dir(an existing directory). No working directory is passed toGitlab::Popen, because that would recreatedirwith default permissions if the directory had been deleted. - Any output file left by an earlier run is removed first, so a cosign run that exits 0 without writing anything can never be mistaken for success.
- Errors are handled in one place and raised as
SupplyChain::Cosign::Command::Error(InvalidInputfor bad input). Each message names the subcommand and says what went wrong: the exit status or signal (for example an OOM kill) with cosign's stderr, cosign missing fromPATH, or cosign exiting successfully without writing its output. If cosign stops reading stdin early, its own exit status and stderr are reported instead of the broken pipe.
Not in this MR: there is no caller yet. The first subclass, ImportKeyPair (converts the leaf private key into a cosign-encrypted key), is the next MR, stacked on top of this one. Wiring into the provenance publisher comes in later MRs and in https://gitlab.com/gitlab-org/gitlab/-/work_items/603913.
No feature flag change is needed. Nothing calls this code yet, and the feature is behind the existing slsa_provenance_statement flag.
No database, API, UI or documentation changes.
Tests: spec/lib/supply_chain/cosign/command_spec.rb (14 examples, fast_spec_helper, with Gitlab::Popen stubbed so CI does not need cosign installed), using a shared context in spec/support/shared_contexts/lib/supply_chain/cosign/stubbed_cosign_shared_context.rb. 100% line (45/45) and branch (12/12) coverage of command.rb.
References
- Issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/603914
- Verification approach decision: https://gitlab.com/gitlab-org/gitlab/-/work_items/630121
- Wiring issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/603913
- Epic: https://gitlab.com/groups/gitlab-org/-/work_items/22313
- ADR 007: https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/slsa_level_3/decisions/007_attestations_private_dedicated_self_hosted/
Screenshots or screen recordings
Not applicable. This is a backend-only change with no UI.
How to set up and validate locally
In a Rails console, with cosign installed:
-
Define a throwaway subclass and run it:
You would need to have
cosigninstalled locallybrew install cosign # if you want to test this locallycmd = Class.new(SupplyChain::Cosign::Command) do const_set(:SUBCOMMAND, %w[signing-config create].freeze) private def arguments = ['--out', output_path] def output_path = path('signing-config.json') end dir = Dir.mktmpdir path = cmd.new(dir: dir).execute File.read(path)Expect a JSON signing config.
-
Show the error path: run the same subclass with cosign removed from
PATHand expectSupplyChain::Cosign::Command::Errorwith a message containing "could not be run: Errno::ENOENT". -
Run the specs:
bin/rspec spec/lib/supply_chain/cosign/command_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.