Skip to content

Commit 7b55522

Browse files
committed
[WIP] feat: Add OpenTofu commands.
1 parent 212950e commit 7b55522

16 files changed

Lines changed: 1059 additions & 9 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ settings.local.json
99
# Ignore Byebug command history file.
1010
.byebug_history
1111

12-
## Environment normalization:
12+
# Environment normalization:
1313
/.bundle/
14+
/bin/
1415
/vendor/bundle
1516
/lib/bundler/man/
1617

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ AllCops:
99
NewCops: enable
1010
SuggestExtensions: true
1111
Exclude:
12+
- 'bin/**/*'
1213
- 'out/**/*'
1314
- 'vendor/**/*'
1415
- 'doc/**/*'
@@ -26,7 +27,9 @@ Metrics/MethodLength:
2627
Naming/FileName:
2728
Exclude:
2829
- 'lib/cfa-eng-cli.rb'
30+
- 'lib/cfa_eng_cli/config/Doppler.rb'
2931
- 'README.md'
32+
- 'CLAUDE.md'
3033

3134
# Count multi-line hashes and arrays in examples as one line.
3235
RSpec/ExampleLength:

CLAUDE.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
bundle exec rake # lint + tests (default)
9+
bundle exec rake rubocop # lint only
10+
bundle exec rake spec # all tests
11+
bundle exec rspec spec/unit/cfa_eng_cli/bastion_spec.rb # single test file
12+
```
13+
14+
## Architecture
15+
16+
This is a Ruby CLI gem for Code for America engineering teams, built on [Thor](https://github.com/rails/thor). It manages AWS bastion host connections and named profiles.
17+
18+
**Entry point:** `bin/cfa-eng` — defines the top-level Thor class and registers two subcommand groups.
19+
20+
**Two subcommand groups:**
21+
- `bastion` — creates/deletes tunnel configs and opens SSH tunnels via AWS Session Manager port forwarding
22+
- `profile` — CRUD for named connection profiles stored as YAML in `~/.codeforamerica/profiles/`
23+
24+
**Command layer** (`lib/cfa_eng_cli/commands/`): Each group is a Thor subcommand class inheriting from `Commands::Command` (base.rb). Commands delegate to business logic classes rather than containing logic themselves.
25+
26+
**Business logic** (`lib/cfa_eng_cli/`):
27+
- `Bastion` — discovers running EC2 bastion instances by project/environment tags, constructs `SessionTarget`, and opens port-forwarding tunnels
28+
- `SessionManager` — wraps the AWS SSM `StartSession` API and shells out to the `session-manager-plugin` binary to maintain the connection
29+
- `SessionTarget` — simple data object holding instance ID, region, and AWS profile name
30+
31+
**Config system** (`lib/cfa_eng_cli/config/`): Uses the [ConfigSL](https://github.com/codeforamerica/configsl) gem for declarative schema definitions. `Config::Base` handles YAML serialization. `Config::Profile` and `Config::RemoteTunnel` define the data shapes. Profile YAML files live at `~/.codeforamerica/profiles/<name>.yaml`.

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ source 'https://rubygems.org'
55
gemspec
66

77
# TODO: Move to gemspec once a new release has been cut.
8-
gem 'configsl', git: 'https://github.com/jamesiarmes/configsl.git', branch: 'collections'
8+
gem 'configsl', git: 'https://github.com/jamesiarmes/configsl.git', branch: 'main'
99

1010
group :development do
1111
gem 'rake', '~> 13.4'

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
GIT
22
remote: https://github.com/jamesiarmes/configsl.git
3-
revision: 2023f113349cb1e70918ccb6b0598ba11a6add02
4-
branch: collections
3+
revision: 297b420f32e47752638d2d43823f5c6a71866f39
4+
branch: main
55
specs:
66
configsl (1.0.2)
77
facets (~> 3.1)

README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,244 @@
33
> [!CAUTION]
44
> This is a work in progress. You are welcome to try it out, and contribute,
55
> but it should not be considered stable or production-ready.
6+
7+
`cfa-eng` is a CLI tool for Code for America engineering teams. It manages AWS
8+
bastion host connections, named profiles, and OpenTofu infrastructure operations.
9+
10+
## Requirements
11+
12+
- Ruby 3.3+
13+
- [AWS CLI](https://aws.amazon.com/cli/) with the
14+
[Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html)
15+
- [Doppler CLI](https://docs.doppler.com/docs/install-cli) (for `tofu` commands)
16+
- [OpenTofu](https://opentofu.org/docs/intro/install/) (for `tofu` commands)
17+
18+
## Installation
19+
20+
```bash
21+
gem install cfa-eng-cli
22+
```
23+
24+
Or add it to your `Gemfile`:
25+
26+
```ruby
27+
gem 'cfa-eng-cli'
28+
```
29+
30+
## Configuration
31+
32+
`cfa-eng` uses **profiles** to store named sets of connection settings. Most
33+
commands require a profile, specified with `--profile` or the `CFA_PROFILE`
34+
environment variable.
35+
36+
```bash
37+
export CFA_PROFILE=my-app
38+
```
39+
40+
Profile files are stored as YAML in `~/.codeforamerica/profiles/`.
41+
42+
<!-- cspell:ignore codeforamerica -->
43+
44+
## Commands
45+
46+
### `profile` — Manage profiles
47+
48+
#### `profile create`
49+
50+
Creates a new profile. All fields can be supplied as options; any omitted fields
51+
are prompted for interactively.
52+
53+
```bash
54+
cfa-eng profile create
55+
cfa-eng profile create --name my-app --project snap --environment staging \
56+
--aws-profile shared-services-dev --region us-east-1
57+
```
58+
59+
| Option | Description |
60+
|---|---|
61+
| `--name` | Profile name |
62+
| `--project` | Project name |
63+
| `--environment` | Deployment environment (e.g. `staging`, `production`) |
64+
| `--aws-profile` | AWS CLI profile to use for credentials |
65+
| `--region` | Primary AWS region (default: `us-east-1`) |
66+
| `--doppler-project` | Doppler project for secrets (default: `shared-services`) |
67+
| `--doppler-environment` | Doppler environment prefix used for config names, e.g. `infra` produces configs like `infra_dev`, `infra_prod`, `infra_<env>` (default: `infra`) |
68+
69+
#### `profile list`
70+
71+
Lists all saved profiles.
72+
73+
```bash
74+
cfa-eng profile list
75+
```
76+
77+
#### `profile delete PROFILE`
78+
79+
Deletes a profile. Prompts for confirmation unless `--yes` is given.
80+
81+
```bash
82+
cfa-eng profile delete my-app
83+
cfa-eng profile delete my-app --yes
84+
```
85+
86+
#### `profile rename PROFILE NAME`
87+
88+
Renames a profile. Prompts for confirmation unless `--yes` is given.
89+
90+
```bash
91+
cfa-eng profile rename my-app my-renamed-app
92+
cfa-eng profile rename my-app my-renamed-app --yes
93+
```
94+
95+
---
96+
97+
### `bastion` — Connect to bastion hosts
98+
99+
The `bastion` commands connect to AWS bastion hosts via SSM Session Manager port
100+
forwarding. A profile is required for all bastion commands.
101+
102+
All `bastion` commands accept:
103+
104+
| Option | Description |
105+
|---|---|
106+
| `--profile NAME` | Profile to use (default: `$CFA_PROFILE`) |
107+
108+
#### `bastion create-tunnel`
109+
110+
Adds a tunnel configuration to a profile. Prompts interactively for tunnel
111+
settings.
112+
113+
```bash
114+
cfa-eng bastion create-tunnel --profile my-app
115+
```
116+
117+
#### `bastion delete-tunnel NAME`
118+
119+
Removes a named tunnel configuration from a profile.
120+
121+
```bash
122+
cfa-eng bastion delete-tunnel my-db --profile my-app
123+
```
124+
125+
#### `bastion tunnel`
126+
127+
Opens an SSH tunnel through the bastion to a remote host using a saved tunnel
128+
configuration.
129+
130+
```bash
131+
cfa-eng bastion tunnel --name my-db --profile my-app
132+
```
133+
134+
| Option | Description |
135+
|---|---|
136+
| `--name` | Name of the tunnel configuration to use |
137+
138+
---
139+
140+
### `tofu` — Run OpenTofu operations
141+
142+
The `tofu` commands wrap common OpenTofu operations. They automatically fetch
143+
secrets from Doppler, select the appropriate AWS profile for the environment,
144+
and run `tofu init` before each operation.
145+
146+
A profile is required for all `tofu` commands and supplies the environment,
147+
AWS credentials, project name, and Doppler project.
148+
149+
All `tofu` commands accept:
150+
151+
| Option | Description |
152+
|---|---|
153+
| `--profile NAME` | Profile to use (default: `$CFA_PROFILE`) |
154+
155+
These commands must be run from the root of an OpenTofu repository (a directory
156+
containing `tofu/configs/`).
157+
158+
#### `tofu plan CONFIG`
159+
160+
Runs `tofu plan -concise` for the given configuration layer.
161+
162+
```bash
163+
cfa-eng tofu plan foundation --profile my-app
164+
cfa-eng tofu plan application --profile my-app --args="-out tfplan"
165+
```
166+
167+
| Option | Description |
168+
|---|---|
169+
| `--args STRING` | Extra arguments forwarded to `tofu plan` |
170+
171+
#### `tofu apply CONFIG`
172+
173+
Runs `tofu apply` for the given configuration layer.
174+
175+
```bash
176+
cfa-eng tofu apply foundation --profile my-app
177+
cfa-eng tofu apply application --profile my-app --args="-auto-approve"
178+
```
179+
180+
| Option | Description |
181+
|---|---|
182+
| `--args STRING` | Extra arguments forwarded to `tofu apply` |
183+
184+
#### `tofu destroy CONFIG`
185+
186+
Runs `tofu destroy` for the given configuration layer.
187+
188+
```bash
189+
cfa-eng tofu destroy application --profile my-app
190+
```
191+
192+
| Option | Description |
193+
|---|---|
194+
| `--args STRING` | Extra arguments forwarded to `tofu destroy` |
195+
196+
#### `tofu output CONFIG`
197+
198+
Shows the outputs for the given configuration layer.
199+
200+
```bash
201+
cfa-eng tofu output foundation --profile my-app
202+
cfa-eng tofu output foundation --profile my-app --args="-json"
203+
```
204+
205+
| Option | Description |
206+
|---|---|
207+
| `--args STRING` | Extra arguments forwarded to `tofu output` |
208+
209+
#### `tofu force-unlock CONFIG LOCK_ID`
210+
211+
Force-unlocks a stuck state lock for the given configuration layer. Prompts for
212+
confirmation before proceeding. Does not fetch Doppler secrets.
213+
214+
```bash
215+
cfa-eng tofu force-unlock foundation abc-1234-5678 --profile my-app
216+
```
217+
218+
> [!WARNING]
219+
> Only use this if you are certain the lock is stale. Unlocking while another
220+
> operation is in progress will corrupt state.
221+
222+
#### `tofu bootstrap`
223+
224+
Bootstraps the remote S3 state backend for a new environment. If the state
225+
bucket does not yet exist, it applies the `foundation` configuration with a
226+
local backend first and then migrates state to S3.
227+
228+
```bash
229+
cfa-eng tofu bootstrap --profile my-app
230+
```
231+
232+
> [!NOTE]
233+
> `tofu bootstrap` always operates on `tofu/configs/foundation`.
234+
235+
### AWS profile selection
236+
237+
For `tofu` commands, the AWS profile is taken from the saved profile's
238+
`aws_profile` field. If no `aws_profile` is stored in the profile, the CLI
239+
selects a default based on the environment:
240+
241+
| Environment | Default AWS profile |
242+
|---|---|
243+
| `production` | `shared-services-prod` |
244+
| All others | `shared-services-dev` |
245+
246+
A red warning is printed before any operation targeting `production`.

cfa-eng-cli.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Gem::Specification.new do |s|
1111
s.authors = ['Code for America']
1212
s.email = 'infra@codeforamerica.org'
1313
s.files = Dir['lib/**/*'] + Dir['Gemfile*'] + ['Rakefile']
14+
s.bindir = 'exe'
15+
s.executables << 'cfa-eng'
1416
s.homepage = 'https://codeforamerica.org'
1517
s.metadata = {
1618
'bug_tracker_uri' => 'https://github.com/codeforamerica/cfa-eng-url/issues',

bin/cfa-eng renamed to exe/cfa-eng

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class CfaEng < Thor
2222

2323
desc 'profile SUBCOMMAND', 'Commands for managing profiles.'
2424
subcommand 'profile', CfaEngCli::Commands::Profile
25+
26+
desc 'tofu SUBCOMMAND', 'Commands for running OpenTofu operations.'
27+
subcommand 'tofu', CfaEngCli::Commands::Tofu
2528
end
2629

2730
CfaEng.start(ARGV)

lib/cfa-eng-cli.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative 'cfa_eng_cli/bastion'
44
require_relative 'cfa_eng_cli/commands'
5+
require_relative 'cfa_eng_cli/open_tofu'
56
require_relative 'cfa_eng_cli/config'
67
require_relative 'cfa_eng_cli/version'
78

lib/cfa_eng_cli/commands.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
require_relative 'commands/bastion'
44
require_relative 'commands/profile'
5+
require_relative 'commands/tofu'

0 commit comments

Comments
 (0)