cpl 1.1.2.rc.0 → 1.2.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.
@@ -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