shelly 0.1.34 → 0.1.35

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/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