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
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'restclient'
|
2
|
+
require 'term/ansicolor'
|
3
|
+
require 'launchy'
|
4
|
+
|
5
|
+
module Heroku
|
6
|
+
module Kensa
|
7
|
+
class Client
|
8
|
+
|
9
|
+
def initialize(args, options)
|
10
|
+
@args = args
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def filename
|
15
|
+
@options[:filename]
|
16
|
+
end
|
17
|
+
|
18
|
+
class CommandInvalid < Exception; end
|
19
|
+
|
20
|
+
def run!
|
21
|
+
command = @args.shift
|
22
|
+
raise CommandInvalid unless command && respond_to?(command)
|
23
|
+
send(command)
|
24
|
+
end
|
25
|
+
|
26
|
+
def init
|
27
|
+
Manifest.new(filename, @options).write
|
28
|
+
Screen.new.message "Initialized new addon manifest in #{filename}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def test
|
32
|
+
case check = @args.shift
|
33
|
+
when "manifest"
|
34
|
+
run_check ManifestCheck
|
35
|
+
when "provision"
|
36
|
+
run_check ManifestCheck
|
37
|
+
run_check ProvisionCheck
|
38
|
+
when "deprovision"
|
39
|
+
id = ARGV.shift || abort("! no id specified; see usage")
|
40
|
+
run_check ManifestCheck
|
41
|
+
run_check DeprovisionCheck, :id => id
|
42
|
+
when "sso"
|
43
|
+
id = ARGV.shift || abort("! no id specified; see usage")
|
44
|
+
run_check ManifestCheck
|
45
|
+
run_check SsoCheck, :id => id
|
46
|
+
else
|
47
|
+
abort "! Unknown test '#{check}'; see usage"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def run
|
52
|
+
abort "! missing command to run; see usage" if ARGV.empty?
|
53
|
+
run_check ManifestCheck
|
54
|
+
run_check AllCheck, :args => ARGV
|
55
|
+
end
|
56
|
+
|
57
|
+
def sso
|
58
|
+
id = ARGV.shift || abort("! no id specified; see usage")
|
59
|
+
data = Yajl::Parser.parse(resolve_manifest).merge(:id => id)
|
60
|
+
sso = Sso.new(data.merge(@options))
|
61
|
+
puts "Opening #{sso.full_url}"
|
62
|
+
Launchy.open sso.full_url
|
63
|
+
end
|
64
|
+
|
65
|
+
def push
|
66
|
+
require_heroku
|
67
|
+
client = Heroku::Command.run "auth:client", ['--ignore-keys']
|
68
|
+
host = ENV['ADDONS_HOST'] || 'https://addons.heroku.com'
|
69
|
+
resource = RestClient::Resource.new(host, client.user, client.password)
|
70
|
+
resource['provider/addons'].post(resolve_manifest)
|
71
|
+
puts "Manifest pushed succesfully"
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def resolve_manifest
|
77
|
+
if File.exists?(filename)
|
78
|
+
File.read(filename)
|
79
|
+
else
|
80
|
+
abort("fatal: #{filename} not found")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def run_check(klass, args={})
|
85
|
+
screen = Screen.new
|
86
|
+
data = Yajl::Parser.parse(resolve_manifest)
|
87
|
+
check = klass.new(data.merge(@options.merge(args)), screen)
|
88
|
+
check.call
|
89
|
+
screen.finish
|
90
|
+
end
|
91
|
+
|
92
|
+
def require_heroku
|
93
|
+
require 'heroku'
|
94
|
+
require 'heroku/command'
|
95
|
+
require 'heroku/commands/auth'
|
96
|
+
rescue LoadError
|
97
|
+
abort("fatal: Could not load the Heroku gem. Plase make sure the Heroku gem is available and up to date")
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
class Screen
|
102
|
+
include Term::ANSIColor
|
103
|
+
|
104
|
+
def test(msg)
|
105
|
+
$stdout.puts
|
106
|
+
$stdout.puts
|
107
|
+
$stdout.print "Testing #{msg}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def check(msg)
|
111
|
+
$stdout.puts
|
112
|
+
$stdout.print " Check #{msg}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def error(msg)
|
116
|
+
$stdout.print "\n", magenta(" ! #{msg}")
|
117
|
+
end
|
118
|
+
|
119
|
+
def result(status)
|
120
|
+
msg = status ? bold("[PASS]") : red(bold("[FAIL]"))
|
121
|
+
$stdout.print " #{msg}"
|
122
|
+
end
|
123
|
+
|
124
|
+
def message(msg)
|
125
|
+
$stdout.puts msg
|
126
|
+
end
|
127
|
+
|
128
|
+
def finish
|
129
|
+
$stdout.puts
|
130
|
+
$stdout.puts
|
131
|
+
$stdout.puts "done."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'restclient'
|
2
|
+
|
3
|
+
module Heroku
|
4
|
+
module Kensa
|
5
|
+
module HTTP
|
6
|
+
|
7
|
+
def get(path, params={})
|
8
|
+
path = "#{path}?" + params.map { |k, v| "#{k}=#{v}" }.join("&") unless params.empty?
|
9
|
+
request(:get, [], path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def post(credentials, path, payload=nil)
|
13
|
+
request(:post, credentials, path, payload)
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete(credentials, path, payload=nil)
|
17
|
+
request(:delete, credentials, path, payload)
|
18
|
+
end
|
19
|
+
|
20
|
+
def request(meth, credentials, path, payload=nil)
|
21
|
+
code = nil
|
22
|
+
body = nil
|
23
|
+
|
24
|
+
begin
|
25
|
+
args = [
|
26
|
+
(Yajl::Encoder.encode(payload) if payload),
|
27
|
+
{
|
28
|
+
:accept => "application/json",
|
29
|
+
:content_type => "application/json"
|
30
|
+
}
|
31
|
+
].compact
|
32
|
+
|
33
|
+
user, pass = credentials
|
34
|
+
body = RestClient::Resource.new(url, user, pass)[path].send(
|
35
|
+
meth,
|
36
|
+
*args
|
37
|
+
).to_s
|
38
|
+
|
39
|
+
code = 200
|
40
|
+
rescue RestClient::ExceptionWithResponse => boom
|
41
|
+
code = boom.http_code
|
42
|
+
body = boom.http_body
|
43
|
+
rescue Errno::ECONNREFUSED
|
44
|
+
code = -1
|
45
|
+
body = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
[code, body]
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Heroku
|
2
|
+
module Kensa
|
3
|
+
class Manifest
|
4
|
+
|
5
|
+
def initialize(filename = 'addon-manifest.json', options = {})
|
6
|
+
@filename, @options = filename, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def skeleton_json
|
10
|
+
<<-JSON
|
11
|
+
{
|
12
|
+
"id": "myaddon",
|
13
|
+
"plans": [
|
14
|
+
{ "id": "basic" },
|
15
|
+
{ "id": "premium" }
|
16
|
+
],
|
17
|
+
"api": {
|
18
|
+
"config_vars": [ "MYADDON_URL" ],
|
19
|
+
"username": "heroku",
|
20
|
+
"password": "b1EWrHYXE1R5J71D",#{ sso_key }
|
21
|
+
"production": "https://yourapp.com/",
|
22
|
+
"test": "http://localhost:4567/"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
JSON
|
26
|
+
end
|
27
|
+
|
28
|
+
def skeleton
|
29
|
+
Yajl::Parser.parse skeleton_json
|
30
|
+
end
|
31
|
+
|
32
|
+
def write
|
33
|
+
open(@filename, 'w') { |f| f << skeleton_json }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def sso_key
|
39
|
+
unless @options[:sso] === false
|
40
|
+
%{\n "sso_salt": #{ generate_password(16).inspect },}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
PasswordChars = chars = ['a'..'z', 'A'..'Z', '0'..'9'].map { |r| r.to_a }.flatten
|
45
|
+
def generate_password(size=16)
|
46
|
+
Array.new(size) { PasswordChars[rand(PasswordChars.size)] }.join
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'restclient'
|
2
|
+
|
3
|
+
module Heroku
|
4
|
+
module Kensa
|
5
|
+
class Sso
|
6
|
+
attr_accessor :id, :url
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
@id = data[:id]
|
10
|
+
@salt = data['api']['sso_salt']
|
11
|
+
|
12
|
+
env = data.fetch :env, 'test'
|
13
|
+
@url = data["api"][env].chomp('/')
|
14
|
+
end
|
15
|
+
|
16
|
+
def path
|
17
|
+
"/heroku/resources/#{id}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def full_url
|
21
|
+
[ url, path, querystring ].join
|
22
|
+
end
|
23
|
+
|
24
|
+
def make_token(t)
|
25
|
+
Digest::SHA1.hexdigest([@id, @salt, t].join(':'))
|
26
|
+
end
|
27
|
+
|
28
|
+
def querystring
|
29
|
+
return '' unless @salt
|
30
|
+
|
31
|
+
t = Time.now.to_i
|
32
|
+
"?token=#{make_token(t)}×tamp=#{t}&nav-data=#{sample_nav_data}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def sample_nav_data
|
36
|
+
json = Yajl::Encoder.encode({
|
37
|
+
:addon => 'Your Addon',
|
38
|
+
:appname => 'myapp',
|
39
|
+
:addons => [
|
40
|
+
{ :slug => 'cron', :name => 'Cron'},
|
41
|
+
{ :slug => 'custom_domains+wildcard', :name => 'Custom Domains + Wildcard'},
|
42
|
+
{ :slug => 'youraddon', :name => 'Your Addon'},
|
43
|
+
]
|
44
|
+
})
|
45
|
+
base64_url_variant(json)
|
46
|
+
end
|
47
|
+
|
48
|
+
def base64_url_variant(text)
|
49
|
+
base64 = [text].pack('m').gsub('=', '').gsub("\n", '')
|
50
|
+
base64.tr('+/','-_')
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
class AllCheckTest < Test::Unit::TestCase
|
4
|
+
include Heroku::Kensa
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@data = Manifest.new.skeleton
|
8
|
+
@data['api']['username'] = 'test'
|
9
|
+
@data['api']['password'] = 'secret'
|
10
|
+
@data['api']['test'] += "working"
|
11
|
+
@file = File.dirname(__FILE__) + "/resources/runner.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def check; AllCheck; end
|
15
|
+
|
16
|
+
test "valid on script exit 0" do
|
17
|
+
@data[:args] = "ruby #{@file}"
|
18
|
+
assert_valid
|
19
|
+
end
|
20
|
+
|
21
|
+
test "invalid on script exit non 0" do
|
22
|
+
@data[:args] = "ruby #{@file} fail"
|
23
|
+
assert_invalid
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/test/deprovision_check.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
require
|
1
|
+
require 'test/helper'
|
2
2
|
|
3
3
|
class DeprovisionCheckTest < Test::Unit::TestCase
|
4
|
-
include Heroku::
|
4
|
+
include Heroku::Kensa
|
5
5
|
|
6
6
|
setup do
|
7
|
-
@data = Manifest.skeleton.merge :id => 123
|
7
|
+
@data = Manifest.new.skeleton.merge :id => 123
|
8
8
|
@responses = [
|
9
9
|
[200, ""],
|
10
10
|
[401, ""],
|
data/test/helper.rb
CHANGED
data/test/manifest_check_test.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
require
|
1
|
+
require 'test/helper'
|
2
2
|
|
3
3
|
class ManifestCheckTest < Test::Unit::TestCase
|
4
|
-
include Heroku::
|
4
|
+
include Heroku::Kensa
|
5
5
|
|
6
6
|
def check ; ManifestCheck ; end
|
7
7
|
|
8
8
|
setup do
|
9
|
-
@data = Manifest.skeleton
|
9
|
+
@data = Manifest.new.skeleton
|
10
10
|
@data["plans"] << {
|
11
11
|
"id" => "advanced",
|
12
12
|
"name" => "Advanced",
|
@@ -24,11 +24,6 @@ class ManifestCheckTest < Test::Unit::TestCase
|
|
24
24
|
assert_invalid
|
25
25
|
end
|
26
26
|
|
27
|
-
test "has a name" do
|
28
|
-
@data.delete("name")
|
29
|
-
assert_invalid
|
30
|
-
end
|
31
|
-
|
32
27
|
test "api key exists" do
|
33
28
|
@data.delete("api")
|
34
29
|
assert_invalid
|
@@ -114,29 +109,4 @@ class ManifestCheckTest < Test::Unit::TestCase
|
|
114
109
|
assert_invalid
|
115
110
|
end
|
116
111
|
|
117
|
-
test "all plans have a name" do
|
118
|
-
@data["plans"].first.delete("name")
|
119
|
-
assert_invalid
|
120
|
-
end
|
121
|
-
|
122
|
-
test "all plans have a unique name" do
|
123
|
-
@data["plans"].first["name"] = @data["plans"].last["name"]
|
124
|
-
assert_invalid
|
125
|
-
end
|
126
|
-
|
127
|
-
test "plans have a price" do
|
128
|
-
@data["plans"].first.delete("price")
|
129
|
-
assert_invalid
|
130
|
-
end
|
131
|
-
|
132
|
-
test "plans have an integer price" do
|
133
|
-
@data["plans"].first["price"] = "fiddy cent"
|
134
|
-
assert_invalid
|
135
|
-
end
|
136
|
-
|
137
|
-
test "plans have a valid price_unit" do
|
138
|
-
@data["plans"].first["price_unit"] = "first ov da munth"
|
139
|
-
assert_invalid
|
140
|
-
end
|
141
|
-
|
142
112
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
class ManifestTest < Test::Unit::TestCase
|
4
|
+
include Heroku::Kensa
|
5
|
+
|
6
|
+
context 'manifest' do
|
7
|
+
setup { @manifest = Manifest.new }
|
8
|
+
|
9
|
+
test 'have sso salt' do
|
10
|
+
assert_not_nil @manifest.skeleton['api']['sso_salt']
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'manifest without sso' do
|
15
|
+
setup do
|
16
|
+
options = { :sso => false }
|
17
|
+
@manifest = Manifest.new 'test.txt', options
|
18
|
+
end
|
19
|
+
|
20
|
+
test 'exclude sso salt' do
|
21
|
+
assert_nil @manifest.skeleton['api']['sso_salt']
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,10 +1,10 @@
|
|
1
|
-
require
|
1
|
+
require 'test/helper'
|
2
2
|
|
3
3
|
class ProvisionCheckTest < Test::Unit::TestCase
|
4
|
-
include Heroku::
|
4
|
+
include Heroku::Kensa
|
5
5
|
|
6
6
|
setup do
|
7
|
-
@data = Manifest.skeleton
|
7
|
+
@data = Manifest.new.skeleton
|
8
8
|
@data['api']['username'] = 'test'
|
9
9
|
@data['api']['password'] = 'secret'
|
10
10
|
end
|