cpl 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d187c354ac6a9a75c0aea498f8fbc060f741809d075439ee22663fdf06f265d
4
- data.tar.gz: ffde2f0f3be4663ba85baadf599c6a12882c2ba11734f9220d26366bacc5368d
3
+ metadata.gz: ad4122f8c7a91e2f665f348383580db755fc380d98d27d843ecbd30d49f5f7f7
4
+ data.tar.gz: 7a8d3d37524fad90a3849f5f2e233beaee64c59dbc62719e7951fec20b12a05f
5
5
  SHA512:
6
- metadata.gz: 70941d48b8ccc5bb3fb46b6c96c91fd0d3c2f13f2a702822ee6541231d90957df5a3b3c9d60d2091afad9f6e76be8870953f69085a97315c2a865d534d0c1ce7
7
- data.tar.gz: 3a109996c812ec2b2e9dc76c189bd5d3518835e5b221ff8f12416982f3542f1753dcba441dbb3ec32224e807d0c83db54fe78e21cad43da0d829b8d65593a6d5
6
+ metadata.gz: f075cf14b7d694b0d208993340f34643079752e853314fd31da964899c712d5ace36d7419e33e650dfaf47f3b18a5d708cb9140b4053931ffe193269e11c50c5
7
+ data.tar.gz: cfad8e5b8ef74ca29c7df36dd36aaeb64c83ef184d401441604a88cbd2555a96af96917f9fccdc839e375de6311649c59b4b0dd8e2f22e88286b6aca94ed41e1
data/CHANGELOG.md CHANGED
@@ -14,6 +14,44 @@ 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
+
33
+ ## [1.0.4] - 2023-07-21
34
+
35
+ ### Fixed
36
+
37
+ - Fixed issue where `run` commands fail when not providing image. [PR 68](https://github.com/shakacode/heroku-to-control-plane/pull/68) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
38
+
39
+ ## [1.0.3] - 2023-07-07
40
+
41
+ ### Fixed
42
+
43
+ - Fixed `run` commands when specifying image. [PR 62](https://github.com/shakacode/heroku-to-control-plane/pull/62) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
44
+ - Fixed `run:cleanup` command for non-interactive workloads. [PR 63](https://github.com/shakacode/heroku-to-control-plane/pull/63) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
45
+ - Fixed `run:cleanup` command for all apps that start with name. [PR 64](https://github.com/shakacode/heroku-to-control-plane/pull/64) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
46
+ - Fixed `cleanup-old-images` command for all apps that start with name. [PR 65](https://github.com/shakacode/heroku-to-control-plane/pull/65) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
47
+ - Fixed `--help` option. [PR 66](https://github.com/shakacode/heroku-to-control-plane/pull/66) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
48
+
49
+ ### Added
50
+
51
+ - Added `--use-local-token` option to `run:detached` command. [PR 61](https://github.com/shakacode/heroku-to-control-plane/pull/61) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
52
+
53
+ ## [1.0.2] - 2023-07-02
54
+
17
55
  ### Added
18
56
 
19
57
  - Added steps to migrate to docs. [PR 57](https://github.com/shakacode/heroku-to-control-plane/pull/57) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
@@ -33,6 +71,9 @@ _Please add entries here for your pull requests that are not yet released._
33
71
 
34
72
  - Initial release
35
73
 
36
- [Unreleased]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.0.1...HEAD
74
+ [Unreleased]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.0.4...HEAD
75
+ [1.0.4]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.0.3...v1.0.4
76
+ [1.0.3]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.0.2...v1.0.3
77
+ [1.0.2]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.0.1...v1.0.2
37
78
  [1.0.1]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.0.0...v1.0.1
38
79
  [1.0.0]: https://github.com/shakacode/heroku-to-control-plane/releases/tag/v1.0.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpl (1.0.3)
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.1)
29
- reline (>= 0.3.0)
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.5)
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
@@ -100,7 +100,7 @@ module Command
100
100
  # Override image if specified
101
101
  image = config.options[:image]
102
102
  image = latest_image if image == "latest"
103
- container_spec["image"] = "/org/#{config.org}/image/#{image}"
103
+ container_spec["image"] = "/org/#{config.org}/image/#{image}" if image
104
104
 
105
105
  # Set runner
106
106
  container_spec["env"] ||= []
@@ -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
@@ -91,7 +91,7 @@ module Command
91
91
  # Override image if specified
92
92
  image = config.options[:image]
93
93
  image = latest_image if image == "latest"
94
- container_spec["image"] = "/org/#{config.org}/image/#{image}"
94
+ container_spec["image"] = "/org/#{config.org}/image/#{image}" if image
95
95
 
96
96
  # Set cron job props
97
97
  spec["type"] = "cron"
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.3"
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.3
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-07 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