chef-stove 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/bin/stove +5 -0
  4. data/lib/stove.rb +87 -0
  5. data/lib/stove/artifactory.rb +83 -0
  6. data/lib/stove/cli.rb +199 -0
  7. data/lib/stove/config.rb +76 -0
  8. data/lib/stove/cookbook.rb +120 -0
  9. data/lib/stove/cookbook/metadata.rb +245 -0
  10. data/lib/stove/error.rb +56 -0
  11. data/lib/stove/filter.rb +60 -0
  12. data/lib/stove/mash.rb +25 -0
  13. data/lib/stove/mixins/insideable.rb +13 -0
  14. data/lib/stove/mixins/instanceable.rb +24 -0
  15. data/lib/stove/mixins/optionable.rb +41 -0
  16. data/lib/stove/mixins/validatable.rb +11 -0
  17. data/lib/stove/packager.rb +156 -0
  18. data/lib/stove/plugins/artifactory.rb +14 -0
  19. data/lib/stove/plugins/base.rb +48 -0
  20. data/lib/stove/plugins/git.rb +70 -0
  21. data/lib/stove/plugins/supermarket.rb +18 -0
  22. data/lib/stove/rake_task.rb +22 -0
  23. data/lib/stove/runner.rb +39 -0
  24. data/lib/stove/supermarket.rb +79 -0
  25. data/lib/stove/util.rb +56 -0
  26. data/lib/stove/validator.rb +68 -0
  27. data/lib/stove/version.rb +3 -0
  28. data/templates/errors/abstract_method.erb +5 -0
  29. data/templates/errors/artifactory_key_validation_failed.erb +11 -0
  30. data/templates/errors/git_clean_validation_failed.erb +1 -0
  31. data/templates/errors/git_failed.erb +5 -0
  32. data/templates/errors/git_repository_validation_failed.erb +3 -0
  33. data/templates/errors/git_tagging_failed.erb +5 -0
  34. data/templates/errors/git_up_to_date_validation_failed.erb +7 -0
  35. data/templates/errors/metadata_not_found.erb +1 -0
  36. data/templates/errors/server_unavailable.erb +1 -0
  37. data/templates/errors/stove_error.erb +1 -0
  38. data/templates/errors/supermarket_already_exists.erb +5 -0
  39. data/templates/errors/supermarket_key_validation_failed.erb +3 -0
  40. data/templates/errors/supermarket_username_validation_failed.erb +3 -0
  41. metadata +212 -0
@@ -0,0 +1,60 @@
1
+ module Stove
2
+ class Filter
3
+ include Logify
4
+
5
+ include Mixin::Insideable
6
+
7
+ #
8
+ # The class that created this filter.
9
+ #
10
+ # @return [~Plugin::Base]
11
+ #
12
+ attr_reader :klass
13
+
14
+ #
15
+ # The message given by the filter.
16
+ #
17
+ # @return [String]
18
+ #
19
+ attr_reader :message
20
+
21
+ #
22
+ # The block captured by the filter.
23
+ #
24
+ # @return [Proc]
25
+ #
26
+ attr_reader :block
27
+
28
+ #
29
+ # Create a new filter object.
30
+ #
31
+ # @param [~Plugin::Base] klass
32
+ # the class that created this filter
33
+ # @param [String] message
34
+ # the message given by the filter
35
+ # @param [Proc] block
36
+ # the block captured by this filter
37
+ #
38
+ def initialize(klass, message, &block)
39
+ @klass = klass
40
+ @message = message
41
+ @block = block
42
+ end
43
+
44
+ #
45
+ # Execute this filter in the context of the creating class, inside the
46
+ # given cookbook's path.
47
+ #
48
+ # @param [Cookbook]
49
+ # the cookbook to run this filter against
50
+ #
51
+ def run(cookbook, options = {})
52
+ log.info(message)
53
+ instance = klass.new(cookbook, options)
54
+
55
+ inside(cookbook) do
56
+ instance.instance_eval(&block)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module Stove
2
+ class Mash < ::Hash
3
+ def method_missing(m, *args, &block)
4
+ if has_key?(m.to_sym)
5
+ self[m.to_sym]
6
+ elsif has_key?(m.to_s)
7
+ self[m.to_s]
8
+ else
9
+ super
10
+ end
11
+ end
12
+
13
+ def methods(include_private = false)
14
+ super + self.keys.map(&:to_sym)
15
+ end
16
+
17
+ def respond_to?(m, include_private = false)
18
+ if has_key?(m.to_sym) || has_key?(m.to_s)
19
+ true
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module Stove
2
+ module Mixin::Insideable
3
+ #
4
+ # Execute the command inside the cookbook.
5
+ #
6
+ # @param [Cookbook]
7
+ # the cookbook to execute inside of
8
+ #
9
+ def inside(cookbook, &block)
10
+ Dir.chdir(cookbook.path, &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ require 'singleton'
2
+
3
+ module Stove
4
+ module Mixin::Instanceable
5
+ def self.included(base)
6
+ base.send(:include, Singleton)
7
+ base.send(:extend, ClassMethods)
8
+ end
9
+
10
+ def self.extended(base)
11
+ base.send(:include, Singleton)
12
+ base.send(:extend, ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ def to_s; instance.to_s; end
17
+ def inspect; instance.inspect; end
18
+
19
+ def method_missing(m, *args, &block)
20
+ instance.send(m, *args, &block)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ module Stove
2
+ module Mixin::Optionable
3
+ def self.included(base)
4
+ base.send(:extend, ClassMethods)
5
+ end
6
+
7
+ def self.extended(base)
8
+ base.send(:extend, ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ #
13
+ # This is a magical method. It does three things:
14
+ #
15
+ # 1. Defines a class method getter and setter for the given option
16
+ # 2. Defines an instance method that delegates to the class method
17
+ # 3. (Optionally) sets the initial value
18
+ #
19
+ # @param [String, Symbol] name
20
+ # the name of the option
21
+ # @param [Object] initial
22
+ # the initial value to set (optional)
23
+ #
24
+ def option(name, initial = UNSET_VALUE)
25
+ define_singleton_method(name) do |value = UNSET_VALUE|
26
+ if value == UNSET_VALUE
27
+ instance_variable_get("@#{name}")
28
+ else
29
+ instance_variable_set("@#{name}", value)
30
+ end
31
+ end
32
+
33
+ define_method(name) { self.class.send(name) }
34
+
35
+ unless initial == UNSET_VALUE
36
+ send(name, initial)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module Stove
2
+ module Mixin::Validatable
3
+ def validate(id, &block)
4
+ validations[id] = Validator.new(self, id, &block)
5
+ end
6
+
7
+ def validations
8
+ @validations ||= {}
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,156 @@
1
+ require 'rubygems/package'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'zlib'
5
+
6
+ module Stove
7
+ class Packager
8
+ include Logify
9
+
10
+ ACCEPTABLE_FILES = [
11
+ '.foodcritic',
12
+ 'README.*',
13
+ 'CHANGELOG.*',
14
+ 'CONTRIBUTING.md',
15
+ 'MAINTAINERS.md',
16
+ 'metadata.{json,rb}',
17
+ 'attributes/*.rb',
18
+ 'definitions/*.rb',
19
+ 'files/**/*',
20
+ 'libraries/**/*.rb',
21
+ 'providers/**/*.rb',
22
+ 'recipes/*.rb',
23
+ 'resources/**/*.rb',
24
+ 'templates/**/*',
25
+ ].freeze
26
+
27
+ ACCEPTABLE_FILES_LIST = ACCEPTABLE_FILES.join(',').freeze
28
+
29
+ TMP_FILES = [
30
+ /^(?:.*[\\\/])?\.[^\\\/]+\.sw[p-z]$/,
31
+ /~$/,
32
+ ].freeze
33
+
34
+ # The cookbook to package.
35
+ #
36
+ # @erturn [Stove::Cookbook]
37
+ attr_reader :cookbook
38
+
39
+ # Whether to include the new extended metadata attributes.
40
+ #
41
+ # @return [true, false]
42
+ attr_reader :extended_metadata
43
+
44
+ # Create a new packager instance.
45
+ #
46
+ # @param [Stove::Cookbook]
47
+ # the cookbook to package
48
+ # @param [true, false] extended_metadata
49
+ # include new extended metadata attributes
50
+ def initialize(cookbook, extended_metadata = false)
51
+ @cookbook = cookbook
52
+ @extended_metadata = extended_metadata
53
+ end
54
+
55
+ # A map from physical file path to tarball file path
56
+ #
57
+ # @example
58
+ # # Assuming +cookbook.name+ is 'apt'
59
+ #
60
+ # {
61
+ # '/home/user/apt-cookbook/metadata.json' => 'apt/metadata.json',
62
+ # '/home/user/apt-cookbook/README.md' => 'apt/README.md'
63
+ # }
64
+ #
65
+ # @return [Hash<String, String>]
66
+ # the map of file paths
67
+ def packaging_slip
68
+ root = File.expand_path(cookbook.path)
69
+ path = File.join(root, "{#{ACCEPTABLE_FILES_LIST}}")
70
+
71
+ Dir.glob(path, File::FNM_DOTMATCH)
72
+ .reject { |path| %w(. ..).include?(File.basename(path)) }
73
+ .reject { |path| TMP_FILES.any? { |regex| path.match(regex) } }
74
+ .map { |path| [path, path.sub(/^#{Regexp.escape(root)}/, cookbook.name)] }
75
+ .reduce({}) do |map, (cookbook_file, tarball_file)|
76
+ map[cookbook_file] = tarball_file
77
+ map
78
+ end
79
+ end
80
+
81
+ def tarball
82
+ # Generate the metadata.json on the fly
83
+ metadata_json = File.join(cookbook.path, 'metadata.json')
84
+ json = JSON.fast_generate(cookbook.metadata.to_hash(extended_metadata))
85
+ File.open(metadata_json, 'wb') { |f| f.write(json) }
86
+
87
+ io = tar(File.dirname(cookbook.path), packaging_slip)
88
+ tgz = gzip(io)
89
+
90
+ tempfile = Tempfile.new([cookbook.name, '.tar.gz'], Dir.tmpdir)
91
+ tempfile.binmode
92
+
93
+ while buffer = tgz.read(1024)
94
+ tempfile.write(buffer)
95
+ end
96
+
97
+ tempfile.rewind
98
+ tempfile
99
+ ensure
100
+ if defined?(metadata_json) && File.exist?(File.join(cookbook.path, 'metadata.rb'))
101
+ File.delete(metadata_json)
102
+ end
103
+ end
104
+
105
+ #
106
+ # Create a tar file from the given root and packaging slip
107
+ #
108
+ # @param [String] root
109
+ # the root where the tar files are being created
110
+ # @param [Hash<String, String>] slip
111
+ # the map from physical file path to tarball file path
112
+ #
113
+ # @return [StringIO]
114
+ # the io object that contains the tarball contents
115
+ #
116
+ def tar(root, slip)
117
+ io = StringIO.new('', 'r+b')
118
+ Gem::Package::TarWriter.new(io) do |tar|
119
+ slip.each do |original_file, tarball_file|
120
+ mode = File.stat(original_file).mode
121
+
122
+ if File.directory?(original_file)
123
+ tar.mkdir(tarball_file, mode)
124
+ else
125
+ tar.add_file(tarball_file, mode) do |tf|
126
+ File.open(original_file, 'rb') { |f| tf.write(f.read) }
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ io.rewind
133
+ io
134
+ end
135
+
136
+ #
137
+ # GZip the given IO object (like a File or StringIO).
138
+ #
139
+ # @param [IO] io
140
+ # the io object to gzip
141
+ #
142
+ # @return [IO]
143
+ # the gzipped IO object
144
+ #
145
+ def gzip(io)
146
+ gz = StringIO.new('')
147
+ z = Zlib::GzipWriter.new(gz)
148
+ z.write(io.string)
149
+ z.close
150
+
151
+ # z was closed to write the gzip footer, so
152
+ # now we need a new StringIO
153
+ StringIO.new(gz.string)
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,14 @@
1
+ module Stove
2
+ class Plugin::Artifactory < Plugin::Base
3
+ id 'artifactory'
4
+ description 'Publish the release to an Artifactory server'
5
+
6
+ validate(:key) do
7
+ Config.artifactory_key && !Config.artifactory_key.strip.empty?
8
+ end
9
+
10
+ run('Publishing the release to the Artifactory server') do
11
+ Artifactory.upload(cookbook, options[:extended_metadata])
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ module Stove
2
+ class Plugin::Base
3
+ include Logify
4
+
5
+ extend Mixin::Optionable
6
+ extend Mixin::Validatable
7
+
8
+ class << self
9
+ def run(description, &block)
10
+ actions << Proc.new do |instance|
11
+ log.info { description }
12
+ instance.instance_eval(&block)
13
+ end
14
+ end
15
+
16
+ def actions
17
+ @actions ||= []
18
+ end
19
+ end
20
+
21
+ option :id
22
+ option :description
23
+
24
+ attr_reader :cookbook
25
+ attr_reader :options
26
+
27
+ def initialize(cookbook, options = {})
28
+ @cookbook, @options = cookbook, options
29
+ end
30
+
31
+ def run
32
+ run_validations
33
+ run_actions
34
+ end
35
+
36
+ def run_validations
37
+ self.class.validations.each do |id, validation|
38
+ validation.run(cookbook, options)
39
+ end
40
+ end
41
+
42
+ def run_actions
43
+ self.class.actions.each do |action|
44
+ action.call(self)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,70 @@
1
+ module Stove
2
+ class Plugin::Git < Plugin::Base
3
+ id 'git'
4
+ description 'Tag and push to a git remote'
5
+
6
+ validate(:repository) do
7
+ File.directory?(File.join(Dir.pwd, '.git'))
8
+ end
9
+
10
+ validate(:clean) do
11
+ git_null('status --porcelain').strip.empty?
12
+ end
13
+
14
+ validate(:up_to_date) do
15
+ git_null('fetch')
16
+ local_sha = git_null("rev-parse #{branch}").strip
17
+ remote_sha = git_null("rev-parse #{remote}/#{branch}").strip
18
+
19
+ log.debug("Local SHA: #{local_sha}")
20
+ log.debug("Remote SHA: #{remote_sha}")
21
+
22
+ local_sha == remote_sha
23
+ end
24
+
25
+ run('Tagging new release') do
26
+ annotation_type = options[:sign] ? '-s' : '-a'
27
+ tag = cookbook.tag_version
28
+
29
+ git %|tag #{annotation_type} #{tag} -m "Release #{tag}"|
30
+ git %|push #{remote} #{branch}|
31
+ git %|push #{remote} #{tag}|
32
+ end
33
+
34
+ private
35
+
36
+ def git(command, errors = true)
37
+ log.debug("the command matches")
38
+ log.debug("Running `git #{command}', errors: #{errors}")
39
+ Dir.chdir(cookbook.path) do
40
+ response = %x|git #{command}|
41
+
42
+ if errors && !$?.success?
43
+ raise Error::GitTaggingFailed.new(command: command) if command =~ /^tag/
44
+ raise Error::GitFailed.new(command: command)
45
+ end
46
+
47
+ response
48
+ end
49
+ end
50
+
51
+ def git_null(command)
52
+ null = case RbConfig::CONFIG['host_os']
53
+ when /mswin|mingw|cygwin/
54
+ 'NUL'
55
+ else
56
+ '/dev/null'
57
+ end
58
+
59
+ git("#{command} 2>#{null}", false)
60
+ end
61
+
62
+ def remote
63
+ options[:remote]
64
+ end
65
+
66
+ def branch
67
+ options[:branch]
68
+ end
69
+ end
70
+ end