berkshelf-api 0.1.0

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +17 -0
  5. data/CONTRIBUTING.md +33 -0
  6. data/Gemfile +40 -0
  7. data/Guardfile +20 -0
  8. data/LICENSE +201 -0
  9. data/README.md +37 -0
  10. data/Thorfile +39 -0
  11. data/berkshelf-api.gemspec +35 -0
  12. data/bin/berks-api +5 -0
  13. data/lib/berkshelf-api.rb +1 -0
  14. data/lib/berkshelf/api.rb +25 -0
  15. data/lib/berkshelf/api/application.rb +114 -0
  16. data/lib/berkshelf/api/cache_builder.rb +60 -0
  17. data/lib/berkshelf/api/cache_builder/worker.rb +116 -0
  18. data/lib/berkshelf/api/cache_builder/worker/chef_server.rb +46 -0
  19. data/lib/berkshelf/api/cache_builder/worker/opscode.rb +59 -0
  20. data/lib/berkshelf/api/cache_manager.rb +96 -0
  21. data/lib/berkshelf/api/config.rb +23 -0
  22. data/lib/berkshelf/api/cucumber.rb +11 -0
  23. data/lib/berkshelf/api/dependency_cache.rb +123 -0
  24. data/lib/berkshelf/api/endpoint.rb +17 -0
  25. data/lib/berkshelf/api/endpoint/v1.rb +19 -0
  26. data/lib/berkshelf/api/errors.rb +8 -0
  27. data/lib/berkshelf/api/generic_server.rb +50 -0
  28. data/lib/berkshelf/api/logging.rb +37 -0
  29. data/lib/berkshelf/api/mixin.rb +7 -0
  30. data/lib/berkshelf/api/mixin/services.rb +48 -0
  31. data/lib/berkshelf/api/rack_app.rb +5 -0
  32. data/lib/berkshelf/api/remote_cookbook.rb +3 -0
  33. data/lib/berkshelf/api/rest_gateway.rb +62 -0
  34. data/lib/berkshelf/api/rspec.rb +20 -0
  35. data/lib/berkshelf/api/rspec/server.rb +29 -0
  36. data/lib/berkshelf/api/site_connector.rb +7 -0
  37. data/lib/berkshelf/api/site_connector/opscode.rb +162 -0
  38. data/lib/berkshelf/api/srv_ctl.rb +63 -0
  39. data/lib/berkshelf/api/version.rb +5 -0
  40. data/spec/fixtures/reset.pem +27 -0
  41. data/spec/spec_helper.rb +53 -0
  42. data/spec/support/actor_mocking.rb +7 -0
  43. data/spec/support/chef_server.rb +73 -0
  44. data/spec/unit/berkshelf/api/application_spec.rb +24 -0
  45. data/spec/unit/berkshelf/api/cache_builder/worker/chef_server_spec.rb +59 -0
  46. data/spec/unit/berkshelf/api/cache_builder/worker/opscode_spec.rb +41 -0
  47. data/spec/unit/berkshelf/api/cache_builder/worker_spec.rb +80 -0
  48. data/spec/unit/berkshelf/api/cache_builder_spec.rb +37 -0
  49. data/spec/unit/berkshelf/api/cache_manager_spec.rb +123 -0
  50. data/spec/unit/berkshelf/api/config_spec.rb +24 -0
  51. data/spec/unit/berkshelf/api/dependency_cache_spec.rb +109 -0
  52. data/spec/unit/berkshelf/api/endpoint/v1_spec.rb +18 -0
  53. data/spec/unit/berkshelf/api/logging_spec.rb +28 -0
  54. data/spec/unit/berkshelf/api/mixin/services_spec.rb +68 -0
  55. data/spec/unit/berkshelf/api/rack_app_spec.rb +6 -0
  56. data/spec/unit/berkshelf/api/rest_gateway_spec.rb +26 -0
  57. data/spec/unit/berkshelf/api/site_connector/opscode_spec.rb +85 -0
  58. data/spec/unit/berkshelf/api/srv_ctl_spec.rb +56 -0
  59. metadata +293 -0
@@ -0,0 +1,23 @@
1
+ require 'buff/config/json'
2
+
3
+ module Berkshelf::API
4
+ class Config < Buff::Config::JSON
5
+ class << self
6
+ # @return [String]
7
+ def default_path
8
+ File.expand_path("~/.berkshelf/api-server/config.json")
9
+ end
10
+ end
11
+
12
+ attribute 'endpoints',
13
+ type: Array,
14
+ default: [
15
+ {
16
+ type: "opscode",
17
+ options: {
18
+ url: 'http://cookbooks.opscode.com/api/v1'
19
+ }
20
+ }
21
+ ]
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'rspec'
2
+
3
+ World(Berkshelf::API::RSpec)
4
+
5
+ Given(/^the Berkshelf API server cache is up to date$/) do
6
+ cache_builder.build
7
+ end
8
+
9
+ Given(/^the Berkshelf API server's cache is empty$/) do
10
+ cache_manager.clear
11
+ end
@@ -0,0 +1,123 @@
1
+ module Berkshelf::API
2
+ # @note
3
+ # DependencyCache stores its data internally as a Mash
4
+ # The structure is as follows
5
+ #
6
+ # {
7
+ # "cookbook_name" => {
8
+ # "x.y.z" => {
9
+ # :dependencies => { "cookbook_name" => "constraint" },
10
+ # :platforms => { "platform" => "constraint" }
11
+ # }
12
+ # }
13
+ # }
14
+ class DependencyCache
15
+ class << self
16
+ # Read an archived cache and re-instantiate it
17
+ #
18
+ # @param [#to_s] filepath
19
+ # path to an archived cache
20
+ #
21
+ # @raise [Berkshelf::API::SaveNotFoundError]
22
+ # @raise [Berkshelf::API::InvalidSaveError]
23
+ #
24
+ # @return [DependencyCache]
25
+ def from_file(filepath)
26
+ contents = JSON.parse(File.read(filepath.to_s))
27
+ new(contents)
28
+ rescue Errno::ENOENT => ex
29
+ raise SaveNotFoundError.new(ex)
30
+ rescue JSON::ParserError => ex
31
+ raise InvalidSaveError.new(ex)
32
+ end
33
+ end
34
+
35
+ extend Forwardable
36
+ def_delegators :@cache, :[], :[]=
37
+
38
+ # @param [Hash] contents
39
+ def initialize(contents = {})
40
+ @cache = Hash[contents]
41
+ end
42
+
43
+ # @param [RemoteCookbook] cookbook
44
+ # @param [Ridley::Chef::Cookbook::Metadata] metadata
45
+ #
46
+ # @return [Hash]
47
+ def add(cookbook, metadata)
48
+ platforms = metadata.platforms || Hash.new
49
+ dependencies = metadata.dependencies || Hash.new
50
+ @cache[cookbook.name.to_s] ||= Hash.new
51
+ @cache[cookbook.name.to_s][cookbook.version.to_s] = {
52
+ platforms: platforms,
53
+ dependencies: dependencies,
54
+ location_type: cookbook.location_type,
55
+ location_path: cookbook.location_path
56
+ }
57
+ end
58
+
59
+ # Clear any items added to this instance
60
+ #
61
+ # @return [Hash]
62
+ def clear
63
+ @cache.clear
64
+ end
65
+
66
+ # Check if the cache knows about the given cookbook version
67
+ #
68
+ # @param [#to_s] name
69
+ # @param [#to_s] version
70
+ #
71
+ # @return [Boolean]
72
+ def has_cookbook?(name, version)
73
+ unless cookbook = @cache[name.to_s]
74
+ return false
75
+ end
76
+
77
+ cookbook.has_key?(version.to_s)
78
+ end
79
+
80
+ # @param [String] name
81
+ # @param [String] version
82
+ #
83
+ # @return [Hash]
84
+ def remove(name, version)
85
+ @cache[name.to_s].delete(version.to_s)
86
+ if @cache[name.to_s].empty?
87
+ @cache.delete(name.to_s)
88
+ end
89
+ @cache
90
+ end
91
+
92
+ # @return [Boolean]
93
+ def empty?
94
+ @cache.empty?
95
+ end
96
+
97
+ # @return [Hash]
98
+ def to_hash
99
+ @cache.to_hash
100
+ end
101
+
102
+ # @return [String]
103
+ def to_json(options = {})
104
+ JSON.generate(to_hash, options)
105
+ end
106
+
107
+ # @return [Array<RemoteCookbook>]
108
+ def cookbooks
109
+ [].tap do |remote_cookbooks|
110
+ @cache.each_pair do |name, versions|
111
+ versions.each do |version, metadata|
112
+ remote_cookbooks << RemoteCookbook.new(name, version, metadata[:location_type], metadata[:location_path])
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def save(path)
119
+ FileUtils.mkdir_p(File.dirname(path))
120
+ File.open(path, 'w+') { |f| f.write(self.to_json) }
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,17 @@
1
+ require 'grape'
2
+
3
+ module Berkshelf::API
4
+ module Endpoint
5
+ class Base < Grape::API
6
+ # Force inbound requests to be JSON
7
+ def call(env)
8
+ env['CONTENT_TYPE'] = 'application/json'
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ Dir["#{File.dirname(__FILE__)}/endpoint/*.rb"].sort.each do |path|
16
+ require "berkshelf/api/endpoint/#{File.basename(path, '.rb')}"
17
+ end
@@ -0,0 +1,19 @@
1
+ module Berkshelf::API
2
+ module Endpoint
3
+ class V1 < Endpoint::Base
4
+ helpers Berkshelf::API::Mixin::Services
5
+ version 'v1', using: :header, vendor: 'berkshelf'
6
+ format :json
7
+
8
+ rescue_from Grape::Exceptions::Validation do |e|
9
+ body = JSON.generate({status: e.status, message: e.message, param: e.param})
10
+ rack_response(body, e.status, "Content-type" => "application/json")
11
+ end
12
+
13
+ desc "list all known cookbooks"
14
+ get 'universe' do
15
+ cache_manager.cache
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ module Berkshelf::API
2
+ class APIError < StandardError; end
3
+ # Thrown when an actor was expected to be running but wasn't
4
+ class NotStartedError < APIError; end
5
+ class SaveNotFoundError < APIError; end
6
+ class InvalidSaveError < APIError; end
7
+ class MetadataLoadError < APIError; end
8
+ end
@@ -0,0 +1,50 @@
1
+ module Berkshelf::API
2
+ module GenericServer
3
+ class << self
4
+ def included(base)
5
+ base.send(:include, Celluloid)
6
+ base.send(:extend, ClassMethods)
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ # Returns the currently running instance of the including Class
12
+ #
13
+ # @return [Celluloid::Actor]
14
+ def instance
15
+ unless Application[server_name] && Application[server_name].alive?
16
+ raise NotStartedError, "#{server_name} not running"
17
+ end
18
+ Application[server_name]
19
+ end
20
+
21
+ # Set the name that the actor will be registered as with the applicaiton
22
+ #
23
+ # @param [#to_sym, nil]
24
+ #
25
+ # @return [Symbol]
26
+ def server_name(name = nil)
27
+ return @server_name if name.nil?
28
+ @server_name = name.to_sym
29
+ end
30
+
31
+ # Start the cache manager and add it to the application's registry.
32
+ #
33
+ # @note you probably do not want to manually start the cache manager unless you
34
+ # are testing the application. Start the entire application with {Berkshelf::API::Application.run}
35
+ def start(*args)
36
+ Application[server_name] = new(*args)
37
+ end
38
+
39
+ # Stop the cache manager if it's running.
40
+ #
41
+ # @note you probably don't want to manually stop the cache manager unless you are testing
42
+ # the application. Stop the entire application with {Berkshelf::API::Application.shutdown}
43
+ def stop
44
+ unless actor = Application[server_name]
45
+ actor.terminate
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ module Berkshelf::API
2
+ module Logging
3
+ class << self
4
+ # @return [Logger]
5
+ attr_accessor :logger
6
+
7
+ # @option options [String, Fixnum] :location (STDOUT)
8
+ # @option options [String, nil] :level ("INFO")
9
+ # - "DEBUG
10
+ # - "INFO"
11
+ # - "WARN"
12
+ # - "ERROR"
13
+ # - "FATAL"
14
+ # @option options [Logger::Formatter] :formatter
15
+ #
16
+ # @return [Logger]
17
+ def init(options = {})
18
+ level = options[:level] || "INFO"
19
+ location = options[:location] || STDOUT
20
+ formatter = options[:formatter] || nil
21
+
22
+ Celluloid.logger = @logger = Logger.new(location).tap do |log|
23
+ log.level = Logger::Severity.const_get(level.upcase)
24
+ log.formatter = formatter if formatter
25
+ end
26
+ end
27
+ end
28
+
29
+ init
30
+
31
+ # @return [Logger]
32
+ def logger
33
+ Logging.logger
34
+ end
35
+ alias_method :log, :logger
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ module Berkshelf::API
2
+ module Mixin; end
3
+ end
4
+
5
+ Dir["#{File.dirname(__FILE__)}/mixin/*.rb"].sort.each do |path|
6
+ require_relative "mixin/#{File.basename(path, '.rb')}"
7
+ end
@@ -0,0 +1,48 @@
1
+ module Berkshelf::API
2
+ module Mixin
3
+ module Services
4
+ class << self
5
+ def included(base)
6
+ base.extend(ClassMethods)
7
+ base.send(:include, ClassMethods)
8
+ end
9
+
10
+ def extended(base)
11
+ base.send(:include, ClassMethods)
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ # @raise [Berkshelf::API::NotStartedError] if the cache manager has not been started
17
+ #
18
+ # @return [Berkshelf::API::CacheBuilder]
19
+ def cache_builder
20
+ app_actor(:cache_builder)
21
+ end
22
+
23
+ # @raise [Berkshelf::API::NotStartedError] if the cache manager has not been started
24
+ #
25
+ # @return [Berkshelf::API::CacheManager]
26
+ def cache_manager
27
+ app_actor(:cache_manager)
28
+ end
29
+
30
+ # @raise [Berkshelf::API::NotStartedError] if the rest gateway has not been started
31
+ #
32
+ # @return [Berkshelf::API::RESTGateway]
33
+ def rest_gateway
34
+ app_actor(:rest_gateway)
35
+ end
36
+
37
+ private
38
+
39
+ def app_actor(id)
40
+ unless Application[id] && Application[id].alive?
41
+ raise NotStartedError, "#{id} not running"
42
+ end
43
+ Application[id]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ module Berkshelf::API
2
+ class RackApp < Endpoint::Base
3
+ mount Berkshelf::API::Endpoint::V1
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Berkshelf::API
2
+ class RemoteCookbook < Struct.new(:name, :version, :location_type, :location_path); end
3
+ end
@@ -0,0 +1,62 @@
1
+ require 'reel'
2
+
3
+ module Berkshelf::API
4
+ class RESTGateway < Reel::Server
5
+ extend Forwardable
6
+ include Berkshelf::API::GenericServer
7
+ include Berkshelf::API::Logging
8
+
9
+ DEFAULT_OPTIONS = {
10
+ host: '0.0.0.0',
11
+ port: 26200,
12
+ quiet: false,
13
+ workers: 10
14
+ }.freeze
15
+
16
+ # @return [String]
17
+ attr_reader :host
18
+ # @return [Integer]
19
+ attr_reader :port
20
+ # @return [Integer]
21
+ attr_reader :workers
22
+
23
+ def_delegator :handler, :rack_app
24
+
25
+ server_name :rest_gateway
26
+ finalizer :finalize_callback
27
+
28
+ # @option options [String] :host ('0.0.0.0')
29
+ # @option options [Integer] :port (26100)
30
+ # @option options [Boolean] :quiet (false)
31
+ # @option options [Integer] :workers (10)
32
+ def initialize(options = {})
33
+ options = DEFAULT_OPTIONS.merge(options)
34
+ options[:app] = Berkshelf::API::RackApp.new
35
+
36
+ @host = options[:host]
37
+ @port = options[:port]
38
+ @workers = options[:workers]
39
+ @handler = ::Rack::Handler::Reel.new(options)
40
+ @pool = ::Reel::RackWorker.pool(size: @workers, args: [ @handler ])
41
+
42
+ log.info "REST Gateway listening on #{@host}:#{@port}"
43
+ super(@host, @port, &method(:on_connect))
44
+ end
45
+
46
+ # @param [Reel::Connection] connection
47
+ def on_connect(connection)
48
+ pool.handle(connection.detach)
49
+ end
50
+
51
+ private
52
+
53
+ # @return [Reel::RackWorker]
54
+ attr_reader :pool
55
+ # @return [Rack::Handler::Reel]
56
+ attr_reader :handler
57
+
58
+ def finalize_callback
59
+ pool.terminate if pool && pool.alive?
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,20 @@
1
+ require 'berkshelf/api'
2
+
3
+ module Berkshelf::API
4
+ module RSpec
5
+ require_relative 'rspec/server'
6
+
7
+ include Mixin::Services
8
+
9
+ def berks_dependency(name, version, options = {})
10
+ options[:platforms] ||= Hash.new
11
+ options[:dependencies] ||= Hash.new
12
+ cookbook = RemoteCookbook.new(name, version,
13
+ CacheBuilder::Worker::Opscode.worker_type, SiteConnector::Opscode::V1_API)
14
+ metadata = Ridley::Chef::Cookbook::Metadata.new
15
+ options[:platforms].each { |name, version| metadata.supports(name, version) }
16
+ options[:dependencies].each { |name, constraint| metadata.depends(name, constraint) }
17
+ cache_manager.add(cookbook, metadata)
18
+ end
19
+ end
20
+ end