cpl 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/CHANGELOG.md +111 -85
- data/CONTRIBUTING.md +2 -2
- data/Gemfile.lock +2 -2
- data/README.md +14 -5
- data/cpl.gemspec +1 -1
- data/docs/commands.md +30 -6
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/tips.md +1 -40
- data/examples/controlplane.yml +12 -3
- data/lib/command/apply_template.rb +70 -80
- data/lib/command/base.rb +82 -71
- data/lib/command/build_image.rb +2 -2
- data/lib/command/cleanup_images.rb +1 -1
- data/lib/command/cleanup_stale_apps.rb +1 -1
- data/lib/command/copy_image_from_upstream.rb +3 -3
- data/lib/command/delete.rb +17 -5
- data/lib/command/deploy_image.rb +6 -21
- data/lib/command/doctor.rb +47 -0
- data/lib/command/latest_image.rb +1 -1
- data/lib/command/no_command.rb +1 -0
- data/lib/command/promote_app_from_upstream.rb +1 -1
- data/lib/command/run.rb +17 -11
- data/lib/command/setup_app.rb +80 -16
- data/lib/command/test.rb +1 -0
- data/lib/command/version.rb +1 -0
- data/lib/core/config.rb +40 -12
- data/lib/core/controlplane.rb +53 -0
- data/lib/core/controlplane_api.rb +13 -7
- data/lib/core/controlplane_api_direct.rb +1 -1
- data/lib/core/doctor_service.rb +104 -0
- data/lib/core/helpers.rb +10 -0
- data/lib/core/shell.rb +7 -0
- data/lib/core/template_parser.rb +76 -0
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +25 -11
- data/templates/app.yml +0 -5
- metadata +8 -7
- data/googlee2da545df05d92f9.html +0 -1
- data/lib/core/controlplane_api_cli.rb +0 -10
- data/templates/secrets.yml +0 -11
data/lib/command/deploy_image.rb
CHANGED
@@ -10,9 +10,9 @@ module Command
|
|
10
10
|
DESCRIPTION = "Deploys the latest image to app workloads, and runs a release script (optional)"
|
11
11
|
LONG_DESCRIPTION = <<~DESC
|
12
12
|
- Deploys the latest image to app workloads
|
13
|
-
-
|
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
14
|
- The release script is run in the context of `cpl run` with the latest image
|
15
|
-
-
|
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
16
|
DESC
|
17
17
|
|
18
18
|
def call # rubocop:disable Metrics/MethodLength
|
@@ -20,7 +20,7 @@ module Command
|
|
20
20
|
|
21
21
|
deployed_endpoints = {}
|
22
22
|
|
23
|
-
image = latest_image
|
23
|
+
image = cp.latest_image
|
24
24
|
if cp.fetch_image_details(image).nil?
|
25
25
|
raise "Image '#{image}' does not exist in the Docker repository on Control Plane " \
|
26
26
|
"(see https://console.cpln.io/console/org/#{config.org}/repository/#{config.app}). " \
|
@@ -48,24 +48,9 @@ module Command
|
|
48
48
|
|
49
49
|
private
|
50
50
|
|
51
|
-
def run_release_script
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
raise "Can't find release script in '#{release_script_path}'." unless File.exist?(release_script_path)
|
56
|
-
|
57
|
-
progress.puts("Running release script...\n\n")
|
58
|
-
|
59
|
-
release_script = File.read(release_script_path)
|
60
|
-
begin
|
61
|
-
Cpl::Cli.start(["run", "-a", config.app, "--image", "latest", "--", release_script])
|
62
|
-
rescue SystemExit => e
|
63
|
-
progress.puts
|
64
|
-
|
65
|
-
raise "Failed to run release script." if e.status.nonzero?
|
66
|
-
|
67
|
-
progress.puts("Finished running release script.\n\n")
|
68
|
-
end
|
51
|
+
def run_release_script
|
52
|
+
release_script = config[:release_script]
|
53
|
+
run_command_in_latest_image(release_script, title: "release script")
|
69
54
|
end
|
70
55
|
end
|
71
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
|
+
cpl doctor
|
18
|
+
|
19
|
+
# Runs config validation.
|
20
|
+
cpl doctor --validations config
|
21
|
+
|
22
|
+
# Runs templates validation (requires app).
|
23
|
+
cpl 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/latest_image.rb
CHANGED
data/lib/command/no_command.rb
CHANGED
@@ -14,7 +14,7 @@ module Command
|
|
14
14
|
- Runs `cpl copy-image-from-upstream` to copy the latest image from upstream
|
15
15
|
- Runs `cpl deploy-image` to deploy the image
|
16
16
|
- If `.controlplane/controlplane.yml` includes the `release_script`, `cpl deploy-image` will use the `--run-release-phase` option
|
17
|
-
-
|
17
|
+
- If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
18
18
|
DESC
|
19
19
|
|
20
20
|
def call
|
data/lib/command/run.rb
CHANGED
@@ -183,7 +183,7 @@ module Command
|
|
183
183
|
# Override image if specified
|
184
184
|
image = config.options[:image]
|
185
185
|
image_link = if image
|
186
|
-
image = latest_image if image == "latest"
|
186
|
+
image = cp.latest_image if image == "latest"
|
187
187
|
"/org/#{config.org}/image/#{image}"
|
188
188
|
else
|
189
189
|
original_container_spec["image"]
|
@@ -395,7 +395,7 @@ module Command
|
|
395
395
|
script += interactive_runner_script if interactive
|
396
396
|
|
397
397
|
script +=
|
398
|
-
if @log_method == 1
|
398
|
+
if @log_method == 1 || @interactive
|
399
399
|
args_join(config.args)
|
400
400
|
else
|
401
401
|
<<~SCRIPT
|
@@ -442,16 +442,22 @@ module Command
|
|
442
442
|
)
|
443
443
|
end
|
444
444
|
|
445
|
-
def resolve_job_status
|
446
|
-
|
447
|
-
|
448
|
-
|
445
|
+
def resolve_job_status # rubocop:disable Metrics/MethodLength
|
446
|
+
loop do
|
447
|
+
result = cp.fetch_cron_workload(runner_workload, location: location)
|
448
|
+
job_details = result&.dig("items")&.find { |item| item["id"] == job }
|
449
|
+
status = job_details&.dig("status")
|
450
|
+
|
451
|
+
Shell.debug("JOB STATUS", status)
|
449
452
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
453
|
+
case status
|
454
|
+
when "active"
|
455
|
+
sleep 1
|
456
|
+
when "successful"
|
457
|
+
break ExitCode::SUCCESS
|
458
|
+
else
|
459
|
+
break ExitCode::ERROR_DEFAULT
|
460
|
+
end
|
455
461
|
end
|
456
462
|
end
|
457
463
|
|
data/lib/command/setup_app.rb
CHANGED
@@ -5,18 +5,27 @@ module Command
|
|
5
5
|
NAME = "setup-app"
|
6
6
|
OPTIONS = [
|
7
7
|
app_option(required: true),
|
8
|
-
skip_secret_access_binding_option
|
8
|
+
skip_secret_access_binding_option,
|
9
|
+
skip_secrets_setup_option,
|
10
|
+
skip_post_creation_hook_option
|
9
11
|
].freeze
|
10
12
|
DESCRIPTION = "Creates an app and all its workloads"
|
11
13
|
LONG_DESCRIPTION = <<~DESC
|
12
14
|
- Creates an app and all its workloads
|
13
15
|
- Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
|
14
|
-
- This should only be used for temporary apps like review apps, never for persistent apps like production (to update workloads for those, use 'cpl apply-template' instead)
|
15
|
-
-
|
16
|
-
|
16
|
+
- This should only be used for temporary apps like review apps, never for persistent apps like production or staging (to update workloads for those, use 'cpl apply-template' instead)
|
17
|
+
- Configures app to have org-level secrets with default name "{APP_PREFIX}-secrets"
|
18
|
+
using org-level policy with default name "{APP_PREFIX}-secrets-policy" (names can be customized, see docs)
|
19
|
+
- Creates identity for secrets if it does not exist
|
20
|
+
- Use `--skip-secrets-setup` to prevent the automatic setup of secrets,
|
21
|
+
or set it through `skip_secrets_setup` in the `.controlplane/controlplane.yml` file
|
22
|
+
- Runs a post-creation hook after the app is created if `hooks.post_creation` is specified in the `.controlplane/controlplane.yml` file
|
23
|
+
- If the hook exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
24
|
+
- Use `--skip-post-creation-hook` to skip the hook if specified in `controlplane.yml`
|
17
25
|
DESC
|
26
|
+
VALIDATIONS = %w[config templates].freeze
|
18
27
|
|
19
|
-
def call # rubocop:disable Metrics/MethodLength
|
28
|
+
def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
20
29
|
templates = config[:setup_app_templates]
|
21
30
|
|
22
31
|
app = cp.fetch_gvc
|
@@ -26,24 +35,79 @@ module Command
|
|
26
35
|
"or run 'cpl apply-template #{templates.join(' ')} -a #{config.app}'."
|
27
36
|
end
|
28
37
|
|
29
|
-
|
38
|
+
skip_secrets_setup = config.options[:skip_secret_access_binding] ||
|
39
|
+
config.options[:skip_secrets_setup] || config.current[:skip_secrets_setup]
|
30
40
|
|
31
|
-
|
41
|
+
create_secret_and_policy_if_not_exist unless skip_secrets_setup
|
42
|
+
|
43
|
+
args = []
|
44
|
+
args.push("--add-app-identity") unless skip_secrets_setup
|
45
|
+
Cpl::Cli.start(["apply-template", *templates, "-a", config.app, *args])
|
46
|
+
|
47
|
+
bind_identity_to_policy unless skip_secrets_setup
|
48
|
+
run_post_creation_hook unless config.options[:skip_post_creation_hook]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def create_secret_and_policy_if_not_exist
|
54
|
+
create_secret_if_not_exists
|
55
|
+
create_policy_if_not_exists
|
32
56
|
|
33
57
|
progress.puts
|
58
|
+
end
|
34
59
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
60
|
+
def create_secret_if_not_exists
|
61
|
+
if cp.fetch_secret(config.secrets)
|
62
|
+
progress.puts("Secret '#{config.secrets}' already exists. Skipping creation...")
|
63
|
+
else
|
64
|
+
step("Creating secret '#{config.secrets}'") do
|
65
|
+
cp.apply_hash(build_secret_hash)
|
66
|
+
end
|
42
67
|
end
|
68
|
+
end
|
43
69
|
|
44
|
-
|
45
|
-
|
70
|
+
def create_policy_if_not_exists
|
71
|
+
if cp.fetch_policy(config.secrets_policy)
|
72
|
+
progress.puts("Policy '#{config.secrets_policy}' already exists. Skipping creation...")
|
73
|
+
else
|
74
|
+
step("Creating policy '#{config.secrets_policy}'") do
|
75
|
+
cp.apply_hash(build_policy_hash)
|
76
|
+
end
|
46
77
|
end
|
47
78
|
end
|
79
|
+
|
80
|
+
def build_secret_hash
|
81
|
+
{
|
82
|
+
"kind" => "secret",
|
83
|
+
"name" => config.secrets,
|
84
|
+
"type" => "dictionary",
|
85
|
+
"data" => {}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_policy_hash
|
90
|
+
{
|
91
|
+
"kind" => "policy",
|
92
|
+
"name" => config.secrets_policy,
|
93
|
+
"targetKind" => "secret",
|
94
|
+
"targetLinks" => ["//secret/#{config.secrets}"]
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def bind_identity_to_policy
|
99
|
+
progress.puts
|
100
|
+
|
101
|
+
step("Binding identity '#{config.identity}' to policy '#{config.secrets_policy}'") do
|
102
|
+
cp.bind_identity_to_policy(config.identity_link, config.secrets_policy)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def run_post_creation_hook
|
107
|
+
post_creation_hook = config.current.dig(:hooks, :post_creation)
|
108
|
+
return unless post_creation_hook
|
109
|
+
|
110
|
+
run_command_in_latest_image(post_creation_hook, title: "post-creation hook")
|
111
|
+
end
|
48
112
|
end
|
49
113
|
end
|
data/lib/command/test.rb
CHANGED
data/lib/command/version.rb
CHANGED
data/lib/core/config.rb
CHANGED
@@ -18,6 +18,8 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
18
18
|
|
19
19
|
ensure_required_options!
|
20
20
|
|
21
|
+
warn_deprecated_options
|
22
|
+
|
21
23
|
Shell.verbose_mode(options[:verbose])
|
22
24
|
trace_mode = options[:trace]
|
23
25
|
return unless trace_mode
|
@@ -38,10 +40,34 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
38
40
|
current&.fetch(:name)
|
39
41
|
end
|
40
42
|
|
43
|
+
def identity
|
44
|
+
"#{app}-identity"
|
45
|
+
end
|
46
|
+
|
47
|
+
def identity_link
|
48
|
+
"/org/#{org}/gvc/#{app}/identity/#{identity}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def secrets
|
52
|
+
current&.dig(:secrets_name) || "#{app_prefix}-secrets"
|
53
|
+
end
|
54
|
+
|
55
|
+
def secrets_policy
|
56
|
+
current&.dig(:secrets_policy_name) || "#{secrets}-policy"
|
57
|
+
end
|
58
|
+
|
41
59
|
def location
|
42
60
|
@location ||= load_location_from_options || load_location_from_env || load_location_from_file
|
43
61
|
end
|
44
62
|
|
63
|
+
def location_link
|
64
|
+
"/org/#{org}/location/#{location}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def image_link(image)
|
68
|
+
"/org/#{org}/image/#{image}"
|
69
|
+
end
|
70
|
+
|
45
71
|
def domain
|
46
72
|
@domain ||= load_domain_from_options || load_domain_from_file
|
47
73
|
end
|
@@ -84,6 +110,8 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
84
110
|
@apps ||= config[:apps].to_h do |app_name, app_options|
|
85
111
|
ensure_config_app!(app_name, app_options)
|
86
112
|
|
113
|
+
check_deprecated_options(app_options)
|
114
|
+
|
87
115
|
app_options_with_new_keys = app_options.to_h do |key, value|
|
88
116
|
new_key = new_option_keys[key]
|
89
117
|
new_key ? [new_key, value] : [key, value]
|
@@ -96,14 +124,7 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
96
124
|
def current
|
97
125
|
return unless app
|
98
126
|
|
99
|
-
@current ||=
|
100
|
-
app_config = find_app_config(app)
|
101
|
-
ensure_config_app!(app, app_config)
|
102
|
-
|
103
|
-
warn_deprecated_options(app_config)
|
104
|
-
|
105
|
-
app_config
|
106
|
-
end
|
127
|
+
@current ||= find_app_config(app)
|
107
128
|
end
|
108
129
|
|
109
130
|
def app_matches?(app_name1, app_name2, app_options)
|
@@ -275,11 +296,18 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
275
296
|
strip_str_and_validate(current.fetch(:default_domain))
|
276
297
|
end
|
277
298
|
|
278
|
-
def
|
279
|
-
deprecated_option_keys
|
280
|
-
|
299
|
+
def check_deprecated_options(app_options)
|
300
|
+
@deprecated_option_keys ||= {}
|
301
|
+
|
302
|
+
new_option_keys.each do |old_key, new_key|
|
303
|
+
@deprecated_option_keys[old_key] = new_key if app_options.key?(old_key)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def warn_deprecated_options
|
308
|
+
return if !@deprecated_option_keys || @deprecated_option_keys.empty?
|
281
309
|
|
282
|
-
deprecated_option_keys.each do |old_key, new_key|
|
310
|
+
@deprecated_option_keys.each do |old_key, new_key|
|
283
311
|
Shell.warn_deprecated("Option '#{old_key}' is deprecated, " \
|
284
312
|
"please use '#{new_key}' instead (in 'controlplane.yml').")
|
285
313
|
end
|
data/lib/core/controlplane.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
class Controlplane # rubocop:disable Metrics/ClassLength
|
4
4
|
attr_reader :config, :api, :gvc, :org
|
5
5
|
|
6
|
+
NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE"
|
7
|
+
|
6
8
|
def initialize(config)
|
7
9
|
@config = config
|
8
10
|
@api = ControlplaneApi.new
|
@@ -37,6 +39,51 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
37
39
|
|
38
40
|
# image
|
39
41
|
|
42
|
+
def latest_image(a_gvc = gvc, a_org = org, refresh: false)
|
43
|
+
@latest_image ||= {}
|
44
|
+
@latest_image[a_gvc] = nil if refresh
|
45
|
+
@latest_image[a_gvc] ||=
|
46
|
+
begin
|
47
|
+
items = query_images(a_gvc, a_org)["items"]
|
48
|
+
latest_image_from(items, app_name: a_gvc)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def latest_image_next(a_gvc = gvc, a_org = org, commit: nil)
|
53
|
+
commit ||= config.options[:commit]
|
54
|
+
|
55
|
+
@latest_image_next ||= {}
|
56
|
+
@latest_image_next[a_gvc] ||= begin
|
57
|
+
latest_image_name = latest_image(a_gvc, a_org)
|
58
|
+
image = latest_image_name.split(":").first
|
59
|
+
image += ":#{extract_image_number(latest_image_name) + 1}"
|
60
|
+
image += "_#{commit}" if commit
|
61
|
+
image
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def latest_image_from(items, app_name: gvc, name_only: true)
|
66
|
+
matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") }
|
67
|
+
|
68
|
+
# Or special string to indicate no image available
|
69
|
+
if matching_items.empty?
|
70
|
+
name_only ? "#{app_name}:#{NO_IMAGE_AVAILABLE}" : nil
|
71
|
+
else
|
72
|
+
latest_item = matching_items.max_by { |item| extract_image_number(item["name"]) }
|
73
|
+
name_only ? latest_item["name"] : latest_item
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def extract_image_number(image_name)
|
78
|
+
return 0 if image_name.end_with?(NO_IMAGE_AVAILABLE)
|
79
|
+
|
80
|
+
image_name.match(/:(\d+)/)&.captures&.first.to_i
|
81
|
+
end
|
82
|
+
|
83
|
+
def extract_image_commit(image_name)
|
84
|
+
image_name.match(/_(\h+)$/)&.captures&.first
|
85
|
+
end
|
86
|
+
|
40
87
|
def query_images(a_gvc = gvc, a_org = org, partial_gvc_match: nil)
|
41
88
|
partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
|
42
89
|
gvc_op = partial_gvc_match ? "~" : "="
|
@@ -324,6 +371,12 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
324
371
|
api.log_get(org: org, gvc: gvc, workload: workload, replica: replica, from: from, to: to)
|
325
372
|
end
|
326
373
|
|
374
|
+
# secrets
|
375
|
+
|
376
|
+
def fetch_secret(secret)
|
377
|
+
api.fetch_secret(org: org, secret: secret)
|
378
|
+
end
|
379
|
+
|
327
380
|
# identities
|
328
381
|
|
329
382
|
def fetch_identity(identity, a_gvc = gvc)
|
@@ -52,7 +52,7 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
52
52
|
# params << "direction=forward"
|
53
53
|
params = params.map { |k, v| %(#{k}=#{CGI.escape(v)}) }.join("&")
|
54
54
|
|
55
|
-
|
55
|
+
api_json("/logs/org/#{org}/loki/api/v1/query_range?#{params}", method: :get, host: :logs)
|
56
56
|
end
|
57
57
|
|
58
58
|
def query_workloads(org:, gvc:, workload:, gvc_op_type:, workload_op_type:) # rubocop:disable Metrics/MethodLength
|
@@ -116,6 +116,14 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
116
116
|
api_json("/org/#{org}/domain/#{domain}", method: :patch, body: data)
|
117
117
|
end
|
118
118
|
|
119
|
+
def fetch_secret(org:, secret:)
|
120
|
+
api_json("/org/#{org}/secret/#{secret}", method: :get)
|
121
|
+
end
|
122
|
+
|
123
|
+
def delete_secret(org:, secret:)
|
124
|
+
api_json("/org/#{org}/secret/#{secret}", method: :delete)
|
125
|
+
end
|
126
|
+
|
119
127
|
def fetch_identity(org:, gvc:, identity:)
|
120
128
|
api_json("/org/#{org}/gvc/#{gvc}/identity/#{identity}", method: :get)
|
121
129
|
end
|
@@ -124,6 +132,10 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
124
132
|
api_json("/org/#{org}/policy/#{policy}", method: :get)
|
125
133
|
end
|
126
134
|
|
135
|
+
def delete_policy(org:, policy:)
|
136
|
+
api_json("/org/#{org}/policy/#{policy}", method: :delete)
|
137
|
+
end
|
138
|
+
|
127
139
|
private
|
128
140
|
|
129
141
|
def fetch_query_pages(result)
|
@@ -152,13 +164,7 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
152
164
|
result
|
153
165
|
end
|
154
166
|
|
155
|
-
# switch between cpln rest and api
|
156
167
|
def api_json(...)
|
157
168
|
ControlplaneApiDirect.new.call(...)
|
158
169
|
end
|
159
|
-
|
160
|
-
# only for api (where not impelemented in cpln rest)
|
161
|
-
def api_json_direct(...)
|
162
|
-
ControlplaneApiDirect.new.call(...)
|
163
|
-
end
|
164
170
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ValidationError < StandardError; end
|
4
|
+
|
5
|
+
class DoctorService
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_validations(validations, silent_if_passing: false) # rubocop:disable Metrics/MethodLength
|
13
|
+
@any_failed_validation = false
|
14
|
+
|
15
|
+
validations.each do |validation|
|
16
|
+
case validation
|
17
|
+
when "config"
|
18
|
+
validate_config
|
19
|
+
when "templates"
|
20
|
+
validate_templates
|
21
|
+
else
|
22
|
+
raise ValidationError, Shell.color("ERROR: Invalid validation '#{validation}'.", :red)
|
23
|
+
end
|
24
|
+
|
25
|
+
progress.puts("#{Shell.color('[PASS]', :green)} #{validation}") unless silent_if_passing
|
26
|
+
rescue ValidationError => e
|
27
|
+
@any_failed_validation = true
|
28
|
+
|
29
|
+
progress.puts("#{Shell.color('[FAIL]', :red)} #{validation}\n\n#{e.message}\n\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
exit(ExitCode::ERROR_DEFAULT) if @any_failed_validation
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_config
|
36
|
+
check_for_app_names_contained_in_others
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_templates
|
40
|
+
@template_parser = TemplateParser.new(config)
|
41
|
+
filenames = Dir.glob("#{@template_parser.template_dir}/*.yml")
|
42
|
+
templates = @template_parser.parse(filenames)
|
43
|
+
|
44
|
+
check_for_duplicate_templates(templates)
|
45
|
+
warn_deprecated_template_variables
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def check_for_app_names_contained_in_others
|
51
|
+
app_names_contained_in_others = find_app_names_contained_in_others
|
52
|
+
return if app_names_contained_in_others.empty?
|
53
|
+
|
54
|
+
message = "App names contained in others found below. Please ensure that app names are unique."
|
55
|
+
list = app_names_contained_in_others
|
56
|
+
.map { |app_prefix, app_name| " - '#{app_prefix}' is a prefix of '#{app_name}'" }
|
57
|
+
.join("\n")
|
58
|
+
raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_app_names_contained_in_others # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
62
|
+
app_names = config.apps.keys.map(&:to_s).sort
|
63
|
+
app_prefixes = config.apps
|
64
|
+
.select { |_, app_options| app_options[:match_if_app_name_starts_with] }
|
65
|
+
.keys
|
66
|
+
.map(&:to_s)
|
67
|
+
.sort
|
68
|
+
app_prefixes.each_with_object([]) do |app_prefix, app_names_contained_in_others|
|
69
|
+
app_names.each do |app_name|
|
70
|
+
if app_prefix != app_name && app_name.start_with?(app_prefix)
|
71
|
+
app_names_contained_in_others.push([app_prefix, app_name])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def check_for_duplicate_templates(templates)
|
78
|
+
grouped_templates = templates.group_by { |template| [template["kind"], template["name"]] }
|
79
|
+
duplicate_templates = grouped_templates.select { |_, group| group.size > 1 }
|
80
|
+
return if duplicate_templates.empty?
|
81
|
+
|
82
|
+
message = "Duplicate templates found with the kind/names below. Please ensure that templates are unique."
|
83
|
+
list = duplicate_templates
|
84
|
+
.map { |(kind, name), _| " - kind: #{kind}, name: #{name}" }
|
85
|
+
.join("\n")
|
86
|
+
raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def warn_deprecated_template_variables
|
90
|
+
deprecated_variables = @template_parser.deprecated_variables
|
91
|
+
return if deprecated_variables.empty?
|
92
|
+
|
93
|
+
message = "Please replace these variables in the templates, " \
|
94
|
+
"as support for them will be removed in a future major version bump:"
|
95
|
+
list = deprecated_variables
|
96
|
+
.map { |old_key, new_key| " - #{old_key} -> #{new_key}" }
|
97
|
+
.join("\n")
|
98
|
+
progress.puts("\n#{Shell.color("DEPRECATED: #{message}", :yellow)}\n#{list}\n\n")
|
99
|
+
end
|
100
|
+
|
101
|
+
def progress
|
102
|
+
$stderr
|
103
|
+
end
|
104
|
+
end
|
data/lib/core/helpers.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require "securerandom"
|
4
4
|
|
5
5
|
module Helpers
|
6
|
+
module_function
|
7
|
+
|
6
8
|
def strip_str_and_validate(str)
|
7
9
|
return str if str.nil?
|
8
10
|
|
@@ -13,4 +15,12 @@ module Helpers
|
|
13
15
|
def random_four_digits
|
14
16
|
SecureRandom.random_number(1000..9999)
|
15
17
|
end
|
18
|
+
|
19
|
+
def normalize_command_name(name)
|
20
|
+
name.to_s.tr("_", "-")
|
21
|
+
end
|
22
|
+
|
23
|
+
def normalize_option_name(name)
|
24
|
+
"--#{name.to_s.tr('_', '-')}"
|
25
|
+
end
|
16
26
|
end
|