harrison 0.0.1
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.
- 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
|