cpl 2.0.2 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +39 -1
- data/Gemfile.lock +3 -3
- data/README.md +24 -3
- data/docs/commands.md +37 -8
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/tips.md +1 -40
- data/examples/controlplane.yml +24 -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/ps_wait.rb +7 -3
- data/lib/command/run.rb +87 -34
- 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 +58 -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/template_parser.rb +76 -0
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +24 -6
- data/templates/app.yml +0 -5
- metadata +6 -4
- data/lib/core/controlplane_api_cli.rb +0 -10
- data/templates/secrets.yml +0 -11
data/lib/command/run.rb
CHANGED
@@ -36,12 +36,17 @@ module Command
|
|
36
36
|
- - log async fetching for non-interactive mode
|
37
37
|
- The Dockerfile entrypoint is used as the command by default, which assumes `exec "${@}"` to be present,
|
38
38
|
and the args ["bash", "-c", cmd_to_run] are passed
|
39
|
-
- The entrypoint can be
|
39
|
+
- The entrypoint can be overridden through `--entrypoint`, which must be a single command or a script path that exists in the container,
|
40
40
|
and the args ["bash", "-c", cmd_to_run] are passed,
|
41
41
|
unless the entrypoint is `bash`, in which case the args ["-c", cmd_to_run] are passed
|
42
42
|
- Providing `--entrypoint none` sets the entrypoint to `bash` by default
|
43
43
|
- If `fix_terminal_size` is `true` in the `.controlplane/controlplane.yml` file,
|
44
|
-
the remote terminal size will be fixed to match the local terminal size (may also be
|
44
|
+
the remote terminal size will be fixed to match the local terminal size (may also be overridden through `--terminal-size`)
|
45
|
+
- By default, all jobs use a CPU size of 1 (1 core) and a memory size of 2Gi (2 gibibytes)
|
46
|
+
(can be configured through `runner_job_default_cpu` and `runner_job_default_memory` in `controlplane.yml`,
|
47
|
+
and also overridden per job through `--cpu` and `--memory`)
|
48
|
+
- By default, the job is stopped if it takes longer than 6 hours to finish
|
49
|
+
(can be configured though `runner_job_timeout` in `controlplane.yml`)
|
45
50
|
DESC
|
46
51
|
EXAMPLES = <<~EX
|
47
52
|
```sh
|
@@ -84,12 +89,17 @@ module Command
|
|
84
89
|
```
|
85
90
|
EX
|
86
91
|
|
92
|
+
DEFAULT_JOB_CPU = "1"
|
93
|
+
DEFAULT_JOB_MEMORY = "2Gi"
|
94
|
+
DEFAULT_JOB_TIMEOUT = 21_600 # 6 hours
|
95
|
+
DEFAULT_JOB_HISTORY_LIMIT = 10
|
87
96
|
MAGIC_END = "---cpl run command finished---"
|
88
97
|
|
89
98
|
attr_reader :interactive, :detached, :location, :original_workload, :runner_workload,
|
99
|
+
:default_image, :default_cpu, :default_memory, :job_timeout, :job_history_limit,
|
90
100
|
:container, :expected_deployed_version, :job, :replica, :command
|
91
101
|
|
92
|
-
def call # rubocop:disable Metrics/MethodLength
|
102
|
+
def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
93
103
|
@interactive = config.options[:interactive] || interactive_command?
|
94
104
|
@detached = config.options[:detached]
|
95
105
|
@log_method = config.options[:log_method]
|
@@ -97,6 +107,11 @@ module Command
|
|
97
107
|
@location = config.location
|
98
108
|
@original_workload = config.options[:workload] || config[:one_off_workload]
|
99
109
|
@runner_workload = "#{original_workload}-runner"
|
110
|
+
@default_image = "#{config.app}:#{Controlplane::NO_IMAGE_AVAILABLE}"
|
111
|
+
@default_cpu = config.current[:runner_job_default_cpu] || DEFAULT_JOB_CPU
|
112
|
+
@default_memory = config.current[:runner_job_default_memory] || DEFAULT_JOB_MEMORY
|
113
|
+
@job_timeout = config.current[:runner_job_timeout] || DEFAULT_JOB_TIMEOUT
|
114
|
+
@job_history_limit = DEFAULT_JOB_HISTORY_LIMIT
|
100
115
|
|
101
116
|
unless interactive
|
102
117
|
@internal_sigint = false
|
@@ -110,12 +125,10 @@ module Command
|
|
110
125
|
end
|
111
126
|
end
|
112
127
|
|
113
|
-
if cp.fetch_workload(runner_workload).nil?
|
114
|
-
|
115
|
-
wait_for_runner_workload_create
|
116
|
-
end
|
128
|
+
create_runner_workload if cp.fetch_workload(runner_workload).nil?
|
129
|
+
wait_for_runner_workload_deploy
|
117
130
|
update_runner_workload
|
118
|
-
wait_for_runner_workload_update
|
131
|
+
wait_for_runner_workload_update if expected_deployed_version
|
119
132
|
|
120
133
|
start_job
|
121
134
|
wait_for_replica_for_job
|
@@ -154,6 +167,11 @@ module Command
|
|
154
167
|
container_spec.delete("livenessProbe")
|
155
168
|
container_spec.delete("readinessProbe")
|
156
169
|
|
170
|
+
# Set image, CPU, and memory to default values
|
171
|
+
container_spec["image"] = default_image
|
172
|
+
container_spec["cpu"] = default_cpu
|
173
|
+
container_spec["memory"] = default_memory
|
174
|
+
|
157
175
|
# Ensure cron workload won't run per schedule
|
158
176
|
spec["defaultOptions"]["suspend"] = true
|
159
177
|
|
@@ -163,9 +181,14 @@ module Command
|
|
163
181
|
|
164
182
|
# Set cron job props
|
165
183
|
spec["type"] = "cron"
|
184
|
+
spec["job"] = {
|
185
|
+
# Next job set to run on January 1st, 2029
|
186
|
+
"schedule" => "0 0 1 1 1",
|
166
187
|
|
167
|
-
|
168
|
-
|
188
|
+
"restartPolicy" => "Never",
|
189
|
+
"activeDeadlineSeconds" => job_timeout,
|
190
|
+
"historyLimit" => job_history_limit
|
191
|
+
}
|
169
192
|
|
170
193
|
# Create runner workload
|
171
194
|
cp.apply_hash("kind" => "workload", "name" => runner_workload, "spec" => spec)
|
@@ -173,46 +196,60 @@ module Command
|
|
173
196
|
end
|
174
197
|
|
175
198
|
def update_runner_workload # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
176
|
-
|
177
|
-
|
178
|
-
should_update = false
|
199
|
+
should_update = false
|
200
|
+
spec = nil
|
179
201
|
|
202
|
+
step("Checking if runner workload '#{runner_workload}' needs to be updated") do # rubocop:disable Metrics/BlockLength
|
180
203
|
_, original_container_spec = base_workload_specs(original_workload)
|
181
204
|
spec, container_spec = base_workload_specs(runner_workload)
|
182
205
|
|
183
|
-
#
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
if container_spec["image"] !=
|
192
|
-
container_spec["image"] =
|
206
|
+
# Keep ENV synced between original and runner workloads
|
207
|
+
original_env_str = original_container_spec["env"]&.sort_by { |env| env["name"] }.to_s
|
208
|
+
env_str = container_spec["env"]&.sort_by { |env| env["name"] }.to_s
|
209
|
+
if original_env_str != env_str
|
210
|
+
container_spec["env"] = original_container_spec["env"]
|
211
|
+
should_update = true
|
212
|
+
end
|
213
|
+
|
214
|
+
if container_spec["image"] != default_image
|
215
|
+
container_spec["image"] = default_image
|
193
216
|
should_update = true
|
194
217
|
end
|
195
218
|
|
196
|
-
|
197
|
-
|
198
|
-
container_spec["cpu"] = config.options[:cpu]
|
219
|
+
if container_spec["cpu"] != default_cpu
|
220
|
+
container_spec["cpu"] = default_cpu
|
199
221
|
should_update = true
|
200
222
|
end
|
201
|
-
|
202
|
-
|
223
|
+
|
224
|
+
if container_spec["memory"] != default_memory
|
225
|
+
container_spec["memory"] = default_memory
|
203
226
|
should_update = true
|
204
227
|
end
|
205
228
|
|
206
|
-
|
229
|
+
if spec["job"]["activeDeadlineSeconds"] != job_timeout
|
230
|
+
spec["job"]["activeDeadlineSeconds"] = job_timeout
|
231
|
+
should_update = true
|
232
|
+
end
|
207
233
|
|
234
|
+
if spec["job"]["historyLimit"] != job_history_limit
|
235
|
+
spec["job"]["historyLimit"] = job_history_limit
|
236
|
+
should_update = true
|
237
|
+
end
|
238
|
+
|
239
|
+
true
|
240
|
+
end
|
241
|
+
|
242
|
+
return unless should_update
|
243
|
+
|
244
|
+
step("Updating runner workload '#{runner_workload}'") do
|
208
245
|
# Update runner workload
|
209
|
-
@expected_deployed_version
|
246
|
+
@expected_deployed_version = cp.cron_workload_deployed_version(runner_workload) + 1
|
210
247
|
cp.apply_hash("kind" => "workload", "name" => runner_workload, "spec" => spec)
|
211
248
|
end
|
212
249
|
end
|
213
250
|
|
214
|
-
def
|
215
|
-
step("Waiting for runner workload '#{runner_workload}' to be
|
251
|
+
def wait_for_runner_workload_deploy
|
252
|
+
step("Waiting for runner workload '#{runner_workload}' to be deployed", retry_on_failure: true) do
|
216
253
|
!cp.cron_workload_deployed_version(runner_workload).nil?
|
217
254
|
end
|
218
255
|
end
|
@@ -302,12 +339,14 @@ module Command
|
|
302
339
|
def base_workload_specs(workload)
|
303
340
|
spec = cp.fetch_workload!(workload).fetch("spec")
|
304
341
|
container_spec = spec["containers"].detect { _1["name"] == original_workload } || spec["containers"].first
|
305
|
-
@container = container_spec["name"]
|
306
342
|
|
307
343
|
[spec, container_spec]
|
308
344
|
end
|
309
345
|
|
310
346
|
def build_job_start_yaml # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
347
|
+
_, original_container_spec = base_workload_specs(original_workload)
|
348
|
+
@container = original_container_spec["name"]
|
349
|
+
|
311
350
|
job_start_hash = { "name" => container }
|
312
351
|
|
313
352
|
if config.options[:use_local_token]
|
@@ -335,6 +374,18 @@ module Command
|
|
335
374
|
job_start_hash["args"].push('eval "$CPL_RUNNER_SCRIPT"')
|
336
375
|
end
|
337
376
|
|
377
|
+
image = config.options[:image]
|
378
|
+
image_link = if image
|
379
|
+
image = cp.latest_image if image == "latest"
|
380
|
+
"/org/#{config.org}/image/#{image}"
|
381
|
+
else
|
382
|
+
original_container_spec["image"]
|
383
|
+
end
|
384
|
+
|
385
|
+
job_start_hash["image"] = image_link
|
386
|
+
job_start_hash["cpu"] = config.options[:cpu] if config.options[:cpu]
|
387
|
+
job_start_hash["memory"] = config.options[:memory] if config.options[:memory]
|
388
|
+
|
338
389
|
job_start_hash.to_yaml
|
339
390
|
end
|
340
391
|
|
@@ -434,6 +485,8 @@ module Command
|
|
434
485
|
end
|
435
486
|
|
436
487
|
def print_detached_commands
|
488
|
+
return unless replica
|
489
|
+
|
437
490
|
app_workload_replica_config = app_workload_replica_args.join(" ")
|
438
491
|
progress.puts(
|
439
492
|
"\n\n" \
|
@@ -451,7 +504,7 @@ module Command
|
|
451
504
|
Shell.debug("JOB STATUS", status)
|
452
505
|
|
453
506
|
case status
|
454
|
-
when "active"
|
507
|
+
when "active", "pending"
|
455
508
|
sleep 1
|
456
509
|
when "successful"
|
457
510
|
break ExitCode::SUCCESS
|
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 ? "~" : "="
|
@@ -209,6 +256,11 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
209
256
|
api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
|
210
257
|
end
|
211
258
|
|
259
|
+
def workload_suspended?(workload)
|
260
|
+
details = fetch_workload!(workload)
|
261
|
+
details["spec"]["defaultOptions"]["suspend"]
|
262
|
+
end
|
263
|
+
|
212
264
|
def workload_force_redeployment(workload)
|
213
265
|
cmd = "cpln workload force-redeployment #{workload} #{gvc_org}"
|
214
266
|
perform!(cmd)
|
@@ -324,6 +376,12 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
324
376
|
api.log_get(org: org, gvc: gvc, workload: workload, replica: replica, from: from, to: to)
|
325
377
|
end
|
326
378
|
|
379
|
+
# secrets
|
380
|
+
|
381
|
+
def fetch_secret(secret)
|
382
|
+
api.fetch_secret(org: org, secret: secret)
|
383
|
+
end
|
384
|
+
|
327
385
|
# identities
|
328
386
|
|
329
387
|
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
|