cloulu 0.2.6 → 0.3.0

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.
@@ -0,0 +1,317 @@
1
+ require "yaml"
2
+ require "set"
3
+
4
+ require "manifests-vmc-plugin/loader"
5
+
6
+
7
+ module VMCManifests
8
+ MANIFEST_FILE = "manifest.yml"
9
+
10
+ @@showed_manifest_usage = false
11
+
12
+ def manifest
13
+ return @manifest if @manifest
14
+
15
+ if manifest_file && File.exists?(manifest_file)
16
+ @manifest = load_manifest(manifest_file)
17
+ end
18
+ end
19
+
20
+ def save_manifest(save_to = manifest_file)
21
+ fail "No manifest to save!" unless @manifest
22
+
23
+ File.open(save_to, "w") do |io|
24
+ YAML.dump(@manifest, io)
25
+ end
26
+ end
27
+
28
+ # find the manifest file to work with
29
+ def manifest_file
30
+ return @manifest_file if @manifest_file
31
+
32
+ unless path = input[:manifest]
33
+ where = Dir.pwd
34
+ while true
35
+ if File.exists?(File.join(where, MANIFEST_FILE))
36
+ path = File.join(where, MANIFEST_FILE)
37
+ break
38
+ elsif File.basename(where) == "/"
39
+ path = nil
40
+ break
41
+ else
42
+ where = File.expand_path("../", where)
43
+ end
44
+ end
45
+ end
46
+
47
+ return unless path
48
+
49
+ @manifest_file = File.expand_path(path)
50
+ end
51
+
52
+ # load and resolve a given manifest file
53
+ def load_manifest(file)
54
+ Loader.new(file, self).manifest
55
+ end
56
+
57
+ # dynamic symbol resolution
58
+ def resolve_symbol(sym)
59
+ case sym
60
+ when "target-url"
61
+ client_target
62
+
63
+ when "target-base"
64
+ target_base
65
+
66
+ when "random-word"
67
+ sprintf("%04x", rand(0x0100000))
68
+
69
+ when /^ask (.+)/
70
+ ask($1)
71
+ end
72
+ end
73
+
74
+ # find apps by an identifier, which may be either a tag, a name, or a path
75
+ def find_apps(identifier)
76
+ return [] unless manifest
77
+
78
+ apps = apps_by(:name, identifier)
79
+
80
+ if apps.empty?
81
+ apps = apps_by(:path, from_manifest(identifier))
82
+ end
83
+
84
+ apps
85
+ end
86
+
87
+ # return all the apps described by the manifest
88
+ def all_apps
89
+ manifest[:applications]
90
+ end
91
+
92
+ def current_apps
93
+ manifest[:applications].select do |app|
94
+ next unless app[:path]
95
+ from_manifest(app[:path]) == Dir.pwd
96
+ end
97
+ end
98
+
99
+ # splits the user's input, resolving paths with the manifest,
100
+ # into internal/external apps
101
+ #
102
+ # internal apps are returned as their data in the manifest
103
+ #
104
+ # external apps are the strings that the user gave, to be
105
+ # passed along wholesale to the wrapped command
106
+ def apps_in_manifest(input = nil, use_name = true, &blk)
107
+ names_or_paths =
108
+ if input.has?(:apps)
109
+ # names may be given but be [], which will still cause
110
+ # interaction, so use #direct instead of #[] here
111
+ input.direct(:apps)
112
+ elsif input.has?(:app)
113
+ [input.direct(:app)]
114
+ elsif input.has?(:name)
115
+ [input.direct(:name)]
116
+ else
117
+ []
118
+ end
119
+
120
+ internal = []
121
+ external = []
122
+
123
+ names_or_paths.each do |x|
124
+ if x.is_a?(String)
125
+ if x =~ %r([/\\])
126
+ apps = find_apps(File.expand_path(x))
127
+
128
+ if apps.empty?
129
+ fail("Path #{b(x)} is not present in manifest #{b(relative_manifest_file)}.")
130
+ end
131
+ else
132
+ apps = find_apps(x)
133
+ end
134
+
135
+ if !apps.empty?
136
+ internal += apps
137
+ else
138
+ external << x
139
+ end
140
+ else
141
+ external << x
142
+ end
143
+ end
144
+
145
+ [internal, external]
146
+ end
147
+
148
+ def create_manifest_for(app, path)
149
+ meta = {
150
+ "name" => app.name,
151
+ "framework" => app.framework.name,
152
+ "runtime" => app.runtime.name,
153
+ "memory" => human_size(app.memory * 1024 * 1024, 0),
154
+ "instances" => app.total_instances,
155
+ "url" => app.url ? app.url.sub(target_base, '${target-base}') : "none",
156
+ "path" => path
157
+ }
158
+
159
+ services = app.services
160
+
161
+ unless services.empty?
162
+ meta["services"] = {}
163
+
164
+ services.each do |i|
165
+ if v2?
166
+ p = i.service_plan
167
+ s = p.service
168
+
169
+ meta["services"][i.name] = {
170
+ "label" => s.label,
171
+ "provider" => s.provider,
172
+ "version" => s.version,
173
+ "plan" => p.name
174
+ }
175
+ else
176
+ meta["services"][i.name] = {
177
+ "vendor" => i.vendor,
178
+ "version" => i.version,
179
+ "tier" => i.tier
180
+ }
181
+ end
182
+ end
183
+ end
184
+
185
+ if cmd = app.command
186
+ meta["command"] = cmd
187
+ end
188
+
189
+ if buildpack = app.buildpack
190
+ meta["buildpack"] = buildpack
191
+ end
192
+
193
+ meta
194
+ end
195
+
196
+ private
197
+
198
+ def relative_manifest_file
199
+ Pathname.new(manifest_file).relative_path_from(Pathname.pwd)
200
+ end
201
+
202
+ def show_manifest_usage
203
+ return if @@showed_manifest_usage
204
+
205
+ path = relative_manifest_file
206
+ line "Using manifest file #{c(path, :name)}"
207
+ line
208
+
209
+ @@showed_manifest_usage = true
210
+ end
211
+
212
+ def no_apps
213
+ fail "No applications or manifest to operate on."
214
+ end
215
+
216
+ def warn_reset_changes
217
+ line c("Not applying manifest changes without --reset", :warning)
218
+ line "See `vmc diff` for more details."
219
+ line
220
+ end
221
+
222
+ def apps_by(attr, val)
223
+ manifest[:applications].select do |info|
224
+ info[attr] == val
225
+ end
226
+ end
227
+
228
+ # expand a path relative to the manifest file's directory
229
+ def from_manifest(path)
230
+ File.expand_path(path, File.dirname(manifest_file))
231
+ end
232
+
233
+
234
+ def ask_to_save(input, app)
235
+ return if manifest_file
236
+ return unless ask("Save configuration?", :default => false)
237
+
238
+ manifest = create_manifest_for(app, input[:path])
239
+
240
+ with_progress("Saving to #{c("manifest.yml", :name)}") do
241
+ File.open("manifest.yml", "w") do |io|
242
+ YAML.dump(
243
+ { "applications" => [manifest] },
244
+ io)
245
+ end
246
+ end
247
+ end
248
+
249
+ def env_hash(val)
250
+ if val.is_a?(Hash)
251
+ val
252
+ else
253
+ hash = {}
254
+
255
+ val.each do |pair|
256
+ name, val = pair.split("=", 2)
257
+ hash[name] = val
258
+ end
259
+
260
+ hash
261
+ end
262
+ end
263
+
264
+ def setup_env(app, info)
265
+ return unless info[:env]
266
+ app.env = env_hash(info[:env])
267
+ end
268
+
269
+ def setup_services(app, info)
270
+ return if !info[:services] || info[:services].empty?
271
+
272
+ offerings = client.services
273
+
274
+ to_bind = []
275
+
276
+ info[:services].each do |name, svc|
277
+ name = name.to_s
278
+
279
+ if instance = client.service_instance_by_name(name)
280
+ to_bind << instance
281
+ else
282
+ offering = offerings.find { |o|
283
+ o.label == (svc[:label] || svc[:type] || svc[:vendor]) &&
284
+ (!svc[:version] || o.version == svc[:version]) &&
285
+ (o.provider == (svc[:provider] || "core"))
286
+ }
287
+
288
+ fail "Unknown service offering: #{svc.inspect}." unless offering
289
+
290
+ if v2?
291
+ plan = offering.service_plans.find { |p|
292
+ p.name == (svc[:plan] || "D100")
293
+ }
294
+
295
+ fail "Unknown service plan: #{svc[:plan]}." unless plan
296
+ end
297
+
298
+ invoke :create_service,
299
+ :name => name,
300
+ :offering => offering,
301
+ :plan => plan,
302
+ :app => app
303
+ end
304
+ end
305
+
306
+ to_bind.each do |s|
307
+ next if app.binds?(s)
308
+
309
+ # TODO: splat
310
+ invoke :bind_service, :app => app, :service => s
311
+ end
312
+ end
313
+
314
+ def target_base
315
+ client_target.sub(/^[^\.]+\./, "")
316
+ end
317
+ end
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+
3
+ require "manifests-vmc-plugin/errors"
4
+
5
+
6
+ describe VMCManifests::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-vmc-plugin/loader"
4
+ require "manifests-vmc-plugin/errors"
5
+
6
+
7
+ describe VMCManifests::Builder do
8
+ subject { VMCManifests::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(VMCManifests::InvalidManifest)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,176 @@
1
+ require "spec_helper"
2
+
3
+ require "manifests-vmc-plugin/loader"
4
+
5
+
6
+ describe VMCManifests::Normalizer do
7
+ let(:manifest) { {} }
8
+ let(:loader) { VMCManifests::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