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.
@@ -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