smart_proxy_container_gateway 1.0.3 → 1.0.4
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.
- checksums.yaml +4 -4
- data/lib/smart_proxy_container_gateway/container_gateway_api.rb +86 -30
- data/lib/smart_proxy_container_gateway/container_gateway_main.rb +95 -23
- data/lib/smart_proxy_container_gateway/foreman_api.rb +13 -5
- data/lib/smart_proxy_container_gateway/sequel_migrations/003_authorization_reorg.rb +65 -0
- data/lib/smart_proxy_container_gateway/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c0467076082cd7138c8415f2915d72c9b69fea3af5909d9c396ed4b247f5009
|
4
|
+
data.tar.gz: 66a1866562b0fb767f1a7c94f40ad3572d89a67966269db711dfd2ca967449e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d5fb41b0be01a09736defddc0ec23ccf2ef81199700194cf46fbbc4afaaa6b2ffeebb79afa6119822a50f225ae7675ce1ea19946c49ff96882912c924a752b02
|
7
|
+
data.tar.gz: 7098744ff44801ee8051e3bdcdb87d2d7379eada25517552683023ef0cdd5932a52bdffd4b874e22b465cc7309754cc09b87a7d2fd36365be020a006c5f732cc
|
@@ -2,7 +2,6 @@ require 'sinatra'
|
|
2
2
|
require 'smart_proxy_container_gateway/container_gateway'
|
3
3
|
require 'smart_proxy_container_gateway/container_gateway_main'
|
4
4
|
require 'smart_proxy_container_gateway/foreman_api'
|
5
|
-
require 'sequel'
|
6
5
|
require 'sqlite3'
|
7
6
|
|
8
7
|
module Proxy
|
@@ -10,7 +9,7 @@ module Proxy
|
|
10
9
|
class Api < ::Sinatra::Base
|
11
10
|
include ::Proxy::Log
|
12
11
|
helpers ::Proxy::Helpers
|
13
|
-
|
12
|
+
helpers ::Sinatra::Authorization::Helpers
|
14
13
|
|
15
14
|
get '/v1/_ping/?' do
|
16
15
|
Proxy::ContainerGateway.ping
|
@@ -18,6 +17,7 @@ module Proxy
|
|
18
17
|
|
19
18
|
get '/v2/?' do
|
20
19
|
if auth_header.present? && (auth_header.unauthorized_token? || auth_header.valid_user_token?)
|
20
|
+
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
21
21
|
Proxy::ContainerGateway.ping
|
22
22
|
else
|
23
23
|
redirect_authorization_headers
|
@@ -26,31 +26,32 @@ module Proxy
|
|
26
26
|
end
|
27
27
|
|
28
28
|
get '/v2/:repository/manifests/:tag/?' do
|
29
|
-
|
30
|
-
redirect_authorization_headers
|
31
|
-
halt 401, "unauthorized"
|
32
|
-
end
|
29
|
+
handle_repo_auth(params, auth_header, request)
|
33
30
|
redirection_location = Proxy::ContainerGateway.manifests(params[:repository], params[:tag])
|
34
31
|
redirect to(redirection_location)
|
35
32
|
end
|
36
33
|
|
37
34
|
get '/v2/:repository/blobs/:digest/?' do
|
38
|
-
|
39
|
-
redirect_authorization_headers
|
40
|
-
halt 401, "unauthorized"
|
41
|
-
end
|
35
|
+
handle_repo_auth(params, auth_header, request)
|
42
36
|
redirection_location = Proxy::ContainerGateway.blobs(params[:repository], params[:digest])
|
43
37
|
redirect to(redirection_location)
|
44
38
|
end
|
45
39
|
|
46
40
|
get '/v1/search/?' do
|
47
41
|
# Checks for podman client and issues a 404 in that case. Podman
|
48
|
-
# examines the response from a /
|
42
|
+
# examines the response from a /v1/search request. If the result
|
49
43
|
# is a 4XX, it will then proceed with a request to /_catalog
|
50
44
|
if !request.env['HTTP_USER_AGENT'].nil? && request.env['HTTP_USER_AGENT'].downcase.include?('libpod')
|
51
45
|
halt 404, "not found"
|
52
46
|
end
|
53
47
|
|
48
|
+
if auth_header.present? && !auth_header.blank?
|
49
|
+
username = auth_header.v1_foreman_authorized_username
|
50
|
+
if username.nil?
|
51
|
+
halt 401, "unauthorized"
|
52
|
+
end
|
53
|
+
params[:user] = username
|
54
|
+
end
|
54
55
|
repositories = Proxy::ContainerGateway.v1_search(params)
|
55
56
|
|
56
57
|
content_type :json
|
@@ -58,13 +59,23 @@ module Proxy
|
|
58
59
|
end
|
59
60
|
|
60
61
|
get '/v2/_catalog/?' do
|
61
|
-
|
62
|
-
|
63
|
-
|
62
|
+
catalog = []
|
63
|
+
if auth_header.present?
|
64
|
+
if auth_header.unauthorized_token?
|
65
|
+
catalog = Proxy::ContainerGateway.catalog
|
66
|
+
elsif auth_header.valid_user_token?
|
67
|
+
catalog = Proxy::ContainerGateway.catalog(auth_header.user)
|
68
|
+
else
|
69
|
+
redirect_authorization_headers
|
70
|
+
halt 401, "unauthorized"
|
71
|
+
end
|
72
|
+
else
|
73
|
+
redirect_authorization_headers
|
74
|
+
halt 401, "unauthorized"
|
75
|
+
end
|
64
76
|
|
65
|
-
get '/v2/unauthenticated_repository_list/?' do
|
66
77
|
content_type :json
|
67
|
-
{ repositories:
|
78
|
+
{ repositories: catalog }.to_json
|
68
79
|
end
|
69
80
|
|
70
81
|
get '/v2/token' do
|
@@ -83,28 +94,56 @@ module Proxy
|
|
83
94
|
token_response_body = JSON.parse(token_response.body)
|
84
95
|
ContainerGateway.insert_token(request.params['account'], token_response_body['token'],
|
85
96
|
token_response_body['expires_at'])
|
97
|
+
|
98
|
+
repo_response = ForemanApi.new.fetch_user_repositories(auth_header.raw_header, request.params)
|
99
|
+
if repo_response.code.to_i != 200
|
100
|
+
halt repo_response.code.to_i, repo_response.body
|
101
|
+
else
|
102
|
+
ContainerGateway.update_user_repositories(request.params['account'],
|
103
|
+
JSON.parse(repo_response.body)['repositories'])
|
104
|
+
end
|
86
105
|
return token_response_body.to_json
|
87
106
|
end
|
88
107
|
end
|
89
108
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
109
|
+
get '/users/?' do
|
110
|
+
do_authorize_any
|
111
|
+
|
112
|
+
content_type :json
|
113
|
+
{ users: User.map(:name) }.to_json
|
114
|
+
end
|
115
|
+
|
116
|
+
put '/user_repository_mapping/?' do
|
117
|
+
do_authorize_any
|
118
|
+
|
119
|
+
ContainerGateway.update_user_repo_mapping(params)
|
120
|
+
{}
|
121
|
+
end
|
122
|
+
|
123
|
+
put '/repository_list/?' do
|
124
|
+
do_authorize_any
|
125
|
+
|
126
|
+
ContainerGateway.update_repository_list(params['repositories'])
|
127
|
+
{}
|
104
128
|
end
|
105
129
|
|
106
130
|
private
|
107
131
|
|
132
|
+
def handle_repo_auth(params, auth_header, request)
|
133
|
+
user_token_is_valid = false
|
134
|
+
# FIXME: Getting unauthenticated token here...
|
135
|
+
if auth_header.present? && auth_header.valid_user_token?
|
136
|
+
user_token_is_valid = true
|
137
|
+
username = auth_header.user.name
|
138
|
+
end
|
139
|
+
username = request.params['account'] if username.nil?
|
140
|
+
|
141
|
+
return if Proxy::ContainerGateway.authorized_for_repo?(params[:repository], user_token_is_valid, username)
|
142
|
+
|
143
|
+
redirect_authorization_headers
|
144
|
+
halt 401, "unauthorized"
|
145
|
+
end
|
146
|
+
|
108
147
|
def redirect_authorization_headers
|
109
148
|
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
110
149
|
response.headers['Www-Authenticate'] = "Bearer realm=\"https://#{request.host}/v2/token\"," \
|
@@ -123,6 +162,10 @@ module Proxy
|
|
123
162
|
@value = value || ''
|
124
163
|
end
|
125
164
|
|
165
|
+
def user
|
166
|
+
ContainerGateway.token_user(@value.split(' ')[1])
|
167
|
+
end
|
168
|
+
|
126
169
|
def valid_user_token?
|
127
170
|
token_auth? && ContainerGateway.valid_token?(@value.split(' ')[1])
|
128
171
|
end
|
@@ -146,6 +189,19 @@ module Proxy
|
|
146
189
|
def basic_auth?
|
147
190
|
@value.split(' ')[0] == 'Basic'
|
148
191
|
end
|
192
|
+
|
193
|
+
def blank?
|
194
|
+
Base64.decode64(@value.split(' ')[1]) == ':'
|
195
|
+
end
|
196
|
+
|
197
|
+
# A special case for the V1 API. Defer authentication to Foreman and return the username. `nil` if not authorized.
|
198
|
+
def v1_foreman_authorized_username
|
199
|
+
username = Base64.decode64(@value.split(' ')[1]).split(':')[0]
|
200
|
+
auth_response = ForemanApi.new.fetch_token(raw_header, { 'account' => username })
|
201
|
+
return username if auth_response.code.to_i == 200 && (JSON.parse(auth_response.body)['token'] != 'unauthenticated')
|
202
|
+
|
203
|
+
nil
|
204
|
+
end
|
149
205
|
end
|
150
206
|
end
|
151
207
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'uri'
|
3
3
|
require 'digest'
|
4
|
-
|
4
|
+
require 'sequel'
|
5
5
|
module Proxy
|
6
6
|
module ContainerGateway
|
7
7
|
extend ::Proxy::Util
|
8
8
|
extend ::Proxy::Log
|
9
9
|
|
10
10
|
class << self
|
11
|
+
Sequel.extension :migration, :core_extensions
|
11
12
|
def pulp_registry_request(uri)
|
12
13
|
http_client = Net::HTTP.new(uri.host, uri.port)
|
13
14
|
http_client.ca_file = pulp_ca
|
@@ -49,7 +50,8 @@ module Proxy
|
|
49
50
|
|
50
51
|
repo_count = 0
|
51
52
|
repositories = []
|
52
|
-
|
53
|
+
user = params[:user].nil? ? nil : User.find(name: params[:user])
|
54
|
+
Proxy::ContainerGateway.catalog(user).each do |repo_name|
|
53
55
|
break if repo_count >= params[:n]
|
54
56
|
|
55
57
|
if params[:q].nil? || params[:q] == "" || repo_name.include?(params[:q])
|
@@ -60,48 +62,103 @@ module Proxy
|
|
60
62
|
repositories
|
61
63
|
end
|
62
64
|
|
63
|
-
def catalog
|
64
|
-
|
65
|
+
def catalog(user = nil)
|
66
|
+
if user.nil?
|
67
|
+
unauthenticated_repos
|
68
|
+
else
|
69
|
+
(unauthenticated_repos + user.repositories_dataset.map(:name)).sort
|
70
|
+
end
|
65
71
|
end
|
66
72
|
|
67
73
|
def unauthenticated_repos
|
68
|
-
|
69
|
-
conn[:unauthenticated_repositories].order(:name).map(:name)
|
74
|
+
Repository.where(auth_required: false).order(:name).map(:name)
|
70
75
|
end
|
71
76
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
77
|
+
# Replaces the entire list of repositories
|
78
|
+
def update_repository_list(repo_list)
|
79
|
+
RepositoryUser.dataset.delete
|
80
|
+
Repository.dataset.delete
|
81
|
+
repo_list.each do |repo|
|
82
|
+
Repository.find_or_create(name: repo['repository'],
|
83
|
+
auth_required: repo['auth_required'].to_s.downcase == "true")
|
78
84
|
end
|
79
85
|
end
|
80
86
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
87
|
+
# Replaces the entire user-repo mapping for all logged-in users
|
88
|
+
def update_user_repo_mapping(user_repo_maps)
|
89
|
+
# Get hash map of all users and their repositories
|
90
|
+
# Ex: {"users"=> [{"admin"=>[{"repository"=>"repo", "auth_required"=>"true"}]}]}
|
91
|
+
# Go through list of repositories and add them to the DB
|
92
|
+
RepositoryUser.dataset.delete
|
93
|
+
user_repo_maps['users'].each do |user_repo_map|
|
94
|
+
user_repo_map.each do |user, repos|
|
95
|
+
repos.each do |repo|
|
96
|
+
found_repo = Repository.find(name: repo['repository'],
|
97
|
+
auth_required: repo['auth_required'].to_s.downcase == "true")
|
98
|
+
if found_repo.nil?
|
99
|
+
logger.warn("#{repo['repository']} does not exist in this smart proxy's environments")
|
100
|
+
elsif found_repo.auth_required
|
101
|
+
found_repo.add_user(User.find(name: user))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Replaces the user-repo mapping for a single user
|
109
|
+
def update_user_repositories(username, repositories)
|
110
|
+
user = User.where(name: username).first
|
111
|
+
user.remove_all_repositories
|
112
|
+
repositories.each do |repo_name|
|
113
|
+
found_repo = Repository.find(name: repo_name)
|
114
|
+
if found_repo.nil?
|
115
|
+
logger.warn("#{repo_name} does not exist in this smart proxy's environments")
|
116
|
+
elsif user.repositories_dataset.where(name: repo_name).first.nil? && found_repo.auth_required
|
117
|
+
user.add_repository(found_repo)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def authorized_for_repo?(repo_name, user_token_is_valid, username = nil)
|
123
|
+
repository = Repository.where(name: repo_name).first
|
124
|
+
|
125
|
+
# Repository doesn't exist
|
126
|
+
return false if repository.nil?
|
127
|
+
|
128
|
+
# Repository doesn't require auth
|
129
|
+
return true unless repository.auth_required
|
130
|
+
|
131
|
+
if username && user_token_is_valid && repository.auth_required
|
132
|
+
# User is logged in and has access to the repository
|
133
|
+
user = User.find(name: username)
|
134
|
+
return !user.repositories_dataset.where(name: repo_name).first.nil?
|
135
|
+
end
|
136
|
+
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def token_user(token)
|
141
|
+
User[AuthenticationToken.find(token_checksum: Digest::SHA256.hexdigest(token)).user_id]
|
85
142
|
end
|
86
143
|
|
87
144
|
def valid_token?(token)
|
88
|
-
|
89
|
-
tokens.where(token_checksum: Digest::SHA256.hexdigest(token)).where do
|
145
|
+
AuthenticationToken.where(token_checksum: Digest::SHA256.hexdigest(token)).where do
|
90
146
|
expire_at > Sequel::CURRENT_TIMESTAMP
|
91
147
|
end.count.positive?
|
92
148
|
end
|
93
149
|
|
94
150
|
def insert_token(username, token, expire_at_string, clear_expired_tokens: true)
|
95
|
-
tokens = initialize_db[:authentication_tokens]
|
96
151
|
checksum = Digest::SHA256.hexdigest(token)
|
152
|
+
user = User.find_or_create(name: username)
|
97
153
|
|
98
|
-
|
99
|
-
|
100
|
-
|
154
|
+
AuthenticationToken.where(:token_checksum => checksum).delete
|
155
|
+
AuthenticationToken.create(token_checksum: checksum, expire_at: expire_at_string.to_s, user_id: user.id)
|
156
|
+
AuthenticationToken.where { expire_at < Sequel::CURRENT_TIMESTAMP }.delete if clear_expired_tokens
|
101
157
|
end
|
102
158
|
|
103
159
|
def initialize_db
|
104
|
-
|
160
|
+
file_path = Proxy::ContainerGateway::Plugin.settings.sqlite_db_path
|
161
|
+
conn = Sequel.connect("sqlite://#{file_path}")
|
105
162
|
container_gateway_path = $LOAD_PATH.detect { |path| path.include? 'smart_proxy_container_gateway' }
|
106
163
|
begin
|
107
164
|
Sequel::Migrator.check_current(conn, "#{container_gateway_path}/smart_proxy_container_gateway/sequel_migrations")
|
@@ -131,5 +188,20 @@ module Proxy
|
|
131
188
|
)
|
132
189
|
end
|
133
190
|
end
|
191
|
+
|
192
|
+
class Repository < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories])
|
193
|
+
many_to_many :users
|
194
|
+
end
|
195
|
+
|
196
|
+
class User < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:users])
|
197
|
+
many_to_many :repositories
|
198
|
+
one_to_many :authentication_tokens
|
199
|
+
end
|
200
|
+
|
201
|
+
class RepositoryUser < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories_users]); end
|
202
|
+
|
203
|
+
class AuthenticationToken < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:authentication_tokens])
|
204
|
+
many_to_one :users
|
205
|
+
end
|
134
206
|
end
|
135
207
|
end
|
@@ -3,15 +3,15 @@ require 'uri'
|
|
3
3
|
module Proxy
|
4
4
|
module ContainerGateway
|
5
5
|
class ForemanApi
|
6
|
-
def
|
7
|
-
|
8
|
-
|
6
|
+
def registry_request(auth_header, params, suffix)
|
7
|
+
uri = URI.join(Proxy::SETTINGS.foreman_url, Proxy::ContainerGateway::Plugin.settings.katello_registry_path, suffix)
|
8
|
+
uri.query = process_params(params)
|
9
9
|
|
10
|
-
req = Net::HTTP::Get.new(
|
10
|
+
req = Net::HTTP::Get.new(uri)
|
11
11
|
req.add_field('Authorization', auth_header)
|
12
12
|
req.add_field('Accept', 'application/json')
|
13
13
|
req.content_type = 'application/json'
|
14
|
-
http = Net::HTTP.new(
|
14
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
15
15
|
http.use_ssl = true
|
16
16
|
|
17
17
|
http.request(req)
|
@@ -21,6 +21,14 @@ module Proxy
|
|
21
21
|
params = params_in.slice('scope', 'account').compact
|
22
22
|
URI.encode_www_form(params)
|
23
23
|
end
|
24
|
+
|
25
|
+
def fetch_token(auth_header, params)
|
26
|
+
registry_request(auth_header, params, 'token')
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch_user_repositories(auth_header, params)
|
30
|
+
registry_request(auth_header, params, '_catalog')
|
31
|
+
end
|
24
32
|
end
|
25
33
|
end
|
26
34
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
# TODO: Should I be migrating the existing data?
|
4
|
+
|
5
|
+
create_table(:repositories) do
|
6
|
+
primary_key :id
|
7
|
+
String :name, null: false
|
8
|
+
Boolean :auth_required, null: false
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table(:users) do
|
12
|
+
primary_key :id
|
13
|
+
String :name, null: false
|
14
|
+
end
|
15
|
+
|
16
|
+
# Migrate unauthenticated_repositories to the new repositories table (TODO: can I select `false` like that?)
|
17
|
+
from(:repositories).insert(%i[name auth_required],
|
18
|
+
from(:unauthenticated_repositories).select(:name, false))
|
19
|
+
|
20
|
+
# Migrate names from authentication_tokens to the new users table
|
21
|
+
from(:users).insert([:name], from(:authentication_tokens).select(:username))
|
22
|
+
|
23
|
+
alter_table(:authentication_tokens) do
|
24
|
+
add_foreign_key :user_id, :users
|
25
|
+
end
|
26
|
+
|
27
|
+
# Populate the new user_id foreign key for all authentication_tokens
|
28
|
+
from(:authentication_tokens).insert([:user_id],
|
29
|
+
from(:users).select(:id).where(name: self[:authentication_tokens][:username]))
|
30
|
+
|
31
|
+
alter_table(:authentication_tokens) do
|
32
|
+
drop_column :username
|
33
|
+
end
|
34
|
+
|
35
|
+
create_join_table(repository_id: :repositories, user_id: :users)
|
36
|
+
drop_table :unauthenticated_repositories
|
37
|
+
end
|
38
|
+
|
39
|
+
down do
|
40
|
+
alter_table(:authentication_tokens) do
|
41
|
+
add_column :username, String
|
42
|
+
end
|
43
|
+
|
44
|
+
# Repopulate the name column with usernames
|
45
|
+
from(:authentication_tokens).update(username:
|
46
|
+
from(:users).select(:name).where(id: self[:authentication_tokens][:user_id]))
|
47
|
+
|
48
|
+
alter_table(:authentication_tokens) do
|
49
|
+
drop_foreign_key :user_id
|
50
|
+
end
|
51
|
+
|
52
|
+
create_table(:unauthenticated_repositories) do
|
53
|
+
primary_key :id
|
54
|
+
String :name, null: false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Repopulate the unauthenticated_repositories table
|
58
|
+
from(:unauthenticated_repositories).insert([:username],
|
59
|
+
from(:repositories).select(:name).where(auth_required: true))
|
60
|
+
|
61
|
+
drop_table :users
|
62
|
+
drop_table :repositories
|
63
|
+
drop_join_table(repository_id: :repositories, user_id: :users)
|
64
|
+
end
|
65
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_container_gateway
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ian Ballou
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-04-
|
11
|
+
date: 2021-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -57,6 +57,7 @@ files:
|
|
57
57
|
- lib/smart_proxy_container_gateway/foreman_api.rb
|
58
58
|
- lib/smart_proxy_container_gateway/sequel_migrations/001_initial.rb
|
59
59
|
- lib/smart_proxy_container_gateway/sequel_migrations/002_auth_tokens.rb
|
60
|
+
- lib/smart_proxy_container_gateway/sequel_migrations/003_authorization_reorg.rb
|
60
61
|
- lib/smart_proxy_container_gateway/version.rb
|
61
62
|
- settings.d/container_gateway.yml.example
|
62
63
|
homepage: http://github.com/ianballou/smart_proxy_container_gateway
|