cpflow 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/check_cpln_links.yml +19 -0
  3. data/.github/workflows/command_docs.yml +24 -0
  4. data/.github/workflows/rspec-shared.yml +56 -0
  5. data/.github/workflows/rspec.yml +28 -0
  6. data/.github/workflows/rubocop.yml +24 -0
  7. data/.gitignore +18 -0
  8. data/.overcommit.yml +16 -0
  9. data/.rubocop.yml +22 -0
  10. data/.simplecov_spawn.rb +10 -0
  11. data/CHANGELOG.md +259 -0
  12. data/CONTRIBUTING.md +73 -0
  13. data/Gemfile +7 -0
  14. data/Gemfile.lock +126 -0
  15. data/LICENSE +21 -0
  16. data/README.md +546 -0
  17. data/Rakefile +21 -0
  18. data/bin/cpflow +6 -0
  19. data/cpflow +6 -0
  20. data/cpflow.gemspec +41 -0
  21. data/docs/assets/grafana-alert.png +0 -0
  22. data/docs/assets/memcached.png +0 -0
  23. data/docs/assets/sidekiq-pre-stop-hook.png +0 -0
  24. data/docs/commands.md +454 -0
  25. data/docs/dns.md +15 -0
  26. data/docs/migrating.md +262 -0
  27. data/docs/postgres.md +436 -0
  28. data/docs/redis.md +128 -0
  29. data/docs/secrets-and-env-values.md +42 -0
  30. data/docs/tips.md +150 -0
  31. data/docs/troubleshooting.md +6 -0
  32. data/examples/circleci.yml +104 -0
  33. data/examples/controlplane.yml +159 -0
  34. data/lib/command/apply_template.rb +209 -0
  35. data/lib/command/base.rb +540 -0
  36. data/lib/command/build_image.rb +49 -0
  37. data/lib/command/cleanup_images.rb +136 -0
  38. data/lib/command/cleanup_stale_apps.rb +79 -0
  39. data/lib/command/config.rb +48 -0
  40. data/lib/command/copy_image_from_upstream.rb +108 -0
  41. data/lib/command/delete.rb +149 -0
  42. data/lib/command/deploy_image.rb +56 -0
  43. data/lib/command/doctor.rb +47 -0
  44. data/lib/command/env.rb +22 -0
  45. data/lib/command/exists.rb +23 -0
  46. data/lib/command/generate.rb +45 -0
  47. data/lib/command/info.rb +222 -0
  48. data/lib/command/latest_image.rb +19 -0
  49. data/lib/command/logs.rb +49 -0
  50. data/lib/command/maintenance.rb +42 -0
  51. data/lib/command/maintenance_off.rb +62 -0
  52. data/lib/command/maintenance_on.rb +62 -0
  53. data/lib/command/maintenance_set_page.rb +34 -0
  54. data/lib/command/no_command.rb +23 -0
  55. data/lib/command/open.rb +33 -0
  56. data/lib/command/open_console.rb +26 -0
  57. data/lib/command/promote_app_from_upstream.rb +38 -0
  58. data/lib/command/ps.rb +41 -0
  59. data/lib/command/ps_restart.rb +37 -0
  60. data/lib/command/ps_start.rb +51 -0
  61. data/lib/command/ps_stop.rb +82 -0
  62. data/lib/command/ps_wait.rb +40 -0
  63. data/lib/command/run.rb +573 -0
  64. data/lib/command/setup_app.rb +113 -0
  65. data/lib/command/test.rb +23 -0
  66. data/lib/command/version.rb +18 -0
  67. data/lib/constants/exit_code.rb +7 -0
  68. data/lib/core/config.rb +316 -0
  69. data/lib/core/controlplane.rb +552 -0
  70. data/lib/core/controlplane_api.rb +170 -0
  71. data/lib/core/controlplane_api_direct.rb +112 -0
  72. data/lib/core/doctor_service.rb +104 -0
  73. data/lib/core/helpers.rb +26 -0
  74. data/lib/core/shell.rb +100 -0
  75. data/lib/core/template_parser.rb +76 -0
  76. data/lib/cpflow/version.rb +6 -0
  77. data/lib/cpflow.rb +288 -0
  78. data/lib/deprecated_commands.json +9 -0
  79. data/lib/generator_templates/Dockerfile +27 -0
  80. data/lib/generator_templates/controlplane.yml +62 -0
  81. data/lib/generator_templates/entrypoint.sh +8 -0
  82. data/lib/generator_templates/templates/app.yml +21 -0
  83. data/lib/generator_templates/templates/postgres.yml +176 -0
  84. data/lib/generator_templates/templates/rails.yml +36 -0
  85. data/rakelib/create_release.rake +81 -0
  86. data/script/add_command +37 -0
  87. data/script/check_command_docs +3 -0
  88. data/script/check_cpln_links +45 -0
  89. data/script/rename_command +43 -0
  90. data/script/update_command_docs +62 -0
  91. data/templates/app.yml +13 -0
  92. data/templates/daily-task.yml +32 -0
  93. data/templates/maintenance.yml +25 -0
  94. data/templates/memcached.yml +24 -0
  95. data/templates/postgres.yml +32 -0
  96. data/templates/rails.yml +27 -0
  97. data/templates/redis.yml +21 -0
  98. data/templates/redis2.yml +37 -0
  99. data/templates/sidekiq.yml +38 -0
  100. metadata +341 -0
@@ -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