harrison 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +137 -0
- data/Rakefile +7 -0
- data/TODO +26 -0
- data/bin/harrison +7 -0
- data/harrison.gemspec +32 -0
- data/lib/harrison/base.rb +101 -0
- data/lib/harrison/config.rb +9 -0
- data/lib/harrison/deploy.rb +110 -0
- data/lib/harrison/package.rb +87 -0
- data/lib/harrison/ssh.rb +80 -0
- data/lib/harrison/version.rb +3 -0
- data/lib/harrison.rb +91 -0
- data/spec/fixtures/Harrisonfile +34 -0
- data/spec/fixtures/eval_script.rb +1 -0
- data/spec/fixtures/nested/.gitkeep +0 -0
- data/spec/spec_helper.rb +83 -0
- data/spec/unit/harrison/base_spec.rb +221 -0
- data/spec/unit/harrison/deploy_spec.rb +181 -0
- data/spec/unit/harrison/package_spec.rb +127 -0
- data/spec/unit/harrison/ssh_spec.rb +5 -0
- data/spec/unit/harrison_spec.rb +157 -0
- metadata +212 -0
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
.DS_Store
|
2
|
+
*.gem
|
3
|
+
*.rbc
|
4
|
+
.bundle
|
5
|
+
.config
|
6
|
+
.yardoc
|
7
|
+
Gemfile.lock
|
8
|
+
InstalledFiles
|
9
|
+
_yardoc
|
10
|
+
coverage
|
11
|
+
doc/
|
12
|
+
lib/bundler/man
|
13
|
+
pkg
|
14
|
+
rdoc
|
15
|
+
spec/reports
|
16
|
+
test/tmp
|
17
|
+
test/version_tmp
|
18
|
+
tmp
|
19
|
+
*.bundle
|
20
|
+
*.so
|
21
|
+
*.o
|
22
|
+
*.a
|
23
|
+
mkmf.log
|
24
|
+
/Harrisonfile
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jesse Scott
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# Harrison
|
2
|
+
|
3
|
+
Simple artifact-based deployment for web applications.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'harrison'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install harrison
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
First, create a Harrisonfile in the root of your project. Here's an example:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# Project-wide Config
|
25
|
+
Harrison.config do |h|
|
26
|
+
h.project = 'harrison'
|
27
|
+
h.git_src = "git@github.com:scotje/harrison.git"
|
28
|
+
end
|
29
|
+
|
30
|
+
Harrison.package do |h|
|
31
|
+
# Where to build package.
|
32
|
+
h.host = 'build-server.example.com'
|
33
|
+
h.user = 'jesse'
|
34
|
+
|
35
|
+
# Things we don't want to package.
|
36
|
+
h.exclude = %w(.git ./config ./coverage ./examples ./log ./pkg ./tmp ./spec)
|
37
|
+
|
38
|
+
# Define the build process here.
|
39
|
+
h.run do |h|
|
40
|
+
# Bundle Install
|
41
|
+
h.remote_exec("cd #{h.commit} && bash -l -c \"bundle install --path=vendor --without=\\\"development packaging test doc\\\"\"")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Harrison.deploy do |h|
|
46
|
+
h.hosts = [ 'util-server-01.example.com', 'app-server-01.example.com', 'app-server-02.example.com' ]
|
47
|
+
h.user = 'jesse'
|
48
|
+
h.base_dir = '/opt'
|
49
|
+
|
50
|
+
# Run block will be invoked once for each host after new code is in place.
|
51
|
+
h.run do |h|
|
52
|
+
# You can interrogate h.host to see what host you are currently running on.
|
53
|
+
if h.host =~ /util/
|
54
|
+
# Do something on the util box.
|
55
|
+
else
|
56
|
+
puts "Reloading Unicorn on #{h.host}..."
|
57
|
+
h.remote_exec("sudo -- /etc/init.d/unicorn_#{h.project} reload")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Next, ensure that your SSH key is authorized to log in as the `user` you have specified in
|
64
|
+
the Harrisonfile for each task. (Or be ready to type the password a lot. :weary:)
|
65
|
+
|
66
|
+
### Building a Release
|
67
|
+
|
68
|
+
Use the `harrison package` command:
|
69
|
+
|
70
|
+
```
|
71
|
+
$ harrison package
|
72
|
+
```
|
73
|
+
|
74
|
+
By default this will build and package `HEAD` of your current branch. You may specify another commit to
|
75
|
+
build using the `--commit` option:
|
76
|
+
|
77
|
+
```
|
78
|
+
$ harrison package --commit mybranch
|
79
|
+
```
|
80
|
+
|
81
|
+
The `--commit` option understands anything that `git rev-parse` understands. *NOTE: The commit you
|
82
|
+
reference must be pushed to the repository referenced as `git_src` in the Harrisonfile before
|
83
|
+
you can build it.*
|
84
|
+
|
85
|
+
The packaged release artifact will, by default, be saved into a 'pkg' subfolder:
|
86
|
+
|
87
|
+
```
|
88
|
+
$ harrison package
|
89
|
+
Packaging 5a547d8 for "harrison" on build-server.example.com...
|
90
|
+
Sucessfully packaged 5a547d8 to pkg/20140711170226-5a547d8.tar.gz
|
91
|
+
```
|
92
|
+
|
93
|
+
There are some additional options available, run `harrison package --help` to see everything available.
|
94
|
+
|
95
|
+
|
96
|
+
### Deploying a Release
|
97
|
+
|
98
|
+
Use the `harrison deploy` command passing the artifact to be deployed as an argument:
|
99
|
+
|
100
|
+
```
|
101
|
+
$ harrison deploy pkg/20140711170226-5a547d8.tar.gz
|
102
|
+
```
|
103
|
+
|
104
|
+
By default, this will deploy to the list of hosts defined in your Harrisonfile.
|
105
|
+
|
106
|
+
You can override the target hosts by passing a `--hosts` option:
|
107
|
+
|
108
|
+
```
|
109
|
+
$ harrison deploy pkg/20140711170226-5a547d8.tar.gz --hosts test-app-server-01.example.com test-app-server-02.example.com
|
110
|
+
```
|
111
|
+
|
112
|
+
You can also pass an `--env` option to deploy into multi-stage environments:
|
113
|
+
|
114
|
+
```
|
115
|
+
$ harrison deploy pkg/20140711170226-5a547d8.tar.gz --env prod
|
116
|
+
```
|
117
|
+
|
118
|
+
This value can then be tested to alter the default target hosts in your Harrisonfile:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
if h.env =~ /prod/
|
122
|
+
h.hosts = [ 'app-server-01.prod.example.com', 'app-server-02.prod.example.com' ]
|
123
|
+
else
|
124
|
+
h.hosts = [ 'app-server-01.stage.example.com', 'app-server-02.stage.example.com' ]
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
There are some additional options available, run `harrison deploy --help` to see everything available.
|
129
|
+
|
130
|
+
|
131
|
+
## Contributing
|
132
|
+
|
133
|
+
1. Fork it ( https://github.com/scotje/harrison/fork )
|
134
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
135
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
136
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
137
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/TODO
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Bugs:
|
2
|
+
-----
|
3
|
+
[ ] Test that artifact exists before doing anything for deploy task.
|
4
|
+
|
5
|
+
In Progress:
|
6
|
+
------------
|
7
|
+
[ ] Unit Testing
|
8
|
+
|
9
|
+
Definitely:
|
10
|
+
-----------
|
11
|
+
[ ] Rollback action.
|
12
|
+
[ ] Error handling (with automatic rollback/undo)
|
13
|
+
[ ] Further de-compose actions so that specific parts can be easily overridden.
|
14
|
+
[ ] When deploying via a proxy, cache artifact on proxy before deploying to hosts.
|
15
|
+
[ ] Banner text in --help output.
|
16
|
+
[ ] Improve unit testing, try to minimize coupling to implementation details.
|
17
|
+
[ ] Include branch name in artifact file name.
|
18
|
+
|
19
|
+
Maybe:
|
20
|
+
------
|
21
|
+
[ ] Allow more elaborate hosts config, e.g.: h.hosts = [ { host: '10.16.18.207', tags: %w(util migrate) } ]
|
22
|
+
[ ] Some kind of --dry-run option.
|
23
|
+
[ ] Allow deploy_via to include alternate user/connection options.
|
24
|
+
[ ] Rename "releases" => "builds" (and "deploys" => "releases"?)
|
25
|
+
[ ] Upload artifact to all hosts before the rest of the deployment process begins.
|
26
|
+
[ ] --force option for deploy task to overwrite an existing release
|
data/bin/harrison
ADDED
data/harrison.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'harrison/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "harrison"
|
8
|
+
spec.version = Harrison::VERSION
|
9
|
+
spec.authors = ["Jesse Scott"]
|
10
|
+
spec.email = ["jesse@puppetlabs.com"]
|
11
|
+
spec.summary = %q{Simple artifact-based deployment for web applications.}
|
12
|
+
spec.homepage = "https://github.com/scotje/harrison"
|
13
|
+
spec.license = "Apache 2.0"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = ["harrison"]
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.required_ruby_version = '>= 1.9.3'
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "trollop"
|
23
|
+
spec.add_runtime_dependency "net-ssh"
|
24
|
+
spec.add_runtime_dependency "net-scp"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rspec"
|
29
|
+
spec.add_development_dependency "debugger" if RUBY_VERSION < "2.0.0"
|
30
|
+
spec.add_development_dependency "byebug" if RUBY_VERSION >= "2.0.0"
|
31
|
+
spec.add_development_dependency "sourcify"
|
32
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Harrison
|
2
|
+
class Base
|
3
|
+
attr_reader :ssh
|
4
|
+
attr_accessor :options
|
5
|
+
|
6
|
+
def initialize(arg_opts=[], opts={})
|
7
|
+
# Config helpers for Harrisonfile.
|
8
|
+
self.class.option_helper(:user)
|
9
|
+
|
10
|
+
@arg_opts = arg_opts
|
11
|
+
@arg_opts << [ :debug, "Output debug messages.", :type => :boolean, :default => false ]
|
12
|
+
|
13
|
+
@options = opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.option_helper(option)
|
17
|
+
send :define_method, option do
|
18
|
+
@options[option]
|
19
|
+
end
|
20
|
+
|
21
|
+
send :define_method, "#{option}=" do |val|
|
22
|
+
@options[option] = val
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Find config from Harrison.config if it's not on this class.
|
27
|
+
def method_missing(meth, *args, &block)
|
28
|
+
if Harrison.config.respond_to?(meth)
|
29
|
+
Harrison.config.send(meth, *args, &block)
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def exec(cmd)
|
36
|
+
result = `#{cmd}`
|
37
|
+
abort("ERROR: Unable to execute local command: \"#{cmd}\"") if !$?.success? || result.nil?
|
38
|
+
result.strip
|
39
|
+
end
|
40
|
+
|
41
|
+
def remote_exec(cmd)
|
42
|
+
result = ssh.exec(cmd)
|
43
|
+
abort("ERROR: Unable to execute remote command: \"#{cmd}\"") if result.nil?
|
44
|
+
result.strip
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse(args)
|
48
|
+
opt_parser = Trollop::Parser.new
|
49
|
+
|
50
|
+
@arg_opts.each do |arg_opt|
|
51
|
+
opt_parser.opt(*arg_opt)
|
52
|
+
end
|
53
|
+
|
54
|
+
@options.merge!(Trollop::with_standard_exception_handling(opt_parser) do
|
55
|
+
opt_parser.parse(args)
|
56
|
+
end)
|
57
|
+
|
58
|
+
Harrison.send(:remove_const, "DEBUG") if Harrison.const_defined?("DEBUG")
|
59
|
+
Harrison.const_set("DEBUG", @options[:debug])
|
60
|
+
end
|
61
|
+
|
62
|
+
def run(&block)
|
63
|
+
if block_given?
|
64
|
+
# If called with a block, convert it to a proc and store.
|
65
|
+
@run_block = block
|
66
|
+
else
|
67
|
+
# Otherwise, invoke the previously stored block with self.
|
68
|
+
@run_block && @run_block.call(self)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def download(remote_path, local_path)
|
73
|
+
ssh.download(remote_path, local_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def upload(local_path, remote_path)
|
77
|
+
ssh.upload(local_path, remote_path)
|
78
|
+
end
|
79
|
+
|
80
|
+
def close
|
81
|
+
ssh.close if @ssh
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def ssh
|
87
|
+
@ssh ||= Harrison::SSH.new(host: @options[:host], user: @options[:user])
|
88
|
+
end
|
89
|
+
|
90
|
+
def ensure_local_dir(dir)
|
91
|
+
@_ensured_local ||= {}
|
92
|
+
@_ensured_local[dir] || (system("if [ ! -d #{dir} ] ; then mkdir -p #{dir} ; fi") && @_ensured_local[dir] = true) || abort("Error: Unable to create local directory \"#{dir}\".")
|
93
|
+
end
|
94
|
+
|
95
|
+
def ensure_remote_dir(host, dir)
|
96
|
+
@_ensured_remote ||= {}
|
97
|
+
@_ensured_remote[host] ||= {}
|
98
|
+
@_ensured_remote[host][dir] || (ssh.exec("if [ ! -d #{dir} ] ; then mkdir -p #{dir} ; fi") && @_ensured_remote[host][dir] = true) || abort("Error: Unable to create remote directory \"#{dir}\" on \"#{host}\".")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Harrison
|
2
|
+
class Deploy < Base
|
3
|
+
attr_accessor :artifact
|
4
|
+
attr_accessor :host # The specific host among --hosts that we are currently working on.
|
5
|
+
attr_accessor :release_dir
|
6
|
+
attr_accessor :deploy_link
|
7
|
+
|
8
|
+
def initialize(opts={})
|
9
|
+
# Config helpers for Harrisonfile.
|
10
|
+
self.class.option_helper(:hosts)
|
11
|
+
self.class.option_helper(:env)
|
12
|
+
self.class.option_helper(:base_dir)
|
13
|
+
self.class.option_helper(:deploy_via)
|
14
|
+
|
15
|
+
# Command line opts for this action. Will be merged with common opts.
|
16
|
+
arg_opts = [
|
17
|
+
[ :hosts, "List of remote hosts to deploy to. Can also be specified in Harrisonfile.", :type => :strings ],
|
18
|
+
[ :env, "Environment to deploy to. This can be examined in your Harrisonfile to calculate target hosts.", :type => :string ],
|
19
|
+
]
|
20
|
+
|
21
|
+
super(arg_opts, opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse(args)
|
25
|
+
super
|
26
|
+
|
27
|
+
# Preserve argv hosts if it's been passed.
|
28
|
+
@_argv_hosts = self.hosts.dup if self.hosts
|
29
|
+
|
30
|
+
# Make sure they passed an artifact.
|
31
|
+
self.artifact = args[1] || abort("ERROR: You must specify the artifact to be deployed as an argument to this command.")
|
32
|
+
end
|
33
|
+
|
34
|
+
def remote_exec(cmd)
|
35
|
+
super("cd #{remote_project_dir} && #{cmd}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def run(&block)
|
39
|
+
return super if block_given?
|
40
|
+
|
41
|
+
# Override Harrisonfile hosts if it was passed on argv.
|
42
|
+
self.hosts = @_argv_hosts if @_argv_hosts
|
43
|
+
|
44
|
+
if !self.hosts || self.hosts.empty?
|
45
|
+
abort("ERROR: You must specify one or more hosts to deploy to, either in your Harrisonfile or via --hosts.")
|
46
|
+
end
|
47
|
+
|
48
|
+
# Default base_dir.
|
49
|
+
self.base_dir ||= '/opt'
|
50
|
+
|
51
|
+
puts "Deploying #{artifact} for \"#{project}\" onto #{hosts.size} hosts..."
|
52
|
+
|
53
|
+
self.release_dir = "#{remote_project_dir}/releases/" + File.basename(artifact, '.tar.gz')
|
54
|
+
self.deploy_link = "#{remote_project_dir}/deploys/" + Time.new.utc.strftime('%Y-%m-%d_%H%M%S')
|
55
|
+
|
56
|
+
hosts.each do |h|
|
57
|
+
self.host = h
|
58
|
+
|
59
|
+
ensure_remote_dir(self.host, "#{remote_project_dir}/deploys")
|
60
|
+
ensure_remote_dir(self.host, "#{remote_project_dir}/releases")
|
61
|
+
|
62
|
+
# Make folder for release or bail if it already exists.
|
63
|
+
remote_exec("mkdir #{release_dir}")
|
64
|
+
|
65
|
+
# Upload artifact to host.
|
66
|
+
upload(artifact, "#{remote_project_dir}/releases/")
|
67
|
+
|
68
|
+
# Unpack.
|
69
|
+
remote_exec("cd #{release_dir} && tar -xzf ../#{File.basename(artifact)}")
|
70
|
+
|
71
|
+
# Clean up artifact.
|
72
|
+
remote_exec("rm -f #{remote_project_dir}/releases/#{File.basename(artifact)}")
|
73
|
+
|
74
|
+
# Symlink a new deploy to this release.
|
75
|
+
remote_exec("ln -s #{release_dir} #{deploy_link}")
|
76
|
+
|
77
|
+
# Symlink current to new deploy.
|
78
|
+
remote_exec("ln -sfn #{deploy_link} #{remote_project_dir}/current")
|
79
|
+
|
80
|
+
# Run user supplied deploy code to restart server or whatever.
|
81
|
+
super
|
82
|
+
|
83
|
+
close(self.host)
|
84
|
+
end
|
85
|
+
|
86
|
+
puts "Sucessfully deployed #{artifact} to #{hosts.join(', ')}."
|
87
|
+
end
|
88
|
+
|
89
|
+
def close(host=nil)
|
90
|
+
if host
|
91
|
+
@_conns[host].close if @_conns && @_conns[host]
|
92
|
+
elsif @_conns
|
93
|
+
@_conns.keys.each do |host|
|
94
|
+
@_conns[host].close unless @_conns[host].closed?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def ssh
|
102
|
+
@_conns ||= {}
|
103
|
+
@_conns[self.host] ||= Harrison::SSH.new(host: self.host, user: @options[:user], proxy: self.deploy_via)
|
104
|
+
end
|
105
|
+
|
106
|
+
def remote_project_dir
|
107
|
+
"#{base_dir}/#{project}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Harrison
|
2
|
+
class Package < Base
|
3
|
+
def initialize(opts={})
|
4
|
+
# Config helpers for Harrisonfile.
|
5
|
+
self.class.option_helper(:host)
|
6
|
+
self.class.option_helper(:commit)
|
7
|
+
self.class.option_helper(:purge)
|
8
|
+
self.class.option_helper(:pkg_dir)
|
9
|
+
self.class.option_helper(:remote_dir)
|
10
|
+
self.class.option_helper(:exclude)
|
11
|
+
|
12
|
+
# Command line opts for this action. Will be merged with common opts.
|
13
|
+
arg_opts = [
|
14
|
+
[ :commit, "Specific commit to be packaged. Accepts anything that `git rev-parse` understands.", :type => :string, :default => "HEAD" ],
|
15
|
+
[ :purge, "Remove all previously packaged commits and working copies from the build host when finished.", :type => :boolean, :default => false ],
|
16
|
+
[ :pkg_dir, "Local folder to save package to.", :type => :string, :default => "pkg" ],
|
17
|
+
[ :remote_dir, "Remote working folder.", :type => :string, :default => "~/.harrison" ],
|
18
|
+
]
|
19
|
+
|
20
|
+
super(arg_opts, opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
def remote_exec(cmd)
|
24
|
+
ensure_remote_dir(self.host, "#{remote_project_dir}/package")
|
25
|
+
|
26
|
+
super("cd #{remote_project_dir}/package && #{cmd}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def run(&block)
|
30
|
+
return super if block_given?
|
31
|
+
|
32
|
+
# Resolve commit ref to an actual short SHA.
|
33
|
+
resolve_commit!
|
34
|
+
|
35
|
+
puts "Packaging #{commit} for \"#{project}\" on #{host}..."
|
36
|
+
|
37
|
+
# Make sure the folder to save the artifact to locally exists.
|
38
|
+
ensure_local_dir(pkg_dir)
|
39
|
+
|
40
|
+
# Fetch/clone git repo on remote host.
|
41
|
+
remote_exec("if [ -d cached ] ; then cd cached && git fetch origin -p ; else git clone #{git_src} cached ; fi")
|
42
|
+
|
43
|
+
# Check out target commit.
|
44
|
+
remote_exec("cd cached && git reset --hard #{commit} && git clean -f -d")
|
45
|
+
|
46
|
+
# Make a build folder of the target commit.
|
47
|
+
remote_exec("rm -rf #{commit} && cp -a cached #{commit}")
|
48
|
+
|
49
|
+
# Run user supplied build code.
|
50
|
+
# TODO: alter remote_exec to set directory context to commit dir?
|
51
|
+
super
|
52
|
+
|
53
|
+
# Package build folder into tgz.
|
54
|
+
remote_exec("rm -f #{artifact_name(commit)}.tar.gz && cd #{commit} && tar #{excludes_for_tar} -czf ../#{artifact_name(commit)}.tar.gz .")
|
55
|
+
|
56
|
+
# Download (Expand remote path since Net::SCP doesn't expand ~)
|
57
|
+
download(remote_exec("readlink -m #{artifact_name(commit)}.tar.gz"), "#{pkg_dir}/#{artifact_name(commit)}.tar.gz")
|
58
|
+
|
59
|
+
if purge
|
60
|
+
remote_exec("cd .. && rm -rf package")
|
61
|
+
end
|
62
|
+
|
63
|
+
puts "Sucessfully packaged #{commit} to #{pkg_dir}/#{artifact_name(commit)}.tar.gz"
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def remote_project_dir
|
69
|
+
"#{remote_dir}/#{project}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def resolve_commit!
|
73
|
+
self.commit = exec("git rev-parse --short #{self.commit} 2>/dev/null")
|
74
|
+
end
|
75
|
+
|
76
|
+
def excludes_for_tar
|
77
|
+
return '' if !exclude || exclude.empty?
|
78
|
+
|
79
|
+
"--exclude \"#{exclude.join('" --exclude "')}\""
|
80
|
+
end
|
81
|
+
|
82
|
+
def artifact_name(commit)
|
83
|
+
@_timestamp ||= Time.new.utc.strftime('%Y%m%d%H%M%S')
|
84
|
+
"#{@_timestamp}-#{commit}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/harrison/ssh.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/ssh/proxy/command'
|
3
|
+
require 'net/scp'
|
4
|
+
|
5
|
+
module Harrison
|
6
|
+
class SSH
|
7
|
+
def initialize(opts={})
|
8
|
+
if opts[:proxy]
|
9
|
+
@proxy = Net::SSH::Proxy::Command.new("ssh #{opts[:proxy]} \"nc %h %p\" 2>/dev/null")
|
10
|
+
@conn = Net::SSH.start(opts[:host], opts[:user], forward_agent: true, proxy: @proxy)
|
11
|
+
else
|
12
|
+
@conn = Net::SSH.start(opts[:host], opts[:user], forward_agent: true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Helper to catch non-zero exit status and report errors.
|
17
|
+
def exec(command)
|
18
|
+
puts "INFO (ssh-exec #{desc}): #{command}" if Harrison::DEBUG
|
19
|
+
|
20
|
+
stdout_data = ""
|
21
|
+
stderr_data = ""
|
22
|
+
exit_code = nil
|
23
|
+
|
24
|
+
@conn.open_channel do |channel|
|
25
|
+
channel.exec(command) do |ch, success|
|
26
|
+
warn "Couldn't execute command (ssh.channel.exec) on remote host: #{command}" unless success
|
27
|
+
|
28
|
+
channel.on_data do |ch,data|
|
29
|
+
stdout_data += data
|
30
|
+
end
|
31
|
+
|
32
|
+
channel.on_extended_data do |ch,type,data|
|
33
|
+
stderr_data += data
|
34
|
+
end
|
35
|
+
|
36
|
+
channel.on_request("exit-status") do |ch,data|
|
37
|
+
exit_code = data.read_long
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@conn.loop
|
43
|
+
|
44
|
+
if Harrison::DEBUG || exit_code != 0
|
45
|
+
warn "STDERR (ssh-exec #{desc}): #{stderr_data.strip}" unless stderr_data.empty?
|
46
|
+
warn "STDOUT (ssh-exec #{desc}): #{stdout_data.strip}" unless stdout_data.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
(exit_code == 0) ? stdout_data : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def download(remote_path, local_path)
|
53
|
+
puts "INFO (scp-down #{desc}): #{local_path} <<< #{remote_path}" if Harrison::DEBUG
|
54
|
+
@conn.scp.download!(remote_path, local_path)
|
55
|
+
end
|
56
|
+
|
57
|
+
def upload(local_path, remote_path)
|
58
|
+
puts "INFO (scp-up #{desc}): #{local_path} >>> #{remote_path}" if Harrison::DEBUG
|
59
|
+
@conn.scp.upload!(local_path, remote_path)
|
60
|
+
end
|
61
|
+
|
62
|
+
def close
|
63
|
+
# net-ssh doesn't seem to know how to close proxy::command connections
|
64
|
+
Process.kill("TERM", @conn.transport.socket.pid) if @proxy
|
65
|
+
@conn.close
|
66
|
+
end
|
67
|
+
|
68
|
+
def closed?
|
69
|
+
@conn.closed?
|
70
|
+
end
|
71
|
+
|
72
|
+
def desc
|
73
|
+
if @proxy
|
74
|
+
"#{@conn.host} (via #{@proxy.command_line.split(' ')[1]})"
|
75
|
+
else
|
76
|
+
@conn.host
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|