deployhq 1.3.4 → 2.0.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 +4 -4
- data/bin/deployhq +1 -1
- data/lib/deploy.rb +23 -15
- data/lib/deploy/cli.rb +87 -115
- data/lib/deploy/cli/deployment_progress_output.rb +61 -0
- data/lib/deploy/cli/websocket_client.rb +144 -0
- data/lib/deploy/configuration.rb +23 -0
- data/lib/deploy/errors.rb +9 -7
- data/lib/deploy/request.rb +14 -14
- data/lib/deploy/{base.rb → resource.rb} +26 -26
- data/lib/deploy/resources/deployment.rb +51 -0
- data/lib/deploy/resources/deployment_step.rb +12 -0
- data/lib/deploy/resources/deployment_step_log.rb +7 -0
- data/lib/deploy/{project.rb → resources/project.rb} +1 -1
- data/lib/deploy/{server.rb → resources/server.rb} +6 -6
- data/lib/deploy/{server_group.rb → resources/server_group.rb} +6 -6
- data/lib/deploy/version.rb +1 -1
- metadata +26 -9
- data/lib/deploy/deployment.rb +0 -36
- data/lib/deploy/deployment_status_poll.rb +0 -34
- data/lib/deploy/deployment_tap.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da343b392a7afa950b197f8181112b114b05d6e0
|
4
|
+
data.tar.gz: 1b7b875b6bb5a77a4b9672836df13cdbdb7278bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c60eaf8aa6aeca85fb367d04071198c77abeabad94d72b757588153baf928e2fa4021af1585acad20bfeea345433e38a36ce6dcdcce8fe69aeedd0880ce9e98
|
7
|
+
data.tar.gz: 71a2b3e4179e7893bbb643b172f049125caa9c218f197cbcbed4df12a68e93209b73f1ef7a3b638f468e96168396220911ac3aa070f6eb5132580827238c40aa
|
data/bin/deployhq
CHANGED
data/lib/deploy.rb
CHANGED
@@ -11,25 +11,33 @@ require 'time'
|
|
11
11
|
## This is a ruby API library for the DeployHQ deployment platform.
|
12
12
|
|
13
13
|
require 'deploy/errors'
|
14
|
+
require 'deploy/configuration'
|
14
15
|
require 'deploy/request'
|
15
|
-
require 'deploy/
|
16
|
-
|
17
|
-
require 'deploy/project'
|
18
|
-
require 'deploy/deployment'
|
19
|
-
require 'deploy/
|
20
|
-
require 'deploy/
|
21
|
-
require 'deploy/server'
|
22
|
-
require 'deploy/server_group'
|
23
|
-
require 'deploy/version'
|
16
|
+
require 'deploy/resource'
|
17
|
+
|
18
|
+
require 'deploy/resources/project'
|
19
|
+
require 'deploy/resources/deployment'
|
20
|
+
require 'deploy/resources/deployment_step'
|
21
|
+
require 'deploy/resources/deployment_step_log'
|
22
|
+
require 'deploy/resources/server'
|
23
|
+
require 'deploy/resources/server_group'
|
24
24
|
|
25
|
+
require 'deploy/version'
|
25
26
|
|
26
27
|
module Deploy
|
27
28
|
class << self
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
def configure
|
30
|
+
@configuration ||= Configuration.new
|
31
|
+
yield @configuration if block_given?
|
32
|
+
@configuration
|
33
|
+
end
|
34
|
+
|
35
|
+
def configuration
|
36
|
+
@configuration ||= Configuration.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def configuration_file=(file_location)
|
40
|
+
@configuration = Configuration.from_file(file_location)
|
41
|
+
end
|
34
42
|
end
|
35
43
|
end
|
data/lib/deploy/cli.rb
CHANGED
@@ -1,61 +1,86 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
require 'highline/import'
|
3
|
+
|
3
4
|
require 'deploy'
|
5
|
+
require 'deploy/cli/websocket_client'
|
6
|
+
require 'deploy/cli/deployment_progress_output'
|
4
7
|
|
5
8
|
HighLine.colorize_strings
|
6
9
|
|
7
10
|
module Deploy
|
8
11
|
class CLI
|
9
|
-
|
10
12
|
## Constants for formatting output
|
11
|
-
TAP_COLOURS = {:info => :yellow, :error => :red, :success => :green}
|
12
13
|
PROTOCOL_NAME = {:ssh => "SSH/SFTP", :ftp => "FTP", :s3 => "Amazon S3", :rackspace => "Rackspace CloudFiles"}
|
13
14
|
|
14
|
-
class
|
15
|
-
|
15
|
+
class << self
|
16
|
+
def invoke(args)
|
17
|
+
@options = OpenStruct.new
|
18
|
+
|
19
|
+
parser = OptionParser.new do |opts|
|
20
|
+
opts.banner = "Usage: deployhq [options] command"
|
21
|
+
opts.separator ""
|
22
|
+
opts.separator "Commands:"
|
23
|
+
opts.separator "deploy\t\t Start a new deployment"
|
24
|
+
opts.separator "servers\t\t List configured servers and server groups"
|
25
|
+
opts.separator "configure\t\t Create a new configuration file for this tool"
|
26
|
+
opts.separator ""
|
27
|
+
opts.separator "Common Options:"
|
28
|
+
|
29
|
+
@options.config_file = './Deployfile'
|
30
|
+
opts.on("-c", "--config path", 'Configuration file path') do |config_file_path|
|
31
|
+
@options.config_file = config_file_path
|
32
|
+
end
|
16
33
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
puts "Couldn't find configuration file at #{@config_file_path}"
|
22
|
-
exit 1
|
23
|
-
end
|
34
|
+
opts.on("-p", "--project project",
|
35
|
+
"Project to operate on (default is read from project: in config file)") do |project_permalink|
|
36
|
+
@options.project = project_permalink
|
37
|
+
end
|
24
38
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
opts.on_tail('-v', '--version', "Shows Version") do
|
40
|
+
puts Deploy::VERSION
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on_tail("-h", "--help", "Displays Help") do
|
45
|
+
puts opts
|
46
|
+
exit
|
47
|
+
end
|
30
48
|
end
|
31
|
-
end
|
32
|
-
end
|
33
49
|
|
34
|
-
|
50
|
+
begin
|
51
|
+
parser.parse!(args)
|
52
|
+
command = args.pop
|
53
|
+
rescue OptionParser::InvalidOption
|
54
|
+
STDERR.puts parser.to_s
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
unless command == 'configure'
|
59
|
+
begin
|
60
|
+
Deploy.configuration_file = @options.config_file
|
61
|
+
rescue Errno::ENOENT
|
62
|
+
STDERR.puts "Couldn't find configuration file at #{@options.config_file.inspect}"
|
63
|
+
exit 1
|
64
|
+
end
|
35
65
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
opts.on("-c", "--config", 'Configuration file path') do |v|
|
41
|
-
options[:config_file] = v
|
66
|
+
project_permalink = @options.project || Deploy.configuration.project
|
67
|
+
if project_permalink.nil?
|
68
|
+
STDERR.puts "Project must be specified in config file or as --project argument"
|
69
|
+
exit 1
|
42
70
|
end
|
43
|
-
end.parse!
|
44
71
|
|
45
|
-
|
46
|
-
|
47
|
-
Deploy.email = @configuration.username
|
48
|
-
Deploy.api_key = @configuration.api_key
|
49
|
-
@project = Deploy::Project.find(@configuration.project)
|
72
|
+
@project = Deploy::Project.find(project_permalink)
|
73
|
+
end
|
50
74
|
|
51
|
-
case
|
75
|
+
case command
|
52
76
|
when 'deploy'
|
53
77
|
deploy
|
54
78
|
when 'servers'
|
55
79
|
server_list
|
80
|
+
when 'configure'
|
81
|
+
configure
|
56
82
|
else
|
57
|
-
puts
|
58
|
-
return
|
83
|
+
STDERR.puts parser.to_s
|
59
84
|
end
|
60
85
|
end
|
61
86
|
|
@@ -94,98 +119,45 @@ module Deploy
|
|
94
119
|
end
|
95
120
|
|
96
121
|
latest_revision = @project.latest_revision(parent.preferred_branch)
|
97
|
-
|
98
|
-
|
99
|
-
@server_names = @deployment.servers.each_with_object({}) do |server, hsh|
|
100
|
-
hsh[server['id']] = server['name']
|
101
|
-
end
|
102
|
-
@longest_server_name = @server_names.values.map(&:length).max
|
103
|
-
|
104
|
-
last_tap = nil
|
105
|
-
last_tap_lines = 0
|
106
|
-
current_status = 'pending'
|
107
|
-
previous_status = ''
|
108
|
-
STDOUT.print "Waiting for deployment capacity..."
|
109
|
-
while ['running', 'pending'].include?(current_status)
|
110
|
-
sleep 1
|
122
|
+
deployment = @project.deploy(parent.identifier, parent.last_revision, latest_revision)
|
111
123
|
|
112
|
-
|
113
|
-
|
114
|
-
# Status only gets returned from the API if it has changed
|
115
|
-
current_status = poll.status if poll.status
|
116
|
-
|
117
|
-
if current_status == 'pending'
|
118
|
-
STDOUT.print "."
|
119
|
-
elsif current_status == 'running' && previous_status == 'pending'
|
120
|
-
STDOUT.puts "\n"
|
121
|
-
end
|
122
|
-
|
123
|
-
if current_status != 'pending'
|
124
|
-
poll.taps.each do |tap|
|
125
|
-
# Delete most recent tap and redraw it if it's been updated
|
126
|
-
if tap.id.to_i == last_tap
|
127
|
-
if tap.updated
|
128
|
-
# Restore the cursor to the start of the last entry so we can overwrite
|
129
|
-
STDOUT.print "\e[#{last_tap_lines}A\r"
|
130
|
-
else
|
131
|
-
next
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
tap_output = format_tap(tap)
|
136
|
-
last_tap_lines = tap_output.count("\n")
|
137
|
-
last_tap = tap.id.to_i
|
138
|
-
|
139
|
-
STDOUT.print tap_output
|
140
|
-
STDOUT.flush
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
previous_status = current_status
|
145
|
-
end
|
124
|
+
STDOUT.print "Waiting for an available deployment slot..."
|
125
|
+
DeploymentProgressOutput.new(deployment).monitor
|
146
126
|
end
|
147
127
|
|
148
|
-
def
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
128
|
+
def configure
|
129
|
+
configuration = {
|
130
|
+
account: ask_config_question("Account Domain (e.g. atech.deployhq.com)", /\A[a-z0-9\.\-]+.deployhq.com\z/),
|
131
|
+
username: ask_config_question("Username or e-mail address"),
|
132
|
+
api_key: ask_config_question("API key (You can find this in Settings -> Security)"),
|
133
|
+
project: ask_config_question("Default project to use (please use permalink from web URL)")
|
134
|
+
}
|
154
135
|
|
155
|
-
|
156
|
-
|
136
|
+
confirmation = true
|
137
|
+
if File.exists?(@options.config_file)
|
138
|
+
confirmation = agree("File already exists at #{@options.config_file}. Overwrite? ")
|
157
139
|
end
|
158
|
-
end
|
159
|
-
|
160
|
-
## Data formatters
|
161
140
|
|
162
|
-
|
163
|
-
server_name = @server_names[tap.server_id]
|
141
|
+
return unless confirmation
|
164
142
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
server_name = ' '
|
170
|
-
end
|
143
|
+
file_data = JSON.pretty_generate(configuration)
|
144
|
+
File.write(@options.config_file, file_data)
|
145
|
+
say("File written to #{@options.config_file}")
|
146
|
+
end
|
171
147
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
s << "\n"
|
180
|
-
s << " " * server_name.length
|
181
|
-
s << " "
|
182
|
-
s << backend_line.color(text_colour).gsub(/\<[^\>]*\>/, '')
|
183
|
-
end
|
184
|
-
end
|
185
|
-
s << "\n"
|
148
|
+
def ask_config_question(question_text, valid_format = /.+/)
|
149
|
+
question_text = "#{question_text}: "
|
150
|
+
ask(question_text) do |q|
|
151
|
+
q.whitespace = :remove
|
152
|
+
q.responses[:not_valid] = "That answer is not valid"
|
153
|
+
q.responses[:ask_on_error] = :question
|
154
|
+
q.validate = valid_format
|
186
155
|
end
|
187
156
|
end
|
188
157
|
|
158
|
+
private
|
159
|
+
|
160
|
+
## Data formatters
|
189
161
|
def format_server(server)
|
190
162
|
server_params = {
|
191
163
|
"Name" => server.name,
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Deploy
|
2
|
+
class CLI
|
3
|
+
class DeploymentProgressOutput
|
4
|
+
SERVER_TAG_COLOURS = %w[32 33 34 35 36].cycle
|
5
|
+
|
6
|
+
attr_reader :deployment, :step_index, :server_tags
|
7
|
+
|
8
|
+
def initialize(deployment)
|
9
|
+
@deployment = deployment
|
10
|
+
|
11
|
+
@step_index = @deployment.steps.each_with_object({}) { |s, hsh| hsh[s.identifier] = s }
|
12
|
+
@server_tags = @deployment.servers.each_with_object({}) do |s, hsh|
|
13
|
+
hsh[s.id] = "\e[#{SERVER_TAG_COLOURS.next};1m[#{s.name}]\e[0m "
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def monitor
|
18
|
+
websocket_client = Deploy::CLI::WebsocketClient.new
|
19
|
+
|
20
|
+
subscription = websocket_client.subscribe('deployment', @deployment.identifier)
|
21
|
+
subscription.on('log-entry', &method(:handle_log_entry))
|
22
|
+
subscription.on('status-change', &method(:handle_status_change))
|
23
|
+
|
24
|
+
websocket_client.run
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def handle_log_entry(payload)
|
30
|
+
step = step_index[payload['step']]
|
31
|
+
server_tag = server_tags[step.server]
|
32
|
+
|
33
|
+
line = "\n"
|
34
|
+
line << server_tag if server_tag
|
35
|
+
line << payload['message']
|
36
|
+
|
37
|
+
if payload['detail']
|
38
|
+
padding_width = 0
|
39
|
+
padding_width += (server_tag.length - 11) if server_tag
|
40
|
+
padding = ' ' * padding_width
|
41
|
+
|
42
|
+
payload['detail'].split("\n").each do |detail_line|
|
43
|
+
line << "\n#{padding}| #{detail_line}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
STDOUT.print line
|
48
|
+
end
|
49
|
+
|
50
|
+
def handle_status_change(payload)
|
51
|
+
if payload['status'] == 'completed'
|
52
|
+
STDOUT.print "\nDeployment has finished successfully!\n"
|
53
|
+
elsif payload['status'] == 'failed'
|
54
|
+
STDOUT.print "\nDeployment has failed!\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
throw(:finished) if %w[completed failed].include?(payload['status'])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'websocket-eventmachine-client'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Deploy
|
5
|
+
class CLI
|
6
|
+
# Manages a connection and associated subscriptions to DeployHQ's websocket
|
7
|
+
class WebsocketClient
|
8
|
+
attr_reader :subscriptions
|
9
|
+
|
10
|
+
class Subscription
|
11
|
+
attr_reader :exchange, :routing_key
|
12
|
+
|
13
|
+
def initialize(exchange, routing_key)
|
14
|
+
@exchange = exchange
|
15
|
+
@routing_key = routing_key
|
16
|
+
@events = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def on(event, &block)
|
20
|
+
@events[event] ||= []
|
21
|
+
@events[event] << block
|
22
|
+
end
|
23
|
+
|
24
|
+
def dispatch(event, payload)
|
25
|
+
return unless @events[event]
|
26
|
+
|
27
|
+
@events[event].each do |block|
|
28
|
+
block.call(payload)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def subscribed?
|
33
|
+
@subscribed == true
|
34
|
+
end
|
35
|
+
|
36
|
+
def subscribed!
|
37
|
+
@subscribed = true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
@subscriptions = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def subscribe(exchange, routing_key)
|
46
|
+
key = subscription_key(exchange, routing_key)
|
47
|
+
subscriptions[key] ||= Subscription.new(exchange, routing_key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def run
|
51
|
+
catch(:finished) do
|
52
|
+
EM.run do
|
53
|
+
connection.onopen do
|
54
|
+
logger.info "connected"
|
55
|
+
end
|
56
|
+
|
57
|
+
connection.onmessage do |msg, type|
|
58
|
+
receive(msg)
|
59
|
+
end
|
60
|
+
|
61
|
+
connection.onclose do |code, reason|
|
62
|
+
logger.info "disconnected #{code} #{reason}"
|
63
|
+
reset_connection
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def dispatch(event, payload, mq = nil)
|
72
|
+
case event
|
73
|
+
when 'Welcome'
|
74
|
+
authenticate
|
75
|
+
when 'Authenticated'
|
76
|
+
request_subscriptions
|
77
|
+
when 'Subscribed'
|
78
|
+
successful_subscription(payload)
|
79
|
+
when 'Error', 'InternalError'
|
80
|
+
websocket_error(payload)
|
81
|
+
else
|
82
|
+
subscription_event(event, payload, mq) if mq
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def authenticate
|
87
|
+
send('Authenticate', api_key: Deploy.configuration.api_key)
|
88
|
+
end
|
89
|
+
|
90
|
+
def request_subscriptions
|
91
|
+
subscriptions.each do |_key, subscription|
|
92
|
+
send('Subscribe', exchange: subscription.exchange, routing_key: subscription.routing_key)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def successful_subscription(payload)
|
97
|
+
key = subscription_key(payload['exchange'], payload['routing_key'])
|
98
|
+
subscription = subscriptions[key]
|
99
|
+
subscription.subscribed! if subscription
|
100
|
+
end
|
101
|
+
|
102
|
+
def websocket_error(payload)
|
103
|
+
raise Deploy::Errors::WebsocketError, payload['error']
|
104
|
+
end
|
105
|
+
|
106
|
+
def subscription_event(event, payload, mq)
|
107
|
+
key = subscription_key(mq["e"], mq["rk"])
|
108
|
+
subscription = subscriptions[key]
|
109
|
+
subscription.dispatch(event, payload) if subscription
|
110
|
+
end
|
111
|
+
|
112
|
+
def receive(msg)
|
113
|
+
logger.debug "< #{msg}"
|
114
|
+
decoded = JSON.parse(msg)
|
115
|
+
dispatch(decoded['event'], decoded['payload'], decoded['mq'])
|
116
|
+
end
|
117
|
+
|
118
|
+
def send(action, payload = {})
|
119
|
+
msg = JSON.dump(action: action, payload: payload)
|
120
|
+
logger.debug "> #{msg}"
|
121
|
+
connection.send(msg)
|
122
|
+
end
|
123
|
+
|
124
|
+
def connection
|
125
|
+
uri = "#{Deploy.configuration.websocket_hostname}/pushwss"
|
126
|
+
@connection ||= WebSocket::EventMachine::Client.connect(uri: uri)
|
127
|
+
end
|
128
|
+
|
129
|
+
def reset_connection
|
130
|
+
@connection = nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def logger
|
134
|
+
@logger ||= Logger.new(STDOUT)
|
135
|
+
@logger.level = Logger::ERROR
|
136
|
+
@logger
|
137
|
+
end
|
138
|
+
|
139
|
+
def subscription_key(exchange, routing_key)
|
140
|
+
[exchange, routing_key].join('-')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Deploy
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :account, :username, :api_key, :project
|
4
|
+
attr_writer :websocket_hostname
|
5
|
+
|
6
|
+
def websocket_hostname
|
7
|
+
@websocket_hostname || 'wss://websocket.deployhq.com'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_file(path)
|
11
|
+
file_contents = File.read(path)
|
12
|
+
parsed_contents = JSON.parse(file_contents)
|
13
|
+
|
14
|
+
self.new.tap do |config|
|
15
|
+
config.account = parsed_contents['account']
|
16
|
+
config.username = parsed_contents['username']
|
17
|
+
config.api_key = parsed_contents['api_key']
|
18
|
+
config.project = parsed_contents['project']
|
19
|
+
config.websocket_hostname = parsed_contents['websocket_hostname']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/deploy/errors.rb
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
module Deploy
|
2
|
-
|
2
|
+
|
3
3
|
## Base level error which all other deploy errors will inherit from. It may also be
|
4
4
|
## invoked for errors which don't directly relate to other errors below.
|
5
5
|
class Error < StandardError; end
|
6
|
-
|
6
|
+
|
7
7
|
module Errors
|
8
|
-
|
8
|
+
|
9
9
|
## The service is currently unavailable. This may be caused by rate limiting or the API
|
10
10
|
## or the service has been disabled by the system
|
11
11
|
class ServiceUnavailable < Error; end
|
12
|
-
|
12
|
+
|
13
13
|
## Access was denied to the remote service
|
14
14
|
class AccessDenied < Error; end
|
15
|
-
|
15
|
+
|
16
16
|
## A communication error occured while talking to the Deploy API
|
17
17
|
class CommunicationError < Error; end
|
18
|
-
|
18
|
+
|
19
|
+
# Raised from the websocket client when we receive an error event
|
20
|
+
class WebsocketError < Error; end
|
19
21
|
end
|
20
|
-
end
|
22
|
+
end
|
data/lib/deploy/request.rb
CHANGED
@@ -1,36 +1,36 @@
|
|
1
1
|
module Deploy
|
2
2
|
class Request
|
3
|
-
|
3
|
+
|
4
4
|
attr_reader :path, :method
|
5
5
|
attr_accessor :data
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(path, method = :get)
|
8
8
|
@path = path
|
9
9
|
@method = method
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def success?
|
13
13
|
@success || false
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def output
|
17
17
|
@output || nil
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
## Make a request to the Deploy API using net/http. Data passed can be a hash or a string
|
21
21
|
## Hashes will be converted to JSON before being sent to the remote service.
|
22
22
|
def make
|
23
|
-
uri = URI.parse([Deploy.
|
23
|
+
uri = URI.parse([Deploy.configuration.account, @path].join('/'))
|
24
24
|
http_request = http_class.new(uri.request_uri)
|
25
|
-
http_request.basic_auth(Deploy.
|
25
|
+
http_request.basic_auth(Deploy.configuration.username, Deploy.configuration.api_key)
|
26
26
|
http_request["Accept"] = "application/json"
|
27
27
|
http_request["Content-type"] = "application/json"
|
28
|
-
|
28
|
+
|
29
29
|
http = Net::HTTP.new(uri.host, uri.port)
|
30
30
|
if uri.scheme == 'https'
|
31
31
|
http.use_ssl = true
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
data = self.data.to_json if self.data.is_a?(Hash) && self.data.respond_to?(:to_json)
|
35
35
|
http_result = http.request(http_request, data)
|
36
36
|
@output = http_result.body
|
@@ -50,10 +50,10 @@ module Deploy
|
|
50
50
|
end
|
51
51
|
self
|
52
52
|
end
|
53
|
-
|
53
|
+
|
54
54
|
private
|
55
|
-
|
56
|
-
def http_class
|
55
|
+
|
56
|
+
def http_class
|
57
57
|
case @method
|
58
58
|
when :post then Net::HTTP::Post
|
59
59
|
when :put then Net::HTTP::Put
|
@@ -62,6 +62,6 @@ module Deploy
|
|
62
62
|
Net::HTTP::Get
|
63
63
|
end
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
66
|
end
|
67
|
-
end
|
67
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Deploy
|
2
|
-
class
|
3
|
-
|
2
|
+
class Resource
|
3
|
+
|
4
4
|
## Store all attributes for the model we're working with.
|
5
5
|
attr_accessor :id, :attributes, :errors
|
6
|
-
|
6
|
+
|
7
7
|
## Pass any methods via. the attributes hash to see if they exist
|
8
8
|
## before resuming normal method_missing behaviour
|
9
9
|
def method_missing(method, *params)
|
@@ -16,9 +16,9 @@ module Deploy
|
|
16
16
|
self.attributes[key]
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
class << self
|
21
|
-
|
21
|
+
|
22
22
|
## Find a record or set of records. Passing :all will return all records and passing an integer
|
23
23
|
## will return the individual record for the ID passed.
|
24
24
|
def find(type, params = {})
|
@@ -27,7 +27,7 @@ module Deploy
|
|
27
27
|
else find_single(type, params)
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
## Find all objects and return an array of objects with the attributes set.
|
32
32
|
def find_all(params)
|
33
33
|
output = JSON.parse(Request.new(collection_path(params)).make.output)
|
@@ -39,7 +39,7 @@ module Deploy
|
|
39
39
|
create_object(o, params)
|
40
40
|
end
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
## Find a single object and return an object for it.
|
44
44
|
def find_single(id, params = {})
|
45
45
|
o = JSON.parse(Request.new(member_path(id, params)).make.output)
|
@@ -49,44 +49,44 @@ module Deploy
|
|
49
49
|
raise Deploy::Errors::NotFound, "Record not found"
|
50
50
|
end
|
51
51
|
end
|
52
|
-
|
52
|
+
|
53
53
|
## Post to the specified object on the collection path
|
54
54
|
def post(path)
|
55
55
|
Request.new(path.to_s, :post).make
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
## Return the collection path for this model. Very lazy pluralizion here
|
59
59
|
## at the moment, nothing in Deploy needs to be pluralized with anything
|
60
60
|
## other than just adding an 's'.
|
61
61
|
def collection_path(params = {})
|
62
62
|
class_name.downcase + 's'
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
## Return the member path for the passed ID & attributes
|
66
66
|
def member_path(id, params = {})
|
67
67
|
[collection_path, id].join('/')
|
68
68
|
end
|
69
|
-
|
69
|
+
|
70
70
|
## Return the deploy class name
|
71
71
|
def class_name
|
72
72
|
self.name.to_s.split('::').last.downcase
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
private
|
76
|
-
|
77
|
-
## Create a new object with the specified attributes and getting and ID.
|
76
|
+
|
77
|
+
## Create a new object with the specified attributes and getting and ID.
|
78
78
|
## Returns the newly created object
|
79
79
|
def create_object(attributes, objects = [])
|
80
80
|
o = self.new
|
81
81
|
o.attributes = attributes
|
82
82
|
o.id = attributes['id']
|
83
|
-
for key, object in objects.select{|k,v| v.kind_of?(Deploy::
|
83
|
+
for key, object in objects.select{|k,v| v.kind_of?(Deploy::Resource)}
|
84
84
|
o.attributes[key.to_s] = object
|
85
85
|
end
|
86
86
|
o
|
87
87
|
end
|
88
88
|
end
|
89
|
-
|
89
|
+
|
90
90
|
## Run a post on the member path. Returns the ouput from the post, false if a conflict or raises
|
91
91
|
## a Deploy::Error. Optionally, pass a second 'data' parameter to send data to the post action.
|
92
92
|
def post(action, data = nil)
|
@@ -95,17 +95,17 @@ module Deploy
|
|
95
95
|
request.data = data
|
96
96
|
request.make
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
## Delete this record from the remote service. Returns true or false depending on the success
|
100
100
|
## status of the destruction.
|
101
101
|
def destroy
|
102
102
|
Request.new(self.class.member_path(self.id, default_params), :delete).make.success?
|
103
103
|
end
|
104
|
-
|
104
|
+
|
105
105
|
def new_record?
|
106
106
|
self.id.nil?
|
107
107
|
end
|
108
|
-
|
108
|
+
|
109
109
|
def save
|
110
110
|
new_record? ? create : update
|
111
111
|
end
|
@@ -126,7 +126,7 @@ module Deploy
|
|
126
126
|
|
127
127
|
## Push the updated attributes to the remote. Returns true if the record was saved successfully
|
128
128
|
## other false if not. If not saved successfully, the errors hash will be updated with an array
|
129
|
-
## of all errors with the submission.
|
129
|
+
## of all errors with the submission.
|
130
130
|
def update
|
131
131
|
request = Request.new(self.class.member_path(self.id, default_params), :put)
|
132
132
|
request.data = {self.class.class_name.downcase.to_sym => attributes_to_post}
|
@@ -137,9 +137,9 @@ module Deploy
|
|
137
137
|
false
|
138
138
|
end
|
139
139
|
end
|
140
|
-
|
140
|
+
|
141
141
|
private
|
142
|
-
|
142
|
+
|
143
143
|
## Populate the errors hash from the given raw JSON output
|
144
144
|
def populate_errors(json)
|
145
145
|
self.errors = Hash.new
|
@@ -148,12 +148,12 @@ module Deploy
|
|
148
148
|
r
|
149
149
|
end
|
150
150
|
end
|
151
|
-
|
151
|
+
|
152
152
|
## An array of params which should always be sent with this instances requests
|
153
153
|
def default_params
|
154
154
|
Hash.new
|
155
155
|
end
|
156
|
-
|
156
|
+
|
157
157
|
## Attributes which can be passed for update & creation
|
158
158
|
def attributes_to_post
|
159
159
|
self.attributes.inject(Hash.new) do |r,(key,value)|
|
@@ -161,6 +161,6 @@ module Deploy
|
|
161
161
|
r
|
162
162
|
end
|
163
163
|
end
|
164
|
-
|
164
|
+
|
165
165
|
end
|
166
|
-
end
|
166
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Deploy
|
2
|
+
class Deployment < Resource
|
3
|
+
class << self
|
4
|
+
def collection_path(params = {})
|
5
|
+
"projects/#{params[:project].permalink}/deployments"
|
6
|
+
end
|
7
|
+
|
8
|
+
def member_path(id, params = {})
|
9
|
+
"projects/#{params[:project].permalink}/deployments/#{id}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def default_params
|
14
|
+
{:project => self.project}
|
15
|
+
end
|
16
|
+
|
17
|
+
def project
|
18
|
+
if self.attributes['project'].is_a?(Hash)
|
19
|
+
self.attributes['project'] = Project.send(:create_object, self.attributes['project'])
|
20
|
+
end
|
21
|
+
self.attributes['project']
|
22
|
+
end
|
23
|
+
|
24
|
+
def servers
|
25
|
+
if attributes['servers'].is_a?(Array)
|
26
|
+
@servers ||= attributes['servers'].map do |server_params|
|
27
|
+
Server.new.tap do |server|
|
28
|
+
server.id = server_params['id']
|
29
|
+
server.attributes = server_params
|
30
|
+
end
|
31
|
+
end
|
32
|
+
else
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def steps
|
38
|
+
if attributes['steps'].is_a?(Array)
|
39
|
+
@steps ||= attributes['steps'].map do |step_params|
|
40
|
+
DeploymentStep.new.tap do |step|
|
41
|
+
step.id = step_params['identifier']
|
42
|
+
step.attributes = step_params
|
43
|
+
step.attributes['deployment'] = self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
else
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Deploy
|
2
|
+
class DeploymentStep < Resource
|
3
|
+
def default_params
|
4
|
+
{ deployment: self.deployment, project: self.deployment.project }
|
5
|
+
end
|
6
|
+
|
7
|
+
def logs(params = {})
|
8
|
+
params = default_params.merge(step: self).merge(params)
|
9
|
+
DeploymentStepLog.find(:all, params)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module Deploy
|
2
|
-
class Server <
|
3
|
-
|
2
|
+
class Server < Resource
|
3
|
+
|
4
4
|
class << self
|
5
5
|
def collection_path(params = {})
|
6
6
|
"projects/#{params[:project].permalink}/servers"
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def member_path(id, params = {})
|
10
10
|
"projects/#{params[:project].permalink}/servers/#{identifier}"
|
11
11
|
end
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def default_params
|
15
15
|
{:project => self.project}
|
16
16
|
end
|
@@ -26,6 +26,6 @@ module Deploy
|
|
26
26
|
end
|
27
27
|
end.join(' ')
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
end
|
31
|
-
end
|
31
|
+
end
|
@@ -1,20 +1,20 @@
|
|
1
1
|
module Deploy
|
2
|
-
class ServerGroup <
|
3
|
-
|
2
|
+
class ServerGroup < Resource
|
3
|
+
|
4
4
|
class << self
|
5
5
|
def collection_path(params = {})
|
6
6
|
"projects/#{params[:project].permalink}/server_groups"
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def member_path(id, params = {})
|
10
10
|
"projects/#{params[:project].permalink}/server_groups/#{identifier}"
|
11
11
|
end
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def default_params
|
15
15
|
{:project => self.project}
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def servers
|
19
19
|
@servers ||= self.attributes['servers'].map {|server_attr| Deploy::Server.send(:create_object, server_attr) }
|
20
20
|
end
|
@@ -32,4 +32,4 @@ module Deploy
|
|
32
32
|
end
|
33
33
|
|
34
34
|
end
|
35
|
-
end
|
35
|
+
end
|
data/lib/deploy/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deployhq
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Wentworth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: websocket-eventmachine-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.2'
|
41
55
|
description: |2
|
42
56
|
API and CLI client for the DeployHQ deployment platform. Provides the
|
43
57
|
deployhq executable.
|
@@ -49,16 +63,19 @@ extra_rdoc_files: []
|
|
49
63
|
files:
|
50
64
|
- bin/deployhq
|
51
65
|
- lib/deploy.rb
|
52
|
-
- lib/deploy/base.rb
|
53
66
|
- lib/deploy/cli.rb
|
54
|
-
- lib/deploy/
|
55
|
-
- lib/deploy/
|
56
|
-
- lib/deploy/
|
67
|
+
- lib/deploy/cli/deployment_progress_output.rb
|
68
|
+
- lib/deploy/cli/websocket_client.rb
|
69
|
+
- lib/deploy/configuration.rb
|
57
70
|
- lib/deploy/errors.rb
|
58
|
-
- lib/deploy/project.rb
|
59
71
|
- lib/deploy/request.rb
|
60
|
-
- lib/deploy/
|
61
|
-
- lib/deploy/
|
72
|
+
- lib/deploy/resource.rb
|
73
|
+
- lib/deploy/resources/deployment.rb
|
74
|
+
- lib/deploy/resources/deployment_step.rb
|
75
|
+
- lib/deploy/resources/deployment_step_log.rb
|
76
|
+
- lib/deploy/resources/project.rb
|
77
|
+
- lib/deploy/resources/server.rb
|
78
|
+
- lib/deploy/resources/server_group.rb
|
62
79
|
- lib/deploy/version.rb
|
63
80
|
homepage: https://www.deployhq.com
|
64
81
|
licenses:
|
data/lib/deploy/deployment.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
module Deploy
|
2
|
-
class Deployment < Base
|
3
|
-
|
4
|
-
class << self
|
5
|
-
def collection_path(params = {})
|
6
|
-
"projects/#{params[:project].permalink}/deployments"
|
7
|
-
end
|
8
|
-
|
9
|
-
def member_path(id, params = {})
|
10
|
-
"projects/#{params[:project].permalink}/deployments/#{id}"
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
def default_params
|
15
|
-
{:project => self.project}
|
16
|
-
end
|
17
|
-
|
18
|
-
def project
|
19
|
-
if self.attributes['project'].is_a?(Hash)
|
20
|
-
self.attributes['project'] = Project.send(:create_object, self.attributes['project'])
|
21
|
-
end
|
22
|
-
self.attributes['project']
|
23
|
-
end
|
24
|
-
|
25
|
-
def taps(params={})
|
26
|
-
params = {:deployment => self, :project => self.project}.merge(params)
|
27
|
-
DeploymentTap.find(:all, params)
|
28
|
-
end
|
29
|
-
|
30
|
-
def status_poll(params = {})
|
31
|
-
params = {:deployment => self, :project => self.project}.merge(params)
|
32
|
-
DeploymentStatusPoll.poll(params)
|
33
|
-
end
|
34
|
-
|
35
|
-
end
|
36
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
module Deploy
|
2
|
-
class DeploymentStatusPoll
|
3
|
-
attr_accessor :attributes
|
4
|
-
|
5
|
-
def initialize(parsed_json)
|
6
|
-
self.attributes = parsed_json
|
7
|
-
end
|
8
|
-
|
9
|
-
def status
|
10
|
-
@status ||= attributes['status']
|
11
|
-
end
|
12
|
-
|
13
|
-
def taps
|
14
|
-
return [] unless attributes['taps']
|
15
|
-
@taps ||= attributes['taps'].map { |t| DeploymentTap.send(:create_object, t) }
|
16
|
-
end
|
17
|
-
|
18
|
-
class << self
|
19
|
-
def poll_url(params)
|
20
|
-
base = "projects/#{params[:project].permalink}/deployments/#{params[:deployment].identifier}/logs/poll"
|
21
|
-
base += "?status=#{params[:status]}"
|
22
|
-
base += "&since=#{params[:since]}" if params[:since]
|
23
|
-
base
|
24
|
-
end
|
25
|
-
|
26
|
-
def poll(params = {})
|
27
|
-
req = Request.new(poll_url(params)).make
|
28
|
-
parsed = JSON.parse(req.output)
|
29
|
-
|
30
|
-
new(parsed)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module Deploy
|
2
|
-
class DeploymentTap < Base
|
3
|
-
|
4
|
-
class << self
|
5
|
-
def collection_path(params = {})
|
6
|
-
base = "projects/#{params[:project].permalink}/deployments/#{params[:deployment].identifier}.js"
|
7
|
-
base += "?since=#{params[:since]}" if params[:since]
|
8
|
-
base
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
def default_params
|
13
|
-
{:deployment => self.deployment, :project => self.deployment.project}
|
14
|
-
end
|
15
|
-
|
16
|
-
end
|
17
|
-
end
|