chef-stove 7.1.1

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.
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