batali 0.1.0 → 0.1.2

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