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 +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
|