apcera-stager-api 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +15 -0
  4. data/CHANGELOG.md +17 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +21 -0
  7. data/README.md +86 -0
  8. data/Rakefile +8 -0
  9. data/apcera-stager-api-contrib.gemspec +25 -0
  10. data/lib/apcera-stager-api.rb +1 -0
  11. data/lib/apcera/stager/error.rb +8 -0
  12. data/lib/apcera/stager/loader.rb +11 -0
  13. data/lib/apcera/stager/stager.rb +335 -0
  14. data/spec/apcera/stager/stager_spec.rb +571 -0
  15. data/spec/fixtures/cassettes/complete.yml +55 -0
  16. data/spec/fixtures/cassettes/dependencies_add.yml +101 -0
  17. data/spec/fixtures/cassettes/dependencies_remove.yml +101 -0
  18. data/spec/fixtures/cassettes/done.yml +28 -0
  19. data/spec/fixtures/cassettes/download.yml +37400 -0
  20. data/spec/fixtures/cassettes/environment_add.yml +53 -0
  21. data/spec/fixtures/cassettes/environment_remove.yml +53 -0
  22. data/spec/fixtures/cassettes/fail.yml +28 -0
  23. data/spec/fixtures/cassettes/invalid/complete.yml +55 -0
  24. data/spec/fixtures/cassettes/invalid/dependencies_add.yml +53 -0
  25. data/spec/fixtures/cassettes/invalid/dependencies_remove.yml +53 -0
  26. data/spec/fixtures/cassettes/invalid/done.yml +55 -0
  27. data/spec/fixtures/cassettes/invalid/download.yml +54 -0
  28. data/spec/fixtures/cassettes/invalid/environment_add.yml +53 -0
  29. data/spec/fixtures/cassettes/invalid/environment_remove.yml +53 -0
  30. data/spec/fixtures/cassettes/invalid/fail.yml +28 -0
  31. data/spec/fixtures/cassettes/invalid/metadata.yml +28 -0
  32. data/spec/fixtures/cassettes/invalid/provides_add.yml +53 -0
  33. data/spec/fixtures/cassettes/invalid/provides_remove.yml +53 -0
  34. data/spec/fixtures/cassettes/invalid/relaunch.yml +53 -0
  35. data/spec/fixtures/cassettes/invalid/snapshot.yml +53 -0
  36. data/spec/fixtures/cassettes/invalid/templates_add.yml +53 -0
  37. data/spec/fixtures/cassettes/invalid/templates_remove.yml +53 -0
  38. data/spec/fixtures/cassettes/invalid/upload.yml +55 -0
  39. data/spec/fixtures/cassettes/metadata.yml +51 -0
  40. data/spec/fixtures/cassettes/provides_add.yml +53 -0
  41. data/spec/fixtures/cassettes/provides_remove.yml +53 -0
  42. data/spec/fixtures/cassettes/relaunch.yml +28 -0
  43. data/spec/fixtures/cassettes/snapshot.yml +28 -0
  44. data/spec/fixtures/cassettes/templates_add.yml +53 -0
  45. data/spec/fixtures/cassettes/templates_remove.yml +53 -0
  46. data/spec/fixtures/cassettes/upload.yml +55 -0
  47. data/spec/spec_helper.rb +31 -0
  48. data/spec/tmp/.gitkeep +0 -0
  49. metadata +190 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2bfd1296f1530b54b9151231b57d1d8abc47cac8
4
+ data.tar.gz: 949490f21990c5015e341479cd3e7b9b057e2eba
5
+ SHA512:
6
+ metadata.gz: c1118b6f5b0ddf98df6358ee79f156b1fed19e0b1eaa0292e20c522f83cbfd700175fafc70ffec29457d2b662b179ddd9021096a0a4853a995798a61126138a9
7
+ data.tar.gz: f9cf88bf8cb869e537fee9e40408cf9b9bac59437480b418b180ea82f9a3ffbfde75f45d3d620d1f024be25494968c06d59703eca72dd3a13a8a3008834e9b4b
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .ruby-gemset
7
+ .ruby-version
8
+ .rvmrc
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp/*
21
+ .powenv
22
+ .idea/
@@ -0,0 +1,15 @@
1
+ before_install:
2
+ - gem update --system 2.1.11
3
+ language: ruby
4
+ rvm:
5
+ - "1.8.7"
6
+ - "1.9.2"
7
+ - "1.9.3"
8
+ - "2.0.0"
9
+ - "2.1.0"
10
+ - "rbx"
11
+ - "jruby"
12
+ matrix:
13
+ allow_failures:
14
+ - rvm: "rbx"
15
+ - rvm: "jruby"
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+ Documentation started at the 0.2.5 release.
4
+
5
+ ## 0.2.5 apcera-stager-api
6
+
7
+ ### Added
8
+ - Nothing.
9
+
10
+ ### Deprecated
11
+ - Nothing.
12
+
13
+ ### Removed
14
+ - Nothing.
15
+
16
+ ### Fixed
17
+ - Has been renamed to apcera-stager-api
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014-2015 Apcera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,86 @@
1
+ # apcera-stager-api
2
+
3
+ Simple gem to assist users writing stagers for the Apcera Platform.
4
+
5
+ ## Usage
6
+
7
+ First create your Gemfile. The contents should be:
8
+
9
+ ```ruby
10
+ source "https://rubygems.org"
11
+
12
+ gem "apcera-stager-api"
13
+ ```
14
+
15
+ Then run `bundle install` to download and install the gem and its dependencies.
16
+
17
+ Now that your environment is setup, create a new file for your stager. We recommend "stager.rb".
18
+
19
+ Start off with the contents:
20
+
21
+ ```ruby
22
+ #!/usr/bin/env ruby
23
+
24
+ require "bundler"
25
+ Bundler.setup
26
+
27
+ require "apcera-stager-api"
28
+
29
+ stager = Apcera::Stager.new
30
+
31
+ # Download the package from the staging coordinator.
32
+ # This is the code that requires staging!
33
+ puts "Downloading Package..."
34
+ stager.download
35
+
36
+ # Extract the package to the "app" directory.
37
+ puts "Extracting Package..."
38
+ stager.extract("app")
39
+
40
+ # Your custom staging logic goes here.
41
+ # This will be a set of commands to execute in order to stage
42
+ # your applications.
43
+
44
+ # Finish staging, this will upload your final package to the
45
+ # staging coordinator.
46
+ puts "Completed Staging..."
47
+ stager.complete
48
+ ```
49
+
50
+ ## Deploying
51
+
52
+ Deploying just requires you to run `apc stager create`.
53
+
54
+ ```console
55
+ apc stager create mystager --start-command="./stager.rb" --staging=/apcera::ruby --pipeline
56
+ ```
57
+
58
+ Now you can deploy apps using your stager with `apc app create`.
59
+
60
+ ```console
61
+ apc app create someapp --staging=mystager --start
62
+ ```
63
+
64
+ ## License
65
+
66
+ The MIT License (MIT)
67
+
68
+ Copyright (c) 2014-2015 Apcera
69
+
70
+ Permission is hereby granted, free of charge, to any person obtaining a copy
71
+ of this software and associated documentation files (the "Software"), to deal
72
+ in the Software without restriction, including without limitation the rights
73
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
74
+ copies of the Software, and to permit persons to whom the Software is
75
+ furnished to do so, subject to the following conditions:
76
+
77
+ The above copyright notice and this permission notice shall be included in all
78
+ copies or substantial portions of the Software.
79
+
80
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
81
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
82
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
83
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
84
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
85
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
86
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require File.join('bundler', 'gem_tasks')
4
+ require File.join('rspec', 'core', 'rake_task')
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.add_dependency "rest-client"
4
+ gem.add_dependency 'json'
5
+
6
+ gem.authors = ["Josh Ellithorpe"]
7
+ gem.email = ["josh@apcera.com"]
8
+ gem.description = %q{Apcera Stager API Library}
9
+ gem.summary = %q{Apcera Stager API library which makes it super easy to write stagers for the Apcera Platform.}
10
+ gem.homepage = "http://apcera.com"
11
+ gem.license = "MIT"
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
16
+ gem.name = "apcera-stager-api"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = "0.2.5"
19
+
20
+ gem.add_development_dependency 'rspec', '~> 2.6.0'
21
+ gem.add_development_dependency 'rake'
22
+ gem.add_development_dependency 'webmock', '1.11'
23
+ gem.add_development_dependency 'simplecov'
24
+ gem.add_development_dependency 'vcr'
25
+ end
@@ -0,0 +1 @@
1
+ require File.join('apcera', 'stager', 'loader.rb')
@@ -0,0 +1,8 @@
1
+ module Apcera
2
+ module Error
3
+ class DownloadError < StandardError; end
4
+ class StagerURLRequired < StandardError; end
5
+ class ExecuteError < StandardError; end
6
+ class AppPathError < StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require 'rest-client'
5
+ require 'json'
6
+
7
+ base = File.expand_path File.dirname(__FILE__)
8
+
9
+ Dir[File.join(base, '*.rb')].each do |file|
10
+ require file
11
+ end
@@ -0,0 +1,335 @@
1
+ module Apcera
2
+ class Stager
3
+ attr_accessor :stager_url, :app_path, :root_path, :pkg_path, :updated_pkg_path, :system_options
4
+
5
+ PKG_NAME = "pkg.tar.gz"
6
+ UPDATED_PKG_NAME = "updated.tar.gz"
7
+
8
+ def initialize(options = {})
9
+ # Require stager url. Needed to talk to the Staging Coordinator.
10
+ @stager_url = options[:stager_url] || ENV["STAGER_URL"]
11
+ raise Apcera::Error::StagerURLRequired.new("stager_url required") unless @stager_url
12
+
13
+ # Setup the environment, some test items here.
14
+ setup_environment
15
+ end
16
+
17
+ # Setup /stagerfs chroot environment so it is ready to run commands
18
+ # from pulled in dependencies. This does the following:
19
+ # - Setup working resolv.conf
20
+ # - Bind mounts /proc to /stagerfs/proc
21
+ # - Recursively bind mounts /dev to /stagerfs/dev
22
+ def setup_chroot
23
+ execute("sudo mkdir -p /stagerfs/etc")
24
+ execute("sudo cp /etc/resolv.conf /stagerfs/etc/resolv.conf")
25
+
26
+ execute("sudo mkdir -p /stagerfs/proc")
27
+ execute("sudo mount --bind /proc /stagerfs/proc")
28
+
29
+ execute("sudo mkdir -p /stagerfs/dev")
30
+ execute("sudo mount --rbind /dev /stagerfs/dev")
31
+ end
32
+
33
+ # Download a package from the staging coordinator.
34
+ # We use Net::HTTP here because it supports streaming downloads.
35
+ def download
36
+ uri = URI(@stager_url + "/data")
37
+
38
+ Net::HTTP.start(uri.host.to_s, uri.port.to_s) do |http|
39
+ request = Net::HTTP::Get.new uri.request_uri
40
+
41
+ http.request request do |response|
42
+ if response.code.to_i == 200
43
+ open @pkg_path, 'wb' do |io|
44
+ response.read_body do |chunk|
45
+ io.write chunk
46
+ end
47
+ end
48
+ else
49
+ raise Apcera::Error::DownloadError.new("package download failed.\n")
50
+ end
51
+ end
52
+ end
53
+ rescue => e
54
+ fail e
55
+ end
56
+
57
+ # Execute a command in the shell.
58
+ # We don't want real commands in tests.
59
+ def execute(cmd)
60
+ Bundler.with_clean_env do
61
+ result = system(cmd, @system_options)
62
+ if !result
63
+ raise Apcera::Error::ExecuteError.new("failed to execute: #{cmd}.\n")
64
+ end
65
+
66
+ result
67
+ end
68
+ rescue => e
69
+ fail e
70
+ end
71
+
72
+ # Execute a command in the app dir. Useful helper.
73
+ def execute_app(cmd)
74
+ raise_app_path_error if @app_path == nil
75
+ Bundler.with_clean_env do
76
+ Dir.chdir(@app_path) do |app_path|
77
+ result = system(cmd, @system_options)
78
+ if !result
79
+ raise Apcera::Error::ExecuteError.new("failed to execute: #{cmd}.\n")
80
+ end
81
+
82
+ result
83
+ end
84
+ end
85
+ rescue => e
86
+ fail e
87
+ end
88
+
89
+ # Extract the package to a given location.
90
+ def extract(location)
91
+ @app_path = File.join(@root_path, location)
92
+ Dir.mkdir(@app_path) unless Dir.exists?(@app_path)
93
+
94
+ execute_app("tar -zxf #{@pkg_path}")
95
+ rescue => e
96
+ fail e
97
+ end
98
+
99
+ # Upload the new package to the staging coordinator. If we have an app extracted we
100
+ # send that to the staging coordinator. If no app was ever extracted we just
101
+ # upload the unmodified app.
102
+ def upload
103
+ if @app_path == nil
104
+ unless File.exist?(@pkg_path)
105
+ download
106
+ end
107
+
108
+ upload_file(@pkg_path)
109
+ else
110
+ app_dir = Pathname.new(@app_path).relative_path_from(Pathname.new(@root_path)).to_s
111
+ execute_app("cd #{app_path}/.. && tar czf #{@updated_pkg_path} #{app_dir}")
112
+
113
+ upload_file(@updated_pkg_path)
114
+ end
115
+ rescue => e
116
+ fail e
117
+ end
118
+
119
+ # Snapshot the stager filesystem for app
120
+ def snapshot
121
+ response = RestClient.post(@stager_url+"/snapshot", {})
122
+ rescue => e
123
+ fail e
124
+ end
125
+
126
+ # Add environment variable to package.
127
+ def environment_add(key, value)
128
+ response = RestClient.put(stager_meta_url, {
129
+ :resource => "environment",
130
+ :action => "add",
131
+ :key => key,
132
+ :value => value
133
+ })
134
+ rescue => e
135
+ fail e
136
+ end
137
+
138
+ # Delete environment variable from package.
139
+ def environment_remove(key)
140
+ response = RestClient.put(stager_meta_url, {
141
+ :resource => "environment",
142
+ :action => "remove",
143
+ :key => key
144
+ })
145
+ rescue => e
146
+ fail e
147
+ end
148
+
149
+ # Add provides to package.
150
+ def provides_add(type, name)
151
+ response = RestClient.put(stager_meta_url, {
152
+ :resource => "provides",
153
+ :action => "add",
154
+ :type => type,
155
+ :name => name
156
+ })
157
+ rescue => e
158
+ fail e
159
+ end
160
+
161
+ # Delete provides from package.
162
+ def provides_remove(type, name)
163
+ response = RestClient.put(stager_meta_url, {
164
+ :resource => "provides",
165
+ :action => "remove",
166
+ :type => type,
167
+ :name => name
168
+ })
169
+ rescue => e
170
+ fail e
171
+ end
172
+
173
+ # Add dependencies to package.
174
+ def dependencies_add(type, name)
175
+ exists = self.meta["dependencies"].detect { |dep| dep["type"] == type && dep["name"] == name }
176
+ return false if exists
177
+
178
+ response = RestClient.put(stager_meta_url, {
179
+ :resource => "dependencies",
180
+ :action => "add",
181
+ :type => type,
182
+ :name => name
183
+ })
184
+
185
+ true
186
+ rescue => e
187
+ fail e
188
+ end
189
+
190
+ # Delete dependencies from package.
191
+ def dependencies_remove(type, name)
192
+ exists = self.meta["dependencies"].detect { |dep| dep["type"] == type && dep["name"] == name}
193
+ return false if !exists
194
+
195
+ response = RestClient.put(stager_meta_url, {
196
+ :resource => "dependencies",
197
+ :action => "remove",
198
+ :type => type,
199
+ :name => name
200
+ })
201
+
202
+ true
203
+ rescue => e
204
+ fail e
205
+ end
206
+
207
+ # Add template to package.
208
+ def templates_add(path, left_delimiter = "{{", right_delimiter = "}}")
209
+ response = RestClient.put(stager_meta_url, {
210
+ :resource => "templates",
211
+ :action => "add",
212
+ :path => path,
213
+ :left_delimiter => left_delimiter,
214
+ :right_delimiter => right_delimiter
215
+ })
216
+ rescue => e
217
+ fail e
218
+ end
219
+
220
+ # Delete template from package.
221
+ def templates_remove(path, left_delimiter = "{{", right_delimiter = "}}")
222
+ response = RestClient.put(stager_meta_url, {
223
+ :resource => "templates",
224
+ :action => "remove",
225
+ :path => path,
226
+ :left_delimiter => left_delimiter,
227
+ :right_delimiter => right_delimiter
228
+ })
229
+ rescue => e
230
+ fail e
231
+ end
232
+
233
+ # Get metadata for the package being staged.
234
+ def meta
235
+ response = RestClient.get(stager_meta_url)
236
+ return JSON.parse(response.to_s)
237
+ rescue => e
238
+ output_error "Error: #{e.message}.\n"
239
+ raise e
240
+ end
241
+
242
+ # Tell the staging coordinator you are done.
243
+ def done
244
+ response = RestClient.post(@stager_url+"/done", {})
245
+ exit0r 0
246
+ rescue => e
247
+ fail e
248
+ end
249
+
250
+ # Tell the staging coordinator you need to relaunch.
251
+ def relaunch
252
+ response = RestClient.post(@stager_url+"/relaunch", {})
253
+ exit0r 0
254
+ rescue => e
255
+ fail e
256
+ end
257
+
258
+ # Finish staging, compress your app dir and send to the staging coordinator.
259
+ # Then tell the staging coordinator we are done.
260
+ def complete
261
+ upload
262
+ done
263
+ end
264
+
265
+ # Returns the start command for the package.
266
+ def start_command
267
+ self.meta["environment"]["START_COMMAND"]
268
+ end
269
+
270
+ # Easily set the start command
271
+ def start_command=(val)
272
+ self.environment_add("START_COMMAND", val)
273
+ end
274
+
275
+ # Returns the start path for the package.
276
+ def start_path
277
+ self.meta["environment"]["START_PATH"]
278
+ end
279
+
280
+ # Easily set the start path
281
+ def start_path=(val)
282
+ self.environment_add("START_PATH", val)
283
+ end
284
+
285
+ # Fail the stager, something went wrong.
286
+ def fail(error = nil)
287
+ output_error "Error: #{error.message}.\n" if error
288
+ RestClient.post(@stager_url+"/failed", {})
289
+ rescue => e
290
+ output_error "Error: #{e.message}.\n"
291
+ ensure
292
+ exit0r 1
293
+ end
294
+
295
+ # Exit, needed for tests to not quit.
296
+ def exit0r(code)
297
+ exit code
298
+ end
299
+
300
+ # Output to stderr
301
+ def output_error(text)
302
+ $stderr.puts text
303
+ end
304
+
305
+ # Output to stdout
306
+ def output(text)
307
+ $stdout.puts text
308
+ end
309
+
310
+ private
311
+
312
+ def raise_app_path_error
313
+ raise Apcera::Error::AppPathError.new("app path not set, please run extract!\n")
314
+ end
315
+
316
+ def setup_environment
317
+ # When staging we use the root path. These are overridden in tests.
318
+ @root_path = "/tmp"
319
+ @pkg_path = File.join(@root_path, PKG_NAME)
320
+ @updated_pkg_path = File.join(@root_path, UPDATED_PKG_NAME)
321
+ @system_options = {}
322
+ end
323
+
324
+ def stager_meta_url
325
+ @stager_url + "/meta"
326
+ end
327
+
328
+ def upload_file(file)
329
+ sha256 = Digest::SHA256.file(file)
330
+ File.open(file, "rb") do |f|
331
+ response = RestClient.post(@stager_url+"/data?sha256=#{sha256.to_s}", f, { :content_type => "application/octet-stream" } )
332
+ end
333
+ end
334
+ end
335
+ end