cpl 1.1.2.rc.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,6 +7,7 @@ module Command
7
7
  REQUIRES_ARGS = true
8
8
  OPTIONS = [
9
9
  app_option(required: true),
10
+ location_option,
10
11
  skip_confirm_option
11
12
  ].freeze
12
13
  DESCRIPTION = "Applies application-specific configs from templates"
@@ -35,13 +36,12 @@ module Command
35
36
  ```
36
37
  EX
37
38
 
38
- def call # rubocop:disable Metrics/MethodLength
39
+ def call # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
39
40
  ensure_templates!
40
41
 
41
- @app_status = :existing
42
- @created_workloads = []
43
- @failed_workloads = []
44
- @skipped_workloads = []
42
+ @created_items = []
43
+ @failed_templates = []
44
+ @skipped_templates = []
45
45
 
46
46
  @asked_for_confirmation = false
47
47
 
@@ -57,9 +57,11 @@ module Command
57
57
 
58
58
  pending_templates.each do |template, filename|
59
59
  step("Applying template '#{template}'", abort_on_error: false) do
60
- apply_template(filename)
61
- if $CHILD_STATUS.success?
62
- report_success(template)
60
+ items = apply_template(filename)
61
+ if items
62
+ items.each do |item|
63
+ report_success(item)
64
+ end
63
65
  else
64
66
  report_failure(template)
65
67
  end
@@ -68,10 +70,9 @@ module Command
68
70
  end
69
71
  end
70
72
 
71
- print_app_status
72
- print_created_workloads
73
- print_failed_workloads
74
- print_skipped_workloads
73
+ print_created_items
74
+ print_failed_templates
75
+ print_skipped_templates
75
76
  end
76
77
 
77
78
  private
@@ -126,7 +127,7 @@ module Command
126
127
  def apply_template(filename)
127
128
  data = File.read(filename)
128
129
  .gsub("APP_GVC", config.app)
129
- .gsub("APP_LOCATION", config[:default_location])
130
+ .gsub("APP_LOCATION", config.location)
130
131
  .gsub("APP_ORG", config.org)
131
132
  .gsub("APP_IMAGE", latest_image)
132
133
 
@@ -134,62 +135,37 @@ module Command
134
135
  cp.apply_template(data)
135
136
  end
136
137
 
137
- def report_success(template)
138
- if template == "gvc"
139
- @app_status = :success
140
- else
141
- @created_workloads.push(template)
142
- end
138
+ def report_success(item)
139
+ @created_items.push(item)
143
140
  end
144
141
 
145
142
  def report_failure(template)
146
- if template == "gvc"
147
- @app_status = :failure
148
- else
149
- @failed_workloads.push(template)
150
- end
143
+ @failed_templates.push(template)
151
144
  end
152
145
 
153
146
  def report_skipped(template)
154
- if template == "gvc"
155
- @app_status = :skipped
156
- else
157
- @skipped_workloads.push(template)
158
- end
159
- end
160
-
161
- def print_app_status
162
- return if @app_status == :existing
163
-
164
- case @app_status
165
- when :success
166
- progress.puts("\n#{Shell.color("Created app '#{config.app}'.", :green)}")
167
- when :failure
168
- progress.puts("\n#{Shell.color("Failed to create app '#{config.app}'.", :red)}")
169
- when :skipped
170
- progress.puts("\n#{Shell.color("Skipped app '#{config.app}' (already exists).", :blue)}")
171
- end
147
+ @skipped_templates.push(template)
172
148
  end
173
149
 
174
- def print_created_workloads
175
- return unless @created_workloads.any?
150
+ def print_created_items
151
+ return unless @created_items.any?
176
152
 
177
- workloads = @created_workloads.map { |template| " - #{template}" }.join("\n")
178
- progress.puts("\n#{Shell.color('Created workloads:', :green)}\n#{workloads}")
153
+ created = @created_items.map { |item| " - [#{item[:kind]}] #{item[:name]}" }.join("\n")
154
+ progress.puts("\n#{Shell.color('Created items:', :green)}\n#{created}")
179
155
  end
180
156
 
181
- def print_failed_workloads
182
- return unless @failed_workloads.any?
157
+ def print_failed_templates
158
+ return unless @failed_templates.any?
183
159
 
184
- workloads = @failed_workloads.map { |template| " - #{template}" }.join("\n")
185
- progress.puts("\n#{Shell.color('Failed to create workloads:', :red)}\n#{workloads}")
160
+ failed = @failed_templates.map { |template| " - #{template}" }.join("\n")
161
+ progress.puts("\n#{Shell.color('Failed to apply templates:', :red)}\n#{failed}")
186
162
  end
187
163
 
188
- def print_skipped_workloads
189
- return unless @skipped_workloads.any?
164
+ def print_skipped_templates
165
+ return unless @skipped_templates.any?
190
166
 
191
- workloads = @skipped_workloads.map { |template| " - #{template}" }.join("\n")
192
- progress.puts("\n#{Shell.color('Skipped workloads (already exist):', :blue)}\n#{workloads}")
167
+ skipped = @skipped_templates.map { |template| " - #{template}" }.join("\n")
168
+ progress.puts("\n#{Shell.color('Skipped templates (already exist):', :blue)}\n#{skipped}")
193
169
  end
194
170
  end
195
171
  end
data/lib/command/base.rb CHANGED
@@ -23,6 +23,8 @@ module Command
23
23
  EXAMPLES = ""
24
24
  # If `true`, hides the command from `cpl help`
25
25
  HIDE = false
26
+ # Whether or not to show key information like ORG and APP name in commands
27
+ WITH_INFO_HEADER = true
26
28
 
27
29
  NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE"
28
30
 
@@ -38,6 +40,10 @@ module Command
38
40
  end
39
41
  end
40
42
 
43
+ def self.common_options
44
+ [org_option, verbose_option, trace_option]
45
+ end
46
+
41
47
  def self.org_option(required: false)
42
48
  {
43
49
  name: :org,
@@ -103,6 +109,19 @@ module Command
103
109
  }
104
110
  end
105
111
 
112
+ def self.location_option(required: false)
113
+ {
114
+ name: :location,
115
+ params: {
116
+ aliases: ["-l"],
117
+ banner: "LOCATION_NAME",
118
+ desc: "Location name",
119
+ type: :string,
120
+ required: required
121
+ }
122
+ }
123
+ end
124
+
106
125
  def self.upstream_token_option(required: false)
107
126
  {
108
127
  name: :upstream_token,
@@ -176,6 +195,29 @@ module Command
176
195
  }
177
196
  end
178
197
 
198
+ def self.verbose_option(required: false)
199
+ {
200
+ name: :verbose,
201
+ params: {
202
+ aliases: ["-d"],
203
+ desc: "Shows detailed logs",
204
+ type: :boolean,
205
+ required: required
206
+ }
207
+ }
208
+ end
209
+
210
+ def self.trace_option(required: false)
211
+ {
212
+ name: :trace,
213
+ params: {
214
+ desc: "Shows trace of API calls. WARNING: may contain sensitive data",
215
+ type: :boolean,
216
+ required: required
217
+ }
218
+ }
219
+ end
220
+
179
221
  def self.all_options
180
222
  methods.grep(/_option$/).map { |method| send(method.to_s) }
181
223
  end
@@ -227,6 +269,7 @@ module Command
227
269
  end
228
270
 
229
271
  def latest_image_next(app = config.app, org = config.org, commit: nil)
272
+ # debugger
230
273
  commit ||= config.options[:commit]
231
274
 
232
275
  @latest_image_next ||= {}
@@ -29,7 +29,7 @@ module Command
29
29
  ensure_docker_running!
30
30
 
31
31
  @upstream = config[:upstream]
32
- @upstream_org = config.apps[@upstream.to_sym][:cpln_org]
32
+ @upstream_org = config.apps[@upstream.to_sym][:cpln_org] || ENV.fetch("CPLN_ORG_UPSTREAM", nil)
33
33
  ensure_upstream_org!
34
34
 
35
35
  create_upstream_profile
@@ -51,7 +51,10 @@ module Command
51
51
  end
52
52
 
53
53
  def ensure_upstream_org!
54
- raise "Can't find option 'cpln_org' for app '#{@upstream}' in 'controlplane.yml'." unless @upstream_org
54
+ return if @upstream_org
55
+
56
+ raise "Can't find option 'cpln_org' for app '#{@upstream}' in 'controlplane.yml', " \
57
+ "and CPLN_ORG_UPSTREAM env var is not set."
55
58
  end
56
59
 
57
60
  def create_upstream_profile
@@ -7,21 +7,45 @@ module Command
7
7
  app_option(required: true),
8
8
  skip_confirm_option
9
9
  ].freeze
10
- DESCRIPTION = "Deletes the whole app (GVC with all workloads and all images)"
10
+ DESCRIPTION = "Deletes the whole app (GVC with all workloads, all volumesets and all images)"
11
11
  LONG_DESCRIPTION = <<~DESC
12
- - Deletes the whole app (GVC with all workloads and all images)
12
+ - Deletes the whole app (GVC with all workloads, all volumesets and all images)
13
13
  - Will ask for explicit user confirmation
14
14
  DESC
15
15
 
16
16
  def call
17
+ return progress.puts("App '#{config.app}' does not exist.") if cp.fetch_gvc.nil?
18
+
19
+ check_volumesets
20
+ check_images
17
21
  return unless confirm_delete
18
22
 
23
+ delete_volumesets
19
24
  delete_gvc
20
25
  delete_images
21
26
  end
22
27
 
23
28
  private
24
29
 
30
+ def check_volumesets
31
+ @volumesets = cp.fetch_volumesets["items"]
32
+ return progress.puts("No volumesets to delete.") unless @volumesets.any?
33
+
34
+ message = "The following volumesets will be deleted along with the app:"
35
+ volumesets_list = @volumesets.map { |volumeset| "- #{volumeset['name']}" }.join("\n")
36
+ progress.puts("#{Shell.color(message, :red)}\n#{volumesets_list}\n\n")
37
+ end
38
+
39
+ def check_images
40
+ @images = cp.query_images["items"]
41
+ .select { |image| image["name"].start_with?("#{config.app}:") }
42
+ return progress.puts("No images to delete.") unless @images.any?
43
+
44
+ message = "The following images will be deleted along with the app:"
45
+ images_list = @images.map { |image| "- #{image['name']}" }.join("\n")
46
+ progress.puts("#{Shell.color(message, :red)}\n#{images_list}\n\n")
47
+ end
48
+
25
49
  def confirm_delete
26
50
  return true if config.options[:yes]
27
51
 
@@ -33,22 +57,27 @@ module Command
33
57
  end
34
58
 
35
59
  def delete_gvc
36
- return progress.puts("App '#{config.app}' does not exist.") if cp.fetch_gvc.nil?
37
-
38
60
  step("Deleting app '#{config.app}'") do
39
61
  cp.gvc_delete
40
62
  end
41
63
  end
42
64
 
43
- def delete_images
44
- images = cp.query_images["items"]
45
- .filter_map { |item| item["name"] if item["name"].start_with?("#{config.app}:") }
65
+ def delete_volumesets
66
+ @volumesets.each do |volumeset|
67
+ step("Deleting volumeset '#{volumeset['name']}'") do
68
+ # If the volumeset is attached to a workload, we need to delete the workload first
69
+ workload = volumeset.dig("status", "usedByWorkload")&.split("/")&.last
70
+ cp.delete_workload(workload) if workload
46
71
 
47
- return progress.puts("No images to delete.") unless images.any?
72
+ cp.delete_volumeset(volumeset["name"])
73
+ end
74
+ end
75
+ end
48
76
 
49
- images.each do |image|
50
- step("Deleting image '#{image}'") do
51
- cp.image_delete(image)
77
+ def delete_images
78
+ @images.each do |image|
79
+ step("Deleting image '#{image['name']}'") do
80
+ cp.image_delete(image["name"])
52
81
  end
53
82
  end
54
83
  end
data/lib/command/env.rb CHANGED
@@ -10,6 +10,7 @@ module Command
10
10
  LONG_DESCRIPTION = <<~DESC
11
11
  - Displays app-specific environment variables
12
12
  DESC
13
+ WITH_INFO_HEADER = false
13
14
 
14
15
  def call
15
16
  cp.fetch_gvc!.dig("spec", "env").map do |prop|
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Command
4
+ class Generator < Thor::Group
5
+ include Thor::Actions
6
+
7
+ def copy_files
8
+ directory("generator_templates", ".controlplane")
9
+ end
10
+
11
+ def self.source_root
12
+ File.expand_path("../", __dir__)
13
+ end
14
+ end
15
+
16
+ class Generate < Base
17
+ NAME = "generate"
18
+ DESCRIPTION = "Creates base Control Plane config and template files"
19
+ LONG_DESCRIPTION = <<~DESC
20
+ Creates base Control Plane config and template files
21
+ DESC
22
+ EXAMPLES = <<~EX
23
+ ```sh
24
+ # Creates .controlplane directory with Control Plane config and other templates
25
+ cpl generate
26
+ ```
27
+ EX
28
+ WITH_INFO_HEADER = false
29
+
30
+ def call
31
+ if controlplane_directory_exists?
32
+ Shell.warn("The directory '.controlplane' already exists!")
33
+ return
34
+ end
35
+
36
+ Generator.start
37
+ end
38
+
39
+ private
40
+
41
+ def controlplane_directory_exists?
42
+ Dir.exist? ".controlplane"
43
+ end
44
+ end
45
+ end
data/lib/command/info.rb CHANGED
@@ -4,7 +4,6 @@ module Command
4
4
  class Info < Base # rubocop:disable Metrics/ClassLength
5
5
  NAME = "info"
6
6
  OPTIONS = [
7
- org_option,
8
7
  app_option
9
8
  ].freeze
10
9
  DESCRIPTION = "Displays the diff between defined/available apps/workloads (apps equal GVCs)"
@@ -17,7 +16,7 @@ module Command
17
16
  DESC
18
17
  EXAMPLES = <<~EX
19
18
  ```sh
20
- # Shows diff for all apps in all orgs.
19
+ # Shows diff for all apps in all orgs (based on `.controlplane/controlplane.yml`).
21
20
  cpl info
22
21
 
23
22
  # Shows diff for all apps in a specific org.
@@ -27,6 +26,7 @@ module Command
27
26
  cpl info -a $APP_NAME
28
27
  ```
29
28
  EX
29
+ WITH_INFO_HEADER = false
30
30
 
31
31
  def call
32
32
  @missing_apps_workloads = {}
@@ -84,14 +84,14 @@ module Command
84
84
  def orgs # rubocop:disable Metrics/MethodLength
85
85
  result = []
86
86
 
87
- if config.options[:org]
88
- result.push(config.options[:org])
87
+ if config.org
88
+ result.push(config.org)
89
89
  else
90
90
  config.apps.each do |app_name, app_options|
91
91
  next if config.app && !app_matches?(config.app, app_name, app_options)
92
92
 
93
93
  org = app_options[:cpln_org]
94
- result.push(org) unless result.include?(org)
94
+ result.push(org) if org && !result.include?(org)
95
95
  end
96
96
  end
97
97
 
@@ -10,6 +10,7 @@ module Command
10
10
  LONG_DESCRIPTION = <<~DESC
11
11
  - Displays the latest image name
12
12
  DESC
13
+ WITH_INFO_HEADER = false
13
14
 
14
15
  def call
15
16
  puts latest_image
@@ -14,6 +14,7 @@ module Command
14
14
  - Optionally specify the maintenance workload through `maintenance_workload` in the `.controlplane/controlplane.yml` file (defaults to 'maintenance')
15
15
  - Maintenance mode is only supported for domains that use path based routing mode and have a route configured for the prefix '/' on either port 80 or 443
16
16
  DESC
17
+ WITH_INFO_HEADER = false
17
18
 
18
19
  def call # rubocop:disable Metrics/MethodLength
19
20
  one_off_workload = config[:one_off_workload]
@@ -9,11 +9,14 @@ module Command
9
9
  - Called when no command was specified
10
10
  DESC
11
11
  HIDE = true
12
+ WITH_INFO_HEADER = false
12
13
 
13
14
  def call
14
- return unless config.options[:version]
15
-
16
- Cpl::Cli.start(["version"])
15
+ if config.options[:version]
16
+ Cpl::Cli.start(["version"])
17
+ else
18
+ Cpl::Cli.start(["help"])
19
+ end
17
20
  end
18
21
  end
19
22
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Command
4
+ class OpenConsole < Base
5
+ NAME = "open-console"
6
+ OPTIONS = [
7
+ app_option(required: true),
8
+ workload_option
9
+ ].freeze
10
+ DESCRIPTION = "Opens the app console on Control Plane in the default browser"
11
+ LONG_DESCRIPTION = <<~DESC
12
+ - Opens the app console on Control Plane in the default browser
13
+ - Can also go directly to a workload page if `--workload` is provided
14
+ DESC
15
+
16
+ def call
17
+ workload = config.options[:workload]
18
+ url = "https://console.cpln.io/console/org/#{config.org}/gvc/#{config.app}"
19
+ url += "/workload/#{workload}" if workload
20
+ url += "/-info"
21
+ opener = `which xdg-open open`.split("\n").grep_v("not found").first
22
+
23
+ exec %(#{opener} "#{url}")
24
+ end
25
+ end
26
+ end
data/lib/command/ps.rb CHANGED
@@ -5,6 +5,7 @@ module Command
5
5
  NAME = "ps"
6
6
  OPTIONS = [
7
7
  app_option(required: true),
8
+ location_option,
8
9
  workload_option
9
10
  ].freeze
10
11
  DESCRIPTION = "Shows running replicas in app"
@@ -20,16 +21,19 @@ module Command
20
21
  cpl ps -a $APP_NAME -w $WORKLOAD_NAME
21
22
  ```
22
23
  EX
24
+ WITH_INFO_HEADER = false
23
25
 
24
26
  def call
25
27
  cp.fetch_gvc!
26
28
 
29
+ location = config.location
30
+
27
31
  workloads = [config.options[:workload]] if config.options[:workload]
28
32
  workloads ||= config[:app_workloads] + config[:additional_workloads]
29
33
  workloads.each do |workload|
30
34
  cp.fetch_workload!(workload)
31
35
 
32
- result = cp.workload_get_replicas(workload, location: config[:default_location])
36
+ result = cp.workload_get_replicas(workload, location: location)
33
37
  result["items"].each { |replica| puts replica }
34
38
  end
35
39
  end
data/lib/command/run.rb CHANGED
@@ -10,6 +10,7 @@ module Command
10
10
  app_option(required: true),
11
11
  image_option,
12
12
  workload_option,
13
+ location_option,
13
14
  use_local_token_option,
14
15
  terminal_size_option
15
16
  ].freeze
@@ -17,7 +18,6 @@ module Command
17
18
  LONG_DESCRIPTION = <<~DESC
18
19
  - Runs one-off **_interactive_** replicas (analog of `heroku run`)
19
20
  - Uses `Standard` workload type and `cpln exec` as the execution method, with CLI streaming
20
- - May not work correctly with tasks that last over 5 minutes (there's a Control Plane scaling bug at the moment)
21
21
  - If `fix_terminal_size` is `true` in the `.controlplane/controlplane.yml` file, the remote terminal size will be fixed to match the local terminal size (may also be overriden through `--terminal-size`)
22
22
 
23
23
  > **IMPORTANT:** Useful for development where it's needed for interaction, and where network connection drops and
@@ -57,7 +57,7 @@ module Command
57
57
  attr_reader :location, :workload, :one_off, :container
58
58
 
59
59
  def call # rubocop:disable Metrics/MethodLength
60
- @location = config[:default_location]
60
+ @location = config.location
61
61
  @workload = config.options["workload"] || config[:one_off_workload]
62
62
  @one_off = "#{workload}-run-#{rand(1000..9999)}"
63
63
 
@@ -9,6 +9,7 @@ module Command
9
9
  app_option(required: true),
10
10
  image_option,
11
11
  workload_option,
12
+ location_option,
12
13
  use_local_token_option
13
14
  ].freeze
14
15
  DESCRIPTION = "Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)"
@@ -47,7 +48,7 @@ module Command
47
48
  attr_reader :location, :workload, :one_off, :container
48
49
 
49
50
  def call # rubocop:disable Metrics/MethodLength
50
- @location = config[:default_location]
51
+ @location = config.location
51
52
  @workload = config.options["workload"] || config[:one_off_workload]
52
53
  @one_off = "#{workload}-runner-#{rand(1000..9999)}"
53
54
 
@@ -9,12 +9,12 @@ module Command
9
9
  DESCRIPTION = "Creates an app and all its workloads"
10
10
  LONG_DESCRIPTION = <<~DESC
11
11
  - Creates an app and all its workloads
12
- - Specify the templates for the app and workloads through `setup` in the `.controlplane/controlplane.yml` file
13
- - This should 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)
12
+ - Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
13
+ - 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)
14
14
  DESC
15
15
 
16
16
  def call
17
- templates = config[:setup]
17
+ templates = config[:setup_app_templates]
18
18
 
19
19
  app = cp.fetch_gvc
20
20
  if app
@@ -8,6 +8,7 @@ module Command
8
8
  - Displays the current version of the CLI
9
9
  - Can also be done with `cpl --version` or `cpl -v`
10
10
  DESC
11
+ WITH_INFO_HEADER = false
11
12
 
12
13
  def call
13
14
  puts Cpl::VERSION