mantra 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -2
- data/README.md +126 -16
- data/bin/mantra +17 -0
- data/ci/README.md +6 -0
- data/ci/docker/Dockerfile +20 -0
- data/ci/docker/build-docker-image +10 -0
- data/ci/pipeline.yml +42 -0
- data/ci/scripts/run-tests +3 -0
- data/ci/stub.example.yml +6 -0
- data/ci/stub.yml +33 -0
- data/ci/tasks/run-tests-dev.yml +10 -0
- data/ci/tasks/run-tests-stable.yml +13 -0
- data/lib/mantra/certificate.rb +35 -0
- data/lib/mantra/certificates/cortege.rb +64 -0
- data/lib/mantra/certificates/group.rb +26 -0
- data/lib/mantra/command.rb +51 -0
- data/lib/mantra/commands/find.rb +38 -0
- data/lib/mantra/commands/transform.rb +38 -0
- data/lib/mantra/helpers/object_with_type.rb +60 -0
- data/lib/mantra/helpers/regexp_helper.rb +12 -0
- data/lib/mantra/helpers/template_helper.rb +70 -0
- data/lib/mantra/manifest/array_element.rb +84 -0
- data/lib/mantra/manifest/element.rb +175 -0
- data/lib/mantra/manifest/ext.rb +33 -0
- data/lib/mantra/manifest/hash_element.rb +88 -0
- data/lib/mantra/manifest/leaf_element.rb +38 -0
- data/lib/mantra/manifest/root_element.rb +38 -0
- data/lib/mantra/manifest/scope/array_scope.rb +53 -0
- data/lib/mantra/manifest/scope/empty_scope.rb +24 -0
- data/lib/mantra/manifest/scope/hash_scope.rb +20 -0
- data/lib/mantra/manifest/scope/leaf_scope.rb +0 -0
- data/lib/mantra/manifest/scope.rb +94 -0
- data/lib/mantra/manifest.rb +47 -0
- data/lib/mantra/merge_tool/spiff.rb +23 -0
- data/lib/mantra/merge_tool.rb +13 -0
- data/lib/mantra/transform/extract.rb +10 -0
- data/lib/mantra/transform/extract_certificates.rb +18 -0
- data/lib/mantra/transform/extract_certificates_to_files.rb +12 -0
- data/lib/mantra/transform/extract_section.rb +10 -0
- data/lib/mantra/transform/inputs/any.rb +10 -0
- data/lib/mantra/transform/inputs/array.rb +16 -0
- data/lib/mantra/transform/inputs/file.rb +16 -0
- data/lib/mantra/transform/inputs/folder.rb +9 -0
- data/lib/mantra/transform/inputs/hash.rb +16 -0
- data/lib/mantra/transform/inputs/string.rb +9 -0
- data/lib/mantra/transform/merge.rb +26 -0
- data/lib/mantra/transform/replace.rb +34 -0
- data/lib/mantra/transform/templatize_ip_address.rb +129 -0
- data/lib/mantra/transform/templatize_value.rb +63 -0
- data/lib/mantra/transform.rb +118 -0
- data/lib/mantra/version.rb +1 -1
- data/lib/mantra.rb +9 -1
- data/mantra.gemspec +6 -5
- metadata +69 -8
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,94 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Manifest
|
3
|
+
class Scope
|
4
|
+
|
5
|
+
class ScopeParseError < Exception; end
|
6
|
+
|
7
|
+
include Helpers::ObjectWithType
|
8
|
+
include Helpers::RegexpHelper
|
9
|
+
|
10
|
+
def self.parse(scope_or_string)
|
11
|
+
return scope_or_string if scope_or_string.is_a?(Scope)
|
12
|
+
parse_selector(scope_or_string)
|
13
|
+
end
|
14
|
+
|
15
|
+
HASH_SELECTOR_REGEXP = /^([a-zA-Z0-9\_\-\=\*]+)\.?(.*)$/
|
16
|
+
ARRAY_SELECTOR_REGEXP = /^\[([a-zA-Z0-9\:\_\-\=\*]*)\]\.?(.*)$/
|
17
|
+
|
18
|
+
def self.parse_selector(selector)
|
19
|
+
case selector
|
20
|
+
when HASH_SELECTOR_REGEXP
|
21
|
+
head_selector, tail_selector = split_selector(selector, HASH_SELECTOR_REGEXP)
|
22
|
+
HashScope.new(scope: head_selector, next: parse_selector(tail_selector))
|
23
|
+
when ARRAY_SELECTOR_REGEXP
|
24
|
+
head_selector, tail_selector = split_selector(selector, ARRAY_SELECTOR_REGEXP)
|
25
|
+
ArrayScope.new(scope: head_selector, next: parse_selector(tail_selector))
|
26
|
+
when ""
|
27
|
+
EmptyScope.new()
|
28
|
+
else
|
29
|
+
raise_parse_error(selector)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.split_selector(selector, matcher_regex)
|
34
|
+
matcher = selector.match(matcher_regex)
|
35
|
+
raise_parse_error(selector) if matcher.nil?
|
36
|
+
head_selector = matcher[1]
|
37
|
+
tail_selector = matcher[2]
|
38
|
+
return head_selector, tail_selector
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.raise_parse_error(selector)
|
42
|
+
raise ScopeParseError.new("Can't parse selector: #{selector}")
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :scope, :next
|
46
|
+
|
47
|
+
def initialize(options={})
|
48
|
+
self.scope, self.next = options[:scope], options[:next]
|
49
|
+
end
|
50
|
+
|
51
|
+
def last?
|
52
|
+
self.is_a?(EmptyScope)
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :tail, :next
|
56
|
+
|
57
|
+
def match?(manifest_element)
|
58
|
+
raise "not implemented"
|
59
|
+
end
|
60
|
+
|
61
|
+
def filter(element, &block)
|
62
|
+
return [] if !has_same_type?(element)
|
63
|
+
matched_elements = _filter(element)
|
64
|
+
if self.last?
|
65
|
+
block.call(matched_elements) if block_given?
|
66
|
+
matched_elements
|
67
|
+
else
|
68
|
+
matched_elements.map do |e|
|
69
|
+
self.next.filter(e)
|
70
|
+
end.compact.flatten
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def has_same_type?(element)
|
75
|
+
raise element.inspect if element.is_a?(Array)
|
76
|
+
self.type == element.type
|
77
|
+
end
|
78
|
+
|
79
|
+
def _filter(element)
|
80
|
+
raise "not implemented"
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_a
|
84
|
+
[self, self.tail.to_a].flatten
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
require "mantra/manifest/scope/array_scope"
|
92
|
+
require "mantra/manifest/scope/empty_scope"
|
93
|
+
# require "mantra/manifest/scope/leaf_scope"
|
94
|
+
require "mantra/manifest/scope/hash_scope"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "mantra/manifest/scope"
|
3
|
+
require "mantra/manifest/element"
|
4
|
+
require "mantra/manifest/ext"
|
5
|
+
require "mantra/manifest/root_element"
|
6
|
+
require "mantra/manifest/array_element"
|
7
|
+
require "mantra/manifest/hash_element"
|
8
|
+
require "mantra/manifest/leaf_element"
|
9
|
+
|
10
|
+
module Mantra
|
11
|
+
class Manifest
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
include Mantra::Manifest::Ext
|
15
|
+
|
16
|
+
class UnknownScopeError < Exception; end
|
17
|
+
class FileNotFoundError < Exception; end
|
18
|
+
|
19
|
+
attr_accessor :root, :file
|
20
|
+
|
21
|
+
def_delegators :@root, :merge, :to_ruby_object, :traverse,
|
22
|
+
:select, :find, :add_node, :fetch, :get
|
23
|
+
|
24
|
+
def initialize(manifest_object_or_path)
|
25
|
+
if manifest_object_or_path.is_a?(String) && File.exist?(manifest_object_or_path)
|
26
|
+
manifest_object = YAML.load_file(manifest_object_or_path)
|
27
|
+
self.file = manifest_object_or_path
|
28
|
+
self.root = Element.create(manifest_object)
|
29
|
+
elsif manifest_object_or_path.is_a?(Hash) || manifest_object_or_path.is_a?(Array)
|
30
|
+
self.root = Element.create(manifest_object_or_path)
|
31
|
+
else
|
32
|
+
raise "Don't know how initialize manifest. Expected existing file path or hash, " +
|
33
|
+
"got #{manifest_object_or_path.class.inspect}: #{manifest_object_or_path.inspect}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def write(manifest_path)
|
38
|
+
File.open(manifest_path, 'w') { |f| f.write(self.to_ruby_object.to_yaml) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def save
|
42
|
+
raise FileNotFoundError.new("File is not specified") if self.file.nil?
|
43
|
+
self.write(self.file)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Mantra
|
2
|
+
class MergeTool
|
3
|
+
class Spiff < MergeTool
|
4
|
+
type :spiff
|
5
|
+
|
6
|
+
def templatize(string, scope, start, finish)
|
7
|
+
first_part = string[0..start-1]
|
8
|
+
value = string[start..finish]
|
9
|
+
last_part = string[finish..-1]
|
10
|
+
first_part = first_part[3..-1] if first_part.end_with?("(( ")
|
11
|
+
last_part = last_part[0..-4] if last_part.end_with?(" ))")
|
12
|
+
["((", "\"#{first_part}\"", scope, "\"#{last_part}\"", "))"].select do |s|
|
13
|
+
s != "\"\""
|
14
|
+
end.join(" ")
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_template?(string)
|
18
|
+
!!string.match(/^\(\(.*\)\)$/)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class ExtractCertificates < Transform
|
4
|
+
type :"extract-certificates"
|
5
|
+
|
6
|
+
description "Extracts certificates from source manifest, " +
|
7
|
+
"templatize source manifest and places certificates to target manifest."
|
8
|
+
input "source", type: :file,
|
9
|
+
validate: :file_exists,
|
10
|
+
description: "Source manifest with ceritificates, that will become a template"
|
11
|
+
input "target", type: :file,
|
12
|
+
description: "Target manifest with extracted ceritificates"
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class Input
|
4
|
+
class ArrayInput < Input
|
5
|
+
type :array
|
6
|
+
|
7
|
+
def validate_file_exists(value, name)
|
8
|
+
unless File.exist?(value)
|
9
|
+
raise ValidationError.new("File does not exist: #{value}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class Input
|
4
|
+
class FileInput < Input
|
5
|
+
type :file
|
6
|
+
|
7
|
+
def validate_file_exists(value, name)
|
8
|
+
unless File.exist?(value)
|
9
|
+
raise ValidationError.new("File does not exist: #{value}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class Input
|
4
|
+
class HashInput < Input
|
5
|
+
type :hash
|
6
|
+
|
7
|
+
# def validate_file_exists(value, name)
|
8
|
+
# unless ::File.exist?(value)
|
9
|
+
# raise ValidationError.new("File does not exist: #{value}")
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class Merge < Mantra::Transform
|
4
|
+
type :"merge"
|
5
|
+
attr_accessor :manifest
|
6
|
+
|
7
|
+
description "Merge"
|
8
|
+
|
9
|
+
input "value", description: "value that should be merged in", type: :any
|
10
|
+
|
11
|
+
input "scope", description: "scope (or path) in stub file that will have current value (for instance meta.networks)",
|
12
|
+
type: :string
|
13
|
+
|
14
|
+
def perform
|
15
|
+
@manifest = previous_transform.manifest
|
16
|
+
value_manifest_element = Mantra::Manifest::Element.create(self.value).content
|
17
|
+
|
18
|
+
scope = @manifest.fetch(self.scope)
|
19
|
+
scope.each do |s|
|
20
|
+
s.merge(value_manifest_element)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class Replace < Mantra::Transform
|
4
|
+
type :"replace"
|
5
|
+
attr_accessor :manifest
|
6
|
+
|
7
|
+
description "Merge"
|
8
|
+
|
9
|
+
input "value", description: "value that should be merged in", type: :any
|
10
|
+
|
11
|
+
input "scope", description: "scope (or path) in stub file that will have current value (for instance meta.networks)",
|
12
|
+
type: :string
|
13
|
+
|
14
|
+
def perform
|
15
|
+
@manifest = previous_transform.manifest
|
16
|
+
# value_manifest_element = Mantra::Manifest::Element.create(self.value).content
|
17
|
+
|
18
|
+
scope = @manifest.fetch(self.scope)
|
19
|
+
scope.each do |s|
|
20
|
+
puts "psssss!!"
|
21
|
+
puts s.class
|
22
|
+
s.content = self.value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def raise_if_no_source_manifest
|
27
|
+
if self.source.nil? || !File.exist?(self.source)
|
28
|
+
raise Manifest::FileNotFoundError.new("Source manifest does not exist: #{self.source.inspect}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class TemplatizeIpAddress < Transform
|
4
|
+
type :"templatize-ip-address"
|
5
|
+
include Mantra::Helpers::RegexpHelper
|
6
|
+
include Mantra::Helpers::TemplateHelper
|
7
|
+
|
8
|
+
description "Templatize ip addresses"
|
9
|
+
|
10
|
+
input "source", description: "Source manifest with ceritificates, that will become a template",
|
11
|
+
type: :file,
|
12
|
+
validate: :file_exists
|
13
|
+
|
14
|
+
input "target", description: "Target manifest with extracted ceritificates",
|
15
|
+
type: :file
|
16
|
+
|
17
|
+
input "quads", description: "quad that is going to be extracted",
|
18
|
+
type: :array
|
19
|
+
|
20
|
+
def perform
|
21
|
+
raise_error_if_no_source_manifest
|
22
|
+
ensure_yml_file_exist(self.target)
|
23
|
+
|
24
|
+
source_manifest.traverse do |node|
|
25
|
+
template, values = if is_ip_address?(node.content)
|
26
|
+
splitter = QuadSplitter.new(node.content, quads)
|
27
|
+
["(( #{splitter.parts(templatize: true).join(" ")} ))", splitter.values]
|
28
|
+
elsif is_network_range?(node.content)
|
29
|
+
ip_address, network_prefix_size = *node.content.split("/")
|
30
|
+
puts "NETWORK RANGE! #{[ip_address, network_prefix_size].inspect}"
|
31
|
+
splitter = QuadSplitter.new(ip_address, quads)
|
32
|
+
network_range_parts = splitter.parts + ["/#{network_prefix_size}"]
|
33
|
+
# puts "PARTS! #{network_range_parts.inspect}"
|
34
|
+
resulting_template = "(( #{templatize(network_range_parts).join(" ")} ))"
|
35
|
+
[resulting_template, splitter.values]
|
36
|
+
elsif is_ip_range?(node.content)
|
37
|
+
ip_address1, ip_address2 = *node.content.split("-").map { |ip| ip.strip }
|
38
|
+
splitter1 = QuadSplitter.new(ip_address1, quads)
|
39
|
+
splitter2 = QuadSplitter.new(ip_address2, quads)
|
40
|
+
result = templatize(splitter1.parts + ["-"] + splitter2.parts)
|
41
|
+
["(( #{result.join(" ")} ))", splitter1.values]
|
42
|
+
end
|
43
|
+
unless template.nil?
|
44
|
+
puts template.inspect if node.content == "192.168.3.0/24"
|
45
|
+
raise UnknownScopeError.new("Can't templatize non leaf ip address") unless node.leaf?
|
46
|
+
node.content = template
|
47
|
+
values.each_pair do |scope, value|
|
48
|
+
scope_element = Manifest::Element.element_with_selector(scope, value)
|
49
|
+
target_manifest.merge(scope_element)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
source_manifest.save
|
55
|
+
target_manifest.save
|
56
|
+
end
|
57
|
+
|
58
|
+
def ip_address_matcher
|
59
|
+
"(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})"
|
60
|
+
end
|
61
|
+
|
62
|
+
def is_network_range?(value)
|
63
|
+
!!value.to_s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/\d{1,2}$/)
|
64
|
+
end
|
65
|
+
|
66
|
+
def is_ip_range?(value)
|
67
|
+
!!value.to_s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\s*-\s*(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
68
|
+
end
|
69
|
+
|
70
|
+
def is_ip_address?(value)
|
71
|
+
!!value.to_s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
72
|
+
end
|
73
|
+
|
74
|
+
# def templatize(parts)
|
75
|
+
# raise "wrong!!!"
|
76
|
+
# merged_parts = [parts.shift]
|
77
|
+
# parts.each do |p|
|
78
|
+
# if !p.is_a?(Scope) && !merged_parts.last.is_a?(Scope)
|
79
|
+
# merged_parts.last.concat(p)
|
80
|
+
# else
|
81
|
+
# merged_parts << p
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
# merged_parts.select { |p| !p.empty? }.map do |p|
|
85
|
+
# p.is_a?(Scope) ? p : "\"#{p}\""
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
|
89
|
+
class QuadSplitter
|
90
|
+
include Mantra::Helpers::TemplateHelper
|
91
|
+
attr_accessor :values, :parts
|
92
|
+
def initialize(ip_address, template_quads)
|
93
|
+
@values = {}
|
94
|
+
extract_options = template_quads.map do |quad|
|
95
|
+
quad["range_object"] = if quad["number"]
|
96
|
+
index = quad["number"].to_i - 1
|
97
|
+
(index..index)
|
98
|
+
elsif quad["range"]
|
99
|
+
index1, index2 = *quad["range"].split("-").map { |v| v.strip.to_i - 1 }
|
100
|
+
(index1..index2)
|
101
|
+
end
|
102
|
+
quad
|
103
|
+
end
|
104
|
+
|
105
|
+
template = IpAddressTemplate.new(ip_address)
|
106
|
+
|
107
|
+
extract_options.each do |option|
|
108
|
+
range = option["range_object"]
|
109
|
+
value_to_extract = template.quads[range].join(".")
|
110
|
+
if option["with_value"].nil? || value_to_extract == option["with_value"]
|
111
|
+
template.replace_with_scope(range, option["scope"])
|
112
|
+
@values[option["scope"]] = value_to_extract
|
113
|
+
end
|
114
|
+
end
|
115
|
+
@parts = template.parts
|
116
|
+
end
|
117
|
+
|
118
|
+
def parts(options={templatize: false})
|
119
|
+
if options[:templatize]
|
120
|
+
templatize(@parts)
|
121
|
+
else
|
122
|
+
@parts
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Mantra
|
2
|
+
class Transform
|
3
|
+
class TemplatizeValue < Transform
|
4
|
+
type :"templatize-value"
|
5
|
+
|
6
|
+
include Mantra::Helpers::RegexpHelper
|
7
|
+
|
8
|
+
description "Extracts specified value to stub file"
|
9
|
+
|
10
|
+
input "source", description: "Source manifest with ceritificates, that will become a template",
|
11
|
+
type: :file,
|
12
|
+
validate: :file_exists
|
13
|
+
|
14
|
+
input "target", description: "Target manifest with extracted ceritificates",
|
15
|
+
type: :file
|
16
|
+
|
17
|
+
input "value", description: "value you want to templatize (for instance 'domain.com')",
|
18
|
+
type: :string
|
19
|
+
|
20
|
+
input "scope", description: "scope (or path) in stub file that will have current value (for instance meta.networks)",
|
21
|
+
type: :string
|
22
|
+
|
23
|
+
input "in", description: "wildcard matcher (for instance 'https://*.domain.com')",
|
24
|
+
type: :string,
|
25
|
+
optional: true
|
26
|
+
|
27
|
+
input "regexp", description: "string with ruby regexp to match values",
|
28
|
+
type: :string,
|
29
|
+
optional: true
|
30
|
+
|
31
|
+
def perform
|
32
|
+
raise_error_if_no_source_manifest
|
33
|
+
ensure_yml_file_exist(self.target)
|
34
|
+
# value_matcher = to_regexp(value)
|
35
|
+
|
36
|
+
raise "scope must not match value wildcard" if scope.match(value)
|
37
|
+
|
38
|
+
source_manifest.traverse do |node|
|
39
|
+
if node.content.to_s.match(value)
|
40
|
+
# match = node.content.to_s.match(value_matcher)[0]
|
41
|
+
match = node.content.to_s.match(value)[0]
|
42
|
+
begin_index = node.content.to_s.index(match)
|
43
|
+
end_index = begin_index + match.size
|
44
|
+
final_value = merge_tool.templatize(node.content.to_s, scope, begin_index, end_index)
|
45
|
+
node.content = final_value
|
46
|
+
scope_element = Manifest::Element.element_with_selector(scope, value)
|
47
|
+
target_manifest.merge(scope_element)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
source_manifest.save
|
52
|
+
target_manifest.save
|
53
|
+
end
|
54
|
+
|
55
|
+
def raise_if_no_source_manifest
|
56
|
+
if self.source.nil? || !File.exist?(self.source)
|
57
|
+
raise Manifest::FileNotFoundError.new("Source manifest does not exist: #{self.source.inspect}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|