batali 0.1.0 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 12ae5060ad51ee573081c34f077e1242908dddd7
4
- data.tar.gz: b569cf195a11b93a1b7c1ddf87df063c63df1693
3
+ metadata.gz: 68737e2f016b576dbe9b038f1d6a3930b17fd8ee
4
+ data.tar.gz: 47a886779b940b36035dfa68d8cf07f4631bb24f
5
5
  SHA512:
6
- metadata.gz: 909cfbcd842a0f2187efab1e4bf9453bc15234e38a0de13317b6ae1a5cf02b222cf499b4caf8ef6da814aa12c1cb74f2dbbde92089645ac9410e230f63f0d1e5
7
- data.tar.gz: 1523d71a2af4dd85a487effeeb1bc967160f1093fec75925019afee3f3b128d0e32f3d07d983740d08f754e3732838ba909d672d2c565456466fbc660a333642
6
+ metadata.gz: cdd04d4d64c21b700d89b85403367badaa16d4168461e4b393105d39e4e2b243e79cde92b5dad431fc1a765f461bced6b341bb8cb63067c778dc1d83796e7a2a
7
+ data.tar.gz: c4beb1a4fe18118e09c53962f04603aeeb5246b8fcbf565577ccc773b1aeb03d11c9b5bbdc414ef704c3765ed423bd184b74ff4a373f7fc66e0ad5af2bcb2f5a
data/CHANGELOG.md CHANGED
@@ -1,2 +1,7 @@
1
+ # v0.1.2
2
+ * Add support for git and path origins
3
+ * Cache assets from origins locally for reuse
4
+ * Add initial support for infrastructure resolution
5
+
1
6
  # v0.1.0
2
7
  * Initial release
data/README.md CHANGED
@@ -27,10 +27,120 @@ by default.
27
27
 
28
28
  _IT WILL DESTROY YOUR COOKBOOKS DIRECTORY BY DEFAULT_
29
29
 
30
- There is other cool stuff too, to be documented later. Currently
31
- only site sources can be defined (no path, or git, or anything else).
30
+ ## Commands
32
31
 
32
+ * `batali resolve` - Resolve dependencies and produce `batali.manifest`
33
+ * `batali install` - Install entries from the `batali.manifest`
34
+ * `batali update` - Perform `resolve` and then `install`
35
+
36
+ ## Features
37
+
38
+ ### Origins
39
+
40
+ Currently supported "origins":
41
+
42
+ * RemoteSite
43
+ * Path
44
+ * Git
45
+
46
+ #### RemoteSite
47
+
48
+ This is simply a supermarket endpoint:
49
+
50
+ ```ruby
51
+ source 'https://supermarket.chef.io'
52
+ ```
53
+
54
+ Multiple endpoints can be provided by specifying multiple
55
+ `source` lines. They can also be named:
56
+
57
+ ```ruby
58
+ source 'https://supermarket.chef.io', :name => 'opscode'
59
+ source 'https://cookbooks.example.com', :name => 'example'
60
+ ```
61
+
62
+ ##### Path
63
+
64
+ Paths are defined via cookbook entries:
65
+
66
+ ```ruby
67
+ cookbook 'example', path: '/path/to/example'
68
+ ```
69
+
70
+ ##### Git
71
+
72
+ Git sources are defined via cookbook entries:
73
+
74
+ ```ruby
75
+ cookbook 'example', git: 'git://git.example.com/example-repo.git', ref: 'master'
76
+ ```
77
+
78
+ ### Least Impact Updates
79
+
80
+ After a `batali.manifest` file has been generated, subsequent `resolve` requests
81
+ will update cookbook versions using a "least impact" approach. This means that
82
+ by default if the `Batali` file has not changed, running a `batali resolve` will
83
+ be a noop, even if new versions of cookbooks may be available. This helps to reduce
84
+ unintended upgrades that may break things due to a required cookbook update. Allowing
85
+ a cookbook to be updated is done simply by adding it to the request:
86
+
87
+ ```
88
+ $ batali resolve example
89
+ ```
90
+
91
+ This will only update the version of the example cookbook, and any dependency cookbooks
92
+ that _must_ be updated to provide resolution. Multiple cookbooks can be listed:
93
+
94
+ ```
95
+ $ batali resolve example ipsum lorem
96
+ ```
97
+
98
+ or this feature can be disabled to allow everything to be updated to the latest
99
+ possible versions:
100
+
101
+ ```
102
+ $ batali resolve --no-least-impact
103
+ ```
104
+
105
+ ### Light weight
106
+
107
+ One of the goals for batali was being light weight resolver, in the same vein as
108
+ the [librarian][1] project. This means it does nothing more than manage local cookbooks. This
109
+ includes dependency and constraint resolution, as well as providing a local installation
110
+ of assets defined within the generated manifest. It provides no extra features outside of
111
+ that scope.
112
+
113
+ ### Multiple platform support
114
+
115
+ Batali does not rely on the [chef][2] gem to function. This removes any dependencies on
116
+ gems that may be incompatible outside the MRI platform.
117
+
118
+ ### Isolated manifest files
119
+
120
+ Manifest files are fully isolated. The resolver does not need to perform any actions
121
+ for installing cookbooks defined within the manifest. This allows for easy transmission
122
+ and direct installation of a manifest without the requirement of re-pulling information
123
+ from sources.
124
+
125
+ ### Infrastructure manifests
126
+
127
+ Batali aims to solve the issue of full infrastructure resolution: resolving dependencies
128
+ from an infrastructure repository. Resolving a single dependency path will not provide
129
+ a correct resolution. This is because environments or run lists can provide extra constraints
130
+ that will result in unsolvable resolutions on individual nodes. In this case we want
131
+ to know what cookbooks are _allowed_ within a solution, and ensure all those cookbooks
132
+ are available. Batali provides infrastructure level manifests by setting the `infrastructure`
133
+ flag:
134
+
135
+ ```
136
+ $ batali resolve --infrastructure
137
+ ```
138
+
139
+ _NOTE: Depending on constraints defined within the Batali file, this can be a very large manifest_
33
140
 
34
141
  # Info
35
142
 
36
143
  * Repository: https://github.com/hw-labs/batali
144
+
145
+ [1]: https://rubygems.org/gems/librarian "A Framework for Bundlers"
146
+ [2]: https://rubygems.org/gems/chef "A systems integration framework"
data/batali.gemspec CHANGED
@@ -10,10 +10,12 @@ Gem::Specification.new do |s|
10
10
  s.description = 'Magic'
11
11
  s.require_path = 'lib'
12
12
  s.license = 'Apache 2.0'
13
- s.add_runtime_dependency 'grimoire', '>= 0.1.2'
13
+ s.add_runtime_dependency 'attribute_struct', '>= 0.2.14'
14
+ s.add_runtime_dependency 'grimoire', '>= 0.1.4'
14
15
  s.add_runtime_dependency 'bogo', '>= 0.1.12'
15
16
  s.add_runtime_dependency 'bogo-cli', '>= 0.1.8'
16
17
  s.add_runtime_dependency 'bogo-config', '>= 0.1.10'
18
+ s.add_runtime_dependency 'git'
17
19
  s.add_runtime_dependency 'http'
18
20
  s.add_development_dependency 'minitest'
19
21
  s.add_development_dependency 'pry'
data/bin/batali CHANGED
@@ -32,6 +32,7 @@ Bogo::Cli::Setup.define do
32
32
  on :d, 'dry-run', 'Print changes'
33
33
  on :l, 'least-impact', 'Update cookbooks with minimal version impact', :default => true
34
34
  on :i, 'install', 'Install cookbooks after update', :default => true
35
+ on :I, 'infrastructure', 'Resolve infrastructure cookbooks'
35
36
 
36
37
  run do |opts, args|
37
38
  Batali::Command::Update.new({:update => opts.to_hash}, args).execute!
@@ -43,6 +44,7 @@ Bogo::Cli::Setup.define do
43
44
  self.instance_exec(&global_opts)
44
45
  on :d, 'dry-run', 'Print changes'
45
46
  on :l, 'least-impact', 'Update cookbooks with minimal version impact', :default => true
47
+ on :I, 'infrastructure', 'Resolve infrastructure cookbooks'
46
48
 
47
49
  run do |opts, args|
48
50
  Batali::Command::Resolve.new({:resolve => opts.to_hash}, args).execute!
data/lib/batali/b_file.rb CHANGED
@@ -4,8 +4,8 @@ module Batali
4
4
 
5
5
  class Struct < AttributeStruct
6
6
 
7
- def cookbook(*args)
8
- set!(:cookbook, args)
7
+ def cookbook(*args, &block)
8
+ set!(:cookbook, args, &block)
9
9
  self
10
10
  end
11
11
 
@@ -24,38 +24,58 @@ module Batali
24
24
 
25
25
  class BFile < Bogo::Config
26
26
 
27
- class Cookbook < Grimoire::Utility
27
+ # @return [Proc] cookbook convert
28
+ def self.cookbook_coerce
29
+ proc do |v|
30
+ case v
31
+ when Array
32
+ case v.last
33
+ when String
34
+ Cookbook.new(
35
+ :name => v.first,
36
+ :constraint => v.slice(1, v.size)
37
+ )
38
+ when Hash
39
+ c_name = v.first
40
+ Cookbook.new(
41
+ v.last.merge(
42
+ :name => c_name
43
+ )
44
+ )
45
+ else
46
+ raise ArgumentError.new "Unable to coerce given type `#{v.class}` to `Batali::BFile::Cookbook`!"
47
+ end
48
+ when String
49
+ Cookbook.new(:name => v)
50
+ else
51
+ raise ArgumentError.new "Unable to coerce given type `#{v.class}` to `Batali::BFile::Cookbook`!"
52
+ end
53
+ end
54
+ end
55
+
56
+ class Cookbook < Utility
28
57
  attribute :name, String, :required => true
29
58
  attribute :constraint, String, :multiple => true
30
- attribute :git, Smash, :coerce => lambda{|v| v.to_smash}
59
+ attribute :git, String
60
+ attribute :ref, String
31
61
  attribute :path, String
32
62
  end
33
63
 
34
- attribute :source, RemoteSite, :multiple => true, :coerce => lambda{|v| RemoteSite.new(:endpoint => v)}
35
- attribute :cookbook, Cookbook, :multiple => true, :coerce => lambda{|v|
36
- case v
37
- when Array
38
- Cookbook.new(
39
- :name => v.first,
40
- :constraint => v.slice(1, v.size)
41
- )
42
- when String
43
- Cookbook.new(:name => v)
44
- when Hash
45
- c_name = v.keys.first
46
- constraints = v.values.first.to_a.flatten.find_all{|i| i.is_a?(String)}
47
- Cookbook.new(
48
- :name => c_name,
49
- :constraint => constraints
50
- )
51
- else
52
- raise ArgumentError.new "Unable to coerce given type `#{v.class}` to `Batali::BFile::Cookbook`!"
53
- end
54
- }
64
+ class Restriction < Utility
65
+ attribute :cookbook, String, :required => true
66
+ attribute :source, String, :required => true
67
+ end
68
+
69
+ class Group < Utility
70
+ attribute :name, String, :required => true
71
+ attribute :cookbook, Cookbook, :multiple => true, :required => true, :coerce => BFile.cookbook_coerce
72
+ end
73
+
74
+ attribute :restrict, Restriction, :multiple => true, :coerce => lambda{|v| Restriction.new(:cookbook => v.first, :source => v.last)}
75
+ attribute :source, Origin::RemoteSite, :multiple => true, :coerce => lambda{|v| Origin::RemoteSite.new(:endpoint => v)}
76
+ attribute :group, Group, :multiple => true, :coerce => lambda{|v| Group.new()}
77
+ attribute :cookbook, Cookbook, :multiple => true, :coerce => BFile.cookbook_coerce
55
78
 
56
- ## TODO: supported values still required
57
- # attribute :restrict -- restrict cookbooks of name `x` to source named `y`
58
- # attribute :group -- cookbook grouping (i.e. :integration)
59
79
  end
60
80
 
61
81
  end
@@ -16,25 +16,31 @@ module Batali
16
16
  FileUtils.mkdir_p(install_path)
17
17
  nil
18
18
  end
19
- run_action('Installing cookbooks') do
20
- manifest.cookbook.each do |unit|
21
- asset_path = unit.source.asset
22
- begin
23
- FileUtils.mv(
24
- File.join(
25
- asset_path,
26
- unit.name
27
- ),
28
- File.join(
29
- install_path,
30
- unit.name
19
+ if(manifest.cookbook.nil? || manifest.cookbook.empty?)
20
+ ui.error "No cookbooks defined within manifest! Try resolving first. (`batali resolve`)"
21
+ else
22
+ run_action('Installing cookbooks') do
23
+ manifest.cookbook.each do |unit|
24
+ if(unit.source.respond_to?(:cache))
25
+ unit.source.cache = cache_directory(
26
+ Bogo::Utility.snake(unit.source.class.name.split('::').last)
31
27
  )
32
- )
33
- ensure
34
- FileUtils.rm_rf(asset_path)
28
+ end
29
+ asset_path = unit.source.asset
30
+ begin
31
+ FileUtils.cp_r(
32
+ File.join(asset_path, '.'),
33
+ File.join(
34
+ install_path,
35
+ "#{unit.name}-#{unit.version}"
36
+ )
37
+ )
38
+ ensure
39
+ unit.source.clean_asset(asset_path)
40
+ end
35
41
  end
42
+ nil
36
43
  end
37
- nil
38
44
  end
39
45
  end
40
46
  end
@@ -11,15 +11,17 @@ module Batali
11
11
  def execute!
12
12
  system = Grimoire::System.new
13
13
  run_action 'Loading sources' do
14
- batali_file.source.map(&:units).flatten.map do |unit|
15
- system.add_unit(unit)
16
- end
14
+ UnitLoader.new(
15
+ :file => batali_file,
16
+ :system => system,
17
+ :cache => cache_directory(:git)
18
+ ).populate!
17
19
  nil
18
20
  end
19
21
  requirements = Grimoire::RequirementList.new(
20
22
  :name => :batali_resolv,
21
23
  :requirements => batali_file.cookbook.map{ |ckbk|
22
- [ckbk.name, *(ckbk.constraint.empty? ? ['> 0'] : ckbk.constraint)]
24
+ [ckbk.name, *(ckbk.constraint.nil? || ckbk.constraint.empty? ? ['> 0'] : ckbk.constraint)]
23
25
  }
24
26
  )
25
27
  solv = Grimoire::Solver.new(
@@ -27,27 +29,39 @@ module Batali
27
29
  :system => system,
28
30
  :score_keeper => score_keeper
29
31
  )
30
- results = []
31
- run_action 'Resolving dependency constraints' do
32
- results = solv.generate!
33
- nil
34
- end
35
- if(results.empty?)
36
- ui.error 'No solutions found defined requirements!'
32
+ if(opts[:infrastructure])
33
+ ui.info 'Performing infrastructure path resolution.'
34
+ run_action 'Writing infrastructure manifest file' do
35
+ File.open('batali.manifest', 'w') do |file|
36
+ manifest = Manifest.new(:cookbook => solv.world.units.values.flatten)
37
+ file.write MultiJson.dump(manifest, :pretty => true)
38
+ nil
39
+ end
40
+ end
37
41
  else
38
- ideal_solution = results.pop
39
- dry_run('manifest file write') do
40
- run_action 'Writing manifest' do
41
- manifest = Manifest.new(:cookbook => ideal_solution.units)
42
- File.open('batali.manifest', 'w') do |file|
43
- file.write MultiJson.dump(manifest, :pretty => true)
42
+ ui.info 'Performing single path resolution.'
43
+ results = []
44
+ run_action 'Resolving dependency constraints' do
45
+ results = solv.generate!
46
+ nil
47
+ end
48
+ if(results.empty?)
49
+ ui.error 'No solutions found defined requirements!'
50
+ else
51
+ ideal_solution = results.pop
52
+ dry_run('manifest file write') do
53
+ run_action 'Writing manifest' do
54
+ manifest = Manifest.new(:cookbook => ideal_solution.units)
55
+ File.open('batali.manifest', 'w') do |file|
56
+ file.write MultiJson.dump(manifest, :pretty => true)
57
+ end
58
+ nil
44
59
  end
45
- nil
46
60
  end
61
+ ui.info "Number of solutions collected for defined requirements: #{results.size + 1}"
62
+ ui.info 'Ideal solution:'
63
+ ui.puts ideal_solution.units.sort_by(&:name).map{|u| "#{u.name}<#{u.version}>"}
47
64
  end
48
- ui.info "Found #{results.size} solutions for defined requirements."
49
- ui.info 'Ideal solution:'
50
- ui.puts ideal_solution.units.sort_by(&:name).map{|u| "#{u.name}<#{u.version}>"}
51
65
  end
52
66
  end
53
67
 
@@ -1,4 +1,5 @@
1
1
  require 'batali'
2
+ require 'fileutils'
2
3
 
3
4
  module Batali
4
5
  # Customized command base for Batali
@@ -32,6 +33,18 @@ module Batali
32
33
  end
33
34
  end
34
35
 
36
+ # @return [String] path to local cache
37
+ def cache_directory(*args)
38
+ memoize(['cache_directory', *args].join('_')) do
39
+ directory = opts.fetch(:cache_directory, '/tmp/batali-cache')
40
+ unless(args.empty?)
41
+ directory = File.join(directory, *args.map(&:to_s))
42
+ end
43
+ FileUtils.mkdir_p(directory)
44
+ directory
45
+ end
46
+ end
47
+
35
48
  # Do not execute block if dry run
36
49
  #
37
50
  # @param action [String] action to be performed
data/lib/batali/git.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'git'
2
+ require 'batali'
3
+
4
+ module Batali
5
+ module Git
6
+
7
+ # @return [String] path to repository clone
8
+ def base_path
9
+ File.join(cache, Base64.urlsafe_encode64(url))
10
+ end
11
+
12
+ # Clone the repository to the local machine
13
+ #
14
+ # @return [TrueClass]
15
+ def clone_repository
16
+ if(File.directory?(base_path))
17
+ repo = ::Git.open(base_path)
18
+ repo.checkout('master')
19
+ repo.pull
20
+ repo.fetch
21
+ else
22
+ ::Git.clone(url, base_path)
23
+ end
24
+ true
25
+ end
26
+
27
+ # Duplicate reference and store
28
+ #
29
+ # @return [String] commit SHA
30
+ # @note this will update ref to SHA
31
+ def ref_dup
32
+ git = ::Git.open(base_path)
33
+ git.checkout(ref)
34
+ self.ref = git.log.first.sha
35
+ self.path = File.join(cache, ref)
36
+ unless(File.directory?(path))
37
+ FileUtils.mkdir_p(path)
38
+ FileUtils.cp_r(File.join(base_path, '.'), path)
39
+ FileUtils.rm_rf(File.join(path, '.git'))
40
+ end
41
+ self.path
42
+ end
43
+
44
+ # Load attributes into class
45
+ def self.included(klass)
46
+ klass.class_eval do
47
+ attribute :url, String, :required => true
48
+ attribute :ref, String, :required => true
49
+ attribute :cache, String
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -2,11 +2,11 @@ require 'batali'
2
2
 
3
3
  module Batali
4
4
  # Collection of resolved units
5
- class Manifest < Grimoire::Utility
5
+ class Manifest < Utility
6
6
 
7
7
  include Bogo::Memoization
8
8
 
9
- attribute :cookbook, Unit, :multiple => true, :coerce => lambda{|v| Unit.new(v)}
9
+ attribute :cookbook, Unit, :multiple => true, :coerce => lambda{|v| Unit.new(v)}, :default => []
10
10
 
11
11
  # Build manifest from given path. If no file exists, empty
12
12
  # manifest will be provided.
@@ -0,0 +1,54 @@
1
+ require 'git'
2
+ require 'batali'
3
+
4
+ module Batali
5
+ class Origin
6
+ # Fetch unit from local path
7
+ class Git < Path
8
+
9
+ include Batali::Git
10
+
11
+ def initialize(args={})
12
+ unless(args[:path])
13
+ args[:path] = '/dev/null'
14
+ end
15
+ super
16
+ self.identifier = Smash.new(
17
+ :url => url,
18
+ :ref => ref
19
+ ).checksum
20
+ unless(name?)
21
+ self.name = self.identifier
22
+ end
23
+ end
24
+
25
+ # @return [Array<Unit>]
26
+ def units
27
+ memoize(:g_units) do
28
+ items = super
29
+ items.first.source = Source::Git.new(
30
+ :url => url,
31
+ :ref => ref,
32
+ :path => path
33
+ )
34
+ items
35
+ end
36
+ end
37
+
38
+ # @return [Smash] metadata information
39
+ def load_metadata
40
+ fetch_repo
41
+ super
42
+ end
43
+
44
+ # @return [String] path to repository
45
+ def fetch_repo
46
+ memoize(:fetch_repo) do
47
+ clone_repository
48
+ ref_dup
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,79 @@
1
+ require 'batali'
2
+
3
+ module Batali
4
+ class Origin
5
+ # Fetch unit from local path
6
+ class Path < Origin
7
+
8
+ class Metadata < AttributeStruct
9
+
10
+ def depends(*args)
11
+ set!(:depends, args)
12
+ self
13
+ end
14
+
15
+ end
16
+
17
+ include Bogo::Memoization
18
+
19
+ attribute :path, String, :required => true
20
+
21
+ def initialize(*_)
22
+ super
23
+ self.identifier = Smash.new(:path => path).checksum
24
+ unless(name?)
25
+ self.name = self.identifier
26
+ end
27
+ end
28
+
29
+ # @return [Array<Unit>]
30
+ def units
31
+ memoize(:units) do
32
+ info = load_metadata
33
+ [
34
+ Unit.new(
35
+ :name => info[:name],
36
+ :version => info[:version],
37
+ :dependencies => info[:depends],
38
+ :source => Smash.new(
39
+ :type => :path,
40
+ :version => info[:version],
41
+ :path => path,
42
+ :dependencies => info[:depends].map{ |dep|
43
+ if(dep.size == 1)
44
+ dep.push('> 0')
45
+ else
46
+ dep.to_a
47
+ end
48
+ }
49
+ )
50
+ )
51
+ ]
52
+ end
53
+ end
54
+
55
+ # @return [Smash] metadata information
56
+ def load_metadata
57
+ memoize(:metadata) do
58
+ if(File.exists?(json = File.join(path, 'metadata.json')))
59
+ MultiJson.load(json).to_smash
60
+ elsif(File.exists?(rb = File.join(path, 'metadata.rb')))
61
+ struct = Metadata.new
62
+ struct.set_state!(:value_collapse => true)
63
+ File.readlines(rb).find_all do |line|
64
+ line.start_with?('name') ||
65
+ line.start_with?('version') ||
66
+ line.start_with?('depends')
67
+ end.each do |line|
68
+ struct.instance_eval(line)
69
+ end
70
+ struct._dump.to_smash
71
+ else
72
+ raise Errno::ENOENT.new('No metadata file available to load!')
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,100 @@
1
+ require 'batali'
2
+ require 'digest/sha2'
3
+ require 'securerandom'
4
+ require 'http'
5
+ require 'fileutils'
6
+
7
+ module Batali
8
+ class Origin
9
+ # Fetch unit information from remote site
10
+ class RemoteSite < Origin
11
+
12
+ # Site suffix for API endpoint
13
+ COOKBOOK_API_SUFFIX = 'api/v1/cookbooks'
14
+
15
+ include Bogo::Memoization
16
+
17
+ attribute :name, String
18
+ attribute :identifier, String
19
+ attribute :endpoint, String, :required => true
20
+ attribute :force_update, [TrueClass, FalseClass], :required => true, :default => false
21
+ attribute :update_interval, Integer, :required => true, :default => 60
22
+ attribute :cache, String, :default => File.expand_path('~/.batali/cache/remote_site'), :required => true
23
+
24
+ def initialize(*_)
25
+ super
26
+ endpoint = URI.join(self.endpoint, COOKBOOK_API_SUFFIX).to_s
27
+ self.identifier = Digest::SHA256.hexdigest(endpoint)
28
+ unless(name?)
29
+ self.name = self.identifier
30
+ end
31
+ end
32
+
33
+ # @return [String] cache directory path
34
+ def cache_directory
35
+ memoize(:cache_directory) do
36
+ path = File.join(cache, identifier)
37
+ FileUtils.mkdir_p(path)
38
+ path
39
+ end
40
+ end
41
+
42
+ # @return [Array<Unit>] all units
43
+ def units
44
+ memoize(:units) do
45
+ items.map do |u_name, versions|
46
+ versions.map do |version, info|
47
+ Unit.new(
48
+ :name => u_name,
49
+ :version => version,
50
+ :dependencies => info[:dependencies].to_a,
51
+ :source => Smash.new(
52
+ :type => :site,
53
+ :url => info[:download_url],
54
+ :version => version,
55
+ :dependencies => info[:dependencies]
56
+ )
57
+ )
58
+ end
59
+ end.flatten
60
+ end
61
+ end
62
+
63
+ protected
64
+
65
+ # @return [Smash] all info
66
+ def items
67
+ memoize(:items) do
68
+ MultiJson.load(File.read(fetch)).to_smash
69
+ end
70
+ end
71
+
72
+ # Fetch the universe
73
+ #
74
+ # @return [String] path to universe file
75
+ def fetch
76
+ do_fetch = true
77
+ if(File.exists?(universe_path))
78
+ age = Time.now - File.mtime(universe_path)
79
+ if(age < update_interval)
80
+ do_fetch = false
81
+ end
82
+ end
83
+ if(do_fetch)
84
+ t_uni = "#{universe_path}.#{SecureRandom.urlsafe_base64}"
85
+ File.open(t_uni, 'w') do |file|
86
+ file.write HTTP.get(URI.join(endpoint, 'universe')).body.to_s
87
+ end
88
+ FileUtils.mv(t_uni, universe_path)
89
+ end
90
+ universe_path
91
+ end
92
+
93
+ # @return [String] path to universe file
94
+ def universe_path
95
+ File.join(cache_directory, 'universe.json')
96
+ end
97
+
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,20 @@
1
+ require 'batali'
2
+
3
+ module Batali
4
+ # Cookbook source origin
5
+ class Origin < Utility
6
+
7
+ autoload :RemoteSite, 'batali/origin/remote_site'
8
+ autoload :Git, 'batali/origin/git'
9
+ autoload :Path, 'batali/origin/path'
10
+
11
+ attribute :name, String, :required => true
12
+ attribute :identifier, String
13
+
14
+ # @return [Array<Unit>] all units
15
+ def units
16
+ raise NotImplementedError.new 'Abstract class'
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ require 'batali'
2
+ require 'fileutils'
3
+ require 'tmpdir'
4
+
5
+ module Batali
6
+ # Source of asset
7
+ class Source
8
+ # Path based source
9
+ class Git < Path
10
+
11
+ include Bogo::Memoization
12
+ include Batali::Git
13
+
14
+ attribute :path, String
15
+
16
+ # @return [String] directory containing contents
17
+ def asset
18
+ clone_repository
19
+ self.path = ref_dup
20
+ super
21
+ end
22
+
23
+ # Overload to remove non-relevant attributes
24
+ def to_json(*args)
25
+ MultiJson.dump(
26
+ Smash.new(
27
+ :url => url,
28
+ :ref => ref,
29
+ :type => self.class.name
30
+ ), *args
31
+ )
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -13,6 +13,8 @@ module Batali
13
13
  attr_reader :dependencies
14
14
  # @return [String] version
15
15
  attr_reader :version
16
+ # @return [String] local cache path
17
+ attr_accessor :cache
16
18
 
17
19
  attribute :url, String, :required => true
18
20
  attribute :version, String, :required => true
@@ -38,31 +40,40 @@ module Batali
38
40
 
39
41
  # @return [String] directory
40
42
  def asset
41
- path = Dir.mktmpdir('batali')
42
- result = HTTP.get(url)
43
- while(result.code == 302)
44
- result = HTTP.get(result.headers['Location'])
45
- end
46
- File.open(a_path = File.join(path, 'asset'), 'w') do |file|
47
- while(content = result.body.readpartial(2048))
48
- file.write content
43
+ path = File.join(cache, Base64.urlsafe_encode64(url))
44
+ unless(File.directory?(path))
45
+ FileUtils.mkdir_p(path)
46
+ result = HTTP.get(url)
47
+ while(result.code == 302)
48
+ result = HTTP.get(result.headers['Location'])
49
49
  end
50
- end
51
- ext = Gem::Package::TarReader.new(
52
- Zlib::GzipReader.open(a_path)
53
- )
54
- ext.rewind
55
- ext.each do |entry|
56
- next unless entry.file?
57
- n_path = File.join(path, entry.full_name)
58
- FileUtils.mkdir_p(File.dirname(n_path))
59
- File.open(n_path, 'w') do |file|
60
- while(content = entry.read(2048))
61
- file.write(content)
50
+ File.open(a_path = File.join(path, 'asset'), 'w') do |file|
51
+ while(content = result.body.readpartial(2048))
52
+ file.write content
53
+ end
54
+ end
55
+ ext = Gem::Package::TarReader.new(
56
+ Zlib::GzipReader.open(a_path)
57
+ )
58
+ ext.rewind
59
+ ext.each do |entry|
60
+ next unless entry.file?
61
+ n_path = File.join(path, entry.full_name)
62
+ FileUtils.mkdir_p(File.dirname(n_path))
63
+ File.open(n_path, 'w') do |file|
64
+ while(content = entry.read(2048))
65
+ file.write(content)
66
+ end
62
67
  end
63
68
  end
69
+ FileUtils.rm(a_path)
64
70
  end
65
- path
71
+ Dir.glob(File.join(path, '*')).first
72
+ end
73
+
74
+ # @return [TrueClass, FalseClass]
75
+ def clean_asset(asset_path)
76
+ super File.dirname(asset_path)
66
77
  end
67
78
 
68
79
  end
data/lib/batali/source.rb CHANGED
@@ -2,7 +2,7 @@ require 'batali'
2
2
 
3
3
  module Batali
4
4
  # Source of asset
5
- class Source < Grimoire::Utility
5
+ class Source < Utility
6
6
 
7
7
  autoload :Path, 'batali/source/path'
8
8
  autoload :Site, 'batali/source/site'
@@ -25,6 +25,20 @@ module Batali
25
25
  raise NotImplementedError.new 'Abstract class'
26
26
  end
27
27
 
28
+ # @return [TrueClass, FalseClass]
29
+ def clean_asset(asset_path)
30
+ if(self.respond_to?(:cache))
31
+ false
32
+ else
33
+ if(File.exists?(asset_path))
34
+ FileUtils.rm_rf(asset_path)
35
+ true
36
+ else
37
+ false
38
+ end
39
+ end
40
+ end
41
+
28
42
  # Build a source
29
43
  #
30
44
  # @param args [Hash]
@@ -0,0 +1,70 @@
1
+ require 'batali'
2
+
3
+ module Batali
4
+
5
+ class UnitLoader < Utility
6
+
7
+ include Bogo::Memoization
8
+
9
+ attribute :file, BFile, :required => true
10
+ attribute :system, Grimoire::System, :required => true
11
+ attribute :cache, String, :required => true
12
+
13
+ # Populate the system with units
14
+ #
15
+ # @return [self]
16
+ def populate!
17
+ memoize(:populate) do
18
+ file.source.each do |src|
19
+ src.units.find_all do |unit|
20
+ if(restrictions[unit.name])
21
+ restrictions[unit.name] == src.identifier
22
+ else
23
+ true
24
+ end
25
+ end.each do |unit|
26
+ system.add_unit(unit)
27
+ end
28
+ end
29
+ file.cookbook.each do |ckbk|
30
+ if(ckbk.path)
31
+ source = Origin::Path.new(
32
+ :name => ckbk.name,
33
+ :path => ckbk.path
34
+ )
35
+ elsif(ckbk.git)
36
+ source = Origin::Git.new(
37
+ :name => ckbk.name,
38
+ :url => ckbk.git,
39
+ :ref => ckbk.ref || 'master',
40
+ :cache => cache
41
+ )
42
+ end
43
+ if(source)
44
+ system.add_unit(source.units.first)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # @return [Smash]
51
+ def restrictions
52
+ memoize(:restrictions) do
53
+ rest = (file.restrict || Smash.new).to_smash
54
+ file.cookbook.each do |ckbk|
55
+ if(ckbk.path)
56
+ rest[ckbk.name] = Smash.new(:path => ckbk.path).checksum
57
+ elsif(ckbk.git)
58
+ rest[ckbk.name] = Smash.new(
59
+ :url => ckbk.git,
60
+ :ref => ckbk.ref
61
+ ).checksum
62
+ end
63
+ end
64
+ rest
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,7 @@
1
+ require 'batali'
2
+
3
+ module Batali
4
+ class Utility < Grimoire::Utility
5
+
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Batali
2
- VERSION = Gem::Version.new('0.1.0')
2
+ VERSION = Gem::Version.new('0.1.2')
3
3
  end
data/lib/batali.rb CHANGED
@@ -5,11 +5,14 @@ module Batali
5
5
 
6
6
  autoload :Command, 'batali/command'
7
7
  autoload :Config, 'batali/config'
8
+ autoload :Git, 'batali/git'
8
9
  autoload :Manifest, 'batali/manifest'
9
- autoload :RemoteSite, 'batali/remote_site'
10
+ autoload :Origin, 'batali/origin'
10
11
  autoload :ScoreKeeper, 'batali/score_keeper'
11
12
  autoload :Source, 'batali/source'
12
13
  autoload :Unit, 'batali/unit'
14
+ autoload :UnitLoader, 'batali/unit_loader'
15
+ autoload :Utility, 'batali/utility'
13
16
 
14
17
  end
15
18
 
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: batali
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Roberts
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-06 00:00:00.000000000 Z
11
+ date: 2015-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: attribute_struct
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.14
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.14
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: grimoire
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ">="
18
32
  - !ruby/object:Gem::Version
19
- version: 0.1.2
33
+ version: 0.1.4
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - ">="
25
39
  - !ruby/object:Gem::Version
26
- version: 0.1.2
40
+ version: 0.1.4
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bogo
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - ">="
67
81
  - !ruby/object:Gem::Version
68
82
  version: 0.1.10
83
+ - !ruby/object:Gem::Dependency
84
+ name: git
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: http
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -129,14 +157,21 @@ files:
129
157
  - lib/batali/command/resolve.rb
130
158
  - lib/batali/command/update.rb
131
159
  - lib/batali/config.rb
160
+ - lib/batali/git.rb
132
161
  - lib/batali/manifest.rb
133
162
  - lib/batali/monkey.rb
134
- - lib/batali/remote_site.rb
163
+ - lib/batali/origin.rb
164
+ - lib/batali/origin/git.rb
165
+ - lib/batali/origin/path.rb
166
+ - lib/batali/origin/remote_site.rb
135
167
  - lib/batali/score_keeper.rb
136
168
  - lib/batali/source.rb
169
+ - lib/batali/source/git.rb
137
170
  - lib/batali/source/path.rb
138
171
  - lib/batali/source/site.rb
139
172
  - lib/batali/unit.rb
173
+ - lib/batali/unit_loader.rb
174
+ - lib/batali/utility.rb
140
175
  - lib/batali/version.rb
141
176
  homepage: https://github.com/hw-labs/batali
142
177
  licenses:
@@ -1,98 +0,0 @@
1
- require 'batali'
2
- require 'digest/sha2'
3
- require 'securerandom'
4
- require 'http'
5
- require 'fileutils'
6
-
7
- module Batali
8
- # Fetch unit information from remote site
9
- class RemoteSite < Grimoire::Utility
10
-
11
- # Site suffix for API endpoint
12
- COOKBOOK_API_SUFFIX = 'api/v1/cookbooks'
13
-
14
- include Bogo::Memoization
15
-
16
- attribute :name, String
17
- attribute :identifier, String
18
- attribute :endpoint, String, :required => true
19
- attribute :force_update, [TrueClass, FalseClass], :required => true, :default => false
20
- attribute :update_interval, Integer, :required => true, :default => 10000 # NOTE: reset this default to 60/120 when ready
21
- attribute :cache, String, :default => File.expand_path('~/.batali/cache/remote_site'), :required => true
22
-
23
- def initialize(*_)
24
- super
25
- endpoint = URI.join(self.endpoint, COOKBOOK_API_SUFFIX).to_s
26
- self.identifier = Digest::SHA256.hexdigest(endpoint)
27
- unless(name?)
28
- self.name = self.identifier
29
- end
30
- end
31
-
32
- # @return [String] cache directory path
33
- def cache_directory
34
- memoize(:cache_directory) do
35
- path = File.join(cache, identifier)
36
- FileUtils.mkdir_p(path)
37
- path
38
- end
39
- end
40
-
41
- # @return [Array<Unit>] all units
42
- def units
43
- memoize(:units) do
44
- items.map do |u_name, versions|
45
- versions.map do |version, info|
46
- Unit.new(
47
- :name => u_name,
48
- :version => version,
49
- :dependencies => info[:dependencies].to_a,
50
- :source => Smash.new(
51
- :type => :site,
52
- :url => info[:download_url],
53
- :version => version,
54
- :dependencies => info[:dependencies]
55
- )
56
- )
57
- end
58
- end.flatten
59
- end
60
- end
61
-
62
- protected
63
-
64
- # @return [Smash] all info
65
- def items
66
- memoize(:items) do
67
- MultiJson.load(File.read(fetch)).to_smash
68
- end
69
- end
70
-
71
- # Fetch the universe
72
- #
73
- # @return [String] path to universe file
74
- def fetch
75
- do_fetch = true
76
- if(File.exists?(universe_path))
77
- age = Time.now - File.mtime(universe_path)
78
- if(age < update_interval)
79
- do_fetch = false
80
- end
81
- end
82
- if(do_fetch)
83
- t_uni = "#{universe_path}.#{SecureRandom.urlsafe_base64}"
84
- File.open(t_uni, 'w') do |file|
85
- file.write HTTP.get(URI.join(endpoint, 'universe')).body.to_s
86
- end
87
- FileUtils.mv(t_uni, universe_path)
88
- end
89
- universe_path
90
- end
91
-
92
- # @return [String] path to universe file
93
- def universe_path
94
- File.join(cache_directory, 'universe.json')
95
- end
96
-
97
- end
98
- end