berkshelf-api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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