cpl 1.4.0 → 2.0.0.rc.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/command_docs.yml +1 -1
  3. data/.github/workflows/rspec-shared.yml +56 -0
  4. data/.github/workflows/rspec.yml +19 -31
  5. data/.github/workflows/rubocop.yml +2 -10
  6. data/.gitignore +2 -0
  7. data/.simplecov_spawn.rb +10 -0
  8. data/CHANGELOG.md +8 -0
  9. data/CONTRIBUTING.md +32 -2
  10. data/Gemfile.lock +34 -29
  11. data/README.md +34 -25
  12. data/cpl.gemspec +1 -1
  13. data/docs/commands.md +54 -54
  14. data/docs/dns.md +6 -0
  15. data/docs/migrating.md +10 -10
  16. data/docs/tips.md +12 -10
  17. data/examples/circleci.yml +3 -3
  18. data/examples/controlplane.yml +25 -16
  19. data/lib/command/apply_template.rb +9 -9
  20. data/lib/command/base.rb +132 -37
  21. data/lib/command/build_image.rb +4 -9
  22. data/lib/command/cleanup_stale_apps.rb +1 -1
  23. data/lib/command/copy_image_from_upstream.rb +0 -7
  24. data/lib/command/delete.rb +39 -7
  25. data/lib/command/deploy_image.rb +18 -3
  26. data/lib/command/exists.rb +1 -1
  27. data/lib/command/generate.rb +1 -1
  28. data/lib/command/info.rb +7 -3
  29. data/lib/command/logs.rb +22 -2
  30. data/lib/command/maintenance_off.rb +1 -1
  31. data/lib/command/maintenance_on.rb +1 -1
  32. data/lib/command/open.rb +2 -2
  33. data/lib/command/open_console.rb +2 -2
  34. data/lib/command/ps.rb +1 -1
  35. data/lib/command/ps_start.rb +2 -1
  36. data/lib/command/ps_stop.rb +40 -8
  37. data/lib/command/ps_wait.rb +3 -2
  38. data/lib/command/run.rb +430 -69
  39. data/lib/command/setup_app.rb +4 -1
  40. data/lib/constants/exit_code.rb +7 -0
  41. data/lib/core/config.rb +1 -1
  42. data/lib/core/controlplane.rb +109 -48
  43. data/lib/core/controlplane_api.rb +7 -1
  44. data/lib/core/controlplane_api_cli.rb +3 -3
  45. data/lib/core/controlplane_api_direct.rb +1 -1
  46. data/lib/core/shell.rb +15 -9
  47. data/lib/cpl/version.rb +1 -1
  48. data/lib/cpl.rb +48 -9
  49. data/lib/deprecated_commands.json +2 -1
  50. data/lib/generator_templates/controlplane.yml +2 -2
  51. data/script/check_cpln_links +3 -3
  52. data/templates/{gvc.yml → app.yml} +5 -0
  53. data/templates/secrets.yml +8 -0
  54. metadata +23 -26
  55. data/.rspec +0 -1
  56. data/lib/command/run_cleanup.rb +0 -116
  57. data/lib/command/run_detached.rb +0 -176
  58. data/lib/core/scripts.rb +0 -34
  59. data/templates/identity.yml +0 -3
  60. data/templates/secrets-policy.yml +0 -4
  61. /data/lib/generator_templates/templates/{gvc.yml → app.yml} +0 -0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cpl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 2.0.0.rc.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-03-21 00:00:00.000000000 Z
12
+ date: 2024-05-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -109,6 +109,20 @@ dependencies:
109
109
  - - "~>"
110
110
  - !ruby/object:Gem::Version
111
111
  version: 3.12.0
112
+ - !ruby/object:Gem::Dependency
113
+ name: rspec-retry
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: 0.6.2
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: 0.6.2
112
126
  - !ruby/object:Gem::Dependency
113
127
  name: rubocop
114
128
  requirement: !ruby/object:Gem::Requirement
@@ -179,20 +193,6 @@ dependencies:
179
193
  - - "~>"
180
194
  - !ruby/object:Gem::Version
181
195
  version: 0.9.6
182
- - !ruby/object:Gem::Dependency
183
- name: vcr
184
- requirement: !ruby/object:Gem::Requirement
185
- requirements:
186
- - - "~>"
187
- - !ruby/object:Gem::Version
188
- version: 6.1.0
189
- type: :development
190
- prerelease: false
191
- version_requirements: !ruby/object:Gem::Requirement
192
- requirements:
193
- - - "~>"
194
- - !ruby/object:Gem::Version
195
- version: 6.1.0
196
196
  - !ruby/object:Gem::Dependency
197
197
  name: webmock
198
198
  requirement: !ruby/object:Gem::Requirement
@@ -218,12 +218,13 @@ extra_rdoc_files: []
218
218
  files:
219
219
  - ".github/workflows/check_cpln_links.yml"
220
220
  - ".github/workflows/command_docs.yml"
221
+ - ".github/workflows/rspec-shared.yml"
221
222
  - ".github/workflows/rspec.yml"
222
223
  - ".github/workflows/rubocop.yml"
223
224
  - ".gitignore"
224
225
  - ".overcommit.yml"
225
- - ".rspec"
226
226
  - ".rubocop.yml"
227
+ - ".simplecov_spawn.rb"
227
228
  - CHANGELOG.md
228
229
  - CONTRIBUTING.md
229
230
  - Gemfile
@@ -276,18 +277,16 @@ files:
276
277
  - lib/command/ps_stop.rb
277
278
  - lib/command/ps_wait.rb
278
279
  - lib/command/run.rb
279
- - lib/command/run_cleanup.rb
280
- - lib/command/run_detached.rb
281
280
  - lib/command/setup_app.rb
282
281
  - lib/command/test.rb
283
282
  - lib/command/version.rb
283
+ - lib/constants/exit_code.rb
284
284
  - lib/core/config.rb
285
285
  - lib/core/controlplane.rb
286
286
  - lib/core/controlplane_api.rb
287
287
  - lib/core/controlplane_api_cli.rb
288
288
  - lib/core/controlplane_api_direct.rb
289
289
  - lib/core/helpers.rb
290
- - lib/core/scripts.rb
291
290
  - lib/core/shell.rb
292
291
  - lib/cpl.rb
293
292
  - lib/cpl/version.rb
@@ -295,7 +294,7 @@ files:
295
294
  - lib/generator_templates/Dockerfile
296
295
  - lib/generator_templates/controlplane.yml
297
296
  - lib/generator_templates/entrypoint.sh
298
- - lib/generator_templates/templates/gvc.yml
297
+ - lib/generator_templates/templates/app.yml
299
298
  - lib/generator_templates/templates/postgres.yml
300
299
  - lib/generator_templates/templates/rails.yml
301
300
  - rakelib/create_release.rake
@@ -304,15 +303,13 @@ files:
304
303
  - script/check_cpln_links
305
304
  - script/rename_command
306
305
  - script/update_command_docs
306
+ - templates/app.yml
307
307
  - templates/daily-task.yml
308
- - templates/gvc.yml
309
- - templates/identity.yml
310
308
  - templates/maintenance.yml
311
309
  - templates/memcached.yml
312
310
  - templates/postgres.yml
313
311
  - templates/rails.yml
314
312
  - templates/redis.yml
315
- - templates/secrets-policy.yml
316
313
  - templates/secrets.yml
317
314
  - templates/sidekiq.yml
318
315
  homepage: https://github.com/shakacode/heroku-to-control-plane
@@ -331,9 +328,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
331
328
  version: 2.7.0
332
329
  required_rubygems_version: !ruby/object:Gem::Requirement
333
330
  requirements:
334
- - - ">="
331
+ - - ">"
335
332
  - !ruby/object:Gem::Version
336
- version: '0'
333
+ version: 1.3.1
337
334
  requirements: []
338
335
  rubygems_version: 3.4.21
339
336
  signing_key:
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --require spec_helper
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Command
4
- class RunCleanup < Base
5
- NAME = "run:cleanup"
6
- OPTIONS = [
7
- app_option(required: true),
8
- skip_confirm_option
9
- ].freeze
10
- DESCRIPTION = "Deletes stale run workloads for an app"
11
- LONG_DESCRIPTION = <<~DESC
12
- - Deletes stale run workloads for an app
13
- - Workloads are considered stale based on how many days since created
14
- - `stale_run_workload_created_days` in the `.controlplane/controlplane.yml` file specifies the number of days after created that the workload is considered stale
15
- - Works for both interactive workloads (created with `cpl run`) and non-interactive workloads (created with `cpl run:detached`)
16
- - Will ask for explicit user confirmation of deletion
17
- DESC
18
-
19
- def call # rubocop:disable Metrics/MethodLength
20
- return progress.puts("No stale run workloads found.") if stale_run_workloads.empty?
21
-
22
- progress.puts("Stale run workloads:")
23
- stale_run_workloads.each do |workload|
24
- output = ""
25
- output += if config.should_app_start_with?(config.app)
26
- " #{workload[:app]} - #{workload[:name]}"
27
- else
28
- " #{workload[:name]}"
29
- end
30
- output += " (#{Shell.color("#{workload[:date]} - #{workload[:days]} days ago", :red)})"
31
- progress.puts(output)
32
- end
33
-
34
- return unless confirm_delete
35
-
36
- progress.puts
37
- stale_run_workloads.each do |workload|
38
- delete_workload(workload)
39
- end
40
- end
41
-
42
- private
43
-
44
- def app_matches?(app, app_name, app_options)
45
- app == app_name.to_s || (app_options[:match_if_app_name_starts_with] && app.start_with?(app_name.to_s))
46
- end
47
-
48
- def find_app_options(app)
49
- @app_options ||= {}
50
- @app_options[app] ||= config.apps.find do |app_name, app_options|
51
- app_matches?(app, app_name, app_options)
52
- end&.last
53
- end
54
-
55
- def find_workloads(app)
56
- app_options = find_app_options(app)
57
- return [] if app_options.nil?
58
-
59
- (app_options[:app_workloads] + app_options[:additional_workloads] + [app_options[:one_off_workload]]).uniq
60
- end
61
-
62
- def stale_run_workloads # rubocop:disable Metrics/MethodLength
63
- @stale_run_workloads ||=
64
- begin
65
- defined_workloads = find_workloads(config.app)
66
-
67
- run_workloads = []
68
-
69
- now = DateTime.now
70
- stale_run_workload_created_days = config[:stale_run_workload_created_days]
71
-
72
- interactive_workloads = cp.query_workloads("-run-", partial_workload_match: true)["items"]
73
- non_interactive_workloads = cp.query_workloads("-runner-", partial_workload_match: true)["items"]
74
- workloads = interactive_workloads + non_interactive_workloads
75
-
76
- workloads.each do |workload|
77
- app_name = workload["links"].find { |link| link["rel"] == "gvc" }["href"].split("/").last
78
- workload_name = workload["name"]
79
-
80
- original_workload_name, workload_number = workload_name.split(/-run-|-runner-/)
81
- next unless defined_workloads.include?(original_workload_name) && workload_number.match?(/^\d{4}$/)
82
-
83
- created_date = DateTime.parse(workload["created"])
84
- diff_in_days = (now - created_date).to_i
85
- next unless diff_in_days >= stale_run_workload_created_days
86
-
87
- run_workloads.push({
88
- app: app_name,
89
- name: workload_name,
90
- date: created_date,
91
- days: diff_in_days
92
- })
93
- end
94
-
95
- run_workloads
96
- end
97
- end
98
-
99
- def confirm_delete
100
- return true if config.options[:yes]
101
-
102
- Shell.confirm("\nAre you sure you want to delete these #{stale_run_workloads.length} run workloads?")
103
- end
104
-
105
- def delete_workload(workload)
106
- message = if config.should_app_start_with?(config.app)
107
- "Deleting run workload '#{workload[:app]} - #{workload[:name]}'"
108
- else
109
- "Deleting run workload '#{workload[:name]}'"
110
- end
111
- step(message) do
112
- cp.delete_workload(workload[:name], workload[:app])
113
- end
114
- end
115
- end
116
- end
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Command
4
- class RunDetached < Base # rubocop:disable Metrics/ClassLength
5
- NAME = "run:detached"
6
- USAGE = "run:detached COMMAND"
7
- REQUIRES_ARGS = true
8
- OPTIONS = [
9
- app_option(required: true),
10
- image_option,
11
- workload_option,
12
- location_option,
13
- use_local_token_option,
14
- clean_on_failure_option
15
- ].freeze
16
- DESCRIPTION = "Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)"
17
- LONG_DESCRIPTION = <<~DESC
18
- - Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)
19
- - Uses `Cron` workload type with log async fetching
20
- - Implemented with only async execution methods, more suitable for production tasks
21
- - Has alternative log fetch implementation with only JSON-polling and no WebSockets
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
26
- DESC
27
- EXAMPLES = <<~EX
28
- ```sh
29
- cpl run:detached rails db:prepare -a $APP_NAME
30
-
31
- # Need to quote COMMAND if setting ENV value or passing args.
32
- cpl run:detached -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
33
-
34
- # Uses a different image (which may not be promoted yet).
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
37
-
38
- # Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
39
- cpl run:detached -a $APP_NAME -w other-workload -- rails db:migrate:status
40
-
41
- # Overrides remote CPLN_TOKEN env variable with local token.
42
- # Useful when superuser rights are needed in remote container.
43
- cpl run:detached -a $APP_NAME --use-local-token -- rails db:migrate:status
44
- ```
45
- EX
46
-
47
- WORKLOAD_SLEEP_CHECK = 2
48
-
49
- attr_reader :location, :workload_to_clone, :workload_clone, :container
50
-
51
- def call
52
- @location = config.location
53
- @workload_to_clone = config.options["workload"] || config[:one_off_workload]
54
- @workload_clone = "#{workload_to_clone}-runner-#{random_four_digits}"
55
-
56
- step("Cloning workload '#{workload_to_clone}' on app '#{config.options[:app]}' to '#{workload_clone}'") do
57
- clone_workload
58
- end
59
-
60
- wait_for_workload(workload_clone)
61
- show_logs_waiting
62
- ensure
63
- exit(1) if @crashed
64
- end
65
-
66
- private
67
-
68
- def clone_workload # rubocop:disable Metrics/MethodLength
69
- # Get base specs of workload
70
- spec = cp.fetch_workload!(workload_to_clone).fetch("spec")
71
- container_spec = spec["containers"].detect { _1["name"] == workload_to_clone } || spec["containers"].first
72
- @container = container_spec["name"]
73
-
74
- # remove other containers if any
75
- spec["containers"] = [container_spec]
76
-
77
- # Set runner
78
- container_spec["command"] = "bash"
79
- container_spec["args"] = ["-c", 'eval "$CONTROLPLANE_RUNNER"']
80
-
81
- # Ensure one-off workload will be running
82
- spec["defaultOptions"]["suspend"] = false
83
-
84
- # Ensure no scaling
85
- spec["defaultOptions"]["autoscaling"]["minScale"] = 1
86
- spec["defaultOptions"]["autoscaling"]["maxScale"] = 1
87
- spec["defaultOptions"]["capacityAI"] = false
88
-
89
- # Override image if specified
90
- image = config.options[:image]
91
- image = latest_image if image == "latest"
92
- container_spec["image"] = "/org/#{config.org}/image/#{image}" if image
93
-
94
- # Set cron job props
95
- spec["type"] = "cron"
96
- spec["job"] = { "schedule" => "* * * * *", "restartPolicy" => "Never" }
97
- spec["defaultOptions"]["autoscaling"] = {}
98
- container_spec.delete("ports")
99
-
100
- container_spec["env"] ||= []
101
- container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN",
102
- "value" => ControlplaneApiDirect.new.api_token[:token] }
103
- container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
104
-
105
- # Create workload clone
106
- cp.apply_hash("kind" => "workload", "name" => workload_clone, "spec" => spec)
107
- end
108
-
109
- def runner_script # rubocop:disable Metrics/MethodLength
110
- script = "echo '-- STARTED RUNNER SCRIPT --'\n"
111
- script += Scripts.helpers_cleanup
112
-
113
- if config.options["use_local_token"]
114
- script += <<~SHELL
115
- CPLN_TOKEN=$CONTROLPLANE_TOKEN
116
- SHELL
117
- end
118
-
119
- script += <<~SHELL
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
134
- SHELL
135
-
136
- script
137
- end
138
-
139
- def show_logs_waiting # rubocop:disable Metrics/MethodLength
140
- progress.puts("Scheduled, fetching logs (it's a cron job, so it may take up to a minute to start)...\n\n")
141
- begin
142
- @finished = false
143
- while cp.fetch_workload(workload_clone) && !@finished
144
- sleep(WORKLOAD_SLEEP_CHECK)
145
- print_uniq_logs
146
- end
147
- rescue RuntimeError => e
148
- progress.puts(Shell.color("ERROR: #{e}", :red))
149
- retry
150
- end
151
- progress.puts("\nFinished workload and logger.")
152
- end
153
-
154
- def print_uniq_logs
155
- @printed_log_entries ||= []
156
- ts = Time.now.to_i
157
- entries = normalized_log_entries(from: ts - 60, to: ts)
158
-
159
- (entries - @printed_log_entries).sort.each do |(_ts, val)|
160
- @crashed = true if val.match?(/^----- CRASHED -----$/)
161
- @finished = true if val.match?(/^-- FINISHED RUNNER SCRIPT(, DELETING WORKLOAD)? --$/)
162
- puts val
163
- end
164
-
165
- @printed_log_entries = entries # as well truncate old entries if any
166
- end
167
-
168
- def normalized_log_entries(from:, to:)
169
- log = cp.log_get(workload: workload_clone, from: from, to: to)
170
-
171
- log["data"]["result"]
172
- .each_with_object([]) { |obj, result| result.concat(obj["values"]) }
173
- .select { |ts, _val| ts[..-10].to_i > from }
174
- end
175
- end
176
- end
data/lib/core/scripts.rb DELETED
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Scripts
4
- module_function
5
-
6
- def assert_replicas(gvc:, workload:, location:)
7
- <<~SHELL
8
- REPLICAS_QTY=$( \
9
- curl ${CPLN_ENDPOINT}/org/shakacode-staging/gvc/#{gvc}/workload/#{workload}/deployment/#{location} \
10
- -H "Authorization: ${CONTROLPLANE_TOKEN}" -s | grep -o '"replicas":[0-9]*' | grep -o '[0-9]*')
11
-
12
- if [ "$REPLICAS_QTY" -gt 0 ]; then
13
- echo "-- MULTIPLE REPLICAS ATTEMPT: $REPLICAS_QTY --"
14
- exit -1
15
- fi
16
- SHELL
17
- end
18
-
19
- def helpers_cleanup
20
- <<~SHELL
21
- unset CONTROLPLANE_RUNNER
22
- SHELL
23
- end
24
-
25
- # NOTE: please escape all '/' as '//' (as it is ruby interpolation here as well)
26
- def http_dummy_server_ruby
27
- 'require "socket";s=TCPServer.new(ENV["PORT"] || 80);' \
28
- 'loop do c=s.accept;c.puts("HTTP/1.1 200 OK\\nContent-Length: 2\\n\\nOk");c.close end'
29
- end
30
-
31
- def http_ping_ruby
32
- 'require "net/http";uri=URI(ENV["CPLN_GLOBAL_ENDPOINT"]);loop do puts(Net::HTTP.get(uri));sleep(5);end'
33
- end
34
- end
@@ -1,3 +0,0 @@
1
- # Identity is needed to access secrets
2
- kind: identity
3
- name: {{APP_IDENTITY}}
@@ -1,4 +0,0 @@
1
- # Policy is needed to allow identities to access secrets
2
- kind: policy
3
- name: {{APP_SECRETS_POLICY}}
4
- targetKind: secret