putpaws 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +230 -0
- data/lib/Putpawsfile +6 -1
- data/lib/putpaws/ai/ai_task.rb +1 -0
- data/lib/putpaws/ai/guide.rb +119 -0
- data/lib/putpaws/ai/tasks/ai_task.rake +14 -0
- data/lib/putpaws/application_config.rb +23 -3
- data/lib/putpaws/code_build/build_task.rb +1 -0
- data/lib/putpaws/code_build/project_command.rb +57 -0
- data/lib/putpaws/code_build/tasks/build_task.rake +39 -0
- data/lib/putpaws/ecs/run_command.rb +115 -0
- data/lib/putpaws/ecs/task_command.rb +63 -9
- data/lib/putpaws/ecs/tasks/ecs_task.rake +113 -6
- data/lib/putpaws/iam/grant_command.rb +205 -0
- data/lib/putpaws/iam/iam_task.rb +1 -0
- data/lib/putpaws/iam/operator_config.rb +46 -0
- data/lib/putpaws/iam/tasks/iam_task.rake +65 -0
- data/lib/putpaws/info.rb +1 -0
- data/lib/putpaws/provision/all.rb +29 -0
- data/lib/putpaws/provision/aws_clients.rb +59 -0
- data/lib/putpaws/provision/config_writer.rb +88 -0
- data/lib/putpaws/provision/policy_generator.rb +221 -0
- data/lib/putpaws/provision/preset.rb +91 -0
- data/lib/putpaws/provision/presets/rails-nginx/preset.json +32 -0
- data/lib/putpaws/provision/presets/rails-nginx/templates/taskdef-app.json.erb +29 -0
- data/lib/putpaws/provision/presets/rails-nginx/templates/taskdef-web.json.erb +48 -0
- data/lib/putpaws/provision/provision_config.rb +156 -0
- data/lib/putpaws/provision/provision_task.rb +1 -0
- data/lib/putpaws/provision/resources/base.rb +49 -0
- data/lib/putpaws/provision/resources/cluster.rb +30 -0
- data/lib/putpaws/provision/resources/codebuild_project.rb +85 -0
- data/lib/putpaws/provision/resources/iam_role.rb +89 -0
- data/lib/putpaws/provision/resources/log_group.rb +50 -0
- data/lib/putpaws/provision/resources/security_group.rb +72 -0
- data/lib/putpaws/provision/resources/service.rb +71 -0
- data/lib/putpaws/provision/resources/task_definition.rb +119 -0
- data/lib/putpaws/provision/runner.rb +218 -0
- data/lib/putpaws/provision/state.rb +45 -0
- data/lib/putpaws/provision/tasks/provision.rake +129 -0
- data/lib/putpaws/provision/util.rb +50 -0
- data/lib/putpaws/tasks/info.rake +35 -0
- data/lib/putpaws/version.rb +1 -1
- metadata +80 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbb1de2cdc7c119427ca5dfa073f5e8d07f504ff73f93a464f2912a8ef5168ed
|
|
4
|
+
data.tar.gz: 042f6202ae3cb0396b0ab7210b2d299504f7e099e8a0cf43d9c6314f0b6384ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12b7e736fe78e9a49569627fd296e9f5540cbea849f8b78a7edbb56c8652de73b82d1b2ac92fef279a682f64e4e83e061ba20e2c3afe1b3c99dce7fd35526fe1
|
|
7
|
+
data.tar.gz: efad6347afdc46833694e48070afd0330fe42f8bb0bc16638b6b88a7d5b6d1a091ce50f5ce1ddec4f92cc07e54796f42cc9802c2b5aa07ed4af1d96ba5ebd171
|
data/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Put your pAWS up!!
|
|
2
|
+
|
|
3
|
+
## Command names
|
|
4
|
+
|
|
5
|
+
The short commands follow a "put your paws ..." image:
|
|
6
|
+
|
|
7
|
+
- `putpaws ready` ... put paws ready: — place your paws and prepare
|
|
8
|
+
- `putpaws steady` ... put paws steady: — hold steady, right before the start
|
|
9
|
+
- `putpaws up` ... put paws up: — everything is ready, raise them up
|
|
10
|
+
- `putpaws ahead` ... put paws ahead: — reach ahead and probe (dry-run)
|
|
11
|
+
- `putpaws away` ... put paws away: — take your paws off and let AI take over
|
|
12
|
+
|
|
13
|
+
## Provisioning
|
|
14
|
+
|
|
15
|
+
Build service infra (security group / ECS cluster / task definitions / service / log groups / CodeBuild) idempotently.
|
|
16
|
+
Deletion is out of scope on purpose: putpaws only creates and updates.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
# Interview basic infra (existing VPC / ECR / SES / S3 / SSM parameter prefix)
|
|
20
|
+
# and generate provision.json + IAM role drafts
|
|
21
|
+
bundle exec putpaws ready
|
|
22
|
+
|
|
23
|
+
# After reviewing/editing the drafts under policies/, create the IAM roles
|
|
24
|
+
# from them as-is and fill their ARNs into provision.json. Idempotent:
|
|
25
|
+
# edit a draft and run again to update the role.
|
|
26
|
+
bundle exec putpaws steady
|
|
27
|
+
|
|
28
|
+
# Dry-run: show what would be created / updated / skipped
|
|
29
|
+
bundle exec putpaws ahead
|
|
30
|
+
|
|
31
|
+
# Build the rest as far as possible. `up` stops with instructions where a manual
|
|
32
|
+
# step is required. Fix things by hand, then just run `up` again. Safe to re-run any time.
|
|
33
|
+
bundle exec putpaws up
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Running `steady` is your explicit confirmation of the drafts: `up` never touches IAM
|
|
37
|
+
and stops until the role ARNs are filled. Filling in an existing role ARN in
|
|
38
|
+
provision.json instead makes `steady` leave that role alone
|
|
39
|
+
(useful when IAM is managed by another team).
|
|
40
|
+
|
|
41
|
+
Files live under `.putpaws/provisioning/`:
|
|
42
|
+
|
|
43
|
+
- `provisioning/{service_name}/provision.json` ... inputs (edit by hand; `steady` fills the role ARNs)
|
|
44
|
+
- `provisioning/{service_name}/state.json` ... created resource info (written by putpaws)
|
|
45
|
+
- `provisioning/{service_name}/policies/` ... IAM role drafts to review, applied as-is by `putpaws steady`
|
|
46
|
+
- `provisioning/presets/{preset}/` ... defaults and task definition templates (copied here on `ready`, edit freely)
|
|
47
|
+
|
|
48
|
+
Presets are searched in this order: project local (`.putpaws/provisioning/presets/`),
|
|
49
|
+
user global (directories in `PUTPAWS_PRESETS_PATH`, then `~/.putpaws/presets/`), and the ones bundled in the gem.
|
|
50
|
+
Whichever you pick on `ready` is copied into the project so the project owns its snapshot.
|
|
51
|
+
|
|
52
|
+
Security groups are either specified (one or more existing ones; putpaws never touches their rules)
|
|
53
|
+
or a new one is created for the service when left empty on `ready`.
|
|
54
|
+
|
|
55
|
+
When the service step completes, the new service is reflected into
|
|
56
|
+
`.putpaws/application.json` and `.putpaws/infra.json` (diff is shown and confirmed),
|
|
57
|
+
so `ecs:attach`, `ecs:shell`, `ecs:run` and `log:*` work immediately.
|
|
58
|
+
Secrets are handled by reference only: task definitions point to existing
|
|
59
|
+
SSM parameters (Ex: `/{service_name}/RAILS_MASTER_KEY`) and putpaws never touches the values.
|
|
60
|
+
|
|
61
|
+
A scheduler role is always created too, and the `target` entry of infra.json carries it,
|
|
62
|
+
so `scheduler:deploy` works any time by just writing `.putpaws/schedule.json`.
|
|
63
|
+
|
|
64
|
+
CodeBuild is created as CI: builds run inside the VPC with the same subnets and
|
|
65
|
+
security group as the service, so `db:migrate` against RDS works from the build.
|
|
66
|
+
Note that the private subnets need a NAT gateway (or VPC endpoints) so that
|
|
67
|
+
builds can reach GitHub / ECR / CloudWatch Logs.
|
|
68
|
+
|
|
69
|
+
## Operator permissions
|
|
70
|
+
|
|
71
|
+
Declare named operator profiles in `.putpaws/operators.json` (generated with examples on first run),
|
|
72
|
+
then resolve them into managed policies per service, idempotently
|
|
73
|
+
(CREATE / UPDATE as a new policy version / SKIP — re-running never piles up policies):
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
bundle exec putpaws awesome-api-staging iam:grant profile=developer
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
{
|
|
81
|
+
"developer": { "groups": ["attach", "shell", "deploy", "logs", "codebuild"] },
|
|
82
|
+
"viewer": { "groups": ["logs"] }
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Groups map to putpaws command namespaces: `attach` (ecs:attach/forward), `shell` (ecs:shell/run —
|
|
87
|
+
note this implies reading all secrets of the service), `deploy` (ecs:deploy), `logs` (log:*),
|
|
88
|
+
`codebuild` (code_build:build), `scheduler` (scheduler:deploy).
|
|
89
|
+
`extra_statements` on a profile appends raw IAM statements.
|
|
90
|
+
The policy is named `{service}-operator-{profile}`. Attaching it to users/groups is left to your admin
|
|
91
|
+
(an attach command example is printed).
|
|
92
|
+
|
|
93
|
+
## AI setup
|
|
94
|
+
|
|
95
|
+
`putpaws away` installs a guide for AI agents (Claude Code etc.) into the project:
|
|
96
|
+
`.claude/skills/putpaws/SKILL.md` (commands, read-only vs mutating classification,
|
|
97
|
+
non-interactive recipes, suggested permission allowlist) and a pointer block in
|
|
98
|
+
`CLAUDE.md` (or `AGENTS.md` when there is no CLAUDE.md). Re-run it after updating putpaws.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
bundle exec putpaws away
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Example
|
|
105
|
+
|
|
106
|
+
### Info
|
|
107
|
+
|
|
108
|
+
Show resolved settings for a service without touching AWS
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
bundle exec putpaws awesome-api-staging info
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### ECS
|
|
115
|
+
|
|
116
|
+
Attach to staging specific container
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
bundle exec putpaws awesome-api-staging ecs:attach container=app
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Run port forwarding session through specific container
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
bundle exec putpaws awesome-api-staging ecs:forward container=app remote=example-rds-host:3306 local=:1050
|
|
126
|
+
# local=:1050 is optional by the way, then random number is selected.
|
|
127
|
+
# Please check standard output in your shell for the auto-generated local port.
|
|
128
|
+
|
|
129
|
+
# You can access specified remote host with subsequent command in another shell like:
|
|
130
|
+
mysql -u awesome_user -p --port 1050 -h 127.0.0.1
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Redeploy the service (update-service with force new deployment).
|
|
134
|
+
With a mutable image tag like `latest`, this rolls out the newly pushed image.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
bundle exec putpaws awesome-api-staging ecs:deploy
|
|
138
|
+
|
|
139
|
+
# Optionally change desired count / task definition, and wait until stable
|
|
140
|
+
bundle exec putpaws awesome-api-staging ecs:deploy desired=2 taskdef=awesome-api-staging-web wait=true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Run a command on a temporary task. The task terminates itself when the command finishes.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
bundle exec putpaws awesome-api-staging ecs:run cmd='bundle exec rake db:migrate'
|
|
147
|
+
|
|
148
|
+
# Pass wait=true to wait until the task stops and check the exit code
|
|
149
|
+
bundle exec putpaws awesome-api-staging ecs:run cmd='bundle exec rake db:migrate' wait=true
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Launch a temporary task for operation and attach to it (like SSH-ing into the environment).
|
|
153
|
+
The task is launched with its command overridden by `sleep <ttl>`, so it always terminates itself when ttl passes even if your shell is gone.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
# ttl is 30 minutes by default
|
|
157
|
+
bundle exec putpaws awesome-api-staging ecs:shell
|
|
158
|
+
|
|
159
|
+
# Specify ttl: "45" and "45m" mean 45 minutes, "90s" and "2h" also work
|
|
160
|
+
bundle exec putpaws awesome-api-staging ecs:shell ttl=45m
|
|
161
|
+
|
|
162
|
+
# The task is stopped immediately when you exit the shell.
|
|
163
|
+
# Pass keep=true to keep it running until ttl passes.
|
|
164
|
+
bundle exec putpaws awesome-api-staging ecs:shell keep=true
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Temporary tasks are launched with `group` / `started_by` set to `putpaws-run` or `putpaws-shell`,
|
|
168
|
+
so you can identify them on the AWS console and on the task list of `ecs:attach` / `ecs:forward`.
|
|
169
|
+
|
|
170
|
+
`ecs:run` and `ecs:shell` require `network` and `target` in `.putpaws/application.json`,
|
|
171
|
+
which refer to the `network` / `target` sections of `.putpaws/infra.json` (the same ones used by scheduler).
|
|
172
|
+
|
|
173
|
+
### CloudWatch Logs
|
|
174
|
+
|
|
175
|
+
`tail -f`
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
bundle exec putpaws awesome-api-staging log:tailf
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Find logs between specific date range using time symbol
|
|
182
|
+
|
|
183
|
+
- `s`: second
|
|
184
|
+
- `m`: minute
|
|
185
|
+
- `h`: hour
|
|
186
|
+
- `d`: day
|
|
187
|
+
- `w`: week
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
# Find logs since 2 hours ago
|
|
191
|
+
bundle exec putpaws awesome-api-staging log:tailf since=2h
|
|
192
|
+
|
|
193
|
+
# Find logs since 1 day ago for 3 hours
|
|
194
|
+
bundle exec putpaws awesome-api-staging log:tail since=1d for=3h
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Set up
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
gem 'putpaws'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Setting Example
|
|
204
|
+
|
|
205
|
+
`.putpaws/application.json`
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
{
|
|
209
|
+
"awesome-api-staging": {
|
|
210
|
+
"region": "ap-northeast-1",
|
|
211
|
+
"cluster": "cluster-staging",
|
|
212
|
+
"service": null,
|
|
213
|
+
"task_name_prefix": "awesome-api",
|
|
214
|
+
"log_group_prefix": "/ecs/awesome/awesome-api-staging",
|
|
215
|
+
"log_region": null,
|
|
216
|
+
"build_log_group_prefix": "/aws/codebuild/awesome-api-staging",
|
|
217
|
+
"build_project_name_prefix": "awesome-api-staging",
|
|
218
|
+
"network": "awesome-private-staging",
|
|
219
|
+
"target": "awesome-staging"
|
|
220
|
+
},
|
|
221
|
+
"awesome-api-production": {
|
|
222
|
+
"region": "ap-northeast-1",
|
|
223
|
+
"cluster": "cluster-production",
|
|
224
|
+
"service": null,
|
|
225
|
+
"task_name_prefix": "awesome-api",
|
|
226
|
+
"log_group_prefix": "/ecs/awesome/awesome-api-production",
|
|
227
|
+
"log_region": null
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
data/lib/Putpawsfile
CHANGED
|
@@ -4,4 +4,9 @@ require "putpaws/setup"
|
|
|
4
4
|
require "putpaws/hello"
|
|
5
5
|
require "putpaws/ecs/ecs_task"
|
|
6
6
|
require "putpaws/cloud_watch/log_task"
|
|
7
|
-
require "putpaws/scheduler/scheduler_task"
|
|
7
|
+
require "putpaws/scheduler/scheduler_task"
|
|
8
|
+
require "putpaws/code_build/build_task"
|
|
9
|
+
require "putpaws/provision/provision_task"
|
|
10
|
+
require "putpaws/iam/iam_task"
|
|
11
|
+
require "putpaws/info"
|
|
12
|
+
require "putpaws/ai/ai_task"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
load File.expand_path("../tasks/ai_task.rake", __FILE__)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'putpaws/version'
|
|
3
|
+
|
|
4
|
+
module Putpaws
|
|
5
|
+
module Ai
|
|
6
|
+
# `putpaws away`: put paws away — take your paws off and let AI take over.
|
|
7
|
+
# Installs a guide for AI agents (Claude Code etc.) into the host project:
|
|
8
|
+
# a skill file plus a pointer block in CLAUDE.md / AGENTS.md.
|
|
9
|
+
# Re-runnable: everything is regenerated in place.
|
|
10
|
+
class Guide
|
|
11
|
+
SKILL_PATH = File.join('.claude', 'skills', 'putpaws', 'SKILL.md')
|
|
12
|
+
BEGIN_MARK = '<!-- putpaws:begin -->'
|
|
13
|
+
END_MARK = '<!-- putpaws:end -->'
|
|
14
|
+
|
|
15
|
+
def self.install!(root: '.', services: nil)
|
|
16
|
+
services ||= Putpaws::ApplicationConfig.all.map(&:name)
|
|
17
|
+
skill_path = File.join(root, SKILL_PATH)
|
|
18
|
+
FileUtils.mkdir_p(File.dirname(skill_path))
|
|
19
|
+
File.write(skill_path, skill_content(services: services))
|
|
20
|
+
pointer_path = pointer_target(root)
|
|
21
|
+
update_pointer!(pointer_path)
|
|
22
|
+
[skill_path, pointer_path]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Prefer an existing CLAUDE.md; otherwise use AGENTS.md (created if absent).
|
|
26
|
+
def self.pointer_target(root)
|
|
27
|
+
claude = File.join(root, 'CLAUDE.md')
|
|
28
|
+
return claude if File.exist?(claude)
|
|
29
|
+
File.join(root, 'AGENTS.md')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.update_pointer!(path)
|
|
33
|
+
block = pointer_block
|
|
34
|
+
content = File.exist?(path) ? File.read(path) : ''
|
|
35
|
+
if content.include?(BEGIN_MARK)
|
|
36
|
+
content = content.sub(/#{Regexp.escape(BEGIN_MARK)}.*#{Regexp.escape(END_MARK)}/m, block.strip)
|
|
37
|
+
else
|
|
38
|
+
content = content.empty? ? block : content.rstrip + "\n\n" + block
|
|
39
|
+
end
|
|
40
|
+
File.write(path, content)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.pointer_block
|
|
44
|
+
<<~MD
|
|
45
|
+
#{BEGIN_MARK}
|
|
46
|
+
## putpaws (AWS operations)
|
|
47
|
+
|
|
48
|
+
This project uses the putpaws gem for AWS/ECS operations (logs, deploy, migrations, one-off tasks).
|
|
49
|
+
Read `.claude/skills/putpaws/SKILL.md` for commands and safety notes before running AWS operations.
|
|
50
|
+
Quick reference: services are the top-level keys of `.putpaws/application.json`;
|
|
51
|
+
`bundle exec putpaws <service> info` shows resolved settings without touching AWS.
|
|
52
|
+
#{END_MARK}
|
|
53
|
+
MD
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.skill_content(services: [])
|
|
57
|
+
services_line = services.empty? ? '(none configured yet — check .putpaws/application.json)' : services.join(', ')
|
|
58
|
+
<<~MD
|
|
59
|
+
---
|
|
60
|
+
name: putpaws
|
|
61
|
+
description: AWS operations for this project (CloudWatch logs, ECS deploy, db:migrate and one-off tasks on Fargate, CodeBuild CI) via the putpaws CLI. Use when asked to investigate logs, deploy, run rake tasks on AWS, or inspect ECS services.
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
# putpaws
|
|
65
|
+
|
|
66
|
+
Capistrano-style AWS operation commands. General form:
|
|
67
|
+
|
|
68
|
+
bundle exec putpaws <service> <command> key=value
|
|
69
|
+
|
|
70
|
+
Services in this project: #{services_line}
|
|
71
|
+
(services are the top-level keys of `.putpaws/application.json`)
|
|
72
|
+
|
|
73
|
+
Discover commands with `bundle exec putpaws -T` (or `-D` for full descriptions).
|
|
74
|
+
|
|
75
|
+
## Read-only commands (safe to run without asking)
|
|
76
|
+
|
|
77
|
+
- `bundle exec putpaws <service> info` — resolved settings, no AWS access
|
|
78
|
+
- `bundle exec putpaws <service> log:tail since=2h` — CloudWatch logs (units: s/m/h/d/w, add `for=1h` to bound the range)
|
|
79
|
+
- `bundle exec putpaws <service> log:tailf` — follow logs (long-running; prefer `log:tail` in automation)
|
|
80
|
+
- `bundle exec putpaws ahead` — dry-run of provisioning (CREATE/UPDATE/SKIP)
|
|
81
|
+
|
|
82
|
+
## Mutating commands (confirm with the user before running)
|
|
83
|
+
|
|
84
|
+
- `bundle exec putpaws <service> ecs:run cmd='bundle exec rake db:migrate' wait=true`
|
|
85
|
+
— runs a one-off command on a temporary Fargate task and exits non-zero on failure.
|
|
86
|
+
The best choice for migrations and rake tasks. Fully non-interactive.
|
|
87
|
+
- `bundle exec putpaws <service> ecs:deploy wait=true` — redeploy the service (force new deployment)
|
|
88
|
+
- `bundle exec putpaws <service> code_build:build branch=main` — start the CI build (build + push + deploy per buildspec)
|
|
89
|
+
- `bundle exec putpaws <service> scheduler:deploy` — deploy schedules from `.putpaws/schedule.json`
|
|
90
|
+
|
|
91
|
+
## Interactive commands (avoid in non-TTY sessions; meant for humans)
|
|
92
|
+
|
|
93
|
+
- `ecs:attach`, `ecs:shell`, `ecs:forward` — open live sessions into containers
|
|
94
|
+
- `ready`, `steady`, `up`, `iam:grant` — provisioning flows with confirmation prompts
|
|
95
|
+
|
|
96
|
+
## Config files (source of truth, safe to read)
|
|
97
|
+
|
|
98
|
+
- `.putpaws/application.json` — services and their cluster/log/build settings
|
|
99
|
+
- `.putpaws/infra.json` — network (subnets/security groups) and target (cluster/taskdef) definitions
|
|
100
|
+
- `.putpaws/schedule.json` — named schedules, `.putpaws/operators.json` — operator permission profiles
|
|
101
|
+
- `.putpaws/provisioning/<service>/` — provisioning inputs (provision.json), state, IAM role drafts
|
|
102
|
+
|
|
103
|
+
## Suggested permission allowlist (.claude/settings.json)
|
|
104
|
+
|
|
105
|
+
"permissions": {
|
|
106
|
+
"allow": [
|
|
107
|
+
"Bash(bundle exec putpaws -T*)",
|
|
108
|
+
"Bash(bundle exec putpaws * info)",
|
|
109
|
+
"Bash(bundle exec putpaws * log:*)"
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
Generated by putpaws #{Putpaws::VERSION}. Refresh with `bundle exec putpaws away` after updating the gem.
|
|
115
|
+
MD
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "putpaws/ai/guide"
|
|
2
|
+
|
|
3
|
+
namespace :ai do
|
|
4
|
+
desc "Install the AI agent guide (.claude/skills/putpaws/SKILL.md + a pointer in CLAUDE.md/AGENTS.md). Re-runnable. (alias: away)"
|
|
5
|
+
task :install do
|
|
6
|
+
files = Putpaws::Ai::Guide.install!
|
|
7
|
+
puts "Generated:"
|
|
8
|
+
files.each{|f| puts " #{f}"}
|
|
9
|
+
puts "Re-run `bundle exec putpaws away` after updating putpaws to refresh the guide."
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "Alias of ai:install"
|
|
14
|
+
task away: 'ai:install'
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'pathname'
|
|
3
3
|
require "putpaws/schedule_config"
|
|
4
|
+
require "putpaws/infra_network_config"
|
|
5
|
+
require "putpaws/infra_target_config"
|
|
4
6
|
|
|
5
7
|
class Putpaws::ApplicationConfig < Struct.new(
|
|
6
|
-
:name, :region,
|
|
8
|
+
:name, :region,
|
|
7
9
|
:cluster, :service, :task_name_prefix, :ecs_region,
|
|
8
10
|
:log_group_prefix, :log_region,
|
|
9
|
-
:
|
|
11
|
+
:build_project_name_prefix, :build_log_group_prefix, :build_region,
|
|
10
12
|
:schedules,
|
|
13
|
+
:network, :target,
|
|
11
14
|
keyword_init: true)
|
|
12
15
|
def self.load(path_prefix: '.putpaws')
|
|
13
16
|
@application_data ||= begin
|
|
@@ -37,7 +40,7 @@ class Putpaws::ApplicationConfig < Struct.new(
|
|
|
37
40
|
{
|
|
38
41
|
region: ecs_region || region,
|
|
39
42
|
cluster: cluster,
|
|
40
|
-
|
|
43
|
+
service: service,
|
|
41
44
|
task_name_prefix: task_name_prefix,
|
|
42
45
|
}
|
|
43
46
|
end
|
|
@@ -59,4 +62,21 @@ class Putpaws::ApplicationConfig < Struct.new(
|
|
|
59
62
|
def schedules
|
|
60
63
|
@schedules ||= (self[:schedules] || []).map{|x| Putpaws::ScheduleConfig.find(x)}.compact
|
|
61
64
|
end
|
|
65
|
+
|
|
66
|
+
def network
|
|
67
|
+
return nil unless self[:network]
|
|
68
|
+
@network ||= Putpaws::InfraNetworkConfig.find(self[:network])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def target
|
|
72
|
+
return nil unless self[:target]
|
|
73
|
+
@target ||= Putpaws::InfraTargetConfig.find(self[:target])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def codebuild_command_params
|
|
77
|
+
{
|
|
78
|
+
region: build_region || region,
|
|
79
|
+
project_name_prefix: build_project_name_prefix,
|
|
80
|
+
}
|
|
81
|
+
end
|
|
62
82
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
load File.expand_path("../tasks/build_task.rake", __FILE__)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'aws-sdk-codebuild'
|
|
2
|
+
# require 'aws-sdk-ssm'
|
|
3
|
+
|
|
4
|
+
module Putpaws::CodeBuild
|
|
5
|
+
class ProjectCommand
|
|
6
|
+
def self.config(config)
|
|
7
|
+
new(**config.codebuild_command_params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :codebuild_client
|
|
11
|
+
attr_reader :region, :project_name_prefix
|
|
12
|
+
attr_accessor :codebuild_project
|
|
13
|
+
def initialize(region:, project_name_prefix: nil)
|
|
14
|
+
@codebuild_client = Aws::CodeBuild::Client.new({region: region})
|
|
15
|
+
@region = region
|
|
16
|
+
@project_name_prefix = project_name_prefix
|
|
17
|
+
@codebuild_project = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def list_codebuild_projects
|
|
21
|
+
res = codebuild_client.batch_get_projects(names: [project_name_prefix])
|
|
22
|
+
res.projects.select{|pr| pr.name.start_with?(project_name_prefix)}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start_build_with_specific_version(source_version:, environment_variables: {}, buildspec_override: nil, artifacts_override: nil)
|
|
26
|
+
current_variables = codebuild_project.environment.environment_variables.map{|x| [x.name, {type: x.type, value: x.value}]}.to_h
|
|
27
|
+
new_variables = environment_variables.reduce(current_variables) {|acc,(k,v)| acc[k] = {type: 'PLANTTEXT', value: v}}
|
|
28
|
+
environment_variables_override = new_variables.map{|k,v| v.merge(name: k)}
|
|
29
|
+
params = {
|
|
30
|
+
project_name: codebuild_project.name,
|
|
31
|
+
source_version: source_version,
|
|
32
|
+
environment_variables_override: environment_variables_override,
|
|
33
|
+
}
|
|
34
|
+
if buildspec_override
|
|
35
|
+
params = params.merge(buildspec_override: buildspec_override)
|
|
36
|
+
end
|
|
37
|
+
if artifacts_override
|
|
38
|
+
params = params.merge(artifacts_override: artifacts_override)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
codebuild_client.start_build(**params)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_source_version(type:, version:)
|
|
45
|
+
type = type.to_sym
|
|
46
|
+
if type == :branch
|
|
47
|
+
"refs/heads/#{version}"
|
|
48
|
+
elsif type == :tag
|
|
49
|
+
"refs/tags/#{version}"
|
|
50
|
+
elsif type == :commit_id
|
|
51
|
+
version
|
|
52
|
+
else
|
|
53
|
+
raise "Please specify valid type: branch or tag or commit_id"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "putpaws/prompt"
|
|
2
|
+
require "putpaws/code_build/project_command"
|
|
3
|
+
|
|
4
|
+
namespace :code_build do
|
|
5
|
+
desc "Set CodeBuild project."
|
|
6
|
+
task :set_project do
|
|
7
|
+
aws = Putpaws::CodeBuild::ProjectCommand.config(fetch(:app))
|
|
8
|
+
codebuild_projects = aws.list_codebuild_projects.map{|x|
|
|
9
|
+
[x.name, x]
|
|
10
|
+
}.to_h
|
|
11
|
+
prompt = Putpaws::Prompt.safe
|
|
12
|
+
selected = prompt.select("Choose a project you're going to operate", codebuild_projects.keys)
|
|
13
|
+
codebuild_project = codebuild_projects[selected]
|
|
14
|
+
|
|
15
|
+
set :codebuild_project, codebuild_project
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Start build."
|
|
19
|
+
task build: :set_project do
|
|
20
|
+
aws = Putpaws::CodeBuild::ProjectCommand.config(fetch(:app))
|
|
21
|
+
|
|
22
|
+
source_version = if ENV['branch']
|
|
23
|
+
aws.build_source_version(type: :branch, version: ENV['branch'])
|
|
24
|
+
elsif ENV['tag']
|
|
25
|
+
aws.build_source_version(type: :tag, version: ENV['tag'])
|
|
26
|
+
elsif ENV['commit_id']
|
|
27
|
+
aws.build_source_version(type: :commit_id, version: ENV['commit_id'])
|
|
28
|
+
else
|
|
29
|
+
raise "Please specify required parameter like: branch=main or tag=release/20240601 or commit_id=0f0f0f0f"
|
|
30
|
+
end
|
|
31
|
+
codebuild_project = fetch(:codebuild_project)
|
|
32
|
+
aws.codebuild_project = codebuild_project
|
|
33
|
+
aws.start_build_with_specific_version(
|
|
34
|
+
source_version: source_version,
|
|
35
|
+
environment_variables: ENV['env'] ? JSON.parse(ENV['env']) : {},
|
|
36
|
+
buildspec_override: ENV['buildspec']
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'aws-sdk-ecs'
|
|
2
|
+
|
|
3
|
+
module Putpaws::Ecs
|
|
4
|
+
class RunCommand
|
|
5
|
+
DEFAULT_TTL = '30m'
|
|
6
|
+
SECONDS = {
|
|
7
|
+
's' => 1,
|
|
8
|
+
'm' => 60,
|
|
9
|
+
'h' => 60 * 60,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def self.config(config)
|
|
13
|
+
unless config.network && config.target
|
|
14
|
+
raise "Please set `network` and `target` at .putpaws/application.json\n" +
|
|
15
|
+
"They refer to `network` and `target` sections at .putpaws/infra.json\n" +
|
|
16
|
+
"(Ex) \"network\": \"awesome-private-staging\", \"target\": \"awesome-staging\""
|
|
17
|
+
end
|
|
18
|
+
new(
|
|
19
|
+
region: config.ecs_region || config.region,
|
|
20
|
+
network: config.network,
|
|
21
|
+
target: config.target,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# "45" and "45m" mean 45 minutes. "90s" and "2h" are also acceptable.
|
|
26
|
+
def self.parse_ttl(ut)
|
|
27
|
+
ut = DEFAULT_TTL if ut.nil? || ut.strip.empty?
|
|
28
|
+
matched, number, unit = ut.strip.match(/\A(\d+)([smh])?\z/).to_a
|
|
29
|
+
raise "Invalid ttl: #{ut} (Ex) ttl=30m ttl=2h ttl=90s" unless matched
|
|
30
|
+
number.to_i * SECONDS[unit || 'm']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :ecs_client
|
|
34
|
+
attr_reader :region, :network, :target
|
|
35
|
+
def initialize(region:, network:, target:)
|
|
36
|
+
@ecs_client = Aws::ECS::Client.new({region: region})
|
|
37
|
+
@region = region
|
|
38
|
+
@network = network
|
|
39
|
+
@target = target
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cluster_name
|
|
43
|
+
target.cluster.split('/').last
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def container_name
|
|
47
|
+
target.container_name || 'app'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Launch a temporary task whose main process is overridden with the given
|
|
51
|
+
# command, so that the task terminates itself when the command finishes.
|
|
52
|
+
# started_by and group are set so that temporary tasks are identifiable.
|
|
53
|
+
def run_ecs_task(command:, started_by:, group:, container: nil)
|
|
54
|
+
res = ecs_client.run_task({
|
|
55
|
+
cluster: target.cluster,
|
|
56
|
+
task_definition: target.task_definition,
|
|
57
|
+
count: 1,
|
|
58
|
+
launch_type: 'FARGATE',
|
|
59
|
+
enable_execute_command: true,
|
|
60
|
+
started_by: started_by,
|
|
61
|
+
group: group,
|
|
62
|
+
network_configuration: {
|
|
63
|
+
awsvpc_configuration: {
|
|
64
|
+
subnets: network.subnets,
|
|
65
|
+
security_groups: network.security_groups,
|
|
66
|
+
assign_public_ip: network.assign_public_ip,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
overrides: {
|
|
70
|
+
container_overrides: [
|
|
71
|
+
{
|
|
72
|
+
name: container || container_name,
|
|
73
|
+
command: ['/bin/sh', '-c', command],
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
failure = res.failures.first
|
|
79
|
+
raise "Failed to run task: #{failure.reason} #{failure.detail}" if failure
|
|
80
|
+
res.tasks.first
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def wait_until_attachable(task_arn, timeout: 600, interval: 5)
|
|
84
|
+
deadline = Time.now + timeout
|
|
85
|
+
loop do
|
|
86
|
+
res = ecs_client.describe_tasks(cluster: target.cluster, tasks: [task_arn])
|
|
87
|
+
task = res.tasks.first
|
|
88
|
+
if task.nil? || task.last_status == 'STOPPED'
|
|
89
|
+
raise "Task was stopped: #{task && task.stopped_reason}"
|
|
90
|
+
end
|
|
91
|
+
ctn = task.containers.detect{|c| c.name == container_name}
|
|
92
|
+
agent = ctn && (ctn.managed_agents || []).detect{|a| a.name == 'ExecuteCommandAgent'}
|
|
93
|
+
return task if task.last_status == 'RUNNING' && agent && agent.last_status == 'RUNNING'
|
|
94
|
+
raise "Timed out waiting for task to be attachable: #{task_arn}" if Time.now > deadline
|
|
95
|
+
sleep interval
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def wait_until_stopped(task_arn, timeout: 3600, interval: 10)
|
|
100
|
+
deadline = Time.now + timeout
|
|
101
|
+
loop do
|
|
102
|
+
res = ecs_client.describe_tasks(cluster: target.cluster, tasks: [task_arn])
|
|
103
|
+
task = res.tasks.first
|
|
104
|
+
raise "Task not found: #{task_arn}" unless task
|
|
105
|
+
return task if task.last_status == 'STOPPED'
|
|
106
|
+
raise "Timed out waiting for task to stop: #{task_arn}" if Time.now > deadline
|
|
107
|
+
sleep interval
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def stop_ecs_task(task_arn, reason: 'Stopped by putpaws')
|
|
112
|
+
ecs_client.stop_task(cluster: target.cluster, task: task_arn, reason: reason)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|