cpl 0.6.0 → 1.0.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.
data/lib/core/config.rb CHANGED
@@ -15,24 +15,15 @@ class Config
15
15
  @app = options[:app]
16
16
 
17
17
  load_app_config
18
-
19
- @apps = config[:apps]
20
-
21
- pick_current_config if app
22
- warn_deprecated_options if current
18
+ load_apps
23
19
  end
24
20
 
25
21
  def [](key)
26
22
  ensure_current_config!
27
23
 
28
- old_key = old_option_keys[key]
29
- if current.key?(key)
30
- current.fetch(key)
31
- elsif old_key && current.key?(old_key)
32
- current.fetch(old_key)
33
- else
34
- raise "Can't find option '#{key}' for app '#{app}' in 'controlplane.yml'."
35
- end
24
+ raise "Can't find option '#{key}' for app '#{app}' in 'controlplane.yml'." unless current.key?(key)
25
+
26
+ current.fetch(key)
36
27
  end
37
28
 
38
29
  def script_path
@@ -49,8 +40,8 @@ class Config
49
40
  raise "Can't find current config, please specify an app." unless current
50
41
  end
51
42
 
52
- def ensure_current_config_app!(app)
53
- raise "Can't find app '#{app}' in 'controlplane.yml'." unless current
43
+ def ensure_current_config_app!(app_name)
44
+ raise "Can't find app '#{app_name}' in 'controlplane.yml'." unless current
54
45
  end
55
46
 
56
47
  def ensure_config!
@@ -61,20 +52,36 @@ class Config
61
52
  raise "Can't find key 'apps' in 'controlplane.yml'." unless config[:apps]
62
53
  end
63
54
 
64
- def ensure_config_app!(app, options)
65
- raise "App '#{app}' is empty in 'controlplane.yml'." unless options
55
+ def ensure_config_app!(app_name, app_options)
56
+ raise "App '#{app_name}' is empty in 'controlplane.yml'." unless app_options
57
+ end
58
+
59
+ def app_matches_current?(app_name, app_options)
60
+ app && (app_name.to_s == app || (app_options[:match_if_app_name_starts_with] && app.start_with?(app_name.to_s)))
66
61
  end
67
62
 
68
- def pick_current_config
69
- config[:apps].each do |c_app, c_data|
70
- ensure_config_app!(c_app, c_data)
71
- next unless c_app.to_s == app || (c_data[:match_if_app_name_starts_with] && app.start_with?(c_app.to_s))
63
+ def pick_current_config(app_name, app_options)
64
+ @current = app_options
65
+ @org = self[:cpln_org]
66
+ ensure_current_config_app!(app_name)
67
+ end
68
+
69
+ def load_apps # rubocop:disable Metrics/MethodLength
70
+ @apps = config[:apps].to_h do |app_name, app_options|
71
+ ensure_config_app!(app_name, app_options)
72
+
73
+ app_options_with_new_keys = app_options.to_h do |key, value|
74
+ new_key = new_option_keys[key]
75
+ new_key ? [new_key, value] : [key, value]
76
+ end
77
+
78
+ if app_matches_current?(app_name, app_options_with_new_keys)
79
+ pick_current_config(app_name, app_options_with_new_keys)
80
+ warn_deprecated_options(app_options)
81
+ end
72
82
 
73
- @current = c_data
74
- @org = self[:cpln_org]
75
- break
83
+ [app_name, app_options_with_new_keys]
76
84
  end
77
- ensure_current_config_app!(app)
78
85
  end
79
86
 
80
87
  def load_app_config
@@ -100,19 +107,19 @@ class Config
100
107
  end
101
108
  end
102
109
 
103
- def old_option_keys
110
+ def new_option_keys
104
111
  {
105
- cpln_org: :org,
106
- default_location: :location,
107
- match_if_app_name_starts_with: :prefix
112
+ org: :cpln_org,
113
+ location: :default_location,
114
+ prefix: :match_if_app_name_starts_with
108
115
  }
109
116
  end
110
117
 
111
- def warn_deprecated_options
112
- deprecated_option_keys = old_option_keys.filter { |_new_key, old_key| current.key?(old_key) }
118
+ def warn_deprecated_options(app_options)
119
+ deprecated_option_keys = new_option_keys.filter { |old_key| app_options.key?(old_key) }
113
120
  return if deprecated_option_keys.empty?
114
121
 
115
- deprecated_option_keys.each do |new_key, old_key|
122
+ deprecated_option_keys.each do |old_key, new_key|
116
123
  Shell.warn_deprecated("Option '#{old_key}' is deprecated, " \
117
124
  "please use '#{new_key}' instead (in 'controlplane.yml').")
118
125
  end
@@ -35,10 +35,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
35
35
 
36
36
  # image
37
37
 
38
- def image_build(image, dockerfile:, push: true)
39
- cmd = "cpln image build --org #{org} --name #{image} --dir #{config.app_dir} --dockerfile #{dockerfile}"
40
- cmd += " --push" if push
38
+ def image_build(image, dockerfile:, build_args: [], push: true)
39
+ cmd = "docker build -t #{image} -f #{dockerfile}"
40
+ build_args.each { |build_arg| cmd += " --build-arg #{build_arg}" }
41
+ cmd += " #{config.app_dir}"
41
42
  perform!(cmd)
43
+
44
+ image_push(image) if push
42
45
  end
43
46
 
44
47
  def image_query(app_name = config.app, org_name = config.org)
@@ -97,7 +100,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
97
100
  gvc_data = fetch_gvc(a_gvc)
98
101
  return gvc_data if gvc_data
99
102
 
100
- raise "Can't find GVC '#{gvc}', please create it with 'cpl setup gvc -a #{config.app}'."
103
+ raise "Can't find app '#{gvc}', please create it with 'cpl setup-app -a #{config.app}'."
101
104
  end
102
105
 
103
106
  def gvc_delete(a_gvc = gvc)
@@ -122,7 +125,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
122
125
  workload_data = fetch_workload(workload)
123
126
  return workload_data if workload_data
124
127
 
125
- raise "Can't find workload '#{workload}', please create it with 'cpl setup #{workload} -a #{config.app}'."
128
+ raise "Can't find workload '#{workload}', please create it with 'cpl apply-template #{workload} -a #{config.app}'."
129
+ end
130
+
131
+ def query_workloads(workload, partial_match: false)
132
+ op = partial_match ? "~" : "="
133
+
134
+ api.query_workloads(org: org, gvc: gvc, workload: workload, op_type: op)
126
135
  end
127
136
 
128
137
  def workload_get_replicas(workload, location:)
@@ -136,6 +145,10 @@ class Controlplane # rubocop:disable Metrics/ClassLength
136
145
  $CHILD_STATUS.success? ? YAML.safe_load(result) : nil
137
146
  end
138
147
 
148
+ def fetch_workload_deployments(workload)
149
+ api.workload_deployments(workload: workload, gvc: gvc, org: org)
150
+ end
151
+
139
152
  def workload_set_image_ref(workload, container:, image:)
140
153
  cmd = "cpln workload update #{workload} #{gvc_org}"
141
154
  cmd += " --set spec.containers.#{container}.image=/org/#{config.org}/image/#{image}"
@@ -143,10 +156,26 @@ class Controlplane # rubocop:disable Metrics/ClassLength
143
156
  perform!(cmd)
144
157
  end
145
158
 
146
- def workload_set_suspend(workload, value)
159
+ def set_workload_env_var(workload, container:, name:, value:)
160
+ data = fetch_workload!(workload)
161
+ data["spec"]["containers"].each do |container_data|
162
+ next unless container_data["name"] == container
163
+
164
+ container_data["env"].each do |env_data|
165
+ next unless env_data["name"] == name
166
+
167
+ env_data["value"] = value
168
+ end
169
+ end
170
+
171
+ api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
172
+ end
173
+
174
+ def set_workload_suspend(workload, value)
147
175
  data = fetch_workload!(workload)
148
176
  data["spec"]["defaultOptions"]["suspend"] = value
149
- apply(data)
177
+
178
+ api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
150
179
  end
151
180
 
152
181
  def workload_force_redeployment(workload)
@@ -155,15 +184,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
155
184
  perform!(cmd)
156
185
  end
157
186
 
158
- def workload_delete(workload)
159
- cmd = "cpln workload delete #{workload} #{gvc_org}"
160
- cmd += " 2> /dev/null"
161
- perform(cmd)
162
- end
163
-
164
- def workload_delete!(workload)
165
- cmd = "cpln workload delete #{workload} #{gvc_org}"
166
- perform!(cmd)
187
+ def delete_workload(workload)
188
+ api.delete_workload(org: org, gvc: gvc, workload: workload)
167
189
  end
168
190
 
169
191
  def workload_connect(workload, location:, container: nil, shell: nil)
@@ -180,6 +202,40 @@ class Controlplane # rubocop:disable Metrics/ClassLength
180
202
  perform!(cmd)
181
203
  end
182
204
 
205
+ # domain
206
+
207
+ def find_domain_route(data)
208
+ port = data["spec"]["ports"].find { |current_port| current_port["number"] == 80 || current_port["number"] == 443 }
209
+ return nil if port.nil? || port["routes"].nil?
210
+
211
+ route = port["routes"].find { |current_route| current_route["prefix"] == "/" }
212
+ return nil if route.nil?
213
+
214
+ route
215
+ end
216
+
217
+ def find_domain_for(workloads)
218
+ domains = api.list_domains(org: org)["items"]
219
+ domains.find do |domain_data|
220
+ route = find_domain_route(domain_data)
221
+ next false if route.nil?
222
+
223
+ workloads.any? { |workload| route["workloadLink"].split("/").last == workload }
224
+ end
225
+ end
226
+
227
+ def get_domain_workload(data)
228
+ route = find_domain_route(data)
229
+ route["workloadLink"].split("/").last
230
+ end
231
+
232
+ def set_domain_workload(data, workload)
233
+ route = find_domain_route(data)
234
+ route["workloadLink"] = "/org/#{org}/gvc/#{gvc}/workload/#{workload}"
235
+
236
+ api.update_domain(org: org, domain: data["name"], data: data)
237
+ end
238
+
183
239
  # logs
184
240
 
185
241
  def logs(workload:)
@@ -33,6 +33,29 @@ class ControlplaneApi
33
33
  api_json_direct("/logs/org/#{org}/loki/api/v1/query_range?#{params}", method: :get, host: :logs)
34
34
  end
35
35
 
36
+ def query_workloads(org:, gvc:, workload:, op_type:) # rubocop:disable Metrics/MethodLength
37
+ body = {
38
+ kind: "string",
39
+ spec: {
40
+ match: "all",
41
+ terms: [
42
+ {
43
+ rel: "gvc",
44
+ op: "=",
45
+ value: gvc
46
+ },
47
+ {
48
+ property: "name",
49
+ op: op_type,
50
+ value: workload
51
+ }
52
+ ]
53
+ }
54
+ }
55
+
56
+ api_json("/org/#{org}/workload/-query", method: :post, body: body)
57
+ end
58
+
36
59
  def workload_list(org:, gvc:)
37
60
  api_json("/org/#{org}/gvc/#{gvc}/workload", method: :get)
38
61
  end
@@ -45,10 +68,26 @@ class ControlplaneApi
45
68
  api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :get)
46
69
  end
47
70
 
71
+ def update_workload(org:, gvc:, workload:, data:)
72
+ api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :patch, body: data)
73
+ end
74
+
48
75
  def workload_deployments(org:, gvc:, workload:)
49
76
  api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}/deployment", method: :get)
50
77
  end
51
78
 
79
+ def delete_workload(org:, gvc:, workload:)
80
+ api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :delete)
81
+ end
82
+
83
+ def list_domains(org:)
84
+ api_json("/org/#{org}/domain", method: :get)
85
+ end
86
+
87
+ def update_domain(org:, domain:, data:)
88
+ api_json("/org/#{org}/domain/#{domain}", method: :patch, body: data)
89
+ end
90
+
52
91
  private
53
92
 
54
93
  # switch between cpln rest and api
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ControlplaneApiDirect
4
- API_METHODS = { get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put, delete: Net::HTTP::Delete }.freeze
4
+ API_METHODS = {
5
+ get: Net::HTTP::Get,
6
+ patch: Net::HTTP::Patch,
7
+ post: Net::HTTP::Post,
8
+ put: Net::HTTP::Put,
9
+ delete: Net::HTTP::Delete
10
+ }.freeze
5
11
  API_HOSTS = { api: "https://api.cpln.io", logs: "https://logs.cpln.io" }.freeze
6
12
 
7
13
  # API_TOKEN_REGEX = Regexp.union(
@@ -11,11 +17,12 @@ class ControlplaneApiDirect
11
17
 
12
18
  API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
13
19
 
14
- def call(url, method:, host: :api) # rubocop:disable Metrics/MethodLength
20
+ def call(url, method:, host: :api, body: nil) # rubocop:disable Metrics/MethodLength
15
21
  uri = URI("#{API_HOSTS[host]}#{url}")
16
22
  request = API_METHODS[method].new(uri)
17
23
  request["Content-Type"] = "application/json"
18
24
  request["Authorization"] = api_token
25
+ request.body = body.to_json if body
19
26
 
20
27
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
21
28
 
@@ -31,13 +38,16 @@ class ControlplaneApiDirect
31
38
  end
32
39
  end
33
40
 
41
+ # rubocop:disable Style/ClassVars
34
42
  def api_token
35
43
  return @@api_token if defined?(@@api_token)
36
44
 
37
- @@api_token = ENV.fetch("CPLN_TOKEN", `cpln profile token`.chomp) # rubocop:disable Style/ClassVars
45
+ @@api_token = ENV.fetch("CPLN_TOKEN", nil)
46
+ @@api_token = `cpln profile token`.chomp if @@api_token.nil?
38
47
  return @@api_token if @@api_token.match?(API_TOKEN_REGEX)
39
48
 
40
49
  raise "Unknown API token format. " \
41
50
  "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
42
51
  end
52
+ # rubocop:enable Style/ClassVars
43
53
  end
data/lib/core/scripts.rb CHANGED
@@ -10,7 +10,7 @@ module Scripts
10
10
  -H "Authorization: ${CONTROLPLANE_TOKEN}" -s | grep -o '"replicas":[0-9]*' | grep -o '[0-9]*')
11
11
 
12
12
  if [ "$REPLICAS_QTY" -gt 0 ]; then
13
- echo "-- MULTIPLE REPLICAS ATTEMPT !!!! replicas: $REPLICAS_QTY"
13
+ echo "-- MULTIPLE REPLICAS ATTEMPT: $REPLICAS_QTY --"
14
14
  exit -1
15
15
  fi
16
16
  SHELL
@@ -24,7 +24,7 @@ module Scripts
24
24
 
25
25
  # NOTE: please escape all '/' as '//' (as it is ruby interpolation here as well)
26
26
  def http_dummy_server_ruby
27
- 'require "socket";s=TCPServer.new(ENV["PORT"]);' \
27
+ 'require "socket";s=TCPServer.new(ENV["PORT"] || 80);' \
28
28
  'loop do c=s.accept;c.puts("HTTP/1.1 200 OK\\nContent-Length: 2\\n\\nOk");c.close end'
29
29
  end
30
30
 
data/lib/cpl/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpl
4
- VERSION = "0.6.0"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ bundle exec rake update_command_docs
3
+ git diff --exit-code || exit 1
@@ -0,0 +1,30 @@
1
+ kind: workload
2
+ name: daily-task
3
+ spec:
4
+ # https://docs.controlplane.com/reference/workload#cron-configuration
5
+ type: cron
6
+ job:
7
+ # Run daily job at 2am, see cron docs
8
+ schedule: 0 2 * * *
9
+ # Never or OnFailure
10
+ restartPolicy: Never
11
+ containers:
12
+ - name: daily-task
13
+ args:
14
+ - bundle
15
+ - exec
16
+ - rails
17
+ - db:prepare
18
+ cpu: 50m
19
+ inheritEnv: true
20
+ image: '/org/APP_ORG/image/APP_IMAGE'
21
+ memory: 256Mi
22
+ defaultOptions:
23
+ autoscaling:
24
+ maxScale: 1
25
+ capacityAI: false
26
+ firewallConfig:
27
+ external:
28
+ outboundAllowCIDR:
29
+ - 0.0.0.0/0
30
+ identityLink: /org/APP_ORG/gvc/APP_GVC/identity/APP_GVC-identity
@@ -0,0 +1,24 @@
1
+ kind: workload
2
+ name: maintenance
3
+ spec:
4
+ type: standard
5
+ containers:
6
+ - name: maintenance
7
+ env:
8
+ - name: PORT
9
+ value: '3000'
10
+ - name: PAGE_URL
11
+ value: ''
12
+ image: 'shakacode/maintenance-mode'
13
+ ports:
14
+ - number: 3000
15
+ protocol: http
16
+ defaultOptions:
17
+ autoscaling:
18
+ maxScale: 1
19
+ capacityAI: false
20
+ timeoutSeconds: 60
21
+ firewallConfig:
22
+ external:
23
+ inboundAllowCIDR:
24
+ - 0.0.0.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: 0.6.0
4
+ version: 1.0.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: 2023-04-27 00:00:00.000000000 Z
12
+ date: 2023-05-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -151,6 +151,48 @@ dependencies:
151
151
  - - "~>"
152
152
  - !ruby/object:Gem::Version
153
153
  version: 0.22.0
154
+ - !ruby/object:Gem::Dependency
155
+ name: timecop
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: 0.9.6
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: 0.9.6
168
+ - !ruby/object:Gem::Dependency
169
+ name: vcr
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: 6.1.0
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - "~>"
180
+ - !ruby/object:Gem::Version
181
+ version: 6.1.0
182
+ - !ruby/object:Gem::Dependency
183
+ name: webmock
184
+ requirement: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - "~>"
187
+ - !ruby/object:Gem::Version
188
+ version: 3.18.1
189
+ type: :development
190
+ prerelease: false
191
+ version_requirements: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - "~>"
194
+ - !ruby/object:Gem::Version
195
+ version: 3.18.1
154
196
  description: CLI for providing Heroku-like platform-as-a-service on Control Plane
155
197
  email:
156
198
  - justin@shakacode.com
@@ -160,6 +202,7 @@ executables:
160
202
  extensions: []
161
203
  extra_rdoc_files: []
162
204
  files:
205
+ - ".github/workflows/command_docs.yml"
163
206
  - ".github/workflows/rspec.yml"
164
207
  - ".github/workflows/rubocop.yml"
165
208
  - ".gitignore"
@@ -197,6 +240,10 @@ files:
197
240
  - lib/command/info.rb
198
241
  - lib/command/latest_image.rb
199
242
  - lib/command/logs.rb
243
+ - lib/command/maintenance.rb
244
+ - lib/command/maintenance_off.rb
245
+ - lib/command/maintenance_on.rb
246
+ - lib/command/maintenance_set_page.rb
200
247
  - lib/command/no_command.rb
201
248
  - lib/command/open.rb
202
249
  - lib/command/promote_app_from_upstream.rb
@@ -205,6 +252,7 @@ files:
205
252
  - lib/command/ps_start.rb
206
253
  - lib/command/ps_stop.rb
207
254
  - lib/command/run.rb
255
+ - lib/command/run_cleanup.rb
208
256
  - lib/command/run_detached.rb
209
257
  - lib/command/setup_app.rb
210
258
  - lib/command/test.rb
@@ -222,10 +270,13 @@ files:
222
270
  - lib/main.rb
223
271
  - rakelib/create_release.rake
224
272
  - script/add_command
225
- - script/generate_commands_docs
273
+ - script/check_command_docs
226
274
  - script/rename_command
275
+ - script/update_command_docs
276
+ - templates/daily-task.yml
227
277
  - templates/gvc.yml
228
278
  - templates/identity.yml
279
+ - templates/maintenance.yml
229
280
  - templates/memcached.yml
230
281
  - templates/postgres.yml
231
282
  - templates/rails.yml