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
@@ -2,22 +2,31 @@ module PuppetForge
|
|
2
2
|
module V3
|
3
3
|
class Base
|
4
4
|
|
5
|
-
#
|
6
|
-
|
7
|
-
|
5
|
+
# Enables navigation of the Forge API's paginated datasets.
|
6
|
+
class PaginatedCollection < Array
|
7
|
+
|
8
|
+
# Default pagination limit for API request
|
9
|
+
LIMIT = 20
|
8
10
|
|
9
|
-
# In addition to the standard Her::Collection arguments, this
|
10
|
-
# constructor requires a reference to the paginated class. This enables
|
11
|
-
# the collection to load related pages.
|
12
|
-
#
|
13
11
|
# @api private
|
14
|
-
# @param klass [
|
12
|
+
# @param klass [PuppetForge::V3::Base] the class to page over
|
15
13
|
# @param data [Array] the current data page
|
16
14
|
# @param metadata [Hash<(:limit, :total, :offset)>] page metadata
|
17
15
|
# @param errors [Object] errors for the page request
|
18
|
-
def initialize(klass, data, metadata, errors)
|
19
|
-
super(
|
16
|
+
def initialize(klass, data = [], metadata = {:total => 0, :offset => 0, :limit => LIMIT}, errors = nil)
|
17
|
+
super()
|
18
|
+
@metadata = metadata
|
19
|
+
@errors = errors
|
20
20
|
@klass = klass
|
21
|
+
|
22
|
+
data.each do |item|
|
23
|
+
self << @klass.new(item)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# For backwards compatibility, all returns the current object.
|
28
|
+
def all
|
29
|
+
self
|
21
30
|
end
|
22
31
|
|
23
32
|
# An enumerator that iterates over the entire collection, independent
|
@@ -26,7 +35,7 @@ module PuppetForge
|
|
26
35
|
#
|
27
36
|
# @return [Enumerator] an iterator for the entire collection
|
28
37
|
def unpaginated
|
29
|
-
page = @klass.get_collection(metadata[:first])
|
38
|
+
page = @klass.get_collection(@metadata[:first])
|
30
39
|
Enumerator.new do |emitter|
|
31
40
|
loop do
|
32
41
|
page.each { |x| emitter << x }
|
@@ -42,7 +51,7 @@ module PuppetForge
|
|
42
51
|
# @!method offset
|
43
52
|
# @return [Integer] the offset for the current page
|
44
53
|
[ :total, :limit, :offset ].each do |info|
|
45
|
-
define_method(info) { metadata[info] }
|
54
|
+
define_method(info) { @metadata[info] }
|
46
55
|
end
|
47
56
|
|
48
57
|
[ :next, :previous ].each do |link|
|
@@ -53,7 +62,7 @@ module PuppetForge
|
|
53
62
|
# Returns the previous page if a previous page exists.
|
54
63
|
# @return [PaginatedCollection, nil] the previous page
|
55
64
|
define_method(link) do
|
56
|
-
return unless path = metadata[link]
|
65
|
+
return unless path = @metadata[link]
|
57
66
|
@klass.get_collection(path)
|
58
67
|
end
|
59
68
|
|
@@ -64,7 +73,7 @@ module PuppetForge
|
|
64
73
|
# Returns the url of the previous page if a previous page exists.
|
65
74
|
# @return [String, nil] the previous page's url
|
66
75
|
define_method("#{link}_url") do
|
67
|
-
metadata[link]
|
76
|
+
@metadata[link]
|
68
77
|
end
|
69
78
|
end
|
70
79
|
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require 'set'
|
4
|
+
require 'semantic_puppet/version'
|
5
|
+
|
6
|
+
module PuppetForge
|
7
|
+
module V3
|
8
|
+
# This class provides a data structure representing a module's metadata.
|
9
|
+
# @api private
|
10
|
+
class Metadata
|
11
|
+
|
12
|
+
attr_accessor :module_name
|
13
|
+
|
14
|
+
DEFAULTS = {
|
15
|
+
'name' => nil,
|
16
|
+
'version' => nil,
|
17
|
+
'author' => nil,
|
18
|
+
'summary' => nil,
|
19
|
+
'license' => 'Apache-2.0',
|
20
|
+
'source' => '',
|
21
|
+
'project_page' => nil,
|
22
|
+
'issues_url' => nil,
|
23
|
+
'dependencies' => Set.new.freeze,
|
24
|
+
}
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@data = DEFAULTS.dup
|
28
|
+
@data['dependencies'] = @data['dependencies'].dup
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a filesystem-friendly version of this module name.
|
32
|
+
def dashed_name
|
33
|
+
PuppetForge::V3.normalize_name(@data['name']) if @data['name']
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a string that uniquely represents this version of this module.
|
37
|
+
def release_name
|
38
|
+
return nil unless @data['name'] && @data['version']
|
39
|
+
[ dashed_name, @data['version'] ].join('-')
|
40
|
+
end
|
41
|
+
|
42
|
+
alias :name :module_name
|
43
|
+
alias :full_module_name :dashed_name
|
44
|
+
|
45
|
+
# Merges the current set of metadata with another metadata hash. This
|
46
|
+
# method also handles the validation of module names and versions, in an
|
47
|
+
# effort to be proactive about module publishing constraints.
|
48
|
+
def update(data, with_dependencies = true)
|
49
|
+
process_name(data) if data['name']
|
50
|
+
process_version(data) if data['version']
|
51
|
+
process_source(data) if data['source']
|
52
|
+
merge_dependencies(data) if with_dependencies && data['dependencies']
|
53
|
+
|
54
|
+
@data.merge!(data)
|
55
|
+
return self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates the name and version_requirement for a dependency, then creates
|
59
|
+
# the Dependency and adds it.
|
60
|
+
# Returns the Dependency that was added.
|
61
|
+
def add_dependency(name, version_requirement=nil, repository=nil)
|
62
|
+
validate_name(name)
|
63
|
+
validate_version_range(version_requirement) if version_requirement
|
64
|
+
|
65
|
+
if dup = @data['dependencies'].find { |d| d.full_module_name == name && d.version_requirement != version_requirement }
|
66
|
+
raise ArgumentError, "Dependency conflict for #{full_module_name}: Dependency #{name} was given conflicting version requirements #{version_requirement} and #{dup.version_requirement}. Verify that there are no duplicates in the metadata.json or the Modulefile."
|
67
|
+
end
|
68
|
+
|
69
|
+
dep = Dependency.new(name, version_requirement, repository)
|
70
|
+
@data['dependencies'].add(dep)
|
71
|
+
|
72
|
+
dep
|
73
|
+
end
|
74
|
+
|
75
|
+
# Provides an accessor for the now defunct 'description' property. This
|
76
|
+
# addresses a regression in Puppet 3.6.x where previously valid templates
|
77
|
+
# refering to the 'description' property were broken.
|
78
|
+
# @deprecated
|
79
|
+
def description
|
80
|
+
@data['description']
|
81
|
+
end
|
82
|
+
|
83
|
+
def dependencies
|
84
|
+
@data['dependencies'].to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns a hash of the module's metadata. Used by Puppet's automated
|
88
|
+
# serialization routines.
|
89
|
+
#
|
90
|
+
# @see Puppet::Network::FormatSupport#to_data_hash
|
91
|
+
def to_hash
|
92
|
+
@data
|
93
|
+
end
|
94
|
+
alias :to_data_hash :to_hash
|
95
|
+
|
96
|
+
def to_json
|
97
|
+
data = @data.dup.merge('dependencies' => dependencies)
|
98
|
+
|
99
|
+
contents = data.keys.map do |k|
|
100
|
+
value = (JSON.pretty_generate(data[k]) rescue data[k].to_json)
|
101
|
+
"#{k.to_json}: #{value}"
|
102
|
+
end
|
103
|
+
|
104
|
+
"{\n" + contents.join(",\n").gsub(/^/, ' ') + "\n}\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Expose any metadata keys as callable reader methods.
|
108
|
+
def method_missing(name, *args)
|
109
|
+
return @data[name.to_s] if @data.key? name.to_s
|
110
|
+
super
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Do basic validation and parsing of the name parameter.
|
116
|
+
def process_name(data)
|
117
|
+
validate_name(data['name'])
|
118
|
+
author, @module_name = data['name'].split(/[-\/]/, 2)
|
119
|
+
|
120
|
+
data['author'] ||= author if @data['author'] == DEFAULTS['author']
|
121
|
+
end
|
122
|
+
|
123
|
+
# Do basic validation on the version parameter.
|
124
|
+
def process_version(data)
|
125
|
+
validate_version(data['version'])
|
126
|
+
end
|
127
|
+
|
128
|
+
# Do basic parsing of the source parameter. If the source is hosted on
|
129
|
+
# GitHub, we can predict sensible defaults for both project_page and
|
130
|
+
# issues_url.
|
131
|
+
def process_source(data)
|
132
|
+
if data['source'] =~ %r[://]
|
133
|
+
source_uri = URI.parse(data['source'])
|
134
|
+
else
|
135
|
+
source_uri = URI.parse("http://#{data['source']}")
|
136
|
+
end
|
137
|
+
|
138
|
+
if source_uri.host =~ /^(www\.)?github\.com$/
|
139
|
+
source_uri.scheme = 'https'
|
140
|
+
source_uri.path.sub!(/\.git$/, '')
|
141
|
+
data['project_page'] ||= @data['project_page'] || source_uri.to_s
|
142
|
+
data['issues_url'] ||= @data['issues_url'] || source_uri.to_s.sub(/\/*$/, '') + '/issues'
|
143
|
+
end
|
144
|
+
|
145
|
+
rescue URI::Error
|
146
|
+
return
|
147
|
+
end
|
148
|
+
|
149
|
+
# Validates and parses the dependencies.
|
150
|
+
def merge_dependencies(data)
|
151
|
+
data['dependencies'].each do |dep|
|
152
|
+
add_dependency(dep['name'], dep['version_requirement'], dep['repository'])
|
153
|
+
end
|
154
|
+
|
155
|
+
# Clear dependencies so @data dependencies are not overwritten
|
156
|
+
data.delete 'dependencies'
|
157
|
+
end
|
158
|
+
|
159
|
+
# Validates that the given module name is both namespaced and well-formed.
|
160
|
+
def validate_name(name)
|
161
|
+
return if name =~ /\A[a-z0-9]+[-\/][a-z][a-z0-9_]*\Z/i
|
162
|
+
|
163
|
+
namespace, modname = name.split(/[-\/]/, 2)
|
164
|
+
modname = :namespace_missing if namespace == ''
|
165
|
+
|
166
|
+
err = case modname
|
167
|
+
when nil, '', :namespace_missing
|
168
|
+
"the field must be a namespaced module name"
|
169
|
+
when /[^a-z0-9_]/i
|
170
|
+
"the module name contains non-alphanumeric (or underscore) characters"
|
171
|
+
when /^[^a-z]/i
|
172
|
+
"the module name must begin with a letter"
|
173
|
+
else
|
174
|
+
"the namespace contains non-alphanumeric characters"
|
175
|
+
end
|
176
|
+
|
177
|
+
raise ArgumentError, "Invalid 'name' field in metadata.json: #{err}"
|
178
|
+
end
|
179
|
+
|
180
|
+
# Validates that the version string can be parsed by SemanticPuppet.
|
181
|
+
def validate_version(version)
|
182
|
+
return if SemanticPuppet::Version.valid?(version)
|
183
|
+
|
184
|
+
err = "version string cannot be parsed as a valid Semantic Version"
|
185
|
+
raise ArgumentError, "Invalid 'version' field in metadata.json: #{err}"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Validates that the version range can be parsed by SemanticPuppet.
|
189
|
+
def validate_version_range(version_range)
|
190
|
+
SemanticPuppet::VersionRange.parse(version_range)
|
191
|
+
rescue ArgumentError => e
|
192
|
+
raise ArgumentError, "Invalid 'version_range' field in metadata.json: #{e}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
@@ -6,8 +6,8 @@ module PuppetForge
|
|
6
6
|
|
7
7
|
# Models a specific release version of a Puppet Module on the Forge.
|
8
8
|
class Release < Base
|
9
|
-
lazy :module
|
10
|
-
|
9
|
+
lazy :module, 'Module'
|
10
|
+
|
11
11
|
# Returns a fully qualified URL for downloading this release from the Forge.
|
12
12
|
#
|
13
13
|
# @return [String] fully qualified download URL for release
|
@@ -21,15 +21,40 @@ module PuppetForge
|
|
21
21
|
|
22
22
|
# Downloads the Release tarball to the specified file path.
|
23
23
|
#
|
24
|
-
# @
|
25
|
-
# @param file [String] the file to create
|
24
|
+
# @param path [Pathname]
|
26
25
|
# @return [void]
|
27
|
-
def download(
|
28
|
-
self.class.
|
29
|
-
|
26
|
+
def download(path)
|
27
|
+
resp = self.class.conn.get(file_url)
|
28
|
+
path.open('wb') { |fh| fh.write(resp.body) }
|
29
|
+
rescue Faraday::ResourceNotFound => e
|
30
|
+
raise PuppetForge::ReleaseNotFound, "The module release #{slug} does not exist on #{conn.url_prefix}.", e.backtrace
|
31
|
+
end
|
32
|
+
|
33
|
+
# Verify that a downloaded module matches the checksum in the metadata for this release.
|
34
|
+
#
|
35
|
+
# @param path [Pathname]
|
36
|
+
# @return [void]
|
37
|
+
def verify(path)
|
38
|
+
expected_md5 = file_md5
|
39
|
+
file_md5 = Digest::MD5.file(path).hexdigest
|
40
|
+
if expected_md5 != file_md5
|
41
|
+
raise ChecksumMismatch.new("Expected #{path} checksum to be #{expected_md5}, got #{file_md5}")
|
30
42
|
end
|
31
|
-
nil
|
32
43
|
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def file_url
|
48
|
+
"/v3/files/#{slug}.tar.gz"
|
49
|
+
end
|
50
|
+
|
51
|
+
def resource_url
|
52
|
+
"/v3/releases/#{slug}"
|
53
|
+
end
|
54
|
+
|
55
|
+
class ChecksumMismatch < StandardError
|
56
|
+
end
|
57
|
+
|
33
58
|
end
|
34
59
|
end
|
35
60
|
end
|
data/lib/puppet_forge/v3/user.rb
CHANGED
data/lib/puppet_forge/version.rb
CHANGED
data/puppet_forge.gemspec
CHANGED
@@ -8,7 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = PuppetForge::VERSION
|
9
9
|
spec.authors = ["Puppet Labs"]
|
10
10
|
spec.email = ["forge-team+api@puppetlabs.com"]
|
11
|
-
spec.summary = "Access
|
11
|
+
spec.summary = "Access the Puppet Forge API from Ruby for resource information and to download releases."
|
12
|
+
spec.description = %q{Tools that can be used to access Forge API information on Modules, Users, and Releases. As well as download, unpack, and install Releases to a directory.}
|
12
13
|
spec.homepage = "https://github.com/puppetlabs/forge-ruby"
|
13
14
|
spec.license = "Apache-2.0"
|
14
15
|
|
@@ -19,8 +20,10 @@ Gem::Specification.new do |spec|
|
|
19
20
|
|
20
21
|
spec.required_ruby_version = '>= 1.9.3'
|
21
22
|
|
22
|
-
spec.add_runtime_dependency "
|
23
|
-
spec.add_runtime_dependency "
|
23
|
+
spec.add_runtime_dependency "faraday", "~> 0.9.0"
|
24
|
+
spec.add_runtime_dependency "faraday_middleware", "~> 0.9.0"
|
25
|
+
spec.add_dependency 'semantic_puppet', '~> 0.1.0'
|
26
|
+
spec.add_dependency 'minitar'
|
24
27
|
|
25
28
|
spec.add_development_dependency "bundler", "~> 1.6"
|
26
29
|
spec.add_development_dependency "rake"
|
@@ -1,16 +1,19 @@
|
|
1
1
|
{
|
2
2
|
"uri": "/v3/modules/puppetlabs-apache",
|
3
|
+
"slug": "puppetlabs-apache",
|
3
4
|
"name": "apache",
|
4
5
|
"downloads": 81387,
|
5
6
|
"created_at": "2010-05-20 22:43:19 -0700",
|
6
7
|
"updated_at": "2014-01-06 14:42:07 -0800",
|
7
8
|
"owner": {
|
8
9
|
"uri": "/v3/users/puppetlabs",
|
10
|
+
"slug": "puppetlabs",
|
9
11
|
"username": "puppetlabs",
|
10
12
|
"gravatar_id": "fdd009b7c1ec96e088b389f773e87aec"
|
11
13
|
},
|
12
14
|
"current_release": {
|
13
15
|
"uri": "/v3/releases/puppetlabs-apache-0.10.0",
|
16
|
+
"slug": "puppetlabs-apache-0.10.0",
|
14
17
|
"module": {
|
15
18
|
"uri": "/v3/modules/puppetlabs-apache",
|
16
19
|
"name": "apache",
|
@@ -318,73 +321,90 @@
|
|
318
321
|
"releases": [
|
319
322
|
{
|
320
323
|
"uri": "/v3/releases/puppetlabs-apache-0.10.0",
|
324
|
+
"slug": "puppetlabs-apache-0.10.0",
|
321
325
|
"version": "0.10.0"
|
322
326
|
},
|
323
327
|
{
|
324
328
|
"uri": "/v3/releases/puppetlabs-apache-0.9.0",
|
329
|
+
"slug": "puppetlabs-apache-0.9.0",
|
325
330
|
"version": "0.9.0"
|
326
331
|
},
|
327
332
|
{
|
328
333
|
"uri": "/v3/releases/puppetlabs-apache-0.8.1",
|
334
|
+
"slug": "puppetlabs-apache-0.8.1",
|
329
335
|
"version": "0.8.1"
|
330
336
|
},
|
331
337
|
{
|
332
338
|
"uri": "/v3/releases/puppetlabs-apache-0.8.0",
|
339
|
+
"slug": "puppetlabs-apache-0.8.0",
|
333
340
|
"version": "0.8.0"
|
334
341
|
},
|
335
342
|
{
|
336
343
|
"uri": "/v3/releases/puppetlabs-apache-0.7.0",
|
344
|
+
"slug": "puppetlabs-apache-0.7.0",
|
337
345
|
"version": "0.7.0"
|
338
346
|
},
|
339
347
|
{
|
340
348
|
"uri": "/v3/releases/puppetlabs-apache-0.6.0",
|
349
|
+
"slug": "puppetlabs-apache-0.6.0",
|
341
350
|
"version": "0.6.0"
|
342
351
|
},
|
343
352
|
{
|
344
353
|
"uri": "/v3/releases/puppetlabs-apache-0.5.0-rc1",
|
354
|
+
"slug": "puppetlabs-apache-0.5.0-rc1",
|
345
355
|
"version": "0.5.0-rc1"
|
346
356
|
},
|
347
357
|
{
|
348
358
|
"uri": "/v3/releases/puppetlabs-apache-0.4.0",
|
359
|
+
"slug": "puppetlabs-apache-0.4.0",
|
349
360
|
"version": "0.4.0"
|
350
361
|
},
|
351
362
|
{
|
352
363
|
"uri": "/v3/releases/puppetlabs-apache-0.3.0",
|
364
|
+
"slug": "puppetlabs-apache-0.3.0",
|
353
365
|
"version": "0.3.0"
|
354
366
|
},
|
355
367
|
{
|
356
368
|
"uri": "/v3/releases/puppetlabs-apache-0.2.2",
|
369
|
+
"slug": "puppetlabs-apache-0.2.2",
|
357
370
|
"version": "0.2.2"
|
358
371
|
},
|
359
372
|
{
|
360
373
|
"uri": "/v3/releases/puppetlabs-apache-0.2.1",
|
374
|
+
"slug": "puppetlabs-apache-0.2.1",
|
361
375
|
"version": "0.2.1"
|
362
376
|
},
|
363
377
|
{
|
364
378
|
"uri": "/v3/releases/puppetlabs-apache-0.2.0",
|
379
|
+
"slug": "puppetlabs-apache-0.2.0",
|
365
380
|
"version": "0.2.0"
|
366
381
|
},
|
367
382
|
{
|
368
383
|
"uri": "/v3/releases/puppetlabs-apache-0.1.1",
|
384
|
+
"slug": "puppetlabs-apache-0.1.1",
|
369
385
|
"version": "0.1.1"
|
370
386
|
},
|
371
387
|
{
|
372
388
|
"uri": "/v3/releases/puppetlabs-apache-0.0.4",
|
389
|
+
"slug": "puppetlabs-apache-0.0.4",
|
373
390
|
"version": "0.0.4"
|
374
391
|
},
|
375
392
|
{
|
376
393
|
"uri": "/v3/releases/puppetlabs-apache-0.0.3",
|
394
|
+
"slug": "puppetlabs-apache-0.0.3",
|
377
395
|
"version": "0.0.3"
|
378
396
|
},
|
379
397
|
{
|
380
398
|
"uri": "/v3/releases/puppetlabs-apache-0.0.2",
|
399
|
+
"slug": "puppetlabs-apache-0.0.2",
|
381
400
|
"version": "0.0.2"
|
382
401
|
},
|
383
402
|
{
|
384
403
|
"uri": "/v3/releases/puppetlabs-apache-0.0.1",
|
404
|
+
"slug": "puppetlabs-apache-0.0.1",
|
385
405
|
"version": "0.0.1"
|
386
406
|
}
|
387
407
|
],
|
388
408
|
"homepage_url": "https://github.com/puppetlabs/puppetlabs-apache",
|
389
409
|
"issues_url": "https://tickets.puppetlabs.com"
|
390
|
-
}
|
410
|
+
}
|