shelly 0.1.34 → 0.1.35

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.1.34 / 2012-12-06
2
+
3
+ * [feature] Support for organizations
4
+
5
+ * [feature] New cloud can be created with existing organizatino with --organization [-o] option
6
+
7
+ Usage: ```shelly add --organization=ORGANIZATION_NAME```
8
+
9
+ * Manage users within organization
10
+
11
+ All ```shelly user``` commands affected
12
+
13
+ * [feature] Show organizations with associated clouds
14
+
1
15
  ## 0.1.34 / 2012-11-11
2
16
 
3
17
  * [feature] Accept DSA keys when logging in or registering
data/lib/shelly.rb CHANGED
@@ -13,6 +13,7 @@ require "shelly/model"
13
13
 
14
14
  module Shelly
15
15
  autoload :App, "shelly/app"
16
+ autoload :Organization, "shelly/organization"
16
17
  autoload :Cloudfile, "shelly/cloudfile"
17
18
  autoload :Client, "shelly/client"
18
19
  autoload :StructureValidator, "shelly/structure_validator"
data/lib/shelly/app.rb CHANGED
@@ -10,7 +10,7 @@ module Shelly
10
10
 
11
11
  attr_accessor :code_name, :databases, :ruby_version, :environment,
12
12
  :git_url, :domains, :web_server_ip, :size, :thin, :redeem_code,
13
- :content
13
+ :content, :organization
14
14
 
15
15
  def initialize(code_name = nil, content = nil)
16
16
  self.code_name = code_name
@@ -43,7 +43,9 @@ module Shelly
43
43
  end
44
44
 
45
45
  def create
46
- attributes = {:code_name => code_name, :redeem_code => redeem_code}
46
+ attributes = {:code_name => code_name,
47
+ :redeem_code => redeem_code,
48
+ :organization_name => organization}
47
49
  response = shelly.create_app(attributes)
48
50
  self.git_url = response["git_url"]
49
51
  self.domains = response["domains"]
@@ -132,19 +134,6 @@ module Shelly
132
134
  "#{File.basename(Dir.pwd)}-#{guessed || 'staging'}".downcase.dasherize
133
135
  end
134
136
 
135
- def collaborations
136
- @collaborations ||= Array(shelly.collaborations(code_name)).
137
- sort_by { |c| c["email"] }
138
- end
139
-
140
- def active_collaborations
141
- collaborations.select { |c| c["active"] }
142
- end
143
-
144
- def inactive_collaborations
145
- collaborations.select { |c| !c["active"] }
146
- end
147
-
148
137
  def configs
149
138
  @configs ||= shelly.app_configs(code_name)
150
139
  end
@@ -5,6 +5,7 @@ require "shelly/cli/backup"
5
5
  require "shelly/cli/deploy"
6
6
  require "shelly/cli/config"
7
7
  require "shelly/cli/file"
8
+ require "shelly/cli/organization"
8
9
 
9
10
  module Shelly
10
11
  module CLI
@@ -14,6 +15,7 @@ module Shelly
14
15
  register_subcommand(Deploy, "deploy", "deploy <command>", "View deploy logs")
15
16
  register_subcommand(Config, "config", "config <command>", "Manage application configuration files")
16
17
  register_subcommand(File, "file", "file <command>", "Upload and download files to and from persistent storage")
18
+ register_subcommand(Organization, "organization", "organization <command>", "View organizations")
17
19
 
18
20
  check_unknown_options!(:except => :rake)
19
21
 
@@ -79,6 +81,8 @@ module Shelly
79
81
  :desc => "Server size [large, small]"
80
82
  method_option "redeem-code", :type => :string, :aliases => "-r",
81
83
  :desc => "Redeem code for free credits"
84
+ method_option "organization", :type => :string, :aliases => "-o",
85
+ :desc => "Add cloud to existing organization"
82
86
  method_option "skip-requirements-check", :type => :boolean,
83
87
  :desc => "Skip Shelly Cloud requirements check"
84
88
  desc "add", "Add a new cloud"
@@ -92,6 +96,7 @@ module Shelly
92
96
  app.databases = options["databases"] || ask_for_databases
93
97
  app.size = options["size"] || "large"
94
98
  app.redeem_code = options["redeem-code"]
99
+ app.organization = options["organization"]
95
100
  app.create
96
101
 
97
102
  if overwrite_remote?(app)
@@ -120,6 +125,12 @@ module Shelly
120
125
  say_new_line
121
126
  say_error "Fix erros in the below command and type it again to create your cloud" , :with_exit => false
122
127
  say_error "shelly add --code-name=#{app.code_name.downcase.dasherize} --databases=#{app.databases.join(',')} --size=#{app.size}"
128
+ rescue Client::ForbiddenException
129
+ say_error "You have to be the owner of '#{options[:organization]}' organization to add clouds"
130
+ rescue Client::NotFoundException => e
131
+ raise unless e.resource == :organization
132
+ say_error "Organization '#{options[:organization]}' not found", :with_exit => false
133
+ say_error "You can list organizations you have access to with `shelly organization list`"
123
134
  end
124
135
 
125
136
  map "status" => :list
@@ -0,0 +1,35 @@
1
+ require "shelly/cli/command"
2
+
3
+ module Shelly
4
+ module CLI
5
+ class Organization < Command
6
+ namespace :organization
7
+ include Helpers
8
+
9
+ before_hook :logged_in?, :only => [:list]
10
+
11
+ desc "list", "Lists organizations"
12
+ def list
13
+ user = Shelly::User.new
14
+ organizations = user.organizations
15
+ say "You have access to the following organizations and clouds:", :green
16
+ say_new_line
17
+ organizations.each do |organization|
18
+ say organization.name, :green
19
+ if organization.apps.present?
20
+ apps_table = organization.apps.map do |app|
21
+ state = app.state
22
+ msg = if state == "deploy_failed" || state == "configuration_failed"
23
+ " (deployment log: `shelly deploys show last -c #{app["code_name"]}`)"
24
+ end
25
+ [app.to_s, "| #{state.humanize}#{msg}"]
26
+ end
27
+ print_table(apps_table, :ident => 2, :colwidth => 35)
28
+ else
29
+ print_wrapped "No clouds", :ident => 2
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,4 +1,5 @@
1
1
  require "shelly/cli/command"
2
+ require "shelly/cli/organization"
2
3
 
3
4
  module Shelly
4
5
  module CLI
@@ -7,46 +8,93 @@ module Shelly
7
8
  include Helpers
8
9
 
9
10
  before_hook :logged_in?, :only => [:list, :add, :delete]
10
- class_option :cloud, :type => :string, :aliases => "-c", :desc => "Specify cloud"
11
11
 
12
- desc "list", "List users with access to clouds defined in Cloudfile"
12
+ desc "list", "List users with access to organizations"
13
13
  def list
14
- app = multiple_clouds(options[:cloud], "user list")
15
- say "Cloud #{app}:"
16
- app.active_collaborations.each { |c| say " #{c["email"]}" }
17
- app.inactive_collaborations.each { |c|
18
- say " #{c["email"]} (invited)" }
14
+ user = Shelly::User.new
15
+ organizations = user.organizations
16
+
17
+ say "Organizations with users:"
18
+
19
+ organizations.each do |organization|
20
+ say " #{organization.name}", :green
21
+ if organization.memberships.present?
22
+ members_table = organization.owners.map { |owner| [owner["email"], " | owner"] }
23
+ members_table += organization.members.map { |member| [member["email"], " | member"] }
24
+ members_table += organization.inactive_members.map { |inactive| [inactive["email"] + " (invited)", " | #{humman_owner(inactive["owner"])}"] }
25
+ print_table(members_table, :ident => 4, :colwidth => 45)
26
+ say_new_line
27
+ end
28
+ end
19
29
  end
20
30
 
21
- desc "add [EMAIL]", "Add new developer to clouds defined in Cloudfile"
31
+ method_option :organization, :type => :string, :aliases => "-o", :desc => "Specify organization"
32
+ desc "add [EMAIL]", "Add new developer to organization"
22
33
  def add(email = nil)
23
- user = Shelly::User.new
24
- app = multiple_clouds(options[:cloud], "user add")
34
+ organization = organization_present?(options[:organization], "user add [EMAIL]")
35
+
25
36
  user_email = email || ask_for_email({:guess_email => false})
26
- user.send_invitation(app.to_s, user_email)
27
- say "Sending invitation to #{user_email} to work on #{app}", :green
37
+ owner = yes?("Should this user have owner privileges? (yes/no)")
38
+ organization.send_invitation(user_email, owner)
39
+
40
+ say "Sending invitation to #{user_email} to work on #{organization} organization", :green
41
+ rescue Client::ForbiddenException
42
+ say_error "You have to be organization's owner to add new members"
43
+ rescue Client::NotFoundException => e
44
+ raise unless e.resource == :organization
45
+ say_error "Organization '#{options[:organization]}' not found", :with_exit => false
46
+ say_error "You can list organizations you have access to with `shelly organization list`"
28
47
  rescue Client::ValidationException => e
29
- if e.errors.include?(["email", "#{email} has already been taken"])
30
- say_error "User #{email} is already in the cloud #{app}", :with_exit => false
48
+ if e.errors.include?(["email", "#{email} has been already taken"])
49
+ say_error "User #{email} is already in the organization #{organization}"
31
50
  else
32
51
  e.each_error { |error| say_error error, :with_exit => false }
33
52
  exit 1
34
53
  end
35
54
  end
36
55
 
37
- desc "delete [EMAIL]", "Remove developer from clouds defined in Cloudfile"
56
+ method_option :organization, :type => :string, :aliases => "-o", :desc => "Specify organization"
57
+ desc "delete [EMAIL]", "Remove developer from organization"
38
58
  def delete(email = nil)
39
- user = Shelly::User.new
40
- app = multiple_clouds(options[:cloud], "user delete")
59
+ organization = organization_present?(options[:organization], "user delete [EMAIL]")
60
+
41
61
  user_email = email || ask_for_email({:guess_email => false})
42
- user.delete_collaboration(app.to_s, user_email)
43
- say "User #{user_email} deleted from cloud #{app}"
62
+ organization.delete_member(user_email)
63
+
64
+ say "User #{user_email} deleted from organization #{organization}"
65
+ rescue Client::ForbiddenException
66
+ say_error "You have to be organization's owner to remove members"
44
67
  rescue Client::ConflictException => e
45
68
  say_error e[:message]
46
69
  rescue Client::NotFoundException => e
47
- raise unless e.resource == :user
48
- say_error "User '#{user_email}' not found", :with_exit => false
49
- say_error "You can list users with `shelly user list`"
70
+ if e.resource == :user
71
+ say_error "User '#{user_email}' not found", :with_exit => false
72
+ say_error "You can list users with `shelly user list`"
73
+ elsif e.resource == :organization
74
+ say_error "Organization '#{options[:organization]}' not found", :with_exit => false
75
+ say_error "You can list organizations you have access to with `shelly organization list`"
76
+ else
77
+ raise
78
+ end
79
+ end
80
+
81
+ no_tasks do
82
+ def humman_owner(owner)
83
+ owner ? "owner" : "member"
84
+ end
85
+
86
+ def organization_present?(name, action)
87
+ unless name
88
+ say_error "You have to specify organization", :with_exit => false
89
+ say "Select organization using `shelly #{action} --organization ORGANIZATION_NAME`"
90
+ Shelly::CLI::Organization.new.list
91
+ exit 1
92
+ else
93
+ Shelly::Organization.new("name" => name).tap do |org|
94
+ org.members
95
+ end
96
+ end
97
+ end
50
98
  end
51
99
  end
52
100
  end
data/lib/shelly/client.rb CHANGED
@@ -19,6 +19,7 @@ module Shelly
19
19
  end
20
20
 
21
21
  class UnauthorizedException < APIException; end
22
+ class ForbiddenException < APIException; end
22
23
  class ConflictException < APIException; end
23
24
  class GemVersionException < APIException; end
24
25
  class GatewayTimeoutException < APIException; end
@@ -87,16 +88,17 @@ module Shelly
87
88
  delete("/apps/#{cloud}/configs/#{CGI.escape(path)}")
88
89
  end
89
90
 
90
- def send_invitation(cloud, email)
91
- post("/apps/#{cloud}/collaborations", :email => email)
91
+ def send_invitation(name, email, owner = false)
92
+ post("/organizations/#{name}/memberships", :email => email, :owner => owner)
92
93
  end
93
94
 
94
- def delete_collaboration(cloud, email)
95
- delete("/apps/#{cloud}/collaborations/#{email}")
95
+ def delete_member(name, email)
96
+ delete("/organizations/#{name}/memberships/#{email}")
96
97
  end
97
98
 
98
99
  def create_app(attributes)
99
- post("/apps", :app => attributes)
100
+ organization = attributes.delete(:organization_name)
101
+ post("/apps", :app => attributes, :organization_name => organization)
100
102
  end
101
103
 
102
104
  def delete_app(code_name)
@@ -127,6 +129,14 @@ module Shelly
127
129
  get("/apps/#{code_name}")
128
130
  end
129
131
 
132
+ def organizations
133
+ get("/organizations")
134
+ end
135
+
136
+ def organization(name)
137
+ get("/organizations/#{name}")
138
+ end
139
+
130
140
  def statistics(code_name)
131
141
  get("/apps/#{code_name}/statistics")
132
142
  end
@@ -167,8 +177,8 @@ module Shelly
167
177
  post("/apps/#{code_name}/database_backups", :kind => kind)
168
178
  end
169
179
 
170
- def collaborations(cloud)
171
- get("/apps/#{cloud}/collaborations")
180
+ def members(name)
181
+ get("/organizations/#{name}/memberships")
172
182
  end
173
183
 
174
184
  def redeploy(cloud)
@@ -244,6 +254,7 @@ module Shelly
244
254
  if (400..599).include?(code)
245
255
  exception_class = case response.code
246
256
  when 401; UnauthorizedException
257
+ when 403; ForbiddenException
247
258
  when 404; NotFoundException
248
259
  when 409; ConflictException
249
260
  when 412; GemVersionException
@@ -0,0 +1,45 @@
1
+ module Shelly
2
+ class Organization < Model
3
+ attr_reader :name, :app_code_names
4
+
5
+ def initialize(attributes = {})
6
+ @name = attributes["name"]
7
+ @app_code_names = attributes["app_code_names"]
8
+ end
9
+
10
+ def apps
11
+ app_code_names.map do |code_name|
12
+ Shelly::App.new(code_name)
13
+ end
14
+ end
15
+
16
+ def memberships
17
+ @members ||= Array(shelly.members(name)).
18
+ sort_by { |c| c["email"] }
19
+ end
20
+
21
+ def owners
22
+ memberships.select { |c| c["owner"] } - inactive_members
23
+ end
24
+
25
+ def members
26
+ memberships.select { |c| !c["owner"] } - inactive_members
27
+ end
28
+
29
+ def inactive_members
30
+ memberships.select { |c| !c["active"] }
31
+ end
32
+
33
+ def send_invitation(email, owner)
34
+ shelly.send_invitation(name, email, owner)
35
+ end
36
+
37
+ def delete_member(email)
38
+ shelly.delete_member(name, email)
39
+ end
40
+
41
+ def to_s
42
+ name
43
+ end
44
+ end
45
+ end
data/lib/shelly/user.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'shelly/organization'
2
+
1
3
  module Shelly
2
4
  class User < Model
3
5
  attr_accessor :email, :password
@@ -11,6 +13,12 @@ module Shelly
11
13
  shelly.apps
12
14
  end
13
15
 
16
+ def organizations
17
+ shelly.organizations.map do |attributes|
18
+ Shelly::Organization.new(attributes)
19
+ end
20
+ end
21
+
14
22
  def register
15
23
  ssh_key = File.read(ssh_key_path) if ssh_key_exists?
16
24
  shelly.register_user(email, password, ssh_key)
@@ -29,14 +37,6 @@ module Shelly
29
37
  shelly.token["token"]
30
38
  end
31
39
 
32
- def send_invitation(cloud, email)
33
- shelly.send_invitation(cloud, email)
34
- end
35
-
36
- def delete_collaboration(cloud, email)
37
- shelly.delete_collaboration(cloud, email)
38
- end
39
-
40
40
  def load_credentials
41
41
  return unless credentials_exists?
42
42
  @email, @password = File.read(credentials_path).split("\n")
@@ -1,3 +1,3 @@
1
1
  module Shelly
2
- VERSION = "0.1.34"
2
+ VERSION = "0.1.35"
3
3
  end
@@ -47,13 +47,6 @@ describe Shelly::App do
47
47
  end
48
48
  end
49
49
 
50
- describe "#collaborations" do
51
- it "should fetch app's users" do
52
- @client.should_receive(:collaborations).with("foo-staging")
53
- @app.collaborations
54
- end
55
- end
56
-
57
50
  describe "#add_git_remote" do
58
51
  before do
59
52
  @app.stub(:git_url).and_return("git@git.shellycloud.com:foo-staging.git")
@@ -283,7 +276,8 @@ describe Shelly::App do
283
276
  @app.redeem_code = "foo123"
284
277
  attributes = {
285
278
  :code_name => "fooo",
286
- :redeem_code => "foo123"
279
+ :redeem_code => "foo123",
280
+ :organization_name => nil
287
281
  }
288
282
  @client.should_receive(:create_app).with(attributes).and_return("git_url" => "git@git.shellycloud.com:fooo.git",
289
283
  "domains" => %w(fooo.shellyapp.com))
@@ -27,29 +27,30 @@ describe Shelly::CLI::Main do
27
27
  it "should display available commands" do
28
28
  out = IO.popen("bin/shelly --debug").read.strip
29
29
  out.should include("Tasks:")
30
- out.should include("shelly add # Add a new cloud")
31
- out.should include("shelly backup <command> # Manage database backups")
32
- out.should include("shelly check # Check if application fulfills Shelly Cloud requirements")
33
- out.should include("shelly config <command> # Manage application configuration files")
34
- out.should include("shelly console # Open application console")
35
- out.should include("shelly dbconsole # Run rails dbconsole")
36
- out.should include("shelly delete # Delete the cloud")
37
- out.should include("shelly deploy <command> # View deploy logs")
38
- out.should include("shelly file <command> # Upload and download files to and from persistent storage")
39
- out.should include("shelly help [TASK] # Describe available tasks or one specific task")
40
- out.should include("shelly info # Show basic information about cloud")
41
- out.should include("shelly list # List available clouds")
42
- out.should include("shelly login [EMAIL] # Log into Shelly Cloud")
43
- out.should include("shelly logout # Logout from Shelly Cloud")
44
- out.should include("shelly logs # Show latest application logs")
45
- out.should include("shelly open # Open application page in browser")
46
- out.should include("shelly rake TASK # Run rake task")
47
- out.should include("shelly redeploy # Redeploy application")
48
- out.should include("shelly register [EMAIL] # Register new account")
49
- out.should include("shelly setup # Set up git remotes for deployment on Shelly Cloud")
50
- out.should include("shelly start # Start the cloud")
51
- out.should include("shelly stop # Shutdown the cloud")
52
- out.should include("shelly user <command> # Manage collaborators")
30
+ out.should include("shelly add # Add a new cloud")
31
+ out.should include("shelly backup <command> # Manage database backups")
32
+ out.should include("shelly check # Check if application fulfills Shelly Cloud requirements")
33
+ out.should include("shelly config <command> # Manage application configuration files")
34
+ out.should include("shelly console # Open application console")
35
+ out.should include("shelly dbconsole # Run rails dbconsole")
36
+ out.should include("shelly delete # Delete the cloud")
37
+ out.should include("shelly deploy <command> # View deploy logs")
38
+ out.should include("shelly file <command> # Upload and download files to and from persistent storage")
39
+ out.should include("shelly help [TASK] # Describe available tasks or one specific task")
40
+ out.should include("shelly info # Show basic information about cloud")
41
+ out.should include("shelly list # List available clouds")
42
+ out.should include("shelly login [EMAIL] # Log into Shelly Cloud")
43
+ out.should include("shelly logout # Logout from Shelly Cloud")
44
+ out.should include("shelly logs # Show latest application logs")
45
+ out.should include("shelly open # Open application page in browser")
46
+ out.should include("shelly organization <command> # View organizations")
47
+ out.should include("shelly rake TASK # Run rake task")
48
+ out.should include("shelly redeploy # Redeploy application")
49
+ out.should include("shelly register [EMAIL] # Register new account")
50
+ out.should include("shelly setup # Set up git remotes for deployment on Shelly Cloud")
51
+ out.should include("shelly start # Start the cloud")
52
+ out.should include("shelly stop # Shutdown the cloud")
53
+ out.should include("shelly user <command> # Manage collaborators")
53
54
  out.should include("Options")
54
55
  out.should include("[--debug] # Show debug information")
55
56
  out.should include("-h, [--help] # Describe available tasks or one specific task")
@@ -531,18 +532,45 @@ More info at http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository\e[0m
531
532
  invoke(@main, :add)
532
533
  end
533
534
  end
535
+
536
+ it "should show forbidden exception" do
537
+ @main.options = {:organization => "foo"}
538
+ exception = Shelly::Client::ForbiddenException.new
539
+ @app.should_receive(:create).and_raise(exception)
540
+ $stdout.should_receive(:puts).with(red "You have to be the owner of 'foo' organization to add clouds")
541
+
542
+ expect do
543
+ fake_stdin(["foooo", "none"]) do
544
+ invoke(@main, :add)
545
+ end
546
+ end.to raise_error(SystemExit)
547
+ end
548
+
549
+ it "should show that organization was not found" do
550
+ @main.options = {:organization => "foo"}
551
+ response = {"resource" => "organization"}
552
+ exception = Shelly::Client::NotFoundException.new(response)
553
+ @app.should_receive(:create).and_raise(exception)
554
+ $stdout.should_receive(:puts).with(red "Organization 'foo' not found")
555
+ $stdout.should_receive(:puts).with(red "You can list organizations you have access to with `shelly organization list`")
556
+
557
+ expect do
558
+ fake_stdin(["foooo", "none"]) do
559
+ invoke(@main, :add)
560
+ end
561
+ end.to raise_error(SystemExit)
562
+ end
563
+
534
564
  end
535
565
 
536
566
  describe "#list" do
537
567
  before do
538
- @user = Shelly::User.new
539
568
  @client.stub(:token).and_return("abc")
540
569
  @client.stub(:apps).and_return([
541
570
  {"code_name" => "abc", "state" => "running"},
542
571
  {"code_name" => "fooo", "state" => "deploy_failed"},
543
572
  {"code_name" => "bar", "state" => "configuration_failed"}
544
573
  ])
545
- Shelly::User.stub(:new).and_return(@user)
546
574
  end
547
575
 
548
576
  it "should ensure user has logged in" do