chap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ system_root
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3@chap
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chap.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/chap/(.+)\.rb$}) { |m| "spec/lib/chap_spec.rb" }
7
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
8
+ watch('spec/spec_helper.rb') { "spec" }
9
+ end
10
+
11
+ guard 'bundler' do
12
+ watch('Gemfile')
13
+ watch(/^.+\.gemspec/)
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Tung Nguyen
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,121 @@
1
+ # Chap
2
+
3
+ chef + capistrano = chap: deploy your app with either chef or capistrano. This was written to solve the issue between having 2 deployment systems that are very similar but not exactly the same. With chap you can deploy to a single server by running one command:
4
+
5
+ <pre>
6
+ $ chap deploy
7
+ </pre>
8
+
9
+ The same command is called whether you're using chef or capistrano for deployment. The chap deploy command does the heavy lifting and manages the deploy instead of capistrano or chef.
10
+
11
+ ## Requirements
12
+
13
+ <pre>
14
+ $ gem install chap
15
+ </pre>
16
+
17
+ ## Usage
18
+
19
+ The chap command is meant to be executed on the server which you want to deploy the code.
20
+
21
+ ## Setup
22
+
23
+ Chap requires 3 configuration files: chap.yml, chap.json and node.json.
24
+
25
+ * chap.json: contains capistrano-like configuration settings.
26
+ * node.json: is intended to be the same file that chef solo uses and contains instance specific information, like node[:instance_role].
27
+ : chap.yml: The paths of the chap.json and node.json are configured in this file.
28
+
29
+ Here are examples of the starter setup files that you can generate via:
30
+
31
+ <pre>
32
+ $ chap setup -o /etc/chef
33
+ </pre>
34
+
35
+ <pre>
36
+ $ cat /etc/chef/chap.yml
37
+ chap: /etc/chef/chap.json
38
+ node: /etc/chef/node.json
39
+ $ cat /etc/chef/chap.json
40
+ {
41
+ "repo": "git@github.com:tongueroo/chapdemo.git",
42
+ "branch": "master",
43
+ "application": "chapdemo",
44
+ "deploy_to": "/data/chapdemo",
45
+ "strategy": "checkout",
46
+ "keep": 5,
47
+ "user": "deploy",
48
+ "group": "deploy"
49
+ }
50
+ $ cat /etc/chef/node.json
51
+ {
52
+ "environment": "staging",
53
+ "application": "chapdemo",
54
+ "instance_role": "app"
55
+ }
56
+ </pre>
57
+
58
+ ### Deploy sequence
59
+
60
+ The deploy sequence is based on the sequence from the capistrano and chef deploy resource provider.
61
+
62
+ 1. Download code to [deploy_to]/releases/[timestamp]
63
+ 2. Run chap/deploy hook
64
+ 3. Symlink [deploy_to]/releases/[timestamp] to [deploy_to]/current
65
+ 4. Run chap/restart hook
66
+ 5. Clean up old releases
67
+
68
+ On the server:
69
+
70
+ <pre>
71
+ $ chap deploy
72
+ </pre>
73
+
74
+ From capistrano, on local or deploy box:
75
+
76
+ <pre>
77
+ $ cap deploy # cap recipe calls "chap deploy"
78
+ </pre>
79
+
80
+ Chef Chap LWRP:
81
+
82
+ <pre>
83
+ # chef LWRP creates chap.yml and chap.json setup files and calls "chap deploy"
84
+ chap_deploy "chapdemo" do
85
+ repo "git@github.com:tongueroo/chapdemo.git"
86
+ revision "master"
87
+ end
88
+ </pre>
89
+
90
+ Chap loads up information from node.json because it needs the information for hooks, which tend to work differently for different server roles. For example, the chap/restart hook below will run "touch tmp/restart.txt" for an app role and will run "rvmsudo bluepill restart resque" for a resque role. Example:
91
+
92
+ <pre>
93
+ $ cat chap/restart
94
+ #!/usr/bin/env ruby
95
+ restart = case node[:instance_role]
96
+ when 'app'
97
+ "touch tmp/restart.txt"
98
+ when 'resque'
99
+ "rvmsudo bluepill restart resque"
100
+ end
101
+ run "cd #{current_path} && #{restart}"
102
+ </pre>
103
+
104
+ ## Deploy Hooks
105
+
106
+ Define your deploy hooks in the chap folder of the project. There are 2 deploy hooks.
107
+
108
+ * chap/deploy - runs after the code has been deployed but not yet symlinked
109
+ * chap/restart - runs after the code has been symlinked and app needs to be restarted
110
+
111
+ Deploy hooks get evaluated within the context of a chap deploy run and have some special variables and methods:
112
+
113
+ Special variables:
114
+
115
+ * node - contains data from /etc/chef/node.json. Avaiable as mash.
116
+ * chap - contains data from /etc/chef/chap.json and some special variables added by chap. Avaiable as mash. Special variables: release_path, current_path, shared_path, cached_path. The special variables are also available directly as methods.
117
+
118
+ Special methods:
119
+
120
+ * run - output the command to be ran and runs command.
121
+ * log - log messages to [shared_path]/chap/chap.log.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ task :default => :spec
6
+
7
+ RSpec::Core::RakeTask.new
data/bin/chap ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.expand_path("../../lib", __FILE__))
4
+ require 'chap'
5
+
6
+ Chap::CLI.start
data/chap.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chap/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "chap"
8
+ gem.version = Chap::VERSION
9
+ gem.authors = ["Tung Nguyen"]
10
+ gem.email = ["tongueroo@gmail.com"]
11
+ gem.description = %q{chef + capistrano = chap: deploy your app with either chef or capistrano}
12
+ gem.summary = %q{chef + capistrano = chap: deploy your app with either chef or capistrano}
13
+ gem.homepage = "https://github.com/tongueroo/chap"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "rake"
21
+ gem.add_dependency "json"
22
+ gem.add_dependency "thor"
23
+ gem.add_dependency "colorize"
24
+ gem.add_dependency "logger"
25
+
26
+ gem.add_development_dependency 'rspec'
27
+ gem.add_development_dependency 'guard'
28
+ gem.add_development_dependency 'guard-rspec'
29
+ gem.add_development_dependency 'guard-bundler'
30
+ gem.add_development_dependency 'rb-fsevent'
31
+ # gem.add_development_dependency 'fakefs'
32
+ end
data/lib/chap/cli.rb ADDED
@@ -0,0 +1,26 @@
1
+ module Chap
2
+ class CLI < Thor
3
+ desc "setup", "Sets up chap config files"
4
+ long_desc "Creates chap.json, chap.yml and node.json example files."
5
+ method_option :force, :aliases => '-f', :type => :boolean, :desc => "Overwrite existing files"
6
+ method_option :quiet, :aliases => '-q', :type => :boolean, :desc => "Quiet commands"
7
+ method_option :output, :aliases => '-o', :desc => "Folder which example files will be written to"
8
+ def setup
9
+ Chap::Task.setup(options)
10
+ end
11
+
12
+ desc "deploy", "Deploy application"
13
+ long_desc <<-EOL
14
+ Example:
15
+
16
+ $ chap deploy
17
+
18
+ Deploys code using settings from chap.json and node.json. chap.json and node.json should be referenced in chap.yml.
19
+ EOL
20
+ method_option :quiet, :aliases => '-q', :type => :boolean, :desc => "Quiet commands"
21
+ method_option :config, :aliases => '-c', :default => '/etc/chef/chap.yml', :desc => "chap.yml config to use"
22
+ def deploy
23
+ Chap::Task.deploy(options)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,113 @@
1
+ module Chap
2
+ class Config
3
+ attr_reader :options, :node, :chap
4
+ def initialize(options={})
5
+ @options = options
6
+ # preload all config files so validatation happens before deploys
7
+ _ = yaml, chap, node
8
+ end
9
+
10
+ def yaml
11
+ path = options[:config]
12
+ if File.exist?(path)
13
+ @yaml ||= Mash.from_hash(YAML.load(IO.read(path)))
14
+ else
15
+ puts "ERROR: chap.yaml config does not exist at: #{path}"
16
+ exit 1
17
+ end
18
+ end
19
+
20
+ def chap
21
+ return @chap if @chap
22
+ @chap = load_json(:chap)
23
+ @chap[:release_path] = release_path
24
+ @chap[:current_path] = current_path
25
+ @chap[:cached_path] = cached_path
26
+ @chap
27
+ end
28
+
29
+ def node
30
+ @node ||= load_json(:node)
31
+ end
32
+
33
+ # the chap.json and node.json is assumed to be in th same folder as
34
+ # chap.yml if a relative path is given
35
+ def load_json(key)
36
+ path = if yaml[key] =~ %r{^/} # root path given
37
+ yaml[key]
38
+ else # relative path
39
+ dirname = File.dirname(options[:config])
40
+ "#{dirname}/#{yaml[key]}"
41
+ end
42
+ if File.exist?(path)
43
+ Mash.from_hash(JSON.parse(IO.read(path)))
44
+ else
45
+ puts "ERROR: #{key}.json config does not exist at: #{path}"
46
+ exit 1
47
+ end
48
+ end
49
+
50
+ def timestamp
51
+ @timestamp ||= Time.now.strftime("%Y%m%d%H%M%S")
52
+ end
53
+
54
+ def deploy_to
55
+ chap[:deploy_to]
56
+ end
57
+
58
+ # special attributes added to chap
59
+ def release_path
60
+ return @release_path if @release_path
61
+ @release_path = "#{deploy_to}/releases/#{timestamp}"
62
+ end
63
+
64
+ def current_path
65
+ return @current_path if @current_path
66
+ @current_path = "#{deploy_to}/current"
67
+ end
68
+
69
+ def shared_path
70
+ return @shared_path if @shared_path
71
+ @shared_path = "#{deploy_to}/shared"
72
+ end
73
+
74
+ def cached_path
75
+ return @cached_path if @cached_path
76
+ path = chap[:repo].split(':').last.sub('.git','')
77
+ @cached_path = "#{shared_path}/cache/#{strategy}/#{path}"
78
+ end
79
+
80
+ def strategy
81
+ chap[:strategy] || 'checkout'
82
+ end
83
+
84
+ def log(msg)
85
+ puts msg unless options[:quiet]
86
+ logger.info(msg)
87
+ end
88
+
89
+ def chap_log_path
90
+ return @chap_log_path if @chap_log_path
91
+ dir = "#{shared_path}/chap"
92
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
93
+ @chap_log_path = "#{dir}/chap.log"
94
+ system("cat /dev/null > #{@chap_log_path}")
95
+ @chap_log_path
96
+ end
97
+
98
+ def logger
99
+ return @logger if @logger
100
+ @logger = Logger.new(chap_log_path)
101
+ # @logger.level = Logger::WARN
102
+ @logger
103
+ end
104
+
105
+ def run(cmd)
106
+ log "Running: #{cmd}"
107
+ cmd = "#{cmd} 2>&1" unless cmd.include?(" > ")
108
+ out = `#{cmd}`
109
+ log out
110
+ raise "DeployError" if $?.exitstatus > 0
111
+ end
112
+ end
113
+ end
data/lib/chap/hook.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Chap
2
+ class Hook
3
+ include SpecialMethods
4
+
5
+ attr_reader :options, :config
6
+ def initialize(path, config)
7
+ @path = path
8
+ @config = config
9
+ end
10
+
11
+ def evaluate
12
+ instance_eval(File.read(@path), @path)
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,129 @@
1
+ module Chap
2
+ class Runner
3
+ include SpecialMethods
4
+
5
+ attr_reader :options, :config
6
+ def initialize(options={})
7
+ @options = options
8
+ @config = Config.new(options)
9
+ end
10
+
11
+ def deploy
12
+ setup
13
+ strategy.deploy
14
+ symlink_shared
15
+ hook(:deploy)
16
+ symlink_current
17
+ hook(:restart)
18
+ cleanup
19
+ end
20
+
21
+ def setup
22
+ user = config.chap[:user] || ENV['USER']
23
+ group = config.chap[:group]
24
+ begin
25
+ FileUtils.mkdir_p(deploy_to)
26
+ FileUtils.chown_R user, group, deploy_to
27
+ rescue Exception
28
+ # retry to create deploy_to folder with sudo
29
+ user_group = [user,group].compact.join(':')
30
+ raise unless system("sudo mkdir -p #{deploy_to}")
31
+ raise unless system("sudo chown -R #{user_group} #{deploy_to}")
32
+ end
33
+ dirs = ["#{deploy_to}/releases"]
34
+ dirs += shared_dirs
35
+ dirs.each do |dir|
36
+ FileUtils.mkdir_p(dir)
37
+ end
38
+ end
39
+
40
+ def strategy
41
+ strategy = config.strategy
42
+ klass = Strategy.const_get(camel_case(strategy))
43
+ @strategy ||= klass.new(:config => @config)
44
+ end
45
+
46
+ def camel_case(string)
47
+ return string if string !~ /_/ && string =~ /[A-Z]+.*/
48
+ string.split('_').map{|e| e.capitalize}.join
49
+ end
50
+
51
+ def symlink_shared
52
+ shared_dirs.each do |path|
53
+ src = path
54
+ relative_path = path.sub("#{shared_path}/",'')
55
+ dest = "#{release_path}/#{relative_path}"
56
+ # make sure the directory exist for symlink creation
57
+ dirname = File.dirname(dest)
58
+ FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
59
+ if File.symlink?(dest)
60
+ File.delete(dest)
61
+ elsif File.directory?(dest)
62
+ FileUtils.rm_rf(dest)
63
+ end
64
+ FileUtils.ln_s(src,dest)
65
+ end
66
+ end
67
+
68
+ def symlink_current
69
+ FileUtils.rm(current_path) if File.exist?(current_path)
70
+ FileUtils.ln_s(release_path, current_path)
71
+ log "Current symlink updated".colorize(:green)
72
+ end
73
+
74
+ def cleanup
75
+ log "Cleaning up".colorize(:green)
76
+ remove_old_releases
77
+ logrotate
78
+ end
79
+
80
+ def remove_old_releases
81
+ dirname = File.dirname(release_path)
82
+ releases = Dir.glob("#{dirname}/*").sort.reverse
83
+ keep = config.chap[:keep] || 5
84
+ delete = releases - releases[0..keep-1]
85
+ delete.each do |old_release|
86
+ FileUtils.rm_rf(old_release)
87
+ end
88
+ end
89
+
90
+ def logrotate
91
+ logrotate_file(config.chap_log_path)
92
+ end
93
+
94
+ def logrotate_timestamp
95
+ @logrotate_timestamp ||= Time.now.strftime("%Y-%m-%d-%H-%M-%S")
96
+ end
97
+
98
+ def logrotate_file(log_path)
99
+ dirname = File.dirname(log_path)
100
+ basename = File.basename(log_path, '.log')
101
+ archive = "#{dirname}/#{basename}-#{logrotate_timestamp}.log"
102
+ FileUtils.cp(log_path, archive) if File.exist?(log_path)
103
+ logs = Dir.glob("#{dirname}/#{basename}-*.log").sort.reverse
104
+ delete = logs - logs[0..9] # keep last 10 chap logs
105
+ delete.each do |old_log|
106
+ FileUtils.rm_f(old_log)
107
+ end
108
+ end
109
+
110
+ def shared_dirs
111
+ dirs = config.chap[:shared_dirs] || [
112
+ "public/system",
113
+ "log",
114
+ "tmp/pids"
115
+ ]
116
+ dirs.map! {|p| "#{shared_path}/#{p}"}
117
+ end
118
+
119
+ def hook(name)
120
+ log "Running hook: #{name}".colorize(:green)
121
+ path = "#{release_path}/chap/#{name}"
122
+ if File.exist?(path)
123
+ Hook.new(path, @config).evaluate
124
+ else
125
+ log "chap/#{name} hook does not exist".colorize(:red)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,23 @@
1
+ module Chap
2
+ module SpecialMethods
3
+ SPECIAL_METHODS = %w/deploy_to release_path current_path shared_path cached_path node chap log run/
4
+
5
+ def self.included(base)
6
+ base.send(:extend, ClassMethods)
7
+ base.define_special_methods
8
+ end
9
+
10
+ module ClassMethods
11
+ # delegate to the config class
12
+ def define_special_methods
13
+ SPECIAL_METHODS.each do |method|
14
+ class_eval <<-EOL
15
+ def #{method}(*args)
16
+ @config.#{method}(*args)
17
+ end
18
+ EOL
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module Chap
2
+ module Strategy
3
+ class Base
4
+ include SpecialMethods
5
+
6
+ attr_reader :options, :config
7
+ def initialize(options={})
8
+ @options = options
9
+ @config = options[:config]
10
+ log "Deploying via #{self.class} strategy".colorize(:green)
11
+ end
12
+
13
+ # should download code to the release_path
14
+ def deploy
15
+ raise "Must implement deploy method"
16
+ end
17
+
18
+ end # of Base
19
+ end # of Strategy
20
+ end # of Chap
@@ -0,0 +1,62 @@
1
+ module Chap
2
+ module Strategy
3
+ class Checkout < Base
4
+ def deploy
5
+ update
6
+ copy
7
+ end
8
+
9
+ def update
10
+ log "Updating repo in #{cached_path}".colorize(:green)
11
+ cached_root = File.dirname(cached_path)
12
+ FileUtils.mkdir_p(cached_root) unless File.exist?(cached_root)
13
+ if File.exist?(cached_path)
14
+ sync
15
+ else
16
+ checkout
17
+ end
18
+ end
19
+
20
+ def sync
21
+ command =<<-BASH
22
+ cd #{cached_path} && \
23
+ git fetch -q origin && \
24
+ git fetch --tags -q origin && \
25
+ git reset -q --hard #{revision} && \
26
+ git clean -q -d -x -f
27
+ BASH
28
+ run(command)
29
+ end
30
+
31
+ def checkout
32
+ command =<<BASH
33
+ git clone -q #{config.chap[:repo]} #{cached_path} && \
34
+ cd #{cached_path} && \
35
+ git checkout -q -b deploy #{revision}
36
+ BASH
37
+ run(command)
38
+ end
39
+
40
+ def copy
41
+ command = "cp -RPp #{cached_path} #{release_path} && #{mark}"
42
+ run command
43
+ log "Code copied to #{release_path}".colorize(:green)
44
+ end
45
+
46
+ private
47
+
48
+ def mark
49
+ "(echo #{revision} > #{release_path}/REVISION)"
50
+ end
51
+
52
+ def revision
53
+ return @revision if @revision
54
+ result = `git ls-remote #{config.chap[:repo]} #{config.chap[:branch]}`
55
+ @revision = result.split(/\s/).first
56
+ log "Fetched revision #{@revision}".colorize(:green)
57
+ @revision
58
+ end
59
+
60
+ end # of RemoteCache
61
+ end # of Strategy
62
+ end # of Chap
@@ -0,0 +1,10 @@
1
+ module Chap
2
+ module Strategy
3
+ # useful for specs
4
+ class Copy < Base
5
+ def deploy
6
+ run("cp -RPp #{config.chap[:source]} #{release_path}")
7
+ end
8
+ end
9
+ end # of Strategy
10
+ end # of Chap
@@ -0,0 +1,13 @@
1
+ module Chap
2
+ module Strategy
3
+ class Hardlink < Checkout
4
+ def copy
5
+ copy = File.expand_path("../util/copy.rb", __FILE__)
6
+ command = "#{copy} #{cached_path} #{release_path} && #{mark}"
7
+ run command
8
+ log "Code copied to #{release_path}".colorize(:green)
9
+ end
10
+
11
+ end # of HardLink
12
+ end # of Strategy
13
+ end # of Chap