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
@@ -0,0 +1,88 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+ require 'putpaws/provision/util'
5
+
6
+ module Putpaws
7
+ module Provision
8
+ # Reflects provisioned resources into .putpaws/application.json and
9
+ # .putpaws/infra.json. Only the keys of the target service are touched.
10
+ # Shows a diff and asks for confirmation before writing.
11
+ class ConfigWriter
12
+ attr_reader :config, :state, :prompt, :io
13
+ def initialize(config:, state:, prompt:, io: $stdout)
14
+ @config = config
15
+ @state = state
16
+ @prompt = prompt
17
+ @io = io
18
+ end
19
+
20
+ def application_entry
21
+ {
22
+ region: config.region,
23
+ cluster: config.cluster_name,
24
+ service: config.service_name,
25
+ task_name_prefix: config.service_name,
26
+ log_group_prefix: config.log_group,
27
+ build_log_group_prefix: config.build_log_group,
28
+ build_project_name_prefix: config.build_project_name,
29
+ network: config.service_name,
30
+ target: config.service_name,
31
+ }
32
+ end
33
+
34
+ def network_entry
35
+ {
36
+ subnets: config.base[:subnets],
37
+ security_groups: state.resources[:security_group_ids] || [],
38
+ assign_public_ip: config.settings[:assign_public_ip] || 'DISABLED',
39
+ }
40
+ end
41
+
42
+ def target_entry
43
+ {
44
+ # Used by EventBridge Scheduler (scheduler:deploy).
45
+ scheduler_role: config.roles[:scheduler_role_arn],
46
+ cluster: state.resources[:cluster_arn],
47
+ # Revision-less on purpose: always run the latest ACTIVE revision.
48
+ task_definition: config.service_task_definition_family,
49
+ container_name: config.service_container_name,
50
+ }
51
+ end
52
+
53
+ def apply!
54
+ write_key(path('application.json'), [config.service_name.to_sym], application_entry)
55
+ write_key(path('infra.json'), [:network, config.service_name.to_sym], network_entry)
56
+ write_key(path('infra.json'), [:target, config.service_name.to_sym], target_entry)
57
+ end
58
+
59
+ private
60
+
61
+ def path(basename)
62
+ Pathname.new(config.path_prefix).join(basename)
63
+ end
64
+
65
+ def write_key(file, keys, entry)
66
+ data = file.exist? ? JSON.parse(File.read(file), symbolize_names: true) : {}
67
+ current = keys.reduce(data){|d, k| d.is_a?(Hash) ? d[k] : nil}
68
+ return if current == entry
69
+
70
+ io.puts "=== #{file} : #{keys.join('.')} ==="
71
+ io.puts "--- before"
72
+ io.puts current ? JSON.pretty_generate(current) : "(none)"
73
+ io.puts "+++ after"
74
+ io.puts JSON.pretty_generate(entry)
75
+ unless prompt.yes?("Update #{file}?")
76
+ io.puts "Skipped updating #{file}"
77
+ return
78
+ end
79
+
80
+ FileUtils.cp(file, "#{file}.bak") if file.exist?
81
+ parent = keys[0..-2].reduce(data){|d, k| d[k] ||= {}}
82
+ parent[keys.last] = entry
83
+ Util.write_json(file, data)
84
+ io.puts "Updated #{file}"
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,221 @@
1
+ require 'fileutils'
2
+ require 'putpaws/provision/util'
3
+
4
+ module Putpaws
5
+ module Provision
6
+ # Generates drafts (たたき台) of IAM policies and roles.
7
+ # These are starting points to review and edit by hand before creating.
8
+ class PolicyGenerator
9
+ ECS_TASKS_TRUST = {
10
+ Version: '2012-10-17',
11
+ Statement: [{Effect: 'Allow', Principal: {Service: 'ecs-tasks.amazonaws.com'}, Action: 'sts:AssumeRole'}]
12
+ }
13
+ CODEBUILD_TRUST = {
14
+ Version: '2012-10-17',
15
+ Statement: [{Effect: 'Allow', Principal: {Service: 'codebuild.amazonaws.com'}, Action: 'sts:AssumeRole'}]
16
+ }
17
+
18
+ def scheduler_trust
19
+ {
20
+ Version: '2012-10-17',
21
+ Statement: [{
22
+ Effect: 'Allow',
23
+ Principal: {Service: 'scheduler.amazonaws.com'},
24
+ Action: 'sts:AssumeRole',
25
+ # confused deputy protection: only schedules in this account
26
+ Condition: {StringEquals: {'aws:SourceAccount' => account_id}},
27
+ }]
28
+ }
29
+ end
30
+
31
+ attr_reader :config
32
+ def initialize(config)
33
+ @config = config
34
+ end
35
+
36
+ def service_name; config.service_name; end
37
+ def region; config.region; end
38
+ def account_id; config.account_id; end
39
+
40
+ def write_step1_drafts!
41
+ dir = config.policies_dir
42
+ FileUtils.mkdir_p(dir)
43
+ Util.write_json(dir.join('role-task-execution.json'), task_execution_role_draft)
44
+ Util.write_json(dir.join('role-task.json'), task_role_draft)
45
+ Util.write_json(dir.join('role-codebuild.json'), codebuild_role_draft)
46
+ Util.write_json(dir.join('role-scheduler.json'), scheduler_role_draft)
47
+ %w[role-task-execution.json role-task.json role-codebuild.json role-scheduler.json]
48
+ .map{|f| dir.join(f).to_s}
49
+ end
50
+
51
+ def task_execution_role_draft
52
+ statements = [
53
+ {
54
+ Sid: 'EcrAuth',
55
+ Effect: 'Allow',
56
+ Action: %w[ecr:GetAuthorizationToken],
57
+ Resource: '*',
58
+ },
59
+ {
60
+ Sid: 'EcrPull',
61
+ Effect: 'Allow',
62
+ Action: %w[ecr:BatchCheckLayerAvailability ecr:GetDownloadUrlForLayer ecr:BatchGetImage],
63
+ Resource: config.base[:ecr_repository_arn],
64
+ },
65
+ {
66
+ Sid: 'WriteLogs',
67
+ Effect: 'Allow',
68
+ Action: %w[logs:CreateLogStream logs:PutLogEvents],
69
+ Resource: "arn:aws:logs:#{region}:#{account_id}:log-group:#{config.log_group}:*",
70
+ },
71
+ ]
72
+ unless config.resolved_secrets.empty?
73
+ statements << {
74
+ Sid: 'ReadSecrets',
75
+ Effect: 'Allow',
76
+ Action: %w[ssm:GetParameters],
77
+ Resource: "arn:aws:ssm:#{region}:#{account_id}:parameter#{config.ssm_parameter_prefix}/*",
78
+ }
79
+ end
80
+ {
81
+ SuggestedRoleName: "#{service_name}-task-execution",
82
+ SuggestedPolicyName: "#{service_name}-task-execution-policy",
83
+ AssumeRolePolicyDocument: ECS_TASKS_TRUST,
84
+ PolicyDocument: {Version: '2012-10-17', Statement: statements},
85
+ }
86
+ end
87
+
88
+ def task_role_draft
89
+ statements = [
90
+ {
91
+ Sid: 'EcsExec',
92
+ Effect: 'Allow',
93
+ Action: %w[
94
+ ssmmessages:CreateControlChannel ssmmessages:CreateDataChannel
95
+ ssmmessages:OpenControlChannel ssmmessages:OpenDataChannel
96
+ ],
97
+ Resource: '*',
98
+ },
99
+ ]
100
+ unless Util.blank?(config.base[:ses_identity_arn])
101
+ statements << {
102
+ Sid: 'SendMail',
103
+ Effect: 'Allow',
104
+ Action: %w[ses:SendEmail ses:SendRawEmail],
105
+ Resource: config.base[:ses_identity_arn],
106
+ }
107
+ end
108
+ unless Util.blank?(config.base[:s3_bucket_arn])
109
+ statements << {
110
+ Sid: 'UseBucket',
111
+ Effect: 'Allow',
112
+ Action: %w[s3:GetObject s3:PutObject s3:DeleteObject s3:ListBucket],
113
+ Resource: [config.base[:s3_bucket_arn], "#{config.base[:s3_bucket_arn]}/*"],
114
+ }
115
+ end
116
+ {
117
+ SuggestedRoleName: "#{service_name}-task",
118
+ SuggestedPolicyName: "#{service_name}-task-policy",
119
+ AssumeRolePolicyDocument: ECS_TASKS_TRUST,
120
+ PolicyDocument: {Version: '2012-10-17', Statement: statements},
121
+ }
122
+ end
123
+
124
+ def codebuild_role_draft
125
+ {
126
+ SuggestedRoleName: "#{service_name}-codebuild",
127
+ SuggestedPolicyName: "#{service_name}-codebuild-policy",
128
+ AssumeRolePolicyDocument: CODEBUILD_TRUST,
129
+ PolicyDocument: {
130
+ Version: '2012-10-17',
131
+ Statement: [
132
+ {
133
+ Sid: 'WriteBuildLogs',
134
+ Effect: 'Allow',
135
+ Action: %w[logs:CreateLogStream logs:PutLogEvents],
136
+ Resource: "arn:aws:logs:#{region}:#{account_id}:log-group:#{config.build_log_group}:*",
137
+ },
138
+ {
139
+ Sid: 'EcrAuth',
140
+ Effect: 'Allow',
141
+ Action: %w[ecr:GetAuthorizationToken],
142
+ Resource: '*',
143
+ },
144
+ {
145
+ Sid: 'EcrPushPull',
146
+ Effect: 'Allow',
147
+ Action: %w[
148
+ ecr:BatchCheckLayerAvailability ecr:GetDownloadUrlForLayer ecr:BatchGetImage
149
+ ecr:InitiateLayerUpload ecr:UploadLayerPart ecr:CompleteLayerUpload ecr:PutImage
150
+ ],
151
+ Resource: config.base[:ecr_repository_arn],
152
+ },
153
+ # Deploy at the end of the build:
154
+ # aws ecs update-service --force-new-deployment (+ wait services-stable)
155
+ {
156
+ Sid: 'DeployService',
157
+ Effect: 'Allow',
158
+ Action: %w[ecs:UpdateService ecs:DescribeServices],
159
+ Resource: "arn:aws:ecs:#{region}:#{account_id}:service/#{config.cluster_name}/#{service_name}",
160
+ },
161
+ # Required because builds run inside the VPC (vpc_config).
162
+ {
163
+ Sid: 'ManageBuildEni',
164
+ Effect: 'Allow',
165
+ Action: %w[
166
+ ec2:CreateNetworkInterface ec2:DescribeNetworkInterfaces ec2:DeleteNetworkInterface
167
+ ec2:DescribeSubnets ec2:DescribeSecurityGroups ec2:DescribeDhcpOptions ec2:DescribeVpcs
168
+ ],
169
+ Resource: '*',
170
+ },
171
+ {
172
+ Sid: 'CreateBuildEniPermission',
173
+ Effect: 'Allow',
174
+ Action: %w[ec2:CreateNetworkInterfacePermission],
175
+ Resource: "arn:aws:ec2:#{region}:#{account_id}:network-interface/*",
176
+ Condition: {
177
+ StringEquals: {
178
+ 'ec2:AuthorizedService' => 'codebuild.amazonaws.com',
179
+ 'ec2:Subnet' => (config.base[:subnets] || []).map{|s|
180
+ "arn:aws:ec2:#{region}:#{account_id}:subnet/#{s}"
181
+ },
182
+ }
183
+ },
184
+ },
185
+ ]
186
+ },
187
+ }
188
+ end
189
+
190
+ # Assumed by EventBridge Scheduler to launch tasks (scheduler:deploy).
191
+ # Created for every service so that schedules can be added any time.
192
+ def scheduler_role_draft
193
+ {
194
+ SuggestedRoleName: "#{service_name}-scheduler",
195
+ SuggestedPolicyName: "#{service_name}-scheduler-policy",
196
+ AssumeRolePolicyDocument: scheduler_trust,
197
+ PolicyDocument: {
198
+ Version: '2012-10-17',
199
+ Statement: [
200
+ {
201
+ Sid: 'RunScheduledTask',
202
+ Effect: 'Allow',
203
+ Action: %w[ecs:RunTask],
204
+ Resource: [
205
+ "arn:aws:ecs:#{region}:#{account_id}:task-definition/#{service_name}-*",
206
+ "arn:aws:ecs:#{region}:#{account_id}:task-definition/#{service_name}-*:*",
207
+ ],
208
+ },
209
+ {
210
+ Sid: 'PassRolesToTasks',
211
+ Effect: 'Allow',
212
+ Action: %w[iam:PassRole],
213
+ Resource: "arn:aws:iam::#{account_id}:role/#{service_name}-*",
214
+ },
215
+ ]
216
+ },
217
+ }
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,91 @@
1
+ require 'json'
2
+ require 'pathname'
3
+ require 'fileutils'
4
+
5
+ module Putpaws
6
+ module Provision
7
+ class Preset
8
+ BUILTIN_DIR = File.expand_path('presets', __dir__)
9
+
10
+ def self.local_dir(path_prefix: '.putpaws')
11
+ Pathname.new(path_prefix).join('provisioning', 'presets')
12
+ end
13
+
14
+ # Search order: project local -> user global (PUTPAWS_PRESETS_PATH dirs,
15
+ # then ~/.putpaws/presets) -> builtin (bundled in the gem).
16
+ def self.search_paths(path_prefix: '.putpaws')
17
+ paths = [[local_dir(path_prefix: path_prefix).to_s, 'local']]
18
+ ENV['PUTPAWS_PRESETS_PATH'].to_s.split(File::PATH_SEPARATOR).reject(&:empty?).each do |dir|
19
+ paths << [dir, 'global']
20
+ end
21
+ home = ENV['HOME'].to_s
22
+ paths << [File.join(home, '.putpaws', 'presets'), 'global'] unless home.empty?
23
+ paths << [BUILTIN_DIR, 'builtin']
24
+ paths
25
+ end
26
+
27
+ # [{name:, dir:, source:}] with name collisions resolved by search order.
28
+ def self.catalog(path_prefix: '.putpaws')
29
+ seen = {}
30
+ search_paths(path_prefix: path_prefix).each do |dir, source|
31
+ Dir.glob(File.join(dir, '*/preset.json')).sort.each do |p|
32
+ name = File.basename(File.dirname(p))
33
+ seen[name] ||= {name: name, dir: File.dirname(p), source: source}
34
+ end
35
+ end
36
+ seen.values.sort_by{|e| e[:name]}
37
+ end
38
+
39
+ def self.available(path_prefix: '.putpaws')
40
+ catalog(path_prefix: path_prefix).map{|e| e[:name]}
41
+ end
42
+
43
+ def self.find(name, path_prefix: '.putpaws')
44
+ entry = catalog(path_prefix: path_prefix).detect{|e| e[:name] == name}
45
+ entry && new(name: entry[:name], dir: entry[:dir])
46
+ end
47
+
48
+ # Copy a preset into .putpaws so that the project owns its snapshot and
49
+ # users can edit it. Does nothing when the local preset already exists.
50
+ def self.install(name, path_prefix: '.putpaws')
51
+ entry = catalog(path_prefix: path_prefix).detect{|e| e[:name] == name}
52
+ raise "Unknown preset: #{name}" unless entry
53
+ return new(name: entry[:name], dir: entry[:dir]) if entry[:source] == 'local'
54
+ local = local_dir(path_prefix: path_prefix).join(name)
55
+ FileUtils.mkdir_p(local.dirname)
56
+ FileUtils.cp_r(entry[:dir], local)
57
+ find(name, path_prefix: path_prefix)
58
+ end
59
+
60
+ attr_reader :name, :dir
61
+ def initialize(name:, dir:)
62
+ @name = name
63
+ @dir = dir
64
+ end
65
+
66
+ def data
67
+ @data ||= JSON.parse(File.read(File.join(dir, 'preset.json')), symbolize_names: true)
68
+ end
69
+
70
+ def defaults
71
+ data[:defaults] || {}
72
+ end
73
+
74
+ def task_definitions
75
+ data[:task_definitions] || []
76
+ end
77
+
78
+ def service_task_definition
79
+ data[:service_task_definition]
80
+ end
81
+
82
+ def devops_defaults
83
+ data[:devops_defaults] || {}
84
+ end
85
+
86
+ def template_path(file)
87
+ File.join(dir, 'templates', file)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,32 @@
1
+ {
2
+ "defaults": {
3
+ "cpu": "256",
4
+ "memory": "512",
5
+ "desired_count": 1,
6
+ "container_port": 3000,
7
+ "log_retention_days": 30,
8
+ "image_tag": "latest",
9
+ "nginx_image": "public.ecr.aws/nginx/nginx:stable",
10
+ "assign_public_ip": "DISABLED",
11
+ "env": {
12
+ "RAILS_ENV": "production",
13
+ "RACK_ENV": "production",
14
+ "RAILS_LOG_TO_STDOUT": "1"
15
+ },
16
+ "secrets": {
17
+ "RAILS_MASTER_KEY": "{ssm_parameter_prefix}/RAILS_MASTER_KEY"
18
+ }
19
+ },
20
+ "task_definitions": [
21
+ { "suffix": "app", "template": "taskdef-app.json.erb", "container_name": "app" },
22
+ { "suffix": "web", "template": "taskdef-web.json.erb", "container_name": "app" }
23
+ ],
24
+ "service_task_definition": "app",
25
+ "devops_defaults": {
26
+ "source_type": "GITHUB",
27
+ "environment_image": "aws/codebuild/standard:7.0",
28
+ "compute_type": "BUILD_GENERAL1_SMALL",
29
+ "privileged_mode": true,
30
+ "buildspec": "buildspec.yml"
31
+ }
32
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "family": "<%= service_name %>-app",
3
+ "requiresCompatibilities": ["FARGATE"],
4
+ "networkMode": "awsvpc",
5
+ "cpu": "<%= cpu %>",
6
+ "memory": "<%= memory %>",
7
+ "executionRoleArn": "<%= execution_role_arn %>",
8
+ "taskRoleArn": "<%= task_role_arn %>",
9
+ "containerDefinitions": [
10
+ {
11
+ "name": "app",
12
+ "image": "<%= image %>",
13
+ "essential": true,
14
+ "portMappings": [
15
+ { "containerPort": <%= container_port %>, "protocol": "tcp" }
16
+ ],
17
+ "environment": <%= env_json %>,
18
+ "secrets": <%= secrets_json %>,
19
+ "logConfiguration": {
20
+ "logDriver": "awslogs",
21
+ "options": {
22
+ "awslogs-group": "<%= log_group %>",
23
+ "awslogs-region": "<%= region %>",
24
+ "awslogs-stream-prefix": "app"
25
+ }
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "family": "<%= service_name %>-web",
3
+ "requiresCompatibilities": ["FARGATE"],
4
+ "networkMode": "awsvpc",
5
+ "cpu": "<%= cpu %>",
6
+ "memory": "<%= memory %>",
7
+ "executionRoleArn": "<%= execution_role_arn %>",
8
+ "taskRoleArn": "<%= task_role_arn %>",
9
+ "containerDefinitions": [
10
+ {
11
+ "name": "app",
12
+ "image": "<%= image %>",
13
+ "essential": true,
14
+ "portMappings": [
15
+ { "containerPort": <%= container_port %>, "protocol": "tcp" }
16
+ ],
17
+ "environment": <%= env_json %>,
18
+ "secrets": <%= secrets_json %>,
19
+ "logConfiguration": {
20
+ "logDriver": "awslogs",
21
+ "options": {
22
+ "awslogs-group": "<%= log_group %>",
23
+ "awslogs-region": "<%= region %>",
24
+ "awslogs-stream-prefix": "app"
25
+ }
26
+ }
27
+ },
28
+ {
29
+ "name": "nginx",
30
+ "image": "<%= nginx_image %>",
31
+ "essential": true,
32
+ "portMappings": [
33
+ { "containerPort": 80, "protocol": "tcp" }
34
+ ],
35
+ "dependsOn": [
36
+ { "containerName": "app", "condition": "START" }
37
+ ],
38
+ "logConfiguration": {
39
+ "logDriver": "awslogs",
40
+ "options": {
41
+ "awslogs-group": "<%= log_group %>",
42
+ "awslogs-region": "<%= region %>",
43
+ "awslogs-stream-prefix": "nginx"
44
+ }
45
+ }
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,156 @@
1
+ require 'json'
2
+ require 'pathname'
3
+ require 'putpaws/provision/util'
4
+ require 'putpaws/provision/preset'
5
+
6
+ module Putpaws
7
+ module Provision
8
+ class ProvisionConfig
9
+ def self.dir_for(service_name, path_prefix: '.putpaws')
10
+ Pathname.new(path_prefix).join('provisioning', service_name)
11
+ end
12
+
13
+ def self.services(path_prefix: '.putpaws')
14
+ Dir.glob(Pathname.new(path_prefix).join('provisioning', '*', 'provision.json').to_s)
15
+ .map{|p| File.basename(File.dirname(p))}
16
+ .reject{|name| name == 'presets'}
17
+ .sort
18
+ end
19
+
20
+ def self.load(service_name, path_prefix: '.putpaws')
21
+ path = dir_for(service_name, path_prefix: path_prefix).join('provision.json')
22
+ raise "provision.json not found for #{service_name}. Please run `putpaws ready` first." unless path.exist?
23
+ data = JSON.parse(File.read(path), symbolize_names: true)
24
+ new(data, path_prefix: path_prefix)
25
+ end
26
+
27
+ attr_reader :data, :path_prefix
28
+ def initialize(data, path_prefix: '.putpaws')
29
+ @data = data
30
+ @path_prefix = path_prefix
31
+ end
32
+
33
+ def service_name
34
+ data[:service_name]
35
+ end
36
+
37
+ def region
38
+ data[:region]
39
+ end
40
+
41
+ def preset
42
+ @preset ||= begin
43
+ p = Preset.find(data[:preset], path_prefix: path_prefix)
44
+ raise "Preset not found: #{data[:preset]}" unless p
45
+ p
46
+ end
47
+ end
48
+
49
+ def base
50
+ data[:base] || {}
51
+ end
52
+
53
+ def roles
54
+ data[:roles] || {}
55
+ end
56
+
57
+ def overrides
58
+ data[:overrides] || {}
59
+ end
60
+
61
+ def devops
62
+ data[:devops] || {}
63
+ end
64
+
65
+ def dir
66
+ self.class.dir_for(service_name, path_prefix: path_prefix)
67
+ end
68
+
69
+ def policies_dir
70
+ dir.join('policies')
71
+ end
72
+
73
+ # preset defaults <- provision.json overrides (nil values ignored).
74
+ # env/secrets hashes are merged by key.
75
+ def settings
76
+ @settings ||= begin
77
+ merged = preset.defaults.dup
78
+ overrides.each do |k, v|
79
+ next if v.nil?
80
+ next if k == :cluster_name
81
+ if v.is_a?(Hash) && merged[k].is_a?(Hash)
82
+ merged[k] = merged[k].merge(v)
83
+ else
84
+ merged[k] = v
85
+ end
86
+ end
87
+ merged
88
+ end
89
+ end
90
+
91
+ def devops_settings
92
+ preset.devops_defaults.merge(devops.reject{|_, v| v.nil?})
93
+ end
94
+
95
+ def account_id
96
+ Util.arn_account_id(base[:ecr_repository_arn])
97
+ end
98
+
99
+ def ecr_image_uri(tag: nil)
100
+ "#{Util.ecr_repository_uri(base[:ecr_repository_arn])}:#{tag || settings[:image_tag] || 'latest'}"
101
+ end
102
+
103
+ def ssm_parameter_prefix
104
+ base[:ssm_parameter_prefix]
105
+ end
106
+
107
+ # {ssm_parameter_prefix} placeholders resolved, values as full parameter paths.
108
+ def resolved_secrets
109
+ (settings[:secrets] || {}).map{|name, path|
110
+ [name.to_s, path.to_s.gsub('{ssm_parameter_prefix}', ssm_parameter_prefix.to_s)]
111
+ }.to_h
112
+ end
113
+
114
+ def secret_parameter_arns
115
+ resolved_secrets.values.map{|path|
116
+ "arn:aws:ssm:#{region}:#{account_id}:parameter#{path}"
117
+ }
118
+ end
119
+
120
+ # ===== naming conventions (service_name prefix) =====
121
+
122
+ def sg_name
123
+ service_name
124
+ end
125
+
126
+ def cluster_name
127
+ Util.blank?(overrides[:cluster_name]) ? service_name : overrides[:cluster_name]
128
+ end
129
+
130
+ def log_group
131
+ "/ecs/#{service_name}"
132
+ end
133
+
134
+ def build_log_group
135
+ "/aws/codebuild/#{service_name}"
136
+ end
137
+
138
+ def build_project_name
139
+ service_name
140
+ end
141
+
142
+ def task_definition_family(suffix)
143
+ "#{service_name}-#{suffix}"
144
+ end
145
+
146
+ def service_task_definition_family
147
+ task_definition_family(preset.service_task_definition)
148
+ end
149
+
150
+ def service_container_name
151
+ entry = preset.task_definitions.detect{|t| t[:suffix] == preset.service_task_definition}
152
+ (entry && entry[:container_name]) || 'app'
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1 @@
1
+ load File.expand_path("../tasks/provision.rake", __FILE__)