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