chef-stove 7.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/bin/stove +5 -0
- data/lib/stove.rb +87 -0
- data/lib/stove/artifactory.rb +83 -0
- data/lib/stove/cli.rb +199 -0
- data/lib/stove/config.rb +76 -0
- data/lib/stove/cookbook.rb +120 -0
- data/lib/stove/cookbook/metadata.rb +245 -0
- data/lib/stove/error.rb +56 -0
- data/lib/stove/filter.rb +60 -0
- data/lib/stove/mash.rb +25 -0
- data/lib/stove/mixins/insideable.rb +13 -0
- data/lib/stove/mixins/instanceable.rb +24 -0
- data/lib/stove/mixins/optionable.rb +41 -0
- data/lib/stove/mixins/validatable.rb +11 -0
- data/lib/stove/packager.rb +156 -0
- data/lib/stove/plugins/artifactory.rb +14 -0
- data/lib/stove/plugins/base.rb +48 -0
- data/lib/stove/plugins/git.rb +70 -0
- data/lib/stove/plugins/supermarket.rb +18 -0
- data/lib/stove/rake_task.rb +22 -0
- data/lib/stove/runner.rb +39 -0
- data/lib/stove/supermarket.rb +79 -0
- data/lib/stove/util.rb +56 -0
- data/lib/stove/validator.rb +68 -0
- data/lib/stove/version.rb +3 -0
- data/templates/errors/abstract_method.erb +5 -0
- data/templates/errors/artifactory_key_validation_failed.erb +11 -0
- data/templates/errors/git_clean_validation_failed.erb +1 -0
- data/templates/errors/git_failed.erb +5 -0
- data/templates/errors/git_repository_validation_failed.erb +3 -0
- data/templates/errors/git_tagging_failed.erb +5 -0
- data/templates/errors/git_up_to_date_validation_failed.erb +7 -0
- data/templates/errors/metadata_not_found.erb +1 -0
- data/templates/errors/server_unavailable.erb +1 -0
- data/templates/errors/stove_error.erb +1 -0
- data/templates/errors/supermarket_already_exists.erb +5 -0
- data/templates/errors/supermarket_key_validation_failed.erb +3 -0
- data/templates/errors/supermarket_username_validation_failed.erb +3 -0
- metadata +212 -0
data/lib/stove/filter.rb
ADDED
@@ -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
|
data/lib/stove/mash.rb
ADDED
@@ -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,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,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
|