shelly 0.0.28 → 0.0.29
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.
- data/lib/shelly/app.rb +31 -2
- data/lib/shelly/cli/command.rb +1 -0
- data/lib/shelly/cli/deploys.rb +34 -0
- data/lib/shelly/cli/main.rb +103 -17
- data/lib/shelly/cli/user.rb +19 -12
- data/lib/shelly/client.rb +25 -9
- data/lib/shelly/cloudfile.rb +0 -18
- data/lib/shelly/helpers.rb +43 -11
- data/lib/shelly/version.rb +1 -1
- data/lib/thor/basic.rb +9 -0
- data/lib/thor/options.rb +0 -1
- data/spec/helpers.rb +8 -0
- data/spec/shelly/app_spec.rb +29 -3
- data/spec/shelly/cli/deploys_spec.rb +73 -0
- data/spec/shelly/cli/main_spec.rb +298 -19
- data/spec/shelly/cli/user_spec.rb +17 -17
- data/spec/shelly/client_spec.rb +52 -6
- data/spec/shelly/cloudfile_spec.rb +0 -13
- metadata +151 -132
data/lib/shelly/app.rb
CHANGED
@@ -8,11 +8,19 @@ module Shelly
|
|
8
8
|
attr_accessor :code_name, :databases, :ruby_version, :environment,
|
9
9
|
:git_url, :domains
|
10
10
|
|
11
|
+
def initialize(code_name = nil)
|
12
|
+
self.code_name = code_name
|
13
|
+
end
|
14
|
+
|
11
15
|
def add_git_remote
|
12
16
|
system("git remote rm production > /dev/null 2>&1")
|
13
17
|
system("git remote add production #{git_url}")
|
14
18
|
end
|
15
19
|
|
20
|
+
def remove_git_remote
|
21
|
+
system("git remote rm production > /dev/null 2>&1")
|
22
|
+
end
|
23
|
+
|
16
24
|
def generate_cloudfile
|
17
25
|
@email = current_user.email
|
18
26
|
template = File.read(cloudfile_template_path)
|
@@ -33,11 +41,28 @@ module Shelly
|
|
33
41
|
self.environment = response["environment"]
|
34
42
|
end
|
35
43
|
|
44
|
+
def delete(code_name)
|
45
|
+
shelly.delete_app(code_name)
|
46
|
+
#response = shelly.delete_app(code_name)
|
47
|
+
end
|
48
|
+
|
36
49
|
def create_cloudfile
|
37
50
|
content = generate_cloudfile
|
38
51
|
File.open(cloudfile_path, "a+") { |f| f << content }
|
39
52
|
end
|
40
53
|
|
54
|
+
def logs
|
55
|
+
shelly.cloud_logs(self.code_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def start
|
59
|
+
shelly.start_cloud(self.code_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def stop
|
63
|
+
shelly.stop_cloud(self.code_name)
|
64
|
+
end
|
65
|
+
|
41
66
|
def cloudfile_path
|
42
67
|
File.join(Dir.pwd, "Cloudfile")
|
43
68
|
end
|
@@ -46,8 +71,12 @@ module Shelly
|
|
46
71
|
File.basename(Dir.pwd)
|
47
72
|
end
|
48
73
|
|
49
|
-
def
|
50
|
-
shelly.
|
74
|
+
def ips
|
75
|
+
shelly.app_ips(self.code_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def users
|
79
|
+
shelly.app_users(self.code_name)
|
51
80
|
end
|
52
81
|
|
53
82
|
def open_billing_page
|
data/lib/shelly/cli/command.rb
CHANGED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "shelly/cli/command"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
module Shelly
|
5
|
+
module CLI
|
6
|
+
class Deploys < Command
|
7
|
+
namespace :deploys
|
8
|
+
include Helpers
|
9
|
+
|
10
|
+
desc "list", "Lists deploy logs"
|
11
|
+
def list(cloud = nil)
|
12
|
+
logged_in?
|
13
|
+
say_error "No Cloudfile found" unless Cloudfile.present?
|
14
|
+
multiple_clouds(cloud, "deploy list", "Select cloud to view deploy logs using:")
|
15
|
+
logs = @app.logs
|
16
|
+
unless logs.empty?
|
17
|
+
say "Available deploy logs", :green
|
18
|
+
logs.each do |log|
|
19
|
+
log["failed"] ? say(" * #{log["created_at"]} (failed)") : say(" * #{log["created_at"]}")
|
20
|
+
end
|
21
|
+
else
|
22
|
+
say "No deploy logs available"
|
23
|
+
end
|
24
|
+
rescue Client::APIError => e
|
25
|
+
if e.unauthorized?
|
26
|
+
say_error "You have no access to '#{@app.code_name}' cloud defined in Cloudfile"
|
27
|
+
else
|
28
|
+
say_error e.message
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/shelly/cli/main.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "shelly/cli/command"
|
2
2
|
require "shelly/cli/user"
|
3
|
+
require "shelly/cli/deploys"
|
3
4
|
|
4
5
|
module Shelly
|
5
6
|
module CLI
|
@@ -7,6 +8,7 @@ module Shelly
|
|
7
8
|
include Thor::Actions
|
8
9
|
include Helpers
|
9
10
|
register(User, "user", "user <command>", "Manages users using this cloud")
|
11
|
+
register(Deploys, "deploys", "deploys <command>", "View cloud deploy logs")
|
10
12
|
check_unknown_options!
|
11
13
|
|
12
14
|
map %w(-v --version) => :version
|
@@ -17,11 +19,11 @@ module Shelly
|
|
17
19
|
|
18
20
|
desc "register [EMAIL]", "Registers new user account on Shelly Cloud"
|
19
21
|
def register(email = nil)
|
20
|
-
|
21
|
-
|
22
|
+
user = Shelly::User.new
|
23
|
+
user.ssh_key_registered?
|
22
24
|
say "Registering with email: #{email}" if email
|
23
|
-
|
24
|
-
|
25
|
+
user.email = (email || ask_for_email)
|
26
|
+
user.password = ask_for_password
|
25
27
|
user.register
|
26
28
|
if user.ssh_key_exists?
|
27
29
|
say "Uploading your public SSH key from #{user.ssh_key_path}"
|
@@ -44,16 +46,19 @@ module Shelly
|
|
44
46
|
|
45
47
|
desc "login [EMAIL]", "Logs user in to Shelly Cloud"
|
46
48
|
def login(email = nil)
|
47
|
-
user = Shelly::User.new
|
49
|
+
user = Shelly::User.new
|
50
|
+
raise Errno::ENOENT, user.ssh_key_path unless user.ssh_key_exists?
|
51
|
+
user.email = email || ask_for_email
|
52
|
+
user.password = ask_for_password(:with_confirmation => false)
|
48
53
|
user.login
|
49
54
|
say "Login successful"
|
55
|
+
# FIXME: remove conflict boolean, move it to rescue block
|
50
56
|
begin user.upload_ssh_key
|
51
|
-
|
57
|
+
conflict = false
|
52
58
|
rescue RestClient::Conflict
|
53
59
|
conflict = true
|
54
60
|
end
|
55
61
|
say "Uploading your public SSH key" if conflict == false
|
56
|
-
email = nil
|
57
62
|
list
|
58
63
|
rescue Client::APIError => e
|
59
64
|
if e.validation?
|
@@ -65,6 +70,9 @@ module Shelly
|
|
65
70
|
say_error "#{e.url}", :with_exit => false
|
66
71
|
end
|
67
72
|
exit 1
|
73
|
+
rescue Errno::ENOENT => e
|
74
|
+
say_error e, :with_exit => false
|
75
|
+
say_error "Use ssh-keygen to generate ssh key pair"
|
68
76
|
end
|
69
77
|
|
70
78
|
method_option "code-name", :type => :string, :aliases => "-c",
|
@@ -131,21 +139,100 @@ module Shelly
|
|
131
139
|
def ip
|
132
140
|
say_error "Must be run inside your project git repository" unless App.inside_git_repository?
|
133
141
|
say_error "No Cloudfile found" unless Cloudfile.present?
|
134
|
-
@cloudfile =
|
135
|
-
@cloudfile.
|
136
|
-
|
137
|
-
|
138
|
-
|
142
|
+
@cloudfile = Cloudfile.new
|
143
|
+
@cloudfile.clouds.each do |cloud|
|
144
|
+
begin
|
145
|
+
@app = App.new(cloud)
|
146
|
+
say "Cloud #{cloud}:", :green
|
147
|
+
ips = @app.ips
|
148
|
+
print_wrapped "Web server IP: #{ips['web_server_ip']}", :ident => 2
|
149
|
+
print_wrapped "Mail server IP: #{ips['mail_server_ip']}", :ident => 2
|
150
|
+
rescue Client::APIError => e
|
151
|
+
if e.unauthorized?
|
152
|
+
say_error "You have no access to '#{cloud}' cloud defined in Cloudfile", :with_exit => false
|
153
|
+
else
|
154
|
+
say_error e.message, :with_exit => false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
desc "start [CODE-NAME]", "Starts specific cloud"
|
161
|
+
def start(cloud = nil)
|
162
|
+
logged_in?
|
163
|
+
say_error "No Cloudfile found" unless Cloudfile.present?
|
164
|
+
multiple_clouds(cloud, "start", "Select which to start using:")
|
165
|
+
@app.start
|
166
|
+
say "Starting cloud #{@app.code_name}. Check status with:", :green
|
167
|
+
say " shelly list"
|
168
|
+
rescue RestClient::Conflict => e
|
169
|
+
response = JSON.parse(e.response)
|
170
|
+
case response['state']
|
171
|
+
when "running"
|
172
|
+
say_error "Not starting: cloud '#{@app.code_name}' is already running"
|
173
|
+
when "deploying", "configuring"
|
174
|
+
say_error "Not starting: cloud '#{@app.code_name}' is currently deploying"
|
175
|
+
when "no_code"
|
176
|
+
say_error "Not starting: no source code provided", :with_exit => false
|
177
|
+
say_error "Push source code using:", :with_exit => false
|
178
|
+
say " git push production master"
|
179
|
+
when "deploy_failed", "configuration_failed"
|
180
|
+
say_error "Not starting: deployment failed", :with_exit => false
|
181
|
+
say_error "Support has been notified", :with_exit => false
|
182
|
+
say_error "See #{response['link']} for reasons of failure"
|
183
|
+
when "no_billing"
|
184
|
+
say_error "Please fill in billing details to start foo-production. Opening browser.", :with_exit => false
|
185
|
+
@app.open_billing_page
|
139
186
|
end
|
187
|
+
exit 1
|
140
188
|
rescue Client::APIError => e
|
141
189
|
if e.unauthorized?
|
142
|
-
|
143
|
-
exit 1
|
144
|
-
else
|
145
|
-
say_error e.message
|
190
|
+
say_error "You have no access to '#{@app.code_name}' cloud defined in Cloudfile"
|
146
191
|
end
|
147
192
|
end
|
148
193
|
|
194
|
+
desc "stop [CODE-NAME]", "Stops specific cloud"
|
195
|
+
def stop(cloud = nil)
|
196
|
+
logged_in?
|
197
|
+
say_error "No Cloudfile found" unless Cloudfile.present?
|
198
|
+
multiple_clouds(cloud, "stop", "Select which to stop using:")
|
199
|
+
@app.stop
|
200
|
+
say "Cloud '#{@app.code_name}' stopped"
|
201
|
+
rescue Client::APIError => e
|
202
|
+
if e.unauthorized?
|
203
|
+
say_error "You have no access to '#{@app.code_name}' cloud defined in Cloudfile"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
desc "delete CODE-NAME", "Delete cloud from Shelly Cloud"
|
208
|
+
def delete(code_name = nil)
|
209
|
+
user = Shelly::User.new
|
210
|
+
user.token
|
211
|
+
if code_name.present?
|
212
|
+
app = Shelly::App.new
|
213
|
+
say "You are about to delete application: #{code_name}."
|
214
|
+
say "Press Control-C at any moment to cancel."
|
215
|
+
say "Please confirm each question by typing yes and pressing Enter."
|
216
|
+
say_new_line
|
217
|
+
ask_to_delete_files
|
218
|
+
ask_to_delete_database
|
219
|
+
ask_to_delete_application
|
220
|
+
app.delete(code_name)
|
221
|
+
say_new_line
|
222
|
+
say "Scheduling application delete - done"
|
223
|
+
if App.inside_git_repository?
|
224
|
+
app.remove_git_remote
|
225
|
+
say "Removing git remote - done"
|
226
|
+
else
|
227
|
+
say "Missing git remote"
|
228
|
+
end
|
229
|
+
else
|
230
|
+
say_error "Missing CODE-NAME", :with_exit => false
|
231
|
+
say_error "Use: shelly delete CODE-NAME"
|
232
|
+
end
|
233
|
+
rescue Client::APIError => e
|
234
|
+
say_error e.message
|
235
|
+
end
|
149
236
|
|
150
237
|
# FIXME: move to helpers
|
151
238
|
no_tasks do
|
@@ -224,4 +311,3 @@ module Shelly
|
|
224
311
|
end
|
225
312
|
end
|
226
313
|
end
|
227
|
-
|
data/lib/shelly/cli/user.rb
CHANGED
@@ -10,17 +10,19 @@ module Shelly
|
|
10
10
|
def list
|
11
11
|
say_error "Must be run inside your project git repository" unless App.inside_git_repository?
|
12
12
|
say_error "No Cloudfile found" unless Cloudfile.present?
|
13
|
-
@cloudfile =
|
14
|
-
@cloudfile.
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
13
|
+
@cloudfile = Cloudfile.new
|
14
|
+
@cloudfile.clouds.each do |cloud|
|
15
|
+
begin
|
16
|
+
@app = App.new(cloud)
|
17
|
+
say "Cloud #{cloud}:"
|
18
|
+
@app.users.each { |user| say " #{user["email"]}" }
|
19
|
+
rescue Client::APIError => e
|
20
|
+
if e.unauthorized?
|
21
|
+
say_error "You have no access to '#{cloud}' cloud defined in Cloudfile", :with_exit => false
|
22
|
+
else
|
23
|
+
say_error e.message, :with_exit => false
|
24
|
+
end
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
@@ -28,7 +30,8 @@ module Shelly
|
|
28
30
|
def add(email = nil)
|
29
31
|
say_error "Must be run inside your project git repository" unless App.inside_git_repository?
|
30
32
|
say_error "No Cloudfile found" unless Cloudfile.present?
|
31
|
-
@cloudfile
|
33
|
+
@cloudfile = Cloudfile.new
|
34
|
+
@user = Shelly::User.new
|
32
35
|
user_email = email || ask_for_email({:guess_email => false})
|
33
36
|
@cloudfile.clouds.each do |cloud|
|
34
37
|
begin
|
@@ -36,9 +39,13 @@ module Shelly
|
|
36
39
|
rescue Client::APIError => e
|
37
40
|
if e.validation? && e.errors.include?(["email", "#{email} has already been taken"])
|
38
41
|
say_error "User #{email} is already in the cloud #{cloud}", :with_exit => false
|
42
|
+
elsif e.unauthorized?
|
43
|
+
say_error "You have no access to '#{cloud}' cloud defined in Cloudfile", :with_exit => false
|
39
44
|
elsif e.validation?
|
40
45
|
e.each_error { |error| say_error error, :with_exit => false }
|
41
46
|
exit 1
|
47
|
+
else
|
48
|
+
say_error e.message, :with_exit => false
|
42
49
|
end
|
43
50
|
else
|
44
51
|
say "Sending invitation to #{user_email} to work on #{cloud}", :green
|
data/lib/shelly/client.rb
CHANGED
@@ -25,7 +25,7 @@ module Shelly
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def unauthorized?
|
28
|
-
message == "Unauthorized"
|
28
|
+
message == "Unauthorized" || message =~ /Cloud .+ not found/
|
29
29
|
end
|
30
30
|
|
31
31
|
def each_error
|
@@ -64,28 +64,40 @@ module Shelly
|
|
64
64
|
post("/apps", :app => attributes)
|
65
65
|
end
|
66
66
|
|
67
|
+
def delete_app(code_name)
|
68
|
+
delete("/apps/#{code_name}")
|
69
|
+
end
|
70
|
+
|
67
71
|
def add_ssh_key(ssh_key)
|
68
72
|
post("/ssh_key", :ssh_key => ssh_key)
|
69
73
|
end
|
70
74
|
|
75
|
+
def start_cloud(cloud)
|
76
|
+
put("/apps/#{cloud}/start")
|
77
|
+
end
|
78
|
+
|
79
|
+
def stop_cloud(cloud)
|
80
|
+
put("/apps/#{cloud}/stop")
|
81
|
+
end
|
82
|
+
|
71
83
|
def apps
|
72
84
|
get("/apps")
|
73
85
|
end
|
74
86
|
|
87
|
+
def cloud_logs(cloud)
|
88
|
+
get("/apps/#{cloud}/deploys")
|
89
|
+
end
|
90
|
+
|
75
91
|
def ssh_key_available?(ssh_key)
|
76
92
|
get("/users/new", :ssh_key => ssh_key)
|
77
93
|
end
|
78
94
|
|
79
|
-
def
|
80
|
-
apps
|
81
|
-
get("/apps/#{app}/users")
|
82
|
-
end
|
95
|
+
def app_users(cloud)
|
96
|
+
get("/apps/#{cloud}/users")
|
83
97
|
end
|
84
98
|
|
85
|
-
def
|
86
|
-
apps
|
87
|
-
get("/apps/#{app}/ips")
|
88
|
-
end
|
99
|
+
def app_ips(cloud)
|
100
|
+
get("/apps/#{cloud}/ips")
|
89
101
|
end
|
90
102
|
|
91
103
|
def post(path, params = {})
|
@@ -100,6 +112,10 @@ module Shelly
|
|
100
112
|
request(path, :get, params)
|
101
113
|
end
|
102
114
|
|
115
|
+
def delete(path, params = {})
|
116
|
+
request(path, :delete, params)
|
117
|
+
end
|
118
|
+
|
103
119
|
def request(path, method, params = {})
|
104
120
|
options = request_parameters(path, method, params)
|
105
121
|
RestClient::Request.execute(options) do |response, request|
|
data/lib/shelly/cloudfile.rb
CHANGED
@@ -36,23 +36,5 @@ module Shelly
|
|
36
36
|
# FIXME: check if it possible to remove sub("---", "") by passing options to_yaml
|
37
37
|
string.sub("---","").split("\n").map(&:rstrip).join("\n").strip
|
38
38
|
end
|
39
|
-
|
40
|
-
def fetch_users
|
41
|
-
response = shelly.apps_users(clouds)
|
42
|
-
response.inject({}) do |result, app|
|
43
|
-
result[app['code_name']] = app['users'].map do |user|
|
44
|
-
user['email']
|
45
|
-
end
|
46
|
-
result
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def fetch_ips
|
51
|
-
response = shelly.apps_ips(clouds)
|
52
|
-
response.each do |result|
|
53
|
-
result
|
54
|
-
end
|
55
|
-
end
|
56
39
|
end
|
57
40
|
end
|
58
|
-
|
data/lib/shelly/helpers.rb
CHANGED
@@ -27,19 +27,51 @@ module Shelly
|
|
27
27
|
say_error "Email can't be blank, please try again"
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
30
|
+
def ask_to_delete_files
|
31
|
+
delete_files_question = "I want to delete all files stored on Shelly Cloud (yes/no):"
|
32
|
+
delete_files = ask(delete_files_question)
|
33
|
+
exit 1 unless delete_files == "yes"
|
34
|
+
end
|
35
|
+
|
36
|
+
def ask_to_delete_database
|
37
|
+
delete_database_question = "I want to delete all database data stored on Shelly Cloud (yes/no):"
|
38
|
+
delete_database = ask(delete_database_question)
|
39
|
+
exit 1 unless delete_database == "yes"
|
40
|
+
end
|
41
|
+
|
42
|
+
def ask_to_delete_application
|
43
|
+
delete_application_question = "I want to delete the application (yes/no):"
|
44
|
+
delete_application = ask(delete_application_question)
|
45
|
+
exit 1 unless delete_application == "yes"
|
46
|
+
end
|
47
|
+
|
48
|
+
def logged_in?
|
49
|
+
user = Shelly::User.new
|
50
|
+
user.token
|
51
|
+
user
|
52
|
+
rescue Client::APIError => e
|
53
|
+
if e.unauthorized?
|
54
|
+
say_error "You are not logged in. To log in use:", :with_exit => false
|
55
|
+
say " shelly login"
|
56
|
+
exit 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def multiple_clouds(cloud, action, message)
|
61
|
+
clouds = Cloudfile.new.clouds
|
62
|
+
if clouds.count > 1 && cloud.nil?
|
63
|
+
say "You have multiple clouds in Cloudfile. #{message}"
|
64
|
+
say " shelly #{action} #{clouds.first}"
|
65
|
+
say "Available clouds:"
|
66
|
+
clouds.each do |cloud|
|
67
|
+
say " * #{cloud}"
|
40
68
|
end
|
41
|
-
|
69
|
+
exit 1
|
42
70
|
end
|
71
|
+
@app = Shelly::App.new
|
72
|
+
@app.code_name = cloud.nil? ? clouds.first : cloud
|
73
|
+
end
|
74
|
+
|
43
75
|
end
|
44
76
|
end
|
45
77
|
|