cpl 2.0.2 → 2.2.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.
@@ -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.2.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.2.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-06-07 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}}