cpl 2.0.2 → 2.1.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.
data/lib/command/base.rb CHANGED
@@ -8,6 +8,10 @@ module Command
8
8
 
9
9
  include Helpers
10
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
+
11
15
  # Used to call the command (`cpl NAME`)
12
16
  # NAME = ""
13
17
  # Displayed when running `cpl help` or `cpl help NAME` (defaults to `NAME`)
@@ -32,8 +36,8 @@ module Command
32
36
  HIDE = false
33
37
  # Whether or not to show key information like ORG and APP name in commands
34
38
  WITH_INFO_HEADER = true
35
-
36
- NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE"
39
+ # Which validations to run before the command
40
+ VALIDATIONS = %w[config].freeze
37
41
 
38
42
  def initialize(config)
39
43
  @config = config
@@ -269,6 +273,7 @@ module Command
269
273
  def self.skip_secret_access_binding_option(required: false)
270
274
  {
271
275
  name: :skip_secret_access_binding,
276
+ new_name: :skip_secrets_setup,
272
277
  params: {
273
278
  desc: "Skips secret access binding",
274
279
  type: :boolean,
@@ -277,6 +282,17 @@ module Command
277
282
  }
278
283
  end
279
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
+
280
296
  def self.run_release_phase_option(required: false)
281
297
  {
282
298
  name: :run_release_phase,
@@ -378,57 +394,65 @@ module Command
378
394
  }
379
395
  }
380
396
  end
381
- # rubocop:enable Metrics/MethodLength
382
397
 
383
- def self.all_options
384
- methods.grep(/_option$/).map { |method| send(method.to_s) }
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
+ }
385
411
  end
386
412
 
387
- def self.all_options_by_key_name
388
- all_options.each_with_object({}) do |option, result|
389
- option[:params][:aliases]&.each { |current_alias| result[current_alias.to_s] = option }
390
- result["--#{option[:name]}"] = option
391
- end
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
+ }
392
422
  end
393
423
 
394
- def latest_image_from(items, app_name: config.app, name_only: true)
395
- matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") }
396
-
397
- # Or special string to indicate no image available
398
- if matching_items.empty?
399
- name_only ? "#{app_name}:#{NO_IMAGE_AVAILABLE}" : nil
400
- else
401
- latest_item = matching_items.max_by { |item| extract_image_number(item["name"]) }
402
- name_only ? latest_item["name"] : latest_item
403
- end
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
+ }
404
433
  end
405
434
 
406
- def latest_image(app = config.app, org = config.org, refresh: false)
407
- @latest_image ||= {}
408
- @latest_image[app] = nil if refresh
409
- @latest_image[app] ||=
410
- begin
411
- items = cp.query_images(app, org)["items"]
412
- latest_image_from(items, app_name: app)
413
- end
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
+ }
414
444
  end
445
+ # rubocop:enable Metrics/MethodLength
415
446
 
416
- def latest_image_next(app = config.app, org = config.org, commit: nil)
417
- # debugger
418
- commit ||= config.options[:commit]
419
-
420
- @latest_image_next ||= {}
421
- @latest_image_next[app] ||= begin
422
- latest_image_name = latest_image(app, org)
423
- image = latest_image_name.split(":").first
424
- image += ":#{extract_image_number(latest_image_name) + 1}"
425
- image += "_#{commit}" if commit
426
- image
427
- end
447
+ def self.all_options
448
+ methods.grep(/_option$/).map { |method| send(method.to_s) }
428
449
  end
429
450
 
430
- def extract_image_commit(image_name)
431
- image_name.match(/_(\h+)$/)&.captures&.first
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
432
456
  end
433
457
 
434
458
  # NOTE: use simplified variant atm, as shelljoin do different escaping
@@ -486,30 +510,6 @@ module Command
486
510
  @cp ||= Controlplane.new(config)
487
511
  end
488
512
 
489
- def app_location_link
490
- "/org/#{config.org}/location/#{config.location}"
491
- end
492
-
493
- def app_image_link
494
- "/org/#{config.org}/image/#{latest_image}"
495
- end
496
-
497
- def app_identity
498
- "#{config.app}-identity"
499
- end
500
-
501
- def app_identity_link
502
- "/org/#{config.org}/gvc/#{config.app}/identity/#{app_identity}"
503
- end
504
-
505
- def app_secrets
506
- config.current[:secrets_name] || "#{config.app_prefix}-secrets"
507
- end
508
-
509
- def app_secrets_policy
510
- config.current[:secrets_policy_name] || "#{app_secrets}-policy"
511
- end
512
-
513
513
  def ensure_docker_running!
514
514
  result = Shell.cmd("docker", "version", capture_stderr: true)
515
515
  return if result[:success]
@@ -517,13 +517,24 @@ module Command
517
517
  raise "Can't run Docker. Please make sure that it's installed and started, then try again."
518
518
  end
519
519
 
520
- private
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")
521
528
 
522
- # returns 0 if no prior image
523
- def extract_image_number(image_name)
524
- return 0 if image_name.end_with?(NO_IMAGE_AVAILABLE)
529
+ begin
530
+ Cpl::Cli.start(["run", "-a", config.app, "--image", "latest", "--", command])
531
+ rescue SystemExit => e
532
+ progress.puts
525
533
 
526
- image_name.match(/:(\d+)/)&.captures&.first.to_i
534
+ raise "Failed to run #{title}." if e.status.nonzero?
535
+
536
+ progress.puts("Finished running #{title}.\n\n")
537
+ end
527
538
  end
528
539
  end
529
540
  end
@@ -27,7 +27,7 @@ module Command
27
27
 
28
28
  progress.puts("Building image from Dockerfile '#{dockerfile}'...\n\n")
29
29
 
30
- image_name = latest_image_next
30
+ image_name = cp.latest_image_next
31
31
  image_url = "#{config.org}.registry.cpln.io/#{image_name}"
32
32
 
33
33
  commit = config.options[:commit]
@@ -41,7 +41,7 @@ module Command
41
41
  progress.puts("\nPushed image to '/org/#{config.org}/image/#{image_name}'.\n\n")
42
42
 
43
43
  step("Waiting for image to be available", retry_on_failure: true) do
44
- image_name == latest_image(refresh: true)
44
+ image_name == cp.latest_image(refresh: true)
45
45
  end
46
46
  end
47
47
  end
@@ -56,7 +56,7 @@ module Command
56
56
  return images unless cp.fetch_gvc(app)
57
57
 
58
58
  # If app exists, remove latest image, because we don't want to delete the image that is currently deployed
59
- latest_image_name = latest_image_from(images, app_name: app)
59
+ latest_image_name = cp.latest_image_from(images, app_name: app)
60
60
  images.reject { |image| image["name"] == latest_image_name }
61
61
  end
62
62
 
@@ -49,7 +49,7 @@ module Command
49
49
  app_name = gvc["name"]
50
50
 
51
51
  images = cp.query_images(app_name)["items"].select { |item| item["name"].start_with?("#{app_name}:") }
52
- image = latest_image_from(images, app_name: app_name, name_only: false)
52
+ image = cp.latest_image_from(images, app_name: app_name, name_only: false)
53
53
  next unless image
54
54
 
55
55
  created_date = DateTime.parse(image["created"])
@@ -66,8 +66,8 @@ module Command
66
66
  step("Fetching upstream image URL") do
67
67
  cp.profile_switch(@upstream_profile)
68
68
  upstream_image = config.options[:image]
69
- upstream_image = latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
70
- @commit = extract_image_commit(upstream_image)
69
+ upstream_image = cp.latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
70
+ @commit = cp.extract_image_commit(upstream_image)
71
71
  @upstream_image_url = "#{@upstream_org}.registry.cpln.io/#{upstream_image}"
72
72
  end
73
73
  end
@@ -75,7 +75,7 @@ module Command
75
75
  def fetch_app_image_url
76
76
  step("Fetching app image URL") do
77
77
  cp.profile_switch("default")
78
- app_image = latest_image_next(config.app, config.org, commit: @commit)
78
+ app_image = cp.latest_image_next(config.app, config.org, commit: @commit)
79
79
  @app_image_url = "#{config.org}.registry.cpln.io/#{app_image}"
80
80
  end
81
81
  end
@@ -6,13 +6,17 @@ module Command
6
6
  OPTIONS = [
7
7
  app_option(required: true),
8
8
  workload_option,
9
- skip_confirm_option
9
+ skip_confirm_option,
10
+ skip_pre_deletion_hook_option
10
11
  ].freeze
11
12
  DESCRIPTION = "Deletes the whole app (GVC with all workloads, all volumesets and all images) or a specific workload"
12
13
  LONG_DESCRIPTION = <<~DESC
13
14
  - Deletes the whole app (GVC with all workloads, all volumesets and all images) or a specific workload
14
15
  - Also unbinds the app from the secrets policy, as long as both the identity and the policy exist (and are bound)
15
16
  - Will ask for explicit user confirmation
17
+ - Runs a pre-deletion hook before the app is deleted if `hooks.pre_deletion` is specified in the `.controlplane/controlplane.yml` file
18
+ - If the hook exits with a non-zero code, the command will stop executing and also exit with a non-zero code
19
+ - Use `--skip-pre-deletion-hook` to skip the hook if specified in `controlplane.yml`
16
20
  DESC
17
21
  EXAMPLES = <<~EX
18
22
  ```sh
@@ -51,6 +55,7 @@ module Command
51
55
  check_images
52
56
  return unless confirm_delete(config.app)
53
57
 
58
+ run_pre_deletion_hook unless config.options[:skip_pre_deletion_hook]
54
59
  unbind_identity_from_policy
55
60
  delete_volumesets
56
61
  delete_gvc
@@ -119,19 +124,26 @@ module Command
119
124
  end
120
125
 
121
126
  def unbind_identity_from_policy
122
- return if cp.fetch_identity(app_identity).nil?
127
+ return if cp.fetch_identity(config.identity).nil?
123
128
 
124
- policy = cp.fetch_policy(app_secrets_policy)
129
+ policy = cp.fetch_policy(config.secrets_policy)
125
130
  return if policy.nil?
126
131
 
127
132
  is_bound = policy["bindings"].any? do |binding|
128
- binding["principalLinks"].any? { |link| link == app_identity_link }
133
+ binding["principalLinks"].any? { |link| link == config.identity_link }
129
134
  end
130
135
  return unless is_bound
131
136
 
132
137
  step("Unbinding identity from policy for app '#{config.app}'") do
133
- cp.unbind_identity_from_policy(app_identity_link, app_secrets_policy)
138
+ cp.unbind_identity_from_policy(config.identity_link, config.secrets_policy)
134
139
  end
135
140
  end
141
+
142
+ def run_pre_deletion_hook
143
+ pre_deletion_hook = config.current.dig(:hooks, :pre_deletion)
144
+ return unless pre_deletion_hook
145
+
146
+ run_command_in_latest_image(pre_deletion_hook, title: "pre-deletion hook")
147
+ end
136
148
  end
137
149
  end
@@ -10,9 +10,9 @@ module Command
10
10
  DESCRIPTION = "Deploys the latest image to app workloads, and runs a release script (optional)"
11
11
  LONG_DESCRIPTION = <<~DESC
12
12
  - Deploys the latest image to app workloads
13
- - Optionally runs a release script before deploying if specified through `release_script` in the `.controlplane/controlplane.yml` file and `--run-release-phase` is provided
13
+ - Runs a release script before deploying if `release_script` is specified in the `.controlplane/controlplane.yml` file and `--run-release-phase` is provided
14
14
  - The release script is run in the context of `cpl run` with the latest image
15
- - The deploy will fail if the release script exits with a non-zero code or doesn't exist
15
+ - If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code
16
16
  DESC
17
17
 
18
18
  def call # rubocop:disable Metrics/MethodLength
@@ -20,7 +20,7 @@ module Command
20
20
 
21
21
  deployed_endpoints = {}
22
22
 
23
- image = latest_image
23
+ image = cp.latest_image
24
24
  if cp.fetch_image_details(image).nil?
25
25
  raise "Image '#{image}' does not exist in the Docker repository on Control Plane " \
26
26
  "(see https://console.cpln.io/console/org/#{config.org}/repository/#{config.app}). " \
@@ -48,24 +48,9 @@ module Command
48
48
 
49
49
  private
50
50
 
51
- def run_release_script # rubocop:disable Metrics/MethodLength
52
- release_script_name = config[:release_script]
53
- release_script_path = Pathname.new("#{config.app_cpln_dir}/#{release_script_name}").expand_path
54
-
55
- raise "Can't find release script in '#{release_script_path}'." unless File.exist?(release_script_path)
56
-
57
- progress.puts("Running release script...\n\n")
58
-
59
- release_script = File.read(release_script_path)
60
- begin
61
- Cpl::Cli.start(["run", "-a", config.app, "--image", "latest", "--", release_script])
62
- rescue SystemExit => e
63
- progress.puts
64
-
65
- raise "Failed to run release script." if e.status.nonzero?
66
-
67
- progress.puts("Finished running release script.\n\n")
68
- end
51
+ def run_release_script
52
+ release_script = config[:release_script]
53
+ run_command_in_latest_image(release_script, title: "release script")
69
54
  end
70
55
  end
71
56
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Command
4
+ class Doctor < Base
5
+ NAME = "doctor"
6
+ OPTIONS = [
7
+ validations_option,
8
+ app_option
9
+ ].freeze
10
+ DESCRIPTION = "Runs validations"
11
+ LONG_DESCRIPTION = <<~DESC
12
+ - Runs validations
13
+ DESC
14
+ EXAMPLES = <<~EX
15
+ ```sh
16
+ # Runs all validations that don't require additional options by default.
17
+ cpl doctor
18
+
19
+ # Runs config validation.
20
+ cpl doctor --validations config
21
+
22
+ # Runs templates validation (requires app).
23
+ cpl doctor --validations templates -a $APP_NAME
24
+ ```
25
+ EX
26
+ VALIDATIONS = [].freeze
27
+
28
+ def call
29
+ validations = config.options[:validations].split(",")
30
+ ensure_required_options!(validations)
31
+
32
+ doctor_service = DoctorService.new(config)
33
+ doctor_service.run_validations(validations)
34
+ end
35
+
36
+ private
37
+
38
+ def ensure_required_options!(validations)
39
+ validations.each do |validation|
40
+ case validation
41
+ when "templates"
42
+ raise "App is required for templates validation." unless config.app
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -13,7 +13,7 @@ module Command
13
13
  WITH_INFO_HEADER = false
14
14
 
15
15
  def call
16
- puts latest_image
16
+ puts cp.latest_image
17
17
  end
18
18
  end
19
19
  end
@@ -10,6 +10,7 @@ module Command
10
10
  DESC
11
11
  HIDE = true
12
12
  WITH_INFO_HEADER = false
13
+ VALIDATIONS = [].freeze
13
14
 
14
15
  def call
15
16
  if config.options[:version]
@@ -14,7 +14,7 @@ module Command
14
14
  - Runs `cpl copy-image-from-upstream` to copy the latest image from upstream
15
15
  - Runs `cpl deploy-image` to deploy the image
16
16
  - If `.controlplane/controlplane.yml` includes the `release_script`, `cpl deploy-image` will use the `--run-release-phase` option
17
- - The deploy will fail if the release script exits with a non-zero code
17
+ - If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code
18
18
  DESC
19
19
 
20
20
  def call
data/lib/command/run.rb CHANGED
@@ -183,7 +183,7 @@ module Command
183
183
  # Override image if specified
184
184
  image = config.options[:image]
185
185
  image_link = if image
186
- image = latest_image if image == "latest"
186
+ image = cp.latest_image if image == "latest"
187
187
  "/org/#{config.org}/image/#{image}"
188
188
  else
189
189
  original_container_spec["image"]
@@ -5,18 +5,27 @@ module Command
5
5
  NAME = "setup-app"
6
6
  OPTIONS = [
7
7
  app_option(required: true),
8
- skip_secret_access_binding_option
8
+ skip_secret_access_binding_option,
9
+ skip_secrets_setup_option,
10
+ skip_post_creation_hook_option
9
11
  ].freeze
10
12
  DESCRIPTION = "Creates an app and all its workloads"
11
13
  LONG_DESCRIPTION = <<~DESC
12
14
  - Creates an app and all its workloads
13
15
  - Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
14
- - This should only be used for temporary apps like review apps, never for persistent apps like production (to update workloads for those, use 'cpl apply-template' instead)
15
- - Automatically binds the app to the secrets policy, as long as both the identity and the policy exist
16
- - Use `--skip-secret-access-binding` to prevent the automatic bind
16
+ - This should only be used for temporary apps like review apps, never for persistent apps like production or staging (to update workloads for those, use 'cpl apply-template' instead)
17
+ - Configures app to have org-level secrets with default name "{APP_PREFIX}-secrets"
18
+ using org-level policy with default name "{APP_PREFIX}-secrets-policy" (names can be customized, see docs)
19
+ - Creates identity for secrets if it does not exist
20
+ - Use `--skip-secrets-setup` to prevent the automatic setup of secrets,
21
+ or set it through `skip_secrets_setup` in the `.controlplane/controlplane.yml` file
22
+ - Runs a post-creation hook after the app is created if `hooks.post_creation` is specified in the `.controlplane/controlplane.yml` file
23
+ - If the hook exits with a non-zero code, the command will stop executing and also exit with a non-zero code
24
+ - Use `--skip-post-creation-hook` to skip the hook if specified in `controlplane.yml`
17
25
  DESC
26
+ VALIDATIONS = %w[config templates].freeze
18
27
 
19
- def call # rubocop:disable Metrics/MethodLength
28
+ def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
20
29
  templates = config[:setup_app_templates]
21
30
 
22
31
  app = cp.fetch_gvc
@@ -26,24 +35,79 @@ module Command
26
35
  "or run 'cpl apply-template #{templates.join(' ')} -a #{config.app}'."
27
36
  end
28
37
 
29
- Cpl::Cli.start(["apply-template", *templates, "-a", config.app])
38
+ skip_secrets_setup = config.options[:skip_secret_access_binding] ||
39
+ config.options[:skip_secrets_setup] || config.current[:skip_secrets_setup]
30
40
 
31
- return if config.options[:skip_secret_access_binding]
41
+ create_secret_and_policy_if_not_exist unless skip_secrets_setup
42
+
43
+ args = []
44
+ args.push("--add-app-identity") unless skip_secrets_setup
45
+ Cpl::Cli.start(["apply-template", *templates, "-a", config.app, *args])
46
+
47
+ bind_identity_to_policy unless skip_secrets_setup
48
+ run_post_creation_hook unless config.options[:skip_post_creation_hook]
49
+ end
50
+
51
+ private
52
+
53
+ def create_secret_and_policy_if_not_exist
54
+ create_secret_if_not_exists
55
+ create_policy_if_not_exists
32
56
 
33
57
  progress.puts
58
+ end
34
59
 
35
- if cp.fetch_identity(app_identity).nil? || cp.fetch_policy(app_secrets_policy).nil?
36
- raise "Can't bind identity to policy: identity '#{app_identity}' or " \
37
- "policy '#{app_secrets_policy}' doesn't exist. " \
38
- "Please create them or use `--skip-secret-access-binding` to ignore this message." \
39
- "You can also set a custom secrets name with `secrets_name` " \
40
- "and a custom secrets policy name with `secrets_policy_name` " \
41
- "in the `.controlplane/controlplane.yml` file."
60
+ def create_secret_if_not_exists
61
+ if cp.fetch_secret(config.secrets)
62
+ progress.puts("Secret '#{config.secrets}' already exists. Skipping creation...")
63
+ else
64
+ step("Creating secret '#{config.secrets}'") do
65
+ cp.apply_hash(build_secret_hash)
66
+ end
42
67
  end
68
+ end
43
69
 
44
- step("Binding identity to policy") do
45
- cp.bind_identity_to_policy(app_identity_link, app_secrets_policy)
70
+ def create_policy_if_not_exists
71
+ if cp.fetch_policy(config.secrets_policy)
72
+ progress.puts("Policy '#{config.secrets_policy}' already exists. Skipping creation...")
73
+ else
74
+ step("Creating policy '#{config.secrets_policy}'") do
75
+ cp.apply_hash(build_policy_hash)
76
+ end
46
77
  end
47
78
  end
79
+
80
+ def build_secret_hash
81
+ {
82
+ "kind" => "secret",
83
+ "name" => config.secrets,
84
+ "type" => "dictionary",
85
+ "data" => {}
86
+ }
87
+ end
88
+
89
+ def build_policy_hash
90
+ {
91
+ "kind" => "policy",
92
+ "name" => config.secrets_policy,
93
+ "targetKind" => "secret",
94
+ "targetLinks" => ["//secret/#{config.secrets}"]
95
+ }
96
+ end
97
+
98
+ def bind_identity_to_policy
99
+ progress.puts
100
+
101
+ step("Binding identity '#{config.identity}' to policy '#{config.secrets_policy}'") do
102
+ cp.bind_identity_to_policy(config.identity_link, config.secrets_policy)
103
+ end
104
+ end
105
+
106
+ def run_post_creation_hook
107
+ post_creation_hook = config.current.dig(:hooks, :post_creation)
108
+ return unless post_creation_hook
109
+
110
+ run_command_in_latest_image(post_creation_hook, title: "post-creation hook")
111
+ end
48
112
  end
49
113
  end
data/lib/command/test.rb CHANGED
@@ -11,6 +11,7 @@ module Command
11
11
  - For debugging purposes
12
12
  DESC
13
13
  HIDE = true
14
+ VALIDATIONS = [].freeze
14
15
 
15
16
  def call
16
17
  # Modify this method to trigger the code you want to test.
@@ -9,6 +9,7 @@ module Command
9
9
  - Can also be done with `cpl --version` or `cpl -v`
10
10
  DESC
11
11
  WITH_INFO_HEADER = false
12
+ VALIDATIONS = [].freeze
12
13
 
13
14
  def call
14
15
  puts Cpl::VERSION