google-cloud-gemserver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "google/cloud/gemserver/cli"
4
+
5
+ Google::Cloud::Gemserver::CLI.start ARGV
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,33 @@
1
+ # Copyright 2017 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Google
16
+ module Cloud
17
+ ##
18
+ #
19
+ # # Gemserver
20
+ #
21
+ # Gemserver provides a command line interface to create, manage, and deploy
22
+ # a gemserver to a Google Cloud Platform project.
23
+ #
24
+ module Gemserver
25
+ autoload :CLI, "google/cloud/gemserver/cli"
26
+ autoload :VERSION, "google/cloud/gemserver/version"
27
+ autoload :Configuration, "google/cloud/gemserver/configuration"
28
+ autoload :GCS, "google/cloud/gemserver/gcs"
29
+ autoload :Authentication, "google/cloud/gemserver/authentication"
30
+ autoload :Backend, "google/cloud/gemserver/backend"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,254 @@
1
+ # Copyright 2017 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "google/cloud/gemserver"
16
+ require "json"
17
+ require "googleauth"
18
+ require "net/http"
19
+ require "uri"
20
+
21
+ module Google
22
+ module Cloud
23
+ module Gemserver
24
+ ##
25
+ #
26
+ # # Authentication
27
+ #
28
+ # Manages the permissions of the currently logged in user with the gcloud
29
+ # sdk.
30
+ #
31
+ class Authentication
32
+
33
+ ##
34
+ # The project id of the Google App Engine project the gemserver was
35
+ # deployed to.
36
+ # @return [String]
37
+ attr_accessor :proj
38
+
39
+ ##
40
+ # Creates the Authentication object and sets the project id field.
41
+ def initialize
42
+ @proj = Configuration.new[:proj_id]
43
+ end
44
+
45
+ ##
46
+ # Checks if the currently logged in user can modify the gemserver
47
+ # i.e. create keys.
48
+ #
49
+ # @return [Boolean]
50
+ def can_modify?
51
+ user = curr_user
52
+ owners.each do |owner|
53
+ return true if extract_account(owner) == user
54
+ end
55
+ editors.each do |editor|
56
+ return true if extract_account(editor) == user
57
+ end
58
+ puts "You are either not authenticated with gcloud or lack access" \
59
+ " to the gemserver."
60
+ false
61
+ end
62
+
63
+ ##
64
+ # Generates an access token from a user authenticated by gcloud.
65
+ #
66
+ # @return [String]
67
+ def access_token
68
+ return unless can_modify?
69
+ scope = ["https://www.googleapis.com/auth/cloud-platform"]
70
+ auth = Google::Auth.get_application_default scope
71
+ auth.fetch_access_token!
72
+ end
73
+
74
+ ##
75
+ # @private Implicitly checks if the account that generated the token
76
+ # has edit permissions on the Google Cloud Platform project by issuing
77
+ # a redundant update to the project (update to original settings).
78
+ #
79
+ # @param [String] The authentication token generated from gcloud.
80
+ #
81
+ # @return [Boolean]
82
+ def validate_token auth_header
83
+ token = auth_header.split.drop(1)[0]
84
+
85
+ appengine_url = "https://appengine.googleapis.com"
86
+ endpoint = "/v1/apps/#{@proj}/services/default?updateMask=split"
87
+ version = appengine_version token
88
+ split = {
89
+ "split" => {
90
+ "allocations" => {
91
+ version.to_s => 1
92
+ }
93
+ }
94
+ }
95
+ res = send_req appengine_url, endpoint, Net::HTTP::Patch, token, split
96
+ if check_status(res)
97
+ op = JSON.parse(res.body)["name"]
98
+ wait_for_op appengine_url, "/v1/#{op}", token
99
+ true
100
+ else
101
+ false
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ ##
108
+ # @private Fetches the latest version of the deployed Google App Engine
109
+ # instance running the gemserver (default service only).
110
+ #
111
+ # @param [String] The authentication token generated from gcloud.
112
+ #
113
+ # @return [String]
114
+ def appengine_version token
115
+ appengine_url = "https://appengine.googleapis.com"
116
+ path = "/v1/apps/#{@proj}/services/default"
117
+ res = send_req appengine_url, path, Net::HTTP::Get, token
118
+
119
+ fail "Unauthorized" unless check_status(res)
120
+
121
+ JSON.parse(res.body)["split"]["allocations"].first[0]
122
+ end
123
+
124
+ ##
125
+ # @private Sends a request to a given URL with given parameters.
126
+ #
127
+ # @param [String] dom The protocol + domain name of the request.
128
+ #
129
+ # @param [String] path The path of the URL.
130
+ #
131
+ # @param [Net::HTTP] type The type of request to be made.
132
+ #
133
+ # @param [String] token The authentication token used in the header.
134
+ #
135
+ # @param [Hash] params Additional parameters send in the request body.
136
+ #
137
+ # @return [Net::HTTPResponse]
138
+ def send_req dom, path, type, token, params = nil
139
+ uri = URI.parse dom
140
+ http = Net::HTTP.new uri.host, uri.port
141
+ http.use_ssl = true if dom.include? "https"
142
+
143
+ req = type.new path
144
+ req["Authorization"] = Signet::OAuth2.generate_bearer_authorization_header token
145
+ unless type == Net::HTTP::Get
146
+ if params
147
+ req["Content-Type"] = "application/json"
148
+ req.body = params.to_json
149
+ end
150
+ end
151
+ http.request req
152
+ end
153
+
154
+ ##
155
+ # @private Waits for a project update operation to complete.
156
+ #
157
+ # @param [String] dom The domain and protocol of the request.
158
+ #
159
+ # @param [String] path The path of the request containing the operation
160
+ # ID.
161
+ #
162
+ # @param [String] token The authorization token in the request.
163
+ #
164
+ # @param [Integer] timeout The length of time the operation is polled.
165
+ def wait_for_op dom, path, token, timeout = 60
166
+ start = Time.now
167
+ loop do
168
+ if Time.now - start > timeout
169
+ fail "Operation at #{path} failed to complete in time"
170
+ else
171
+ res = send_req dom, path, Net::HTTP::Get, token
172
+ if JSON.parse(res.body)["done"] == true
173
+ break
174
+ end
175
+ sleep 1
176
+ end
177
+ end
178
+ end
179
+
180
+ ##
181
+ # @private Checks if a request response matches a given status code.
182
+ #
183
+ # @param [Net::HTTPResponse] reponse The response from a request.
184
+ #
185
+ # @param [Integer] code The desired response code.
186
+ #
187
+ # @return [Boolean]
188
+ def check_status response, code = 200
189
+ response.code.to_i == code
190
+ end
191
+
192
+ ##
193
+ # @private Fetches the members with a specific role that have access
194
+ # to the Google App Engine project the gemserver was deployed to.
195
+ #
196
+ # @return [Array]
197
+ def members type
198
+ yml = YAML.load(run_cmd "gcloud projects get-iam-policy #{@proj}")
199
+ yml["bindings"].select do |member_set|
200
+ member_set["role"] == type
201
+ end[0]["members"]
202
+ end
203
+
204
+ ##
205
+ # @private Fetches members with a role of editor that can access the
206
+ # gemserver.
207
+ #
208
+ # @return [Array]
209
+ def editors
210
+ members "roles/editor"
211
+ end
212
+
213
+ ##
214
+ # @private Fetches members with a role of owner that can access the
215
+ # gemserver.
216
+ #
217
+ # @return [Array]
218
+ def owners
219
+ members "roles/owner"
220
+ end
221
+
222
+ ##
223
+ # @private Fetches the active account of the currently logged in user.
224
+ #
225
+ # @return [String]
226
+ def curr_user
227
+ raw = run_cmd "gcloud auth list --format json"
228
+ JSON.load(raw).map do |i|
229
+ return i["account"] if i["status"] == "ACTIVE"
230
+ end
231
+ abort "You are not authenticated with gcloud"
232
+ end
233
+
234
+ ##
235
+ # @private Parses a gcloud "member" and removes the account prefix.
236
+ #
237
+ # @param [String] acc The member the account is extracted from.
238
+ #
239
+ # @return [String]
240
+ def extract_account acc
241
+ acc[acc.index(":") + 1 .. acc.size]
242
+ end
243
+
244
+ ##
245
+ # @private Runs a given command on the local machine.
246
+ #
247
+ # @param [String] args The command to be run.
248
+ def run_cmd args
249
+ `#{args}`
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,33 @@
1
+ # Copyright 2017 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Google
16
+ module Cloud
17
+ module Gemserver
18
+ ##
19
+ #
20
+ # # Backend
21
+ #
22
+ # Contains services that run on Google App Engine directly leveraging
23
+ # tools such as Cloud SQL proxy.
24
+ #
25
+ module Backend
26
+ autoload :GemstashServer, "google/cloud/gemserver/backend/gemstash_server"
27
+ autoload :Key, "google/cloud/gemserver/backend/key"
28
+ autoload :Stats, "google/cloud/gemserver/backend/stats"
29
+ autoload :StorageSync, "google/cloud/gemserver/backend/storage_sync"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # Copyright 2017 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # @https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "patched/configuration"
16
+ require "patched/dependencies"
17
+ require "patched/env"
18
+ require "patched/gem_pusher"
19
+ require "patched/gem_yanker"
20
+ require "patched/storage"
21
+ require "patched/web"
22
+ require "gemstash"
23
+
24
+ module Google
25
+ module Cloud
26
+ module Gemserver
27
+ module Backend
28
+ ##
29
+ #
30
+ # # GemstashServer
31
+ #
32
+ # The class that runs gemstash specific commands and starts the gemstash
33
+ # server. Parts of gemstash are monkey-patched with lib/patched for
34
+ # compatibility with Google Cloud Platform services such as Cloud Storage
35
+ # and Cloud SQL.
36
+ module GemstashServer
37
+
38
+ ##
39
+ # Runs a given command through the gemstash gem.
40
+ #
41
+ # @param [String] args The argument passed to gemstash.
42
+ def self.start args
43
+ Gemstash::CLI.start args
44
+ end
45
+
46
+ ##
47
+ # Fetches the gemstash environment given a configuration file.
48
+ #
49
+ # @param [String] config_path The path to the configuration file.
50
+ #
51
+ # @return [Gemstash::Env]
52
+ def self.env config_path
53
+ config = Gemstash::Configuration.new file: config_path
54
+ Gemstash::Env.new config
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,152 @@
1
+ # Copyright 2017 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "google/cloud/gemserver"
16
+ require "stringio"
17
+ require "fileutils"
18
+ require "yaml"
19
+
20
+ module Google
21
+ module Cloud
22
+ module Gemserver
23
+ module Backend
24
+ ##
25
+ # # Key
26
+ #
27
+ # Manages the creation and deletion of a key used to push gems to the
28
+ # gemserver and download them.
29
+ #
30
+ class Key
31
+ ##
32
+ # A mapping from gemserver permissions to gemstash permissions.
33
+ MAPPING = {
34
+ "write" => %w[push yank],
35
+ "read" => %w[fetch]
36
+ }.freeze
37
+
38
+ ##
39
+ # Aliases for read and write permissions.
40
+ ALL = ["both", "all", "", nil].freeze
41
+
42
+ ##
43
+ # Path to the credentials file checked when pushing gems to the gem
44
+ # server (or any endpoint).
45
+ GEM_CREDS = File.expand_path("~/.gem")
46
+
47
+ ##
48
+ # The length of a key generated by gemstash.
49
+ KEY_LENGTH = 32
50
+
51
+ ##
52
+ # Creates a key with given permissions.
53
+ #
54
+ # @param permissions [String] The permissions for a key. Optional.
55
+ #
56
+ # @return [String]
57
+ def self.create_key permissions = nil
58
+ mapped = map_perms permissions
59
+ args = base_args.concat mapped
60
+ output = capture_stdout { GemstashServer.start args }
61
+ key = parse_key(output)
62
+ puts "Created key: #{key}"
63
+ key
64
+ end
65
+
66
+ ##
67
+ # Deletes a given key.
68
+ #
69
+ # @param [String] key The key to delete.
70
+ def self.delete_key key
71
+ args = [
72
+ "--remove",
73
+ "--key=#{key}"
74
+ ]
75
+ GemstashServer.start base_args.concat(args)
76
+ puts "Deleted key: #{key}"
77
+ true
78
+ end
79
+
80
+ ##
81
+ # @private Maps read/write permissions to the permissions the
82
+ # gemstash gem uses.
83
+ #
84
+ # @param [String] perms The permissions to be mapped.
85
+ def self.map_perms perms
86
+ if perms == "write"
87
+ MAPPING["write"]
88
+ elsif ALL.include? perms
89
+ MAPPING["write"] + MAPPING["read"]
90
+ else
91
+ MAPPING["read"]
92
+ end
93
+ end
94
+
95
+ ##
96
+ # @private Temporarily routes stdout to a temporary variable such
97
+ # that stdout from gemstash is captured.
98
+ #
99
+ # @return [String]
100
+ def self.capture_stdout
101
+ old_stdout = $stdout
102
+ $stdout = StringIO.new
103
+ yield
104
+ $stdout.string
105
+ ensure
106
+ $stdout = old_stdout
107
+ end
108
+
109
+ ##
110
+ # @private The arguments passed to every gemstash key generation
111
+ # command.
112
+ #
113
+ # @return [Array]
114
+ def self.base_args
115
+ [
116
+ "authorize",
117
+ "--config-file=#{Google::Cloud::Gemserver::Configuration.new.config_path}"
118
+ ]
119
+ end
120
+
121
+ ##
122
+ # @private Outputs important information to the user on how they
123
+ # should set up their keys so pushing/installing gems works as
124
+ # intended.
125
+ def self.output_key_info
126
+ puts "Note: remember to add this key to ~/.gem/credentials" \
127
+ " so that you are able to push gems to the gemserver."
128
+ puts "Note: remember to add this key to your bundle config so " \
129
+ "that `bundle install` works for private gems (bundle config" \
130
+ " http://my-gemserver.appspot.com/private/ my-key"
131
+ end
132
+
133
+ ##
134
+ # Parses the key from output generated from the corresponding key
135
+ # creation command in gemstash.
136
+ #
137
+ # @param [String] output The output to parse.
138
+ #
139
+ # @return [String]
140
+ def self.parse_key output
141
+ i = output.index(":")
142
+ output[i+2..i+2+KEY_LENGTH].chomp
143
+ end
144
+
145
+ private_class_method :map_perms
146
+ private_class_method :capture_stdout
147
+ private_class_method :parse_key
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end