cpflow 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/check_cpln_links.yml +19 -0
  3. data/.github/workflows/command_docs.yml +24 -0
  4. data/.github/workflows/rspec-shared.yml +56 -0
  5. data/.github/workflows/rspec.yml +28 -0
  6. data/.github/workflows/rubocop.yml +24 -0
  7. data/.gitignore +18 -0
  8. data/.overcommit.yml +16 -0
  9. data/.rubocop.yml +22 -0
  10. data/.simplecov_spawn.rb +10 -0
  11. data/CHANGELOG.md +259 -0
  12. data/CONTRIBUTING.md +73 -0
  13. data/Gemfile +7 -0
  14. data/Gemfile.lock +126 -0
  15. data/LICENSE +21 -0
  16. data/README.md +546 -0
  17. data/Rakefile +21 -0
  18. data/bin/cpflow +6 -0
  19. data/cpflow +6 -0
  20. data/cpflow.gemspec +41 -0
  21. data/docs/assets/grafana-alert.png +0 -0
  22. data/docs/assets/memcached.png +0 -0
  23. data/docs/assets/sidekiq-pre-stop-hook.png +0 -0
  24. data/docs/commands.md +454 -0
  25. data/docs/dns.md +15 -0
  26. data/docs/migrating.md +262 -0
  27. data/docs/postgres.md +436 -0
  28. data/docs/redis.md +128 -0
  29. data/docs/secrets-and-env-values.md +42 -0
  30. data/docs/tips.md +150 -0
  31. data/docs/troubleshooting.md +6 -0
  32. data/examples/circleci.yml +104 -0
  33. data/examples/controlplane.yml +159 -0
  34. data/lib/command/apply_template.rb +209 -0
  35. data/lib/command/base.rb +540 -0
  36. data/lib/command/build_image.rb +49 -0
  37. data/lib/command/cleanup_images.rb +136 -0
  38. data/lib/command/cleanup_stale_apps.rb +79 -0
  39. data/lib/command/config.rb +48 -0
  40. data/lib/command/copy_image_from_upstream.rb +108 -0
  41. data/lib/command/delete.rb +149 -0
  42. data/lib/command/deploy_image.rb +56 -0
  43. data/lib/command/doctor.rb +47 -0
  44. data/lib/command/env.rb +22 -0
  45. data/lib/command/exists.rb +23 -0
  46. data/lib/command/generate.rb +45 -0
  47. data/lib/command/info.rb +222 -0
  48. data/lib/command/latest_image.rb +19 -0
  49. data/lib/command/logs.rb +49 -0
  50. data/lib/command/maintenance.rb +42 -0
  51. data/lib/command/maintenance_off.rb +62 -0
  52. data/lib/command/maintenance_on.rb +62 -0
  53. data/lib/command/maintenance_set_page.rb +34 -0
  54. data/lib/command/no_command.rb +23 -0
  55. data/lib/command/open.rb +33 -0
  56. data/lib/command/open_console.rb +26 -0
  57. data/lib/command/promote_app_from_upstream.rb +38 -0
  58. data/lib/command/ps.rb +41 -0
  59. data/lib/command/ps_restart.rb +37 -0
  60. data/lib/command/ps_start.rb +51 -0
  61. data/lib/command/ps_stop.rb +82 -0
  62. data/lib/command/ps_wait.rb +40 -0
  63. data/lib/command/run.rb +573 -0
  64. data/lib/command/setup_app.rb +113 -0
  65. data/lib/command/test.rb +23 -0
  66. data/lib/command/version.rb +18 -0
  67. data/lib/constants/exit_code.rb +7 -0
  68. data/lib/core/config.rb +316 -0
  69. data/lib/core/controlplane.rb +552 -0
  70. data/lib/core/controlplane_api.rb +170 -0
  71. data/lib/core/controlplane_api_direct.rb +112 -0
  72. data/lib/core/doctor_service.rb +104 -0
  73. data/lib/core/helpers.rb +26 -0
  74. data/lib/core/shell.rb +100 -0
  75. data/lib/core/template_parser.rb +76 -0
  76. data/lib/cpflow/version.rb +6 -0
  77. data/lib/cpflow.rb +288 -0
  78. data/lib/deprecated_commands.json +9 -0
  79. data/lib/generator_templates/Dockerfile +27 -0
  80. data/lib/generator_templates/controlplane.yml +62 -0
  81. data/lib/generator_templates/entrypoint.sh +8 -0
  82. data/lib/generator_templates/templates/app.yml +21 -0
  83. data/lib/generator_templates/templates/postgres.yml +176 -0
  84. data/lib/generator_templates/templates/rails.yml +36 -0
  85. data/rakelib/create_release.rake +81 -0
  86. data/script/add_command +37 -0
  87. data/script/check_command_docs +3 -0
  88. data/script/check_cpln_links +45 -0
  89. data/script/rename_command +43 -0
  90. data/script/update_command_docs +62 -0
  91. data/templates/app.yml +13 -0
  92. data/templates/daily-task.yml +32 -0
  93. data/templates/maintenance.yml +25 -0
  94. data/templates/memcached.yml +24 -0
  95. data/templates/postgres.yml +32 -0
  96. data/templates/rails.yml +27 -0
  97. data/templates/redis.yml +21 -0
  98. data/templates/redis2.yml +37 -0
  99. data/templates/sidekiq.yml +38 -0
  100. 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
@@ -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