cpl 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +8 -5
- data/README.md +6 -2
- data/docs/commands.md +11 -9
- data/lib/command/apply_template.rb +1 -1
- data/lib/command/base.rb +10 -4
- data/lib/command/cleanup_images.rb +136 -0
- data/lib/command/cleanup_stale_apps.rb +1 -1
- data/lib/command/copy_image_from_upstream.rb +2 -1
- data/lib/command/delete.rb +1 -1
- data/lib/command/run.rb +2 -2
- data/lib/command/run_cleanup.rb +2 -6
- data/lib/command/run_detached.rb +2 -2
- data/lib/core/config.rb +7 -4
- data/lib/core/controlplane.rb +10 -11
- data/lib/core/controlplane_api.rb +50 -18
- data/lib/core/controlplane_api_direct.rb +11 -2
- data/lib/cpl/version.rb +1 -1
- data/lib/deprecated_commands.json +1 -0
- data/rakelib/create_release.rake +1 -1
- metadata +3 -3
- data/lib/command/cleanup_old_images.rb +0 -88
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad4122f8c7a91e2f665f348383580db755fc380d98d27d843ecbd30d49f5f7f7
|
4
|
+
data.tar.gz: 7a8d3d37524fad90a3849f5f2e233beaee64c59dbc62719e7951fec20b12a05f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
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.
|
29
|
-
|
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.
|
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.
|
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-
|
168
|
-
|
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-
|
49
|
+
### `cleanup-images`
|
50
50
|
|
51
|
-
- Deletes all images for an app that are older than the specified amount of days
|
52
|
-
- Specify the
|
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
|
-
-
|
56
|
+
- Never deletes the latest image
|
55
57
|
|
56
58
|
```sh
|
57
|
-
cpl cleanup-
|
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
|
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
|
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.
|
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.
|
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.
|
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 += "_#{
|
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.
|
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
|
data/lib/command/delete.rb
CHANGED
@@ -41,7 +41,7 @@ module Command
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def delete_images
|
44
|
-
images = cp.
|
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
|
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
|
data/lib/command/run_cleanup.rb
CHANGED
@@ -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
|
-
|
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|
|
data/lib/command/run_detached.rb
CHANGED
@@ -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
|
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?(
|
38
|
-
apps[
|
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.
|
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|
|
data/lib/core/controlplane.rb
CHANGED
@@ -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,
|
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:
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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("#{
|
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:
|
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
data/rakelib/create_release.rake
CHANGED
@@ -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
|
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
|
+
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-
|
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/
|
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
|