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.
Files changed (45) hide show
  1. data/CHANGELOG.md +23 -0
  2. data/MAINTAINERS +13 -0
  3. data/README.md +48 -6
  4. data/lib/puppet_forge.rb +4 -0
  5. data/lib/puppet_forge/connection.rb +81 -0
  6. data/lib/puppet_forge/connection/connection_failure.rb +26 -0
  7. data/lib/puppet_forge/error.rb +34 -0
  8. data/lib/{her → puppet_forge}/lazy_accessors.rb +20 -27
  9. data/lib/{her → puppet_forge}/lazy_relations.rb +28 -9
  10. data/lib/puppet_forge/middleware/symbolify_json.rb +72 -0
  11. data/lib/puppet_forge/tar.rb +10 -0
  12. data/lib/puppet_forge/tar/mini.rb +81 -0
  13. data/lib/puppet_forge/unpacker.rb +68 -0
  14. data/lib/puppet_forge/v3.rb +11 -0
  15. data/lib/puppet_forge/v3/base.rb +106 -73
  16. data/lib/puppet_forge/v3/base/paginated_collection.rb +23 -14
  17. data/lib/puppet_forge/v3/metadata.rb +197 -0
  18. data/lib/puppet_forge/v3/module.rb +2 -1
  19. data/lib/puppet_forge/v3/release.rb +33 -8
  20. data/lib/puppet_forge/v3/user.rb +2 -0
  21. data/lib/puppet_forge/version.rb +1 -1
  22. data/puppet_forge.gemspec +6 -3
  23. data/spec/fixtures/v3/modules/puppetlabs-apache.json +21 -1
  24. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.1.json +4 -1
  25. data/spec/integration/forge/v3/module_spec.rb +79 -0
  26. data/spec/integration/forge/v3/release_spec.rb +75 -0
  27. data/spec/integration/forge/v3/user_spec.rb +70 -0
  28. data/spec/spec_helper.rb +15 -8
  29. data/spec/unit/forge/connection/connection_failure_spec.rb +30 -0
  30. data/spec/unit/forge/connection_spec.rb +53 -0
  31. data/spec/unit/{her → forge}/lazy_accessors_spec.rb +20 -13
  32. data/spec/unit/{her → forge}/lazy_relations_spec.rb +60 -46
  33. data/spec/unit/forge/middleware/symbolify_json_spec.rb +63 -0
  34. data/spec/unit/forge/tar/mini_spec.rb +85 -0
  35. data/spec/unit/forge/tar_spec.rb +9 -0
  36. data/spec/unit/forge/unpacker_spec.rb +58 -0
  37. data/spec/unit/forge/v3/base/paginated_collection_spec.rb +68 -46
  38. data/spec/unit/forge/v3/base_spec.rb +1 -1
  39. data/spec/unit/forge/v3/metadata_spec.rb +300 -0
  40. data/spec/unit/forge/v3/module_spec.rb +14 -36
  41. data/spec/unit/forge/v3/release_spec.rb +9 -30
  42. data/spec/unit/forge/v3/user_spec.rb +7 -7
  43. metadata +127 -41
  44. checksums.yaml +0 -7
  45. 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,10 @@
1
+
2
+ module PuppetForge
3
+ class Tar
4
+ require 'puppet_forge/tar/mini'
5
+
6
+ def self.instance
7
+ Mini.new
8
+ end
9
+ end
10
+ end
@@ -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
@@ -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'
@@ -1,97 +1,130 @@
1
- require 'her'
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. This class provides
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 Her::Model
18
- include Her::LazyAccessors
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
- Her::API.new :url => "#{PuppetForge.host}/v3/" do |c|
32
- c.use PuppetForge::Middleware::JSONForHer
33
- c.use adapter
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
- # Overrides Her::Model#request to allow end users to dynamically update
39
- # both the Forge host being communicated with and the user agent string.
40
- #
41
- # @api private
42
- # @api her
43
- # @see Her::Model#request
44
- # @see PuppetForge.host
45
- # @see PuppetForge.user_agent
46
- def request(*args)
47
- unless her_api.base_uri =~ /^#{PuppetForge.host}/
48
- her_api.connection.url_prefix = "#{PuppetForge.host}/v3/"
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
- her_api.connection.headers[:user_agent] = %W[
52
- #{PuppetForge.user_agent}
53
- PuppetForge.gem/#{PuppetForge::VERSION}
54
- Her/#{Her::VERSION}
55
- Faraday/#{Faraday::VERSION}
56
- Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})
57
- ].join(' ').strip
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
- super
77
+ self.new(resp.body)
60
78
  end
61
79
 
62
- # Overrides Her::Model#new_collection with custom logic for handling the
63
- # paginated collections produced by the Forge API. These collections are
64
- # then wrapped in a {PaginatedCollection}, which enables navigation of
65
- # the paginated dataset.
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
- # FIXME: We should provide an actual unique identifier.
81
- primary_key :slug
82
- store_metadata :_metadata
83
- after_initialize do
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
- # Since our data is primarily URI based rather than ID based, we should
88
- # use our URIs as the request_path whenever possible.
89
- #
90
- # @api private
91
- # @api her
92
- # @see Her::Model::Paths#request_path
93
- def request_path(*args)
94
- if has_attribute? :uri then uri else super end
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