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
# File 'lib/harrison/deploy.rb', line 15

def initialize(opts={})
  # Config helpers for Harrisonfile.
  self.class.option_helper(:hosts)
  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 ],
    [ :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



49
50
51
52
53
# File 'lib/harrison/deploy.rb', line 49

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

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

#cleanup_deploys(limit) ⇒ Object



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

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



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

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



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

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


59
60
61
# File 'lib/harrison/deploy.rb', line 59

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



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

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



55
56
57
# File 'lib/harrison/deploy.rb', line 55

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


72
73
74
75
76
77
# File 'lib/harrison/deploy.rb', line 72

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



79
80
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
# File 'lib/harrison/deploy.rb', line 79

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


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

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