cpl 1.2.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check_cpln_links.yml +19 -0
  3. data/.overcommit.yml +3 -0
  4. data/CHANGELOG.md +43 -1
  5. data/Gemfile.lock +9 -5
  6. data/README.md +43 -7
  7. data/cpl.gemspec +1 -0
  8. data/docs/commands.md +30 -23
  9. data/docs/dns.md +9 -0
  10. data/docs/tips.md +11 -1
  11. data/examples/controlplane.yml +22 -1
  12. data/lib/command/apply_template.rb +58 -10
  13. data/lib/command/base.rb +79 -2
  14. data/lib/command/build_image.rb +5 -1
  15. data/lib/command/cleanup_stale_apps.rb +0 -2
  16. data/lib/command/copy_image_from_upstream.rb +5 -4
  17. data/lib/command/deploy_image.rb +20 -2
  18. data/lib/command/info.rb +11 -26
  19. data/lib/command/maintenance.rb +8 -4
  20. data/lib/command/maintenance_off.rb +8 -4
  21. data/lib/command/maintenance_on.rb +8 -4
  22. data/lib/command/promote_app_from_upstream.rb +5 -25
  23. data/lib/command/run.rb +20 -22
  24. data/lib/command/run_detached.rb +38 -30
  25. data/lib/command/setup_app.rb +19 -2
  26. data/lib/core/config.rb +36 -14
  27. data/lib/core/controlplane.rb +34 -7
  28. data/lib/core/controlplane_api.rb +12 -0
  29. data/lib/core/controlplane_api_direct.rb +33 -5
  30. data/lib/core/helpers.rb +6 -0
  31. data/lib/cpl/version.rb +1 -1
  32. data/lib/cpl.rb +6 -1
  33. data/lib/generator_templates/controlplane.yml +5 -0
  34. data/lib/generator_templates/templates/gvc.yml +4 -4
  35. data/lib/generator_templates/templates/postgres.yml +1 -1
  36. data/lib/generator_templates/templates/rails.yml +1 -1
  37. data/script/check_cpln_links +45 -0
  38. data/templates/daily-task.yml +3 -2
  39. data/templates/gvc.yml +5 -5
  40. data/templates/identity.yml +2 -1
  41. data/templates/rails.yml +3 -2
  42. data/templates/secrets-policy.yml +4 -0
  43. data/templates/secrets.yml +3 -0
  44. data/templates/sidekiq.yml +3 -2
  45. metadata +21 -2
@@ -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 rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
35
- cpl run:detached rails db:migrate -a $APP_NAME --image latest # Latest sequential image
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 rails db:migrate:status -a $APP_NAME -w other-workload
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 rails db:migrate:status -a $APP_NAME --use-local-token
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, :workload, :one_off, :container
49
+ attr_reader :location, :workload_to_clone, :workload_clone, :container
49
50
 
50
- def call # rubocop:disable Metrics/MethodLength
51
+ def call
51
52
  @location = config.location
52
- @workload = config.options["workload"] || config[:one_off_workload]
53
- @one_off = "#{workload}-runner-#{rand(1000..9999)}"
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 '#{workload}' on app '#{config.options[:app]}' to '#{one_off}'") do
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(one_off)
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!(workload).fetch("spec")
74
- container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
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", "value" => ControlplaneApiDirect.new.api_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" => one_off, "spec" => spec)
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
- if ! eval "#{args_join(config.args)}"; then echo "----- CRASHED -----"; fi
123
-
124
- echo "-- FINISHED RUNNER SCRIPT, DELETING WORKLOAD --"
125
- sleep 10 # grace time for logs propagation
126
- curl ${CPLN_ENDPOINT}${CPLN_WORKLOAD} -H "Authorization: ${CONTROLPLANE_TOKEN}" -X DELETE -s -o /dev/null
127
- while true; do sleep 1; done # wait for SIGTERM
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
- while cp.fetch_workload(one_off)
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: one_off, from: from, to: to)
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"]) }
@@ -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?
@@ -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"].split("/").last == workload }
268
+ workloads.any? { |workload| route["workloadLink"].match?(%r{/org/#{org}/gvc/#{gvc}/workload/#{workload}}) }
268
269
  end
269
270
  end
270
271
 
271
- def get_domain_workload(data)
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"].split("/").last
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(false)
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(false)
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(false)
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
- request["Authorization"] = api_token
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 = ENV.fetch("CPLN_TOKEN", nil)
69
- @@api_token = `cpln profile token`.chomp if @@api_token.nil?
70
- return @@api_token if @@api_token.match?(API_TOKEN_REGEX)
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpl
4
- VERSION = "1.2.0"
4
+ VERSION = "1.4.0"
5
5
  MIN_CPLN_VERSION = "0.0.71"
6
6
  end
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
- raise_args_error.call(args, nil) if (args.empty? && requires_args) || (!args.empty? && !requires_args)
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: APP_GVC
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.APP_GVC.cpln.local can only be accessed
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.APP_GVC.cpln.local:5432/APP_GVC'
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
- - /org/APP_ORG/location/APP_LOCATION
21
+ - {{APP_LOCATION_LINK}}
@@ -106,7 +106,7 @@ bindings:
106
106
  # - use
107
107
  # - view
108
108
  principalLinks:
109
- - //gvc/APP_GVC/identity/postgres-poc-identity
109
+ - //gvc/{{APP_NAME}}/identity/postgres-poc-identity
110
110
  targetKind: secret
111
111
  targetLinks:
112
112
  - //secret/postgres-poc-credentials
@@ -14,7 +14,7 @@ spec:
14
14
  value: debug
15
15
  # Inherit other ENV values from GVC
16
16
  inheritEnv: true
17
- image: '/org/APP_ORG/image/APP_IMAGE'
17
+ image: {{APP_IMAGE_LINK}}
18
18
  # 512 corresponds to a standard 1x dyno type
19
19
  memory: 512Mi
20
20
  ports:
@@ -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"
@@ -18,7 +18,7 @@ spec:
18
18
  - rails
19
19
  - db:prepare
20
20
  inheritEnv: true
21
- image: "/org/APP_ORG/image/APP_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
- identityLink: /org/APP_ORG/gvc/APP_GVC/identity/APP_GVC-identity
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: APP_GVC
2
+ name: {{APP_NAME}}
3
3
  spec:
4
4
  env:
5
5
  - name: MEMCACHE_SERVERS
6
- value: memcached.APP_GVC.cpln.local
6
+ value: memcached.{{APP_NAME}}.cpln.local
7
7
  - name: REDIS_URL
8
- value: redis://redis.APP_GVC.cpln.local:6379
8
+ value: redis://redis.{{APP_NAME}}.cpln.local:6379
9
9
  - name: DATABASE_URL
10
- value: postgres://postgres:password123@postgres.APP_GVC.cpln.local:5432/APP_GVC
10
+ value: postgres://postgres:password123@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}
11
11
  staticPlacement:
12
12
  locationLinks:
13
- - /org/APP_ORG/location/APP_LOCATION
13
+ - {{APP_LOCATION_LINK}}
@@ -1,2 +1,3 @@
1
+ # Identity is needed to access secrets
1
2
  kind: identity
2
- name: APP_GVC-identity
3
+ name: {{APP_IDENTITY}}