emeril 0.5.0
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/.cane +0 -0
- data/.gitignore +17 -0
- data/.tailor +106 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +3 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +261 -0
- data/Rakefile +33 -0
- data/emeril.gemspec +39 -0
- data/lib/emeril.rb +11 -0
- data/lib/emeril/category.rb +26 -0
- data/lib/emeril/git_tagger.rb +122 -0
- data/lib/emeril/logging.rb +17 -0
- data/lib/emeril/metadata_chopper.rb +38 -0
- data/lib/emeril/publisher.rb +130 -0
- data/lib/emeril/rake.rb +5 -0
- data/lib/emeril/rake_tasks.rb +41 -0
- data/lib/emeril/releaser.rb +80 -0
- data/lib/emeril/thor.rb +5 -0
- data/lib/emeril/thor_tasks.rb +45 -0
- data/lib/emeril/version.rb +6 -0
- data/spec/emeril/category_spec.rb +28 -0
- data/spec/emeril/git_tagger_spec.rb +147 -0
- data/spec/emeril/metadata_chopper_spec.rb +70 -0
- data/spec/emeril/publisher_spec.rb +223 -0
- data/spec/emeril/releaser_spec.rb +141 -0
- data/spec/fixtures/vcr_cassettes/known_cookbook.yml +46 -0
- data/spec/fixtures/vcr_cassettes/nonexistant_cookbook.yml +38 -0
- data/spec/spec_helper.rb +27 -0
- metadata +312 -0
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'cane/rake_task'
|
4
|
+
require 'tailor/rake_task'
|
5
|
+
|
6
|
+
Rake::TestTask.new(:unit) do |t|
|
7
|
+
t.libs.push "lib"
|
8
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Run all test suites"
|
13
|
+
task :test => [:unit]
|
14
|
+
|
15
|
+
desc "Run cane to check quality metrics"
|
16
|
+
Cane::RakeTask.new do |cane|
|
17
|
+
cane.canefile = './.cane'
|
18
|
+
end
|
19
|
+
|
20
|
+
Tailor::RakeTask.new
|
21
|
+
|
22
|
+
desc "Display LOC stats"
|
23
|
+
task :stats do
|
24
|
+
puts "\n## Production Code Stats"
|
25
|
+
sh "countloc -r lib"
|
26
|
+
puts "\n## Test Code Stats"
|
27
|
+
sh "countloc -r spec"
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Run all quality tasks"
|
31
|
+
task :quality => [:cane, :tailor, :stats]
|
32
|
+
|
33
|
+
task :default => [:test, :quality]
|
data/emeril.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'emeril/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "emeril"
|
8
|
+
spec.version = Emeril::VERSION
|
9
|
+
spec.authors = ["Fletcher Nichol"]
|
10
|
+
spec.email = ["fnichol@nichol.ca"]
|
11
|
+
spec.description = %q{Release Chef cookbooks}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = "https://github.com/fnichol/emeril"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = []
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = '>= 1.9.2'
|
22
|
+
|
23
|
+
spec.add_dependency 'chef'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'minitest'
|
28
|
+
spec.add_development_dependency 'guard-minitest'
|
29
|
+
spec.add_development_dependency 'mocha'
|
30
|
+
spec.add_development_dependency 'fakefs'
|
31
|
+
spec.add_development_dependency 'vcr'
|
32
|
+
spec.add_development_dependency 'webmock'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'cane'
|
35
|
+
spec.add_development_dependency 'guard-cane'
|
36
|
+
spec.add_development_dependency 'tailor'
|
37
|
+
spec.add_development_dependency 'simplecov'
|
38
|
+
spec.add_development_dependency 'countloc'
|
39
|
+
end
|
data/lib/emeril.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Emeril
|
7
|
+
|
8
|
+
# A category for a cookbook on the Community Site.
|
9
|
+
#
|
10
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
11
|
+
class Category
|
12
|
+
|
13
|
+
# Returns the category for the given cookbook on the Community Site or
|
14
|
+
# nil if it is not present.
|
15
|
+
#
|
16
|
+
# @param [String] cookbook a cookbook name
|
17
|
+
# @return [String,nil] the cookbook category or nil if it is not present
|
18
|
+
# on the Community site
|
19
|
+
#
|
20
|
+
def self.for_cookbook(cookbook)
|
21
|
+
path = "/api/v1/cookbooks/#{cookbook}"
|
22
|
+
response = Net::HTTP.get_response("cookbooks.opscode.com", path)
|
23
|
+
JSON.parse(response.body)['category']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'emeril/logging'
|
4
|
+
|
5
|
+
module Emeril
|
6
|
+
|
7
|
+
# Exception class raised when a git repo is not clean.
|
8
|
+
#
|
9
|
+
class GitNotCleanError < StandardError ; end
|
10
|
+
|
11
|
+
# Exception class raised when a git push does not return successfully.
|
12
|
+
#
|
13
|
+
class GitPushError < StandardError ; end
|
14
|
+
|
15
|
+
# Applies a version tag on a git repository and pushes it to the origin
|
16
|
+
# remote.
|
17
|
+
#
|
18
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
19
|
+
#
|
20
|
+
class GitTagger
|
21
|
+
|
22
|
+
include Logging
|
23
|
+
|
24
|
+
# Creates a new instance.
|
25
|
+
#
|
26
|
+
# @param [Hash] options configuration for a git tagger
|
27
|
+
# @option options [Logger] an optional logger instance
|
28
|
+
# @option options [String] source_path the path to a git repository
|
29
|
+
# @option options [String] tag_prefix a prefix for a git tag version string
|
30
|
+
# @option options [String] version (required) a version string
|
31
|
+
# @raise [ArgumentError] if any required options are not set
|
32
|
+
#
|
33
|
+
def initialize(options = {})
|
34
|
+
@logger = options[:logger]
|
35
|
+
@source_path = options.fetch(:source_path, Dir.pwd)
|
36
|
+
@tag_prefix = case options[:tag_prefix]
|
37
|
+
when nil then
|
38
|
+
DEFAULT_TAG_PREFIX
|
39
|
+
when false
|
40
|
+
""
|
41
|
+
else
|
42
|
+
options[:tag_prefix]
|
43
|
+
end
|
44
|
+
@version = options.fetch(:version) do
|
45
|
+
raise ArgumentError, ":version must be set"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Applies a version tag on a git repository and pushes it to the origin
|
50
|
+
# remote.
|
51
|
+
#
|
52
|
+
def run
|
53
|
+
guard_clean
|
54
|
+
tag_version { git_push } unless already_tagged?
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
DEFAULT_TAG_PREFIX = "v".freeze
|
60
|
+
|
61
|
+
attr_reader :logger, :source_path, :tag_prefix, :version
|
62
|
+
|
63
|
+
def already_tagged?
|
64
|
+
if sh_with_code('git tag')[0].split(/\n/).include?(version_tag)
|
65
|
+
info("Tag #{version_tag} has already been created.")
|
66
|
+
true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def clean?
|
71
|
+
sh_with_code("git status --porcelain")[0].empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def git_push
|
75
|
+
perform_git_push
|
76
|
+
perform_git_push ' --tags'
|
77
|
+
info("Pushed git commits and tags.")
|
78
|
+
end
|
79
|
+
|
80
|
+
def guard_clean
|
81
|
+
clean? or raise GitNotCleanError,
|
82
|
+
"There are files that need to be committed first."
|
83
|
+
end
|
84
|
+
|
85
|
+
def perform_git_push(options = '')
|
86
|
+
cmd = "git push origin master #{options}"
|
87
|
+
out, code = sh_with_code(cmd)
|
88
|
+
if code != 0
|
89
|
+
raise GitPushError,
|
90
|
+
"Couldn't git push. `#{cmd}' failed with the following output:" +
|
91
|
+
"\n\n#{out}\n"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def sh_with_code(cmd, &block)
|
96
|
+
cmd << " 2>&1"
|
97
|
+
outbuf = ''
|
98
|
+
debug(cmd)
|
99
|
+
Dir.chdir(source_path) {
|
100
|
+
outbuf = `#{cmd}`
|
101
|
+
if $? == 0
|
102
|
+
block.call(outbuf) if block
|
103
|
+
end
|
104
|
+
}
|
105
|
+
[outbuf, $?]
|
106
|
+
end
|
107
|
+
|
108
|
+
def tag_version
|
109
|
+
sh_with_code(%{git tag -a -m \"Version #{version}\" #{version_tag}})
|
110
|
+
info("Tagged #{version_tag}.")
|
111
|
+
yield if block_given?
|
112
|
+
rescue
|
113
|
+
error("Untagging #{version_tag} due to error.")
|
114
|
+
sh_with_code("git tag -d #{version_tag}")
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
|
118
|
+
def version_tag
|
119
|
+
"#{tag_prefix}#{version}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Emeril
|
4
|
+
|
5
|
+
# A mixin providing log methods that gracefully fail if no logger is present.
|
6
|
+
#
|
7
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
8
|
+
#
|
9
|
+
module Logging
|
10
|
+
|
11
|
+
%w{debug info warn error fatal}.map(&:to_sym).each do |meth|
|
12
|
+
define_method(meth) do |*args|
|
13
|
+
logger && logger.public_send(meth, *args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Emeril
|
4
|
+
|
5
|
+
# Exception class raised when there is a metadata.rb parsing issue.
|
6
|
+
#
|
7
|
+
class MetadataParseError < StandardError ; end
|
8
|
+
|
9
|
+
# A rather insane and questionable class to quickly consume a metadata.rb
|
10
|
+
# file and return the cookbook name and version attributes.
|
11
|
+
#
|
12
|
+
# @see https://twitter.com/fnichol/status/281650077901144064
|
13
|
+
# @see https://gist.github.com/4343327
|
14
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
15
|
+
#
|
16
|
+
class MetadataChopper < Hash
|
17
|
+
|
18
|
+
# Creates a new instances and loads in the contents of the metdata.rb
|
19
|
+
# file. If you value your life, you may want to avoid reading the
|
20
|
+
# implementation.
|
21
|
+
#
|
22
|
+
# @param metadata_file [String] path to a metadata.rb file
|
23
|
+
#
|
24
|
+
def initialize(metadata_file)
|
25
|
+
eval(IO.read(metadata_file), nil, metadata_file)
|
26
|
+
%w{name version}.map(&:to_sym).each do |attr|
|
27
|
+
if self[attr].nil?
|
28
|
+
raise MetadataParseError,
|
29
|
+
"Missing attribute `#{attr}' must be set in #{metadata_file}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing(meth, *args, &block)
|
35
|
+
self[meth] = args.first
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'chef/cookbook_uploader'
|
4
|
+
require 'chef/cookbook_loader'
|
5
|
+
require 'chef/cookbook_site_streaming_uploader'
|
6
|
+
require 'chef/knife/cookbook_site_share'
|
7
|
+
require 'chef/knife/core/ui'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'tmpdir'
|
10
|
+
|
11
|
+
require 'emeril/logging'
|
12
|
+
|
13
|
+
module Emeril
|
14
|
+
|
15
|
+
# Takes a path to a cookbook and pushes it up to the Community Site.
|
16
|
+
#
|
17
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
18
|
+
#
|
19
|
+
class Publisher
|
20
|
+
|
21
|
+
include Logging
|
22
|
+
|
23
|
+
# Creates a new instance.
|
24
|
+
#
|
25
|
+
# @param [Hash] options configuration for a publisher
|
26
|
+
# @option options [Logger] an optional logger instance
|
27
|
+
# @option options [String] source_path the path to a git repository
|
28
|
+
# @option options [String] name (required) the name of the cookbook
|
29
|
+
# @option options [String] category a Community Site category for the
|
30
|
+
# cookbook
|
31
|
+
# @option options [Chef::Knife] knife_class an alternate Knife plugin class
|
32
|
+
# to create, configure, and invoke
|
33
|
+
# @raise [ArgumentError] if any required options are not set
|
34
|
+
#
|
35
|
+
def initialize(options = {})
|
36
|
+
@logger = options[:logger]
|
37
|
+
@source_path = options.fetch(:source_path, Dir.pwd)
|
38
|
+
@name = options.fetch(:name) { raise ArgumentError, ":name must be set" }
|
39
|
+
@category = options[:category]
|
40
|
+
@knife_class = options.fetch(:knife_class, SharePlugin)
|
41
|
+
validate_chef_config!
|
42
|
+
end
|
43
|
+
|
44
|
+
# Prepares a sandbox copy of the cookbook and uploads it to the Community
|
45
|
+
# Site.
|
46
|
+
#
|
47
|
+
def run
|
48
|
+
sandbox_path = sandbox_cookbook
|
49
|
+
share = knife_class.new
|
50
|
+
share.ui = logging_ui(share.ui)
|
51
|
+
share.config[:cookbook_path] = sandbox_path
|
52
|
+
share.name_args = [name, category]
|
53
|
+
share.run
|
54
|
+
ensure
|
55
|
+
FileUtils.remove_dir(sandbox_path)
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
attr_reader :logger, :source_path, :name, :category, :knife_class
|
61
|
+
|
62
|
+
def validate_chef_config!
|
63
|
+
%w{node_name client_key}.map(&:to_sym).each do |attr|
|
64
|
+
raise "Chef::Config[:#{attr}] must be set" if ::Chef::Config[attr].nil?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def sandbox_cookbook
|
69
|
+
path = Dir.mktmpdir
|
70
|
+
target = File.join(path, name)
|
71
|
+
debug("Creating cookbook sanbox directory at #{target}")
|
72
|
+
FileUtils.mkdir_p(target)
|
73
|
+
FileUtils.cp_r(cookbook_files, target)
|
74
|
+
path
|
75
|
+
end
|
76
|
+
|
77
|
+
def cookbook_files
|
78
|
+
entries = %w{
|
79
|
+
README.* CHANGELOG.* metadata.{json,rb}
|
80
|
+
attributes files libraries providers recipes resources templates
|
81
|
+
}
|
82
|
+
|
83
|
+
Dir.glob("#{source_path}/{#{entries.join(',')}}")
|
84
|
+
end
|
85
|
+
|
86
|
+
def logging_ui(ui)
|
87
|
+
LoggingUI.new(ui.stdout, ui.stderr, ui.stdin, ui.config, logger)
|
88
|
+
end
|
89
|
+
|
90
|
+
# A custom knife UI that sends logging methods to a logger, if it exists.
|
91
|
+
#
|
92
|
+
class LoggingUI < :: Chef::Knife::UI
|
93
|
+
|
94
|
+
def initialize(stdout, stderr, stdin, config, logger)
|
95
|
+
super(stdout, stderr, stdin, config)
|
96
|
+
@logger = logger
|
97
|
+
end
|
98
|
+
|
99
|
+
def msg(message)
|
100
|
+
logger ? logger.info(message) : super
|
101
|
+
end
|
102
|
+
|
103
|
+
def err(message)
|
104
|
+
logger ? logger.error(message) : super
|
105
|
+
end
|
106
|
+
|
107
|
+
def warn(message)
|
108
|
+
logger ? logger.warn(message) : super
|
109
|
+
end
|
110
|
+
|
111
|
+
def fatal(message)
|
112
|
+
logger ? logger.fatal(message) : super
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
attr_reader :logger
|
118
|
+
end
|
119
|
+
|
120
|
+
# A custom cookbook site share knife plugin that intercepts Kernel#exit
|
121
|
+
# calls and converts them to an exception raise.
|
122
|
+
#
|
123
|
+
class SharePlugin < ::Chef::Knife::CookbookSiteShare
|
124
|
+
|
125
|
+
def exit(code)
|
126
|
+
raise "Knife Plugin exited with error code: #{code}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/emeril/rake.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'rake/tasklib'
|
4
|
+
require 'chef/knife'
|
5
|
+
|
6
|
+
require 'emeril'
|
7
|
+
|
8
|
+
module Emeril
|
9
|
+
|
10
|
+
# Emeril Rake task generator.
|
11
|
+
#
|
12
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
13
|
+
#
|
14
|
+
class RakeTasks < ::Rake::TaskLib
|
15
|
+
|
16
|
+
attr_accessor :config
|
17
|
+
|
18
|
+
# Creates Emeril Rake tasks and allows the callee to configure it.
|
19
|
+
#
|
20
|
+
# @yield [self] gives itself to the block
|
21
|
+
#
|
22
|
+
def initialize
|
23
|
+
@config = { :logger => Chef::Log }
|
24
|
+
yield self if block_given?
|
25
|
+
define
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def define
|
31
|
+
metadata = Emeril::MetadataChopper.new("metadata.rb")
|
32
|
+
|
33
|
+
desc "Create git tag for #{metadata[:name]}-#{metadata[:version]}" +
|
34
|
+
" and push to the Community Site"
|
35
|
+
task "release" do
|
36
|
+
Chef::Knife.new.configure_chef
|
37
|
+
Emeril::Releaser.new(@config).run
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|