cpl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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