puppet-forge-server 1.8.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.travis.yml +3 -0
  4. data/lib/puppet_forge_server.rb +4 -0
  5. data/lib/puppet_forge_server/api/v1/modules.rb +7 -3
  6. data/lib/puppet_forge_server/api/v3/modules.rb +7 -2
  7. data/lib/puppet_forge_server/api/v3/releases.rb +9 -4
  8. data/lib/puppet_forge_server/app/frontend.rb +19 -0
  9. data/lib/puppet_forge_server/app/public/css/puppetlabs.css +236 -0
  10. data/lib/puppet_forge_server/app/views/layout.haml +1 -1
  11. data/lib/puppet_forge_server/app/views/module.haml +71 -0
  12. data/lib/puppet_forge_server/app/views/modules.haml +1 -1
  13. data/lib/puppet_forge_server/backends/directory.rb +12 -5
  14. data/lib/puppet_forge_server/backends/proxy.rb +29 -4
  15. data/lib/puppet_forge_server/backends/proxy_v3.rb +20 -6
  16. data/lib/puppet_forge_server/http/http_client.rb +29 -2
  17. data/lib/puppet_forge_server/logger.rb +9 -2
  18. data/lib/puppet_forge_server/models/builder.rb +6 -1
  19. data/lib/puppet_forge_server/models/metadata.rb +1 -1
  20. data/lib/puppet_forge_server/models/module.rb +2 -2
  21. data/lib/puppet_forge_server/server.rb +2 -0
  22. data/lib/puppet_forge_server/utils/archiver.rb +3 -1
  23. data/lib/puppet_forge_server/utils/cache_provider.rb +42 -0
  24. data/lib/puppet_forge_server/utils/encoding.rb +46 -0
  25. data/lib/puppet_forge_server/utils/filtering_inspecter.rb +30 -0
  26. data/lib/puppet_forge_server/utils/md_renderer.rb +54 -0
  27. data/lib/puppet_forge_server/utils/option_parser.rb +23 -1
  28. data/lib/puppet_forge_server/version.rb +1 -1
  29. data/puppet-forge-server.gemspec +8 -0
  30. data/spec/spec_helper.rb +41 -1
  31. data/spec/unit/http/http_client_spec.rb +93 -0
  32. data/spec/unit/utils/encoding_spec.rb +35 -0
  33. metadata +110 -2
@@ -20,7 +20,7 @@
20
20
  - modules.each do |element|
21
21
  %li{:class => element['private'] ? 'clearfix private' : 'clearfix'}
22
22
  .col
23
- %h3= "#{element['owner']['username']}/#{element['name']}"
23
+ %a{:class => "h3", :href => "/module?name=#{element['current_release']['metadata']['name']}" }= "#{element['owner']['username']}/#{element['name']}"
24
24
  %p= element['current_release']['metadata']['summary']
25
25
  %span.release-info= "Version #{element['current_release']['metadata']['version']}"
26
26
  - if element['current_release']['metadata']['issues_url']
@@ -56,9 +56,15 @@ module PuppetForgeServer::Backends
56
56
  end
57
57
 
58
58
  private
59
- def read_metadata(archive_path)
60
- metadata_file = read_from_archive(archive_path, %r[[^/]+/metadata\.json$])
61
- JSON.parse(metadata_file)
59
+ def read_module_data(archive_path)
60
+ file_contents = read_from_archive(archive_path, %r[^([^/]+/)?(metadata\.json|README\.md)$])
61
+ metadata = JSON.parse(file_contents.find {|key, value| key =~ /metadata\.json/}[1])
62
+ begin
63
+ readme = file_contents.find {|key, value| key =~ /README\.md/}[1]
64
+ rescue
65
+ readme = nil
66
+ end
67
+ [metadata, readme]
62
68
  rescue => error
63
69
  warn "Error reading from module archive #{archive_path}: #{error}"
64
70
  return nil
@@ -80,14 +86,15 @@ module PuppetForgeServer::Backends
80
86
  options = ({:with_checksum => true}).merge(options)
81
87
  modules = []
82
88
  Dir["#{@module_dir}/**/#{file_name}"].each do |path|
83
- metadata_raw = read_metadata(path)
89
+ metadata_raw, readme = read_module_data(path)
84
90
  if metadata_raw
85
91
  modules <<
86
92
  PuppetForgeServer::Models::Module.new({
87
93
  :metadata => parse_dependencies(PuppetForgeServer::Models::Metadata.new(normalize_metadata(metadata_raw))),
88
94
  :checksum => options[:with_checksum] == true ? Digest::MD5.file(path).hexdigest : nil,
89
95
  :path => "/#{File.basename(path)}",
90
- :private => ! @readonly
96
+ :private => ! @readonly,
97
+ :readme => readme
91
98
  })
92
99
  else
93
100
  @log.error "Failed reading metadata from #{path}"
@@ -35,14 +35,18 @@ module PuppetForgeServer::Backends
35
35
 
36
36
  def get_file_buffer(relative_path)
37
37
  file_name = relative_path.split('/').last
38
- File.join(@cache_dir, file_name[0].downcase, file_name)
38
+ target_file = File.join(@cache_dir, file_name[0].downcase, file_name)
39
39
  path = Dir["#{@cache_dir}/**/#{file_name}"].first
40
40
  unless File.exist?("#{path}")
41
41
  buffer = download("#{@file_path.chomp('/')}/#{relative_path}")
42
- File.open(File.join(@cache_dir, file_name[0].downcase, file_name), 'wb') do |file|
43
- file.write(buffer.read)
42
+ File.open(target_file, 'wb') do |file|
43
+ bytes = buffer.read
44
+ file.write(bytes)
45
+ @log.debug("Saved #{bytes.size} bytes in filesystem cache for path: #{relative_path}, target file: #{target_file}")
44
46
  end
45
- path = File.join(@cache_dir, file_name[0].downcase, file_name)
47
+ path = target_file
48
+ else
49
+ @log.info("Filesystem cache HIT for path: #{relative_path}")
46
50
  end
47
51
  File.open(path, 'rb')
48
52
  rescue => e
@@ -59,6 +63,27 @@ module PuppetForgeServer::Backends
59
63
  protected
60
64
  attr_reader :log
61
65
 
66
+ def get_non_mutable(relative_url)
67
+ file_name = relative_url.split('/').last
68
+ target_file = File.join(@cache_dir, file_name[0].downcase, file_name)
69
+ path = Dir["#{@cache_dir}/**/#{file_name}"].first
70
+ unless File.exist?("#{path}")
71
+ buffer = get(relative_url)
72
+ File.open(target_file, 'wb') do |file|
73
+ file.write(buffer)
74
+ @log.debug("Saved #{buffer} bytes in filesystem cache for url: #{relative_url}, target file: #{target_file}")
75
+ end
76
+ path = target_file
77
+ else
78
+ @log.info("Filesystem cache HIT for url: #{relative_url}")
79
+ end
80
+ File.binread(path)
81
+ rescue => e
82
+ @log.error("#{self.class.name} failed getting non-mutable url '#{relative_url}'")
83
+ @log.error("Error: #{e}")
84
+ return nil
85
+ end
86
+
62
87
  def get(relative_url)
63
88
  @http_client.get(url(relative_url))
64
89
  end
@@ -30,11 +30,11 @@ module PuppetForgeServer::Backends
30
30
  def get_metadata(author, name, options = {})
31
31
  query ="#{author}-#{name}"
32
32
  begin
33
- releases = options[:version] ? [JSON.parse(get("/v3/releases/#{query}-#{options[:version]}"))] : get_all_result_pages("/v3/releases?module=#{query}")
33
+ releases = get_releases(query, options)
34
34
  get_modules(releases)
35
35
  rescue => e
36
- @log.debug("#{self.class.name} failed querying metadata for '#{query}' with options #{options}")
37
- @log.debug("Error: #{e}")
36
+ @log.error("#{self.class.name} failed querying metadata for '#{query}' with options #{options}")
37
+ @log.error("Error: #{e}")
38
38
  return nil
39
39
  end
40
40
  end
@@ -44,13 +44,26 @@ module PuppetForgeServer::Backends
44
44
  releases = get_all_result_pages("/v3/modules?query=#{query}").map {|element| element['current_release']}
45
45
  get_modules(releases)
46
46
  rescue => e
47
- @log.debug("#{self.class.name} failed querying metadata for '#{query}' with options #{options}")
48
- @log.debug("Error: #{e}")
47
+ @log.error("#{self.class.name} failed querying metadata for '#{query}' with options #{options}")
48
+ @log.error("Error: #{e}")
49
49
  return nil
50
50
  end
51
51
  end
52
52
 
53
53
  private
54
+
55
+ def get_releases(query, options = {})
56
+ version = options[:version]
57
+ unless version.nil?
58
+ url = "/v3/releases/#{query}-#{version}"
59
+ buffer = get_non_mutable(url)
60
+ release = JSON.parse(buffer)
61
+ [ release ]
62
+ else
63
+ get_all_result_pages("/v3/releases?module=#{query}")
64
+ end
65
+ end
66
+
54
67
  def get_all_result_pages(next_page)
55
68
  results = []
56
69
  begin
@@ -80,7 +93,8 @@ module PuppetForgeServer::Backends
80
93
  :checksum => element['file_md5'],
81
94
  :path => element['file_uri'].gsub(/^#{@@FILE_PATH}/, ''),
82
95
  :tags => (element['tags'] + (element['metadata']['tags'] ? element['metadata']['tags'] : [])).flatten.uniq,
83
- :deleted_at => element['deleted_at']
96
+ :deleted_at => element['deleted_at'],
97
+ :readme => element['readme']
84
98
  })
85
99
  end
86
100
  end
@@ -23,6 +23,15 @@ require 'net/http/post/multipart'
23
23
 
24
24
  module PuppetForgeServer::Http
25
25
  class HttpClient
26
+ include PuppetForgeServer::Utils::CacheProvider
27
+ include PuppetForgeServer::Utils::FilteringInspecter
28
+
29
+ def initialize(cache = nil)
30
+ cache = cache_instance if cache.nil?
31
+ cache.extend(PuppetForgeServer::Utils::FilteringInspecter)
32
+ @log = PuppetForgeServer::Logger.get
33
+ @cache = cache
34
+ end
26
35
 
27
36
  def post_file(url, file_hash, options = {})
28
37
  options = { :http => {}, :headers => {}}.merge(options)
@@ -45,11 +54,29 @@ module PuppetForgeServer::Http
45
54
  open_uri(url)
46
55
  end
47
56
 
57
+ def inspect
58
+ cache_inspected = @cache.inspect_without [ :@data ]
59
+ cache_inspected.gsub!(/>$/, ", @size=#{@cache.size}>")
60
+ inspected = inspect_without [ :@cache ]
61
+ inspected.gsub(/>$/, ", @cache=#{cache_inspected}>")
62
+ end
63
+
48
64
  private
65
+
49
66
  def open_uri(url)
50
- ::Timeout.timeout(10) do
51
- open(url, 'User-Agent' => "Puppet-Forge-Server/#{PuppetForgeServer::VERSION}", :allow_redirections => :safe)
67
+ hit_or_miss = @cache.include?(url) ? 'HIT' : 'MISS'
68
+ @log.info "Cache in RAM memory size: #{@cache.size}, #{hit_or_miss} for url: #{url}"
69
+ contents = @cache.fetch(url) do
70
+ tmpfile = ::Timeout.timeout(10) do
71
+ PuppetForgeServer::Logger.get.debug "Fetching data for url: #{url} from remote server"
72
+ open(url, 'User-Agent' => "Puppet-Forge-Server/#{PuppetForgeServer::VERSION}", :allow_redirections => :safe)
73
+ end
74
+ contents = tmpfile.read
75
+ tmpfile.close
76
+ contents
52
77
  end
78
+ @log.debug "Data for url: #{url} fetched, #{contents.size} bytes"
79
+ StringIO.new(contents)
53
80
  end
54
81
  end
55
82
  end
@@ -16,6 +16,7 @@
16
16
 
17
17
 
18
18
  require 'logger'
19
+ require 'logger/colors'
19
20
 
20
21
  module PuppetForgeServer
21
22
  class Logger
@@ -45,7 +46,13 @@ module PuppetForgeServer
45
46
  else
46
47
  method_name
47
48
  end
48
- @loggers.each { |logger| logger.send(method_name, args.first) }
49
+ if args.size > 0
50
+ # setters
51
+ @loggers.each { |logger| logger.send(method_name, args.first) }
52
+ else
53
+ # getters
54
+ @loggers.collect { |logger| logger.send(method_name) }
55
+ end
49
56
  end
50
57
 
51
58
  def respond_to?(method_name, include_private = false)
@@ -70,4 +77,4 @@ module PuppetForgeServer
70
77
  end
71
78
  end
72
79
  end
73
- end
80
+ end
@@ -27,6 +27,11 @@ module PuppetForgeServer::Models
27
27
  end
28
28
 
29
29
  def to_hash(obj = self)
30
+ # Quickly return if it's not one of ours
31
+ unless obj.kind_of? Builder
32
+ return obj
33
+ end
34
+ # Otherwise start building hash
30
35
  hash = {}
31
36
  obj.instance_variables.each do |var|
32
37
  var_value = obj.instance_variable_get(var)
@@ -41,4 +46,4 @@ module PuppetForgeServer::Models
41
46
  hash
42
47
  end
43
48
  end
44
- end
49
+ end
@@ -44,4 +44,4 @@ module PuppetForgeServer::Models
44
44
  @version.eql?(other.version)
45
45
  end
46
46
  end
47
- end
47
+ end
@@ -18,7 +18,7 @@
18
18
  module PuppetForgeServer::Models
19
19
  class Module < Builder
20
20
 
21
- attr_accessor :metadata, :checksum, :path, :private, :deleted_at, :tags
21
+ attr_accessor :metadata, :checksum, :path, :private, :deleted_at, :tags, :readme
22
22
 
23
23
  def initialize(attributes)
24
24
  super(attributes)
@@ -26,4 +26,4 @@ module PuppetForgeServer::Models
26
26
  end
27
27
 
28
28
  end
29
- end
29
+ end
@@ -19,6 +19,7 @@ require 'rack/mount'
19
19
  module PuppetForgeServer
20
20
  class Server
21
21
  include PuppetForgeServer::Utils::OptionParser
22
+ include PuppetForgeServer::Utils::CacheProvider
22
23
  include PuppetForgeServer::Utils::Http
23
24
 
24
25
  def go(args)
@@ -27,6 +28,7 @@ module PuppetForgeServer
27
28
  begin
28
29
  options = parse_options(args)
29
30
  @log = logging(options)
31
+ configure_cache(options[:ram_cache_ttl], options[:ram_cache_size])
30
32
  backends = backends(options)
31
33
  server = build(backends, options[:webui_root])
32
34
  announce(options, backends)
@@ -22,9 +22,11 @@ module PuppetForgeServer::Utils
22
22
  def read_from_archive(archive, name_regex)
23
23
  tar = Gem::Package::TarReader.new(Zlib::GzipReader.open(archive))
24
24
  tar.rewind
25
+ files = {}
25
26
  tar.each do |obj|
26
- return obj.read if obj.full_name =~ name_regex
27
+ files[obj.full_name] = obj.read if obj.file? && obj.full_name =~ name_regex
27
28
  end
29
+ return files unless files.empty?
28
30
  raise "Given name #{name_regex} not found in #{archive}"
29
31
  end
30
32
  end
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Copyright 2015 Centralny Osroder Informatyki (gov.pl)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lrucache'
18
+
19
+ module PuppetForgeServer::Utils
20
+ module CacheProvider
21
+
22
+ opts = PuppetForgeServer::Utils::OptionParser.DEFAULT_OPTIONS
23
+ @@CACHE = LRUCache.new(:ttl => opts[:ram_cache_ttl], :max_size => opts[:ram_cache_size])
24
+
25
+ # Method for fetching application wide cache for fetching HTTP requests
26
+ #
27
+ # @return [LRUCache] a instance of cache for application
28
+ def cache_instance
29
+ @@CACHE
30
+ end
31
+
32
+ # Configure a application wide cache using LSUCache implementation
33
+ #
34
+ # @param [int] ttl a time to live for elements
35
+ # @param [int] size a maximum size for cache
36
+ def configure_cache(ttl, size)
37
+ @@CACHE = LRUCache.new(:ttl => ttl, :max_size => size)
38
+ PuppetForgeServer::Logger.get.info("Using RAM memory LRUCache with time to live of #{ttl}sec and max size of #{size} elements")
39
+ nil
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Copyright 2015 Centralny Osroder Informatyki (gov.pl)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'iconv'
18
+
19
+ module PuppetForgeServer::Utils
20
+ module Encoding
21
+
22
+ @@ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
23
+
24
+ # Converts give text to valid UTF-8
25
+ # @param [string] text given string, can be null
26
+ # @return [string] output string in utf-8
27
+ def to_utf8(text)
28
+ replaced = text
29
+ unless replaced.nil?
30
+ replaced = replaced.force_encoding("UTF-8") if is_ascii_8bit?(replaced)
31
+ replaced = cleanup_utf8(replaced)
32
+ end
33
+ replaced
34
+ end
35
+
36
+ private
37
+
38
+ def is_ascii_8bit?(text)
39
+ text.encoding.name == 'ASCII-8BIT'
40
+ end
41
+
42
+ def cleanup_utf8(text)
43
+ @@ic.iconv(text)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Copyright 2015 Centralny Osroder Informatyki (gov.pl)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module PuppetForgeServer::Utils
18
+ module FilteringInspecter
19
+ def self.inspect_without(object, variables)
20
+ filtered = object.instance_variables.reject { |n| variables.include? n }
21
+ vars = filtered.map { |n| "#{n}=#{object.instance_variable_get(n).inspect}" }
22
+ oid = object.object_id << 1
23
+ "#<%s:0x%x %s>" % [ object.class, oid, vars.join(', ') ]
24
+ end
25
+
26
+ def inspect_without(variables)
27
+ PuppetForgeServer::Utils::FilteringInspecter.inspect_without(self, variables)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Copyright 2015 North Development AB
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'tilt/redcarpet'
18
+
19
+ module PuppetForgeServer::Utils
20
+ module MarkdownRenderer
21
+
22
+ class CustomRenderer < Redcarpet::Render::HTML
23
+ def block_code(code, lang)
24
+ output = "<pre>"
25
+ output << "<code class=\"prettyprint lang-puppet\">#{code}</code>"
26
+ output << "</pre>"
27
+ end
28
+ end
29
+
30
+ def markdown(text)
31
+ options = {
32
+ filter_html: true,
33
+ with_toc_data: true,
34
+ hard_wrap: true,
35
+ prettify: true
36
+ }
37
+
38
+ extensions = {
39
+ autolink: true,
40
+ superscript: true,
41
+ disable_indented_code_blocks: false,
42
+ fenced_code_blocks: true,
43
+ strikethrough: true,
44
+ quote: true,
45
+ tables: true
46
+ }
47
+
48
+ renderer = CustomRenderer.new(options)
49
+ markdown = Redcarpet::Markdown.new(renderer, extensions)
50
+ markdown.render(text)
51
+ end
52
+
53
+ end
54
+ end