cpl 2.0.2 → 2.1.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
@@ -18,6 +18,8 @@ class Config # rubocop:disable Metrics/ClassLength
18
18
 
19
19
  ensure_required_options!
20
20
 
21
+ warn_deprecated_options
22
+
21
23
  Shell.verbose_mode(options[:verbose])
22
24
  trace_mode = options[:trace]
23
25
  return unless trace_mode
@@ -38,10 +40,34 @@ class Config # rubocop:disable Metrics/ClassLength
38
40
  current&.fetch(:name)
39
41
  end
40
42
 
43
+ def identity
44
+ "#{app}-identity"
45
+ end
46
+
47
+ def identity_link
48
+ "/org/#{org}/gvc/#{app}/identity/#{identity}"
49
+ end
50
+
51
+ def secrets
52
+ current&.dig(:secrets_name) || "#{app_prefix}-secrets"
53
+ end
54
+
55
+ def secrets_policy
56
+ current&.dig(:secrets_policy_name) || "#{secrets}-policy"
57
+ end
58
+
41
59
  def location
42
60
  @location ||= load_location_from_options || load_location_from_env || load_location_from_file
43
61
  end
44
62
 
63
+ def location_link
64
+ "/org/#{org}/location/#{location}"
65
+ end
66
+
67
+ def image_link(image)
68
+ "/org/#{org}/image/#{image}"
69
+ end
70
+
45
71
  def domain
46
72
  @domain ||= load_domain_from_options || load_domain_from_file
47
73
  end
@@ -84,6 +110,8 @@ class Config # rubocop:disable Metrics/ClassLength
84
110
  @apps ||= config[:apps].to_h do |app_name, app_options|
85
111
  ensure_config_app!(app_name, app_options)
86
112
 
113
+ check_deprecated_options(app_options)
114
+
87
115
  app_options_with_new_keys = app_options.to_h do |key, value|
88
116
  new_key = new_option_keys[key]
89
117
  new_key ? [new_key, value] : [key, value]
@@ -96,14 +124,7 @@ class Config # rubocop:disable Metrics/ClassLength
96
124
  def current
97
125
  return unless app
98
126
 
99
- @current ||= begin
100
- app_config = find_app_config(app)
101
- ensure_config_app!(app, app_config)
102
-
103
- warn_deprecated_options(app_config)
104
-
105
- app_config
106
- end
127
+ @current ||= find_app_config(app)
107
128
  end
108
129
 
109
130
  def app_matches?(app_name1, app_name2, app_options)
@@ -275,11 +296,18 @@ class Config # rubocop:disable Metrics/ClassLength
275
296
  strip_str_and_validate(current.fetch(:default_domain))
276
297
  end
277
298
 
278
- def warn_deprecated_options(app_options)
279
- deprecated_option_keys = new_option_keys.select { |old_key| app_options.key?(old_key) }
280
- return if deprecated_option_keys.empty?
299
+ def check_deprecated_options(app_options)
300
+ @deprecated_option_keys ||= {}
301
+
302
+ new_option_keys.each do |old_key, new_key|
303
+ @deprecated_option_keys[old_key] = new_key if app_options.key?(old_key)
304
+ end
305
+ end
306
+
307
+ def warn_deprecated_options
308
+ return if !@deprecated_option_keys || @deprecated_option_keys.empty?
281
309
 
282
- deprecated_option_keys.each do |old_key, new_key|
310
+ @deprecated_option_keys.each do |old_key, new_key|
283
311
  Shell.warn_deprecated("Option '#{old_key}' is deprecated, " \
284
312
  "please use '#{new_key}' instead (in 'controlplane.yml').")
285
313
  end
@@ -3,6 +3,8 @@
3
3
  class Controlplane # rubocop:disable Metrics/ClassLength
4
4
  attr_reader :config, :api, :gvc, :org
5
5
 
6
+ NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE"
7
+
6
8
  def initialize(config)
7
9
  @config = config
8
10
  @api = ControlplaneApi.new
@@ -37,6 +39,51 @@ class Controlplane # rubocop:disable Metrics/ClassLength
37
39
 
38
40
  # image
39
41
 
42
+ def latest_image(a_gvc = gvc, a_org = org, refresh: false)
43
+ @latest_image ||= {}
44
+ @latest_image[a_gvc] = nil if refresh
45
+ @latest_image[a_gvc] ||=
46
+ begin
47
+ items = query_images(a_gvc, a_org)["items"]
48
+ latest_image_from(items, app_name: a_gvc)
49
+ end
50
+ end
51
+
52
+ def latest_image_next(a_gvc = gvc, a_org = org, commit: nil)
53
+ commit ||= config.options[:commit]
54
+
55
+ @latest_image_next ||= {}
56
+ @latest_image_next[a_gvc] ||= begin
57
+ latest_image_name = latest_image(a_gvc, a_org)
58
+ image = latest_image_name.split(":").first
59
+ image += ":#{extract_image_number(latest_image_name) + 1}"
60
+ image += "_#{commit}" if commit
61
+ image
62
+ end
63
+ end
64
+
65
+ def latest_image_from(items, app_name: gvc, name_only: true)
66
+ matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") }
67
+
68
+ # Or special string to indicate no image available
69
+ if matching_items.empty?
70
+ name_only ? "#{app_name}:#{NO_IMAGE_AVAILABLE}" : nil
71
+ else
72
+ latest_item = matching_items.max_by { |item| extract_image_number(item["name"]) }
73
+ name_only ? latest_item["name"] : latest_item
74
+ end
75
+ end
76
+
77
+ def extract_image_number(image_name)
78
+ return 0 if image_name.end_with?(NO_IMAGE_AVAILABLE)
79
+
80
+ image_name.match(/:(\d+)/)&.captures&.first.to_i
81
+ end
82
+
83
+ def extract_image_commit(image_name)
84
+ image_name.match(/_(\h+)$/)&.captures&.first
85
+ end
86
+
40
87
  def query_images(a_gvc = gvc, a_org = org, partial_gvc_match: nil)
41
88
  partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
42
89
  gvc_op = partial_gvc_match ? "~" : "="
@@ -324,6 +371,12 @@ class Controlplane # rubocop:disable Metrics/ClassLength
324
371
  api.log_get(org: org, gvc: gvc, workload: workload, replica: replica, from: from, to: to)
325
372
  end
326
373
 
374
+ # secrets
375
+
376
+ def fetch_secret(secret)
377
+ api.fetch_secret(org: org, secret: secret)
378
+ end
379
+
327
380
  # identities
328
381
 
329
382
  def fetch_identity(identity, a_gvc = gvc)
@@ -52,7 +52,7 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
52
52
  # params << "direction=forward"
53
53
  params = params.map { |k, v| %(#{k}=#{CGI.escape(v)}) }.join("&")
54
54
 
55
- api_json_direct("/logs/org/#{org}/loki/api/v1/query_range?#{params}", method: :get, host: :logs)
55
+ api_json("/logs/org/#{org}/loki/api/v1/query_range?#{params}", method: :get, host: :logs)
56
56
  end
57
57
 
58
58
  def query_workloads(org:, gvc:, workload:, gvc_op_type:, workload_op_type:) # rubocop:disable Metrics/MethodLength
@@ -116,6 +116,14 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
116
116
  api_json("/org/#{org}/domain/#{domain}", method: :patch, body: data)
117
117
  end
118
118
 
119
+ def fetch_secret(org:, secret:)
120
+ api_json("/org/#{org}/secret/#{secret}", method: :get)
121
+ end
122
+
123
+ def delete_secret(org:, secret:)
124
+ api_json("/org/#{org}/secret/#{secret}", method: :delete)
125
+ end
126
+
119
127
  def fetch_identity(org:, gvc:, identity:)
120
128
  api_json("/org/#{org}/gvc/#{gvc}/identity/#{identity}", method: :get)
121
129
  end
@@ -124,6 +132,10 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
124
132
  api_json("/org/#{org}/policy/#{policy}", method: :get)
125
133
  end
126
134
 
135
+ def delete_policy(org:, policy:)
136
+ api_json("/org/#{org}/policy/#{policy}", method: :delete)
137
+ end
138
+
127
139
  private
128
140
 
129
141
  def fetch_query_pages(result)
@@ -152,13 +164,7 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
152
164
  result
153
165
  end
154
166
 
155
- # switch between cpln rest and api
156
167
  def api_json(...)
157
168
  ControlplaneApiDirect.new.call(...)
158
169
  end
159
-
160
- # only for api (where not impelemented in cpln rest)
161
- def api_json_direct(...)
162
- ControlplaneApiDirect.new.call(...)
163
- end
164
170
  end
@@ -98,7 +98,7 @@ class ControlplaneApiDirect
98
98
  end
99
99
 
100
100
  def refresh_api_token
101
- @@api_token[:token] = `cpln profile token`.chomp
101
+ @@api_token[:token] = Shell.cmd("cpln", "profile", "token")[:output].chomp
102
102
  end
103
103
 
104
104
  def self.reset_api_token
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ValidationError < StandardError; end
4
+
5
+ class DoctorService
6
+ attr_reader :config
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def run_validations(validations, silent_if_passing: false) # rubocop:disable Metrics/MethodLength
13
+ @any_failed_validation = false
14
+
15
+ validations.each do |validation|
16
+ case validation
17
+ when "config"
18
+ validate_config
19
+ when "templates"
20
+ validate_templates
21
+ else
22
+ raise ValidationError, Shell.color("ERROR: Invalid validation '#{validation}'.", :red)
23
+ end
24
+
25
+ progress.puts("#{Shell.color('[PASS]', :green)} #{validation}") unless silent_if_passing
26
+ rescue ValidationError => e
27
+ @any_failed_validation = true
28
+
29
+ progress.puts("#{Shell.color('[FAIL]', :red)} #{validation}\n\n#{e.message}\n\n")
30
+ end
31
+
32
+ exit(ExitCode::ERROR_DEFAULT) if @any_failed_validation
33
+ end
34
+
35
+ def validate_config
36
+ check_for_app_names_contained_in_others
37
+ end
38
+
39
+ def validate_templates
40
+ @template_parser = TemplateParser.new(config)
41
+ filenames = Dir.glob("#{@template_parser.template_dir}/*.yml")
42
+ templates = @template_parser.parse(filenames)
43
+
44
+ check_for_duplicate_templates(templates)
45
+ warn_deprecated_template_variables
46
+ end
47
+
48
+ private
49
+
50
+ def check_for_app_names_contained_in_others
51
+ app_names_contained_in_others = find_app_names_contained_in_others
52
+ return if app_names_contained_in_others.empty?
53
+
54
+ message = "App names contained in others found below. Please ensure that app names are unique."
55
+ list = app_names_contained_in_others
56
+ .map { |app_prefix, app_name| " - '#{app_prefix}' is a prefix of '#{app_name}'" }
57
+ .join("\n")
58
+ raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
59
+ end
60
+
61
+ def find_app_names_contained_in_others # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
62
+ app_names = config.apps.keys.map(&:to_s).sort
63
+ app_prefixes = config.apps
64
+ .select { |_, app_options| app_options[:match_if_app_name_starts_with] }
65
+ .keys
66
+ .map(&:to_s)
67
+ .sort
68
+ app_prefixes.each_with_object([]) do |app_prefix, app_names_contained_in_others|
69
+ app_names.each do |app_name|
70
+ if app_prefix != app_name && app_name.start_with?(app_prefix)
71
+ app_names_contained_in_others.push([app_prefix, app_name])
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def check_for_duplicate_templates(templates)
78
+ grouped_templates = templates.group_by { |template| [template["kind"], template["name"]] }
79
+ duplicate_templates = grouped_templates.select { |_, group| group.size > 1 }
80
+ return if duplicate_templates.empty?
81
+
82
+ message = "Duplicate templates found with the kind/names below. Please ensure that templates are unique."
83
+ list = duplicate_templates
84
+ .map { |(kind, name), _| " - kind: #{kind}, name: #{name}" }
85
+ .join("\n")
86
+ raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
87
+ end
88
+
89
+ def warn_deprecated_template_variables
90
+ deprecated_variables = @template_parser.deprecated_variables
91
+ return if deprecated_variables.empty?
92
+
93
+ message = "Please replace these variables in the templates, " \
94
+ "as support for them will be removed in a future major version bump:"
95
+ list = deprecated_variables
96
+ .map { |old_key, new_key| " - #{old_key} -> #{new_key}" }
97
+ .join("\n")
98
+ progress.puts("\n#{Shell.color("DEPRECATED: #{message}", :yellow)}\n#{list}\n\n")
99
+ end
100
+
101
+ def progress
102
+ $stderr
103
+ end
104
+ end
data/lib/core/helpers.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require "securerandom"
4
4
 
5
5
  module Helpers
6
+ module_function
7
+
6
8
  def strip_str_and_validate(str)
7
9
  return str if str.nil?
8
10
 
@@ -13,4 +15,12 @@ module Helpers
13
15
  def random_four_digits
14
16
  SecureRandom.random_number(1000..9999)
15
17
  end
18
+
19
+ def normalize_command_name(name)
20
+ name.to_s.tr("_", "-")
21
+ end
22
+
23
+ def normalize_option_name(name)
24
+ "--#{name.to_s.tr('_', '-')}"
25
+ end
16
26
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TemplateParser
4
+ attr_reader :config, :deprecated_variables
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def template_dir
11
+ "#{config.app_cpln_dir}/templates"
12
+ end
13
+
14
+ def template_filename(name)
15
+ "#{template_dir}/#{name}.yml"
16
+ end
17
+
18
+ def parse(filenames)
19
+ @deprecated_variables = {}
20
+
21
+ filenames.each_with_object([]) do |filename, templates|
22
+ yaml_file = File.read(filename)
23
+ yaml_file = replace_variables(yaml_file)
24
+
25
+ template_yamls = yaml_file.split(/^---\s*$/)
26
+ template_yamls.each do |template_yaml|
27
+ template = YAML.safe_load(template_yaml)
28
+ templates.push(template)
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def replace_variables(yaml_file) # rubocop:disable Metrics/MethodLength
36
+ yaml_file = yaml_file
37
+ .gsub("{{APP_ORG}}", config.org)
38
+ .gsub("{{APP_NAME}}", config.app)
39
+ .gsub("{{APP_LOCATION}}", config.location)
40
+ .gsub("{{APP_LOCATION_LINK}}", config.location_link)
41
+ .gsub("{{APP_IMAGE}}", cp.latest_image)
42
+ .gsub("{{APP_IMAGE_LINK}}", config.image_link(cp.latest_image))
43
+ .gsub("{{APP_IDENTITY}}", config.identity)
44
+ .gsub("{{APP_IDENTITY_LINK}}", config.identity_link)
45
+ .gsub("{{APP_SECRETS}}", config.secrets)
46
+ .gsub("{{APP_SECRETS_POLICY}}", config.secrets_policy)
47
+
48
+ find_deprecated_variables(yaml_file)
49
+
50
+ # Kept for backwards compatibility
51
+ yaml_file
52
+ .gsub("APP_ORG", config.org)
53
+ .gsub("APP_GVC", config.app)
54
+ .gsub("APP_LOCATION", config.location)
55
+ .gsub("APP_IMAGE", cp.latest_image)
56
+ end
57
+
58
+ def find_deprecated_variables(yaml_file)
59
+ new_variables.each do |old_key, new_key|
60
+ @deprecated_variables[old_key] = new_key if yaml_file.include?(old_key)
61
+ end
62
+ end
63
+
64
+ def new_variables
65
+ {
66
+ "APP_ORG" => "{{APP_ORG}}",
67
+ "APP_GVC" => "{{APP_NAME}}",
68
+ "APP_LOCATION" => "{{APP_LOCATION}}",
69
+ "APP_IMAGE" => "{{APP_IMAGE}}"
70
+ }
71
+ end
72
+
73
+ def cp
74
+ @cp ||= Controlplane.new(config)
75
+ end
76
+ 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 = "2.0.2"
4
+ VERSION = "2.1.0"
5
5
  MIN_CPLN_VERSION = "2.0.1"
6
6
  end
data/lib/cpl.rb CHANGED
@@ -58,6 +58,8 @@ module Cpl
58
58
  default_task :no_command
59
59
 
60
60
  def self.start(*args)
61
+ ENV["CPLN_SKIP_UPDATE_CHECK"] = "true"
62
+
61
63
  check_cpln_version
62
64
  check_cpl_version
63
65
  fix_help_option
@@ -177,6 +179,7 @@ module Cpl
177
179
  examples = command_class::EXAMPLES
178
180
  hide = command_class::HIDE || deprecated
179
181
  with_info_header = command_class::WITH_INFO_HEADER
182
+ validations = command_class::VALIDATIONS
180
183
 
181
184
  long_description += "\n#{examples}" if examples.length.positive?
182
185
 
@@ -198,9 +201,10 @@ module Cpl
198
201
 
199
202
  @commands_with_extra_options.push(name_for_method.to_sym) if accepts_extra_options
200
203
 
201
- define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/MethodLength
204
+ define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/BlockLength, Metrics/MethodLength
202
205
  if deprecated
203
- ::Shell.warn_deprecated("Command '#{command_key}' is deprecated, " \
206
+ normalized_old_name = ::Helpers.normalize_command_name(command_key)
207
+ ::Shell.warn_deprecated("Command '#{normalized_old_name}' is deprecated, " \
204
208
  "please use '#{name}' instead.")
205
209
  $stderr.puts
206
210
  end
@@ -222,6 +226,11 @@ module Cpl
222
226
 
223
227
  Cpl::Cli.show_info_header(config) if with_info_header
224
228
 
229
+ if validations.any? && ENV.fetch("DISABLE_VALIDATIONS", nil) != "true"
230
+ doctor = DoctorService.new(config)
231
+ doctor.run_validations(validations, silent_if_passing: true)
232
+ end
233
+
225
234
  command_class.new(config).call
226
235
  rescue RuntimeError => e
227
236
  ::Shell.abort(e.message)
@@ -235,14 +244,23 @@ module Cpl
235
244
  check_unknown_options!(except: @commands_with_extra_options)
236
245
  stop_on_unknown_option!
237
246
 
238
- def self.validate_options!(options)
247
+ def self.validate_options!(options) # rubocop:disable Metrics/MethodLength
239
248
  options.each do |name, value|
240
- raise "No value provided for option '#{name}'." if value.to_s.strip.empty?
249
+ normalized_name = ::Helpers.normalize_option_name(name)
250
+ raise "No value provided for option #{normalized_name}." if value.to_s.strip.empty?
251
+
252
+ option = ::Command::Base.all_options.find { |current_option| current_option[:name].to_s == name }
253
+ if option[:new_name]
254
+ normalized_new_name = ::Helpers.normalize_option_name(option[:new_name])
255
+ ::Shell.warn_deprecated("Option #{normalized_name} is deprecated, " \
256
+ "please use #{normalized_new_name} instead.")
257
+ $stderr.puts
258
+ end
241
259
 
242
- params = ::Command::Base.all_options.find { |option| option[:name].to_s == name }[:params]
260
+ params = option[:params]
243
261
  next unless params[:valid_regex]
244
262
 
245
- raise "Invalid value provided for option '#{name}'." unless value.match?(params[:valid_regex])
263
+ raise "Invalid value provided for option #{normalized_name}." unless value.match?(params[:valid_regex])
246
264
  end
247
265
  end
248
266
 
data/templates/app.yml CHANGED
@@ -11,8 +11,3 @@ spec:
11
11
  staticPlacement:
12
12
  locationLinks:
13
13
  - {{APP_LOCATION_LINK}}
14
- ---
15
- # Identity is needed to access secrets
16
- kind: identity
17
- name: {{APP_IDENTITY}}
18
-
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: 2.0.2
4
+ version: 2.1.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-05-18 00:00:00.000000000 Z
12
+ date: 2024-05-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -243,6 +243,7 @@ files:
243
243
  - docs/migrating.md
244
244
  - docs/postgres.md
245
245
  - docs/redis.md
246
+ - docs/secrets-and-env-values.md
246
247
  - docs/tips.md
247
248
  - docs/troubleshooting.md
248
249
  - examples/circleci.yml
@@ -256,6 +257,7 @@ files:
256
257
  - lib/command/copy_image_from_upstream.rb
257
258
  - lib/command/delete.rb
258
259
  - lib/command/deploy_image.rb
260
+ - lib/command/doctor.rb
259
261
  - lib/command/env.rb
260
262
  - lib/command/exists.rb
261
263
  - lib/command/generate.rb
@@ -283,10 +285,11 @@ files:
283
285
  - lib/core/config.rb
284
286
  - lib/core/controlplane.rb
285
287
  - lib/core/controlplane_api.rb
286
- - lib/core/controlplane_api_cli.rb
287
288
  - lib/core/controlplane_api_direct.rb
289
+ - lib/core/doctor_service.rb
288
290
  - lib/core/helpers.rb
289
291
  - lib/core/shell.rb
292
+ - lib/core/template_parser.rb
290
293
  - lib/cpl.rb
291
294
  - lib/cpl/version.rb
292
295
  - lib/deprecated_commands.json
@@ -310,7 +313,6 @@ files:
310
313
  - templates/rails.yml
311
314
  - templates/redis.yml
312
315
  - templates/redis2.yml
313
- - templates/secrets.yml
314
316
  - templates/sidekiq.yml
315
317
  homepage: https://github.com/shakacode/control-plane-flow
316
318
  licenses:
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class ControlplaneApiCli
4
- def call(url, method:)
5
- result = Shell.cmd("cpln", "rest", method, url, "-o", "json", capture_stderr: true)
6
- raise(result[:output]) unless result[:success]
7
-
8
- JSON.parse(result[:output])
9
- end
10
- end
@@ -1,11 +0,0 @@
1
- kind: secret
2
- name: {{APP_SECRETS}}
3
- type: dictionary
4
- data: {}
5
- ---
6
- # Policy is needed to allow identities to access secrets
7
- kind: policy
8
- name: {{APP_SECRETS_POLICY}}
9
- targetKind: secret
10
- targetLinks:
11
- - //secret/{{APP_SECRETS}}