kensa 0.4.2 → 1.0.0.beta1
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 +6 -4
- data/bin/kensa +17 -95
- data/kensa.gemspec +28 -18
- data/lib/heroku/kensa.rb +4 -589
- data/lib/heroku/kensa/check.rb +453 -0
- data/lib/heroku/kensa/client.rb +136 -0
- data/lib/heroku/kensa/http.rb +53 -0
- data/lib/heroku/kensa/manifest.rb +52 -0
- data/lib/heroku/kensa/sso.rb +55 -0
- data/test/all_check_test.rb +26 -0
- data/test/deprovision_check.rb +3 -3
- data/test/helper.rb +1 -0
- data/test/manifest_check_test.rb +3 -33
- data/test/manifest_test.rb +24 -0
- data/test/provision_check_test.rb +3 -3
- data/test/provision_response_check_test.rb +3 -3
- data/test/resources/runner.rb +1 -0
- data/test/resources/{test_server.rb → server.rb} +47 -3
- data/test/sso_check_test.rb +24 -3
- data/test/sso_test.rb +58 -0
- metadata +56 -29
- data/TODO +0 -8
- data/a-server.rb +0 -21
- data/server.rb +0 -13
data/Rakefile
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
desc 'Run all unit tests'
|
1
2
|
task :test do
|
2
3
|
fork do
|
3
|
-
exec "ruby test/resources/
|
4
|
+
exec "ruby test/resources/server.rb > /dev/null 2>&1"
|
4
5
|
end
|
5
6
|
system "turn"
|
6
|
-
system "ps -ax | grep
|
7
|
+
system "ps -ax | grep test/resources/server.rb | grep -v grep | awk '{print $1}' | xargs kill"
|
7
8
|
end
|
8
9
|
|
9
10
|
task :default => :test
|
@@ -20,14 +21,15 @@ begin
|
|
20
21
|
|
21
22
|
gemspec.add_development_dependency(%q<turn>, [">= 0"])
|
22
23
|
gemspec.add_development_dependency(%q<contest>, [">= 0"])
|
24
|
+
gemspec.add_development_dependency(%q<timecop>, [">= 0.3.5"])
|
23
25
|
gemspec.add_dependency(%q<sinatra>, ["~> 0.9"])
|
24
|
-
gemspec.add_dependency(%q<rest-client>, ["~> 1.
|
26
|
+
gemspec.add_dependency(%q<rest-client>, ["~> 1.4.0"])
|
25
27
|
gemspec.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
26
28
|
gemspec.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
27
29
|
gemspec.add_dependency(%q<launchy>, [">= 0.3.2"])
|
28
30
|
gemspec.add_dependency(%q<mechanize>, ["~> 1.0.0"])
|
29
31
|
|
30
|
-
gemspec.version = '0.
|
32
|
+
gemspec.version = '1.0.0.beta1'
|
31
33
|
end
|
32
34
|
rescue LoadError
|
33
35
|
puts "Jeweler not available. Install it with: gem install jeweler"
|
data/bin/kensa
CHANGED
@@ -1,115 +1,32 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'optparse'
|
4
|
-
require 'term/ansicolor'
|
5
|
-
require 'launchy'
|
6
4
|
require 'heroku/kensa'
|
5
|
+
require 'heroku/kensa/client'
|
6
|
+
|
7
|
+
$stdout.sync = true
|
7
8
|
|
8
|
-
fn = "addon-manifest.json"
|
9
9
|
options = {
|
10
|
-
:
|
11
|
-
:env
|
10
|
+
:filename => 'addon-manifest.json',
|
11
|
+
:env => "test",
|
12
|
+
:async => false,
|
12
13
|
}
|
13
14
|
|
14
15
|
ARGV.options do |o|
|
15
|
-
o.on("-f file", "--file") {|filename|
|
16
|
+
o.on("-f file", "--file") {|filename| options[:filename] = filename }
|
16
17
|
o.on("--async") { options[:async] = true }
|
17
18
|
o.on("--production") { options[:env] = "production" }
|
18
19
|
o.on("--plan PLANID") { |plan| options[:plan] = plan }
|
20
|
+
o.on("--without-sso") { options[:sso] = false }
|
19
21
|
o.on("-h", "--help") { command = "help" }
|
20
22
|
o.parse!
|
21
23
|
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
$stdout.sync = true
|
26
|
-
|
27
|
-
class Screen
|
28
|
-
include Term::ANSIColor
|
29
|
-
|
30
|
-
def test(msg)
|
31
|
-
$stdout.puts
|
32
|
-
$stdout.puts
|
33
|
-
$stdout.print "Testing #{msg}"
|
34
|
-
end
|
35
|
-
|
36
|
-
def check(msg)
|
37
|
-
$stdout.puts
|
38
|
-
$stdout.print " Check #{msg}"
|
39
|
-
end
|
40
|
-
|
41
|
-
def error(msg)
|
42
|
-
$stdout.print "\n", magenta(" ! #{msg}")
|
43
|
-
end
|
44
|
-
|
45
|
-
def result(status)
|
46
|
-
msg = status ? bold("[PASS]") : red(bold("[FAIL]"))
|
47
|
-
$stdout.print " #{msg}"
|
48
|
-
end
|
49
|
-
|
50
|
-
def message(msg)
|
51
|
-
$stdout.puts msg
|
52
|
-
end
|
25
|
+
include Heroku::Kensa
|
53
26
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
$stdout.puts "done."
|
58
|
-
end
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
def resolve_manifest(fn)
|
63
|
-
if File.exists?(fn)
|
64
|
-
File.read(fn)
|
65
|
-
else
|
66
|
-
abort("fatal: #{fn} not found")
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def run(klass, fn, extras={})
|
71
|
-
screen = Screen.new
|
72
|
-
data = Yajl::Parser.parse(resolve_manifest(fn))
|
73
|
-
check = klass.new(data.merge(extras), screen)
|
74
|
-
check.call
|
75
|
-
screen.finish
|
76
|
-
end
|
77
|
-
|
78
|
-
include Heroku::Sensei
|
79
|
-
|
80
|
-
case command
|
81
|
-
when "init"
|
82
|
-
Manifest.init(fn)
|
83
|
-
Screen.new.message "Initialized new addon manifest in #{fn}"
|
84
|
-
when "test"
|
85
|
-
case check = ARGV.shift
|
86
|
-
when "manifest"
|
87
|
-
run ManifestCheck, fn
|
88
|
-
when "provision"
|
89
|
-
run ManifestCheck, fn
|
90
|
-
run ProvisionCheck, fn, options
|
91
|
-
when "deprovision"
|
92
|
-
id = ARGV.shift || abort("! no id specified; see usage")
|
93
|
-
run ManifestCheck, fn
|
94
|
-
run DeprovisionCheck, fn, options.merge(:id => id)
|
95
|
-
when "sso"
|
96
|
-
id = ARGV.shift || abort("! no id specified; see usage")
|
97
|
-
run ManifestCheck, fn
|
98
|
-
run SsoCheck, fn, options.merge(:id => id)
|
99
|
-
else
|
100
|
-
abort "! Unknown test '#{check}'; see usage"
|
101
|
-
end
|
102
|
-
when "run"
|
103
|
-
abort "! missing command to run; see usage" if ARGV.empty?
|
104
|
-
run ManifestCheck, fn
|
105
|
-
run AllCheck, fn, options.merge(:args => ARGV)
|
106
|
-
when "sso"
|
107
|
-
id = ARGV.shift || abort("! no id specified; see usage")
|
108
|
-
data = Yajl::Parser.parse(resolve_manifest(fn)).merge(:id => id)
|
109
|
-
sso = Sso.new(data.merge(options))
|
110
|
-
puts "Opening #{sso.full_url}"
|
111
|
-
Launchy.open sso.full_url
|
112
|
-
else
|
27
|
+
begin
|
28
|
+
Client.new(ARGV, options).run!
|
29
|
+
rescue Client::CommandInvalid
|
113
30
|
abort File.read(__FILE__).split('__END__').last
|
114
31
|
end
|
115
32
|
|
@@ -133,6 +50,9 @@ OPTIONS
|
|
133
50
|
--plan plan-id
|
134
51
|
Use the identified plan when doing provision calls
|
135
52
|
|
53
|
+
--without-sso
|
54
|
+
Skip single sign-on authentication when doing provision calls
|
55
|
+
|
136
56
|
COMMANDS
|
137
57
|
|
138
58
|
init Creates a skeleton manifest
|
@@ -143,6 +63,8 @@ COMMANDS
|
|
143
63
|
|
144
64
|
sso <id> Launches the browser on a Heroku session for the specified id
|
145
65
|
|
66
|
+
push Send the manifest to Heroku
|
67
|
+
|
146
68
|
TEST TYPES
|
147
69
|
|
148
70
|
provision
|
data/kensa.gemspec
CHANGED
@@ -1,39 +1,42 @@
|
|
1
1
|
# Generated by jeweler
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{kensa}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "1.0.0.beta1"
|
9
9
|
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new("
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Blake Mizerany", "Pedro Belo", "Adam Wiggins"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-07-19}
|
13
13
|
s.default_executable = %q{kensa}
|
14
14
|
s.description = %q{}
|
15
15
|
s.email = %q{pedro@heroku.com}
|
16
16
|
s.executables = ["kensa"]
|
17
|
-
s.extra_rdoc_files = [
|
18
|
-
"TODO"
|
19
|
-
]
|
20
17
|
s.files = [
|
21
18
|
".gitignore",
|
22
19
|
"Rakefile",
|
23
|
-
"TODO",
|
24
|
-
"a-server.rb",
|
25
20
|
"bin/kensa",
|
26
21
|
"kensa.gemspec",
|
27
22
|
"lib/heroku/kensa.rb",
|
28
|
-
"
|
23
|
+
"lib/heroku/kensa/check.rb",
|
24
|
+
"lib/heroku/kensa/client.rb",
|
25
|
+
"lib/heroku/kensa/http.rb",
|
26
|
+
"lib/heroku/kensa/manifest.rb",
|
27
|
+
"lib/heroku/kensa/sso.rb",
|
29
28
|
"set-env.sh",
|
29
|
+
"test/all_check_test.rb",
|
30
30
|
"test/deprovision_check.rb",
|
31
31
|
"test/helper.rb",
|
32
32
|
"test/manifest_check_test.rb",
|
33
|
+
"test/manifest_test.rb",
|
33
34
|
"test/provision_check_test.rb",
|
34
35
|
"test/provision_response_check_test.rb",
|
35
|
-
"test/resources/
|
36
|
-
"test/
|
36
|
+
"test/resources/runner.rb",
|
37
|
+
"test/resources/server.rb",
|
38
|
+
"test/sso_check_test.rb",
|
39
|
+
"test/sso_test.rb"
|
37
40
|
]
|
38
41
|
s.homepage = %q{http://heroku.com}
|
39
42
|
s.rdoc_options = ["--charset=UTF-8"]
|
@@ -41,13 +44,17 @@ Gem::Specification.new do |s|
|
|
41
44
|
s.rubygems_version = %q{1.3.6}
|
42
45
|
s.summary = %q{}
|
43
46
|
s.test_files = [
|
44
|
-
"test/
|
47
|
+
"test/all_check_test.rb",
|
48
|
+
"test/deprovision_check.rb",
|
45
49
|
"test/helper.rb",
|
46
50
|
"test/manifest_check_test.rb",
|
51
|
+
"test/manifest_test.rb",
|
47
52
|
"test/provision_check_test.rb",
|
48
53
|
"test/provision_response_check_test.rb",
|
49
|
-
"test/resources/
|
50
|
-
"test/
|
54
|
+
"test/resources/runner.rb",
|
55
|
+
"test/resources/server.rb",
|
56
|
+
"test/sso_check_test.rb",
|
57
|
+
"test/sso_test.rb"
|
51
58
|
]
|
52
59
|
|
53
60
|
if s.respond_to? :specification_version then
|
@@ -57,8 +64,9 @@ Gem::Specification.new do |s|
|
|
57
64
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
58
65
|
s.add_development_dependency(%q<turn>, [">= 0"])
|
59
66
|
s.add_development_dependency(%q<contest>, [">= 0"])
|
67
|
+
s.add_development_dependency(%q<timecop>, [">= 0.3.5"])
|
60
68
|
s.add_runtime_dependency(%q<sinatra>, ["~> 0.9"])
|
61
|
-
s.add_runtime_dependency(%q<rest-client>, ["~> 1.
|
69
|
+
s.add_runtime_dependency(%q<rest-client>, ["~> 1.4.0"])
|
62
70
|
s.add_runtime_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
63
71
|
s.add_runtime_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
64
72
|
s.add_runtime_dependency(%q<launchy>, [">= 0.3.2"])
|
@@ -66,8 +74,9 @@ Gem::Specification.new do |s|
|
|
66
74
|
else
|
67
75
|
s.add_dependency(%q<turn>, [">= 0"])
|
68
76
|
s.add_dependency(%q<contest>, [">= 0"])
|
77
|
+
s.add_dependency(%q<timecop>, [">= 0.3.5"])
|
69
78
|
s.add_dependency(%q<sinatra>, ["~> 0.9"])
|
70
|
-
s.add_dependency(%q<rest-client>, ["~> 1.
|
79
|
+
s.add_dependency(%q<rest-client>, ["~> 1.4.0"])
|
71
80
|
s.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
72
81
|
s.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
73
82
|
s.add_dependency(%q<launchy>, [">= 0.3.2"])
|
@@ -76,8 +85,9 @@ Gem::Specification.new do |s|
|
|
76
85
|
else
|
77
86
|
s.add_dependency(%q<turn>, [">= 0"])
|
78
87
|
s.add_dependency(%q<contest>, [">= 0"])
|
88
|
+
s.add_dependency(%q<timecop>, [">= 0.3.5"])
|
79
89
|
s.add_dependency(%q<sinatra>, ["~> 0.9"])
|
80
|
-
s.add_dependency(%q<rest-client>, ["~> 1.
|
90
|
+
s.add_dependency(%q<rest-client>, ["~> 1.4.0"])
|
81
91
|
s.add_dependency(%q<yajl-ruby>, ["~> 0.6"])
|
82
92
|
s.add_dependency(%q<term-ansicolor>, ["~> 1.0"])
|
83
93
|
s.add_dependency(%q<launchy>, [">= 0.3.2"])
|
data/lib/heroku/kensa.rb
CHANGED
@@ -1,589 +1,4 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require 'uri'
|
6
|
-
require 'mechanize'
|
7
|
-
|
8
|
-
module Heroku
|
9
|
-
|
10
|
-
module Sensei
|
11
|
-
|
12
|
-
module Manifest
|
13
|
-
|
14
|
-
def self.init(filename)
|
15
|
-
open(filename, 'w') {|f| f << skeleton_str }
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.skeleton
|
19
|
-
Yajl::Parser.parse(skeleton_str)
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.skeleton_str
|
23
|
-
return <<EOJSON
|
24
|
-
{
|
25
|
-
"id": "myaddon",
|
26
|
-
"name": "My Addon",
|
27
|
-
"plans": [
|
28
|
-
{
|
29
|
-
"id": "basic",
|
30
|
-
"name": "Basic",
|
31
|
-
"price": "0",
|
32
|
-
"price_unit": "month"
|
33
|
-
}
|
34
|
-
],
|
35
|
-
"api": {
|
36
|
-
"config_vars": [
|
37
|
-
"MYADDON_URL"
|
38
|
-
],
|
39
|
-
"production": "https://yourapp.com/",
|
40
|
-
"test": "http://localhost:4567/",
|
41
|
-
"username": "heroku",
|
42
|
-
"password": "#{generate_password(16)}",
|
43
|
-
"sso_salt": "#{generate_password(16)}"
|
44
|
-
}
|
45
|
-
}
|
46
|
-
EOJSON
|
47
|
-
end
|
48
|
-
|
49
|
-
PasswordChars = chars = ['a'..'z', 'A'..'Z', '0'..'9'].map { |r| r.to_a }.flatten
|
50
|
-
def self.generate_password(size=16)
|
51
|
-
Array.new(size) { PasswordChars[rand(PasswordChars.size)] }.join
|
52
|
-
end
|
53
|
-
|
54
|
-
end
|
55
|
-
|
56
|
-
|
57
|
-
class NilScreen
|
58
|
-
|
59
|
-
def test(msg)
|
60
|
-
end
|
61
|
-
|
62
|
-
def check(msg)
|
63
|
-
end
|
64
|
-
|
65
|
-
def error(msg)
|
66
|
-
end
|
67
|
-
|
68
|
-
def result(status)
|
69
|
-
end
|
70
|
-
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
class Check
|
75
|
-
attr_accessor :screen, :data
|
76
|
-
|
77
|
-
class CheckError < StandardError ; end
|
78
|
-
|
79
|
-
def initialize(data, screen=NilScreen.new)
|
80
|
-
@data = data
|
81
|
-
@screen = screen
|
82
|
-
end
|
83
|
-
|
84
|
-
def test(msg)
|
85
|
-
screen.test msg
|
86
|
-
end
|
87
|
-
|
88
|
-
def check(msg)
|
89
|
-
screen.check(msg)
|
90
|
-
if yield
|
91
|
-
screen.result(true)
|
92
|
-
else
|
93
|
-
raise CheckError
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def run(klass, data)
|
98
|
-
c = klass.new(data, screen)
|
99
|
-
instance_eval(&c)
|
100
|
-
end
|
101
|
-
|
102
|
-
def error(msg)
|
103
|
-
raise CheckError, msg
|
104
|
-
end
|
105
|
-
|
106
|
-
def call
|
107
|
-
call!
|
108
|
-
true
|
109
|
-
rescue CheckError => boom
|
110
|
-
screen.result(false)
|
111
|
-
screen.error boom.message if boom.message != boom.class.name
|
112
|
-
|
113
|
-
false
|
114
|
-
end
|
115
|
-
|
116
|
-
def to_proc
|
117
|
-
me = self
|
118
|
-
Proc.new { me.call! }
|
119
|
-
end
|
120
|
-
|
121
|
-
end
|
122
|
-
|
123
|
-
|
124
|
-
class ManifestCheck < Check
|
125
|
-
|
126
|
-
ValidPriceUnits = %w[month dyno_hour]
|
127
|
-
|
128
|
-
def call!
|
129
|
-
test "manifest id key"
|
130
|
-
check "if exists" do
|
131
|
-
data.has_key?("id")
|
132
|
-
end
|
133
|
-
check "is a string" do
|
134
|
-
data["id"].is_a?(String)
|
135
|
-
end
|
136
|
-
check "is not blank" do
|
137
|
-
!data["id"].empty?
|
138
|
-
end
|
139
|
-
|
140
|
-
test "manifest name key"
|
141
|
-
check "if exists" do
|
142
|
-
data.has_key?("name")
|
143
|
-
end
|
144
|
-
check "is a string" do
|
145
|
-
data["name"].is_a?(String)
|
146
|
-
end
|
147
|
-
check "is not blank" do
|
148
|
-
!data["name"].empty?
|
149
|
-
end
|
150
|
-
|
151
|
-
test "manifest api key"
|
152
|
-
check "if exists" do
|
153
|
-
data.has_key?("api")
|
154
|
-
end
|
155
|
-
check "is a hash" do
|
156
|
-
data["api"].is_a?(Hash)
|
157
|
-
end
|
158
|
-
check "contains username" do
|
159
|
-
data["api"].has_key?("username") && data["api"]["username"] != ""
|
160
|
-
end
|
161
|
-
check "contains password" do
|
162
|
-
data["api"].has_key?("password") && data["api"]["password"] != ""
|
163
|
-
end
|
164
|
-
check "contains test url" do
|
165
|
-
data["api"].has_key?("test")
|
166
|
-
end
|
167
|
-
check "contains production url" do
|
168
|
-
data["api"].has_key?("production")
|
169
|
-
end
|
170
|
-
check "production url uses SSL" do
|
171
|
-
data["api"]["production"] =~ /^https:/
|
172
|
-
end
|
173
|
-
check "contains config_vars array" do
|
174
|
-
data["api"].has_key?("config_vars") && data["api"]["config_vars"].is_a?(Array)
|
175
|
-
end
|
176
|
-
check "containst at least one config var" do
|
177
|
-
!data["api"]["config_vars"].empty?
|
178
|
-
end
|
179
|
-
check "all config vars are uppercase strings" do
|
180
|
-
data["api"]["config_vars"].each do |k, v|
|
181
|
-
if k =~ /^[A-Z][0-9A-Z_]+$/
|
182
|
-
true
|
183
|
-
else
|
184
|
-
error "#{k.inspect} is not a valid ENV key"
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
check "all config vars are prefixed with the addon id" do
|
189
|
-
data["api"]["config_vars"].each do |k|
|
190
|
-
if k =~ /^#{data['id'].upcase}_/
|
191
|
-
true
|
192
|
-
else
|
193
|
-
error "#{k} is not a valid ENV key - must be prefixed with #{data['id'].upcase}_"
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
test "plans"
|
199
|
-
check "key must exist" do
|
200
|
-
data.has_key?("plans")
|
201
|
-
end
|
202
|
-
check "is an array" do
|
203
|
-
data["plans"].is_a?(Array)
|
204
|
-
end
|
205
|
-
check "contains at least one plan" do
|
206
|
-
!data["plans"].empty?
|
207
|
-
end
|
208
|
-
check "all plans are a hash" do
|
209
|
-
data["plans"].all? {|plan| plan.is_a?(Hash) }
|
210
|
-
end
|
211
|
-
check "all plans must have an id" do
|
212
|
-
data["plans"].all? {|plan| plan.has_key?("id") }
|
213
|
-
end
|
214
|
-
check "all plans have an unique id" do
|
215
|
-
ids = data["plans"].map {|plan| plan["id"] }
|
216
|
-
ids.size == ids.uniq.size
|
217
|
-
end
|
218
|
-
check "all plans have a name" do
|
219
|
-
data["plans"].all? {|plan| plan.has_key?("name") }
|
220
|
-
end
|
221
|
-
check "all plans have a unique name" do
|
222
|
-
names = data["plans"].map {|plan| plan["name"] }
|
223
|
-
names.size == names.uniq.size
|
224
|
-
end
|
225
|
-
|
226
|
-
data["plans"].each do |plan|
|
227
|
-
check "#{plan["name"]} has a valid price" do
|
228
|
-
if plan["price"] !~ /^\d+$/
|
229
|
-
error "expected an integer"
|
230
|
-
else
|
231
|
-
true
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
check "#{plan["name"]} has a valid price_unit" do
|
236
|
-
if ValidPriceUnits.include?(plan["price_unit"])
|
237
|
-
true
|
238
|
-
else
|
239
|
-
error "expected #{ValidPriceUnits.join(" or ")} but got #{plan["price_unit"].inspect}"
|
240
|
-
end
|
241
|
-
end
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
end
|
246
|
-
|
247
|
-
|
248
|
-
class ProvisionResponseCheck < Check
|
249
|
-
|
250
|
-
def call!
|
251
|
-
response = data[:provision_response]
|
252
|
-
test "response"
|
253
|
-
check "contains an id" do
|
254
|
-
response.is_a?(Hash) && response.has_key?("id")
|
255
|
-
end
|
256
|
-
|
257
|
-
if response.has_key?("config")
|
258
|
-
test "config data"
|
259
|
-
check "is a hash" do
|
260
|
-
response["config"].is_a?(Hash)
|
261
|
-
end
|
262
|
-
|
263
|
-
check "all config keys were previously defined in the manifest" do
|
264
|
-
response["config"].keys.each do |key|
|
265
|
-
error "#{key} is not in the manifest" unless data["api"]["config_vars"].include?(key)
|
266
|
-
end
|
267
|
-
true
|
268
|
-
end
|
269
|
-
|
270
|
-
check "all config values are strings" do
|
271
|
-
response["config"].each do |k, v|
|
272
|
-
if v.is_a?(String)
|
273
|
-
true
|
274
|
-
else
|
275
|
-
error "the key #{k} doesn't contain a string (#{v.inspect})"
|
276
|
-
end
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
check "URL configs vars" do
|
281
|
-
response["config"].each do |key, value|
|
282
|
-
next unless key =~ /_URL$/
|
283
|
-
begin
|
284
|
-
uri = URI.parse(value)
|
285
|
-
error "#{value} is not a valid URI - missing host" unless uri.host
|
286
|
-
error "#{value} is not a valid URI - missing scheme" unless uri.scheme
|
287
|
-
error "#{value} is not a valid URI - pointing to localhost" if @data[:env] == 'production' && uri.host == 'localhost'
|
288
|
-
rescue URI::Error
|
289
|
-
error "#{value} is not a valid URI"
|
290
|
-
end
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
end
|
298
|
-
|
299
|
-
|
300
|
-
module HTTP
|
301
|
-
|
302
|
-
def get(path, params={})
|
303
|
-
path = "#{path}?" + params.map { |k, v| "#{k}=#{v}" }.join("&") unless params.empty?
|
304
|
-
request(:get, [], path)
|
305
|
-
end
|
306
|
-
|
307
|
-
def post(credentials, path, payload=nil)
|
308
|
-
request(:post, credentials, path, payload)
|
309
|
-
end
|
310
|
-
|
311
|
-
def delete(credentials, path, payload=nil)
|
312
|
-
request(:delete, credentials, path, payload)
|
313
|
-
end
|
314
|
-
|
315
|
-
def request(meth, credentials, path, payload=nil)
|
316
|
-
code = nil
|
317
|
-
body = nil
|
318
|
-
|
319
|
-
begin
|
320
|
-
args = [
|
321
|
-
(Yajl::Encoder.encode(payload) if payload),
|
322
|
-
{
|
323
|
-
:accept => "application/json",
|
324
|
-
:content_type => "application/json"
|
325
|
-
}
|
326
|
-
].compact
|
327
|
-
|
328
|
-
user, pass = credentials
|
329
|
-
body = RestClient::Resource.new(url, user, pass)[path].send(
|
330
|
-
meth,
|
331
|
-
*args
|
332
|
-
).to_s
|
333
|
-
|
334
|
-
code = 200
|
335
|
-
rescue RestClient::ExceptionWithResponse => boom
|
336
|
-
code = boom.http_code
|
337
|
-
body = boom.http_body
|
338
|
-
rescue Errno::ECONNREFUSED
|
339
|
-
code = -1
|
340
|
-
body = nil
|
341
|
-
end
|
342
|
-
|
343
|
-
[code, body]
|
344
|
-
end
|
345
|
-
|
346
|
-
end
|
347
|
-
|
348
|
-
class ApiCheck < Check
|
349
|
-
def url
|
350
|
-
env = data[:env] || 'test'
|
351
|
-
data["api"][env].chomp("/")
|
352
|
-
end
|
353
|
-
|
354
|
-
def credentials
|
355
|
-
%w( username password ).map { |attr| data["api"][attr] }
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
|
-
class ProvisionCheck < ApiCheck
|
360
|
-
include HTTP
|
361
|
-
|
362
|
-
READLEN = 1024 * 10
|
363
|
-
APPID = "app#{rand(10000)}@kensa.heroku.com"
|
364
|
-
APPNAME = "myapp"
|
365
|
-
|
366
|
-
def call!
|
367
|
-
json = nil
|
368
|
-
response = nil
|
369
|
-
|
370
|
-
code = nil
|
371
|
-
json = nil
|
372
|
-
path = "/heroku/resources"
|
373
|
-
callback = "http://localhost:7779/callback/999"
|
374
|
-
reader, writer = nil
|
375
|
-
|
376
|
-
payload = {
|
377
|
-
:heroku_id => APPID,
|
378
|
-
:plan => @data[:plan] || @data['plans'].first['id'],
|
379
|
-
:callback_url => callback
|
380
|
-
}
|
381
|
-
|
382
|
-
if data[:async]
|
383
|
-
reader, writer = IO.pipe
|
384
|
-
end
|
385
|
-
|
386
|
-
test "POST /heroku/resources"
|
387
|
-
check "response" do
|
388
|
-
if data[:async]
|
389
|
-
child = fork do
|
390
|
-
Timeout.timeout(10) do
|
391
|
-
reader.close
|
392
|
-
server = TCPServer.open(7779)
|
393
|
-
client = server.accept
|
394
|
-
writer.write(client.readpartial(READLEN))
|
395
|
-
client.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
|
396
|
-
client.close
|
397
|
-
writer.close
|
398
|
-
end
|
399
|
-
end
|
400
|
-
sleep(1)
|
401
|
-
end
|
402
|
-
|
403
|
-
code, json = post(credentials, path, payload)
|
404
|
-
|
405
|
-
if code == 200
|
406
|
-
# noop
|
407
|
-
elsif code == -1
|
408
|
-
error("unable to connect to #{url}")
|
409
|
-
else
|
410
|
-
error("expected 200, got #{code}")
|
411
|
-
end
|
412
|
-
|
413
|
-
true
|
414
|
-
end
|
415
|
-
|
416
|
-
if data[:async]
|
417
|
-
check "async response to PUT #{callback}" do
|
418
|
-
out = reader.readpartial(READLEN)
|
419
|
-
_, json = out.split("\r\n\r\n")
|
420
|
-
end
|
421
|
-
end
|
422
|
-
|
423
|
-
check "valid JSON" do
|
424
|
-
begin
|
425
|
-
response = Yajl::Parser.parse(json)
|
426
|
-
rescue Yajl::ParseError => boom
|
427
|
-
error boom.message
|
428
|
-
end
|
429
|
-
true
|
430
|
-
end
|
431
|
-
|
432
|
-
check "authentication" do
|
433
|
-
wrong_credentials = ['wrong', 'secret']
|
434
|
-
code, _ = post(wrong_credentials, path, payload)
|
435
|
-
error("expected 401, got #{code}") if code != 401
|
436
|
-
true
|
437
|
-
end
|
438
|
-
|
439
|
-
data[:provision_response] = response
|
440
|
-
|
441
|
-
run ProvisionResponseCheck, data
|
442
|
-
end
|
443
|
-
|
444
|
-
ensure
|
445
|
-
reader.close rescue nil
|
446
|
-
writer.close rescue nil
|
447
|
-
end
|
448
|
-
|
449
|
-
|
450
|
-
class DeprovisionCheck < ApiCheck
|
451
|
-
include HTTP
|
452
|
-
|
453
|
-
def call!
|
454
|
-
id = data[:id]
|
455
|
-
raise ArgumentError, "No id specified" if id.nil?
|
456
|
-
|
457
|
-
path = "/heroku/resources/#{CGI::escape(id.to_s)}"
|
458
|
-
|
459
|
-
test "DELETE #{path}"
|
460
|
-
check "response" do
|
461
|
-
code, _ = delete(credentials, path, nil)
|
462
|
-
if code == 200
|
463
|
-
true
|
464
|
-
elsif code == -1
|
465
|
-
error("unable to connect to #{url}")
|
466
|
-
else
|
467
|
-
error("expected 200, got #{code}")
|
468
|
-
end
|
469
|
-
end
|
470
|
-
|
471
|
-
check "authentication" do
|
472
|
-
wrong_credentials = ['wrong', 'secret']
|
473
|
-
code, _ = delete(wrong_credentials, path, nil)
|
474
|
-
error("expected 401, got #{code}") if code != 401
|
475
|
-
true
|
476
|
-
end
|
477
|
-
|
478
|
-
end
|
479
|
-
|
480
|
-
end
|
481
|
-
|
482
|
-
|
483
|
-
class Sso
|
484
|
-
attr_accessor :id, :url
|
485
|
-
|
486
|
-
def initialize(data)
|
487
|
-
@id = data[:id]
|
488
|
-
@salt = data['api']['sso_salt']
|
489
|
-
env = data[:env] || 'test'
|
490
|
-
@url = data["api"][env].chomp('/')
|
491
|
-
end
|
492
|
-
|
493
|
-
def path
|
494
|
-
"/heroku/resources/#{id}"
|
495
|
-
end
|
496
|
-
|
497
|
-
def full_url
|
498
|
-
t = Time.now.to_i
|
499
|
-
"#{url}#{path}?token=#{make_token(t)}×tamp=#{t}"
|
500
|
-
end
|
501
|
-
|
502
|
-
def make_token(t)
|
503
|
-
Digest::SHA1.hexdigest([@id, @salt, t].join(':'))
|
504
|
-
end
|
505
|
-
end
|
506
|
-
|
507
|
-
|
508
|
-
class SsoCheck < ApiCheck
|
509
|
-
include HTTP
|
510
|
-
|
511
|
-
def mechanize_get url
|
512
|
-
agent = Mechanize.new
|
513
|
-
page = agent.get(url)
|
514
|
-
return page, 200
|
515
|
-
rescue Mechanize::ResponseCodeError => error
|
516
|
-
return nil, error.response_code.to_i
|
517
|
-
rescue Errno::ECONNREFUSED
|
518
|
-
error("connection refused to #{url}")
|
519
|
-
end
|
520
|
-
|
521
|
-
def call!
|
522
|
-
sso = Sso.new(data)
|
523
|
-
t = Time.now.to_i
|
524
|
-
|
525
|
-
test "GET #{sso.path}"
|
526
|
-
check "validates token" do
|
527
|
-
page, respcode = mechanize_get sso.url + sso.path + "?token=invalid×tamp=#{t}"
|
528
|
-
error("expected 403, got 200") unless respcode == 403
|
529
|
-
true
|
530
|
-
end
|
531
|
-
|
532
|
-
check "validates timestamp" do
|
533
|
-
prev = (Time.now - 60*6).to_i
|
534
|
-
page, respcode = mechanize_get sso.url + sso.path + "?token=#{sso.make_token(prev)}×tamp=#{prev}"
|
535
|
-
error("expected 403, got 200") unless respcode == 403
|
536
|
-
true
|
537
|
-
end
|
538
|
-
|
539
|
-
check "logs in" do
|
540
|
-
page, respcode = mechanize_get sso.url + sso.path + "?token=#{sso.make_token(t)}×tamp=#{t}"
|
541
|
-
error("expected 200, got #{respcode}") unless respcode == 200
|
542
|
-
true
|
543
|
-
end
|
544
|
-
end
|
545
|
-
end
|
546
|
-
|
547
|
-
|
548
|
-
##
|
549
|
-
# On Testing:
|
550
|
-
# I've opted to not write tests for this
|
551
|
-
# due to the simple nature it's currently in.
|
552
|
-
# If this becomes more complex in even the
|
553
|
-
# least amount, find me (blake) and I'll
|
554
|
-
# help get tests in.
|
555
|
-
class AllCheck < Check
|
556
|
-
|
557
|
-
def call!
|
558
|
-
args = data[:args]
|
559
|
-
run ProvisionCheck, data
|
560
|
-
|
561
|
-
response = data[:provision_response]
|
562
|
-
data.merge!(:id => response["id"])
|
563
|
-
config = response["config"] || Hash.new
|
564
|
-
|
565
|
-
if args
|
566
|
-
screen.message "\n\n"
|
567
|
-
screen.message "Starting #{args.first}..."
|
568
|
-
screen.message ""
|
569
|
-
|
570
|
-
run_in_env(config) { system(*args) }
|
571
|
-
|
572
|
-
screen.message ""
|
573
|
-
screen.message "End of #{args.first}"
|
574
|
-
end
|
575
|
-
|
576
|
-
run DeprovisionCheck, data
|
577
|
-
end
|
578
|
-
|
579
|
-
def run_in_env(env)
|
580
|
-
env.each {|key, value| ENV[key] = value }
|
581
|
-
yield
|
582
|
-
env.keys.each {|key| ENV.delete(key) }
|
583
|
-
end
|
584
|
-
|
585
|
-
end
|
586
|
-
|
587
|
-
end
|
588
|
-
|
589
|
-
end
|
1
|
+
require 'heroku/kensa/http'
|
2
|
+
require 'heroku/kensa/manifest'
|
3
|
+
require 'heroku/kensa/check'
|
4
|
+
require 'heroku/kensa/sso'
|