ey_resolver 0.1.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.
- data/.gitignore +3 -0
- data/Gemfile +14 -0
- data/README.md +32 -0
- data/Rakefile +14 -0
- data/ey_resolver.gemspec +25 -0
- data/lib/ey_resolver.rb +12 -0
- data/lib/ey_resolver/abstract_resolver.rb +61 -0
- data/lib/ey_resolver/account_resolver.rb +47 -0
- data/lib/ey_resolver/app_env_resolver.rb +57 -0
- data/lib/ey_resolver/app_resolver.rb +52 -0
- data/lib/ey_resolver/environment_resolver.rb +67 -0
- data/lib/ey_resolver/examiner.rb +143 -0
- data/lib/ey_resolver/mock.rb +5 -0
- data/lib/ey_resolver/mock/models.rb +13 -0
- data/lib/ey_resolver/mock/models/account.rb +12 -0
- data/lib/ey_resolver/mock/models/app.rb +18 -0
- data/lib/ey_resolver/mock/models/app_environment.rb +16 -0
- data/lib/ey_resolver/mock/models/environment.rb +16 -0
- data/lib/ey_resolver/mock/models/user.rb +10 -0
- data/lib/ey_resolver/query.rb +68 -0
- data/lib/ey_resolver/version.rb +5 -0
- data/spec/account_resolver_spec.rb +120 -0
- data/spec/app_env_resolver_spec.rb +159 -0
- data/spec/app_resolver_spec.rb +128 -0
- data/spec/environment_resolver_spec.rb +166 -0
- data/spec/spec_helper.rb +84 -0
- metadata +139 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# EY::Resolver
|
2
|
+
|
3
|
+
Resolves a set of query options to an Array of AppEnvironment (AppDeployment)
|
4
|
+
that best match the query. At least one query parameter must be 'app\_name'
|
5
|
+
or 'remotes' or it raises EY::Resolver::UnconstrainedError (ArgumentError).
|
6
|
+
|
7
|
+
It works by first doing a pass asking for any AppEnvironments that have the
|
8
|
+
query arguments as substrings, keeping only those that match all query params.
|
9
|
+
|
10
|
+
Next, if remotes were passed, and 'app\_name' was not, it constrains by remotes
|
11
|
+
using [Gitable::URI](https://github.com/martinemde/gitable)'s equivalent
|
12
|
+
matching algorithm to restrict to only those with the right remote URIs
|
13
|
+
(public and private repo URIs should match).
|
14
|
+
|
15
|
+
Lastly, it tries to discover better matches by scoring each match based on the
|
16
|
+
number of exact matches it has. An exact match means the query param is exactly
|
17
|
+
the full string of the matching attribute.
|
18
|
+
|
19
|
+
The results are all AppEnvironments that have the same top exact match score.
|
20
|
+
It's possible for there to be more than one result that matches equally.
|
21
|
+
|
22
|
+
## Example
|
23
|
+
|
24
|
+
query = {
|
25
|
+
'account_name' => 'ey',
|
26
|
+
'app_name' => 'app',
|
27
|
+
'environment_name' => 'resolver',
|
28
|
+
'remotes' => ['git@github.com:engineyard/app-env-resolver.git']
|
29
|
+
}
|
30
|
+
results = EY::Resolver.awsm_resolver(query, current_user).app_environments
|
31
|
+
results # An Array of AppEnvironment (AppDeployment) models that best match the query.
|
32
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
RSpec::Core::RakeTask.new do |t|
|
5
|
+
t.rspec_opts = %w[--color]
|
6
|
+
t.pattern = 'spec/**/*_spec.rb'
|
7
|
+
end
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
task :coverage => [:coverage_env, :spec]
|
11
|
+
|
12
|
+
task :coverage_env do
|
13
|
+
ENV['COVERAGE'] = '1'
|
14
|
+
end
|
data/ey_resolver.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ey_resolver/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ey_resolver"
|
7
|
+
s.version = EY::Resolver::VERSION
|
8
|
+
s.authors = ["Martin Emde"]
|
9
|
+
s.email = ["martin.emde@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Resolves awsm models given constraints}
|
12
|
+
s.description = %q{Resolves apps, environments, app_environments and accounts using a set of constraints}
|
13
|
+
|
14
|
+
s.rubyforge_project = "ey_resolver"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_runtime_dependency "gitable", "~>0.2.3"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
s.add_development_dependency "rspec", "~>2.0"
|
25
|
+
end
|
data/lib/ey_resolver.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'ey_resolver/query'
|
2
|
+
require 'ey_resolver/examiner'
|
3
|
+
require 'ey_resolver/account_resolver'
|
4
|
+
require 'ey_resolver/app_resolver'
|
5
|
+
require 'ey_resolver/app_env_resolver'
|
6
|
+
require 'ey_resolver/environment_resolver'
|
7
|
+
|
8
|
+
module EY
|
9
|
+
class Resolver
|
10
|
+
class UnconstrainedError < ArgumentError; end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'ey_resolver/query'
|
2
|
+
require 'ey_resolver/examiner'
|
3
|
+
|
4
|
+
class EY::Resolver
|
5
|
+
class AbstractResolver
|
6
|
+
def self.model
|
7
|
+
raise "Unimplemented"
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :user, :query, :model
|
11
|
+
|
12
|
+
def initialize(user, params, model = nil)
|
13
|
+
@user = user
|
14
|
+
@query = Query.new(params)
|
15
|
+
@model = model || self.class.model
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches
|
19
|
+
if query.unconstrained?
|
20
|
+
return []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def errors
|
25
|
+
examiner.unconstrained_errors
|
26
|
+
end
|
27
|
+
|
28
|
+
def suggestions
|
29
|
+
errors
|
30
|
+
examiner.suggestions
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def examiner
|
36
|
+
@examiner ||= Examiner.new(query, user)
|
37
|
+
end
|
38
|
+
|
39
|
+
def filter_by_remotes(candidates)
|
40
|
+
return candidates unless query.remotes && !query.remotes.empty?
|
41
|
+
candidates.select do |candidate|
|
42
|
+
query.remotes.any? { |uri| equivalent_remote?(candidate,uri) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def equivalent_remote?(candidate, uri)
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
def best_matches(candidates)
|
51
|
+
groups = candidates.group_by do |candidate|
|
52
|
+
query.count_matches {|key,val| exact_match?(candidate, key, val)}
|
53
|
+
end
|
54
|
+
groups.fetch(groups.keys.max, [])
|
55
|
+
end
|
56
|
+
|
57
|
+
def exact_match?(candidate, key, val)
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'ey_resolver/abstract_resolver'
|
2
|
+
|
3
|
+
class EY::Resolver
|
4
|
+
def self.account_resolver(user, params, model = nil)
|
5
|
+
AccountResolver.new(user, params, model)
|
6
|
+
end
|
7
|
+
|
8
|
+
class AccountResolver < AbstractResolver
|
9
|
+
def self.model
|
10
|
+
::Account
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches
|
14
|
+
if query.unconstrained?
|
15
|
+
return []
|
16
|
+
end
|
17
|
+
|
18
|
+
dm_query = {}
|
19
|
+
dm_query[:name.like] = "%#{query.account_name}%" if query.account_name
|
20
|
+
|
21
|
+
best_matches user.accounts.all(dm_query)
|
22
|
+
end
|
23
|
+
|
24
|
+
def errors
|
25
|
+
@errors ||=
|
26
|
+
super ||
|
27
|
+
examiner.not_found_errors ||
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def exact_match?(account, key, val)
|
34
|
+
case key.to_sym
|
35
|
+
when :remotes
|
36
|
+
app_uris = account.apps.map { |app| app.gitable_uri }.flatten
|
37
|
+
val.any? { |uri| app_uris.any? { |app_uri| app_uri.equivalent?(uri) } }
|
38
|
+
when :app_name
|
39
|
+
account.apps.any? {|app| app.name.downcase.index(val.downcase) } # partial match app if given for account
|
40
|
+
when :account_name
|
41
|
+
account.name.downcase == val.downcase
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'ey_resolver/abstract_resolver'
|
2
|
+
|
3
|
+
class EY::Resolver
|
4
|
+
def self.app_env_resolver(user, params, model = nil)
|
5
|
+
AppEnvResolver.new(user, params, model)
|
6
|
+
end
|
7
|
+
|
8
|
+
class AppEnvResolver < AbstractResolver
|
9
|
+
def self.model
|
10
|
+
::AppEnvironment
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches
|
14
|
+
if query.unconstrained? || query.app_unconstrained?
|
15
|
+
return []
|
16
|
+
end
|
17
|
+
|
18
|
+
dm_query = {}
|
19
|
+
dm_query[model.app.name.like] = "%#{query.app_name}%" if query.app_name
|
20
|
+
dm_query[model.environment.name.like] = "%#{query.environment_name}%" if query.environment_name
|
21
|
+
dm_query[model.app.account.name.like] = "%#{query.account_name}%" if query.account_name
|
22
|
+
|
23
|
+
candidates = user.accounts.apps.app_environments.all(dm_query)
|
24
|
+
best_matches filter_by_remotes(candidates)
|
25
|
+
end
|
26
|
+
|
27
|
+
def errors
|
28
|
+
@errors ||=
|
29
|
+
super ||
|
30
|
+
examiner.app_unconstrained_errors ||
|
31
|
+
examiner.not_found_errors ||
|
32
|
+
examiner.account_scoping_errors ||
|
33
|
+
examiner.association_errors ||
|
34
|
+
examiner.env_not_found_errors ||
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def equivalent_remote?(app_env, uri)
|
41
|
+
app_env.app.gitable_uri.equivalent?(uri)
|
42
|
+
end
|
43
|
+
|
44
|
+
def exact_match?(app_env, key, val)
|
45
|
+
case key.to_sym
|
46
|
+
when :app_name
|
47
|
+
app_env.app.name.downcase == val.downcase
|
48
|
+
when :environment_name
|
49
|
+
app_env.environment.name.downcase == val.downcase
|
50
|
+
when :account_name
|
51
|
+
app_env.app.account.name.downcase == val.downcase
|
52
|
+
else
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'ey_resolver/abstract_resolver'
|
2
|
+
|
3
|
+
class EY::Resolver
|
4
|
+
def self.app_resolver(user, params, model = nil)
|
5
|
+
AppResolver.new(user, params, model)
|
6
|
+
end
|
7
|
+
|
8
|
+
class AppResolver < AbstractResolver
|
9
|
+
def self.model
|
10
|
+
::App
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches
|
14
|
+
if query.unconstrained? || query.app_unconstrained?
|
15
|
+
return []
|
16
|
+
end
|
17
|
+
|
18
|
+
dm_query = {}
|
19
|
+
dm_query[:name.like] = "%#{query.app_name}%" if query.app_name
|
20
|
+
dm_query[model.account.name.like] = "%#{query.account_name}%" if query.account_name
|
21
|
+
|
22
|
+
candidates = user.accounts.apps.all(dm_query)
|
23
|
+
best_matches filter_by_remotes(candidates)
|
24
|
+
end
|
25
|
+
|
26
|
+
def errors
|
27
|
+
@errors ||=
|
28
|
+
super ||
|
29
|
+
examiner.app_unconstrained_errors ||
|
30
|
+
examiner.not_found_errors ||
|
31
|
+
examiner.account_scoping_errors ||
|
32
|
+
[]
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def equivalent_remote?(app, uri)
|
38
|
+
app.gitable_uri.equivalent?(uri)
|
39
|
+
end
|
40
|
+
|
41
|
+
def exact_match?(app, key, val)
|
42
|
+
case key.to_sym
|
43
|
+
when :app_name
|
44
|
+
app.name.downcase == val.downcase
|
45
|
+
when :account_name
|
46
|
+
app.account.name.downcase == val.downcase
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'ey_resolver/abstract_resolver'
|
2
|
+
|
3
|
+
class EY::Resolver
|
4
|
+
def self.environment_resolver(user, params, env_class = nil)
|
5
|
+
EnvironmentResolver.new(user, params, env_class)
|
6
|
+
end
|
7
|
+
|
8
|
+
class EnvironmentResolver < AbstractResolver
|
9
|
+
def self.model
|
10
|
+
::Environment
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches
|
14
|
+
if query.unconstrained? || query.env_unconstrained?
|
15
|
+
return []
|
16
|
+
end
|
17
|
+
|
18
|
+
dm_query = {}
|
19
|
+
dm_query[:name.like] = "%#{query.environment_name}%" if query.environment_name
|
20
|
+
dm_query[model.account.name.like] = "%#{query.account_name}%" if query.account_name
|
21
|
+
dm_query[model.app_environments.app.name.like] = "%#{query.app_name}%" if query.app_name
|
22
|
+
|
23
|
+
candidates = user.accounts.environments.all(dm_query)
|
24
|
+
|
25
|
+
if !query.environment_name && query.remotes
|
26
|
+
# if env name is not specified, and the only constraint is the remotes, require the remotes to match
|
27
|
+
candidates = filter_by_remotes(candidates)
|
28
|
+
end
|
29
|
+
|
30
|
+
best_matches candidates
|
31
|
+
end
|
32
|
+
|
33
|
+
def errors
|
34
|
+
@errors ||=
|
35
|
+
super ||
|
36
|
+
examiner.env_unconstrained_errors ||
|
37
|
+
examiner.not_found_errors ||
|
38
|
+
examiner.account_scoping_errors ||
|
39
|
+
examiner.association_errors ||
|
40
|
+
examiner.env_not_found_errors ||
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def equivalent_remote?(env, uri)
|
47
|
+
app_uris = env.apps.map { |app| app.gitable_uri }.flatten
|
48
|
+
app_uris.any? { |app_uri| app_uri.equivalent?(uri) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def exact_match?(env, key, val)
|
52
|
+
case key.to_sym
|
53
|
+
when :remotes
|
54
|
+
app_uris = env.apps.map { |app| app.gitable_uri }.flatten
|
55
|
+
val.any? { |uri| app_uris.any? { |app_uri| app_uri.equivalent?(uri) } }
|
56
|
+
when :app_name
|
57
|
+
env.app_environments.any? { |app_env| app_env.app.name.downcase == val.downcase }
|
58
|
+
when :environment_name
|
59
|
+
env.name.downcase == val.downcase
|
60
|
+
when :account_name
|
61
|
+
env.account.name.downcase == val.downcase
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
class EY::Resolver
|
2
|
+
|
3
|
+
# The accounts, apps, and envs lookups here are fairly "expensive" so
|
4
|
+
# the order we use them in is important.
|
5
|
+
#
|
6
|
+
# For example, we don't look at the apps matches unless app_name or remotes
|
7
|
+
# were specified.
|
8
|
+
class Examiner
|
9
|
+
attr_reader :query, :suggestions
|
10
|
+
|
11
|
+
def initialize(query, user)
|
12
|
+
@query, @user = query, user
|
13
|
+
problems ||= []
|
14
|
+
@suggestions ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def app_unconstrained_errors
|
18
|
+
if query.app_unconstrained?
|
19
|
+
return ["App name or repository remotes required."]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def env_unconstrained_errors
|
24
|
+
if query.env_unconstrained?
|
25
|
+
return ["Environment name, app name or repository remotes required."]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def unconstrained_errors
|
30
|
+
if query.unconstrained?
|
31
|
+
return ["Must search by account name, app name, remotes, or environment name."]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Specified name was not found
|
36
|
+
def not_found_errors
|
37
|
+
problems = []
|
38
|
+
|
39
|
+
if query.account_name && accounts.empty?
|
40
|
+
problems << "No account found matching #{account_name}."
|
41
|
+
end
|
42
|
+
|
43
|
+
if query.app_constrained? && apps.empty?
|
44
|
+
app_name_or_remotes =
|
45
|
+
if app_name
|
46
|
+
"#{app_name}."
|
47
|
+
elsif query.remotes
|
48
|
+
"remotes:" + query.remotes.map { |uri| "\n\t#{uri}" }.join
|
49
|
+
else
|
50
|
+
"no remotes."
|
51
|
+
end
|
52
|
+
problems << "No application found matching #{app_name_or_remotes}"
|
53
|
+
end
|
54
|
+
|
55
|
+
if query.environment_name && envs.empty?
|
56
|
+
problems << "No environment found matching #{environment_name}."
|
57
|
+
end
|
58
|
+
|
59
|
+
return problems if problems.any?
|
60
|
+
end
|
61
|
+
|
62
|
+
# Account scoping is wrong
|
63
|
+
def account_scoping_errors
|
64
|
+
if !query.account_name
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
problems = []
|
69
|
+
|
70
|
+
if query.app_constrained? && accounts.any? && apps.any? && acc_apps.empty?
|
71
|
+
app_names = apps.map {|a|a.name.inspect}.join(', ')
|
72
|
+
problems << "Application #{app_names} found, but does not exist in account #{account_name}."
|
73
|
+
@suggestions[:apps] = apps
|
74
|
+
end
|
75
|
+
|
76
|
+
if query.environment_name && accounts.any? && envs.any? && acc_envs.empty?
|
77
|
+
problems << "Environment #{environment_name} found, but does not exist in account #{account_name}."
|
78
|
+
@suggestions[:environments] = envs
|
79
|
+
end
|
80
|
+
return problems if problems.any?
|
81
|
+
end
|
82
|
+
|
83
|
+
def env_not_found_errors
|
84
|
+
problems = []
|
85
|
+
|
86
|
+
if !query.environment_name && query.remotes && acc_envs.empty? && acc_apps.any?
|
87
|
+
problems << "No environment found for applications matching remotes:" + query.remotes.map { |uri| "\n\t#{uri}" }.join
|
88
|
+
#@suggestions[:apps] = acc_apps
|
89
|
+
end
|
90
|
+
|
91
|
+
return problems if problems.any?
|
92
|
+
end
|
93
|
+
|
94
|
+
# App doesn't match environment
|
95
|
+
def association_errors
|
96
|
+
problems = []
|
97
|
+
|
98
|
+
if query.environment_name && query.app_name && acc_envs.any? && acc_apps.any?
|
99
|
+
problems << "Application #{app_name} and environment #{environment_name} are not associated."
|
100
|
+
@suggestions[:apps] = acc_apps
|
101
|
+
@suggestions[:environments] = acc_envs
|
102
|
+
elsif query.remotes && acc_envs.any? && acc_apps.any?
|
103
|
+
problems << "Application #{acc_apps.map {|a|a.name.inspect}.join(', ')} and environment #{environment_name} are not associated."
|
104
|
+
@suggestions[:apps] = acc_apps
|
105
|
+
@suggestions[:environments] = acc_envs
|
106
|
+
end
|
107
|
+
|
108
|
+
return problems if problems.any?
|
109
|
+
end
|
110
|
+
|
111
|
+
def account_name
|
112
|
+
query.account_name && query.account_name.inspect
|
113
|
+
end
|
114
|
+
|
115
|
+
def app_name
|
116
|
+
query.app_name && query.app_name.inspect
|
117
|
+
end
|
118
|
+
|
119
|
+
def environment_name
|
120
|
+
query.environment_name && query.environment_name.inspect
|
121
|
+
end
|
122
|
+
|
123
|
+
def accounts
|
124
|
+
@accounts ||= AccountResolver.new(@user, :account_name => query.account_name).matches
|
125
|
+
end
|
126
|
+
|
127
|
+
def apps
|
128
|
+
@apps ||= AppResolver.new(@user, :app_name => query.app_name, :remotes => query.remotes).matches
|
129
|
+
end
|
130
|
+
|
131
|
+
def acc_apps
|
132
|
+
@acc_apps ||= AppResolver.new(@user, :app_name => query.app_name, :remotes => query.remotes, :account_name => query.account_name).matches
|
133
|
+
end
|
134
|
+
|
135
|
+
def envs
|
136
|
+
@envs ||= EnvironmentResolver.new(@user, :environment_name => query.environment_name, :remotes => query.remotes).matches
|
137
|
+
end
|
138
|
+
|
139
|
+
def acc_envs
|
140
|
+
@acc_envs ||= EnvironmentResolver.new(@user, :environment_name => query.environment_name, :remotes => query.remotes, :account_name => query.account_name).matches
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|