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.
- checksums.yaml +7 -0
- data/.yardopts +7 -0
- data/CONTRIBUTING.md +49 -0
- data/LICENSE +202 -0
- data/README.md +179 -0
- data/bin/console +7 -0
- data/bin/google-cloud-gemserver +5 -0
- data/bin/setup +6 -0
- data/lib/google/cloud/gemserver.rb +33 -0
- data/lib/google/cloud/gemserver/authentication.rb +254 -0
- data/lib/google/cloud/gemserver/backend.rb +33 -0
- data/lib/google/cloud/gemserver/backend/gemstash_server.rb +60 -0
- data/lib/google/cloud/gemserver/backend/key.rb +152 -0
- data/lib/google/cloud/gemserver/backend/stats.rb +149 -0
- data/lib/google/cloud/gemserver/backend/storage_sync.rb +186 -0
- data/lib/google/cloud/gemserver/cli.rb +174 -0
- data/lib/google/cloud/gemserver/cli/cloud_sql.rb +196 -0
- data/lib/google/cloud/gemserver/cli/project.rb +143 -0
- data/lib/google/cloud/gemserver/cli/request.rb +130 -0
- data/lib/google/cloud/gemserver/cli/server.rb +337 -0
- data/lib/google/cloud/gemserver/configuration.rb +505 -0
- data/lib/google/cloud/gemserver/gcs.rb +171 -0
- data/lib/google/cloud/gemserver/version.rb +23 -0
- data/lib/patched/configuration.rb +29 -0
- data/lib/patched/dependencies.rb +33 -0
- data/lib/patched/env.rb +42 -0
- data/lib/patched/gem_pusher.rb +28 -0
- data/lib/patched/gem_yanker.rb +28 -0
- data/lib/patched/storage.rb +37 -0
- data/lib/patched/web.rb +64 -0
- metadata +271 -0
data/bin/setup
ADDED
@@ -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
|