ey_resolver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ coverage
3
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'dm-core'
7
+ gem 'dm-migrations'
8
+ gem 'dm-aggregates'
9
+ gem 'dm-sqlite-adapter'
10
+ end
11
+
12
+ group :coverage do
13
+ gem 'simplecov', :require => false
14
+ end
@@ -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
+
@@ -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
@@ -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
@@ -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