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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +230 -0
  3. data/lib/Putpawsfile +6 -1
  4. data/lib/putpaws/ai/ai_task.rb +1 -0
  5. data/lib/putpaws/ai/guide.rb +119 -0
  6. data/lib/putpaws/ai/tasks/ai_task.rake +14 -0
  7. data/lib/putpaws/application_config.rb +23 -3
  8. data/lib/putpaws/code_build/build_task.rb +1 -0
  9. data/lib/putpaws/code_build/project_command.rb +57 -0
  10. data/lib/putpaws/code_build/tasks/build_task.rake +39 -0
  11. data/lib/putpaws/ecs/run_command.rb +115 -0
  12. data/lib/putpaws/ecs/task_command.rb +63 -9
  13. data/lib/putpaws/ecs/tasks/ecs_task.rake +113 -6
  14. data/lib/putpaws/iam/grant_command.rb +205 -0
  15. data/lib/putpaws/iam/iam_task.rb +1 -0
  16. data/lib/putpaws/iam/operator_config.rb +46 -0
  17. data/lib/putpaws/iam/tasks/iam_task.rake +65 -0
  18. data/lib/putpaws/info.rb +1 -0
  19. data/lib/putpaws/provision/all.rb +29 -0
  20. data/lib/putpaws/provision/aws_clients.rb +59 -0
  21. data/lib/putpaws/provision/config_writer.rb +88 -0
  22. data/lib/putpaws/provision/policy_generator.rb +221 -0
  23. data/lib/putpaws/provision/preset.rb +91 -0
  24. data/lib/putpaws/provision/presets/rails-nginx/preset.json +32 -0
  25. data/lib/putpaws/provision/presets/rails-nginx/templates/taskdef-app.json.erb +29 -0
  26. data/lib/putpaws/provision/presets/rails-nginx/templates/taskdef-web.json.erb +48 -0
  27. data/lib/putpaws/provision/provision_config.rb +156 -0
  28. data/lib/putpaws/provision/provision_task.rb +1 -0
  29. data/lib/putpaws/provision/resources/base.rb +49 -0
  30. data/lib/putpaws/provision/resources/cluster.rb +30 -0
  31. data/lib/putpaws/provision/resources/codebuild_project.rb +85 -0
  32. data/lib/putpaws/provision/resources/iam_role.rb +89 -0
  33. data/lib/putpaws/provision/resources/log_group.rb +50 -0
  34. data/lib/putpaws/provision/resources/security_group.rb +72 -0
  35. data/lib/putpaws/provision/resources/service.rb +71 -0
  36. data/lib/putpaws/provision/resources/task_definition.rb +119 -0
  37. data/lib/putpaws/provision/runner.rb +218 -0
  38. data/lib/putpaws/provision/state.rb +45 -0
  39. data/lib/putpaws/provision/tasks/provision.rake +129 -0
  40. data/lib/putpaws/provision/util.rb +50 -0
  41. data/lib/putpaws/tasks/info.rake +35 -0
  42. data/lib/putpaws/version.rb +1 -1
  43. metadata +80 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5383c31d8b2b94dcb948e565f9ab5784137a0b946aa53e2ade93fc5e1bf3152
4
- data.tar.gz: 30babace1c15c9675752567795e4c43414be155cc0f9affccfdb8bd8907a215d
3
+ metadata.gz: cbb1de2cdc7c119427ca5dfa073f5e8d07f504ff73f93a464f2912a8ef5168ed
4
+ data.tar.gz: 042f6202ae3cb0396b0ab7210b2d299504f7e099e8a0cf43d9c6314f0b6384ba
5
5
  SHA512:
6
- metadata.gz: f506cafed6700e2fc5bff6334e7e3374e528652740de99c03a36e89a031603b891b77133b5264fff51f8d1d3bc7db22f8549c88a32658f87883249c8cfe9d331
7
- data.tar.gz: 941ceaa13526fd71c3a37d5d6541490950472dea6bff75aff2b495acc10adb41f46b270e14a6c279d542863792594896443aeca882655321b856a745c38306e0
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
- :build_project, :build_log_group_prefix, :build_region,
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
- # service: service,
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