cpflow 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|