Class: Command::Base

Inherits:
Object
  • Object
show all
Includes:
Helpers
Defined in:
lib/command/base.rb

Overview

rubocop:disable Metrics/ClassLength

Constant Summary collapse

VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS =
%w[config].freeze
VALIDATIONS_WITH_ADDITIONAL_OPTIONS =
%w[templates].freeze
ALL_VALIDATIONS =
VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS + VALIDATIONS_WITH_ADDITIONAL_OPTIONS
SUBCOMMAND_NAME =

Used to call the command (‘cpflow SUBCOMMAND_NAME NAME`)

nil
USAGE =

Used to call the command (‘cpflow NAME`) NAME = “” Displayed when running `cpflow help` or `cpflow help NAME` (defaults to `NAME`)

""
REQUIRES_ARGS =

Throws error if ‘true` and no arguments are passed to the command or if `false` and arguments are passed to the command

false
DEFAULT_ARGS =

Default arguments if none are passed to the command

[].freeze
OPTIONS =

Options for the command (use option methods below)

[].freeze
ACCEPTS_EXTRA_OPTIONS =

Does not throw error if ‘true` and extra options that are not specified in `OPTIONS` are passed to the command

false
EXAMPLES =

Displayed when running ‘cpflow help` DESCRIPTION = “” Displayed when running `cpflow help NAME` LONG_DESCRIPTION = “” Displayed along with `LONG_DESCRIPTION` when running `cpflow help NAME`

""
HIDE =

If ‘true`, hides the command from `cpflow help`

false
WITH_INFO_HEADER =

Whether or not to show key information like ORG and APP name in commands

true
VALIDATIONS =

Which validations to run before the command

%w[config].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers

normalize_command_name, normalize_option_name, random_four_digits, strip_str_and_validate

Constructor Details

#initialize(config) ⇒ Base

Returns a new instance of Base.



44
45
46
# File 'lib/command/base.rb', line 44

def initialize(config)
  @config = config
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



7
8
9
# File 'lib/command/base.rb', line 7

def config
  @config
end

Class Method Details

.add_app_identity_option(required: false) ⇒ Object



447
448
449
450
451
452
453
454
455
456
# File 'lib/command/base.rb', line 447

def self.add_app_identity_option(required: false)
  {
    name: :add_app_identity,
    params: {
      desc: "Adds app identity template if it does not exist",
      type: :boolean,
      required: required
    }
  }
end

.all_commandsObject

rubocop:disable Metrics/MethodLength



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/command/base.rb', line 48

def self.all_commands # rubocop:disable Metrics/MethodLength
  Dir["#{__dir__}/**/*.rb"].each_with_object({}) do |file, result|
    content = File.read(file)

    classname = content.match(/^\s+class (?!Base\b)(\w+) < (?:.*(?!Command::)Base)(?:$| .*$)/)&.captures&.first
    next unless classname

    namespaces = content.scan(/^\s+module (\w+)/).flatten
    full_classname = [*namespaces, classname].join("::").prepend("::")

    command_key = File.basename(file, ".rb")
    prefix = namespaces[1..].map(&:downcase).join("_")
    command_key.prepend(prefix.concat("_")) unless prefix.empty?

    result[command_key.to_sym] = Object.const_get(full_classname)
  end
end

.all_optionsObject

rubocop:enable Metrics/MethodLength



482
483
484
# File 'lib/command/base.rb', line 482

def self.all_options
  methods.grep(/_option$/).map { |method| send(method.to_s) }
end

.all_options_by_key_nameObject



486
487
488
489
490
491
# File 'lib/command/base.rb', line 486

def self.all_options_by_key_name
  all_options.each_with_object({}) do |option, result|
    option[:params][:aliases]&.each { |current_alias| result[current_alias.to_s] = option }
    result["--#{option[:name]}"] = option
  end
end

.app_option(required: false) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/command/base.rb', line 84

def self.app_option(required: false)
  {
    name: :app,
    params: {
      aliases: ["-a"],
      banner: "APP_NAME",
      desc: "Application name",
      type: :string,
      required: required
    }
  }
end

.commit_option(required: false) ⇒ Object



150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/command/base.rb', line 150

def self.commit_option(required: false)
  {
    name: :commit,
    params: {
      aliases: ["-c"],
      banner: "COMMIT_HASH",
      desc: "Commit hash",
      type: :string,
      required: required
    }
  }
end

.common_optionsObject



66
67
68
# File 'lib/command/base.rb', line 66

def self.common_options
  [org_option, verbose_option, trace_option]
end

.cpu_option(required: false) ⇒ Object



368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/command/base.rb', line 368

def self.cpu_option(required: false)
  {
    name: :cpu,
    params: {
      banner: "CPU",
      desc: "Overrides CPU millicores " \
            "(e.g., '100m' for 100 millicores, '1' for 1 core)",
      type: :string,
      required: required,
      valid_regex: /^\d+m?$/
    }
  }
end

.detached_option(required: false) ⇒ Object



357
358
359
360
361
362
363
364
365
366
# File 'lib/command/base.rb', line 357

def self.detached_option(required: false)
  {
    name: :detached,
    params: {
      desc: "Runs non-interactive command, detaches, and prints commands to log and stop the job",
      type: :boolean,
      required: required
    }
  }
end

.dir_option(required: false) ⇒ Object



469
470
471
472
473
474
475
476
477
478
479
# File 'lib/command/base.rb', line 469

def self.dir_option(required: false)
  {
    name: :dir,
    params: {
      banner: "DIR",
      desc: "Output directory",
      type: :string,
      required: required
    }
  }
end

.docker_context_optionObject



458
459
460
461
462
463
464
465
466
467
# File 'lib/command/base.rb', line 458

def self.docker_context_option
  {
    name: :docker_context,
    params: {
      desc: "Path to the docker build context directory",
      type: :string,
      required: false
    }
  }
end

.domain_option(required: false) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
# File 'lib/command/base.rb', line 176

def self.domain_option(required: false)
  {
    name: :domain,
    params: {
      banner: "DOMAIN_NAME",
      desc: "Domain name",
      type: :string,
      required: required
    }
  }
end

.entrypoint_option(required: false) ⇒ Object



396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/command/base.rb', line 396

def self.entrypoint_option(required: false)
  {
    name: :entrypoint,
    params: {
      banner: "ENTRYPOINT",
      desc: "Overrides entrypoint " \
            "(must be a single command or a script path that exists in the container)",
      type: :string,
      required: required,
      valid_regex: /^\S+$/
    }
  }
end

.image_option(required: false) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/command/base.rb', line 123

def self.image_option(required: false)
  {
    name: :image,
    params: {
      aliases: ["-i"],
      banner: "IMAGE_NAME",
      desc: "Image name",
      type: :string,
      required: required
    }
  }
end

.interactive_option(required: false) ⇒ Object



346
347
348
349
350
351
352
353
354
355
# File 'lib/command/base.rb', line 346

def self.interactive_option(required: false)
  {
    name: :interactive,
    params: {
      desc: "Runs interactive command",
      type: :boolean,
      required: required
    }
  }
end

.location_option(required: false) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/command/base.rb', line 163

def self.location_option(required: false)
  {
    name: :location,
    params: {
      aliases: ["-l"],
      banner: "LOCATION_NAME",
      desc: "Location name",
      type: :string,
      required: required
    }
  }
end

.log_method_option(required: false) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/command/base.rb', line 136

def self.log_method_option(required: false)
  {
    name: :log_method,
    params: {
      type: :numeric,
      banner: "LOG_METHOD",
      desc: "Log method",
      required: required,
      valid_values: [1, 2, 3],
      default: 3
    }
  }
end

.logs_limit_option(required: false) ⇒ Object



319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/command/base.rb', line 319

def self.logs_limit_option(required: false)
  {
    name: :limit,
    params: {
      banner: "NUMBER",
      desc: "Limit on number of log entries to show",
      type: :numeric,
      required: required,
      default: 200
    }
  }
end

.logs_since_option(required: false) ⇒ Object



332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/command/base.rb', line 332

def self.logs_since_option(required: false)
  {
    name: :since,
    params: {
      banner: "DURATION",
      desc: "Loopback window for showing logs " \
            "(see https://www.npmjs.com/package/parse-duration for the accepted formats, e.g., '1h')",
      type: :string,
      required: required,
      default: "1h"
    }
  }
end

.memory_option(required: false) ⇒ Object



382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/command/base.rb', line 382

def self.memory_option(required: false)
  {
    name: :memory,
    params: {
      banner: "MEMORY",
      desc: "Overrides memory size " \
            "(e.g., '100Mi' for 100 mebibytes, '1Gi' for 1 gibibyte)",
      type: :string,
      required: required,
      valid_regex: /^\d+[MG]i$/
    }
  }
end

.org_option(required: false) ⇒ Object

rubocop:disable Metrics/MethodLength



71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/command/base.rb', line 71

def self.org_option(required: false)
  {
    name: :org,
    params: {
      aliases: ["-o"],
      banner: "ORG_NAME",
      desc: "Organization name",
      type: :string,
      required: required
    }
  }
end

.replica_option(required: false) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/command/base.rb', line 110

def self.replica_option(required: false)
  {
    name: :replica,
    params: {
      aliases: ["-r"],
      banner: "REPLICA_NAME",
      desc: "Replica name",
      type: :string,
      required: required
    }
  }
end

.run_release_phase_option(required: false) ⇒ Object



308
309
310
311
312
313
314
315
316
317
# File 'lib/command/base.rb', line 308

def self.run_release_phase_option(required: false)
  {
    name: :run_release_phase,
    params: {
      desc: "Runs release phase",
      type: :boolean,
      required: required
    }
  }
end

.skip_confirm_option(required: false) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/command/base.rb', line 201

def self.skip_confirm_option(required: false)
  {
    name: :yes,
    params: {
      aliases: ["-y"],
      banner: "SKIP_CONFIRM",
      desc: "Skip confirmation",
      type: :boolean,
      required: required
    }
  }
end

.skip_post_creation_hook_option(required: false) ⇒ Object



425
426
427
428
429
430
431
432
433
434
# File 'lib/command/base.rb', line 425

def self.skip_post_creation_hook_option(required: false)
  {
    name: :skip_post_creation_hook,
    params: {
      desc: "Skips post-creation hook",
      type: :boolean,
      required: required
    }
  }
end

.skip_pre_deletion_hook_option(required: false) ⇒ Object



436
437
438
439
440
441
442
443
444
445
# File 'lib/command/base.rb', line 436

def self.skip_pre_deletion_hook_option(required: false)
  {
    name: :skip_pre_deletion_hook,
    params: {
      desc: "Skips pre-deletion hook",
      type: :boolean,
      required: required
    }
  }
end

.skip_secret_access_binding_option(required: false) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
# File 'lib/command/base.rb', line 285

def self.skip_secret_access_binding_option(required: false)
  {
    name: :skip_secret_access_binding,
    new_name: :skip_secrets_setup,
    params: {
      desc: "Skips secret access binding",
      type: :boolean,
      required: required
    }
  }
end

.skip_secrets_setup_option(required: false) ⇒ Object



297
298
299
300
301
302
303
304
305
306
# File 'lib/command/base.rb', line 297

def self.skip_secrets_setup_option(required: false)
  {
    name: :skip_secrets_setup,
    params: {
      desc: "Skips secrets setup",
      type: :boolean,
      required: required
    }
  }
end

.terminal_size_option(required: false) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/command/base.rb', line 238

def self.terminal_size_option(required: false)
  {
    name: :terminal_size,
    params: {
      banner: "ROWS,COLS",
      desc: "Override remote terminal size (e.g. `--terminal-size 10,20`)",
      type: :string,
      required: required,
      valid_regex: /^\d+,\d+$/
    }
  }
end

.trace_option(required: false) ⇒ Object



274
275
276
277
278
279
280
281
282
283
# File 'lib/command/base.rb', line 274

def self.trace_option(required: false)
  {
    name: :trace,
    params: {
      desc: "Shows trace of API calls. WARNING: may contain sensitive data",
      type: :boolean,
      required: required
    }
  }
end

.upstream_token_option(required: false) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/command/base.rb', line 188

def self.upstream_token_option(required: false)
  {
    name: :upstream_token,
    params: {
      aliases: ["-t"],
      banner: "UPSTREAM_TOKEN",
      desc: "Upstream token",
      type: :string,
      required: required
    }
  }
end

.use_local_token_option(required: false) ⇒ Object



227
228
229
230
231
232
233
234
235
236
# File 'lib/command/base.rb', line 227

def self.use_local_token_option(required: false)
  {
    name: :use_local_token,
    params: {
      desc: "Override remote CPLN_TOKEN with local token",
      type: :boolean,
      required: required
    }
  }
end

.validations_option(required: false) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/command/base.rb', line 410

def self.validations_option(required: false)
  {
    name: :validations,
    params: {
      banner: "VALIDATION_1,VALIDATION_2,...",
      desc: "Which validations to run " \
            "(must be separated by a comma)",
      type: :string,
      required: required,
      default: VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS.join(","),
      valid_regex: /^(#{ALL_VALIDATIONS.join('|')})(,(#{ALL_VALIDATIONS.join('|')}))*$/
    }
  }
end

.verbose_option(required: false) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
# File 'lib/command/base.rb', line 262

def self.verbose_option(required: false)
  {
    name: :verbose,
    params: {
      aliases: ["-d"],
      desc: "Shows detailed logs",
      type: :boolean,
      required: required
    }
  }
end

.version_option(required: false) ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/command/base.rb', line 214

def self.version_option(required: false)
  {
    name: :version,
    params: {
      aliases: ["-v"],
      banner: "VERSION",
      desc: "Displays the current version of the CLI",
      type: :boolean,
      required: required
    }
  }
end

.wait_option(title = "", required: false) ⇒ Object



251
252
253
254
255
256
257
258
259
260
# File 'lib/command/base.rb', line 251

def self.wait_option(title = "", required: false)
  {
    name: :wait,
    params: {
      desc: "Waits for #{title}",
      type: :boolean,
      required: required
    }
  }
end

.workload_option(required: false) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/command/base.rb', line 97

def self.workload_option(required: false)
  {
    name: :workload,
    params: {
      aliases: ["-w"],
      banner: "WORKLOAD_NAME",
      desc: "Workload name",
      type: :string,
      required: required
    }
  }
end

Instance Method Details

#args_join(args) ⇒ Object

NOTE: use simplified variant atm, as shelljoin do different escaping TODO: most probably need better logic for escaping various quotes



495
496
497
# File 'lib/command/base.rb', line 495

def args_join(args)
  args.join(" ")
end

#cpObject



549
550
551
# File 'lib/command/base.rb', line 549

def cp
  @cp ||= Controlplane.new(config)
end

#ensure_docker_running!Object



553
554
555
556
557
558
# File 'lib/command/base.rb', line 553

def ensure_docker_running!
  result = Shell.cmd("docker", "version", capture_stderr: true)
  return if result[:success]

  raise "Can't run Docker. Please make sure that it's installed and started, then try again."
end

#progressObject



499
500
501
# File 'lib/command/base.rb', line 499

def progress
  $stderr
end

#run_command_in_latest_image(command, title:) ⇒ Object



560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'lib/command/base.rb', line 560

def run_command_in_latest_image(command, title:)
  # Need to prefix the command with '.controlplane/'
  # if it's a file in the '.controlplane' directory,
  # for backwards compatibility
  path = Pathname.new("#{config.app_cpln_dir}/#{command}").expand_path
  command = ".controlplane/#{command}" if File.exist?(path)

  progress.puts("Running #{title}...\n\n")

  begin
    run_cpflow_command("run", "-a", config.app, "--image", "latest", "--", command)
  rescue SystemExit => e
    progress.puts

    raise "Failed to run #{title}." if e.status.nonzero?

    progress.puts("Finished running #{title}.\n\n")
  end
end

#run_cpflow_command(command, *args) ⇒ Object



580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/command/base.rb', line 580

def run_cpflow_command(command, *args)
  common_args = []

  self.class.common_options.each do |option|
    value = config.options[option[:name]]
    next if value.nil?

    name = "--#{option[:name].to_s.tr('_', '-')}"
    common_args.push(name, value)
  end

  Cpflow::Cli.start([command, *common_args, *args])
end

#step(message, abort_on_error: true, retry_on_failure: false, max_retry_count: 1000, wait: 1, &block) ⇒ Object

rubocop:disable Metrics/MethodLength



512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/command/base.rb', line 512

def step(message, abort_on_error: true, retry_on_failure: false, max_retry_count: 1000, wait: 1, &block) # rubocop:disable Metrics/MethodLength
  progress.print("#{message}...")

  Shell.use_tmp_stderr do
    success = false

    begin
      success =
        if retry_on_failure
          with_retry(max_retry_count: max_retry_count, wait: wait, &block)
        else
          yield
        end
    rescue RuntimeError => e
      Shell.write_to_tmp_stderr(e.message)
    end

    step_finish(success, abort_on_error: abort_on_error)
  end
end

#step_finish(success, abort_on_error: true) ⇒ Object



503
504
505
506
507
508
509
510
# File 'lib/command/base.rb', line 503

def step_finish(success, abort_on_error: true)
  if success
    progress.puts(" #{Shell.color('done!', :green)}")
  else
    progress.puts(" #{Shell.color('failed!', :red)}\n\n#{Shell.read_from_tmp_stderr}\n\n")
    exit(ExitCode::ERROR_DEFAULT) if abort_on_error
  end
end

#with_retry(max_retry_count:, wait:) ⇒ Object



533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/command/base.rb', line 533

def with_retry(max_retry_count:, wait:)
  retry_count = 0
  success = false

  while !success && retry_count <= max_retry_count
    success = yield
    break if success

    progress.print(".")
    Kernel.sleep(wait)
    retry_count += 1
  end

  success
end