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