cpl 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,8 @@ module Command
4
4
  class Delete < Base
5
5
  NAME = "delete"
6
6
  OPTIONS = [
7
- app_option(required: true)
7
+ app_option(required: true),
8
+ skip_confirm_option
8
9
  ].freeze
9
10
  DESCRIPTION = "Deletes the whole app (GVC with all workloads and all images)"
10
11
  LONG_DESCRIPTION = <<~HEREDOC
@@ -13,10 +14,7 @@ module Command
13
14
  HEREDOC
14
15
 
15
16
  def call
16
- progress.puts "Type 'delete' to delete #{config.app} and images"
17
- progress.print "> "
18
-
19
- return progress.puts "Not confirmed" unless $stdin.gets.chomp == "delete"
17
+ return unless confirm_delete
20
18
 
21
19
  delete_gvc
22
20
  delete_images
@@ -24,6 +22,16 @@ module Command
24
22
 
25
23
  private
26
24
 
25
+ def confirm_delete
26
+ return true if config.options[:yes]
27
+
28
+ confirmed = Shell.confirm("Are you sure you want to delete '#{config.app}'?")
29
+ return false unless confirmed
30
+
31
+ progress.puts
32
+ true
33
+ end
34
+
27
35
  def delete_gvc
28
36
  progress.puts "- Deleting gvc:"
29
37
 
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Command
4
- class PromoteImage < Base
5
- NAME = "promote-image"
4
+ class DeployImage < Base
5
+ NAME = "deploy-image"
6
6
  OPTIONS = [
7
7
  app_option(required: true)
8
8
  ].freeze
9
- DESCRIPTION = "Promotes the latest image to app workloads"
9
+ DESCRIPTION = "Deploys the latest image to app workloads"
10
10
  LONG_DESCRIPTION = <<~HEREDOC
11
- - Promotes the latest image to app workloads
11
+ - Deploys the latest image to app workloads
12
12
  HEREDOC
13
13
 
14
14
  def call
15
15
  image = latest_image
16
16
 
17
17
  config[:app_workloads].each do |workload|
18
- cp.workload_get(workload).dig("spec", "containers").each do |container|
18
+ cp.workload_get_and_ensure(workload).dig("spec", "containers").each do |container|
19
19
  next unless container["image"].match?(%r{^/org/#{config[:cpln_org]}/image/#{config.app}:})
20
20
 
21
21
  cp.workload_set_image_ref(workload, container: container["name"], image: image)
data/lib/command/env.rb CHANGED
@@ -12,7 +12,7 @@ module Command
12
12
  HEREDOC
13
13
 
14
14
  def call
15
- cp.gvc_get.dig("spec", "env").map do |prop|
15
+ cp.gvc_get_and_ensure.dig("spec", "env").map do |prop|
16
16
  # NOTE: atm no special chars handling, consider adding if needed
17
17
  puts "#{prop['name']}=#{prop['value']}"
18
18
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Command
4
+ class NoCommand < Base
5
+ NAME = "no-command"
6
+ OPTIONS = [version_option].freeze
7
+ DESCRIPTION = "Called when no command was specified"
8
+ LONG_DESCRIPTION = <<~HEREDOC
9
+ - Called when no command was specified
10
+ HEREDOC
11
+ HIDE = true
12
+
13
+ def call
14
+ return unless config.options[:version]
15
+
16
+ Version.new(config).call
17
+ end
18
+ end
19
+ end
data/lib/command/open.rb CHANGED
@@ -23,7 +23,7 @@ module Command
23
23
 
24
24
  def call
25
25
  workload = config.options[:workload] || config[:one_off_workload]
26
- data = cp.workload_get(workload)
26
+ data = cp.workload_get_and_ensure(workload)
27
27
  url = data["status"]["endpoint"]
28
28
  opener = `which xdg-open open`.split("\n").grep_v("not found").first
29
29
 
data/lib/command/run.rb CHANGED
@@ -59,7 +59,7 @@ module Command
59
59
  progress.puts "- Cloning workload '#{workload}' on '#{config.options[:app]}' to '#{one_off}'"
60
60
 
61
61
  # Create a base copy of workload props
62
- spec = cp.workload_get(workload).fetch("spec")
62
+ spec = cp.workload_get_and_ensure(workload).fetch("spec")
63
63
  container = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
64
64
 
65
65
  # remove other containers if any
@@ -57,7 +57,7 @@ module Command
57
57
  progress.puts "- Cloning workload '#{workload}' on '#{config.options[:app]}' to '#{one_off}'"
58
58
 
59
59
  # Get base specs of workload
60
- spec = cp.workload_get(workload).fetch("spec")
60
+ spec = cp.workload_get_and_ensure(workload).fetch("spec")
61
61
  container = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
62
62
 
63
63
  # remove other containers if any
data/lib/command/setup.rb CHANGED
@@ -37,8 +37,7 @@ module Command
37
37
  def call
38
38
  config.args.each do |template|
39
39
  filename = "#{config.app_cpln_dir}/templates/#{template}.yml"
40
- abort("ERROR: Can't find template for '#{template}' at #{filename}") unless File.exist?(filename)
41
-
40
+ ensure_template(template, filename)
42
41
  apply_template(filename)
43
42
  progress.puts(template)
44
43
  end
@@ -46,6 +45,10 @@ module Command
46
45
 
47
46
  private
48
47
 
48
+ def ensure_template(template, filename)
49
+ Shell.abort("Can't find template '#{template}' at '#{filename}', please create it.") unless File.exist?(filename)
50
+ end
51
+
49
52
  def apply_template(filename)
50
53
  data = File.read(filename)
51
54
  .gsub("APP_GVC", config.app)
data/lib/command/test.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  # Be sure to have run: gem install debug
4
4
  require "debug"
5
5
 
6
- # rubocop:disable Lint/Debugger
7
6
  module Command
8
7
  class Test < Base
9
8
  NAME = "test"
@@ -23,4 +22,3 @@ module Command
23
22
  end
24
23
  end
25
24
  end
26
- # rubocop:enable Lint/Debugger
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Command
4
+ class Version < Base
5
+ NAME = "version"
6
+ DESCRIPTION = "Displays the current version of the CLI"
7
+ LONG_DESCRIPTION = <<~HEREDOC
8
+ - Displays the current version of the CLI
9
+ - Can also be done with `cpl --version` or `cpl -v`
10
+ HEREDOC
11
+
12
+ def call
13
+ puts Cpl::VERSION
14
+ end
15
+ end
16
+ end
data/lib/core/config.rb CHANGED
@@ -15,26 +15,19 @@ class Config
15
15
 
16
16
  load_app_config
17
17
  pick_current_config if app
18
+ warn_deprecated_options if current
18
19
  end
19
20
 
20
- def [](key) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
21
- abort("ERROR: should specify app") unless app
22
-
23
- logger = $stderr
21
+ def [](key)
22
+ ensure_current_config
24
23
 
24
+ old_key = old_option_keys[key]
25
25
  if current.key?(key)
26
26
  current.fetch(key)
27
- elsif key == :cpln_org && current.key?(:org)
28
- logger.puts("DEPRECATED: option 'org' is deprecated, use 'cpln_org' instead\n")
29
- current.fetch(:org)
30
- elsif key == :default_location && current.key?(:location)
31
- logger.puts("DEPRECATED: option 'location' is deprecated, use 'default_location' instead\n")
32
- current.fetch(:location)
33
- elsif key == :match_if_app_name_starts_with && current.key?(:prefix)
34
- logger.puts("DEPRECATED: option 'prefix' is deprecated, use 'match_if_app_name_starts_with' instead\n")
35
- current.fetch(:prefix)
27
+ elsif old_key && current.key?(old_key)
28
+ current.fetch(old_key)
36
29
  else
37
- abort("ERROR: should specify #{key} in controlplane.yml")
30
+ Shell.abort("Can't find option '#{key}' for app '#{app}' in 'controlplane.yml'.")
38
31
  end
39
32
  end
40
33
 
@@ -48,13 +41,37 @@ class Config
48
41
 
49
42
  private
50
43
 
44
+ def ensure_current_config
45
+ Shell.abort("Can't find current config, please specify an app.") unless current
46
+ end
47
+
48
+ def ensure_current_config_app(app)
49
+ Shell.abort("Can't find app '#{app}' in 'controlplane.yml'.") unless current
50
+ end
51
+
52
+ def ensure_config
53
+ Shell.abort("'controlplane.yml' is empty.") unless config
54
+ end
55
+
56
+ def ensure_config_apps
57
+ Shell.abort("Can't find key 'apps' in 'controlplane.yml'.") unless config[:apps]
58
+ end
59
+
60
+ def ensure_config_app(app, options)
61
+ Shell.abort("App '#{app}' is empty in 'controlplane.yml'.") unless options
62
+ end
63
+
51
64
  def pick_current_config
65
+ ensure_config
66
+ ensure_config_apps
52
67
  config[:apps].each do |c_app, c_data|
68
+ ensure_config_app(c_app, c_data)
53
69
  if c_app.to_s == app || (c_data[:match_if_app_name_starts_with] && app.start_with?(c_app.to_s))
54
70
  @current = c_data
55
71
  break
56
72
  end
57
73
  end
74
+ ensure_current_config_app(app)
58
75
  end
59
76
 
60
77
  def load_app_config
@@ -73,8 +90,24 @@ class Config
73
90
  path = path.parent
74
91
 
75
92
  if path.root?
76
- puts "ERROR: Can't find project config file, should be 'project_folder/#{CONFIG_FILE_LOCATIION}'"
77
- exit(-1)
93
+ Shell.abort("Can't find project config file at 'project_folder/#{CONFIG_FILE_LOCATIION}', please create it.")
94
+ end
95
+ end
96
+ end
97
+
98
+ def old_option_keys
99
+ {
100
+ cpln_org: :org,
101
+ default_location: :location,
102
+ match_if_app_name_starts_with: :prefix
103
+ }
104
+ end
105
+
106
+ def warn_deprecated_options
107
+ old_option_keys.each do |new_key, old_key|
108
+ if current.key?(old_key)
109
+ Shell.warn_deprecated("Option '#{old_key}' is deprecated, " \
110
+ "please use '#{new_key}' instead (in 'controlplane.yml').")
78
111
  end
79
112
  end
80
113
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Controlplane
3
+ class Controlplane # rubocop:disable Metrics/ClassLength
4
4
  attr_reader :config, :api, :gvc, :org
5
5
 
6
6
  def initialize(config)
@@ -18,8 +18,8 @@ class Controlplane
18
18
  perform(cmd)
19
19
  end
20
20
 
21
- def image_query
22
- cmd = "cpln image query --org #{org} -o yaml --max -1 --prop repository=#{config.app}"
21
+ def image_query(app_name = config.app)
22
+ cmd = "cpln image query --org #{org} -o yaml --max -1 --prop repository=#{app_name}"
23
23
  perform_yaml(cmd)
24
24
  end
25
25
 
@@ -29,10 +29,26 @@ class Controlplane
29
29
 
30
30
  # gvc
31
31
 
32
+ def gvc_query(app_name = config.app)
33
+ # When `match_if_app_name_starts_with` is `true`, we query for any gvc containing the name,
34
+ # otherwise we query for a gvc with the exact name.
35
+ op = config.current[:match_if_app_name_starts_with] ? "~" : "="
36
+
37
+ cmd = "cpln gvc query --org #{org} -o yaml --prop name#{op}#{app_name}"
38
+ perform_yaml(cmd)
39
+ end
40
+
32
41
  def gvc_get(a_gvc = gvc)
33
42
  api.gvc_get(gvc: a_gvc, org: org)
34
43
  end
35
44
 
45
+ def gvc_get_and_ensure(a_gvc = gvc)
46
+ gvc_data = gvc_get(a_gvc)
47
+ return gvc_data if gvc_data
48
+
49
+ Shell.abort("Can't find GVC '#{gvc}', please create it with 'cpl setup gvc -a #{config.app}'.")
50
+ end
51
+
36
52
  def gvc_delete(a_gvc = gvc)
37
53
  api.gvc_delete(gvc: a_gvc, org: org)
38
54
  end
@@ -43,6 +59,13 @@ class Controlplane
43
59
  api.workload_get(workload: workload, gvc: gvc, org: org)
44
60
  end
45
61
 
62
+ def workload_get_and_ensure(workload)
63
+ workload_data = workload_get(workload)
64
+ return workload_data if workload_data
65
+
66
+ Shell.abort("Can't find workload '#{workload}', please create it with 'cpl setup #{workload} -a #{config.app}'.")
67
+ end
68
+
46
69
  def workload_get_replicas(workload, location:)
47
70
  cmd = "cpln workload get-replicas #{workload} #{gvc_org} --location #{location} -o yaml"
48
71
  perform_yaml(cmd)
@@ -55,7 +78,7 @@ class Controlplane
55
78
  end
56
79
 
57
80
  def workload_set_suspend(workload, value)
58
- data = workload_get(workload)
81
+ data = workload_get_and_ensure(workload)
59
82
  data["spec"]["defaultOptions"]["suspend"] = value
60
83
  apply(data)
61
84
  end
@@ -37,6 +37,7 @@ class ControlplaneApiDirect
37
37
  @@api_token = ENV.fetch("CPLN_TOKEN", `cpln profile token`.chomp) # rubocop:disable Style/ClassVars
38
38
  return @@api_token if @@api_token.match?(API_TOKEN_REGEX)
39
39
 
40
- abort("ERROR: Unknown API token format. Please re-run 'cpln profile login' or set correct CPLN_TOKEN env variable")
40
+ Shell.abort("Unknown API token format. " \
41
+ "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable.")
41
42
  end
42
43
  end
data/lib/core/shell.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shell
4
+ def self.shell
5
+ @shell ||= Thor::Shell::Color.new
6
+ end
7
+
8
+ def self.stderr
9
+ @stderr ||= $stderr
10
+ end
11
+
12
+ def self.color(message, color_key)
13
+ shell.set_color(message, color_key)
14
+ end
15
+
16
+ def self.confirm(message)
17
+ shell.yes?("#{message} (y/n)")
18
+ end
19
+
20
+ def self.warn(message)
21
+ stderr.puts(color("WARNING: #{message}\n", :yellow))
22
+ end
23
+
24
+ def self.warn_deprecated(message)
25
+ stderr.puts(color("DEPRECATED: #{message}\n", :yellow))
26
+ end
27
+
28
+ def self.abort(message)
29
+ Kernel.abort(color("ERROR: #{message}\n", :red))
30
+ end
31
+ end
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.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/cpl.rb CHANGED
@@ -37,6 +37,7 @@ module Cpl
37
37
 
38
38
  class Cli < Thor
39
39
  package_name "cpl"
40
+ default_task :no_command
40
41
 
41
42
  def self.start(*args)
42
43
  fix_help_option
@@ -71,7 +72,8 @@ module Cpl
71
72
  def self.deprecated_commands
72
73
  {
73
74
  build: ::Command::BuildImage,
74
- promote: ::Command::PromoteImage,
75
+ promote: ::Command::DeployImage,
76
+ promote_image: ::Command::DeployImage,
75
77
  runner: ::Command::RunDetached
76
78
  }
77
79
  end
@@ -109,8 +111,8 @@ module Cpl
109
111
 
110
112
  define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/MethodLength
111
113
  if deprecated
112
- logger = $stderr
113
- logger.puts("DEPRECATED: command '#{command_key}' is deprecated, use '#{name}' instead\n")
114
+ ::Shell.warn_deprecated("Command '#{command_key}' is deprecated, " \
115
+ "please use '#{name}' instead.")
114
116
  end
115
117
 
116
118
  args = if provided_args.length.positive?
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ desc("Releases the gem package using the given version.
6
+
7
+ IMPORTANT: the gem version must be in valid rubygem format (no dashes).
8
+ This task depends on the gem-release ruby gem.
9
+
10
+ 1st argument: The new version in rubygem format (no dashes). Pass no argument to
11
+ automatically perform a patch version bump.
12
+ 2nd argument: Perform a dry run by passing 'true' as a second argument.
13
+
14
+ Example: `rake create_release[2.1.0,false]`")
15
+
16
+ task :create_release, %i[gem_version dry_run] do |_t, args|
17
+ args_hash = args.to_hash
18
+
19
+ is_dry_run = Release.object_to_boolean(args_hash[:dry_run])
20
+ gem_version = args_hash.fetch(:gem_version, "").strip
21
+ gem_root = Release.gem_root
22
+
23
+ Release.update_the_local_project
24
+ Release.ensure_there_is_nothing_to_commit
25
+ Release.sh_in_dir(gem_root,
26
+ "gem bump --no-commit #{gem_version == '' ? '' : %(--version #{gem_version})}")
27
+ Release.sh_in_dir(gem_root, "bundle install")
28
+ Release.sh_in_dir(gem_root, "git commit -am 'Bump version to #{gem_version}'")
29
+
30
+ # See https://github.com/svenfuchs/gem-release
31
+ Release.release_the_new_gem_version unless is_dry_run
32
+ end
33
+
34
+ module Release
35
+ extend FileUtils
36
+ class << self
37
+ def gem_root
38
+ File.expand_path("..", __dir__)
39
+ end
40
+
41
+ # Executes a string or an array of strings in a shell in the given directory in an unbundled environment
42
+ def sh_in_dir(dir, *shell_commands)
43
+ shell_commands.flatten.each { |shell_command| sh %(cd #{dir} && #{shell_command.strip}) }
44
+ end
45
+
46
+ def ensure_there_is_nothing_to_commit
47
+ status = `git status --porcelain`
48
+
49
+ return if $CHILD_STATUS.success? && status == ""
50
+
51
+ error = if $CHILD_STATUS.success?
52
+ "You have uncommitted code. Please commit or stash your changes before continuing"
53
+ else
54
+ "You do not have Git installed. Please install Git, and commit your changes before continuing"
55
+ end
56
+ raise(error)
57
+ end
58
+
59
+ def object_to_boolean(value)
60
+ [true, "true", "yes", 1, "1", "t"].include?(value.instance_of?(String) ? value.downcase : value)
61
+ end
62
+
63
+ def update_the_local_project
64
+ puts "Pulling latest commits from remote repository"
65
+
66
+ sh_in_dir(gem_root, "git pull --rebase")
67
+ raise "Failed in pulling latest changes from default remore repository." unless $CHILD_STATUS.success?
68
+ rescue Errno::ENOENT
69
+ raise "Ensure you have Git and Bundler installed before continuing."
70
+ end
71
+
72
+ def release_the_new_gem_version
73
+ puts "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
74
+ puts "Use the OTP for RubyGems!"
75
+ puts "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
76
+
77
+ sh_in_dir(gem_root, "gem release")
78
+ end
79
+ end
80
+ end
@@ -6,7 +6,9 @@ require_relative "../lib/cpl"
6
6
  commands_str_arr = []
7
7
 
8
8
  commands = Command::Base.all_commands
9
- commands.each do |_command_key, command_class|
9
+ commands.keys.sort.each do |command_key|
10
+ command_class = commands[command_key]
11
+
10
12
  next if command_class::HIDE
11
13
 
12
14
  name = command_class::NAME