cpl 1.0.4 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21ed6f8fb3464917aa82b84973c7f7ce8436e7a4f8efaad6dd5c7018fb079299
4
- data.tar.gz: 7820a60e8390cbd9225d5cc9f458c4818697fef7ce4cbccc9593410fdcb6e202
3
+ metadata.gz: ad4122f8c7a91e2f665f348383580db755fc380d98d27d843ecbd30d49f5f7f7
4
+ data.tar.gz: 7a8d3d37524fad90a3849f5f2e233beaee64c59dbc62719e7951fec20b12a05f
5
5
  SHA512:
6
- metadata.gz: b382e5b481448da16ddca1e720407463747876005cb18e2efb198ec50aa6df6b117366e1be8bbd3ea55dd5cbee92ce6eb30d3d44ef0d7189f3146afb8fcb674b
7
- data.tar.gz: 8770e0be52ecce004c2989d6dea9102ef147e3164a9573274c3f555e26261cfa355fadcb4a548b8f431bae84edf63ac8830ccac6a295aa035fd87e35af5716f4
6
+ metadata.gz: f075cf14b7d694b0d208993340f34643079752e853314fd31da964899c712d5ace36d7419e33e650dfaf47f3b18a5d708cb9140b4053931ffe193269e11c50c5
7
+ data.tar.gz: cfad8e5b8ef74ca29c7df36dd36aaeb64c83ef184d401441604a88cbd2555a96af96917f9fccdc839e375de6311649c59b4b0dd8e2f22e88286b6aca94ed41e1
data/CHANGELOG.md CHANGED
@@ -14,6 +14,22 @@ Changes since the last non-beta release.
14
14
 
15
15
  _Please add entries here for your pull requests that are not yet released._
16
16
 
17
+ ### Fixed
18
+
19
+ - Fixed issue where `copy-image-from-upstream` command does not copy commit. [PR 70](https://github.com/shakacode/heroku-to-control-plane/pull/70) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
20
+ - Fixed issue where an error is not raised if the app is not defined. [PR 73](https://github.com/shakacode/heroku-to-control-plane/pull/73) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
21
+ - Fixed issue where `CPLN_ENDPOINT` is not used if available. [PR 75](https://github.com/shakacode/heroku-to-control-plane/pull/75) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
22
+
23
+ ### Added
24
+
25
+ - Added `image_retention_max_qty` config to clean up images based on max quantity with `cleanup-images` command. [PR 72](https://github.com/shakacode/heroku-to-control-plane/pull/72) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
26
+
27
+ ### Changed
28
+
29
+ - Updated docs for `run` commands regarding passing arguments at the end. [PR 71](https://github.com/shakacode/heroku-to-control-plane/pull/71) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
30
+ - Renamed `cleanup-old-images` command to `cleanup-images`. [PR 72](https://github.com/shakacode/heroku-to-control-plane/pull/72) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
31
+ - Renamed `old_image_retention_days` config to `image_retention_days`. [PR 72](https://github.com/shakacode/heroku-to-control-plane/pull/72) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
32
+
17
33
  ## [1.0.4] - 2023-07-21
18
34
 
19
35
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpl (1.0.4)
4
+ cpl (1.1.0)
5
5
  debug (~> 1.7.1)
6
6
  dotenv (~> 2.8.1)
7
7
  psych (~> 5.1.0)
@@ -25,8 +25,9 @@ GEM
25
25
  hashdiff (1.0.1)
26
26
  iniparse (1.5.0)
27
27
  io-console (0.6.0)
28
- irb (1.7.4)
29
- reline (>= 0.3.6)
28
+ irb (1.8.1)
29
+ rdoc
30
+ reline (>= 0.3.8)
30
31
  json (2.6.3)
31
32
  overcommit (0.60.0)
32
33
  childprocess (>= 0.6.3, < 5)
@@ -40,8 +41,10 @@ GEM
40
41
  public_suffix (5.0.1)
41
42
  rainbow (3.1.1)
42
43
  rake (13.0.6)
44
+ rdoc (6.5.0)
45
+ psych (>= 4.0.0)
43
46
  regexp_parser (2.6.2)
44
- reline (0.3.6)
47
+ reline (0.3.8)
45
48
  io-console (~> 0.5)
46
49
  rexml (3.2.5)
47
50
  rspec (3.12.0)
@@ -83,7 +86,7 @@ GEM
83
86
  simplecov_json_formatter (~> 0.1)
84
87
  simplecov-html (0.12.3)
85
88
  simplecov_json_formatter (0.1.4)
86
- stringio (3.0.7)
89
+ stringio (3.0.8)
87
90
  thor (1.2.2)
88
91
  timecop (0.9.6)
89
92
  unicode-display_width (2.4.2)
data/README.md CHANGED
@@ -163,9 +163,13 @@ aliases:
163
163
  # when running the command `cpl cleanup-stale-apps`.
164
164
  stale_app_image_deployed_days: 5
165
165
 
166
+ # Images that exceed this quantity will be listed for deletion
167
+ # when running the command `cpl cleanup-images`.
168
+ image_retention_max_qty: 20
169
+
166
170
  # Images created before this amount of days will be listed for deletion
167
- # when running the command `cpl cleanup-old-images`.
168
- old_image_retention_days: 5
171
+ # when running the command `cpl cleanup-images` (`image_retention_max_qty` takes precedence).
172
+ image_retention_days: 5
169
173
 
170
174
  # Run workloads created before this amount of days will be listed for deletion
171
175
  # when running the command `cpl run:cleanup`.
data/docs/commands.md CHANGED
@@ -46,15 +46,17 @@ cpl apply-template gvc postgres redis rails -a $APP_NAME
46
46
  cpl build-image -a $APP_NAME
47
47
  ```
48
48
 
49
- ### `cleanup-old-images`
49
+ ### `cleanup-images`
50
50
 
51
- - Deletes all images for an app that are older than the specified amount of days
52
- - Specify the amount of days through `old_image_retention_days` in the `.controlplane/controlplane.yml` file
51
+ - Deletes all images for an app that either exceed the max quantity or are older than the specified amount of days
52
+ - Specify the max quantity through `image_retention_max_qty` in the `.controlplane/controlplane.yml` file
53
+ - Specify the amount of days through `image_retention_days` in the `.controlplane/controlplane.yml` file
54
+ - If `image_retention_max_qty` is specified, any images that exceed it will be deleted, regardless of `image_retention_days`
53
55
  - Will ask for explicit user confirmation
54
- - Does not affect the latest image, regardless of how old it is
56
+ - Never deletes the latest image
55
57
 
56
58
  ```sh
57
- cpl cleanup-old-images -a $APP_NAME
59
+ cpl cleanup-images -a $APP_NAME
58
60
  ```
59
61
 
60
62
  ### `cleanup-stale-apps`
@@ -314,8 +316,8 @@ cpl run -a $APP_NAME
314
316
  # Need to quote COMMAND if setting ENV value or passing args.
315
317
  cpl run 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
316
318
 
317
- # COMMAND may also be passed at the end (in this case, no need to quote).
318
- cpl run -a $APP_NAME -- rails db:migrate
319
+ # COMMAND may also be passed at the end.
320
+ cpl run -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
319
321
 
320
322
  # Runs command, displays output, and exits shell.
321
323
  cpl run ls / -a $APP_NAME
@@ -362,8 +364,8 @@ cpl run:detached rails db:prepare -a $APP_NAME
362
364
  # Need to quote COMMAND if setting ENV value or passing args.
363
365
  cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
364
366
 
365
- # COMMAND may also be passed at the end (in this case, no need to quote).
366
- cpl run:detached -a $APP_NAME -- rails db:migrate
367
+ # COMMAND may also be passed at the end.
368
+ cpl run:detached -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
367
369
 
368
370
  # Uses a different image (which may not be promoted yet).
369
371
  cpl run:detached rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
@@ -83,7 +83,7 @@ module Command
83
83
  end
84
84
 
85
85
  def ensure_templates!
86
- missing_templates = templates.filter { |_template, filename| !File.exist?(filename) }.to_h
86
+ missing_templates = templates.reject { |_template, filename| File.exist?(filename) }.to_h
87
87
  return if missing_templates.empty?
88
88
 
89
89
  missing_templates_str = missing_templates.map do |template, filename|
data/lib/command/base.rb CHANGED
@@ -206,7 +206,7 @@ module Command
206
206
  end
207
207
 
208
208
  def latest_image_from(items, app_name: config.app, name_only: true)
209
- matching_items = items.filter { |item| item["name"].start_with?("#{app_name}:") }
209
+ matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") }
210
210
 
211
211
  # Or special string to indicate no image available
212
212
  if matching_items.empty?
@@ -221,22 +221,28 @@ module Command
221
221
  @latest_image ||= {}
222
222
  @latest_image[app] ||=
223
223
  begin
224
- items = cp.image_query(app, org)["items"]
224
+ items = cp.query_images(app, org)["items"]
225
225
  latest_image_from(items, app_name: app)
226
226
  end
227
227
  end
228
228
 
229
- def latest_image_next(app = config.app, org = config.org)
229
+ def latest_image_next(app = config.app, org = config.org, commit: nil)
230
+ commit ||= config.options[:commit]
231
+
230
232
  @latest_image_next ||= {}
231
233
  @latest_image_next[app] ||= begin
232
234
  latest_image_name = latest_image(app, org)
233
235
  image = latest_image_name.split(":").first
234
236
  image += ":#{extract_image_number(latest_image_name) + 1}"
235
- image += "_#{config.options[:commit]}" if config.options[:commit]
237
+ image += "_#{commit}" if commit
236
238
  image
237
239
  end
238
240
  end
239
241
 
242
+ def extract_image_commit(image_name)
243
+ image_name.match(/_(\h+)$/)&.captures&.first
244
+ end
245
+
240
246
  # NOTE: use simplified variant atm, as shelljoin do different escaping
241
247
  # TODO: most probably need better logic for escaping various quotes
242
248
  def args_join(args)
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Command
4
+ class CleanupImages < Base # rubocop:disable Metrics/ClassLength
5
+ NAME = "cleanup-images"
6
+ OPTIONS = [
7
+ app_option(required: true),
8
+ skip_confirm_option
9
+ ].freeze
10
+ DESCRIPTION = <<~DESC
11
+ Deletes all images for an app that either exceed the max quantity or are older than the specified amount of days
12
+ DESC
13
+ LONG_DESCRIPTION = <<~DESC
14
+ - Deletes all images for an app that either exceed the max quantity or are older than the specified amount of days
15
+ - Specify the max quantity through `image_retention_max_qty` in the `.controlplane/controlplane.yml` file
16
+ - Specify the amount of days through `image_retention_days` in the `.controlplane/controlplane.yml` file
17
+ - If `image_retention_max_qty` is specified, any images that exceed it will be deleted, regardless of `image_retention_days`
18
+ - Will ask for explicit user confirmation
19
+ - Never deletes the latest image
20
+ DESC
21
+
22
+ def call # rubocop:disable Metrics/MethodLength
23
+ ensure_max_qty_or_days!
24
+
25
+ return progress.puts("No images to delete.") if images_to_delete.empty?
26
+
27
+ progress.puts("Images to delete:")
28
+ images_to_delete.each do |image|
29
+ created = Shell.color((image[:created]).to_s, :red)
30
+ reason = Shell.color(image[:reason], :red)
31
+ progress.puts(" - #{image[:name]} (#{created} - #{reason})")
32
+ end
33
+
34
+ return unless confirm_delete
35
+
36
+ progress.puts
37
+ delete_images
38
+ end
39
+
40
+ private
41
+
42
+ def ensure_max_qty_or_days!
43
+ @image_retention_max_qty = config.current[:image_retention_max_qty]
44
+ @image_retention_days = config.current[:image_retention_days]
45
+ return if @image_retention_max_qty || @image_retention_days
46
+
47
+ raise "Can't find either option 'image_retention_max_qty' or 'image_retention_days' " \
48
+ "for app '#{@config.app}' in 'controlplane.yml'."
49
+ end
50
+
51
+ def app_prefix
52
+ config.should_app_start_with?(config.app) ? "#{config.app}-" : "#{config.app}:"
53
+ end
54
+
55
+ def remove_deployed_image(app, images)
56
+ return images unless cp.fetch_gvc(app)
57
+
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)
60
+ images.reject { |image| image["name"] == latest_image_name }
61
+ end
62
+
63
+ def parse_images_and_sort_by_created(images)
64
+ images = images.map do |image|
65
+ {
66
+ name: image["name"],
67
+ created: DateTime.parse(image["created"])
68
+ }
69
+ end
70
+ images.sort_by { |image| image[:created] }
71
+ end
72
+
73
+ def add_reason_to_images(images, reason)
74
+ images.map do |image|
75
+ {
76
+ **image,
77
+ reason: reason
78
+ }
79
+ end
80
+ end
81
+
82
+ def filter_images_by_max_qty(images)
83
+ return [], images unless @image_retention_max_qty && images.length > @image_retention_max_qty
84
+
85
+ split_index = images.length - @image_retention_max_qty
86
+ excess_images = images[0...split_index]
87
+ remaining_images = images[split_index...]
88
+ excess_images = add_reason_to_images(excess_images, "exceeds max quantity of #{@image_retention_max_qty}")
89
+
90
+ [excess_images, remaining_images]
91
+ end
92
+
93
+ def filter_images_by_days(images)
94
+ return [] unless @image_retention_days
95
+
96
+ now = DateTime.now
97
+ old_images = images.select { |image| (now - image[:created]).to_i >= @image_retention_days }
98
+ add_reason_to_images(old_images, "older than #{@image_retention_days} days")
99
+ end
100
+
101
+ def images_to_delete # rubocop:disable Metrics/MethodLength
102
+ @images_to_delete ||=
103
+ begin
104
+ result_images = []
105
+
106
+ images = cp.query_images["items"].select { |item| item["name"].start_with?(app_prefix) }
107
+ images_by_app = images.group_by { |item| item["repository"] }
108
+ images_by_app.each do |app, app_images|
109
+ app_images = remove_deployed_image(app, app_images)
110
+ app_images = parse_images_and_sort_by_created(app_images)
111
+ excess_images, remaining_images = filter_images_by_max_qty(app_images)
112
+ old_images = filter_images_by_days(remaining_images)
113
+
114
+ result_images += excess_images
115
+ result_images += old_images
116
+ end
117
+
118
+ result_images
119
+ end
120
+ end
121
+
122
+ def confirm_delete
123
+ return true if config.options[:yes]
124
+
125
+ Shell.confirm("\nAre you sure you want to delete these #{images_to_delete.length} images?")
126
+ end
127
+
128
+ def delete_images
129
+ images_to_delete.each do |image|
130
+ step("Deleting image '#{image[:name]}'") do
131
+ cp.image_delete(image[:name])
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -49,7 +49,7 @@ module Command
49
49
  gvcs.each do |gvc|
50
50
  app_name = gvc["name"]
51
51
 
52
- images = cp.image_query(app_name)["items"].filter { |item| item["name"].start_with?("#{app_name}:") }
52
+ images = cp.query_images(app_name)["items"].select { |item| item["name"].start_with?("#{app_name}:") }
53
53
  image = latest_image_from(images, app_name: app_name, name_only: false)
54
54
  next unless image
55
55
 
@@ -70,6 +70,7 @@ module Command
70
70
  cp.profile_switch(@upstream_profile)
71
71
  upstream_image = config.options[:image]
72
72
  upstream_image = latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
73
+ @commit = extract_image_commit(upstream_image)
73
74
  @upstream_image_url = "#{@upstream_org}.registry.cpln.io/#{upstream_image}"
74
75
  end
75
76
  end
@@ -77,7 +78,7 @@ module Command
77
78
  def fetch_app_image_url
78
79
  step("Fetching app image URL") do
79
80
  cp.profile_switch("default")
80
- app_image = latest_image_next(config.app, config.org)
81
+ app_image = latest_image_next(config.app, config.org, commit: @commit)
81
82
  @app_image_url = "#{config.org}.registry.cpln.io/#{app_image}"
82
83
  end
83
84
  end
@@ -41,7 +41,7 @@ module Command
41
41
  end
42
42
 
43
43
  def delete_images
44
- images = cp.image_query["items"]
44
+ images = cp.query_images["items"]
45
45
  .filter_map { |item| item["name"] if item["name"].start_with?("#{config.app}:") }
46
46
 
47
47
  return progress.puts("No images to delete.") unless images.any?
data/lib/command/run.rb CHANGED
@@ -31,8 +31,8 @@ module Command
31
31
  # Need to quote COMMAND if setting ENV value or passing args.
32
32
  cpl run 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
33
33
 
34
- # COMMAND may also be passed at the end (in this case, no need to quote).
35
- cpl run -a $APP_NAME -- rails db:migrate
34
+ # COMMAND may also be passed at the end.
35
+ cpl run -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
36
36
 
37
37
  # Runs command, displays output, and exits shell.
38
38
  cpl run ls / -a $APP_NAME
@@ -69,12 +69,8 @@ module Command
69
69
  now = DateTime.now
70
70
  stale_run_workload_created_days = config[:stale_run_workload_created_days]
71
71
 
72
- interactive_workloads = cp.query_workloads(
73
- "-run-", partial_gvc_match: config.should_app_start_with?(config.app), partial_workload_match: true
74
- )["items"]
75
- non_interactive_workloads = cp.query_workloads(
76
- "-runner-", partial_gvc_match: config.should_app_start_with?(config.app), partial_workload_match: true
77
- )["items"]
72
+ interactive_workloads = cp.query_workloads("-run-", partial_workload_match: true)["items"]
73
+ non_interactive_workloads = cp.query_workloads("-runner-", partial_workload_match: true)["items"]
78
74
  workloads = interactive_workloads + non_interactive_workloads
79
75
 
80
76
  workloads.each do |workload|
@@ -26,8 +26,8 @@ module Command
26
26
  # Need to quote COMMAND if setting ENV value or passing args.
27
27
  cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
28
28
 
29
- # COMMAND may also be passed at the end (in this case, no need to quote).
30
- cpl run:detached -a $APP_NAME -- rails db:migrate
29
+ # COMMAND may also be passed at the end.
30
+ cpl run:detached -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
31
31
 
32
32
  # Uses a different image (which may not be promoted yet).
33
33
  cpl run:detached rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
data/lib/core/config.rb CHANGED
@@ -34,8 +34,8 @@ class Config
34
34
  "#{app_dir}/.controlplane"
35
35
  end
36
36
 
37
- def should_app_start_with?(app)
38
- apps[app.to_sym]&.dig(:match_if_app_name_starts_with) || false
37
+ def should_app_start_with?(app_name)
38
+ apps[app_name.to_sym]&.dig(:match_if_app_name_starts_with) || false
39
39
  end
40
40
 
41
41
  private
@@ -86,6 +86,8 @@ class Config
86
86
 
87
87
  [app_name, app_options_with_new_keys]
88
88
  end
89
+
90
+ ensure_current_config_app!(app) if app
89
91
  end
90
92
 
91
93
  def load_app_config
@@ -115,12 +117,13 @@ class Config
115
117
  {
116
118
  org: :cpln_org,
117
119
  location: :default_location,
118
- prefix: :match_if_app_name_starts_with
120
+ prefix: :match_if_app_name_starts_with,
121
+ old_image_retention_days: :image_retention_days
119
122
  }
120
123
  end
121
124
 
122
125
  def warn_deprecated_options(app_options)
123
- deprecated_option_keys = new_option_keys.filter { |old_key| app_options.key?(old_key) }
126
+ deprecated_option_keys = new_option_keys.select { |old_key| app_options.key?(old_key) }
124
127
  return if deprecated_option_keys.empty?
125
128
 
126
129
  deprecated_option_keys.each do |old_key, new_key|
@@ -35,6 +35,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
35
35
 
36
36
  # image
37
37
 
38
+ def query_images(a_gvc = gvc, a_org = org, partial_gvc_match: nil)
39
+ partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
40
+ gvc_op = partial_gvc_match ? "~" : "="
41
+
42
+ api.query_images(org: a_org, gvc: a_gvc, gvc_op_type: gvc_op)
43
+ end
44
+
38
45
  def image_build(image, dockerfile:, build_args: [], push: true)
39
46
  cmd = "docker build -t #{image} -f #{dockerfile}"
40
47
  build_args.each { |build_arg| cmd += " --build-arg #{build_arg}" }
@@ -44,15 +51,6 @@ class Controlplane # rubocop:disable Metrics/ClassLength
44
51
  image_push(image) if push
45
52
  end
46
53
 
47
- def image_query(app_name = config.app, org_name = config.org)
48
- # When `match_if_app_name_starts_with` is `true`, we query for images from any gvc containing the name,
49
- # otherwise we query for images from a gvc with the exact name.
50
- op = config.should_app_start_with?(app_name) ? "~" : "="
51
-
52
- cmd = "cpln image query --org #{org_name} -o yaml --max -1 --prop repository#{op}#{app_name}"
53
- perform_yaml(cmd)
54
- end
55
-
56
54
  def image_delete(image)
57
55
  api.image_delete(org: org, image: image)
58
56
  end
@@ -132,11 +130,12 @@ class Controlplane # rubocop:disable Metrics/ClassLength
132
130
  raise "Can't find workload '#{workload}', please create it with 'cpl apply-template #{workload} -a #{config.app}'."
133
131
  end
134
132
 
135
- def query_workloads(workload, partial_gvc_match: false, partial_workload_match: false)
133
+ def query_workloads(workload, a_gvc = gvc, a_org = org, partial_workload_match: false, partial_gvc_match: nil)
134
+ partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
136
135
  gvc_op = partial_gvc_match ? "~" : "="
137
136
  workload_op = partial_workload_match ? "~" : "="
138
137
 
139
- api.query_workloads(org: org, gvc: gvc, workload: workload, gvc_op_type: gvc_op, workload_op_type: workload_op)
138
+ api.query_workloads(org: a_org, gvc: a_gvc, workload: workload, gvc_op_type: gvc_op, workload_op_type: workload_op)
140
139
  end
141
140
 
142
141
  def workload_get_replicas(workload, location:)
@@ -13,6 +13,18 @@ class ControlplaneApi
13
13
  api_json("/org/#{org}/gvc/#{gvc}", method: :delete)
14
14
  end
15
15
 
16
+ def query_images(org:, gvc:, gvc_op_type:)
17
+ terms = [
18
+ {
19
+ property: "repository",
20
+ op: gvc_op_type,
21
+ value: gvc
22
+ }
23
+ ]
24
+
25
+ query("/org/#{org}/image", terms)
26
+ end
27
+
16
28
  def image_delete(org:, image:)
17
29
  api_json("/org/#{org}/image/#{image}", method: :delete)
18
30
  end
@@ -34,26 +46,20 @@ class ControlplaneApi
34
46
  end
35
47
 
36
48
  def query_workloads(org:, gvc:, workload:, gvc_op_type:, workload_op_type:) # rubocop:disable Metrics/MethodLength
37
- body = {
38
- kind: "string",
39
- spec: {
40
- match: "all",
41
- terms: [
42
- {
43
- rel: "gvc",
44
- op: gvc_op_type,
45
- value: gvc
46
- },
47
- {
48
- property: "name",
49
- op: workload_op_type,
50
- value: workload
51
- }
52
- ]
49
+ terms = [
50
+ {
51
+ rel: "gvc",
52
+ op: gvc_op_type,
53
+ value: gvc
54
+ },
55
+ {
56
+ property: "name",
57
+ op: workload_op_type,
58
+ value: workload
53
59
  }
54
- }
60
+ ]
55
61
 
56
- api_json("/org/#{org}/workload/-query", method: :post, body: body)
62
+ query("/org/#{org}/workload", terms)
57
63
  end
58
64
 
59
65
  def workload_list(org:, gvc:)
@@ -90,6 +96,32 @@ class ControlplaneApi
90
96
 
91
97
  private
92
98
 
99
+ def fetch_query_pages(result)
100
+ loop do
101
+ next_page_url = result["links"].find { |link| link["rel"] == "next" }&.dig("href")
102
+ break unless next_page_url
103
+
104
+ next_page_result = api_json(next_page_url, method: :get)
105
+ result["items"] += next_page_result["items"]
106
+ result["links"] = next_page_result["links"]
107
+ end
108
+ end
109
+
110
+ def query(url, terms)
111
+ body = {
112
+ kind: "string",
113
+ spec: {
114
+ match: "all",
115
+ terms: terms
116
+ }
117
+ }
118
+
119
+ result = api_json("#{url}/-query", method: :post, body: body)
120
+ fetch_query_pages(result)
121
+
122
+ result
123
+ end
124
+
93
125
  # switch between cpln rest and api
94
126
  def api_json(...)
95
127
  ControlplaneApiDirect.new.call(...)
@@ -18,13 +18,13 @@ class ControlplaneApiDirect
18
18
  API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
19
19
 
20
20
  def call(url, method:, host: :api, body: nil) # rubocop:disable Metrics/MethodLength
21
- uri = URI("#{API_HOSTS[host]}#{url}")
21
+ uri = URI("#{api_host(host)}#{url}")
22
22
  request = API_METHODS[method].new(uri)
23
23
  request["Content-Type"] = "application/json"
24
24
  request["Authorization"] = api_token
25
25
  request.body = body.to_json if body
26
26
 
27
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
27
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
28
28
 
29
29
  case response
30
30
  when Net::HTTPOK
@@ -38,6 +38,15 @@ class ControlplaneApiDirect
38
38
  end
39
39
  end
40
40
 
41
+ def api_host(host)
42
+ case host
43
+ when :api
44
+ ENV.fetch("CPLN_ENDPOINT", API_HOSTS[host])
45
+ else
46
+ API_HOSTS[host]
47
+ end
48
+ end
49
+
41
50
  # rubocop:disable Style/ClassVars
42
51
  def api_token
43
52
  return @@api_token if defined?(@@api_token)
data/lib/cpl/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpl
4
- VERSION = "1.0.4"
4
+ VERSION = "1.1.0"
5
5
  MIN_CPLN_VERSION = "0.0.71"
6
6
  end
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "build": "build-image",
3
+ "cleanup_old_images": "cleanup-images",
3
4
  "promote": "deploy-image",
4
5
  "promote_image": "deploy-image",
5
6
  "runner": "run:detached",
@@ -65,7 +65,7 @@ module Release
65
65
  puts "Pulling latest commits from remote repository"
66
66
 
67
67
  sh_in_dir(gem_root, "git pull --rebase")
68
- raise "Failed in pulling latest changes from default remore repository." unless $CHILD_STATUS.success?
68
+ raise "Failed in pulling latest changes from default remote repository." unless $CHILD_STATUS.success?
69
69
  rescue Errno::ENOENT
70
70
  raise "Ensure you have Git and Bundler installed before continuing."
71
71
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cpl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-07-24 00:00:00.000000000 Z
12
+ date: 2023-09-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -234,7 +234,7 @@ files:
234
234
  - lib/command/apply_template.rb
235
235
  - lib/command/base.rb
236
236
  - lib/command/build_image.rb
237
- - lib/command/cleanup_old_images.rb
237
+ - lib/command/cleanup_images.rb
238
238
  - lib/command/cleanup_stale_apps.rb
239
239
  - lib/command/config.rb
240
240
  - lib/command/copy_image_from_upstream.rb
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Command
4
- class CleanupOldImages < Base
5
- NAME = "cleanup-old-images"
6
- OPTIONS = [
7
- app_option(required: true),
8
- skip_confirm_option
9
- ].freeze
10
- DESCRIPTION = "Deletes all images for an app that are older than the specified amount of days"
11
- LONG_DESCRIPTION = <<~DESC
12
- - Deletes all images for an app that are older than the specified amount of days
13
- - Specify the amount of days through `old_image_retention_days` in the `.controlplane/controlplane.yml` file
14
- - Will ask for explicit user confirmation
15
- - Does not affect the latest image, regardless of how old it is
16
- DESC
17
-
18
- def call
19
- return progress.puts("No old images found.") if old_images.empty?
20
-
21
- progress.puts("Old images:")
22
- old_images.each do |image|
23
- progress.puts(" - #{image[:name]} (#{Shell.color((image[:date]).to_s, :red)})")
24
- end
25
-
26
- return unless confirm_delete
27
-
28
- progress.puts
29
- delete_images
30
- end
31
-
32
- private
33
-
34
- def app_prefix
35
- config.should_app_start_with?(config.app) ? "#{config.app}-" : "#{config.app}:"
36
- end
37
-
38
- def remove_deployed_image(app, app_images)
39
- return app_images unless cp.fetch_gvc(app)
40
-
41
- # If app exists, remove latest image, because we don't want to delete the image that is currently deployed
42
- latest_image_name = latest_image_from(app_images, app_name: app)
43
- app_images.filter { |item| item["name"] != latest_image_name }
44
- end
45
-
46
- def old_images # rubocop:disable Metrics/MethodLength
47
- @old_images ||=
48
- begin
49
- result_images = []
50
-
51
- now = DateTime.now
52
- old_image_retention_days = config[:old_image_retention_days]
53
-
54
- images = cp.image_query["items"].filter { |item| item["name"].start_with?(app_prefix) }
55
- images_by_app = images.group_by { |item| item["repository"] }
56
- images_by_app.each do |app, app_images|
57
- app_images = remove_deployed_image(app, app_images)
58
- app_images.each do |image|
59
- created_date = DateTime.parse(image["created"])
60
- diff_in_days = (now - created_date).to_i
61
- next unless diff_in_days >= old_image_retention_days
62
-
63
- result_images.push({
64
- name: image["name"],
65
- date: created_date
66
- })
67
- end
68
- end
69
-
70
- result_images
71
- end
72
- end
73
-
74
- def confirm_delete
75
- return true if config.options[:yes]
76
-
77
- Shell.confirm("\nAre you sure you want to delete these #{old_images.length} images?")
78
- end
79
-
80
- def delete_images
81
- old_images.each do |image|
82
- step("Deleting image '#{image[:name]}'") do
83
- cp.image_delete(image[:name])
84
- end
85
- end
86
- end
87
- end
88
- end