Class: Harrison::Deploy

Inherits:
Base
  • Object
show all
Defined in:
lib/harrison/deploy.rb

Defined Under Namespace

Classes: Phase

Instance Attribute Summary collapse

Attributes inherited from Base

#options

Instance Method Summary collapse

Methods inherited from Base

#download, #exec, option_helper, #upload

Constructor Details

#initialize(opts = {}) ⇒ Deploy

Returns a new instance of Deploy.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/harrison/deploy.rb', line 15

def initialize(opts={})
  # Config helpers for Harrisonfile.
  self.class.option_helper(:hosts)
  self.class.option_helper(:env)
  self.class.option_helper(:base_dir)
  self.class.option_helper(:deploy_via)
  self.class.option_helper(:keep)
  self.class.option_helper(:confirm)

  # Command line opts for this action. Will be merged with common opts.
  arg_opts = [
    [ :hosts, "List of remote hosts to deploy to. Can also be specified in Harrisonfile.", :type => :strings ],
    [ :env, "Environment to deploy to. This can be examined in your Harrisonfile to calculate target hosts.", :type => :string ],
    [ :keep, "Number of recent deploys to keep after a successful deploy. (Including the most recent deploy.) Defaults to keeping all deploys forever.", :type => :integer ],
    [ :confirm, "Whether to interactively confirm the list of target hosts for deployment.", :type => :flag, :default => true ],
  ]

  super(arg_opts, opts)

  self.add_default_phases
end

Instance Attribute Details

#artifactObject

Returns the value of attribute artifact.



5
6
7
# File 'lib/harrison/deploy.rb', line 5

def artifact
  @artifact
end

Returns the value of attribute deploy_link.



8
9
10
# File 'lib/harrison/deploy.rb', line 8

def deploy_link
  @deploy_link
end

#hostObject

The specific host among –hosts that we are currently working on.



6
7
8
# File 'lib/harrison/deploy.rb', line 6

def host
  @host
end

#phasesObject

Returns the value of attribute phases.



11
12
13
# File 'lib/harrison/deploy.rb', line 11

def phases
  @phases
end

#release_dirObject

Returns the value of attribute release_dir.



7
8
9
# File 'lib/harrison/deploy.rb', line 7

def release_dir
  @release_dir
end

#rollbackObject

Returns the value of attribute rollback.



10
11
12
# File 'lib/harrison/deploy.rb', line 10

def rollback
  @rollback
end

Instance Method Details

#add_phase(name, &block) ⇒ Object



51
52
53
54
55
# File 'lib/harrison/deploy.rb', line 51

def add_phase(name, &block)
  @_phases ||= Hash.new

  @_phases[name] = Harrison::Deploy::Phase.new(name, &block)
end

#cleanup_deploys(limit) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/harrison/deploy.rb', line 173

def cleanup_deploys(limit)
  # Grab a list of deploys to be removed.
  purge_deploys = self.deploys.sort.reverse.slice(limit..-1) || []

  if purge_deploys.size > 0
    puts "[#{self.host}]   Purging #{purge_deploys.size} old deploys. (Keeping #{limit}...)"

    purge_deploys.each do |stale_deploy|
      remote_exec("cd deploys && rm -f #{stale_deploy}")
    end
  end
end

#cleanup_releasesObject



186
187
188
189
190
191
192
193
194
195
# File 'lib/harrison/deploy.rb', line 186

def cleanup_releases
  # Figure out which releases need to be kept.
  keep_releases = self.active_releases

  self.releases.each do |release|
    unless keep_releases.include?(release)
      remote_exec("cd releases && rm -rf #{release}")
    end
  end
end

#close(host = nil) ⇒ Object



197
198
199
200
201
202
203
204
205
# File 'lib/harrison/deploy.rb', line 197

def close(host=nil)
  if host
    @_conns[host].close if @_conns && @_conns[host]
  elsif @_conns
    @_conns.keys.each do |host|
      @_conns[host].close unless @_conns[host].closed?
    end
  end
end


61
62
63
# File 'lib/harrison/deploy.rb', line 61

def current_symlink
  "#{self.remote_project_dir}/current"
end

#invoke_user_blockObject



13
# File 'lib/harrison/deploy.rb', line 13

alias :invoke_user_block :run

#parse(args) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/harrison/deploy.rb', line 37

def parse(args)
  super

  # Preserve argv hosts if it's been passed.
  @_argv_hosts = self.hosts.dup if self.hosts

  self.rollback = args[0] == 'rollback'

  unless self.rollback
    # Make sure they passed an artifact.
    self.artifact = args[1] || abort("ERROR: You must specify the artifact to be deployed as an argument to this command.")
  end
end

#remote_exec(cmd) ⇒ Object



57
58
59
# File 'lib/harrison/deploy.rb', line 57

def remote_exec(cmd)
  super("cd #{remote_project_dir} && #{cmd}")
end


74
75
76
77
78
79
# File 'lib/harrison/deploy.rb', line 74

def revert_current_symlink
  # Restore current symlink to previous if set.
  if @_old_current
    self.remote_exec("ln -sfn #{@_old_current} #{self.current_symlink}")
  end
end

#runObject



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/harrison/deploy.rb', line 81

def run
  # Override Harrisonfile hosts if it was passed on argv.
  self.hosts = @_argv_hosts if @_argv_hosts

  if self.hosts.respond_to?(:call)
    resolved_hosts = self.hosts.call(self)
    self.hosts = resolved_hosts
  end

  # Require at least one host.
  if !self.hosts || self.hosts.empty?
    abort("ERROR: You must specify one or more hosts to deploy/rollback on, either in your Harrisonfile or via --hosts.")
  end

  if self.confirm
    self.hosts.each { |h| puts " - #{h}" }

    exit unless HighLine.new.agree("\nProceed with above-listed hosts?")

    puts ""
  end

  # Default to just built in deployment phases.
  self.phases ||= [ :upload, :extract, :link, :cleanup ]

  # Default base_dir.
  self.base_dir ||= '/opt'

  if self.rollback
    puts "Rolling back \"#{project}\" to previously deployed release on #{hosts.size} hosts...\n\n"

    # Find the prior deploy on the first host.
    self.host = hosts[0]
    last_deploy = self.deploys.sort.reverse[1] || abort("ERROR: No previous deploy to rollback to.")
    self.release_dir = remote_exec("cd deploys && readlink -vn #{last_deploy}")

    # No need to upload or extract for rollback.
    self.phases.delete(:upload)
    self.phases.delete(:extract)

    # Don't cleanup old deploys either.
    self.phases.delete(:cleanup)
  else
    puts "Deploying #{artifact} for \"#{project}\" onto #{hosts.size} hosts...\n\n"
    self.release_dir = "#{remote_project_dir}/releases/" + File.basename(artifact, '.tar.gz')
  end

  self.deploy_link = "#{remote_project_dir}/deploys/" + Time.new.utc.strftime('%Y-%m-%d_%H%M%S')

  progress_stack = []

  failed = catch(:failure) do
    self.phases.each do |phase_name|
      phase = @_phases[phase_name] || abort("ERROR: Could not resolve \"#{phase_name}\" as a deployment phase.")

      self.hosts.each do |host|
        self.host = host

        phase._run(self)

        # Track what phases we have completed on which hosts, in a stack.
        progress_stack << { host: host, phase: phase_name }
      end
    end

    # We want "failed" to be false if nothing was caught.
    false
  end

  if failed
    print "\n"

    progress_stack.reverse.each do |progress|
      self.host = progress[:host]
      phase = @_phases[progress[:phase]]

      # Don't let failures interrupt the rest of the process.
      catch(:failure) do
        phase._fail(self)
      end
    end

    abort "\nDeployment failed, previously completed deployment actions have been reverted."
  else
    if self.rollback
      puts "\nSucessfully rolled back #{project} on #{hosts.join(', ')}."
    else
      puts "\nSucessfully deployed #{artifact} to #{hosts.join(', ')}."
    end
  end
end


65
66
67
68
69
70
71
72
# File 'lib/harrison/deploy.rb', line 65

def update_current_symlink
  # Conditional assignment here makes this idempotent.
  @_old_current ||= self.remote_exec("if [ -L #{current_symlink} ]; then readlink -vn #{current_symlink}; fi")
  @_old_current = nil if @_old_current.empty?

  # Symlink current to new deploy.
  self.remote_exec("ln -sfn #{self.deploy_link} #{self.current_symlink}")
end