google-cloud-gemserver 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,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