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/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,11 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'emeril/category'
4
+ require 'emeril/git_tagger'
5
+ require 'emeril/metadata_chopper'
6
+ require 'emeril/publisher'
7
+ require 'emeril/releaser'
8
+ require 'emeril/version'
9
+
10
+ module Emeril
11
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'emeril/rake_tasks'
4
+
5
+ Emeril::RakeTasks.new
@@ -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