puppet_forge 1.0.6 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +23 -0
- data/MAINTAINERS +13 -0
- data/README.md +48 -6
- data/lib/puppet_forge.rb +4 -0
- data/lib/puppet_forge/connection.rb +81 -0
- data/lib/puppet_forge/connection/connection_failure.rb +26 -0
- data/lib/puppet_forge/error.rb +34 -0
- data/lib/{her → puppet_forge}/lazy_accessors.rb +20 -27
- data/lib/{her → puppet_forge}/lazy_relations.rb +28 -9
- data/lib/puppet_forge/middleware/symbolify_json.rb +72 -0
- data/lib/puppet_forge/tar.rb +10 -0
- data/lib/puppet_forge/tar/mini.rb +81 -0
- data/lib/puppet_forge/unpacker.rb +68 -0
- data/lib/puppet_forge/v3.rb +11 -0
- data/lib/puppet_forge/v3/base.rb +106 -73
- data/lib/puppet_forge/v3/base/paginated_collection.rb +23 -14
- data/lib/puppet_forge/v3/metadata.rb +197 -0
- data/lib/puppet_forge/v3/module.rb +2 -1
- data/lib/puppet_forge/v3/release.rb +33 -8
- data/lib/puppet_forge/v3/user.rb +2 -0
- data/lib/puppet_forge/version.rb +1 -1
- data/puppet_forge.gemspec +6 -3
- data/spec/fixtures/v3/modules/puppetlabs-apache.json +21 -1
- data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.1.json +4 -1
- data/spec/integration/forge/v3/module_spec.rb +79 -0
- data/spec/integration/forge/v3/release_spec.rb +75 -0
- data/spec/integration/forge/v3/user_spec.rb +70 -0
- data/spec/spec_helper.rb +15 -8
- data/spec/unit/forge/connection/connection_failure_spec.rb +30 -0
- data/spec/unit/forge/connection_spec.rb +53 -0
- data/spec/unit/{her → forge}/lazy_accessors_spec.rb +20 -13
- data/spec/unit/{her → forge}/lazy_relations_spec.rb +60 -46
- data/spec/unit/forge/middleware/symbolify_json_spec.rb +63 -0
- data/spec/unit/forge/tar/mini_spec.rb +85 -0
- data/spec/unit/forge/tar_spec.rb +9 -0
- data/spec/unit/forge/unpacker_spec.rb +58 -0
- data/spec/unit/forge/v3/base/paginated_collection_spec.rb +68 -46
- data/spec/unit/forge/v3/base_spec.rb +1 -1
- data/spec/unit/forge/v3/metadata_spec.rb +300 -0
- data/spec/unit/forge/v3/module_spec.rb +14 -36
- data/spec/unit/forge/v3/release_spec.rb +9 -30
- data/spec/unit/forge/v3/user_spec.rb +7 -7
- metadata +127 -41
- checksums.yaml +0 -7
- data/lib/puppet_forge/middleware/json_for_her.rb +0 -37
@@ -0,0 +1,72 @@
|
|
1
|
+
module PuppetForge
|
2
|
+
module Middleware
|
3
|
+
|
4
|
+
# SymbolifyJson is a Faraday Middleware that will process any response formatted as a hash
|
5
|
+
# and change all the keys into symbols (as long as they respond to the method #to_sym.
|
6
|
+
#
|
7
|
+
# This middleware makes no changes to the values of the hash.
|
8
|
+
# If the response is not a hash, no changes will be made.
|
9
|
+
class SymbolifyJson < Faraday::Middleware
|
10
|
+
|
11
|
+
# Processes an array
|
12
|
+
#
|
13
|
+
# @return an array with any hash's keys turned into symbols if possible
|
14
|
+
def process_array(array)
|
15
|
+
array.map do |arg|
|
16
|
+
# Search any arrays and hashes for hash keys
|
17
|
+
if arg.is_a? Hash
|
18
|
+
process_hash(arg)
|
19
|
+
elsif arg.is_a? Array
|
20
|
+
process_array(arg)
|
21
|
+
else
|
22
|
+
arg
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Processes a hash
|
28
|
+
#
|
29
|
+
# @return a hash with all keys turned into symbols if possible
|
30
|
+
def process_hash(hash)
|
31
|
+
|
32
|
+
# hash.map returns an array in the format
|
33
|
+
# [ [key, value], [key2, value2], ... ]
|
34
|
+
# Hash[] converts that into a hash in the format
|
35
|
+
# { key => value, key2 => value2, ... }
|
36
|
+
Hash[hash.map do |key, val|
|
37
|
+
# Convert to a symbol if possible
|
38
|
+
if key.respond_to? :to_sym
|
39
|
+
new_key = key.to_sym
|
40
|
+
else
|
41
|
+
new_key = key
|
42
|
+
end
|
43
|
+
|
44
|
+
# If value is a hash or array look for more hash keys inside.
|
45
|
+
if val.is_a?(Hash)
|
46
|
+
[new_key, process_hash(val)]
|
47
|
+
elsif val.is_a?(Array)
|
48
|
+
[new_key, process_array(val)]
|
49
|
+
else
|
50
|
+
[new_key, val]
|
51
|
+
end
|
52
|
+
end]
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_response(env)
|
56
|
+
if !env["body"].nil? && env["body"].is_a?(Hash)
|
57
|
+
process_hash(env.body)
|
58
|
+
else
|
59
|
+
env.body
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def call(environment)
|
64
|
+
@app.call(environment).on_complete do |env|
|
65
|
+
env.body = process_response(env)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'archive/tar/minitar'
|
3
|
+
|
4
|
+
module PuppetForge
|
5
|
+
class Tar
|
6
|
+
class Mini
|
7
|
+
|
8
|
+
SYMLINK_FLAGS = [2]
|
9
|
+
VALID_TAR_FLAGS = (0..7)
|
10
|
+
|
11
|
+
# @return [Hash{:symbol => Array<String>}] a hash with file-category keys pointing to lists of filenames.
|
12
|
+
def unpack(sourcefile, destdir)
|
13
|
+
# directories need to be changed outside of the Minitar::unpack because directories don't have a :file_done action
|
14
|
+
dirlist = []
|
15
|
+
file_lists = {}
|
16
|
+
Zlib::GzipReader.open(sourcefile) do |reader|
|
17
|
+
file_lists = validate_files(reader)
|
18
|
+
Archive::Tar::Minitar.unpack(reader, destdir, file_lists[:valid]) do |action, name, stats|
|
19
|
+
case action
|
20
|
+
when :file_done
|
21
|
+
FileUtils.chmod('u+rw,g+r,a-st', "#{destdir}/#{name}")
|
22
|
+
when :file_start
|
23
|
+
validate_entry(destdir, name)
|
24
|
+
when :dir
|
25
|
+
validate_entry(destdir, name)
|
26
|
+
dirlist << "#{destdir}/#{name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
dirlist.each {|d| File.chmod(0755, d)}
|
31
|
+
file_lists
|
32
|
+
end
|
33
|
+
|
34
|
+
def pack(sourcedir, destfile)
|
35
|
+
Zlib::GzipWriter.open(destfile) do |writer|
|
36
|
+
Archive::Tar::Minitar.pack(sourcedir, writer)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Categorize all the files in tarfile as :valid, :invalid, or :symlink.
|
43
|
+
#
|
44
|
+
# :invalid files include 'x' and 'g' flags from the PAX standard but and any other non-standard tar flags.
|
45
|
+
# tar format info: http://pic.dhe.ibm.com/infocenter/zos/v1r13/index.jsp?topic=%2Fcom.ibm.zos.r13.bpxa500%2Ftaf.htm
|
46
|
+
# pax format info: http://pic.dhe.ibm.com/infocenter/zos/v1r13/index.jsp?topic=%2Fcom.ibm.zos.r13.bpxa500%2Fpxarchfm.htm
|
47
|
+
# :symlinks are not supported in Puppet modules
|
48
|
+
# :valid files are any of those that can be used in modules
|
49
|
+
# @param tarfile name of the tarfile
|
50
|
+
# @return [Hash{:symbol => Array<String>}] a hash with file-category keys pointing to lists of filenames.
|
51
|
+
def validate_files(tarfile)
|
52
|
+
file_lists = {:valid => [], :invalid => [], :symlinks => []}
|
53
|
+
Archive::Tar::Minitar.open(tarfile).each do |entry|
|
54
|
+
flag = entry.typeflag
|
55
|
+
if flag.nil? || flag =~ /[[:digit:]]/ && SYMLINK_FLAGS.include?(flag.to_i)
|
56
|
+
file_lists[:symlinks] << entry.name
|
57
|
+
elsif flag.nil? || flag =~ /[[:digit:]]/ && VALID_TAR_FLAGS.include?(flag.to_i)
|
58
|
+
file_lists[:valid] << entry.name
|
59
|
+
else
|
60
|
+
file_lists[:invalid] << entry.name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
file_lists
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_entry(destdir, path)
|
67
|
+
if Pathname.new(path).absolute?
|
68
|
+
raise PuppetForge::InvalidPathInPackageError, :entry_path => path, :directory => destdir
|
69
|
+
end
|
70
|
+
|
71
|
+
path = File.expand_path File.join(destdir, path)
|
72
|
+
|
73
|
+
if path !~ /\A#{Regexp.escape destdir}/
|
74
|
+
raise PuppetForge::InvalidPathInPackageError, :entry_path => path, :directory => destdir
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'puppet_forge/error'
|
3
|
+
require 'puppet_forge/tar'
|
4
|
+
|
5
|
+
module PuppetForge
|
6
|
+
class Unpacker
|
7
|
+
# Unpack a tar file into a specified directory
|
8
|
+
#
|
9
|
+
# @param filename [String] the file to unpack
|
10
|
+
# @param target [String] the target directory to unpack into
|
11
|
+
# @return [Hash{:symbol => Array<String>}] a hash with file-category keys pointing to lists of filenames.
|
12
|
+
# The categories are :valid, :invalid and :symlink
|
13
|
+
def self.unpack(filename, target, tmpdir)
|
14
|
+
inst = self.new(filename, target, tmpdir)
|
15
|
+
file_lists = inst.unpack
|
16
|
+
inst.move_into(Pathname.new(target))
|
17
|
+
file_lists
|
18
|
+
end
|
19
|
+
|
20
|
+
# Set the owner/group of the target directory to those of the source
|
21
|
+
# Note: don't call this function on Microsoft Windows
|
22
|
+
#
|
23
|
+
# @param source [Pathname] source of the permissions
|
24
|
+
# @param target [Pathname] target of the permissions change
|
25
|
+
def self.harmonize_ownership(source, target)
|
26
|
+
FileUtils.chown_R(source.stat.uid, source.stat.gid, target)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param filename [String] the file to unpack
|
30
|
+
# @param target [String] the target directory to unpack into
|
31
|
+
def initialize(filename, target, tmpdir)
|
32
|
+
@filename = filename
|
33
|
+
@target = target
|
34
|
+
@tmpdir = tmpdir
|
35
|
+
end
|
36
|
+
|
37
|
+
# @api private
|
38
|
+
def unpack
|
39
|
+
begin
|
40
|
+
PuppetForge::Tar.instance.unpack(@filename, @tmpdir)
|
41
|
+
rescue PuppetForge::ExecutionFailure => e
|
42
|
+
raise RuntimeError, "Could not extract contents of module archive: #{e.message}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @api private
|
47
|
+
def move_into(dir)
|
48
|
+
dir.rmtree if dir.exist?
|
49
|
+
FileUtils.mv(root_dir, dir)
|
50
|
+
ensure
|
51
|
+
FileUtils.rmtree(@tmpdir)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def root_dir
|
56
|
+
return @root_dir if @root_dir
|
57
|
+
|
58
|
+
# Grab the first directory containing a metadata.json file
|
59
|
+
metadata_file = Dir["#{@tmpdir}/**/metadata.json"].sort_by(&:length)[0]
|
60
|
+
|
61
|
+
if metadata_file
|
62
|
+
@root_dir = Pathname.new(metadata_file).dirname
|
63
|
+
else
|
64
|
+
raise "No valid metadata.json found!"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/puppet_forge/v3.rb
CHANGED
@@ -2,9 +2,20 @@ module PuppetForge
|
|
2
2
|
|
3
3
|
# Models specific to the Puppet Forge's v3 API.
|
4
4
|
module V3
|
5
|
+
# Normalize a module name to use a hyphen as the separator between the
|
6
|
+
# author and module.
|
7
|
+
|
8
|
+
# @example
|
9
|
+
# PuppetForge::V3.normalize_name('my/module') #=> 'my-module'
|
10
|
+
# PuppetForge::V3.normalize_name('my-module') #=> 'my-module'
|
11
|
+
def self.normalize_name(name)
|
12
|
+
name.tr('/', '-')
|
13
|
+
end
|
5
14
|
end
|
6
15
|
end
|
7
16
|
|
17
|
+
require 'puppet_forge/v3/metadata'
|
18
|
+
|
8
19
|
require 'puppet_forge/v3/user'
|
9
20
|
require 'puppet_forge/v3/module'
|
10
21
|
require 'puppet_forge/v3/release'
|
data/lib/puppet_forge/v3/base.rb
CHANGED
@@ -1,97 +1,130 @@
|
|
1
|
-
require '
|
2
|
-
require 'her/lazy_accessors'
|
3
|
-
require 'her/lazy_relations'
|
4
|
-
|
5
|
-
require 'puppet_forge/middleware/json_for_her'
|
1
|
+
require 'puppet_forge/connection'
|
6
2
|
require 'puppet_forge/v3/base/paginated_collection'
|
3
|
+
require 'puppet_forge/error'
|
4
|
+
|
5
|
+
require 'puppet_forge/lazy_accessors'
|
6
|
+
require 'puppet_forge/lazy_relations'
|
7
7
|
|
8
8
|
module PuppetForge
|
9
9
|
module V3
|
10
10
|
|
11
|
-
# Acts as the base class for all PuppetForge::V3::* models.
|
12
|
-
# some overrides of behaviors from Her, in addition to convenience methods
|
13
|
-
# and abstractions of common behavior.
|
11
|
+
# Acts as the base class for all PuppetForge::V3::* models.
|
14
12
|
#
|
15
13
|
# @api private
|
16
14
|
class Base
|
17
|
-
include
|
18
|
-
include
|
19
|
-
include Her::LazyRelations
|
20
|
-
|
21
|
-
use_api begin
|
22
|
-
begin
|
23
|
-
# Use Typhoeus if available.
|
24
|
-
Gem::Specification.find_by_name('typhoeus', '~> 0.6')
|
25
|
-
require 'typhoeus/adapters/faraday'
|
26
|
-
adapter = Faraday::Adapter::Typhoeus
|
27
|
-
rescue Gem::LoadError
|
28
|
-
adapter = Faraday::Adapter::NetHttp
|
29
|
-
end
|
15
|
+
include PuppetForge::LazyAccessors
|
16
|
+
include PuppetForge::LazyRelations
|
30
17
|
|
31
|
-
|
32
|
-
|
33
|
-
|
18
|
+
def initialize(json_response)
|
19
|
+
@attributes = json_response
|
20
|
+
orm_resp_item json_response
|
21
|
+
end
|
22
|
+
|
23
|
+
def orm_resp_item(json_response)
|
24
|
+
json_response.each do |key, value|
|
25
|
+
unless respond_to? key
|
26
|
+
define_singleton_method("#{key}") { @attributes[key] }
|
27
|
+
define_singleton_method("#{key}=") { |val| @attributes[key] = val }
|
28
|
+
end
|
34
29
|
end
|
35
30
|
end
|
36
31
|
|
32
|
+
# @return true if attribute exists, false otherwise
|
33
|
+
#
|
34
|
+
def has_attribute?(attr)
|
35
|
+
@attributes.has_key?(:"#{attr}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def attribute(name)
|
39
|
+
@attributes[:"#{name}"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def attributes
|
43
|
+
@attributes
|
44
|
+
end
|
45
|
+
|
37
46
|
class << self
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
47
|
+
|
48
|
+
include PuppetForge::Connection
|
49
|
+
|
50
|
+
API_VERSION = "v3"
|
51
|
+
|
52
|
+
def api_version
|
53
|
+
API_VERSION
|
54
|
+
end
|
55
|
+
|
56
|
+
# @private
|
57
|
+
def request(resource, item = nil, params = {})
|
58
|
+
unless conn.url_prefix =~ /^#{PuppetForge.host}/
|
59
|
+
conn.url_prefix = "#{PuppetForge.host}"
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
if item.nil?
|
64
|
+
uri_path = "/v3/#{resource}"
|
65
|
+
else
|
66
|
+
uri_path = "/v3/#{resource}/#{item}"
|
49
67
|
end
|
50
68
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
69
|
+
PuppetForge::V3::Base.conn.get uri_path, params
|
70
|
+
end
|
71
|
+
|
72
|
+
def find(slug)
|
73
|
+
return nil if slug.nil?
|
74
|
+
|
75
|
+
resp = request("#{self.name.split("::").last.downcase}s", slug)
|
58
76
|
|
59
|
-
|
77
|
+
self.new(resp.body)
|
60
78
|
end
|
61
79
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
#
|
67
|
-
# @api private
|
68
|
-
# @api her
|
69
|
-
# @param parsed_data [Hash<(:data, :errors)>] the parsed response data
|
70
|
-
# @return [PaginatedCollection] the collection
|
71
|
-
def new_collection(parsed_data)
|
72
|
-
col = super :data => parsed_data[:data][:results] || [],
|
73
|
-
:metadata => parsed_data[:data][:pagination] || { limit: 10, total: 0, offset: 0 },
|
74
|
-
:errors => parsed_data[:errors]
|
75
|
-
|
76
|
-
PaginatedCollection.new(self, col.to_a, col.metadata, col.errors)
|
80
|
+
def where(params)
|
81
|
+
resp = request("#{self.name.split("::").last.downcase}s", nil, params)
|
82
|
+
|
83
|
+
new_collection(resp)
|
77
84
|
end
|
78
|
-
end
|
79
85
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
attributes[:slug] ||= uri[/[^\/]+$/]
|
85
|
-
end
|
86
|
+
# Return a paginated collection of all modules
|
87
|
+
def all(params = {})
|
88
|
+
where(params)
|
89
|
+
end
|
86
90
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
91
|
+
def get_collection(uri_path)
|
92
|
+
resource, params = split_uri_path uri_path
|
93
|
+
resp = request(resource, nil, params)
|
94
|
+
|
95
|
+
new_collection(resp)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Faraday's Util#escape method will replace a '+' with '%2B' to prevent it being
|
99
|
+
# interpreted as a space. For compatibility with the Forge API, we would like a '+'
|
100
|
+
# to be interpreted as a space so they are changed to spaces here.
|
101
|
+
def convert_plus_to_space(str)
|
102
|
+
str.gsub(/[+]/, ' ')
|
103
|
+
end
|
104
|
+
|
105
|
+
# @private
|
106
|
+
def split_uri_path(uri_path)
|
107
|
+
all, resource, params = /(?:\/v3\/)([^\/]+)(?:\?)(.*)/.match(uri_path).to_a
|
108
|
+
|
109
|
+
params = convert_plus_to_space(params).split('&')
|
110
|
+
|
111
|
+
param_hash = Hash.new
|
112
|
+
params.each do |param|
|
113
|
+
key, val = param.split('=')
|
114
|
+
param_hash[key] = val
|
115
|
+
end
|
116
|
+
|
117
|
+
[resource, param_hash]
|
118
|
+
end
|
119
|
+
|
120
|
+
# @private
|
121
|
+
def new_collection(faraday_resp)
|
122
|
+
if faraday_resp[:errors].nil?
|
123
|
+
PaginatedCollection.new(self, faraday_resp.body[:results], faraday_resp.body[:pagination], nil)
|
124
|
+
else
|
125
|
+
PaginatedCollection.new(self)
|
126
|
+
end
|
127
|
+
end
|
95
128
|
end
|
96
129
|
end
|
97
130
|
end
|