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