cpl 1.2.0 → 1.4.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/.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