cpflow 3.0.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 (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