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
data/bin/berks-api ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $:.push File.expand_path("../../lib", __FILE__)
3
+ require 'berkshelf/api/srv_ctl'
4
+
5
+ Berkshelf::API::SrvCtl.run(ARGV, File.basename(__FILE__))
@@ -0,0 +1 @@
1
+ require 'berkshelf/api'
@@ -0,0 +1,25 @@
1
+ require 'celluloid'
2
+ require 'hashie'
3
+ require 'ridley'
4
+ require 'faraday'
5
+ require 'json'
6
+
7
+ module Berkshelf
8
+ module API
9
+ require_relative 'api/errors'
10
+ require_relative 'api/logging'
11
+ require_relative 'api/mixin'
12
+ require_relative 'api/generic_server'
13
+
14
+ require_relative 'api/application'
15
+ require_relative 'api/cache_builder'
16
+ require_relative 'api/cache_manager'
17
+ require_relative 'api/config'
18
+ require_relative 'api/dependency_cache'
19
+ require_relative 'api/endpoint'
20
+ require_relative 'api/rack_app'
21
+ require_relative 'api/remote_cookbook'
22
+ require_relative 'api/site_connector'
23
+ require_relative 'api/srv_ctl'
24
+ end
25
+ end
@@ -0,0 +1,114 @@
1
+ trap 'INT' do
2
+ Berkshelf::API::Application.shutdown
3
+ end
4
+
5
+ trap 'TERM' do
6
+ Berkshelf::API::Application.shutdown
7
+ end
8
+
9
+ module Berkshelf::API
10
+ class ApplicationSupervisor < Celluloid::SupervisionGroup
11
+ # @option options [Boolean] :disable_http (false)
12
+ # run the application without the rest gateway
13
+ def initialize(registry, options = {})
14
+ super(registry)
15
+ supervise_as(:cache_manager, Berkshelf::API::CacheManager)
16
+ supervise_as(:cache_builder, Berkshelf::API::CacheBuilder)
17
+
18
+ unless options[:disable_http]
19
+ require_relative 'rest_gateway'
20
+ supervise_as(:rest_gateway, Berkshelf::API::RESTGateway, options)
21
+ end
22
+ end
23
+ end
24
+
25
+ module Application
26
+ class << self
27
+ extend Forwardable
28
+ include Berkshelf::API::Logging
29
+ include Berkshelf::API::Mixin::Services
30
+
31
+ def_delegators :registry, :[], :[]=
32
+
33
+ def config
34
+ @config ||= begin
35
+ Berkshelf::API::Config.from_file(Berkshelf::API::Config.default_path)
36
+ rescue
37
+ Berkshelf::API::Config.new
38
+ end
39
+ end
40
+
41
+ # @option options [String, Fixnum] :log_location (STDOUT)
42
+ # @option options [String, nil] :log_level ("INFO")
43
+ # - "DEBUG
44
+ # - "INFO"
45
+ # - "WARN"
46
+ # - "ERROR"
47
+ # - "FATAL"
48
+ def configure_logger(options = {})
49
+ Logging.init(level: options[:log_level], location: options[:log_location])
50
+ end
51
+
52
+ def instance
53
+ return @instance if @instance
54
+
55
+ raise NotStartedError, "application not running"
56
+ end
57
+
58
+ # The Actor registry for Berkshelf::API.
59
+ #
60
+ # @note Berkshelf::API uses it's own registry instead of Celluloid::Registry.root to
61
+ # avoid conflicts in the larger namespace. Use Berkshelf::API::Application[] to access Berkshelf::API
62
+ # actors instead of Celluloid::Actor[].
63
+ #
64
+ # @return [Celluloid::Registry]
65
+ def registry
66
+ @registry ||= Celluloid::Registry.new
67
+ end
68
+
69
+ # Run the application in the foreground (sleep on main thread)
70
+ #
71
+ # @option options [Boolean] :disable_http (false)
72
+ # run the application without the rest gateway
73
+ def run(options = {})
74
+ loop do
75
+ supervisor = run!(options)
76
+
77
+ sleep 0.1 while supervisor.alive?
78
+
79
+ break if @shutdown
80
+
81
+ log.error "!!! #{self} crashed. Restarting..."
82
+ end
83
+ end
84
+
85
+ # Run the application in the background
86
+ #
87
+ # @option options [Boolean] :disable_http (false)
88
+ # run the application without the rest gateway
89
+ # @option options [Boolean] :eager_build (false)
90
+ # automatically begin and loop all cache builders
91
+ #
92
+ # @return [Berkshelf::API::Application]
93
+ def run!(options = {})
94
+ options = { disable_http: false, eager_build: false }.merge(options)
95
+ configure_logger(options)
96
+ @instance = ApplicationSupervisor.new(registry, options)
97
+ cache_builder.async(:build_loop) if options[:eager_build]
98
+ @instance
99
+ end
100
+
101
+ # @return [Boolean]
102
+ def running?
103
+ instance.alive?
104
+ rescue NotStartedError
105
+ false
106
+ end
107
+
108
+ def shutdown
109
+ @shutdown = true
110
+ instance.terminate
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,60 @@
1
+ module Berkshelf::API
2
+ class CacheBuilder
3
+ require_relative 'cache_builder/worker'
4
+
5
+ class WorkerSupervisor < Celluloid::SupervisionGroup; end
6
+
7
+ BUILD_INTERVAL = 5.0
8
+
9
+ include Berkshelf::API::GenericServer
10
+ include Berkshelf::API::Logging
11
+
12
+ server_name :cache_builder
13
+ finalizer :finalize_callback
14
+
15
+ def initialize
16
+ log.info "Cache Builder starting..."
17
+ @worker_registry = Celluloid::Registry.new
18
+ @worker_supervisor = WorkerSupervisor.new(@worker_registry)
19
+ @building = false
20
+
21
+ Application.config.endpoints.each do |endpoint|
22
+ @worker_supervisor.supervise(CacheBuilder::Worker[endpoint.type], endpoint.options)
23
+ end
24
+ end
25
+
26
+ # Issue a single build command to all workers
27
+ #
28
+ # @return [Array]
29
+ def build
30
+ workers.collect { |actor| actor.future(:build) }.map(&:value)
31
+ end
32
+
33
+ # Issue a build command to all workers at the scheduled interval
34
+ #
35
+ # @param [Fixnum, Float] interval
36
+ def build_loop(interval = BUILD_INTERVAL)
37
+ return if @building
38
+
39
+ loop do
40
+ @building = true
41
+ build
42
+ sleep BUILD_INTERVAL
43
+ end
44
+ end
45
+
46
+ # Return the list of running workers
47
+ #
48
+ # @return [Array<CacheBuilder::Worker::Base>]
49
+ def workers
50
+ @worker_supervisor.actors
51
+ end
52
+
53
+ private
54
+
55
+ def finalize_callback
56
+ log.info "Cache Builder shutting down..."
57
+ @worker_supervisor.terminate if @worker_supervisor && @worker_supervisor.alive?
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,116 @@
1
+ module Berkshelf::API
2
+ class CacheBuilder
3
+ module Worker
4
+ class Base
5
+ class << self
6
+ # @param [#to_s, nil] type
7
+ def worker_type(type = nil)
8
+ return @worker_type if @worker_type
9
+ @worker_type = type.to_s
10
+ Worker.register(@worker_type, self)
11
+ end
12
+ end
13
+
14
+ include Celluloid
15
+ include Berkshelf::API::Logging
16
+ include Berkshelf::API::Mixin::Services
17
+
18
+ attr_reader :options
19
+
20
+ def initialize(options = {}); end
21
+
22
+ # @abstract
23
+ #
24
+ # @param [RemoteCookbook] remote
25
+ #
26
+ # @return [Ridley::Chef::Cookbook::Metadata]
27
+ def metadata(remote)
28
+ raise RuntimeError, "must be implemented"
29
+ end
30
+
31
+ # @abstract
32
+ #
33
+ # @return [Array<RemoteCookbook>]
34
+ # The list of cookbooks this builder can find
35
+ def cookbooks
36
+ raise RuntimeError, "must be implemented"
37
+ end
38
+
39
+ def build
40
+ log.info "#{self} building..."
41
+ log.info "#{self} determining if the cache is stale..."
42
+ if stale?
43
+ log.info "#{self} cache is stale."
44
+ update_cache
45
+ else
46
+ log.info "#{self} cache is up to date."
47
+ end
48
+
49
+ log.info "clearing diff"
50
+ clear_diff
51
+ end
52
+
53
+ # @return [Array<Array<RemoteCookbook>, Array<RemoteCookbook>>]
54
+ def diff
55
+ @diff ||= cache_manager.diff(cookbooks)
56
+ end
57
+
58
+ def update_cache
59
+ created_cookbooks, deleted_cookbooks = diff
60
+
61
+ log.info "#{self} adding (#{created_cookbooks.length}) items..."
62
+ created_cookbooks.collect do |remote|
63
+ [ remote, future(:metadata, remote) ]
64
+ end.each do |remote, metadata|
65
+ cache_manager.add(remote, metadata.value)
66
+ end
67
+
68
+ log.info "#{self} removing (#{deleted_cookbooks.length}) items..."
69
+ deleted_cookbooks.each { |remote| cache_manager.remove(remote.name, remote.version) }
70
+
71
+ log.info "#{self} cache updated."
72
+ cache_manager.save
73
+ end
74
+
75
+ def stale?
76
+ created_cookbooks, deleted_cookbooks = diff
77
+ created_cookbooks.any? || deleted_cookbooks.any?
78
+ end
79
+
80
+ private
81
+
82
+ def clear_diff
83
+ @diff = nil
84
+ end
85
+ end
86
+
87
+ class << self
88
+ # @param [#to_s] name
89
+ #
90
+ # @return [Worker::Base]
91
+ def [](name)
92
+ types[name.to_s]
93
+ end
94
+
95
+ # @param [#to_s] name
96
+ # @param [Worker::Base] klass
97
+ def register(name, klass)
98
+ name = name.to_s
99
+ if types.has_key?(name)
100
+ raise RuntimeError, "worker already registered with the name '#{name}'"
101
+ end
102
+ types[name] = klass
103
+ end
104
+
105
+ # @return [Hash]
106
+ def types
107
+ @types ||= Hash.new
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ Dir["#{File.dirname(__FILE__)}/worker/*.rb"].sort.each do |path|
115
+ require_relative "worker/#{File.basename(path, '.rb')}"
116
+ end
@@ -0,0 +1,46 @@
1
+ module Berkshelf::API
2
+ class CacheBuilder
3
+ module Worker
4
+ class ChefServer < Worker::Base
5
+ worker_type "chef_server"
6
+
7
+ finalizer :finalize_callback
8
+
9
+ def initialize(options = {})
10
+ @connection = Ridley::Client.new_link(server_url: options[:url], client_key: options[:client_key],
11
+ client_name: options[:client_name], ssl: { verify: options[:ssl_verify] })
12
+ super
13
+ end
14
+
15
+ # @return [Array<RemoteCookbook>]
16
+ # The list of cookbooks this builder can find
17
+ def cookbooks
18
+ [].tap do |cookbook_versions|
19
+ connection.cookbook.all.each do |cookbook, versions|
20
+ versions.each do |version|
21
+ cookbook_versions << RemoteCookbook.new(cookbook, version, self.class.worker_type,
22
+ @connection.server_url)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # @param [RemoteCookbook] remote
29
+ #
30
+ # @return [Ridley::Chef::Cookbook::Metadata]
31
+ def metadata(remote)
32
+ metadata_hash = connection.cookbook.find(remote.name, remote.version).metadata
33
+ Ridley::Chef::Cookbook::Metadata.from_hash(metadata_hash)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :connection
39
+
40
+ def finalize_callback
41
+ connection.terminate if connection && connection.alive?
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,59 @@
1
+ module Berkshelf::API
2
+ class CacheBuilder
3
+ module Worker
4
+ class Opscode < Worker::Base
5
+ worker_type "opscode"
6
+
7
+ finalizer :finalize_callback
8
+
9
+ def initialize(options = {})
10
+ @connection = Berkshelf::API::SiteConnector::Opscode.pool_link(size: 25)
11
+ super
12
+ end
13
+
14
+ # @return [Array<RemoteCookbook>]
15
+ # The list of cookbooks this builder can find
16
+ def cookbooks
17
+ [].tap do |cookbook_versions|
18
+ connection.cookbooks.collect do |cookbook|
19
+ [ cookbook, connection.future(:versions, cookbook) ]
20
+ end.each do |cookbook, versions|
21
+ versions.value.each do |version|
22
+ cookbook_versions << RemoteCookbook.new(cookbook, version, self.class.worker_type, @connection.api_uri)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # @param [RemoteCookbook] remote
29
+ #
30
+ # @return [Ridley::Chef::Cookbook::Metadata]
31
+ def metadata(remote)
32
+ Dir.mktmpdir do |destination|
33
+ connection.download(remote.name, remote.version, destination)
34
+ load_metadata(destination, remote.name)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ attr_accessor :connection
41
+
42
+ def finalize_callback
43
+ connection.terminate if connection && connection.alive?
44
+ end
45
+
46
+ def load_metadata(directory, cookbook)
47
+ # The community site does not enforce the name of the cookbook contained in the archive
48
+ # downloaded and extracted. This will just find the first metadata.json and load it.
49
+ file = Dir["#{directory}/**/*/metadata.json"].first
50
+ metadata = File.read(file)
51
+ Ridley::Chef::Cookbook::Metadata.from_json(metadata)
52
+ rescue JSON::ParserError => ex
53
+ log.warn "Error loading metadata for #{cookbook} from: #{file}"
54
+ abort MetadataLoadError.new(ex)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,96 @@
1
+ module Berkshelf::API
2
+ class CacheManager
3
+ class << self
4
+ attr_writer :cache_file
5
+
6
+ # @return [String]
7
+ def cache_file
8
+ @cache_file ||= File.expand_path("~/.berkshelf/api-server/cerch")
9
+ end
10
+ end
11
+
12
+ include Berkshelf::API::GenericServer
13
+ include Berkshelf::API::Logging
14
+
15
+ SAVE_INTERVAL = 30.0
16
+
17
+ server_name :cache_manager
18
+ finalizer :finalize_callback
19
+ exclusive :add, :clear, :remove, :save
20
+
21
+ attr_reader :cache
22
+
23
+ def initialize
24
+ log.info "Cache Manager starting..."
25
+ @cache = DependencyCache.new
26
+ load_save if File.exist?(self.class.cache_file)
27
+ every(SAVE_INTERVAL) { save }
28
+ end
29
+
30
+ # @param [RemoteCookbook] cookbook
31
+ # @param [Ridley::Chef::Cookbook::Metadata] metadata
32
+ #
33
+ # @return [Hash]
34
+ def add(cookbook, metadata)
35
+ @cache.add(cookbook, metadata)
36
+ end
37
+
38
+ # Clear any items added to the cache
39
+ #
40
+ # @return [Hash]
41
+ def clear
42
+ @cache.clear
43
+ end
44
+
45
+ # Check if the cache knows about the given cookbook version
46
+ #
47
+ # @param [#to_s] name
48
+ # @param [#to_s] version
49
+ #
50
+ # @return [Boolean]
51
+ def has_cookbook?(name, version)
52
+ @cache.has_cookbook?(name, version)
53
+ end
54
+
55
+ def load_save
56
+ @cache = DependencyCache.from_file(self.class.cache_file)
57
+ end
58
+
59
+ # Remove the cached item matching the given name and version
60
+ #
61
+ # @param [#to_s] name
62
+ # @param [#to_s] version
63
+ #
64
+ # @return [DependencyCache]
65
+ def remove(name, version)
66
+ @cache.remove(name, version)
67
+ end
68
+
69
+ def save
70
+ log.info "Saving the cache to: #{self.class.cache_file}"
71
+ cache.save(self.class.cache_file)
72
+ log.info "Cache saved!"
73
+ end
74
+
75
+ # @param [Array<RemoteCookbook>] cookbooks
76
+ # An array of RemoteCookbooks representing all the cookbooks on the indexed site
77
+ #
78
+ # @return [Array<Array<RemoteCookbook>, Array<RemoteCookbook>>]
79
+ # A tuple of Arrays of RemoteCookbooks
80
+ # The first array contains items not in the cache
81
+ # The second array contains items in the cache, but not in the cookbooks parameter
82
+ def diff(cookbooks)
83
+ known_cookbooks = cache.cookbooks
84
+ created_cookbooks = cookbooks - known_cookbooks
85
+ deleted_cookbooks = known_cookbooks - cookbooks
86
+ [ created_cookbooks, deleted_cookbooks ]
87
+ end
88
+
89
+ private
90
+
91
+ def finalize_callback
92
+ log.info "Cache Manager shutting down..."
93
+ self.save
94
+ end
95
+ end
96
+ end