cpflow 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/check_cpln_links.yml +19 -0
- data/.github/workflows/command_docs.yml +24 -0
- data/.github/workflows/rspec-shared.yml +56 -0
- data/.github/workflows/rspec.yml +28 -0
- data/.github/workflows/rubocop.yml +24 -0
- data/.gitignore +18 -0
- data/.overcommit.yml +16 -0
- data/.rubocop.yml +22 -0
- data/.simplecov_spawn.rb +10 -0
- data/CHANGELOG.md +259 -0
- data/CONTRIBUTING.md +73 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +126 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/Rakefile +21 -0
- data/bin/cpflow +6 -0
- data/cpflow +6 -0
- data/cpflow.gemspec +41 -0
- data/docs/assets/grafana-alert.png +0 -0
- data/docs/assets/memcached.png +0 -0
- data/docs/assets/sidekiq-pre-stop-hook.png +0 -0
- data/docs/commands.md +454 -0
- data/docs/dns.md +15 -0
- data/docs/migrating.md +262 -0
- data/docs/postgres.md +436 -0
- data/docs/redis.md +128 -0
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/tips.md +150 -0
- data/docs/troubleshooting.md +6 -0
- data/examples/circleci.yml +104 -0
- data/examples/controlplane.yml +159 -0
- data/lib/command/apply_template.rb +209 -0
- data/lib/command/base.rb +540 -0
- data/lib/command/build_image.rb +49 -0
- data/lib/command/cleanup_images.rb +136 -0
- data/lib/command/cleanup_stale_apps.rb +79 -0
- data/lib/command/config.rb +48 -0
- data/lib/command/copy_image_from_upstream.rb +108 -0
- data/lib/command/delete.rb +149 -0
- data/lib/command/deploy_image.rb +56 -0
- data/lib/command/doctor.rb +47 -0
- data/lib/command/env.rb +22 -0
- data/lib/command/exists.rb +23 -0
- data/lib/command/generate.rb +45 -0
- data/lib/command/info.rb +222 -0
- data/lib/command/latest_image.rb +19 -0
- data/lib/command/logs.rb +49 -0
- data/lib/command/maintenance.rb +42 -0
- data/lib/command/maintenance_off.rb +62 -0
- data/lib/command/maintenance_on.rb +62 -0
- data/lib/command/maintenance_set_page.rb +34 -0
- data/lib/command/no_command.rb +23 -0
- data/lib/command/open.rb +33 -0
- data/lib/command/open_console.rb +26 -0
- data/lib/command/promote_app_from_upstream.rb +38 -0
- data/lib/command/ps.rb +41 -0
- data/lib/command/ps_restart.rb +37 -0
- data/lib/command/ps_start.rb +51 -0
- data/lib/command/ps_stop.rb +82 -0
- data/lib/command/ps_wait.rb +40 -0
- data/lib/command/run.rb +573 -0
- data/lib/command/setup_app.rb +113 -0
- data/lib/command/test.rb +23 -0
- data/lib/command/version.rb +18 -0
- data/lib/constants/exit_code.rb +7 -0
- data/lib/core/config.rb +316 -0
- data/lib/core/controlplane.rb +552 -0
- data/lib/core/controlplane_api.rb +170 -0
- data/lib/core/controlplane_api_direct.rb +112 -0
- data/lib/core/doctor_service.rb +104 -0
- data/lib/core/helpers.rb +26 -0
- data/lib/core/shell.rb +100 -0
- data/lib/core/template_parser.rb +76 -0
- data/lib/cpflow/version.rb +6 -0
- data/lib/cpflow.rb +288 -0
- data/lib/deprecated_commands.json +9 -0
- data/lib/generator_templates/Dockerfile +27 -0
- data/lib/generator_templates/controlplane.yml +62 -0
- data/lib/generator_templates/entrypoint.sh +8 -0
- data/lib/generator_templates/templates/app.yml +21 -0
- data/lib/generator_templates/templates/postgres.yml +176 -0
- data/lib/generator_templates/templates/rails.yml +36 -0
- data/rakelib/create_release.rake +81 -0
- data/script/add_command +37 -0
- data/script/check_command_docs +3 -0
- data/script/check_cpln_links +45 -0
- data/script/rename_command +43 -0
- data/script/update_command_docs +62 -0
- data/templates/app.yml +13 -0
- data/templates/daily-task.yml +32 -0
- data/templates/maintenance.yml +25 -0
- data/templates/memcached.yml +24 -0
- data/templates/postgres.yml +32 -0
- data/templates/rails.yml +27 -0
- data/templates/redis.yml +21 -0
- data/templates/redis2.yml +37 -0
- data/templates/sidekiq.yml +38 -0
- metadata +341 -0
@@ -0,0 +1,552 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Controlplane # rubocop:disable Metrics/ClassLength
|
4
|
+
attr_reader :config, :api, :gvc, :org
|
5
|
+
|
6
|
+
NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE"
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
@api = ControlplaneApi.new
|
11
|
+
@gvc = config.app
|
12
|
+
@org = config.org
|
13
|
+
|
14
|
+
ensure_org_exists! if org
|
15
|
+
end
|
16
|
+
|
17
|
+
# profile
|
18
|
+
|
19
|
+
def profile_switch(profile)
|
20
|
+
ENV["CPLN_PROFILE"] = profile
|
21
|
+
ControlplaneApiDirect.reset_api_token
|
22
|
+
end
|
23
|
+
|
24
|
+
def profile_exists?(profile)
|
25
|
+
cmd = "cpln profile get #{profile} -o yaml"
|
26
|
+
perform_yaml!(cmd).length.positive?
|
27
|
+
end
|
28
|
+
|
29
|
+
def profile_create(profile, token)
|
30
|
+
sensitive_data_pattern = /(?<=--token )(\S+)/
|
31
|
+
cmd = "cpln profile create #{profile} --token #{token}"
|
32
|
+
perform!(cmd, sensitive_data_pattern: sensitive_data_pattern)
|
33
|
+
end
|
34
|
+
|
35
|
+
def profile_delete(profile)
|
36
|
+
cmd = "cpln profile delete #{profile}"
|
37
|
+
perform!(cmd)
|
38
|
+
end
|
39
|
+
|
40
|
+
# image
|
41
|
+
|
42
|
+
def latest_image(a_gvc = gvc, a_org = org)
|
43
|
+
@latest_image ||= {}
|
44
|
+
@latest_image[a_gvc] ||=
|
45
|
+
begin
|
46
|
+
items = query_images(a_gvc, a_org)["items"]
|
47
|
+
latest_image_from(items, app_name: a_gvc)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def latest_image_next(a_gvc = gvc, a_org = org, commit: nil)
|
52
|
+
commit ||= config.options[:commit]
|
53
|
+
|
54
|
+
@latest_image_next ||= {}
|
55
|
+
@latest_image_next[a_gvc] ||= begin
|
56
|
+
latest_image_name = latest_image(a_gvc, a_org)
|
57
|
+
image = latest_image_name.split(":").first
|
58
|
+
image += ":#{extract_image_number(latest_image_name) + 1}"
|
59
|
+
image += "_#{commit}" if commit
|
60
|
+
image
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def latest_image_from(items, app_name: gvc, name_only: true)
|
65
|
+
matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") }
|
66
|
+
|
67
|
+
# Or special string to indicate no image available
|
68
|
+
if matching_items.empty?
|
69
|
+
name_only ? "#{app_name}:#{NO_IMAGE_AVAILABLE}" : nil
|
70
|
+
else
|
71
|
+
latest_item = matching_items.max_by { |item| DateTime.parse(item["created"]) }
|
72
|
+
name_only ? latest_item["name"] : latest_item
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def extract_image_number(image_name)
|
77
|
+
return 0 if image_name.end_with?(NO_IMAGE_AVAILABLE)
|
78
|
+
|
79
|
+
image_name.match(/:(\d+)/)&.captures&.first.to_i
|
80
|
+
end
|
81
|
+
|
82
|
+
def extract_image_commit(image_name)
|
83
|
+
image_name.match(/_(\h+)$/)&.captures&.first
|
84
|
+
end
|
85
|
+
|
86
|
+
def query_images(a_gvc = gvc, a_org = org, partial_gvc_match: nil)
|
87
|
+
partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
|
88
|
+
gvc_op = partial_gvc_match ? "~" : "="
|
89
|
+
|
90
|
+
api.query_images(org: a_org, gvc: a_gvc, gvc_op_type: gvc_op)
|
91
|
+
end
|
92
|
+
|
93
|
+
def image_build(image, dockerfile:, docker_args: [], build_args: [], push: true)
|
94
|
+
# https://docs.controlplane.com/guides/push-image#step-2
|
95
|
+
# Might need to use `docker buildx build` if compatiblitity issues arise
|
96
|
+
cmd = "docker build --platform=linux/amd64 -t #{image} -f #{dockerfile}"
|
97
|
+
cmd += " --progress=plain" if ControlplaneApiDirect.trace
|
98
|
+
|
99
|
+
cmd += " #{docker_args.join(' ')}" if docker_args.any?
|
100
|
+
build_args.each { |build_arg| cmd += " --build-arg #{build_arg}" }
|
101
|
+
cmd += " #{config.app_dir}"
|
102
|
+
perform!(cmd)
|
103
|
+
|
104
|
+
image_push(image) if push
|
105
|
+
end
|
106
|
+
|
107
|
+
def fetch_image_details(image)
|
108
|
+
api.fetch_image_details(org: org, image: image)
|
109
|
+
end
|
110
|
+
|
111
|
+
def image_delete(image)
|
112
|
+
api.image_delete(org: org, image: image)
|
113
|
+
end
|
114
|
+
|
115
|
+
def image_login(org_name = config.org)
|
116
|
+
cmd = "cpln image docker-login --org #{org_name}"
|
117
|
+
perform!(cmd, output_mode: :none)
|
118
|
+
end
|
119
|
+
|
120
|
+
def image_pull(image)
|
121
|
+
cmd = "docker pull #{image}"
|
122
|
+
perform!(cmd, output_mode: :none)
|
123
|
+
end
|
124
|
+
|
125
|
+
def image_tag(old_tag, new_tag)
|
126
|
+
cmd = "docker tag #{old_tag} #{new_tag}"
|
127
|
+
perform!(cmd)
|
128
|
+
end
|
129
|
+
|
130
|
+
def image_push(image)
|
131
|
+
cmd = "docker push #{image}"
|
132
|
+
perform!(cmd)
|
133
|
+
end
|
134
|
+
|
135
|
+
# gvc
|
136
|
+
|
137
|
+
def fetch_gvcs
|
138
|
+
api.gvc_list(org: org)
|
139
|
+
end
|
140
|
+
|
141
|
+
def gvc_query(app_name = config.app)
|
142
|
+
# When `match_if_app_name_starts_with` is `true`, we query for any gvc containing the name,
|
143
|
+
# otherwise we query for a gvc with the exact name.
|
144
|
+
op = config.should_app_start_with?(app_name) ? "~" : "="
|
145
|
+
|
146
|
+
cmd = "cpln gvc query --org #{org} -o yaml --prop name#{op}#{app_name}"
|
147
|
+
perform_yaml!(cmd)
|
148
|
+
end
|
149
|
+
|
150
|
+
def fetch_gvc(a_gvc = gvc, a_org = org)
|
151
|
+
api.gvc_get(gvc: a_gvc, org: a_org)
|
152
|
+
end
|
153
|
+
|
154
|
+
def fetch_gvc!(a_gvc = gvc)
|
155
|
+
gvc_data = fetch_gvc(a_gvc)
|
156
|
+
return gvc_data if gvc_data
|
157
|
+
|
158
|
+
raise "Can't find app '#{gvc}', please create it with 'cpflow setup-app -a #{config.app}'."
|
159
|
+
end
|
160
|
+
|
161
|
+
def gvc_delete(a_gvc = gvc)
|
162
|
+
api.gvc_delete(gvc: a_gvc, org: org)
|
163
|
+
end
|
164
|
+
|
165
|
+
# workload
|
166
|
+
|
167
|
+
def fetch_workloads(a_gvc = gvc)
|
168
|
+
api.workload_list(gvc: a_gvc, org: org)
|
169
|
+
end
|
170
|
+
|
171
|
+
def fetch_workloads_by_org(a_org = org)
|
172
|
+
api.workload_list_by_org(org: a_org)
|
173
|
+
end
|
174
|
+
|
175
|
+
def fetch_workload(workload)
|
176
|
+
api.workload_get(workload: workload, gvc: gvc, org: org)
|
177
|
+
end
|
178
|
+
|
179
|
+
def fetch_workload!(workload)
|
180
|
+
workload_data = fetch_workload(workload)
|
181
|
+
return workload_data if workload_data
|
182
|
+
|
183
|
+
raise "Can't find workload '#{workload}', " \
|
184
|
+
"please create it with 'cpflow apply-template #{workload} -a #{config.app}'."
|
185
|
+
end
|
186
|
+
|
187
|
+
def query_workloads(workload, a_gvc = gvc, a_org = org, partial_workload_match: false, partial_gvc_match: nil)
|
188
|
+
partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
|
189
|
+
gvc_op = partial_gvc_match ? "~" : "="
|
190
|
+
workload_op = partial_workload_match ? "~" : "="
|
191
|
+
|
192
|
+
api.query_workloads(org: a_org, gvc: a_gvc, workload: workload, gvc_op_type: gvc_op, workload_op_type: workload_op)
|
193
|
+
end
|
194
|
+
|
195
|
+
def fetch_workload_replicas(workload, location:)
|
196
|
+
cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml"
|
197
|
+
perform_yaml(cmd)
|
198
|
+
end
|
199
|
+
|
200
|
+
def stop_workload_replica(workload, replica, location:)
|
201
|
+
cmd = "cpln workload replica stop #{workload} #{gvc_org} --replica-name #{replica} --location #{location}"
|
202
|
+
perform(cmd, output_mode: :none)
|
203
|
+
end
|
204
|
+
|
205
|
+
def fetch_workload_deployments(workload)
|
206
|
+
api.workload_deployments(workload: workload, gvc: gvc, org: org)
|
207
|
+
end
|
208
|
+
|
209
|
+
def workload_deployment_version_ready?(version, next_version)
|
210
|
+
return false unless version["workload"] == next_version
|
211
|
+
|
212
|
+
version["containers"]&.all? do |_, container|
|
213
|
+
container.dig("resources", "replicas") == container.dig("resources", "replicasReady")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def workload_deployments_ready?(workload, location:, expected_status:)
|
218
|
+
deployed_replicas = fetch_workload_replicas(workload, location: location)["items"].length
|
219
|
+
return deployed_replicas.zero? if expected_status == false
|
220
|
+
|
221
|
+
deployments = fetch_workload_deployments(workload)["items"]
|
222
|
+
deployments.all? do |deployment|
|
223
|
+
next_version = deployment.dig("status", "expectedDeploymentVersion")
|
224
|
+
|
225
|
+
deployment.dig("status", "versions")&.all? do |version|
|
226
|
+
workload_deployment_version_ready?(version, next_version)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def workload_set_image_ref(workload, container:, image:)
|
232
|
+
cmd = "cpln workload update #{workload} #{gvc_org}"
|
233
|
+
cmd += " --set spec.containers.#{container}.image=/org/#{config.org}/image/#{image}"
|
234
|
+
perform!(cmd)
|
235
|
+
end
|
236
|
+
|
237
|
+
def set_workload_env_var(workload, container:, name:, value:)
|
238
|
+
data = fetch_workload!(workload)
|
239
|
+
data["spec"]["containers"].each do |container_data|
|
240
|
+
next unless container_data["name"] == container
|
241
|
+
|
242
|
+
container_data["env"].each do |env_data|
|
243
|
+
next unless env_data["name"] == name
|
244
|
+
|
245
|
+
env_data["value"] = value
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
|
250
|
+
end
|
251
|
+
|
252
|
+
def set_workload_suspend(workload, value)
|
253
|
+
data = fetch_workload!(workload)
|
254
|
+
data["spec"]["defaultOptions"]["suspend"] = value
|
255
|
+
|
256
|
+
api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
|
257
|
+
end
|
258
|
+
|
259
|
+
def workload_suspended?(workload)
|
260
|
+
details = fetch_workload!(workload)
|
261
|
+
details["spec"]["defaultOptions"]["suspend"]
|
262
|
+
end
|
263
|
+
|
264
|
+
def workload_force_redeployment(workload)
|
265
|
+
cmd = "cpln workload force-redeployment #{workload} #{gvc_org}"
|
266
|
+
perform!(cmd)
|
267
|
+
end
|
268
|
+
|
269
|
+
def delete_workload(workload, a_gvc = gvc)
|
270
|
+
api.delete_workload(org: org, gvc: a_gvc, workload: workload)
|
271
|
+
end
|
272
|
+
|
273
|
+
def workload_connect(workload, location:, container: nil, shell: nil)
|
274
|
+
cmd = "cpln workload connect #{workload} #{gvc_org} --location #{location}"
|
275
|
+
cmd += " --container #{container}" if container
|
276
|
+
cmd += " --shell #{shell}" if shell
|
277
|
+
perform!(cmd, output_mode: :all)
|
278
|
+
end
|
279
|
+
|
280
|
+
def workload_exec(workload, replica, location:, container: nil, command: nil)
|
281
|
+
cmd = "cpln workload exec #{workload} #{gvc_org} --replica #{replica} --location #{location}"
|
282
|
+
cmd += " --container #{container}" if container
|
283
|
+
cmd += " -- #{command}"
|
284
|
+
perform!(cmd, output_mode: :all)
|
285
|
+
end
|
286
|
+
|
287
|
+
def start_cron_workload(workload, job_start_yaml, location:)
|
288
|
+
Tempfile.create do |f|
|
289
|
+
f.write(job_start_yaml)
|
290
|
+
f.rewind
|
291
|
+
|
292
|
+
cmd = "cpln workload cron start #{workload} #{gvc_org} --file #{f.path} --location #{location} -o yaml"
|
293
|
+
perform_yaml(cmd)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def fetch_cron_workload(workload, location:)
|
298
|
+
cmd = "cpln workload cron get #{workload} #{gvc_org} --location #{location} -o yaml"
|
299
|
+
perform_yaml(cmd)
|
300
|
+
end
|
301
|
+
|
302
|
+
def cron_workload_deployed_version(workload)
|
303
|
+
current_deployment = fetch_workload_deployments(workload)&.dig("items")&.first
|
304
|
+
return nil unless current_deployment
|
305
|
+
|
306
|
+
ready = current_deployment.dig("status", "ready")
|
307
|
+
last_processed_version = current_deployment.dig("status", "lastProcessedVersion")
|
308
|
+
|
309
|
+
ready ? last_processed_version : nil
|
310
|
+
end
|
311
|
+
|
312
|
+
# volumeset
|
313
|
+
|
314
|
+
def fetch_volumesets(a_gvc = gvc)
|
315
|
+
api.list_volumesets(org: org, gvc: a_gvc)
|
316
|
+
end
|
317
|
+
|
318
|
+
def delete_volumeset(volumeset, a_gvc = gvc)
|
319
|
+
api.delete_volumeset(org: org, gvc: a_gvc, volumeset: volumeset)
|
320
|
+
end
|
321
|
+
|
322
|
+
# domain
|
323
|
+
|
324
|
+
def find_domain_route(data)
|
325
|
+
port = data["spec"]["ports"].find { |current_port| current_port["number"] == 80 || current_port["number"] == 443 }
|
326
|
+
return nil if port.nil? || port["routes"].nil?
|
327
|
+
|
328
|
+
route = port["routes"].find { |current_route| current_route["prefix"] == "/" }
|
329
|
+
return nil if route.nil?
|
330
|
+
|
331
|
+
route
|
332
|
+
end
|
333
|
+
|
334
|
+
def find_domain_for(workloads)
|
335
|
+
domains = api.list_domains(org: org)["items"]
|
336
|
+
domains.find do |domain_data|
|
337
|
+
route = find_domain_route(domain_data)
|
338
|
+
next false if route.nil?
|
339
|
+
|
340
|
+
workloads.any? { |workload| route["workloadLink"].match?(%r{/org/#{org}/gvc/#{gvc}/workload/#{workload}}) }
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def fetch_domain(domain)
|
345
|
+
domain_data = api.fetch_domain(org: org, domain: domain)
|
346
|
+
route = find_domain_route(domain_data)
|
347
|
+
return nil if route.nil?
|
348
|
+
|
349
|
+
domain_data
|
350
|
+
end
|
351
|
+
|
352
|
+
def domain_workload_matches?(data, workload)
|
353
|
+
route = find_domain_route(data)
|
354
|
+
route["workloadLink"].match?(%r{/org/#{org}/gvc/#{gvc}/workload/#{workload}})
|
355
|
+
end
|
356
|
+
|
357
|
+
def set_domain_workload(data, workload)
|
358
|
+
route = find_domain_route(data)
|
359
|
+
route["workloadLink"] = "/org/#{org}/gvc/#{gvc}/workload/#{workload}"
|
360
|
+
|
361
|
+
api.update_domain(org: org, domain: data["name"], data: data)
|
362
|
+
end
|
363
|
+
|
364
|
+
# logs
|
365
|
+
|
366
|
+
def logs(workload:, limit:, since:, replica: nil)
|
367
|
+
query_parts = ["gvc=\"#{gvc}\"", "workload=\"#{workload}\""]
|
368
|
+
query_parts.push("replica=\"#{replica}\"") if replica
|
369
|
+
query = "{#{query_parts.join(',')}}"
|
370
|
+
|
371
|
+
cmd = "cpln logs '#{query}' --org #{org} -t -o raw --limit #{limit} --since #{since}"
|
372
|
+
perform!(cmd, output_mode: :all)
|
373
|
+
end
|
374
|
+
|
375
|
+
def log_get(workload:, from:, to:, replica: nil)
|
376
|
+
api.log_get(org: org, gvc: gvc, workload: workload, replica: replica, from: from, to: to)
|
377
|
+
end
|
378
|
+
|
379
|
+
# secrets
|
380
|
+
|
381
|
+
def fetch_secret(secret)
|
382
|
+
api.fetch_secret(org: org, secret: secret)
|
383
|
+
end
|
384
|
+
|
385
|
+
# identities
|
386
|
+
|
387
|
+
def fetch_identity(identity, a_gvc = gvc)
|
388
|
+
api.fetch_identity(org: org, gvc: a_gvc, identity: identity)
|
389
|
+
end
|
390
|
+
|
391
|
+
# policies
|
392
|
+
|
393
|
+
def fetch_policy(policy)
|
394
|
+
api.fetch_policy(org: org, policy: policy)
|
395
|
+
end
|
396
|
+
|
397
|
+
def bind_identity_to_policy(identity_link, policy)
|
398
|
+
cmd = "cpln policy add-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
|
399
|
+
perform!(cmd)
|
400
|
+
end
|
401
|
+
|
402
|
+
def unbind_identity_from_policy(identity_link, policy)
|
403
|
+
cmd = "cpln policy remove-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
|
404
|
+
perform!(cmd)
|
405
|
+
end
|
406
|
+
|
407
|
+
# apply
|
408
|
+
def apply_template(data) # rubocop:disable Metrics/MethodLength
|
409
|
+
Tempfile.create do |f|
|
410
|
+
f.write(data)
|
411
|
+
f.rewind
|
412
|
+
cmd = "cpln apply #{gvc_org} --file #{f.path}"
|
413
|
+
if Shell.tmp_stderr
|
414
|
+
cmd += " 2> #{Shell.tmp_stderr.path}" if Shell.should_hide_output?
|
415
|
+
|
416
|
+
Shell.debug("CMD", cmd)
|
417
|
+
|
418
|
+
result = Shell.cmd(cmd)
|
419
|
+
parse_apply_result(result[:output]) if result[:success]
|
420
|
+
else
|
421
|
+
Shell.debug("CMD", cmd)
|
422
|
+
|
423
|
+
result = Shell.cmd(cmd)
|
424
|
+
if result[:success]
|
425
|
+
parse_apply_result(result[:output])
|
426
|
+
else
|
427
|
+
Shell.abort("Command exited with non-zero status.")
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def apply_hash(data)
|
434
|
+
apply_template(data.to_yaml)
|
435
|
+
end
|
436
|
+
|
437
|
+
def parse_apply_result(result) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
438
|
+
items = []
|
439
|
+
|
440
|
+
lines = result.split("\n")
|
441
|
+
lines.each do |line|
|
442
|
+
# The line can be in one of these formats:
|
443
|
+
# - "Created /org/shakacode-open-source-examples/gvc/my-app-staging"
|
444
|
+
# - "Created /org/shakacode-open-source-examples/gvc/my-app-staging/workload/redis"
|
445
|
+
# - "Updated gvc 'tutorial-app-test-1'"
|
446
|
+
# - "Updated workload 'redis'"
|
447
|
+
if line.start_with?("Created")
|
448
|
+
matches = line.match(%r{Created\s/org/[^/]+/gvc/([^/]+)($|(/([^/]+)/([^/]+)$))})&.captures
|
449
|
+
next unless matches
|
450
|
+
|
451
|
+
app, _, __, kind, name = matches
|
452
|
+
if kind
|
453
|
+
items.push({ kind: kind, name: name })
|
454
|
+
else
|
455
|
+
items.push({ kind: "app", name: app })
|
456
|
+
end
|
457
|
+
else
|
458
|
+
matches = line.match(/Updated\s([^\s]+)\s'([^\s]+)'$/)&.captures
|
459
|
+
next unless matches
|
460
|
+
|
461
|
+
kind, name = matches
|
462
|
+
kind = "app" if kind == "gvc"
|
463
|
+
items.push({ kind: kind, name: name })
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
items
|
468
|
+
end
|
469
|
+
|
470
|
+
private
|
471
|
+
|
472
|
+
def org_exists?
|
473
|
+
items = api.list_orgs["items"]
|
474
|
+
items.any? { |item| item["name"] == org }
|
475
|
+
end
|
476
|
+
|
477
|
+
def ensure_org_exists!
|
478
|
+
return if org_exists?
|
479
|
+
|
480
|
+
raise "Can't find org '#{org}', please create it in the Control Plane dashboard " \
|
481
|
+
"or ensure that the name is correct."
|
482
|
+
end
|
483
|
+
|
484
|
+
# `output_mode` can be :all, :errors_only or :none.
|
485
|
+
# If not provided, it will be determined based on the `HIDE_COMMAND_OUTPUT` env var
|
486
|
+
# or the return value of `Shell.should_hide_output?`.
|
487
|
+
def build_command(cmd, output_mode: nil) # rubocop:disable Metrics/MethodLength
|
488
|
+
output_mode ||= determine_command_output_mode
|
489
|
+
|
490
|
+
case output_mode
|
491
|
+
when :all
|
492
|
+
cmd
|
493
|
+
when :errors_only
|
494
|
+
"#{cmd} > /dev/null"
|
495
|
+
when :none
|
496
|
+
"#{cmd} > /dev/null 2>&1"
|
497
|
+
else
|
498
|
+
raise "Invalid command output mode '#{output_mode}'."
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def determine_command_output_mode
|
503
|
+
if ENV.fetch("HIDE_COMMAND_OUTPUT", nil) == "true"
|
504
|
+
:none
|
505
|
+
elsif Shell.should_hide_output?
|
506
|
+
:errors_only
|
507
|
+
else
|
508
|
+
:all
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
def perform(cmd, output_mode: nil, sensitive_data_pattern: nil)
|
513
|
+
cmd = build_command(cmd, output_mode: output_mode)
|
514
|
+
|
515
|
+
Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
|
516
|
+
|
517
|
+
kernel_system_with_pid_handling(cmd)
|
518
|
+
end
|
519
|
+
|
520
|
+
# NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing
|
521
|
+
def kernel_system_with_pid_handling(cmd)
|
522
|
+
pid = Process.spawn(cmd)
|
523
|
+
$child_pids << pid # rubocop:disable Style/GlobalVars
|
524
|
+
|
525
|
+
_, status = Process.wait2(pid)
|
526
|
+
$child_pids.delete(pid) # rubocop:disable Style/GlobalVars
|
527
|
+
|
528
|
+
status.exited? ? status.success? : nil
|
529
|
+
rescue SystemCallError
|
530
|
+
nil
|
531
|
+
end
|
532
|
+
|
533
|
+
def perform!(cmd, output_mode: nil, sensitive_data_pattern: nil)
|
534
|
+
success = perform(cmd, output_mode: output_mode, sensitive_data_pattern: sensitive_data_pattern)
|
535
|
+
success || Shell.abort("Command exited with non-zero status.")
|
536
|
+
end
|
537
|
+
|
538
|
+
def perform_yaml(cmd)
|
539
|
+
Shell.debug("CMD", cmd)
|
540
|
+
|
541
|
+
result = Shell.cmd(cmd)
|
542
|
+
YAML.safe_load(result[:output], permitted_classes: [Time]) if result[:success]
|
543
|
+
end
|
544
|
+
|
545
|
+
def perform_yaml!(cmd)
|
546
|
+
perform_yaml(cmd) || Shell.abort("Command exited with non-zero status.")
|
547
|
+
end
|
548
|
+
|
549
|
+
def gvc_org
|
550
|
+
"--gvc #{gvc} --org #{org}"
|
551
|
+
end
|
552
|
+
end
|