cpflow 3.0.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 +7 -0
- data/.github/workflows/check_cpln_links.yml +19 -0
- data/.github/workflows/command_docs.yml +24 -0
- data/.github/workflows/rspec-shared.yml +56 -0
- data/.github/workflows/rspec.yml +28 -0
- data/.github/workflows/rubocop.yml +24 -0
- data/.gitignore +18 -0
- data/.overcommit.yml +16 -0
- data/.rubocop.yml +22 -0
- data/.simplecov_spawn.rb +10 -0
- data/CHANGELOG.md +259 -0
- data/CONTRIBUTING.md +73 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +126 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/Rakefile +21 -0
- data/bin/cpflow +6 -0
- data/cpflow +6 -0
- data/cpflow.gemspec +41 -0
- data/docs/assets/grafana-alert.png +0 -0
- data/docs/assets/memcached.png +0 -0
- data/docs/assets/sidekiq-pre-stop-hook.png +0 -0
- data/docs/commands.md +454 -0
- data/docs/dns.md +15 -0
- data/docs/migrating.md +262 -0
- data/docs/postgres.md +436 -0
- data/docs/redis.md +128 -0
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/tips.md +150 -0
- data/docs/troubleshooting.md +6 -0
- data/examples/circleci.yml +104 -0
- data/examples/controlplane.yml +159 -0
- data/lib/command/apply_template.rb +209 -0
- data/lib/command/base.rb +540 -0
- data/lib/command/build_image.rb +49 -0
- data/lib/command/cleanup_images.rb +136 -0
- data/lib/command/cleanup_stale_apps.rb +79 -0
- data/lib/command/config.rb +48 -0
- data/lib/command/copy_image_from_upstream.rb +108 -0
- data/lib/command/delete.rb +149 -0
- data/lib/command/deploy_image.rb +56 -0
- data/lib/command/doctor.rb +47 -0
- data/lib/command/env.rb +22 -0
- data/lib/command/exists.rb +23 -0
- data/lib/command/generate.rb +45 -0
- data/lib/command/info.rb +222 -0
- data/lib/command/latest_image.rb +19 -0
- data/lib/command/logs.rb +49 -0
- data/lib/command/maintenance.rb +42 -0
- data/lib/command/maintenance_off.rb +62 -0
- data/lib/command/maintenance_on.rb +62 -0
- data/lib/command/maintenance_set_page.rb +34 -0
- data/lib/command/no_command.rb +23 -0
- data/lib/command/open.rb +33 -0
- data/lib/command/open_console.rb +26 -0
- data/lib/command/promote_app_from_upstream.rb +38 -0
- data/lib/command/ps.rb +41 -0
- data/lib/command/ps_restart.rb +37 -0
- data/lib/command/ps_start.rb +51 -0
- data/lib/command/ps_stop.rb +82 -0
- data/lib/command/ps_wait.rb +40 -0
- data/lib/command/run.rb +573 -0
- data/lib/command/setup_app.rb +113 -0
- data/lib/command/test.rb +23 -0
- data/lib/command/version.rb +18 -0
- data/lib/constants/exit_code.rb +7 -0
- data/lib/core/config.rb +316 -0
- data/lib/core/controlplane.rb +552 -0
- data/lib/core/controlplane_api.rb +170 -0
- data/lib/core/controlplane_api_direct.rb +112 -0
- data/lib/core/doctor_service.rb +104 -0
- data/lib/core/helpers.rb +26 -0
- data/lib/core/shell.rb +100 -0
- data/lib/core/template_parser.rb +76 -0
- data/lib/cpflow/version.rb +6 -0
- data/lib/cpflow.rb +288 -0
- data/lib/deprecated_commands.json +9 -0
- data/lib/generator_templates/Dockerfile +27 -0
- data/lib/generator_templates/controlplane.yml +62 -0
- data/lib/generator_templates/entrypoint.sh +8 -0
- data/lib/generator_templates/templates/app.yml +21 -0
- data/lib/generator_templates/templates/postgres.yml +176 -0
- data/lib/generator_templates/templates/rails.yml +36 -0
- data/rakelib/create_release.rake +81 -0
- data/script/add_command +37 -0
- data/script/check_command_docs +3 -0
- data/script/check_cpln_links +45 -0
- data/script/rename_command +43 -0
- data/script/update_command_docs +62 -0
- data/templates/app.yml +13 -0
- data/templates/daily-task.yml +32 -0
- data/templates/maintenance.yml +25 -0
- data/templates/memcached.yml +24 -0
- data/templates/postgres.yml +32 -0
- data/templates/rails.yml +27 -0
- data/templates/redis.yml +21 -0
- data/templates/redis2.yml +37 -0
- data/templates/sidekiq.yml +38 -0
- metadata +341 -0
@@ -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 = cp.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
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class CleanupStaleApps < Base
|
5
|
+
NAME = "cleanup-stale-apps"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
skip_confirm_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Deletes the whole app (GVC with all workloads, all volumesets and all images) for all stale apps"
|
11
|
+
LONG_DESCRIPTION = <<~DESC
|
12
|
+
- Deletes the whole app (GVC with all workloads, all volumesets and all images) for all stale apps
|
13
|
+
- Also unbinds the app from the secrets policy, as long as both the identity and the policy exist (and are bound)
|
14
|
+
- Stale apps are identified based on the creation date of the latest image
|
15
|
+
- Specify the amount of days after an app should be considered stale through `stale_app_image_deployed_days` in the `.controlplane/controlplane.yml` file
|
16
|
+
- If `match_if_app_name_starts_with` is `true` in the `.controlplane/controlplane.yml` file, it will delete all stale apps that start with the name
|
17
|
+
- Will ask for explicit user confirmation
|
18
|
+
DESC
|
19
|
+
|
20
|
+
def call # rubocop:disable Metrics/MethodLength
|
21
|
+
return progress.puts("No stale apps found.") if stale_apps.empty?
|
22
|
+
|
23
|
+
progress.puts("Stale apps:")
|
24
|
+
stale_apps.each do |app|
|
25
|
+
progress.puts(" - #{app[:name]} (#{Shell.color((app[:date]).to_s, :red)})")
|
26
|
+
end
|
27
|
+
|
28
|
+
return unless confirm_delete
|
29
|
+
|
30
|
+
progress.puts
|
31
|
+
stale_apps.each do |app|
|
32
|
+
delete_app(app[:name])
|
33
|
+
progress.puts
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def stale_apps # rubocop:disable Metrics/MethodLength
|
40
|
+
@stale_apps ||=
|
41
|
+
begin
|
42
|
+
apps = []
|
43
|
+
|
44
|
+
now = DateTime.now
|
45
|
+
stale_app_image_deployed_days = config[:stale_app_image_deployed_days]
|
46
|
+
|
47
|
+
gvcs = cp.gvc_query(config.app)["items"]
|
48
|
+
gvcs.each do |gvc|
|
49
|
+
app_name = gvc["name"]
|
50
|
+
|
51
|
+
images = cp.query_images(app_name)["items"].select { |item| item["name"].start_with?("#{app_name}:") }
|
52
|
+
image = cp.latest_image_from(images, app_name: app_name, name_only: false)
|
53
|
+
next unless image
|
54
|
+
|
55
|
+
created_date = DateTime.parse(image["created"])
|
56
|
+
diff_in_days = (now - created_date).to_i
|
57
|
+
next unless diff_in_days >= stale_app_image_deployed_days
|
58
|
+
|
59
|
+
apps.push({
|
60
|
+
name: app_name,
|
61
|
+
date: created_date
|
62
|
+
})
|
63
|
+
end
|
64
|
+
|
65
|
+
apps
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def confirm_delete
|
70
|
+
return true if config.options[:yes]
|
71
|
+
|
72
|
+
Shell.confirm("\nAre you sure you want to delete these #{stale_apps.length} apps?")
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete_app(app)
|
76
|
+
Cpflow::Cli.start(["delete", "-a", app, "--yes"])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Config < Base
|
5
|
+
NAME = "config"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Displays config for each app or a specific app"
|
10
|
+
LONG_DESCRIPTION = <<~DESC
|
11
|
+
- Displays config for each app or a specific app
|
12
|
+
DESC
|
13
|
+
EXAMPLES = <<~EX
|
14
|
+
```sh
|
15
|
+
# Shows the config for each app.
|
16
|
+
cpflow config
|
17
|
+
|
18
|
+
# Shows the config for a specific app.
|
19
|
+
cpflow config -a $APP_NAME
|
20
|
+
```
|
21
|
+
EX
|
22
|
+
|
23
|
+
def call # rubocop:disable Metrics/MethodLength
|
24
|
+
if config.app
|
25
|
+
puts "#{Shell.color("Current config (app '#{config.app}')", :blue)}:"
|
26
|
+
puts pretty_print(config.current)
|
27
|
+
puts
|
28
|
+
else
|
29
|
+
config.apps.each do |app_name, app_options|
|
30
|
+
puts "#{Shell.color("Config for app '#{app_name}'", :blue)}:"
|
31
|
+
puts pretty_print(app_options)
|
32
|
+
puts
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def pretty_print(hash)
|
40
|
+
hash.transform_keys(&:to_s)
|
41
|
+
.to_yaml(indentation: 2)[4..]
|
42
|
+
# Adds an indentation of 2 to the beginning of each line
|
43
|
+
.gsub(/^(\s*)/, " \\1")
|
44
|
+
# Adds an indentation of 2 before the '-' in array items
|
45
|
+
.gsub(/^(\s*)-\s/, "\\1 - ")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class CopyImageFromUpstream < Base
|
5
|
+
NAME = "copy-image-from-upstream"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
upstream_token_option(required: true),
|
9
|
+
image_option
|
10
|
+
].freeze
|
11
|
+
DESCRIPTION = "Copies an image (by default the latest) from a source org to the current org"
|
12
|
+
LONG_DESCRIPTION = <<~DESC
|
13
|
+
- Copies an image (by default the latest) from a source org to the current org
|
14
|
+
- The source app must be specified either through the `CPLN_UPSTREAM` env var or `upstream` in the `.controlplane/controlplane.yml` file
|
15
|
+
- Additionally, the token for the source org must be provided through `--upstream-token` or `-t`
|
16
|
+
- A `cpln` profile will be temporarily created to pull the image from the source org
|
17
|
+
DESC
|
18
|
+
EXAMPLES = <<~EX
|
19
|
+
```sh
|
20
|
+
# Copies the latest image from the source org to the current org.
|
21
|
+
cpflow copy-image-from-upstream -a $APP_NAME --upstream-token $UPSTREAM_TOKEN
|
22
|
+
|
23
|
+
# Copies a specific image from the source org to the current org.
|
24
|
+
cpflow copy-image-from-upstream -a $APP_NAME --upstream-token $UPSTREAM_TOKEN --image appimage:123
|
25
|
+
```
|
26
|
+
EX
|
27
|
+
|
28
|
+
def call # rubocop:disable Metrics/MethodLength
|
29
|
+
ensure_docker_running!
|
30
|
+
|
31
|
+
@upstream = ENV.fetch("CPLN_UPSTREAM", nil) || config[:upstream]
|
32
|
+
@upstream_org = ENV.fetch("CPLN_ORG_UPSTREAM", nil) || config.find_app_config(@upstream)&.dig(:cpln_org)
|
33
|
+
ensure_upstream_org!
|
34
|
+
|
35
|
+
create_upstream_profile
|
36
|
+
fetch_upstream_image_url
|
37
|
+
fetch_app_image_url
|
38
|
+
pull_image_from_upstream
|
39
|
+
push_image_to_app
|
40
|
+
ensure
|
41
|
+
cp.profile_switch("default")
|
42
|
+
delete_upstream_profile
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def ensure_upstream_org!
|
48
|
+
return if @upstream_org
|
49
|
+
|
50
|
+
raise "Can't find option 'cpln_org' for app '#{@upstream}' in 'controlplane.yml', " \
|
51
|
+
"and CPLN_ORG_UPSTREAM env var is not set."
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_upstream_profile
|
55
|
+
step("Creating upstream profile") do
|
56
|
+
loop do
|
57
|
+
@upstream_profile = "upstream-#{random_four_digits}"
|
58
|
+
break unless cp.profile_exists?(@upstream_profile)
|
59
|
+
end
|
60
|
+
|
61
|
+
cp.profile_create(@upstream_profile, config.options[:upstream_token])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_upstream_image_url
|
66
|
+
step("Fetching upstream image URL") do
|
67
|
+
cp.profile_switch(@upstream_profile)
|
68
|
+
upstream_image = config.options[:image]
|
69
|
+
upstream_image = cp.latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
|
70
|
+
@commit = cp.extract_image_commit(upstream_image)
|
71
|
+
@upstream_image_url = "#{@upstream_org}.registry.cpln.io/#{upstream_image}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def fetch_app_image_url
|
76
|
+
step("Fetching app image URL") do
|
77
|
+
cp.profile_switch("default")
|
78
|
+
app_image = cp.latest_image_next(config.app, config.org, commit: @commit)
|
79
|
+
@app_image_url = "#{config.org}.registry.cpln.io/#{app_image}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def pull_image_from_upstream
|
84
|
+
step("Pulling image from '#{@upstream_image_url}'") do
|
85
|
+
cp.profile_switch(@upstream_profile)
|
86
|
+
cp.image_login(@upstream_org)
|
87
|
+
cp.image_pull(@upstream_image_url)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def push_image_to_app
|
92
|
+
step("Pushing image to '#{@app_image_url}'") do
|
93
|
+
cp.profile_switch("default")
|
94
|
+
cp.image_login(config.org)
|
95
|
+
cp.image_tag(@upstream_image_url, @app_image_url)
|
96
|
+
cp.image_push(@app_image_url)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def delete_upstream_profile
|
101
|
+
return unless @upstream_profile
|
102
|
+
|
103
|
+
step("Deleting upstream profile") do
|
104
|
+
cp.profile_delete(@upstream_profile)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Delete < Base # rubocop:disable Metrics/ClassLength
|
5
|
+
NAME = "delete"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
workload_option,
|
9
|
+
skip_confirm_option,
|
10
|
+
skip_pre_deletion_hook_option
|
11
|
+
].freeze
|
12
|
+
DESCRIPTION = "Deletes the whole app (GVC with all workloads, all volumesets and all images) or a specific workload"
|
13
|
+
LONG_DESCRIPTION = <<~DESC
|
14
|
+
- Deletes the whole app (GVC with all workloads, all volumesets and all images) or a specific workload
|
15
|
+
- Also unbinds the app from the secrets policy, as long as both the identity and the policy exist (and are bound)
|
16
|
+
- Will ask for explicit user confirmation
|
17
|
+
- Runs a pre-deletion hook before the app is deleted if `hooks.pre_deletion` is specified in the `.controlplane/controlplane.yml` file
|
18
|
+
- If the hook exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
19
|
+
- Use `--skip-pre-deletion-hook` to skip the hook if specified in `controlplane.yml`
|
20
|
+
DESC
|
21
|
+
EXAMPLES = <<~EX
|
22
|
+
```sh
|
23
|
+
# Deletes the whole app (GVC with all workloads, all volumesets and all images).
|
24
|
+
cpflow delete -a $APP_NAME
|
25
|
+
|
26
|
+
# Deletes a specific workload.
|
27
|
+
cpflow delete -a $APP_NAME -w $WORKLOAD_NAME
|
28
|
+
```
|
29
|
+
EX
|
30
|
+
|
31
|
+
def call
|
32
|
+
workload = config.options[:workload]
|
33
|
+
if workload
|
34
|
+
delete_single_workload(workload)
|
35
|
+
else
|
36
|
+
delete_whole_app
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def delete_single_workload(workload)
|
43
|
+
if cp.fetch_workload(workload).nil?
|
44
|
+
return progress.puts("Workload '#{workload}' does not exist in app '#{config.app}'.")
|
45
|
+
end
|
46
|
+
return unless confirm_delete(workload)
|
47
|
+
|
48
|
+
delete_workload(workload)
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete_whole_app
|
52
|
+
return progress.puts("App '#{config.app}' does not exist.") if cp.fetch_gvc.nil?
|
53
|
+
|
54
|
+
check_volumesets
|
55
|
+
check_images
|
56
|
+
return unless confirm_delete(config.app)
|
57
|
+
|
58
|
+
run_pre_deletion_hook unless config.options[:skip_pre_deletion_hook]
|
59
|
+
unbind_identity_from_policy
|
60
|
+
delete_volumesets
|
61
|
+
delete_gvc
|
62
|
+
delete_images
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_volumesets
|
66
|
+
@volumesets = cp.fetch_volumesets["items"]
|
67
|
+
return progress.puts("No volumesets to delete from app '#{config.app}'.") unless @volumesets.any?
|
68
|
+
|
69
|
+
message = "The following volumesets will be deleted along with the app '#{config.app}':"
|
70
|
+
volumesets_list = @volumesets.map { |volumeset| "- #{volumeset['name']}" }.join("\n")
|
71
|
+
progress.puts("#{Shell.color(message, :red)}\n#{volumesets_list}\n\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
def check_images
|
75
|
+
@images = cp.query_images["items"]
|
76
|
+
.select { |image| image["name"].start_with?("#{config.app}:") }
|
77
|
+
return progress.puts("No images to delete from app '#{config.app}'.") unless @images.any?
|
78
|
+
|
79
|
+
message = "The following images will be deleted along with the app '#{config.app}':"
|
80
|
+
images_list = @images.map { |image| "- #{image['name']}" }.join("\n")
|
81
|
+
progress.puts("#{Shell.color(message, :red)}\n#{images_list}\n\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
def confirm_delete(item)
|
85
|
+
return true if config.options[:yes]
|
86
|
+
|
87
|
+
confirmed = Shell.confirm("Are you sure you want to delete '#{item}'?")
|
88
|
+
return false unless confirmed
|
89
|
+
|
90
|
+
progress.puts
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
def delete_gvc
|
95
|
+
step("Deleting app '#{config.app}'") do
|
96
|
+
cp.gvc_delete
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def delete_workload(workload)
|
101
|
+
step("Deleting workload '#{workload}' from app '#{config.app}'") do
|
102
|
+
cp.delete_workload(workload)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete_volumesets
|
107
|
+
@volumesets.each do |volumeset|
|
108
|
+
step("Deleting volumeset '#{volumeset['name']}' from app '#{config.app}'") do
|
109
|
+
# If the volumeset is attached to a workload, we need to delete the workload first
|
110
|
+
workload = volumeset.dig("status", "usedByWorkload")&.split("/")&.last
|
111
|
+
cp.delete_workload(workload) if workload
|
112
|
+
|
113
|
+
cp.delete_volumeset(volumeset["name"])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def delete_images
|
119
|
+
@images.each do |image|
|
120
|
+
step("Deleting image '#{image['name']}' from app '#{config.app}'") do
|
121
|
+
cp.image_delete(image["name"])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def unbind_identity_from_policy
|
127
|
+
return if cp.fetch_identity(config.identity).nil?
|
128
|
+
|
129
|
+
policy = cp.fetch_policy(config.secrets_policy)
|
130
|
+
return if policy.nil?
|
131
|
+
|
132
|
+
is_bound = policy["bindings"].any? do |binding|
|
133
|
+
binding["principalLinks"].any? { |link| link == config.identity_link }
|
134
|
+
end
|
135
|
+
return unless is_bound
|
136
|
+
|
137
|
+
step("Unbinding identity from policy for app '#{config.app}'") do
|
138
|
+
cp.unbind_identity_from_policy(config.identity_link, config.secrets_policy)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def run_pre_deletion_hook
|
143
|
+
pre_deletion_hook = config.current.dig(:hooks, :pre_deletion)
|
144
|
+
return unless pre_deletion_hook
|
145
|
+
|
146
|
+
run_command_in_latest_image(pre_deletion_hook, title: "pre-deletion hook")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class DeployImage < Base
|
5
|
+
NAME = "deploy-image"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
run_release_phase_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Deploys the latest image to app workloads, and runs a release script (optional)"
|
11
|
+
LONG_DESCRIPTION = <<~DESC
|
12
|
+
- Deploys the latest image to app workloads
|
13
|
+
- Runs a release script before deploying if `release_script` is specified in the `.controlplane/controlplane.yml` file and `--run-release-phase` is provided
|
14
|
+
- The release script is run in the context of `cpflow run` with the latest image
|
15
|
+
- If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
16
|
+
DESC
|
17
|
+
|
18
|
+
def call # rubocop:disable Metrics/MethodLength
|
19
|
+
run_release_script if config.options[:run_release_phase]
|
20
|
+
|
21
|
+
deployed_endpoints = {}
|
22
|
+
|
23
|
+
image = cp.latest_image
|
24
|
+
if cp.fetch_image_details(image).nil?
|
25
|
+
raise "Image '#{image}' does not exist in the Docker repository on Control Plane " \
|
26
|
+
"(see https://console.cpln.io/console/org/#{config.org}/repository/#{config.app}). " \
|
27
|
+
"Use `cpflow build-image` first."
|
28
|
+
end
|
29
|
+
|
30
|
+
config[:app_workloads].each do |workload|
|
31
|
+
workload_data = cp.fetch_workload!(workload)
|
32
|
+
workload_data.dig("spec", "containers").each do |container|
|
33
|
+
next unless container["image"].match?(%r{^/org/#{config.org}/image/#{config.app}:})
|
34
|
+
|
35
|
+
container_name = container["name"]
|
36
|
+
step("Deploying image '#{image}' for workload '#{container_name}'") do
|
37
|
+
cp.workload_set_image_ref(workload, container: container_name, image: image)
|
38
|
+
deployed_endpoints[container_name] = workload_data.dig("status", "endpoint")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
progress.puts("\nDeployed endpoints:")
|
44
|
+
deployed_endpoints.each do |workload, endpoint|
|
45
|
+
progress.puts(" - #{workload}: #{endpoint}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def run_release_script
|
52
|
+
release_script = config[:release_script]
|
53
|
+
run_command_in_latest_image(release_script, title: "release script")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Doctor < Base
|
5
|
+
NAME = "doctor"
|
6
|
+
OPTIONS = [
|
7
|
+
validations_option,
|
8
|
+
app_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Runs validations"
|
11
|
+
LONG_DESCRIPTION = <<~DESC
|
12
|
+
- Runs validations
|
13
|
+
DESC
|
14
|
+
EXAMPLES = <<~EX
|
15
|
+
```sh
|
16
|
+
# Runs all validations that don't require additional options by default.
|
17
|
+
cpflow doctor
|
18
|
+
|
19
|
+
# Runs config validation.
|
20
|
+
cpflow doctor --validations config
|
21
|
+
|
22
|
+
# Runs templates validation (requires app).
|
23
|
+
cpflow doctor --validations templates -a $APP_NAME
|
24
|
+
```
|
25
|
+
EX
|
26
|
+
VALIDATIONS = [].freeze
|
27
|
+
|
28
|
+
def call
|
29
|
+
validations = config.options[:validations].split(",")
|
30
|
+
ensure_required_options!(validations)
|
31
|
+
|
32
|
+
doctor_service = DoctorService.new(config)
|
33
|
+
doctor_service.run_validations(validations)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def ensure_required_options!(validations)
|
39
|
+
validations.each do |validation|
|
40
|
+
case validation
|
41
|
+
when "templates"
|
42
|
+
raise "App is required for templates validation." unless config.app
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/command/env.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Env < Base
|
5
|
+
NAME = "env"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true)
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Displays app-specific environment variables"
|
10
|
+
LONG_DESCRIPTION = <<~DESC
|
11
|
+
- Displays app-specific environment variables
|
12
|
+
DESC
|
13
|
+
WITH_INFO_HEADER = false
|
14
|
+
|
15
|
+
def call
|
16
|
+
cp.fetch_gvc!.dig("spec", "env").map do |prop|
|
17
|
+
# NOTE: atm no special chars handling, consider adding if needed
|
18
|
+
puts "#{prop['name']}=#{prop['value']}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Exists < Base
|
5
|
+
NAME = "exists"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true)
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Shell-checks if an application (GVC) exists, useful in scripts"
|
10
|
+
LONG_DESCRIPTION = <<~DESC
|
11
|
+
- Shell-checks if an application (GVC) exists, useful in scripts, e.g.:
|
12
|
+
DESC
|
13
|
+
EXAMPLES = <<~EX
|
14
|
+
```sh
|
15
|
+
if [ cpflow exists -a $APP_NAME ]; ...
|
16
|
+
```
|
17
|
+
EX
|
18
|
+
|
19
|
+
def call
|
20
|
+
exit(cp.fetch_gvc.nil? ? ExitCode::ERROR_DEFAULT : ExitCode::SUCCESS)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|