cpl 0.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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +60 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +16 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CONTRIBUTING.md +12 -0
  8. data/Gemfile +7 -0
  9. data/Gemfile.lock +104 -0
  10. data/LICENSE +21 -0
  11. data/README.md +318 -0
  12. data/Rakefile +11 -0
  13. data/bin/cpl +6 -0
  14. data/cpl +15 -0
  15. data/cpl.gemspec +42 -0
  16. data/docs/commands.md +219 -0
  17. data/docs/troubleshooting.md +6 -0
  18. data/examples/circleci.yml +106 -0
  19. data/examples/controlplane.yml +44 -0
  20. data/lib/command/base.rb +177 -0
  21. data/lib/command/build_image.rb +25 -0
  22. data/lib/command/config.rb +33 -0
  23. data/lib/command/delete.rb +50 -0
  24. data/lib/command/env.rb +21 -0
  25. data/lib/command/exists.rb +23 -0
  26. data/lib/command/latest_image.rb +18 -0
  27. data/lib/command/logs.rb +29 -0
  28. data/lib/command/open.rb +33 -0
  29. data/lib/command/promote_image.rb +27 -0
  30. data/lib/command/ps.rb +40 -0
  31. data/lib/command/ps_restart.rb +34 -0
  32. data/lib/command/ps_start.rb +34 -0
  33. data/lib/command/ps_stop.rb +34 -0
  34. data/lib/command/run.rb +106 -0
  35. data/lib/command/run_detached.rb +148 -0
  36. data/lib/command/setup.rb +59 -0
  37. data/lib/command/test.rb +26 -0
  38. data/lib/core/config.rb +81 -0
  39. data/lib/core/controlplane.rb +128 -0
  40. data/lib/core/controlplane_api.rb +51 -0
  41. data/lib/core/controlplane_api_cli.rb +10 -0
  42. data/lib/core/controlplane_api_direct.rb +42 -0
  43. data/lib/core/scripts.rb +34 -0
  44. data/lib/cpl/version.rb +5 -0
  45. data/lib/cpl.rb +139 -0
  46. data/lib/main.rb +5 -0
  47. data/postgres.md +436 -0
  48. data/redis.md +112 -0
  49. data/script/generate_commands_docs +60 -0
  50. data/templates/gvc.yml +13 -0
  51. data/templates/identity.yml +2 -0
  52. data/templates/memcached.yml +23 -0
  53. data/templates/postgres.yml +31 -0
  54. data/templates/rails.yml +25 -0
  55. data/templates/redis.yml +20 -0
  56. data/templates/sidekiq.yml +28 -0
  57. metadata +312 -0
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Controlplane
4
+ attr_reader :config, :api, :gvc, :org
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ @api = ControlplaneApi.new
9
+ @gvc = config.app
10
+ @org = config[:cpln_org]
11
+ end
12
+
13
+ # image
14
+
15
+ def image_build(image, dockerfile:, push: true)
16
+ cmd = "cpln image build --org #{org} --name #{image} --dir #{config.app_dir} --dockerfile #{dockerfile}"
17
+ cmd += " --push" if push
18
+ perform(cmd)
19
+ end
20
+
21
+ def image_query
22
+ cmd = "cpln image query --org #{org} -o yaml --max -1 --prop repository=#{config.app}"
23
+ perform_yaml(cmd)
24
+ end
25
+
26
+ def image_delete(image)
27
+ api.image_delete(org: org, image: image)
28
+ end
29
+
30
+ # gvc
31
+
32
+ def gvc_get(a_gvc = gvc)
33
+ api.gvc_get(gvc: a_gvc, org: org)
34
+ end
35
+
36
+ def gvc_delete(a_gvc = gvc)
37
+ api.gvc_delete(gvc: a_gvc, org: org)
38
+ end
39
+
40
+ # workload
41
+
42
+ def workload_get(workload)
43
+ api.workload_get(workload: workload, gvc: gvc, org: org)
44
+ end
45
+
46
+ def workload_get_replicas(workload, location:)
47
+ cmd = "cpln workload get-replicas #{workload} #{gvc_org} --location #{location} -o yaml"
48
+ perform_yaml(cmd)
49
+ end
50
+
51
+ def workload_set_image_ref(workload, container:, image:)
52
+ cmd = "cpln workload update #{workload} #{gvc_org}"
53
+ cmd += " --set spec.containers.#{container}.image=/org/#{config[:cpln_org]}/image/#{image}"
54
+ perform(cmd)
55
+ end
56
+
57
+ def workload_set_suspend(workload, value)
58
+ data = workload_get(workload)
59
+ data["spec"]["defaultOptions"]["suspend"] = value
60
+ apply(data)
61
+ end
62
+
63
+ def workload_force_redeployment(workload)
64
+ cmd = "cpln workload force-redeployment #{workload} #{gvc_org}"
65
+ perform(cmd)
66
+ end
67
+
68
+ def workload_delete(workload, no_raise: false)
69
+ cmd = "cpln workload delete #{workload} #{gvc_org}"
70
+ cmd += " 2> /dev/null" if no_raise
71
+ no_raise ? perform_no_raise(cmd) : perform(cmd)
72
+ end
73
+
74
+ def workload_connect(workload, location:, container: nil, shell: nil)
75
+ cmd = "cpln workload connect #{workload} #{gvc_org} --location #{location}"
76
+ cmd += " --container #{container}" if container
77
+ cmd += " --shell #{shell}" if shell
78
+ perform(cmd)
79
+ end
80
+
81
+ def workload_exec(workload, location:, container: nil, command: nil)
82
+ cmd = "cpln workload exec #{workload} #{gvc_org} --location #{location}"
83
+ cmd += " --container #{container}" if container
84
+ cmd += " -- #{command}"
85
+ perform(cmd)
86
+ end
87
+
88
+ # logs
89
+
90
+ def logs(workload:)
91
+ cmd = "cpln logs '{workload=\"#{workload}\"}' --org #{org} -t -o raw --limit 200"
92
+ perform(cmd)
93
+ end
94
+
95
+ def log_get(workload:, from:, to:)
96
+ api.log_get(org: org, gvc: gvc, workload: workload, from: from, to: to)
97
+ end
98
+
99
+ # apply
100
+
101
+ def apply(data)
102
+ Tempfile.create do |f|
103
+ f.write(data.to_yaml)
104
+ f.rewind
105
+ cmd = "cpln apply #{gvc_org} --file #{f.path} > /dev/null"
106
+ perform(cmd)
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def perform(cmd)
113
+ system(cmd) || exit(false)
114
+ end
115
+
116
+ def perform_no_raise(cmd)
117
+ system(cmd)
118
+ end
119
+
120
+ def perform_yaml(cmd)
121
+ result = `#{cmd}`
122
+ $?.success? ? YAML.safe_load(result) : exit(false) # rubocop:disable Style/SpecialGlobalVars
123
+ end
124
+
125
+ def gvc_org
126
+ "--gvc #{gvc} --org #{org}"
127
+ end
128
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ControlplaneApi
4
+ def gvc_get(org:, gvc:)
5
+ api_json("/org/#{org}/gvc/#{gvc}", method: :get)
6
+ end
7
+
8
+ def gvc_delete(org:, gvc:)
9
+ api_json("/org/#{org}/gvc/#{gvc}", method: :delete)
10
+ end
11
+
12
+ def image_delete(org:, image:)
13
+ api_json("/org/#{org}/image/#{image}", method: :delete)
14
+ end
15
+
16
+ def log_get(org:, gvc:, workload: nil, from: nil, to: nil)
17
+ query = { gvc: gvc }
18
+ query[:workload] = workload if workload
19
+ query = query.map { |k, v| %(#{k}="#{v}") }.join(",").then { "{#{_1}}" }
20
+
21
+ params = { query: query }
22
+ params[:from] = "#{from}000000000" if from
23
+ params[:to] = "#{to}000000000" if to
24
+ # params << "delay_for=0"
25
+ # params << "limit=30"
26
+ # params << "direction=forward"
27
+ params = params.map { |k, v| %(#{k}=#{CGI.escape(v)}) }.join("&")
28
+
29
+ api_json_direct("/logs/org/#{org}/loki/api/v1/query_range?#{params}", method: :get, host: :logs)
30
+ end
31
+
32
+ def workload_get(org:, gvc:, workload:)
33
+ api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :get)
34
+ end
35
+
36
+ def workload_deployments(org:, gvc:, workload:)
37
+ api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}/deployment", method: :get)
38
+ end
39
+
40
+ private
41
+
42
+ # switch between cpln rest and api
43
+ def api_json(...)
44
+ ControlplaneApiDirect.new.call(...)
45
+ end
46
+
47
+ # only for api (where not impelemented in cpln rest)
48
+ def api_json_direct(...)
49
+ ControlplaneApiDirect.new.call(...)
50
+ end
51
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ControlplaneApiCli
4
+ def call(url, method:)
5
+ response = `cpln rest #{method} #{url} -o json`
6
+ raise(response) unless $?.success? # rubocop:disable Style/SpecialGlobalVars
7
+
8
+ JSON.parse(response)
9
+ end
10
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ControlplaneApiDirect
4
+ API_METHODS = { get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put, delete: Net::HTTP::Delete }.freeze
5
+ API_HOSTS = { api: "https://api.cpln.io", logs: "https://logs.cpln.io" }.freeze
6
+
7
+ # API_TOKEN_REGEX = Regexp.union(
8
+ # /^[\w.]{155}$/, # CPLN_TOKEN format
9
+ # /^[\w\-._]{1134}$/ # 'cpln profile token' format
10
+ # ).freeze
11
+
12
+ API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
13
+
14
+ def call(url, method:, host: :api) # rubocop:disable Metrics/MethodLength
15
+ uri = URI("#{API_HOSTS[host]}#{url}")
16
+ request = API_METHODS[method].new(uri)
17
+ request["Content-Type"] = "application/json"
18
+ request["Authorization"] = api_token
19
+
20
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
21
+
22
+ case response
23
+ when Net::HTTPOK
24
+ JSON.parse(response.body)
25
+ when Net::HTTPAccepted
26
+ true
27
+ when Net::HTTPNotFound
28
+ nil
29
+ else
30
+ raise("#{response} #{response.body}")
31
+ end
32
+ end
33
+
34
+ def api_token
35
+ return @@api_token if defined?(@@api_token)
36
+
37
+ @@api_token = ENV.fetch("CPLN_TOKEN", `cpln profile token`.chomp) # rubocop:disable Style/ClassVars
38
+ return @@api_token if @@api_token.match?(API_TOKEN_REGEX)
39
+
40
+ abort("ERROR: Unknown API token format. Please re-run 'cpln profile login' or set correct CPLN_TOKEN env variable")
41
+ end
42
+ end
@@ -0,0 +1,34 @@
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: $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"]);' \
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpl
4
+ VERSION = "0.1.0"
5
+ end
data/lib/cpl.rb ADDED
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dotenv/load"
4
+ require "cgi"
5
+ require "json"
6
+ require "net/http"
7
+ require "pathname"
8
+ require "tempfile"
9
+ require "thor"
10
+ require "yaml"
11
+
12
+ modules = Dir["#{__dir__}/**/*.rb"].reject { |file| file == __FILE__ || file.end_with?("main.rb") }
13
+ modules.sort.each { require(_1) }
14
+
15
+ # Fix for https://github.com/erikhuda/thor/issues/398
16
+ # Copied from https://github.com/rails/thor/issues/398#issuecomment-622988390
17
+ class Thor
18
+ module Shell
19
+ class Basic
20
+ def print_wrapped(message, options = {})
21
+ indent = (options[:indent] || 0).to_i
22
+ if indent.zero?
23
+ stdout.puts(message)
24
+ else
25
+ message.each_line do |message_line|
26
+ stdout.print(" " * indent)
27
+ stdout.puts(message_line.chomp)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ module Cpl
36
+ class Error < StandardError; end
37
+
38
+ class Cli < Thor
39
+ package_name "cpl"
40
+
41
+ def self.start(*args)
42
+ fix_help_option
43
+
44
+ super(*args)
45
+ end
46
+
47
+ # This is so that we're able to run `cpl COMMAND --help` to print the help
48
+ # (it basically changes it to `cpl --help COMMAND`, which Thor recognizes)
49
+ # Based on https://stackoverflow.com/questions/49042591/how-to-add-help-h-flag-to-thor-command
50
+ def self.fix_help_option
51
+ help_mappings = Thor::HELP_MAPPINGS + ["help"]
52
+ matches = help_mappings & ARGV
53
+ matches.each do |match|
54
+ ARGV.delete(match)
55
+ ARGV.unshift(match)
56
+ end
57
+ end
58
+
59
+ # Needed to silence deprecation warning
60
+ def self.exit_on_failure?
61
+ true
62
+ end
63
+
64
+ # Needed to be able to use "run" as a command
65
+ def self.is_thor_reserved_word?(word, type) # rubocop:disable Naming/PredicateName
66
+ return false if word == "run"
67
+
68
+ super(word, type)
69
+ end
70
+
71
+ def self.deprecated_commands
72
+ {
73
+ build: ::Command::BuildImage,
74
+ promote: ::Command::PromoteImage,
75
+ runner: ::Command::RunDetached
76
+ }
77
+ end
78
+
79
+ def self.all_base_commands
80
+ ::Command::Base.all_commands.merge(deprecated_commands)
81
+ end
82
+
83
+ all_base_commands.each do |command_key, command_class| # rubocop:disable Metrics/BlockLength
84
+ deprecated = deprecated_commands[command_key]
85
+
86
+ name = command_class::NAME
87
+ name_for_method = deprecated ? command_key : name.tr("-", "_")
88
+ usage = command_class::USAGE.empty? ? name : command_class::USAGE
89
+ requires_args = command_class::REQUIRES_ARGS
90
+ default_args = command_class::DEFAULT_ARGS
91
+ command_options = command_class::OPTIONS
92
+ description = command_class::DESCRIPTION
93
+ long_description = command_class::LONG_DESCRIPTION
94
+ examples = command_class::EXAMPLES
95
+ hide = command_class::HIDE || deprecated
96
+
97
+ long_description += "\n#{examples}" if examples.length.positive?
98
+
99
+ # `handle_argument_error` does not exist in the context below,
100
+ # so we store it here to be able to use it
101
+ raise_args_error = ->(*args) { handle_argument_error(commands[name_for_method], ArgumentError, *args) }
102
+
103
+ desc(usage, description, hide: hide)
104
+ long_desc(long_description)
105
+
106
+ command_options.each do |option|
107
+ method_option(option[:name], **option[:params])
108
+ end
109
+
110
+ define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/MethodLength
111
+ if deprecated
112
+ logger = $stderr
113
+ logger.puts("DEPRECATED: command '#{command_key}' is deprecated, use '#{name}' instead\n")
114
+ end
115
+
116
+ args = if provided_args.length.positive?
117
+ provided_args
118
+ else
119
+ default_args
120
+ end
121
+
122
+ raise_args_error.call(args, nil) if (args.empty? && requires_args) || (!args.empty? && !requires_args)
123
+
124
+ config = Config.new(args, options)
125
+
126
+ command_class.new(config).call
127
+ end
128
+ rescue StandardError => e
129
+ logger = $stderr
130
+ logger.puts("Unable to load command: #{e.message}")
131
+ end
132
+ end
133
+ end
134
+
135
+ # nice Ctrl+C
136
+ trap "INT" do
137
+ puts
138
+ exit(1)
139
+ end
data/lib/main.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cpl"
4
+
5
+ Cpl::Cli.start