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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +60 -0
- data/.gitignore +14 -0
- data/.rspec +1 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +12 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +104 -0
- data/LICENSE +21 -0
- data/README.md +318 -0
- data/Rakefile +11 -0
- data/bin/cpl +6 -0
- data/cpl +15 -0
- data/cpl.gemspec +42 -0
- data/docs/commands.md +219 -0
- data/docs/troubleshooting.md +6 -0
- data/examples/circleci.yml +106 -0
- data/examples/controlplane.yml +44 -0
- data/lib/command/base.rb +177 -0
- data/lib/command/build_image.rb +25 -0
- data/lib/command/config.rb +33 -0
- data/lib/command/delete.rb +50 -0
- data/lib/command/env.rb +21 -0
- data/lib/command/exists.rb +23 -0
- data/lib/command/latest_image.rb +18 -0
- data/lib/command/logs.rb +29 -0
- data/lib/command/open.rb +33 -0
- data/lib/command/promote_image.rb +27 -0
- data/lib/command/ps.rb +40 -0
- data/lib/command/ps_restart.rb +34 -0
- data/lib/command/ps_start.rb +34 -0
- data/lib/command/ps_stop.rb +34 -0
- data/lib/command/run.rb +106 -0
- data/lib/command/run_detached.rb +148 -0
- data/lib/command/setup.rb +59 -0
- data/lib/command/test.rb +26 -0
- data/lib/core/config.rb +81 -0
- data/lib/core/controlplane.rb +128 -0
- data/lib/core/controlplane_api.rb +51 -0
- data/lib/core/controlplane_api_cli.rb +10 -0
- data/lib/core/controlplane_api_direct.rb +42 -0
- data/lib/core/scripts.rb +34 -0
- data/lib/cpl/version.rb +5 -0
- data/lib/cpl.rb +139 -0
- data/lib/main.rb +5 -0
- data/postgres.md +436 -0
- data/redis.md +112 -0
- data/script/generate_commands_docs +60 -0
- data/templates/gvc.yml +13 -0
- data/templates/identity.yml +2 -0
- data/templates/memcached.yml +23 -0
- data/templates/postgres.yml +31 -0
- data/templates/rails.yml +25 -0
- data/templates/redis.yml +20 -0
- data/templates/sidekiq.yml +28 -0
- 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,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
|
data/lib/core/scripts.rb
ADDED
@@ -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
|
data/lib/cpl/version.rb
ADDED
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
|