deployhq 1.3.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 723ab3de0e1e230b74f7d7693ab21a364e10f740
4
- data.tar.gz: 7e1278b6f5a88dba69ee5bbb5bd77d07a76ae22a
3
+ metadata.gz: da343b392a7afa950b197f8181112b114b05d6e0
4
+ data.tar.gz: 1b7b875b6bb5a77a4b9672836df13cdbdb7278bd
5
5
  SHA512:
6
- metadata.gz: 44315effe285ba11ec9a8cf03676c58b1fc7a7ff380237933d155aee509e37bc602dada22df25fc3ff00d388ba13a15bbe546b8d763a1e2db3ca93de0fd676d1
7
- data.tar.gz: 75271d3366c67bbf22a1699caf048d7b5e32f84a0698507344213867d3ecbd28cada60dbdf1656f6e7a8d7d454ef921e38175599e66b2a3ed82083c76f52912d
6
+ metadata.gz: 3c60eaf8aa6aeca85fb367d04071198c77abeabad94d72b757588153baf928e2fa4021af1585acad20bfeea345433e38a36ce6dcdcce8fe69aeedd0880ce9e98
7
+ data.tar.gz: 71a2b3e4179e7893bbb643b172f049125caa9c218f197cbcbed4df12a68e93209b73f1ef7a3b638f468e96168396220911ac3aa070f6eb5132580827238c40aa
@@ -4,4 +4,4 @@ $LOAD_PATH.unshift(libdir)
4
4
 
5
5
  require 'deploy/cli'
6
6
 
7
- Deploy::CLI.invoke(*ARGV)
7
+ Deploy::CLI.invoke(ARGV)
@@ -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/base'
16
-
17
- require 'deploy/project'
18
- require 'deploy/deployment'
19
- require 'deploy/deployment_tap'
20
- require 'deploy/deployment_status_poll'
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
- ## Domain which you wish to access (e.g. atech.deployhq.com)
29
- attr_accessor :site
30
- ## E-Mail address you wish to authenticate with
31
- attr_accessor :email
32
- ## API key for the user you wish to authenticate with
33
- attr_accessor :api_key
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
@@ -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 Config
15
- AVAILABLE_CONFIG = [:account, :username, :api_key, :project]
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
- def initialize(config_file=nil)
18
- @config_file_path = config_file || File.join(Dir.pwd, 'Deployfile')
19
- @config = JSON.parse(File.read(@config_file_path))
20
- rescue Errno::ENOENT => e
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
- def method_missing(meth, *args, &block)
26
- if AVAILABLE_CONFIG.include?(meth.to_sym)
27
- @config[meth.to_s]
28
- else
29
- super
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
- class << self
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
- def invoke(*args)
37
- options = {}
38
- OptionParser.new do |opts|
39
- opts.banner = "Usage: deployhq [command]"
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
- @configuration = Config.new(options[:config_file])
46
- Deploy.site = @configuration.account
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 args[0]
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 "Usage: deployhq [command]"
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
- @deployment = @project.deploy(parent.identifier, parent.last_revision, latest_revision)
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
- poll = @deployment.status_poll(:since => last_tap, :status => current_status)
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 deployment
149
- @deployment = @project.deployments.first
150
- @server_names = @deployment.servers.each_with_object({}) do |obj, hsh|
151
- hsh[obj.delete("id")] = obj["name"]
152
- end
153
- @longest_server_name = @server_names.values.map(&:length).max
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
- @deployment.taps.reverse.each do |tap|
156
- STDOUT.puts format_tap(tap)
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
- def format_tap(tap)
163
- server_name = @server_names[tap.server_id]
141
+ return unless confirmation
164
142
 
165
- if server_name
166
- padding = (@longest_server_name - server_name.length) / 2.0
167
- server_name = "[#{' ' * padding.ceil} #{server_name} #{' ' * padding.floor}]"
168
- else
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
- text_colour = TAP_COLOURS[tap.tap_type.to_sym] || :white
173
-
174
- String.new.tap do |s|
175
- s << "#{server_name} ".color(text_colour, :bold)
176
- s << tap.message.color(text_colour).gsub(/\<[^\>]*\>/, '')
177
- if tap.backend_message && tap.tap_type == 'command'
178
- tap.backend_message.each_line('<br />') do |backend_line|
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
@@ -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
@@ -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.site, @path].join('/'))
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.email, Deploy.api_key)
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 Base
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::Base)}
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
@@ -0,0 +1,7 @@
1
+ module Deploy
2
+ class DeploymentStepLog < Resource
3
+ def self.collection_path(params = {})
4
+ "projects/#{params[:project].permalink}/deployments/#{params[:deployment].identifier}/steps/#{params[:step].identifier}/logs"
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  module Deploy
2
- class Project < Base
2
+ class Project < Resource
3
3
 
4
4
  ## Return all deployments for this project
5
5
  def deployments
@@ -1,16 +1,16 @@
1
1
  module Deploy
2
- class Server < Base
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 < Base
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
@@ -1,3 +1,3 @@
1
1
  module Deploy
2
- VERSION = "1.3.4"
2
+ VERSION = "2.0.0"
3
3
  end
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: 1.3.4
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-08-16 00:00:00.000000000 Z
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/deployment.rb
55
- - lib/deploy/deployment_status_poll.rb
56
- - lib/deploy/deployment_tap.rb
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/server.rb
61
- - lib/deploy/server_group.rb
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:
@@ -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