emeril 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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