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.
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}}