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 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