manifests-cf-plugin 0.7.0.rc1
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.
- data/Rakefile +5 -0
- data/lib/manifests-cf-plugin.rb +317 -0
- data/lib/manifests-cf-plugin/errors.rb +33 -0
- data/lib/manifests-cf-plugin/loader.rb +31 -0
- data/lib/manifests-cf-plugin/loader/builder.rb +37 -0
- data/lib/manifests-cf-plugin/loader/normalizer.rb +149 -0
- data/lib/manifests-cf-plugin/loader/resolver.rb +79 -0
- data/lib/manifests-cf-plugin/plugin.rb +145 -0
- data/lib/manifests-cf-plugin/version.rb +3 -0
- data/spec/manifests-cf-plugin/errors_spec.rb +29 -0
- data/spec/manifests-cf-plugin/loader/builder_spec.rb +84 -0
- data/spec/manifests-cf-plugin/loader/normalizer_spec.rb +176 -0
- data/spec/manifests-cf-plugin/plugin_spec.rb +365 -0
- data/spec/manifests-cf-plugin_spec.rb +321 -0
- data/spec/spec_helper.rb +17 -0
- metadata +173 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
module CFManifests
|
2
|
+
module Resolver
|
3
|
+
def resolve(manifest, resolver)
|
4
|
+
new = {}
|
5
|
+
|
6
|
+
new[:applications] = manifest[:applications].collect do |app|
|
7
|
+
resolve_lexically(resolver, app, [manifest])
|
8
|
+
end
|
9
|
+
|
10
|
+
resolve_lexically(resolver, new, [new])
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# resolve symbols, with hashes introducing new lexical symbols
|
16
|
+
def resolve_lexically(resolver, val, ctx)
|
17
|
+
case val
|
18
|
+
when Hash
|
19
|
+
new = {}
|
20
|
+
|
21
|
+
val.each do |k, v|
|
22
|
+
new[k] = resolve_lexically(resolver, v, [val] + ctx)
|
23
|
+
end
|
24
|
+
|
25
|
+
new
|
26
|
+
when Array
|
27
|
+
val.collect do |v|
|
28
|
+
resolve_lexically(resolver, v, ctx)
|
29
|
+
end
|
30
|
+
when String
|
31
|
+
val.gsub(/\$\{([^\}]+)\}/) do
|
32
|
+
resolve_symbol(resolver, $1, ctx)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
val
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# resolve a symbol to its value, and then resolve that value
|
40
|
+
def resolve_symbol(resolver, sym, ctx)
|
41
|
+
if found = find_symbol(sym.to_sym, ctx)
|
42
|
+
resolve_lexically(resolver, found, ctx)
|
43
|
+
found
|
44
|
+
elsif dynamic = resolver.resolve_symbol(sym)
|
45
|
+
dynamic
|
46
|
+
else
|
47
|
+
fail("Unknown symbol in manifest: #{sym}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# search for a symbol introduced in the lexical context
|
52
|
+
def find_symbol(sym, ctx)
|
53
|
+
ctx.each do |h|
|
54
|
+
if val = resolve_in(h, sym)
|
55
|
+
return val
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# find a value, searching in explicit properties first
|
63
|
+
def resolve_in(hash, *where)
|
64
|
+
find_in_hash(hash, [:properties] + where) ||
|
65
|
+
find_in_hash(hash, where)
|
66
|
+
end
|
67
|
+
|
68
|
+
# helper for following a path of values in a hash
|
69
|
+
def find_in_hash(hash, where)
|
70
|
+
what = hash
|
71
|
+
where.each do |x|
|
72
|
+
return nil unless what.is_a?(Hash)
|
73
|
+
what = what[x]
|
74
|
+
end
|
75
|
+
|
76
|
+
what
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
require "cf/plugin"
|
4
|
+
require "manifests-cf-plugin"
|
5
|
+
|
6
|
+
|
7
|
+
class ManifestsPlugin < CF::App::Base
|
8
|
+
include CFManifests
|
9
|
+
|
10
|
+
option :manifest, :aliases => "-m", :value => :file,
|
11
|
+
:desc => "Path to manifest file to use"
|
12
|
+
|
13
|
+
|
14
|
+
[ :start, :restart, :instances, :logs, :env, :health, :stats,
|
15
|
+
:scale, :app, :stop, :delete
|
16
|
+
].each do |wrap|
|
17
|
+
name_made_optional = change_argument(wrap, :app, :optional)
|
18
|
+
|
19
|
+
around(wrap) do |cmd, input|
|
20
|
+
wrap_with_optional_name(name_made_optional, cmd, input)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
add_input :push, :reset, :desc => "Reset to values in the manifest",
|
26
|
+
:default => false
|
27
|
+
|
28
|
+
around(:push) do |push, input|
|
29
|
+
wrap_push(push, input)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def wrap_with_optional_name(name_made_optional, cmd, input)
|
35
|
+
return cmd.call if input[:all]
|
36
|
+
|
37
|
+
unless manifest
|
38
|
+
# if the command knows how to handle this
|
39
|
+
if input.has?(:app) || !name_made_optional
|
40
|
+
return cmd.call
|
41
|
+
else
|
42
|
+
return no_apps
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
internal, external = apps_in_manifest(input)
|
47
|
+
|
48
|
+
return cmd.call if internal.empty? && !external.empty?
|
49
|
+
|
50
|
+
show_manifest_usage
|
51
|
+
|
52
|
+
if internal.empty? && external.empty?
|
53
|
+
internal = current_apps if internal.empty?
|
54
|
+
internal = all_apps if internal.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
internal = internal.collect { |app| app[:name] }
|
58
|
+
|
59
|
+
apps = internal + external
|
60
|
+
return no_apps if apps.empty?
|
61
|
+
|
62
|
+
apps.each.with_index do |app, num|
|
63
|
+
line unless quiet? || num == 0
|
64
|
+
cmd.call(input.without(:apps).merge_given(:app => app))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def apply_changes(app, input)
|
69
|
+
app.memory = megabytes(input[:memory]) if input.has?(:memory)
|
70
|
+
app.total_instances = input[:instances] if input.has?(:instances)
|
71
|
+
app.command = input[:command] if input.has?(:command)
|
72
|
+
app.production = input[:plan].upcase.start_with?("P") if input.has?(:plan)
|
73
|
+
app.framework = input[:framework] if input.has?(:framework)
|
74
|
+
app.runtime = input[:runtime] if input.has?(:runtime)
|
75
|
+
app.buildpack = input[:buildpack] if input.has?(:buildpack)
|
76
|
+
end
|
77
|
+
|
78
|
+
def wrap_push(push, input)
|
79
|
+
unless manifest
|
80
|
+
create_and_save_manifest(push, input)
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
particular, external = apps_in_manifest(input)
|
85
|
+
|
86
|
+
unless external.empty?
|
87
|
+
fail "Could not find #{b(external.join(", "))}' in the manifest."
|
88
|
+
end
|
89
|
+
|
90
|
+
apps = particular.empty? ? all_apps : particular
|
91
|
+
|
92
|
+
show_manifest_usage
|
93
|
+
|
94
|
+
spaced(apps) do |app_manifest|
|
95
|
+
push_with_manifest(app_manifest, push, input)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def push_with_manifest(app_manifest, push, input)
|
100
|
+
with_filters(
|
101
|
+
:push => {
|
102
|
+
:create_app => proc { |a|
|
103
|
+
setup_env(a, app_manifest)
|
104
|
+
a
|
105
|
+
},
|
106
|
+
:push_app => proc { |a|
|
107
|
+
setup_services(a, app_manifest)
|
108
|
+
a
|
109
|
+
}
|
110
|
+
}) do
|
111
|
+
app_input = push_input_for(app_manifest, input)
|
112
|
+
|
113
|
+
push.call(app_input)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def push_input_for(app_manifest, input)
|
118
|
+
existing_app = client.app_by_name(app_manifest[:name])
|
119
|
+
rebased_input = input.rebase_given(app_manifest)
|
120
|
+
|
121
|
+
if !existing_app || input[:reset]
|
122
|
+
input = rebased_input
|
123
|
+
else
|
124
|
+
warn_reset_changes if manifest_differs?(existing_app, rebased_input)
|
125
|
+
end
|
126
|
+
|
127
|
+
input.merge(
|
128
|
+
:path => from_manifest(app_manifest[:path]),
|
129
|
+
:name => app_manifest[:name],
|
130
|
+
:bind_services => false,
|
131
|
+
:create_services => false)
|
132
|
+
end
|
133
|
+
|
134
|
+
def manifest_differs?(app, input)
|
135
|
+
apply_changes(app, input)
|
136
|
+
app.changed?
|
137
|
+
end
|
138
|
+
|
139
|
+
def create_and_save_manifest(push, input)
|
140
|
+
with_filters(
|
141
|
+
:push => { :push_app => proc { |a| ask_to_save(input, a); a } }) do
|
142
|
+
push.call
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "manifests-cf-plugin/errors"
|
4
|
+
|
5
|
+
|
6
|
+
describe CFManifests::InvalidManifest do
|
7
|
+
let(:file) { "/path/to/file" }
|
8
|
+
|
9
|
+
subject { described_class.new(file) }
|
10
|
+
|
11
|
+
describe "#initialize" do
|
12
|
+
it "is initialized with a file" do
|
13
|
+
described_class.new(file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#to_s" do
|
18
|
+
it "says the file is malformed" do
|
19
|
+
expect(subject.to_s).to eq(
|
20
|
+
"Manifest file '#{file}' is malformed.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#file" do
|
25
|
+
it "returns the file it was initialized with" do
|
26
|
+
expect(subject.file).to eq(file)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "manifests-cf-plugin/loader"
|
4
|
+
require "manifests-cf-plugin/errors"
|
5
|
+
|
6
|
+
|
7
|
+
describe CFManifests::Builder do
|
8
|
+
subject { CFManifests::Loader.new(nil, nil) }
|
9
|
+
|
10
|
+
describe "#build" do
|
11
|
+
let(:file) { "manifest.yml" }
|
12
|
+
|
13
|
+
before do
|
14
|
+
FakeFS.activate!
|
15
|
+
|
16
|
+
File.open(file, "w") do |io|
|
17
|
+
io.write manifest
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
after do
|
22
|
+
FakeFS.deactivate!
|
23
|
+
FakeFS::FileSystem.clear
|
24
|
+
end
|
25
|
+
|
26
|
+
context "with a simple manifest" do
|
27
|
+
let(:manifest) do
|
28
|
+
<<EOF
|
29
|
+
---
|
30
|
+
foo: bar
|
31
|
+
EOF
|
32
|
+
end
|
33
|
+
|
34
|
+
it "loads the manifest YAML" do
|
35
|
+
expect(subject.build(file)).to eq("foo" => "bar")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "with a manifest that inherits another" do
|
40
|
+
let(:manifest) do
|
41
|
+
<<EOF
|
42
|
+
---
|
43
|
+
inherit: other-manifest.yml
|
44
|
+
foo:
|
45
|
+
baz: c
|
46
|
+
EOF
|
47
|
+
end
|
48
|
+
|
49
|
+
before do
|
50
|
+
FakeFS.activate!
|
51
|
+
|
52
|
+
File.open("other-manifest.yml", "w") do |io|
|
53
|
+
io.write <<OTHER
|
54
|
+
---
|
55
|
+
foo:
|
56
|
+
bar: a
|
57
|
+
baz: b
|
58
|
+
OTHER
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it "merges itself into the parent, by depth" do
|
63
|
+
manifest = subject.build(file)
|
64
|
+
expect(manifest).to include(
|
65
|
+
"foo" => { "bar" => "a", "baz" => "c" })
|
66
|
+
end
|
67
|
+
|
68
|
+
it "does not include the 'inherit' attribute" do
|
69
|
+
manifest = subject.build(file)
|
70
|
+
expect(manifest).to_not include("inherit")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "with an invalid manifest" do
|
75
|
+
let(:manifest) { "" }
|
76
|
+
|
77
|
+
it "raises an error" do
|
78
|
+
expect {
|
79
|
+
subject.build(file)
|
80
|
+
}.to raise_error(CFManifests::InvalidManifest)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "manifests-cf-plugin/loader"
|
4
|
+
|
5
|
+
|
6
|
+
describe CFManifests::Normalizer do
|
7
|
+
let(:manifest) { {} }
|
8
|
+
let(:loader) { CFManifests::Loader.new(nil, nil) }
|
9
|
+
|
10
|
+
describe '#normalize!' do
|
11
|
+
subject do
|
12
|
+
loader.normalize!(manifest)
|
13
|
+
manifest
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'with a manifest where the applications have no path set' do
|
17
|
+
let(:manifest) { { "applications" => { "." => { "name" => "foo" } } } }
|
18
|
+
|
19
|
+
it "sets the path to their tag, assuming it's a path" do
|
20
|
+
expect(subject).to eq(
|
21
|
+
:applications => [{ :name => "foo", :path => "." }])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with a manifest where the url is nil' do
|
26
|
+
let(:manifest) { { "applications" => { "." => { "url" => nil } } } }
|
27
|
+
|
28
|
+
it "sets it to none" do
|
29
|
+
expect(subject).to eq(
|
30
|
+
:applications => [{ :path => ".", :url => "none" }]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with a manifest with a subdomain attribute' do
|
36
|
+
let(:manifest) { { "applications" => { "." => { "subdomain" => "use-this-for-host" } } } }
|
37
|
+
|
38
|
+
it "sets the subdomain key to be host" do
|
39
|
+
expect(subject).to eq(
|
40
|
+
:applications => [{ :path => ".", :host => "use-this-for-host" }]
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when the host attribute is also set" do
|
45
|
+
let(:manifest) { { "applications" => { "." => { "subdomain" => "dont-use-this-for-host", "host" => "canonical-attribute" } } } }
|
46
|
+
|
47
|
+
it 'does not overwrite an explicit host attribute' do
|
48
|
+
expect(subject).to eq(
|
49
|
+
:applications => [{ :path => ".", :host => "canonical-attribute" }]
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with a manifest with toplevel attributes' do
|
56
|
+
context 'and properties' do
|
57
|
+
let(:manifest) {
|
58
|
+
{ "name" => "foo", "properties" => { "fizz" => "buzz" } }
|
59
|
+
}
|
60
|
+
|
61
|
+
it 'keeps the properties at the toplevel' do
|
62
|
+
expect(subject).to eq(
|
63
|
+
:applications => [{ :name => "foo", :path => "." }],
|
64
|
+
:properties => { :fizz => "buzz" })
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'and no applications' do
|
69
|
+
context 'and no path' do
|
70
|
+
let(:manifest) { { "name" => "foo" } }
|
71
|
+
|
72
|
+
it 'adds it as an application with path .' do
|
73
|
+
expect(subject).to eq(
|
74
|
+
:applications => [{ :name => "foo", :path => "." }])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'and a path' do
|
79
|
+
let(:manifest) { { "name" => "foo", "path" => "./foo" } }
|
80
|
+
|
81
|
+
it 'adds it as an application with the proper tag and path' do
|
82
|
+
expect(subject).to eq(
|
83
|
+
:applications => [{ :name => "foo", :path => "./foo" }])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'and applications' do
|
89
|
+
let(:manifest) {
|
90
|
+
{ "runtime" => "ruby19",
|
91
|
+
"applications" => {
|
92
|
+
"./foo" => { "name" => "foo" },
|
93
|
+
"./bar" => { "name" => "bar" },
|
94
|
+
"./baz" => { "name" => "baz", "runtime" => "ruby18" }
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
it "merges the toplevel attributes into the applications" do
|
100
|
+
expect(subject[:applications]).to match_array [
|
101
|
+
{ :name => "foo", :path => "./foo", :runtime => "ruby19" },
|
102
|
+
{ :name => "bar", :path => "./bar", :runtime => "ruby19" },
|
103
|
+
{ :name => "baz", :path => "./baz", :runtime => "ruby18" }
|
104
|
+
]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'with a manifest where applications is a hash' do
|
110
|
+
let(:manifest) { { "applications" => { "foo" => { "name" => "foo" } } } }
|
111
|
+
|
112
|
+
it 'converts the array to a hash, with the path as the key' do
|
113
|
+
expect(subject).to eq(
|
114
|
+
:applications => [{ :name => "foo", :path => "foo" }])
|
115
|
+
end
|
116
|
+
|
117
|
+
context "and the applications had dependencies" do
|
118
|
+
let(:manifest) do
|
119
|
+
{ "applications" => {
|
120
|
+
"bar" => { "name" => "bar", "depends-on" => "foo" },
|
121
|
+
"foo" => { "name" => "foo" }
|
122
|
+
}
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
it "converts using dependency order" do
|
127
|
+
expect(subject).to eq(
|
128
|
+
:applications => [{ :name => "foo", :path => "foo" }, { :name => "bar", :path => "bar" }])
|
129
|
+
end
|
130
|
+
|
131
|
+
context "and there's a circular dependency" do
|
132
|
+
let(:manifest) do
|
133
|
+
{ "applications" => {
|
134
|
+
"bar" => { "name" => "bar", "depends-on" => "foo" },
|
135
|
+
"foo" => { "name" => "foo", "depends-on" => "bar" }
|
136
|
+
}
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
it "doesn't blow up" do
|
141
|
+
expect(subject).to be_true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#normalize_app!' do
|
149
|
+
subject do
|
150
|
+
loader.send(:normalize_app!, manifest)
|
151
|
+
manifest
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'with framework as a hash' do
|
155
|
+
let(:manifest) {
|
156
|
+
{ "name" => "foo",
|
157
|
+
"framework" => { "name" => "ruby19", "mem" => "64M" }
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
it 'sets the framework to just the name' do
|
162
|
+
expect(subject).to eq(
|
163
|
+
"name" => "foo",
|
164
|
+
"framework" => "ruby19")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'with mem instead of memory' do
|
169
|
+
let(:manifest) { { "name" => "foo", "mem" => "128M" } }
|
170
|
+
|
171
|
+
it 'renames mem to memory' do
|
172
|
+
expect(subject).to eq("name" => "foo", "memory" => "128M")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|