cpl 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.overcommit.yml +10 -0
  3. data/Gemfile.lock +10 -3
  4. data/README.md +6 -0
  5. data/cpl.gemspec +1 -0
  6. data/docs/commands.md +51 -3
  7. data/googlee2da545df05d92f9.html +1 -0
  8. data/lib/command/base.rb +65 -7
  9. data/lib/command/build_image.rb +6 -5
  10. data/lib/command/cleanup_old_images.rb +8 -7
  11. data/lib/command/cleanup_stale_apps.rb +11 -9
  12. data/lib/command/config.rb +30 -15
  13. data/lib/command/copy_image_from_upstream.rb +110 -0
  14. data/lib/command/delete.rb +10 -12
  15. data/lib/command/deploy_image.rb +6 -5
  16. data/lib/command/env.rb +2 -2
  17. data/lib/command/exists.rb +4 -4
  18. data/lib/command/info.rb +233 -0
  19. data/lib/command/latest_image.rb +2 -2
  20. data/lib/command/logs.rb +4 -4
  21. data/lib/command/no_command.rb +3 -3
  22. data/lib/command/open.rb +4 -4
  23. data/lib/command/promote_app_from_upstream.rb +58 -0
  24. data/lib/command/ps.rb +10 -13
  25. data/lib/command/ps_restart.rb +9 -6
  26. data/lib/command/ps_start.rb +7 -6
  27. data/lib/command/ps_stop.rb +7 -6
  28. data/lib/command/run.rb +5 -5
  29. data/lib/command/run_detached.rb +7 -5
  30. data/lib/command/setup.rb +71 -13
  31. data/lib/command/test.rb +2 -2
  32. data/lib/command/version.rb +2 -2
  33. data/lib/core/config.rb +26 -19
  34. data/lib/core/controlplane.rb +77 -11
  35. data/lib/core/controlplane_api.rb +12 -0
  36. data/lib/core/controlplane_api_cli.rb +1 -1
  37. data/lib/core/controlplane_api_direct.rb +2 -2
  38. data/lib/core/shell.rb +25 -3
  39. data/lib/cpl/version.rb +1 -1
  40. data/lib/cpl.rb +19 -10
  41. data/lib/deprecated_commands.json +6 -0
  42. data/script/add_command +37 -0
  43. data/script/generate_commands_docs +5 -5
  44. data/script/rename_command +43 -0
  45. metadata +24 -2
data/lib/command/setup.rb CHANGED
@@ -9,7 +9,7 @@ module Command
9
9
  app_option(required: true)
10
10
  ].freeze
11
11
  DESCRIPTION = "Applies application-specific configs from templates"
12
- LONG_DESCRIPTION = <<~HEREDOC
12
+ LONG_DESCRIPTION = <<~DESC
13
13
  - Applies application-specific configs from templates (e.g., for every review-app)
14
14
  - Publishes (creates or updates) those at Control Plane infrastructure
15
15
  - Picks templates from the `.controlplane/templates` directory
@@ -23,8 +23,8 @@ module Command
23
23
  APP_ORG - organization
24
24
  APP_IMAGE - will use latest app image
25
25
  ```
26
- HEREDOC
27
- EXAMPLES = <<~HEREDOC
26
+ DESC
27
+ EXAMPLES = <<~EX
28
28
  ```sh
29
29
  # Applies single template.
30
30
  cpl setup redis -a $APP_NAME
@@ -32,31 +32,89 @@ module Command
32
32
  # Applies several templates (practically creating full app).
33
33
  cpl setup gvc postgres redis rails -a $APP_NAME
34
34
  ```
35
- HEREDOC
35
+ EX
36
+
37
+ def call # rubocop:disable Metrics/MethodLength
38
+ @app_status = :existing
39
+ @created_workloads = []
40
+ @failed_workloads = []
36
41
 
37
- def call
38
42
  config.args.each do |template|
39
43
  filename = "#{config.app_cpln_dir}/templates/#{template}.yml"
40
- ensure_template!(template, filename)
41
- apply_template(filename)
42
- progress.puts(template)
44
+
45
+ step("Applying template '#{template}'", abort_on_error: false) do
46
+ unless File.exist?(filename)
47
+ report_failure(template)
48
+
49
+ raise "Can't find template '#{template}' at '#{filename}', please create it."
50
+ end
51
+
52
+ apply_template(filename)
53
+ if $CHILD_STATUS.success?
54
+ report_success(template)
55
+ else
56
+ report_failure(template)
57
+ end
58
+
59
+ $CHILD_STATUS.success?
60
+ end
43
61
  end
62
+
63
+ print_app_status
64
+ print_created_workloads
65
+ print_failed_workloads
44
66
  end
45
67
 
46
68
  private
47
69
 
48
- def ensure_template!(template, filename)
49
- Shell.abort("Can't find template '#{template}' at '#{filename}', please create it.") unless File.exist?(filename)
50
- end
51
-
52
70
  def apply_template(filename)
53
71
  data = File.read(filename)
54
72
  .gsub("APP_GVC", config.app)
55
73
  .gsub("APP_LOCATION", config[:default_location])
56
- .gsub("APP_ORG", config[:cpln_org])
74
+ .gsub("APP_ORG", config.org)
57
75
  .gsub("APP_IMAGE", latest_image)
58
76
 
59
77
  cp.apply(YAML.safe_load(data))
60
78
  end
79
+
80
+ def report_success(template)
81
+ if template == "gvc"
82
+ @app_status = :success
83
+ else
84
+ @created_workloads.push(template)
85
+ end
86
+ end
87
+
88
+ def report_failure(template)
89
+ if template == "gvc"
90
+ @app_status = :failure
91
+ else
92
+ @failed_workloads.push(template)
93
+ end
94
+ end
95
+
96
+ def print_app_status
97
+ return if @app_status == :existing
98
+
99
+ if @app_status == :success
100
+ progress.puts("\n#{Shell.color("Created app '#{config.app}'.", :green)}")
101
+ else
102
+ progress.puts("\n#{Shell.color("Failed to create app '#{config.app}'.", :red)}")
103
+ end
104
+ end
105
+
106
+ def print_created_workloads
107
+ return unless @created_workloads.any?
108
+
109
+ workloads = @created_workloads.map { |template| " - #{template}" }.join("\n")
110
+ progress.puts("\n#{Shell.color('Created workloads:', :green)}\n#{workloads}")
111
+ end
112
+
113
+ def print_failed_workloads
114
+ return unless @failed_workloads.any?
115
+
116
+ workloads = @failed_workloads.map { |template| " - #{template}" }.join("\n")
117
+ progress.puts("\n#{Shell.color('Failed to create workloads:', :red)}\n#{workloads}")
118
+ end
61
119
  end
62
120
  end
data/lib/command/test.rb CHANGED
@@ -8,9 +8,9 @@ module Command
8
8
  NAME = "test"
9
9
  OPTIONS = all_options
10
10
  DESCRIPTION = "For debugging purposes"
11
- LONG_DESCRIPTION = <<~HEREDOC
11
+ LONG_DESCRIPTION = <<~DESC
12
12
  - For debugging purposes
13
- HEREDOC
13
+ DESC
14
14
  HIDE = true
15
15
 
16
16
  def call
@@ -4,10 +4,10 @@ module Command
4
4
  class Version < Base
5
5
  NAME = "version"
6
6
  DESCRIPTION = "Displays the current version of the CLI"
7
- LONG_DESCRIPTION = <<~HEREDOC
7
+ LONG_DESCRIPTION = <<~DESC
8
8
  - Displays the current version of the CLI
9
9
  - Can also be done with `cpl --version` or `cpl -v`
10
- HEREDOC
10
+ DESC
11
11
 
12
12
  def call
13
13
  puts Cpl::VERSION
data/lib/core/config.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Config
4
4
  attr_reader :config, :current,
5
- :app, :app_dir,
5
+ :org, :app, :apps, :app_dir,
6
6
  # command line options
7
7
  :args, :options
8
8
 
@@ -11,9 +11,13 @@ class Config
11
11
  def initialize(args, options)
12
12
  @args = args
13
13
  @options = options
14
+ @org = options[:org]
14
15
  @app = options[:app]
15
16
 
16
17
  load_app_config
18
+
19
+ @apps = config[:apps]
20
+
17
21
  pick_current_config if app
18
22
  warn_deprecated_options if current
19
23
  end
@@ -27,7 +31,7 @@ class Config
27
31
  elsif old_key && current.key?(old_key)
28
32
  current.fetch(old_key)
29
33
  else
30
- Shell.abort("Can't find option '#{key}' for app '#{app}' in 'controlplane.yml'.")
34
+ raise "Can't find option '#{key}' for app '#{app}' in 'controlplane.yml'."
31
35
  end
32
36
  end
33
37
 
@@ -42,34 +46,33 @@ class Config
42
46
  private
43
47
 
44
48
  def ensure_current_config!
45
- Shell.abort("Can't find current config, please specify an app.") unless current
49
+ raise "Can't find current config, please specify an app." unless current
46
50
  end
47
51
 
48
52
  def ensure_current_config_app!(app)
49
- Shell.abort("Can't find app '#{app}' in 'controlplane.yml'.") unless current
53
+ raise "Can't find app '#{app}' in 'controlplane.yml'." unless current
50
54
  end
51
55
 
52
56
  def ensure_config!
53
- Shell.abort("'controlplane.yml' is empty.") unless config
57
+ raise "'controlplane.yml' is empty." unless config
54
58
  end
55
59
 
56
60
  def ensure_config_apps!
57
- Shell.abort("Can't find key 'apps' in 'controlplane.yml'.") unless config[:apps]
61
+ raise "Can't find key 'apps' in 'controlplane.yml'." unless config[:apps]
58
62
  end
59
63
 
60
64
  def ensure_config_app!(app, options)
61
- Shell.abort("App '#{app}' is empty in 'controlplane.yml'.") unless options
65
+ raise "App '#{app}' is empty in 'controlplane.yml'." unless options
62
66
  end
63
67
 
64
68
  def pick_current_config
65
- ensure_config!
66
- ensure_config_apps!
67
69
  config[:apps].each do |c_app, c_data|
68
70
  ensure_config_app!(c_app, c_data)
69
- if c_app.to_s == app || (c_data[:match_if_app_name_starts_with] && app.start_with?(c_app.to_s))
70
- @current = c_data
71
- break
72
- end
71
+ next unless c_app.to_s == app || (c_data[:match_if_app_name_starts_with] && app.start_with?(c_app.to_s))
72
+
73
+ @current = c_data
74
+ @org = self[:cpln_org]
75
+ break
73
76
  end
74
77
  ensure_current_config_app!(app)
75
78
  end
@@ -78,6 +81,8 @@ class Config
78
81
  config_file = find_app_config_file
79
82
  @config = YAML.safe_load_file(config_file, symbolize_names: true, aliases: true)
80
83
  @app_dir = Pathname.new(config_file).parent.parent.to_s
84
+ ensure_config!
85
+ ensure_config_apps!
81
86
  end
82
87
 
83
88
  def find_app_config_file
@@ -90,7 +95,7 @@ class Config
90
95
  path = path.parent
91
96
 
92
97
  if path.root?
93
- Shell.abort("Can't find project config file at 'project_folder/#{CONFIG_FILE_LOCATIION}', please create it.")
98
+ raise "Can't find project config file at 'project_folder/#{CONFIG_FILE_LOCATIION}', please create it."
94
99
  end
95
100
  end
96
101
  end
@@ -104,11 +109,13 @@ class Config
104
109
  end
105
110
 
106
111
  def warn_deprecated_options
107
- old_option_keys.each do |new_key, old_key|
108
- if current.key?(old_key)
109
- Shell.warn_deprecated("Option '#{old_key}' is deprecated, " \
110
- "please use '#{new_key}' instead (in 'controlplane.yml').")
111
- end
112
+ deprecated_option_keys = old_option_keys.filter { |_new_key, old_key| current.key?(old_key) }
113
+ return if deprecated_option_keys.empty?
114
+
115
+ deprecated_option_keys.each do |new_key, old_key|
116
+ Shell.warn_deprecated("Option '#{old_key}' is deprecated, " \
117
+ "please use '#{new_key}' instead (in 'controlplane.yml').")
112
118
  end
119
+ $stderr.puts
113
120
  end
114
121
  end
@@ -7,7 +7,30 @@ class Controlplane # rubocop:disable Metrics/ClassLength
7
7
  @config = config
8
8
  @api = ControlplaneApi.new
9
9
  @gvc = config.app
10
- @org = config[:cpln_org]
10
+ @org = config.org
11
+ end
12
+
13
+ # profile
14
+
15
+ def profile_switch(profile)
16
+ ENV["CPLN_PROFILE"] = profile
17
+ end
18
+
19
+ def profile_exists?(profile)
20
+ cmd = "cpln profile get #{profile} -o yaml"
21
+ perform_yaml(cmd).length.positive?
22
+ end
23
+
24
+ def profile_create(profile, token)
25
+ cmd = "cpln profile create #{profile} --token #{token}"
26
+ cmd += " > /dev/null" if Shell.tmp_stderr
27
+ perform!(cmd)
28
+ end
29
+
30
+ def profile_delete(profile)
31
+ cmd = "cpln profile delete #{profile}"
32
+ cmd += " > /dev/null" if Shell.tmp_stderr
33
+ perform!(cmd)
11
34
  end
12
35
 
13
36
  # image
@@ -18,8 +41,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
18
41
  perform!(cmd)
19
42
  end
20
43
 
21
- def image_query(app_name = config.app)
22
- cmd = "cpln image query --org #{org} -o yaml --max -1 --prop repository=#{app_name}"
44
+ def image_query(app_name = config.app, org_name = config.org)
45
+ cmd = "cpln image query --org #{org_name} -o yaml --max -1 --prop repository=#{app_name}"
23
46
  perform_yaml(cmd)
24
47
  end
25
48
 
@@ -27,8 +50,36 @@ class Controlplane # rubocop:disable Metrics/ClassLength
27
50
  api.image_delete(org: org, image: image)
28
51
  end
29
52
 
53
+ def image_login(org_name = config.org)
54
+ cmd = "cpln image docker-login --org #{org_name}"
55
+ cmd += " > /dev/null 2>&1" if Shell.tmp_stderr
56
+ perform!(cmd)
57
+ end
58
+
59
+ def image_pull(image)
60
+ cmd = "docker pull #{image}"
61
+ cmd += " > /dev/null" if Shell.tmp_stderr
62
+ perform!(cmd)
63
+ end
64
+
65
+ def image_tag(old_tag, new_tag)
66
+ cmd = "docker tag #{old_tag} #{new_tag}"
67
+ cmd += " > /dev/null" if Shell.tmp_stderr
68
+ perform!(cmd)
69
+ end
70
+
71
+ def image_push(image)
72
+ cmd = "docker push #{image}"
73
+ cmd += " > /dev/null" if Shell.tmp_stderr
74
+ perform!(cmd)
75
+ end
76
+
30
77
  # gvc
31
78
 
79
+ def fetch_gvcs
80
+ api.gvc_list(org: org)
81
+ end
82
+
32
83
  def gvc_query(app_name = config.app)
33
84
  # When `match_if_app_name_starts_with` is `true`, we query for any gvc containing the name,
34
85
  # otherwise we query for a gvc with the exact name.
@@ -38,15 +89,15 @@ class Controlplane # rubocop:disable Metrics/ClassLength
38
89
  perform_yaml(cmd)
39
90
  end
40
91
 
41
- def fetch_gvc(a_gvc = gvc)
42
- api.gvc_get(gvc: a_gvc, org: org)
92
+ def fetch_gvc(a_gvc = gvc, a_org = org)
93
+ api.gvc_get(gvc: a_gvc, org: a_org)
43
94
  end
44
95
 
45
96
  def fetch_gvc!(a_gvc = gvc)
46
97
  gvc_data = fetch_gvc(a_gvc)
47
98
  return gvc_data if gvc_data
48
99
 
49
- Shell.abort("Can't find GVC '#{gvc}', please create it with 'cpl setup gvc -a #{config.app}'.")
100
+ raise "Can't find GVC '#{gvc}', please create it with 'cpl setup gvc -a #{config.app}'."
50
101
  end
51
102
 
52
103
  def gvc_delete(a_gvc = gvc)
@@ -55,6 +106,14 @@ class Controlplane # rubocop:disable Metrics/ClassLength
55
106
 
56
107
  # workload
57
108
 
109
+ def fetch_workloads(a_gvc = gvc)
110
+ api.workload_list(gvc: a_gvc, org: org)
111
+ end
112
+
113
+ def fetch_workloads_by_org(a_org = org)
114
+ api.workload_list_by_org(org: a_org)
115
+ end
116
+
58
117
  def fetch_workload(workload)
59
118
  api.workload_get(workload: workload, gvc: gvc, org: org)
60
119
  end
@@ -63,7 +122,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
63
122
  workload_data = fetch_workload(workload)
64
123
  return workload_data if workload_data
65
124
 
66
- Shell.abort("Can't find workload '#{workload}', please create it with 'cpl setup #{workload} -a #{config.app}'.")
125
+ raise "Can't find workload '#{workload}', please create it with 'cpl setup #{workload} -a #{config.app}'."
67
126
  end
68
127
 
69
128
  def workload_get_replicas(workload, location:)
@@ -73,7 +132,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
73
132
 
74
133
  def workload_set_image_ref(workload, container:, image:)
75
134
  cmd = "cpln workload update #{workload} #{gvc_org}"
76
- cmd += " --set spec.containers.#{container}.image=/org/#{config[:cpln_org]}/image/#{image}"
135
+ cmd += " --set spec.containers.#{container}.image=/org/#{config.org}/image/#{image}"
136
+ cmd += " > /dev/null" if Shell.tmp_stderr
77
137
  perform!(cmd)
78
138
  end
79
139
 
@@ -85,6 +145,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
85
145
 
86
146
  def workload_force_redeployment(workload)
87
147
  cmd = "cpln workload force-redeployment #{workload} #{gvc_org}"
148
+ cmd += " > /dev/null" if Shell.tmp_stderr
88
149
  perform!(cmd)
89
150
  end
90
151
 
@@ -126,12 +187,17 @@ class Controlplane # rubocop:disable Metrics/ClassLength
126
187
 
127
188
  # apply
128
189
 
129
- def apply(data)
190
+ def apply(data) # rubocop:disable Metrics/MethodLength
130
191
  Tempfile.create do |f|
131
192
  f.write(data.to_yaml)
132
193
  f.rewind
133
194
  cmd = "cpln apply #{gvc_org} --file #{f.path} > /dev/null"
134
- perform!(cmd)
195
+ if Shell.tmp_stderr
196
+ cmd += " 2> #{Shell.tmp_stderr.path}"
197
+ perform(cmd)
198
+ else
199
+ perform!(cmd)
200
+ end
135
201
  end
136
202
  end
137
203
 
@@ -147,7 +213,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
147
213
 
148
214
  def perform_yaml(cmd)
149
215
  result = `#{cmd}`
150
- $?.success? ? YAML.safe_load(result) : exit(false) # rubocop:disable Style/SpecialGlobalVars
216
+ $CHILD_STATUS.success? ? YAML.safe_load(result) : exit(false)
151
217
  end
152
218
 
153
219
  def gvc_org
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ControlplaneApi
4
+ def gvc_list(org:)
5
+ api_json("/org/#{org}/gvc", method: :get)
6
+ end
7
+
4
8
  def gvc_get(org:, gvc:)
5
9
  api_json("/org/#{org}/gvc/#{gvc}", method: :get)
6
10
  end
@@ -29,6 +33,14 @@ class ControlplaneApi
29
33
  api_json_direct("/logs/org/#{org}/loki/api/v1/query_range?#{params}", method: :get, host: :logs)
30
34
  end
31
35
 
36
+ def workload_list(org:, gvc:)
37
+ api_json("/org/#{org}/gvc/#{gvc}/workload", method: :get)
38
+ end
39
+
40
+ def workload_list_by_org(org:)
41
+ api_json("/org/#{org}/workload", method: :get)
42
+ end
43
+
32
44
  def workload_get(org:, gvc:, workload:)
33
45
  api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :get)
34
46
  end
@@ -3,7 +3,7 @@
3
3
  class ControlplaneApiCli
4
4
  def call(url, method:)
5
5
  response = `cpln rest #{method} #{url} -o json`
6
- raise(response) unless $?.success? # rubocop:disable Style/SpecialGlobalVars
6
+ raise(response) unless $CHILD_STATUS.success?
7
7
 
8
8
  JSON.parse(response)
9
9
  end
@@ -37,7 +37,7 @@ class ControlplaneApiDirect
37
37
  @@api_token = ENV.fetch("CPLN_TOKEN", `cpln profile token`.chomp) # rubocop:disable Style/ClassVars
38
38
  return @@api_token if @@api_token.match?(API_TOKEN_REGEX)
39
39
 
40
- Shell.abort("Unknown API token format. " \
41
- "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable.")
40
+ raise "Unknown API token format. " \
41
+ "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
42
42
  end
43
43
  end
data/lib/core/shell.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Shell
4
+ class << self
5
+ attr_reader :tmp_stderr
6
+ end
7
+
4
8
  def self.shell
5
9
  @shell ||= Thor::Shell::Color.new
6
10
  end
@@ -9,6 +13,24 @@ class Shell
9
13
  @stderr ||= $stderr
10
14
  end
11
15
 
16
+ def self.use_tmp_stderr
17
+ @tmp_stderr = Tempfile.create
18
+
19
+ yield
20
+
21
+ @tmp_stderr.close
22
+ @tmp_stderr = nil
23
+ end
24
+
25
+ def self.write_to_tmp_stderr(message)
26
+ tmp_stderr.write(message)
27
+ end
28
+
29
+ def self.read_from_tmp_stderr
30
+ tmp_stderr.rewind
31
+ tmp_stderr.read.strip
32
+ end
33
+
12
34
  def self.color(message, color_key)
13
35
  shell.set_color(message, color_key)
14
36
  end
@@ -18,14 +40,14 @@ class Shell
18
40
  end
19
41
 
20
42
  def self.warn(message)
21
- stderr.puts(color("WARNING: #{message}\n", :yellow))
43
+ stderr.puts(color("WARNING: #{message}", :yellow))
22
44
  end
23
45
 
24
46
  def self.warn_deprecated(message)
25
- stderr.puts(color("DEPRECATED: #{message}\n", :yellow))
47
+ stderr.puts(color("DEPRECATED: #{message}", :yellow))
26
48
  end
27
49
 
28
50
  def self.abort(message)
29
- Kernel.abort(color("ERROR: #{message}\n", :red))
51
+ Kernel.abort(color("ERROR: #{message}", :red))
30
52
  end
31
53
  end
data/lib/cpl/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpl
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/cpl.rb CHANGED
@@ -70,12 +70,17 @@ module Cpl
70
70
  end
71
71
 
72
72
  def self.deprecated_commands
73
- {
74
- build: ::Command::BuildImage,
75
- promote: ::Command::DeployImage,
76
- promote_image: ::Command::DeployImage,
77
- runner: ::Command::RunDetached
78
- }
73
+ @deprecated_commands ||= begin
74
+ deprecated_commands_file_path = "#{__dir__}/deprecated_commands.json"
75
+ deprecated_commands_data = File.binread(deprecated_commands_file_path)
76
+ deprecated_commands = JSON.parse(deprecated_commands_data)
77
+ deprecated_commands.to_h do |old_command_name, new_command_name|
78
+ file_name = new_command_name.gsub(/[^A-Za-z]/, "_")
79
+ class_name = file_name.split("_").map(&:capitalize).join
80
+
81
+ [old_command_name, Object.const_get("::Command::#{class_name}")]
82
+ end
83
+ end
79
84
  end
80
85
 
81
86
  def self.all_base_commands
@@ -113,6 +118,7 @@ module Cpl
113
118
  if deprecated
114
119
  ::Shell.warn_deprecated("Command '#{command_key}' is deprecated, " \
115
120
  "please use '#{name}' instead.")
121
+ $stderr.puts
116
122
  end
117
123
 
118
124
  args = if provided_args.length.positive?
@@ -123,13 +129,16 @@ module Cpl
123
129
 
124
130
  raise_args_error.call(args, nil) if (args.empty? && requires_args) || (!args.empty? && !requires_args)
125
131
 
126
- config = Config.new(args, options)
132
+ begin
133
+ config = Config.new(args, options)
127
134
 
128
- command_class.new(config).call
135
+ command_class.new(config).call
136
+ rescue RuntimeError => e
137
+ ::Shell.abort(e.message)
138
+ end
129
139
  end
130
140
  rescue StandardError => e
131
- logger = $stderr
132
- logger.puts("Unable to load command: #{e.message}")
141
+ ::Shell.abort("Unable to load command: #{e.message}")
133
142
  end
134
143
  end
135
144
  end
@@ -0,0 +1,6 @@
1
+ {
2
+ "build": "build-image",
3
+ "promote": "deploy-image",
4
+ "promote-image": "deploy-image",
5
+ "runner": "run:detached"
6
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ command_name = ARGV[0]&.downcase
5
+
6
+ abort("ERROR: Must provide command name.") unless command_name
7
+
8
+ file_name = command_name.gsub(/[^A-Za-z]/, "_")
9
+ file_path = "#{__dir__}/../lib/command/#{file_name}.rb"
10
+
11
+ abort("ERROR: Command '#{command_name}' already exists.") if File.exist?(file_path)
12
+
13
+ class_name = file_name.split("_").map(&:capitalize).join
14
+
15
+ file_data =
16
+ <<~DATA
17
+ # frozen_string_literal: true
18
+
19
+ module Command
20
+ class #{class_name} < Base
21
+ # See `base.rb` for other constants to add here
22
+ NAME = "#{command_name}"
23
+ OPTIONS = [
24
+ # Add options here
25
+ ].freeze
26
+ DESCRIPTION = "Add description here"
27
+ LONG_DESCRIPTION = <<~DESC
28
+ - Add long description here
29
+ DESC
30
+
31
+ def call
32
+ # Add command logic here
33
+ end
34
+ end
35
+ end
36
+ DATA
37
+ File.binwrite(file_path, file_data)
@@ -41,9 +41,9 @@ end
41
41
 
42
42
  commands_str = commands_str_arr.join("\n\n")
43
43
 
44
- commands_path = "#{__dir__}/../docs/commands.md"
45
- commands_data =
46
- <<~HEREDOC
44
+ file_path = "#{__dir__}/../docs/commands.md"
45
+ file_data =
46
+ <<~DATA
47
47
  <!-- NOTE: This file is automatically generated by running `script/generate_commands_docs`. Do NOT edit it manually. -->
48
48
 
49
49
  ### Common Options
@@ -58,5 +58,5 @@ commands_data =
58
58
  ### Commands
59
59
 
60
60
  #{commands_str}
61
- HEREDOC
62
- File.binwrite(commands_path, commands_data)
61
+ DATA
62
+ File.binwrite(file_path, file_data)