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
data/lib/command/base.rb
ADDED
@@ -0,0 +1,540 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../core/helpers"
|
4
|
+
|
5
|
+
module Command
|
6
|
+
class Base # rubocop:disable Metrics/ClassLength
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
include Helpers
|
10
|
+
|
11
|
+
VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS = %w[config].freeze
|
12
|
+
VALIDATIONS_WITH_ADDITIONAL_OPTIONS = %w[templates].freeze
|
13
|
+
ALL_VALIDATIONS = VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS + VALIDATIONS_WITH_ADDITIONAL_OPTIONS
|
14
|
+
|
15
|
+
# Used to call the command (`cpflow NAME`)
|
16
|
+
# NAME = ""
|
17
|
+
# Displayed when running `cpflow help` or `cpflow help NAME` (defaults to `NAME`)
|
18
|
+
USAGE = ""
|
19
|
+
# Throws error if `true` and no arguments are passed to the command
|
20
|
+
# or if `false` and arguments are passed to the command
|
21
|
+
REQUIRES_ARGS = false
|
22
|
+
# Default arguments if none are passed to the command
|
23
|
+
DEFAULT_ARGS = [].freeze
|
24
|
+
# Options for the command (use option methods below)
|
25
|
+
OPTIONS = [].freeze
|
26
|
+
# Does not throw error if `true` and extra options
|
27
|
+
# that are not specified in `OPTIONS` are passed to the command
|
28
|
+
ACCEPTS_EXTRA_OPTIONS = false
|
29
|
+
# Displayed when running `cpflow help`
|
30
|
+
# DESCRIPTION = ""
|
31
|
+
# Displayed when running `cpflow help NAME`
|
32
|
+
# LONG_DESCRIPTION = ""
|
33
|
+
# Displayed along with `LONG_DESCRIPTION` when running `cpflow help NAME`
|
34
|
+
EXAMPLES = ""
|
35
|
+
# If `true`, hides the command from `cpflow help`
|
36
|
+
HIDE = false
|
37
|
+
# Whether or not to show key information like ORG and APP name in commands
|
38
|
+
WITH_INFO_HEADER = true
|
39
|
+
# Which validations to run before the command
|
40
|
+
VALIDATIONS = %w[config].freeze
|
41
|
+
|
42
|
+
def initialize(config)
|
43
|
+
@config = config
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.all_commands
|
47
|
+
Dir["#{__dir__}/*.rb"].each_with_object({}) do |file, result|
|
48
|
+
filename = File.basename(file, ".rb")
|
49
|
+
classname = File.read(file).match(/^\s+class (\w+) < Base($| .*$)/)&.captures&.first
|
50
|
+
result[filename.to_sym] = Object.const_get("::Command::#{classname}") if classname
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.common_options
|
55
|
+
[org_option, verbose_option, trace_option]
|
56
|
+
end
|
57
|
+
|
58
|
+
# rubocop:disable Metrics/MethodLength
|
59
|
+
def self.org_option(required: false)
|
60
|
+
{
|
61
|
+
name: :org,
|
62
|
+
params: {
|
63
|
+
aliases: ["-o"],
|
64
|
+
banner: "ORG_NAME",
|
65
|
+
desc: "Organization name",
|
66
|
+
type: :string,
|
67
|
+
required: required
|
68
|
+
}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.app_option(required: false)
|
73
|
+
{
|
74
|
+
name: :app,
|
75
|
+
params: {
|
76
|
+
aliases: ["-a"],
|
77
|
+
banner: "APP_NAME",
|
78
|
+
desc: "Application name",
|
79
|
+
type: :string,
|
80
|
+
required: required
|
81
|
+
}
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.workload_option(required: false)
|
86
|
+
{
|
87
|
+
name: :workload,
|
88
|
+
params: {
|
89
|
+
aliases: ["-w"],
|
90
|
+
banner: "WORKLOAD_NAME",
|
91
|
+
desc: "Workload name",
|
92
|
+
type: :string,
|
93
|
+
required: required
|
94
|
+
}
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.replica_option(required: false)
|
99
|
+
{
|
100
|
+
name: :replica,
|
101
|
+
params: {
|
102
|
+
aliases: ["-r"],
|
103
|
+
banner: "REPLICA_NAME",
|
104
|
+
desc: "Replica name",
|
105
|
+
type: :string,
|
106
|
+
required: required
|
107
|
+
}
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.image_option(required: false)
|
112
|
+
{
|
113
|
+
name: :image,
|
114
|
+
params: {
|
115
|
+
aliases: ["-i"],
|
116
|
+
banner: "IMAGE_NAME",
|
117
|
+
desc: "Image name",
|
118
|
+
type: :string,
|
119
|
+
required: required
|
120
|
+
}
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.log_method_option(required: false)
|
125
|
+
{
|
126
|
+
name: :log_method,
|
127
|
+
params: {
|
128
|
+
type: :numeric,
|
129
|
+
banner: "LOG_METHOD",
|
130
|
+
desc: "Log method",
|
131
|
+
required: required,
|
132
|
+
valid_values: [1, 2, 3],
|
133
|
+
default: 3
|
134
|
+
}
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.commit_option(required: false)
|
139
|
+
{
|
140
|
+
name: :commit,
|
141
|
+
params: {
|
142
|
+
aliases: ["-c"],
|
143
|
+
banner: "COMMIT_HASH",
|
144
|
+
desc: "Commit hash",
|
145
|
+
type: :string,
|
146
|
+
required: required
|
147
|
+
}
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.location_option(required: false)
|
152
|
+
{
|
153
|
+
name: :location,
|
154
|
+
params: {
|
155
|
+
aliases: ["-l"],
|
156
|
+
banner: "LOCATION_NAME",
|
157
|
+
desc: "Location name",
|
158
|
+
type: :string,
|
159
|
+
required: required
|
160
|
+
}
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.domain_option(required: false)
|
165
|
+
{
|
166
|
+
name: :domain,
|
167
|
+
params: {
|
168
|
+
banner: "DOMAIN_NAME",
|
169
|
+
desc: "Domain name",
|
170
|
+
type: :string,
|
171
|
+
required: required
|
172
|
+
}
|
173
|
+
}
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.upstream_token_option(required: false)
|
177
|
+
{
|
178
|
+
name: :upstream_token,
|
179
|
+
params: {
|
180
|
+
aliases: ["-t"],
|
181
|
+
banner: "UPSTREAM_TOKEN",
|
182
|
+
desc: "Upstream token",
|
183
|
+
type: :string,
|
184
|
+
required: required
|
185
|
+
}
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.skip_confirm_option(required: false)
|
190
|
+
{
|
191
|
+
name: :yes,
|
192
|
+
params: {
|
193
|
+
aliases: ["-y"],
|
194
|
+
banner: "SKIP_CONFIRM",
|
195
|
+
desc: "Skip confirmation",
|
196
|
+
type: :boolean,
|
197
|
+
required: required
|
198
|
+
}
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.version_option(required: false)
|
203
|
+
{
|
204
|
+
name: :version,
|
205
|
+
params: {
|
206
|
+
aliases: ["-v"],
|
207
|
+
banner: "VERSION",
|
208
|
+
desc: "Displays the current version of the CLI",
|
209
|
+
type: :boolean,
|
210
|
+
required: required
|
211
|
+
}
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.use_local_token_option(required: false)
|
216
|
+
{
|
217
|
+
name: :use_local_token,
|
218
|
+
params: {
|
219
|
+
desc: "Override remote CPLN_TOKEN with local token",
|
220
|
+
type: :boolean,
|
221
|
+
required: required
|
222
|
+
}
|
223
|
+
}
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.terminal_size_option(required: false)
|
227
|
+
{
|
228
|
+
name: :terminal_size,
|
229
|
+
params: {
|
230
|
+
banner: "ROWS,COLS",
|
231
|
+
desc: "Override remote terminal size (e.g. `--terminal-size 10,20`)",
|
232
|
+
type: :string,
|
233
|
+
required: required,
|
234
|
+
valid_regex: /^\d+,\d+$/
|
235
|
+
}
|
236
|
+
}
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.wait_option(title = "", required: false)
|
240
|
+
{
|
241
|
+
name: :wait,
|
242
|
+
params: {
|
243
|
+
desc: "Waits for #{title}",
|
244
|
+
type: :boolean,
|
245
|
+
required: required
|
246
|
+
}
|
247
|
+
}
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.verbose_option(required: false)
|
251
|
+
{
|
252
|
+
name: :verbose,
|
253
|
+
params: {
|
254
|
+
aliases: ["-d"],
|
255
|
+
desc: "Shows detailed logs",
|
256
|
+
type: :boolean,
|
257
|
+
required: required
|
258
|
+
}
|
259
|
+
}
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.trace_option(required: false)
|
263
|
+
{
|
264
|
+
name: :trace,
|
265
|
+
params: {
|
266
|
+
desc: "Shows trace of API calls. WARNING: may contain sensitive data",
|
267
|
+
type: :boolean,
|
268
|
+
required: required
|
269
|
+
}
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
def self.skip_secret_access_binding_option(required: false)
|
274
|
+
{
|
275
|
+
name: :skip_secret_access_binding,
|
276
|
+
new_name: :skip_secrets_setup,
|
277
|
+
params: {
|
278
|
+
desc: "Skips secret access binding",
|
279
|
+
type: :boolean,
|
280
|
+
required: required
|
281
|
+
}
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.skip_secrets_setup_option(required: false)
|
286
|
+
{
|
287
|
+
name: :skip_secrets_setup,
|
288
|
+
params: {
|
289
|
+
desc: "Skips secrets setup",
|
290
|
+
type: :boolean,
|
291
|
+
required: required
|
292
|
+
}
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
296
|
+
def self.run_release_phase_option(required: false)
|
297
|
+
{
|
298
|
+
name: :run_release_phase,
|
299
|
+
params: {
|
300
|
+
desc: "Runs release phase",
|
301
|
+
type: :boolean,
|
302
|
+
required: required
|
303
|
+
}
|
304
|
+
}
|
305
|
+
end
|
306
|
+
|
307
|
+
def self.logs_limit_option(required: false)
|
308
|
+
{
|
309
|
+
name: :limit,
|
310
|
+
params: {
|
311
|
+
banner: "NUMBER",
|
312
|
+
desc: "Limit on number of log entries to show",
|
313
|
+
type: :numeric,
|
314
|
+
required: required,
|
315
|
+
default: 200
|
316
|
+
}
|
317
|
+
}
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.logs_since_option(required: false)
|
321
|
+
{
|
322
|
+
name: :since,
|
323
|
+
params: {
|
324
|
+
banner: "DURATION",
|
325
|
+
desc: "Loopback window for showing logs " \
|
326
|
+
"(see https://www.npmjs.com/package/parse-duration for the accepted formats, e.g., '1h')",
|
327
|
+
type: :string,
|
328
|
+
required: required,
|
329
|
+
default: "1h"
|
330
|
+
}
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
def self.interactive_option(required: false)
|
335
|
+
{
|
336
|
+
name: :interactive,
|
337
|
+
params: {
|
338
|
+
desc: "Runs interactive command",
|
339
|
+
type: :boolean,
|
340
|
+
required: required
|
341
|
+
}
|
342
|
+
}
|
343
|
+
end
|
344
|
+
|
345
|
+
def self.detached_option(required: false)
|
346
|
+
{
|
347
|
+
name: :detached,
|
348
|
+
params: {
|
349
|
+
desc: "Runs non-interactive command, detaches, and prints commands to log and stop the job",
|
350
|
+
type: :boolean,
|
351
|
+
required: required
|
352
|
+
}
|
353
|
+
}
|
354
|
+
end
|
355
|
+
|
356
|
+
def self.cpu_option(required: false)
|
357
|
+
{
|
358
|
+
name: :cpu,
|
359
|
+
params: {
|
360
|
+
banner: "CPU",
|
361
|
+
desc: "Overrides CPU millicores " \
|
362
|
+
"(e.g., '100m' for 100 millicores, '1' for 1 core)",
|
363
|
+
type: :string,
|
364
|
+
required: required,
|
365
|
+
valid_regex: /^\d+m?$/
|
366
|
+
}
|
367
|
+
}
|
368
|
+
end
|
369
|
+
|
370
|
+
def self.memory_option(required: false)
|
371
|
+
{
|
372
|
+
name: :memory,
|
373
|
+
params: {
|
374
|
+
banner: "MEMORY",
|
375
|
+
desc: "Overrides memory size " \
|
376
|
+
"(e.g., '100Mi' for 100 mebibytes, '1Gi' for 1 gibibyte)",
|
377
|
+
type: :string,
|
378
|
+
required: required,
|
379
|
+
valid_regex: /^\d+[MG]i$/
|
380
|
+
}
|
381
|
+
}
|
382
|
+
end
|
383
|
+
|
384
|
+
def self.entrypoint_option(required: false)
|
385
|
+
{
|
386
|
+
name: :entrypoint,
|
387
|
+
params: {
|
388
|
+
banner: "ENTRYPOINT",
|
389
|
+
desc: "Overrides entrypoint " \
|
390
|
+
"(must be a single command or a script path that exists in the container)",
|
391
|
+
type: :string,
|
392
|
+
required: required,
|
393
|
+
valid_regex: /^\S+$/
|
394
|
+
}
|
395
|
+
}
|
396
|
+
end
|
397
|
+
|
398
|
+
def self.validations_option(required: false)
|
399
|
+
{
|
400
|
+
name: :validations,
|
401
|
+
params: {
|
402
|
+
banner: "VALIDATION_1,VALIDATION_2,...",
|
403
|
+
desc: "Which validations to run " \
|
404
|
+
"(must be separated by a comma)",
|
405
|
+
type: :string,
|
406
|
+
required: required,
|
407
|
+
default: VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS.join(","),
|
408
|
+
valid_regex: /^(#{ALL_VALIDATIONS.join("|")})(,(#{ALL_VALIDATIONS.join("|")}))*$/
|
409
|
+
}
|
410
|
+
}
|
411
|
+
end
|
412
|
+
|
413
|
+
def self.skip_post_creation_hook_option(required: false)
|
414
|
+
{
|
415
|
+
name: :skip_post_creation_hook,
|
416
|
+
params: {
|
417
|
+
desc: "Skips post-creation hook",
|
418
|
+
type: :boolean,
|
419
|
+
required: required
|
420
|
+
}
|
421
|
+
}
|
422
|
+
end
|
423
|
+
|
424
|
+
def self.skip_pre_deletion_hook_option(required: false)
|
425
|
+
{
|
426
|
+
name: :skip_pre_deletion_hook,
|
427
|
+
params: {
|
428
|
+
desc: "Skips pre-deletion hook",
|
429
|
+
type: :boolean,
|
430
|
+
required: required
|
431
|
+
}
|
432
|
+
}
|
433
|
+
end
|
434
|
+
|
435
|
+
def self.add_app_identity_option(required: false)
|
436
|
+
{
|
437
|
+
name: :add_app_identity,
|
438
|
+
params: {
|
439
|
+
desc: "Adds app identity template if it does not exist",
|
440
|
+
type: :boolean,
|
441
|
+
required: required
|
442
|
+
}
|
443
|
+
}
|
444
|
+
end
|
445
|
+
# rubocop:enable Metrics/MethodLength
|
446
|
+
|
447
|
+
def self.all_options
|
448
|
+
methods.grep(/_option$/).map { |method| send(method.to_s) }
|
449
|
+
end
|
450
|
+
|
451
|
+
def self.all_options_by_key_name
|
452
|
+
all_options.each_with_object({}) do |option, result|
|
453
|
+
option[:params][:aliases]&.each { |current_alias| result[current_alias.to_s] = option }
|
454
|
+
result["--#{option[:name]}"] = option
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# NOTE: use simplified variant atm, as shelljoin do different escaping
|
459
|
+
# TODO: most probably need better logic for escaping various quotes
|
460
|
+
def args_join(args)
|
461
|
+
args.join(" ")
|
462
|
+
end
|
463
|
+
|
464
|
+
def progress
|
465
|
+
$stderr
|
466
|
+
end
|
467
|
+
|
468
|
+
def step_error(error, abort_on_error: true)
|
469
|
+
message = error.message
|
470
|
+
if abort_on_error
|
471
|
+
progress.puts(" #{Shell.color('failed!', :red)}\n\n")
|
472
|
+
Shell.abort(message)
|
473
|
+
else
|
474
|
+
Shell.write_to_tmp_stderr(message)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def step_finish(success)
|
479
|
+
if success
|
480
|
+
progress.puts(" #{Shell.color('done!', :green)}")
|
481
|
+
else
|
482
|
+
progress.puts(" #{Shell.color('failed!', :red)}\n\n#{Shell.read_from_tmp_stderr}\n\n")
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
def step(message, abort_on_error: true, retry_on_failure: false) # rubocop:disable Metrics/MethodLength
|
487
|
+
progress.print("#{message}...")
|
488
|
+
|
489
|
+
Shell.use_tmp_stderr do
|
490
|
+
success = false
|
491
|
+
|
492
|
+
begin
|
493
|
+
if retry_on_failure
|
494
|
+
until (success = yield)
|
495
|
+
progress.print(".")
|
496
|
+
Kernel.sleep(1)
|
497
|
+
end
|
498
|
+
else
|
499
|
+
success = yield
|
500
|
+
end
|
501
|
+
rescue RuntimeError => e
|
502
|
+
step_error(e, abort_on_error: abort_on_error)
|
503
|
+
end
|
504
|
+
|
505
|
+
step_finish(success)
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
def cp
|
510
|
+
@cp ||= Controlplane.new(config)
|
511
|
+
end
|
512
|
+
|
513
|
+
def ensure_docker_running!
|
514
|
+
result = Shell.cmd("docker", "version", capture_stderr: true)
|
515
|
+
return if result[:success]
|
516
|
+
|
517
|
+
raise "Can't run Docker. Please make sure that it's installed and started, then try again."
|
518
|
+
end
|
519
|
+
|
520
|
+
def run_command_in_latest_image(command, title:)
|
521
|
+
# Need to prefix the command with '.controlplane/'
|
522
|
+
# if it's a file in the '.controlplane' directory,
|
523
|
+
# for backwards compatibility
|
524
|
+
path = Pathname.new("#{config.app_cpln_dir}/#{command}").expand_path
|
525
|
+
command = ".controlplane/#{command}" if File.exist?(path)
|
526
|
+
|
527
|
+
progress.puts("Running #{title}...\n\n")
|
528
|
+
|
529
|
+
begin
|
530
|
+
Cpflow::Cli.start(["run", "-a", config.app, "--image", "latest", "--", command])
|
531
|
+
rescue SystemExit => e
|
532
|
+
progress.puts
|
533
|
+
|
534
|
+
raise "Failed to run #{title}." if e.status.nonzero?
|
535
|
+
|
536
|
+
progress.puts("Finished running #{title}.\n\n")
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class BuildImage < Base
|
5
|
+
NAME = "build-image"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
commit_option
|
9
|
+
].freeze
|
10
|
+
ACCEPTS_EXTRA_OPTIONS = true
|
11
|
+
DESCRIPTION = "Builds and pushes the image to Control Plane"
|
12
|
+
LONG_DESCRIPTION = <<~DESC
|
13
|
+
- Builds and pushes the image to Control Plane
|
14
|
+
- Automatically assigns image numbers, e.g., `app:1`, `app:2`, etc.
|
15
|
+
- Uses `.controlplane/Dockerfile` or a different Dockerfile specified through `dockerfile` in the `.controlplane/controlplane.yml` file
|
16
|
+
- If a commit is provided through `--commit` or `-c`, it will be set as the runtime env var `GIT_COMMIT`
|
17
|
+
- Accepts extra options that are passed to `docker build`
|
18
|
+
DESC
|
19
|
+
|
20
|
+
def call # rubocop:disable Metrics/MethodLength
|
21
|
+
ensure_docker_running!
|
22
|
+
|
23
|
+
dockerfile = config.current[:dockerfile] || "Dockerfile"
|
24
|
+
dockerfile = "#{config.app_cpln_dir}/#{dockerfile}"
|
25
|
+
|
26
|
+
raise "Can't find Dockerfile at '#{dockerfile}'." unless File.exist?(dockerfile)
|
27
|
+
|
28
|
+
progress.puts("Building image from Dockerfile '#{dockerfile}'...\n\n")
|
29
|
+
|
30
|
+
image_name = cp.latest_image_next
|
31
|
+
image_url = "#{config.org}.registry.cpln.io/#{image_name}"
|
32
|
+
|
33
|
+
commit = config.options[:commit]
|
34
|
+
build_args = []
|
35
|
+
build_args.push("GIT_COMMIT=#{commit}") if commit
|
36
|
+
|
37
|
+
cp.image_build(image_url, dockerfile: dockerfile,
|
38
|
+
docker_args: config.args,
|
39
|
+
build_args: build_args)
|
40
|
+
|
41
|
+
progress.puts("\nPushed image to '/org/#{config.org}/image/#{image_name}'.\n\n")
|
42
|
+
|
43
|
+
step("Waiting for image to be available", retry_on_failure: true) do
|
44
|
+
images = cp.query_images["items"]
|
45
|
+
images.find { |image| image["name"] == image_name }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|