cpl 1.2.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/check_cpln_links.yml +19 -0
- data/.overcommit.yml +3 -0
- data/CHANGELOG.md +43 -1
- data/Gemfile.lock +9 -5
- data/README.md +43 -7
- data/cpl.gemspec +1 -0
- data/docs/commands.md +30 -23
- data/docs/dns.md +9 -0
- data/docs/tips.md +11 -1
- data/examples/controlplane.yml +22 -1
- data/lib/command/apply_template.rb +58 -10
- data/lib/command/base.rb +79 -2
- data/lib/command/build_image.rb +5 -1
- data/lib/command/cleanup_stale_apps.rb +0 -2
- data/lib/command/copy_image_from_upstream.rb +5 -4
- data/lib/command/deploy_image.rb +20 -2
- data/lib/command/info.rb +11 -26
- data/lib/command/maintenance.rb +8 -4
- data/lib/command/maintenance_off.rb +8 -4
- data/lib/command/maintenance_on.rb +8 -4
- data/lib/command/promote_app_from_upstream.rb +5 -25
- data/lib/command/run.rb +20 -22
- data/lib/command/run_detached.rb +38 -30
- data/lib/command/setup_app.rb +19 -2
- data/lib/core/config.rb +36 -14
- data/lib/core/controlplane.rb +34 -7
- data/lib/core/controlplane_api.rb +12 -0
- data/lib/core/controlplane_api_direct.rb +33 -5
- data/lib/core/helpers.rb +6 -0
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +6 -1
- data/lib/generator_templates/controlplane.yml +5 -0
- data/lib/generator_templates/templates/gvc.yml +4 -4
- data/lib/generator_templates/templates/postgres.yml +1 -1
- data/lib/generator_templates/templates/rails.yml +1 -1
- data/script/check_cpln_links +45 -0
- data/templates/daily-task.yml +3 -2
- data/templates/gvc.yml +5 -5
- data/templates/identity.yml +2 -1
- data/templates/rails.yml +3 -2
- data/templates/secrets-policy.yml +4 -0
- data/templates/secrets.yml +3 -0
- data/templates/sidekiq.yml +3 -2
- metadata +21 -2
data/lib/command/run_detached.rb
CHANGED
@@ -10,7 +10,8 @@ module Command
|
|
10
10
|
image_option,
|
11
11
|
workload_option,
|
12
12
|
location_option,
|
13
|
-
use_local_token_option
|
13
|
+
use_local_token_option,
|
14
|
+
clean_on_failure_option
|
14
15
|
].freeze
|
15
16
|
DESCRIPTION = "Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)"
|
16
17
|
LONG_DESCRIPTION = <<~DESC
|
@@ -19,50 +20,46 @@ module Command
|
|
19
20
|
- Implemented with only async execution methods, more suitable for production tasks
|
20
21
|
- Has alternative log fetch implementation with only JSON-polling and no WebSockets
|
21
22
|
- Less responsive but more stable, useful for CI tasks
|
23
|
+
- Deletes the workload whenever finished with success
|
24
|
+
- Deletes the workload whenever finished with failure by default
|
25
|
+
- Use `--no-clean-on-failure` to disable cleanup to help with debugging failed runs
|
22
26
|
DESC
|
23
27
|
EXAMPLES = <<~EX
|
24
28
|
```sh
|
25
29
|
cpl run:detached rails db:prepare -a $APP_NAME
|
26
30
|
|
27
31
|
# Need to quote COMMAND if setting ENV value or passing args.
|
28
|
-
cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
|
29
|
-
|
30
|
-
# COMMAND may also be passed at the end.
|
31
32
|
cpl run:detached -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
|
32
33
|
|
33
34
|
# Uses a different image (which may not be promoted yet).
|
34
|
-
cpl run:detached
|
35
|
-
cpl run:detached
|
35
|
+
cpl run:detached -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
|
36
|
+
cpl run:detached -a $APP_NAME --image latest -- rails db:migrate # Latest sequential image
|
36
37
|
|
37
38
|
# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
|
38
|
-
cpl run:detached
|
39
|
+
cpl run:detached -a $APP_NAME -w other-workload -- rails db:migrate:status
|
39
40
|
|
40
41
|
# Overrides remote CPLN_TOKEN env variable with local token.
|
41
42
|
# Useful when superuser rights are needed in remote container.
|
42
|
-
cpl run:detached
|
43
|
+
cpl run:detached -a $APP_NAME --use-local-token -- rails db:migrate:status
|
43
44
|
```
|
44
45
|
EX
|
45
46
|
|
46
47
|
WORKLOAD_SLEEP_CHECK = 2
|
47
48
|
|
48
|
-
attr_reader :location, :
|
49
|
+
attr_reader :location, :workload_to_clone, :workload_clone, :container
|
49
50
|
|
50
|
-
def call
|
51
|
+
def call
|
51
52
|
@location = config.location
|
52
|
-
@
|
53
|
-
@
|
53
|
+
@workload_to_clone = config.options["workload"] || config[:one_off_workload]
|
54
|
+
@workload_clone = "#{workload_to_clone}-runner-#{random_four_digits}"
|
54
55
|
|
55
|
-
step("Cloning workload '#{
|
56
|
+
step("Cloning workload '#{workload_to_clone}' on app '#{config.options[:app]}' to '#{workload_clone}'") do
|
56
57
|
clone_workload
|
57
58
|
end
|
58
59
|
|
59
|
-
wait_for_workload(
|
60
|
+
wait_for_workload(workload_clone)
|
60
61
|
show_logs_waiting
|
61
62
|
ensure
|
62
|
-
if cp.fetch_workload(one_off)
|
63
|
-
progress.puts
|
64
|
-
ensure_workload_deleted(one_off)
|
65
|
-
end
|
66
63
|
exit(1) if @crashed
|
67
64
|
end
|
68
65
|
|
@@ -70,8 +67,8 @@ module Command
|
|
70
67
|
|
71
68
|
def clone_workload # rubocop:disable Metrics/MethodLength
|
72
69
|
# Get base specs of workload
|
73
|
-
spec = cp.fetch_workload!(
|
74
|
-
container_spec = spec["containers"].detect { _1["name"] ==
|
70
|
+
spec = cp.fetch_workload!(workload_to_clone).fetch("spec")
|
71
|
+
container_spec = spec["containers"].detect { _1["name"] == workload_to_clone } || spec["containers"].first
|
75
72
|
@container = container_spec["name"]
|
76
73
|
|
77
74
|
# remove other containers if any
|
@@ -101,11 +98,12 @@ module Command
|
|
101
98
|
container_spec.delete("ports")
|
102
99
|
|
103
100
|
container_spec["env"] ||= []
|
104
|
-
container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN",
|
101
|
+
container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN",
|
102
|
+
"value" => ControlplaneApiDirect.new.api_token[:token] }
|
105
103
|
container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
|
106
104
|
|
107
105
|
# Create workload clone
|
108
|
-
cp.apply_hash("kind" => "workload", "name" =>
|
106
|
+
cp.apply_hash("kind" => "workload", "name" => workload_clone, "spec" => spec)
|
109
107
|
end
|
110
108
|
|
111
109
|
def runner_script # rubocop:disable Metrics/MethodLength
|
@@ -119,12 +117,20 @@ module Command
|
|
119
117
|
end
|
120
118
|
|
121
119
|
script += <<~SHELL
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
120
|
+
crashed=0
|
121
|
+
if ! eval "#{args_join(config.args)}"; then
|
122
|
+
crashed=1
|
123
|
+
echo "----- CRASHED -----"
|
124
|
+
fi
|
125
|
+
clean_on_failure=#{config.options[:clean_on_failure] ? 1 : 0}
|
126
|
+
if [ $crashed -eq 0 ] || [ $clean_on_failure -eq 1 ]; then
|
127
|
+
echo "-- FINISHED RUNNER SCRIPT, DELETING WORKLOAD --"
|
128
|
+
sleep 30 # grace time for logs propagation
|
129
|
+
curl ${CPLN_ENDPOINT}${CPLN_WORKLOAD} -H "Authorization: ${CONTROLPLANE_TOKEN}" -X DELETE -s -o /dev/null
|
130
|
+
while true; do sleep 1; done # wait for SIGTERM
|
131
|
+
else
|
132
|
+
echo "-- FINISHED RUNNER SCRIPT --"
|
133
|
+
fi
|
128
134
|
SHELL
|
129
135
|
|
130
136
|
script
|
@@ -133,7 +139,8 @@ module Command
|
|
133
139
|
def show_logs_waiting # rubocop:disable Metrics/MethodLength
|
134
140
|
progress.puts("Scheduled, fetching logs (it's a cron job, so it may take up to a minute to start)...\n\n")
|
135
141
|
begin
|
136
|
-
|
142
|
+
@finished = false
|
143
|
+
while cp.fetch_workload(workload_clone) && !@finished
|
137
144
|
sleep(WORKLOAD_SLEEP_CHECK)
|
138
145
|
print_uniq_logs
|
139
146
|
end
|
@@ -151,6 +158,7 @@ module Command
|
|
151
158
|
|
152
159
|
(entries - @printed_log_entries).sort.each do |(_ts, val)|
|
153
160
|
@crashed = true if val.match?(/^----- CRASHED -----$/)
|
161
|
+
@finished = true if val.match?(/^-- FINISHED RUNNER SCRIPT(, DELETING WORKLOAD)? --$/)
|
154
162
|
puts val
|
155
163
|
end
|
156
164
|
|
@@ -158,7 +166,7 @@ module Command
|
|
158
166
|
end
|
159
167
|
|
160
168
|
def normalized_log_entries(from:, to:)
|
161
|
-
log = cp.log_get(workload:
|
169
|
+
log = cp.log_get(workload: workload_clone, from: from, to: to)
|
162
170
|
|
163
171
|
log["data"]["result"]
|
164
172
|
.each_with_object([]) { |obj, result| result.concat(obj["values"]) }
|
data/lib/command/setup_app.rb
CHANGED
@@ -4,16 +4,19 @@ module Command
|
|
4
4
|
class SetupApp < Base
|
5
5
|
NAME = "setup-app"
|
6
6
|
OPTIONS = [
|
7
|
-
app_option(required: true)
|
7
|
+
app_option(required: true),
|
8
|
+
skip_secret_access_binding_option
|
8
9
|
].freeze
|
9
10
|
DESCRIPTION = "Creates an app and all its workloads"
|
10
11
|
LONG_DESCRIPTION = <<~DESC
|
11
12
|
- Creates an app and all its workloads
|
12
13
|
- Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
|
13
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
|
+
- Automatically binds the app to the secrets policy, as long as both the identity and the policy exist
|
16
|
+
- Use `--skip-secret-access-binding` to prevent the automatic bind
|
14
17
|
DESC
|
15
18
|
|
16
|
-
def call
|
19
|
+
def call # rubocop:disable Metrics/MethodLength
|
17
20
|
templates = config[:setup_app_templates]
|
18
21
|
|
19
22
|
app = cp.fetch_gvc
|
@@ -24,6 +27,20 @@ module Command
|
|
24
27
|
end
|
25
28
|
|
26
29
|
Cpl::Cli.start(["apply-template", *templates, "-a", config.app])
|
30
|
+
|
31
|
+
return if config.options[:skip_secret_access_binding]
|
32
|
+
|
33
|
+
progress.puts
|
34
|
+
|
35
|
+
if cp.fetch_identity(app_identity).nil? || cp.fetch_policy(app_secrets_policy).nil?
|
36
|
+
raise "Can't bind identity to policy: identity '#{app_identity}' or " \
|
37
|
+
"policy '#{app_secrets_policy}' doesn't exist. " \
|
38
|
+
"Please create them or use `--skip-secret-access-binding` to ignore this message."
|
39
|
+
end
|
40
|
+
|
41
|
+
step("Binding identity to policy") do
|
42
|
+
cp.bind_identity_to_policy(app_identity_link, app_secrets_policy)
|
43
|
+
end
|
27
44
|
end
|
28
45
|
end
|
29
46
|
end
|
data/lib/core/config.rb
CHANGED
@@ -34,10 +34,18 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
34
34
|
@app ||= load_app_from_options || load_app_from_env
|
35
35
|
end
|
36
36
|
|
37
|
+
def app_prefix
|
38
|
+
current&.fetch(:name)
|
39
|
+
end
|
40
|
+
|
37
41
|
def location
|
38
42
|
@location ||= load_location_from_options || load_location_from_env || load_location_from_file
|
39
43
|
end
|
40
44
|
|
45
|
+
def domain
|
46
|
+
@domain ||= load_domain_from_options || load_domain_from_file
|
47
|
+
end
|
48
|
+
|
41
49
|
def [](key)
|
42
50
|
ensure_current_config!
|
43
51
|
|
@@ -98,6 +106,24 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
98
106
|
end
|
99
107
|
end
|
100
108
|
|
109
|
+
def app_matches?(app_name1, app_name2, app_options)
|
110
|
+
app_name1 && app_name2 &&
|
111
|
+
(app_name1.to_s == app_name2.to_s ||
|
112
|
+
(app_options[:match_if_app_name_starts_with] && app_name1.to_s.start_with?(app_name2.to_s))
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_app_config(app_name1)
|
117
|
+
@app_configs ||= {}
|
118
|
+
|
119
|
+
@app_configs[app_name1] ||= apps.filter_map do |app_name2, app_config|
|
120
|
+
next unless app_matches?(app_name1, app_name2, app_config)
|
121
|
+
|
122
|
+
app_config[:name] = app_name2
|
123
|
+
app_config
|
124
|
+
end&.last
|
125
|
+
end
|
126
|
+
|
101
127
|
private
|
102
128
|
|
103
129
|
def ensure_current_config!
|
@@ -116,20 +142,6 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
116
142
|
raise "Can't find config for app '#{app_name}' in 'controlplane.yml'." unless app_options
|
117
143
|
end
|
118
144
|
|
119
|
-
def app_matches?(app_name1, app_name2, app_options)
|
120
|
-
app_name1 && app_name2 &&
|
121
|
-
(app_name1.to_s == app_name2.to_s ||
|
122
|
-
(app_options[:match_if_app_name_starts_with] && app_name1.to_s.start_with?(app_name2.to_s))
|
123
|
-
)
|
124
|
-
end
|
125
|
-
|
126
|
-
def find_app_config(app_name1)
|
127
|
-
@app_configs ||= {}
|
128
|
-
@app_configs[app_name1] ||= apps.find do |app_name2, app_config|
|
129
|
-
app_matches?(app_name1, app_name2, app_config)
|
130
|
-
end&.last
|
131
|
-
end
|
132
|
-
|
133
145
|
def ensure_app!
|
134
146
|
return if app
|
135
147
|
|
@@ -253,6 +265,16 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
253
265
|
strip_str_and_validate(current.fetch(:default_location))
|
254
266
|
end
|
255
267
|
|
268
|
+
def load_domain_from_options
|
269
|
+
strip_str_and_validate(options[:domain])
|
270
|
+
end
|
271
|
+
|
272
|
+
def load_domain_from_file
|
273
|
+
return unless current&.key?(:default_domain)
|
274
|
+
|
275
|
+
strip_str_and_validate(current.fetch(:default_domain))
|
276
|
+
end
|
277
|
+
|
256
278
|
def warn_deprecated_options(app_options)
|
257
279
|
deprecated_option_keys = new_option_keys.select { |old_key| app_options.key?(old_key) }
|
258
280
|
return if deprecated_option_keys.empty?
|
data/lib/core/controlplane.rb
CHANGED
@@ -44,12 +44,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
44
44
|
api.query_images(org: a_org, gvc: a_gvc, gvc_op_type: gvc_op)
|
45
45
|
end
|
46
46
|
|
47
|
-
def image_build(image, dockerfile:, build_args: [], push: true)
|
47
|
+
def image_build(image, dockerfile:, docker_args: [], build_args: [], push: true)
|
48
48
|
# https://docs.controlplane.com/guides/push-image#step-2
|
49
49
|
# Might need to use `docker buildx build` if compatiblitity issues arise
|
50
50
|
cmd = "docker build --platform=linux/amd64 -t #{image} -f #{dockerfile}"
|
51
51
|
cmd += " --progress=plain" if ControlplaneApiDirect.trace
|
52
52
|
|
53
|
+
cmd += " #{docker_args.join(' ')}" if docker_args.any?
|
53
54
|
build_args.each { |build_arg| cmd += " --build-arg #{build_arg}" }
|
54
55
|
cmd += " #{config.app_dir}"
|
55
56
|
perform!(cmd)
|
@@ -264,13 +265,21 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
264
265
|
route = find_domain_route(domain_data)
|
265
266
|
next false if route.nil?
|
266
267
|
|
267
|
-
workloads.any? { |workload| route["workloadLink"].
|
268
|
+
workloads.any? { |workload| route["workloadLink"].match?(%r{/org/#{org}/gvc/#{gvc}/workload/#{workload}}) }
|
268
269
|
end
|
269
270
|
end
|
270
271
|
|
271
|
-
def
|
272
|
+
def fetch_domain(domain)
|
273
|
+
domain_data = api.fetch_domain(org: org, domain: domain)
|
274
|
+
route = find_domain_route(domain_data)
|
275
|
+
return nil if route.nil?
|
276
|
+
|
277
|
+
domain_data
|
278
|
+
end
|
279
|
+
|
280
|
+
def domain_workload_matches?(data, workload)
|
272
281
|
route = find_domain_route(data)
|
273
|
-
route["workloadLink"].
|
282
|
+
route["workloadLink"].match?(%r{/org/#{org}/gvc/#{gvc}/workload/#{workload}})
|
274
283
|
end
|
275
284
|
|
276
285
|
def set_domain_workload(data, workload)
|
@@ -291,6 +300,24 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
291
300
|
api.log_get(org: org, gvc: gvc, workload: workload, from: from, to: to)
|
292
301
|
end
|
293
302
|
|
303
|
+
# identities
|
304
|
+
|
305
|
+
def fetch_identity(identity, a_gvc = gvc)
|
306
|
+
api.fetch_identity(org: org, gvc: a_gvc, identity: identity)
|
307
|
+
end
|
308
|
+
|
309
|
+
# policies
|
310
|
+
|
311
|
+
def fetch_policy(policy)
|
312
|
+
api.fetch_policy(org: org, policy: policy)
|
313
|
+
end
|
314
|
+
|
315
|
+
def bind_identity_to_policy(identity_link, policy)
|
316
|
+
cmd = "cpln policy add-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
|
317
|
+
cmd += " > /dev/null" if Shell.should_hide_output?
|
318
|
+
perform!(cmd)
|
319
|
+
end
|
320
|
+
|
294
321
|
# apply
|
295
322
|
def apply_template(data) # rubocop:disable Metrics/MethodLength
|
296
323
|
Tempfile.create do |f|
|
@@ -308,7 +335,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
308
335
|
Shell.debug("CMD", cmd)
|
309
336
|
|
310
337
|
result = `#{cmd}`
|
311
|
-
$CHILD_STATUS.success? ? parse_apply_result(result) : exit(
|
338
|
+
$CHILD_STATUS.success? ? parse_apply_result(result) : exit(1)
|
312
339
|
end
|
313
340
|
end
|
314
341
|
end
|
@@ -361,14 +388,14 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
361
388
|
def perform!(cmd, sensitive_data_pattern: nil)
|
362
389
|
Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
|
363
390
|
|
364
|
-
system(cmd) || exit(
|
391
|
+
system(cmd) || exit(1)
|
365
392
|
end
|
366
393
|
|
367
394
|
def perform_yaml(cmd)
|
368
395
|
Shell.debug("CMD", cmd)
|
369
396
|
|
370
397
|
result = `#{cmd}`
|
371
|
-
$CHILD_STATUS.success? ? YAML.safe_load(result) : exit(
|
398
|
+
$CHILD_STATUS.success? ? YAML.safe_load(result) : exit(1)
|
372
399
|
end
|
373
400
|
|
374
401
|
def gvc_org
|
@@ -94,6 +94,10 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
94
94
|
api_json("/org/#{org}/gvc/#{gvc}/volumeset/#{volumeset}", method: :delete)
|
95
95
|
end
|
96
96
|
|
97
|
+
def fetch_domain(org:, domain:)
|
98
|
+
api_json("/org/#{org}/domain/#{domain}", method: :get)
|
99
|
+
end
|
100
|
+
|
97
101
|
def list_domains(org:)
|
98
102
|
api_json("/org/#{org}/domain", method: :get)
|
99
103
|
end
|
@@ -102,6 +106,14 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
102
106
|
api_json("/org/#{org}/domain/#{domain}", method: :patch, body: data)
|
103
107
|
end
|
104
108
|
|
109
|
+
def fetch_identity(org:, gvc:, identity:)
|
110
|
+
api_json("/org/#{org}/gvc/#{gvc}/identity/#{identity}", method: :get)
|
111
|
+
end
|
112
|
+
|
113
|
+
def fetch_policy(org:, policy:)
|
114
|
+
api_json("/org/#{org}/policy/#{policy}", method: :get)
|
115
|
+
end
|
116
|
+
|
105
117
|
private
|
106
118
|
|
107
119
|
def fetch_query_pages(result)
|
@@ -16,6 +16,7 @@ class ControlplaneApiDirect
|
|
16
16
|
# ).freeze
|
17
17
|
|
18
18
|
API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
|
19
|
+
API_TOKEN_EXPIRY_SECONDS = 300
|
19
20
|
|
20
21
|
class << self
|
21
22
|
attr_accessor :trace
|
@@ -26,7 +27,10 @@ class ControlplaneApiDirect
|
|
26
27
|
uri = URI("#{api_host(host)}#{url}")
|
27
28
|
request = API_METHODS[method].new(uri)
|
28
29
|
request["Content-Type"] = "application/json"
|
29
|
-
|
30
|
+
|
31
|
+
refresh_api_token if should_refresh_api_token?
|
32
|
+
|
33
|
+
request["Authorization"] = api_token[:token]
|
30
34
|
request.body = body.to_json if body
|
31
35
|
|
32
36
|
Shell.debug(method.upcase, "#{uri} #{body&.to_json}")
|
@@ -62,17 +66,41 @@ class ControlplaneApiDirect
|
|
62
66
|
end
|
63
67
|
|
64
68
|
# rubocop:disable Style/ClassVars
|
65
|
-
def api_token
|
69
|
+
def api_token # rubocop:disable Metrics/MethodLength
|
66
70
|
return @@api_token if defined?(@@api_token)
|
67
71
|
|
68
|
-
@@api_token =
|
69
|
-
|
70
|
-
|
72
|
+
@@api_token = {
|
73
|
+
token: ENV.fetch("CPLN_TOKEN", nil),
|
74
|
+
comes_from_profile: false
|
75
|
+
}
|
76
|
+
if @@api_token[:token].nil?
|
77
|
+
@@api_token = {
|
78
|
+
token: `cpln profile token`.chomp,
|
79
|
+
comes_from_profile: true
|
80
|
+
}
|
81
|
+
end
|
82
|
+
return @@api_token if @@api_token[:token].match?(API_TOKEN_REGEX)
|
71
83
|
|
72
84
|
raise "Unknown API token format. " \
|
73
85
|
"Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
|
74
86
|
end
|
75
87
|
|
88
|
+
# Returns `true` when the token is about to expire in 5 minutes
|
89
|
+
def should_refresh_api_token?
|
90
|
+
return false unless api_token[:comes_from_profile]
|
91
|
+
|
92
|
+
payload, = JWT.decode(api_token[:token], nil, false)
|
93
|
+
difference_in_seconds = payload["exp"] - Time.now.to_i
|
94
|
+
|
95
|
+
difference_in_seconds <= API_TOKEN_EXPIRY_SECONDS
|
96
|
+
rescue JWT::DecodeError
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
def refresh_api_token
|
101
|
+
@@api_token[:token] = `cpln profile token`.chomp
|
102
|
+
end
|
103
|
+
|
76
104
|
def self.reset_api_token
|
77
105
|
remove_class_variable(:@@api_token) if defined?(@@api_token)
|
78
106
|
end
|
data/lib/core/helpers.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "securerandom"
|
4
|
+
|
3
5
|
module Helpers
|
4
6
|
def strip_str_and_validate(str)
|
5
7
|
return str if str.nil?
|
@@ -7,4 +9,8 @@ module Helpers
|
|
7
9
|
str = str.strip
|
8
10
|
str.empty? ? nil : str
|
9
11
|
end
|
12
|
+
|
13
|
+
def random_four_digits
|
14
|
+
SecureRandom.random_number(1000..9999)
|
15
|
+
end
|
10
16
|
end
|
data/lib/cpl/version.rb
CHANGED
data/lib/cpl.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "date"
|
3
4
|
require "dotenv/load"
|
4
5
|
require "cgi"
|
5
6
|
require "json"
|
7
|
+
require "jwt"
|
6
8
|
require "net/http"
|
7
9
|
require "pathname"
|
8
10
|
require "tempfile"
|
@@ -142,6 +144,7 @@ module Cpl
|
|
142
144
|
requires_args = command_class::REQUIRES_ARGS
|
143
145
|
default_args = command_class::DEFAULT_ARGS
|
144
146
|
command_options = command_class::OPTIONS + ::Command::Base.common_options
|
147
|
+
accepts_extra_options = command_class::ACCEPTS_EXTRA_OPTIONS
|
145
148
|
description = command_class::DESCRIPTION
|
146
149
|
long_description = command_class::LONG_DESCRIPTION
|
147
150
|
examples = command_class::EXAMPLES
|
@@ -178,7 +181,9 @@ module Cpl
|
|
178
181
|
default_args
|
179
182
|
end
|
180
183
|
|
181
|
-
|
184
|
+
if (args.empty? && requires_args) || (!args.empty? && !requires_args && !accepts_extra_options)
|
185
|
+
raise_args_error.call(args, nil)
|
186
|
+
end
|
182
187
|
|
183
188
|
begin
|
184
189
|
config = Config.new(args, options, required_options)
|
@@ -19,10 +19,15 @@ aliases:
|
|
19
19
|
one_off_workload: rails
|
20
20
|
|
21
21
|
# Workloads that are for the application itself and are using application Docker images.
|
22
|
+
# These are updated with the new image when running the `deploy-image` command,
|
23
|
+
# and are also used by the `info`, `ps:`, and `run:cleanup` commands in order to get all of the defined workloads.
|
24
|
+
# On the other hand, if you have a workload for Redis, that would NOT use the application Docker image
|
25
|
+
# and not be listed here.
|
22
26
|
app_workloads:
|
23
27
|
- rails
|
24
28
|
|
25
29
|
# Additional "service type" workloads, using non-application Docker images.
|
30
|
+
# These are only used by the `info`, `ps:` and `run:cleanup` commands in order to get all of the defined workloads.
|
26
31
|
additional_workloads:
|
27
32
|
- postgres
|
28
33
|
|
@@ -1,15 +1,15 @@
|
|
1
1
|
# Template setup of the GVC, roughly corresponding to a Heroku app
|
2
2
|
kind: gvc
|
3
|
-
name:
|
3
|
+
name: {{APP_NAME}}
|
4
4
|
spec:
|
5
5
|
# For using templates for test apps, put ENV values here, stored in git repo.
|
6
6
|
# Production apps will have values configured manually after app creation.
|
7
7
|
env:
|
8
8
|
- name: DATABASE_URL
|
9
|
-
# Password does not matter because host postgres.
|
9
|
+
# Password does not matter because host postgres.{{APP_NAME}}.cpln.local can only be accessed
|
10
10
|
# locally within CPLN GVC, and postgres running on a CPLN workload is something only for a
|
11
11
|
# test app that lacks persistence.
|
12
|
-
value: 'postgres://the_user:the_password@postgres.
|
12
|
+
value: 'postgres://the_user:the_password@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}'
|
13
13
|
- name: RAILS_ENV
|
14
14
|
value: production
|
15
15
|
- name: RAILS_SERVE_STATIC_FILES
|
@@ -18,4 +18,4 @@ spec:
|
|
18
18
|
# Part of standard configuration
|
19
19
|
staticPlacement:
|
20
20
|
locationLinks:
|
21
|
-
-
|
21
|
+
- {{APP_LOCATION_LINK}}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
bad_links=("controlplane.com/shakacode")
|
4
|
+
proper_links=("shakacode.controlplane.com")
|
5
|
+
|
6
|
+
bold=$(tput bold)
|
7
|
+
normal=$(tput sgr0)
|
8
|
+
|
9
|
+
exit_status=0
|
10
|
+
accumulated_results=""
|
11
|
+
seen_bad_links_indexes=()
|
12
|
+
|
13
|
+
for ((idx = 0; idx < ${#bad_links[@]}; idx++)); do
|
14
|
+
results=$(git grep \
|
15
|
+
--recursive \
|
16
|
+
--line-number \
|
17
|
+
--fixed-strings \
|
18
|
+
--break \
|
19
|
+
--heading \
|
20
|
+
--color=always -- \
|
21
|
+
"${bad_links[idx]}" \
|
22
|
+
':!script/check_cpln_links')
|
23
|
+
|
24
|
+
# Line would become really unwieldly if everything was mushed into the
|
25
|
+
# conditional, so let's ignore this check here.
|
26
|
+
# shellcheck disable=SC2181
|
27
|
+
if [ $? -eq 0 ]; then
|
28
|
+
accumulated_results+="$results"
|
29
|
+
seen_bad_links_indexes+=("$idx")
|
30
|
+
exit_status=1
|
31
|
+
fi
|
32
|
+
done
|
33
|
+
|
34
|
+
if [ "$exit_status" -eq 1 ]; then
|
35
|
+
echo "${bold}[!] Found the following bad links:${normal}"
|
36
|
+
echo ""
|
37
|
+
echo "$accumulated_results"
|
38
|
+
echo ""
|
39
|
+
echo "${bold}[*] Please update accordingly:${normal}"
|
40
|
+
for bad_link_index in "${seen_bad_links_indexes[@]}"; do
|
41
|
+
echo " ${bad_links[bad_link_index]} -> ${proper_links[bad_link_index]}"
|
42
|
+
done
|
43
|
+
fi
|
44
|
+
|
45
|
+
exit "$exit_status"
|
data/templates/daily-task.yml
CHANGED
@@ -18,7 +18,7 @@ spec:
|
|
18
18
|
- rails
|
19
19
|
- db:prepare
|
20
20
|
inheritEnv: true
|
21
|
-
image:
|
21
|
+
image: {{APP_IMAGE_LINK}}
|
22
22
|
defaultOptions:
|
23
23
|
autoscaling:
|
24
24
|
minScale: 1
|
@@ -28,4 +28,5 @@ spec:
|
|
28
28
|
external:
|
29
29
|
outboundAllowCIDR:
|
30
30
|
- 0.0.0.0/0
|
31
|
-
|
31
|
+
# Identity is used for binding workload to secrets
|
32
|
+
identityLink: {{APP_IDENTITY_LINK}}
|
data/templates/gvc.yml
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
kind: gvc
|
2
|
-
name:
|
2
|
+
name: {{APP_NAME}}
|
3
3
|
spec:
|
4
4
|
env:
|
5
5
|
- name: MEMCACHE_SERVERS
|
6
|
-
value: memcached.
|
6
|
+
value: memcached.{{APP_NAME}}.cpln.local
|
7
7
|
- name: REDIS_URL
|
8
|
-
value: redis://redis.
|
8
|
+
value: redis://redis.{{APP_NAME}}.cpln.local:6379
|
9
9
|
- name: DATABASE_URL
|
10
|
-
value: postgres://postgres:password123@postgres.
|
10
|
+
value: postgres://postgres:password123@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}
|
11
11
|
staticPlacement:
|
12
12
|
locationLinks:
|
13
|
-
-
|
13
|
+
- {{APP_LOCATION_LINK}}
|
data/templates/identity.yml
CHANGED