cpl 1.4.0 → 2.0.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
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