Permalink
Browse files

Initial checkin of the CC APIs

Change-Id: I7509a50707588b324e608e3ccd2b3e991b0ea746
  • Loading branch information...
1 parent 0f22cc0 commit 69e6cce2d4e65c1b91b3b2860827b9369247f242 Patrick Bozeman committed May 9, 2012
Showing with 2,121 additions and 6 deletions.
  1. +0 −1 Gemfile
  2. +2 −4 Gemfile.lock
  3. +318 −0 docs/ilia_tour
  4. +26 −1 lib/cloud_controller.rb
  5. +7 −0 lib/cloud_controller/api.rb
  6. +34 −0 lib/cloud_controller/api/app.rb
  7. +27 −0 lib/cloud_controller/api/app_space.rb
  8. +27 −0 lib/cloud_controller/api/framework.rb
  9. +26 −0 lib/cloud_controller/api/organization.rb
  10. +27 −0 lib/cloud_controller/api/runtime.rb
  11. +35 −0 lib/cloud_controller/api/service.rb
  12. +24 −0 lib/cloud_controller/api/service_auth_token.rb
  13. +29 −0 lib/cloud_controller/api/service_binding.rb
  14. +29 −0 lib/cloud_controller/api/service_instance.rb
  15. +27 −0 lib/cloud_controller/api/service_plan.rb
  16. +27 −0 lib/cloud_controller/api/user.rb
  17. +19 −0 lib/cloud_controller/rest_controller.rb
  18. +299 −0 lib/cloud_controller/rest_controller/base.rb
  19. +80 −0 lib/cloud_controller/rest_controller/controller_dsl.rb
  20. +67 −0 lib/cloud_controller/rest_controller/messages.rb
  21. +67 −0 lib/cloud_controller/rest_controller/object_serialization.rb
  22. +65 −0 lib/cloud_controller/rest_controller/routes.rb
  23. +24 −0 spec/api/app_space_spec.rb
  24. +34 −0 spec/api/app_spec.rb
  25. +19 −0 spec/api/framework_spec.rb
  26. +208 −0 spec/api/helpers/collections.rb
  27. +229 −0 spec/api/helpers/creating_and_updating.rb
  28. +23 −0 spec/api/helpers/deleting.rb
  29. +33 −0 spec/api/helpers/invalid_resource.rb
  30. +32 −0 spec/api/helpers/reading.rb
  31. +3 −0 spec/api/info_spec.rb
  32. +21 −0 spec/api/organization_spec.rb
  33. +19 −0 spec/api/runtime_spec.rb
  34. +18 −0 spec/api/service_auth_token_spec.rb
  35. +17 −0 spec/api/service_binding_spec.rb
  36. +23 −0 spec/api/service_instance_spec.rb
  37. +20 −0 spec/api/service_plan_spec.rb
  38. +19 −0 spec/api/service_spec.rb
  39. +93 −0 spec/api/spec_helper.rb
  40. +24 −0 spec/api/user_spec.rb
View
1 Gemfile
@@ -8,7 +8,6 @@ gem "sequel"
gem "sinatra"
gem "sinatra-contrib"
gem "yajl-ruby"
-gem "vcap-concurrency"
gem "vcap_common"
gem "vcap_logging"
View
6 Gemfile.lock
@@ -45,7 +45,7 @@ GEM
ruby-graphviz (1.0.5)
ruby_core_source (0.1.5)
archive-tar-minitar (>= 0.5.2)
- sequel (3.34.1)
+ sequel (3.35.0)
simplecov (0.6.2)
multi_json (~> 1.3)
simplecov-html (~> 0.5.3)
@@ -67,14 +67,13 @@ GEM
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
tilt (1.3.3)
- vcap-concurrency (0.0.1)
vcap_common (1.0.10)
eventmachine (~> 0.12.11.cloudfoundry.3)
nats (~> 0.4.22.beta.8)
posix-spawn (~> 0.3.6)
thin (~> 1.3.1)
yajl-ruby (~> 0.8.3)
- vcap_logging (1.0.0)
+ vcap_logging (1.0.1)
rake
yajl-ruby (0.8.3)
@@ -95,7 +94,6 @@ DEPENDENCIES
sinatra
sinatra-contrib
sqlite3
- vcap-concurrency
vcap_common
vcap_logging
yajl-ruby
View
318 docs/ilia_tour
@@ -0,0 +1,318 @@
+Hi. Lets take a tour of working with the cc locally and do a few basic
+operations.
+
+Heads up.. the CC doesn't match the API doc yet. This tour will guide you
+around the pitfalls.
+
+Run the CC
+
+ First, let's get your cloud controller running. To do so, in another window
+ run:
+
+ bin/cloud_controller -d -m
+
+ -d puts the cc into developer mode. -m will cause it to run migrations.
+
+ You don't really have to go into dev mode, but you do need to run -m at
+ least once.
+
+
+Test the CC
+
+ Your cc should be up and running on port 8080 now, so test it out.
+ with:
+
+ curl -v http://localhost:8080/
+
+ You should get a 404 back and a custom error body. We should probably be
+ nicer about returning something for /, but for now, meh.
+
+ That error body is the standard sort of error that you will expect to see for
+ other types of errors.
+
+Bootstrap an Admin
+
+ Ok.. let's get your admin user created. We really need an external
+ bootstrap tool to insert an initial admin into the db, but for now, you can
+ do the following
+
+ curl -v http://localhost:8080/bootstrap
+
+ You should get back an HTTP 201 and a blob of json. Since the bootstrap call
+ is not a real CC api call, it is not in the standard format with a metadata
+ and entity section.
+
+Let's see a server error:
+
+ If you run the same command again:
+
+ curl -v http://localhost:8080/bootstrap
+
+ You will get back a 500 and a custom server error response body. This is
+ because the bootstrap route bypasses the API. Had you gone through the API
+ you would have gotten a real error message. Still, you didn't get a blob of
+ html on an unhandled error like in the original cc.
+
+ Note the window you are running the cc in. You should see a big stack trace.
+ If you get any weird server errors, go check for those sort of backtraces and
+ send them to me.
+
+Let's create an org:
+
+ Ok, this will be a load of fun!
+
+ curl -X POST -H "Content-Type: application/json" \
+ -d '{"name":"my cool org"}' \
+ http://localhost:8080/v2/organizations
+
+ Oh, you got an auth error. Silly you.
+
+
+Let's create an org with "correct" auth/authz
+
+ Ok, this will be even more fun. Fun like a horror movie. (UAA integration
+ is coming. We used to have our own signed token, but didn't bother checking
+ it in because the UAA is the true new hotness.)
+
+ curl -X POST -H "Content-Type: application/json" \
+ -H "Authorization: iliag@vmware.com" \
+ -d '{"name":"my cool org"}' \
+ http://localhost:8080/v2/organizations
+
+ Now this time you should get something back like:
+
+ {
+ "metadata": {
+ "id": 1,
+ "url": "/v2/organizations/1",
+ "created_at": "2012-05-14 12:49:31 -0700",
+ "updated_at": null
+ },
+ "entity": {
+ "name": "my cool org",
+ "users_url":
+ "/v2/users?q=organization_id:1",
+ "app_spaces_url":
+ "/v2/app_spaces?q=organization_id:1"
+ }
+ }
+
+ Yay.
+
+ Note the Location header in the response. You should be able to do
+ a GET on that.
+
+
+Let's double check our org:
+
+ curl -H "Authorization: iliag@vmware.com" \
+ http://localhost:8080/v2/organizations/1
+
+ {
+ "metadata": {
+ "id": 1,
+ "url": "/v2/organizations/1",
+ "created_at": "2012-05-14 12:49:31 -0700",
+ "updated_at": null
+ },
+ "entity": {
+ "name": "my cool org",
+ "users_url":
+ "/v2/users?q=organization_id:1",
+ "app_spaces_url":
+ "/v2/app_spaces?q=organization_id:1"
+ }
+ }
+
+ Double yay.
+
+
+Let's rename the org:
+
+ curl -v -X PUT -H "Content-Type: application/json" \
+ -H "Authorization: iliag@vmware.com" \
+ -d '{"name":"ilia likes orgs"}' \
+ http://localhost:8080/v2/organizations/1
+
+ {
+ "metadata": {
+ "id": 1,
+ "url": "/v2/organizations/1",
+ "created_at": "2012-05-14 12:49:31 -0700",
+ "updated_at": "2012-05-14 12:55:01 -0700"
+ },
+ "entity": {
+ "name": "ilia likes orgs",
+ "users_url":
+ "/v2/users?q=organization_id:1",
+ "app_spaces_url":
+ "/v2/app_spaces?q=organization_id:1"
+ }
+ }
+
+
+Let's go create a user:
+
+ curl -v -X POST -H "Content-Type: application/json" \
+ -H "Authorization: iliag@vmware.com" \
+ -d '{"id":"1234-4567-89AB-CDEF"}' \
+ http://localhost:8080/v2/users
+
+ {
+ "metadata": {
+ "id": "1234-4567-89AB-CDEF",
+ "url": "/v2/users/1234-4567-89AB-CDEF",
+ "created_at": "2012-05-14 12:57:08 -0700",
+ "updated_at": null
+ },
+ "entity": {
+ "id": "1234-4567-89AB-CDEF",
+ "admin": false,
+ "active": false,
+ "organizations_url":
+ "/v2/organizations?q=user_id:1234-4567-89AB-CDEF",
+ "app_spaces_url":
+ "/v2/app_spaces?q=user_id:1234-4567-89AB-CDEF"
+ }
+ }
+
+So now we should have 2 users, (the boot strap user, plus the one above)
+let's see: (NOTE: The bootstrap user is currently using an email address
+since that is what was provided in the last drop, but for now,
+all uaa_ids are basically arbitrary text strings as far as the CC is concerned)
+
+ curl -v -H "Authorization: iliag@vmware.com" http://localhost:8080/v2/users
+
+ {
+ "total_results": 2,
+ "prev_url": null,
+ "next_url": null,
+ "resources": [
+ {
+ "metadata": {
+ "id": "iliag@vmware.com",
+ "url": "/v2/users/iliag@vmware.com",
+ "created_at": "2012-05-14 12:47:03 -0700",
+ "updated_at": null
+ },
+ "entity": {
+ "id": "iliag@vmware.com",
+ "admin": true,
+ "active": true,
+ "organizations_url": "/v2/organizations?q=user_id:iliag@vmware.com",
+ "app_spaces_url": "/v2/app_spaces?q=user_id:iliag@vmware.com"
+ }
+ },
+ {
+ "metadata": {
+ "id": "1234-4567-89AB-CDEF",
+ "url": "/v2/users/1234-4567-89AB-CDEF",
+ "created_at": "2012-05-14 12:57:08 -0700",
+ "updated_at": null
+ },
+ "entity": {
+ "id": "1234-4567-89AB-CDEF",
+ "admin": false,
+ "active": false,
+ "organizations_url": "/v2/organizations?q=user_id:1234-4567-89AB-CDEF",
+ "app_spaces_url": "/v2/app_spaces?q=user_id:1234-4567-89AB-CDEF"
+ }
+ }
+ ]
+ }
+
+ Yep
+
+Check the orgs the same way:
+
+ curl -v -H "Authorization: iliag@vmware.com" \
+ http://localhost:8080/v2/organizations
+
+ {
+ "total_results": 1,
+ "prev_url": null,
+ "next_url": null,
+ "resources": [
+ {
+ "metadata": {
+ "id": 1,
+ "url": "/v2/organizations/1",
+ "created_at": "2012-05-14 12:49:31 -0700",
+ "updated_at": "2012-05-14 12:55:01 -0700"
+ },
+ "entity": {
+ "name": "ilia likes orgs",
+ "users_url": "/v2/users?q=organization_id:1",
+ "app_spaces_url": "/v2/app_spaces?q=organization_id:1"
+ }
+ }
+ ]
+ }
+
+
+Those url associations work. Let's check the one for
+the 1234-... user.
+
+ curl -v -H "Authorization: iliag@vmware.com" \
+ http://localhost:8080/v2/organizations?q=user_id:1234-4567-89AB-CDEF
+
+ {
+ "total_results": 0,
+ "prev_url": null,
+ "next_url": null,
+ "resources": [
+
+ ]
+ }
+
+ Which is correct because we haven't added the user to an org yet.
+
+
+Now let's add that 1234-... user to an org:
+
+ curl -v -X PUT -H "Content-Type: application/json" \
+ -H "Authorization: iliag@vmware.com" \
+ -d '{"user_ids":["1234-4567-89AB-CDEF"]}' \
+ http://localhost:8080/v2/organizations/1
+
+ {
+ "metadata": {
+ "id": 1,
+ "url": "/v2/organizations/1",
+ "created_at": "2012-05-14 12:49:31 -0700",
+ "updated_at": "2012-05-14 13:09:59 -0700"
+ },
+ "entity": {
+ "name": "ilia likes orgs",
+ "users_url": "/v2/users?q=organization_id:1",
+ "app_spaces_url": "/v2/app_spaces?q=organization_id:1"
+ }
+ }
+
+Now if we fetch the url association for that user again:
+
+ curl -v -H "Authorization: iliag@vmware.com" \
+ http://localhost:8080/v2/organizations?q=user_id:1234-4567-89AB-CDEF
+
+ {
+ "total_results": 1,
+ "prev_url": null,
+ "next_url": null,
+ "resources": [
+ {
+ "metadata": {
+ "id": 1,
+ "url": "/v2/organizations/1",
+ "created_at": "2012-05-14 12:49:31 -0700",
+ "updated_at": "2012-05-14 13:09:59 -0700"
+ },
+ "entity": {
+ "name": "ilia likes orgs",
+ "users_url": "/v2/users?q=organization_id:1",
+ "app_spaces_url": "/v2/app_spaces?q=organization_id:1"
+ }
+ }
+ ]
+ }
+
+ Yep.. he shows up in org 1 as expected.
View
27 lib/cloud_controller.rb
@@ -9,19 +9,32 @@
require "eventmachine/schedule_sync"
require "vcap/common"
-require "vcap/concurrency"
require "vcap/logging"
require "sinatra/vcap"
module VCAP::CloudController
autoload :Models, "cloud_controller/models"
+ include VCAP::RestAPI
class Controller < Sinatra::Base
register Sinatra::VCAP
vcap_configure :reload_path => File.dirname(__FILE__)
+ before do
+ auth_token = env["HTTP_AUTHORIZATION"]
+ if auth_token
+ # FIXME: the commented out code is what we used to have. Now that the
+ # UAA is ready to rock, we should just go right to it. In the mean
+ # time, we'll accept a raw uaa_id just to test different sort of
+ # user types using the somewhat correct flow.
+ # email = Notary.new(@token_config[:key]).decode(auth_token)
+ # @user = Models::User.find(:email => email)
+ @user = VCAP::CloudController::Models::User.find(:id => auth_token)
+ end
+ end
+
# All manual routes here will be removed prior to final release.
# They are manual ad-hoc testing entry points.
get "/hello/sync" do
@@ -44,6 +57,16 @@ class Controller < Sinatra::Base
EM::Timer.new(5) { callback.call("async return from an EM timer\n") }
end
end
+
+ # This is is temporary for ilia
+ get "/bootstrap" do
+ body VCAP::CloudController::Models::User.create_from_hash(
+ :id => "iliag@vmware.com",
+ :admin => true,
+ :active => true).to_json
+
+ VCAP::RestAPI::HTTP::CREATED
+ end
end
end
@@ -52,3 +75,5 @@ class Controller < Sinatra::Base
require "cloud_controller/errors"
require "cloud_controller/permissions"
require "cloud_controller/runner"
+require "cloud_controller/errors"
+require "cloud_controller/api"
View
7 lib/cloud_controller/api.rb
@@ -0,0 +1,7 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+require "cloud_controller/rest_controller"
+
+Dir[File.expand_path("../api/*", __FILE__)].each do |file|
+ require file
+end
View
34 lib/cloud_controller/api/app.rb
@@ -0,0 +1,34 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :App do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :name, String
+ to_one :app_space
+ to_one :runtime
+ to_one :framework
+ attribute :environment_json, Hash, :default => {}
+ attribute :memory, Integer, :default => 256
+ attribute :instances, Integer, :default => 1
+ attribute :file_descriptors, Integer, :default => 256
+ attribute :disk_quota, Integer, :default => 256
+ attribute :state, String, :default => "STOPPED"
+ to_many :service_bindings, :exclude_in => :create
+ end
+
+ query_parameters :app_space_id, :organization_id, :framework_id, :runtime_id
+
+ def self.translate_validation_exception(e, attributes)
+ app_space_and_name_errors = e.errors.on([:app_space_id, :name])
+ if app_space_and_name_errors && app_space_and_name_errors.include?(:unique)
+ AppNameTaken.new(attributes["name"])
+ else
+ AppInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
27 lib/cloud_controller/api/app_space.rb
@@ -0,0 +1,27 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :AppSpace do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :name, String
+ to_one :organization
+ to_many :users
+ to_many :apps
+ end
+
+ query_parameters :organization_id, :user_id, :app_id
+
+ def self.translate_validation_exception(e, attributes)
+ name_errors = e.errors.on([:organization_id, :name])
+ if name_errors && name_errors.include?(:unique)
+ AppSpaceNameTaken.new(attributes["name"])
+ else
+ AppSpaceInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
27 lib/cloud_controller/api/framework.rb
@@ -0,0 +1,27 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :Framework do
+ permissions_required do
+ full Permissions::CFAdmin
+ read Permissions::Authenticated
+ end
+
+ define_attributes do
+ attribute :name, String
+ attribute :description, String
+ to_many :apps
+ end
+
+ query_parameters :name, :app_id
+
+ def self.translate_validation_exception(e, attributes)
+ name_errors = e.errors.on(:name)
+ if name_errors && name_errors.include?(:unique)
+ FrameworkNameTaken.new(attributes["name"])
+ else
+ FrameworkInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
26 lib/cloud_controller/api/organization.rb
@@ -0,0 +1,26 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :Organization do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :name, String
+ to_many :users
+ to_many :app_spaces, :exclude_in => :create
+ end
+
+ query_parameters :name, :user_id, :app_space_id
+
+ def self.translate_validation_exception(e, attributes)
+ name_errors = e.errors.on(:name)
+ if name_errors && name_errors.include?(:unique)
+ OrganizationNameTaken.new(attributes["name"])
+ else
+ OrganizationInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
27 lib/cloud_controller/api/runtime.rb
@@ -0,0 +1,27 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :Runtime do
+ permissions_required do
+ full Permissions::CFAdmin
+ read Permissions::Authenticated
+ end
+
+ define_attributes do
+ attribute :name, String
+ attribute :description, String
+ to_many :apps, :default => []
+ end
+
+ query_parameters :name, :app_id
+
+ def self.translate_validation_exception(e, attributes)
+ name_errors = e.errors.on(:name)
+ if name_errors && name_errors.include?(:unique)
+ RuntimeNameTaken.new(attributes["name"])
+ else
+ RuntimeInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
35 lib/cloud_controller/api/service.rb
@@ -0,0 +1,35 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :Service do
+ permissions_required do
+ full Permissions::CFAdmin
+ read Permissions::Authenticated
+ end
+
+ define_attributes do
+ attribute :label, String
+ attribute :provider, String
+ attribute :url, Message::HTTPS_URL
+ attribute :type, String
+ attribute :description, String
+ attribute :version, String
+ attribute :info_url, Message::URL
+ attribute :acls, {"users" => [String], "wildcards" => [String]}
+ attribute :timeout, Integer
+ attribute :active, Message::Boolean
+ to_many :service_plans
+ end
+
+ query_parameters :service_plan_id
+
+ def self.translate_validation_exception(e, attributes)
+ label_provider_errors = e.errors.on([:label, :provider])
+ if label_provider_errors && label_provider_errors.include?(:unique)
+ ServiceLabelTaken.new("#{attributes["label"]}-#{attributes["provider"]}")
+ else
+ ServiceInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
24 lib/cloud_controller/api/service_auth_token.rb
@@ -0,0 +1,24 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :ServiceAuthToken do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :label, String
+ attribute :provider, String
+ attribute :token, String, :exclude_in => :response
+ end
+
+ def self.translate_validation_exception(e, attributes)
+ label_provider_errors = e.errors.on([:label, :provider])
+ if label_provider_errors && label_provider_errors.include?(:unique)
+ ServiceAuthTokenLabelTaken.new("#{attributes["label"]}-#{attributes["provider"]}")
+ else
+ ServiceAuthTokenInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
29 lib/cloud_controller/api/service_binding.rb
@@ -0,0 +1,29 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :ServiceBinding do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :credentials, Hash
+ attribute :binding_options, Hash, :default => {}
+ attribute :vendor_data, Hash, :default => {}
+ to_one :app
+ to_one :service_instance
+ end
+
+ query_parameters :app_id, :service_instance_id
+
+ def self.translate_validation_exception(e, attributes)
+ unique_errors = e.errors.on([:app_id, :service_instance_id])
+ if unique_errors && unique_errors.include?(:unique)
+ ServiceBindingAppServiceTaken.new(
+ "#{attributes["app_id"]}-#{attributes["service_instance_id"]}")
+ else
+ ServiceBindingInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
29 lib/cloud_controller/api/service_instance.rb
@@ -0,0 +1,29 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :ServiceInstance do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :name, String
+ to_one :app_space
+ to_one :service_plan
+ to_many :service_bindings
+ attribute :credentials, Hash
+ attribute :vendor_data, String, :default => "" # FIXME: notation for access override here
+ end
+
+ query_parameters :app_space_id, :service_plan_id, :service_binding_id
+
+ def self.translate_validation_exception(e, attributes)
+ app_space_and_name_errors = e.errors.on([:app_space_id, :name])
+ if app_space_and_name_errors && app_space_and_name_errors.include?(:unique)
+ ServiceInstanceNameTaken.new(attributes["name"])
+ else
+ ServiceInstanceInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
27 lib/cloud_controller/api/service_plan.rb
@@ -0,0 +1,27 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :ServicePlan do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :name, String
+ attribute :description, String
+ to_one :service
+ to_many :service_instances
+ end
+
+ query_parameters :service_id, :service_instance_id
+
+ def self.translate_validation_exception(e, attributes)
+ name_errors = e.errors.on([:service_id, :name])
+ if name_errors && name_errors.include?(:unique)
+ ServicePlanNameTaken.new("#{attributes["service_id"]}-#{attributes["name"]}")
+ else
+ ServicePlanInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
27 lib/cloud_controller/api/user.rb
@@ -0,0 +1,27 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController
+ rest_controller :User do
+ permissions_required do
+ full Permissions::CFAdmin
+ end
+
+ define_attributes do
+ attribute :id, :exclude_in => :update
+ to_many :organizations
+ to_many :app_spaces
+ attribute :admin, Message::Boolean
+ end
+
+ query_parameters :app_space_id, :organization_id
+
+ def self.translate_validation_exception(e, attributes)
+ id_errors = e.errors.on(:id)
+ if id_errors && id_errors.include?(:unique)
+ UaaIdTaken.new(attributes["id"])
+ else
+ UserInvalid.new(e.errors.full_messages)
+ end
+ end
+ end
+end
View
19 lib/cloud_controller/rest_controller.rb
@@ -0,0 +1,19 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+require "cloud_controller/rest_controller/controller_dsl"
+require "cloud_controller/rest_controller/messages"
+require "cloud_controller/rest_controller/object_serialization"
+require "cloud_controller/rest_controller/routes"
+require "cloud_controller/rest_controller/base"
+
+module VCAP::CloudController
+ def self.rest_controller(name, &blk)
+ klass = Class.new RestController::Base
+ self.const_set name, klass
+ klass.class_eval &blk
+ klass.class_eval do
+ define_messages
+ define_routes
+ end
+ end
+end
View
299 lib/cloud_controller/rest_controller/base.rb
@@ -0,0 +1,299 @@
+# Copyright (c) 2009-2012 VMware, Inr.
+
+module VCAP::CloudController::RestController
+
+ # The base class for all api endpoints.
+ class Base
+ ROUTE_PREFIX = "/v2"
+
+ include VCAP::CloudController
+ include VCAP::CloudController::Errors
+ include VCAP::RestAPI
+ include PermissionManager
+ include Messages
+ include Routes
+
+ # Tell the PermissionManager the types of operations that can be performed.
+ define_permitted_operation :create
+ define_permitted_operation :read
+ define_permitted_operation :update
+ define_permitted_operation :delete
+ define_permitted_operation :enumerate
+
+ # Create a new rest api endpoint.
+ #
+ # @param [Models::User] user The user peforming the rest request. It may
+ # be nil.
+ #
+ # @param [VCAP::Logger] logger The logger to use during the request.
+ #
+ # @param [Sinatra::Request] request The full sinatra request object.
+ def initialize(user, logger, request)
+ @user = user
+ @logger = logger
+ @opts = parse_params(request.params)
+ end
+
+ # Parses and sanitizes query parameters from the sinatra request.
+ #
+ # @return [Hash] the parsed parameter hash
+ def parse_params(params)
+ { :q => params["q"] }
+ end
+
+ # Main entry point for the rest routes. Acts as the final location
+ # for catching any unhandled sequel and db exceptions. By calling
+ # translate_and_log_exception, they will get logged so that we can
+ # address them and will get converted to a generic invalid request
+ # so that they can be investigated and have more accurate error
+ # reporting added.
+ #
+ # @param [Symbol] method The method to dispatch to
+ #
+ # @param [Array] args The arguments to the method beign disptched to.
+ #
+ # @return [Object] Returns an array of [http response code, Header hash,
+ # body string], or just a body string.
+ def dispatch(method, *args)
+ send(method, *args)
+ rescue Sequel::ValidationFailed => e
+ raise self.class.translate_and_log_exception(@logger, e)
+ rescue Sequel::DatabaseError => e
+ raise self.class.translate_and_log_exception(@logger, e)
+ end
+
+ # Create operation
+ #
+ # @param [IO] json An IO object that when read will return the json
+ # serialized request.
+ def create(json)
+ validate_class_access(:create)
+ attributes = Yajl::Parser.new.parse(json)
+ raise InvalidRequest unless attributes
+ obj = model.create_from_hash(attributes)
+ [HTTP::CREATED,
+ { "Location" => "#{self.class.path}/#{obj.id}" },
+ ObjectSerialization.render_json(self.class, obj)]
+ rescue Sequel::ValidationFailed => e
+ raise self.class.translate_validation_exception(e, attributes)
+ end
+
+ # Read operation
+ #
+ # @param [String] id The GUID of the object to read.
+ def read(id)
+ obj = find_id_and_validate_access(:read, id)
+ ObjectSerialization.render_json(self.class, obj)
+ end
+
+ # Update operation
+ #
+ # @param [String] id The GUID of the object to update.
+ #
+ # @param [IO] json An IO object that when read will return the json
+ # serialized request.
+ def update(id, json)
+ obj = find_id_and_validate_access(:update, id)
+ attributes = Yajl::Parser.new.parse(json)
+ obj.update_from_hash(attributes)
+ obj.save
+ [HTTP::CREATED, ObjectSerialization.render_json(self.class, obj)]
+ rescue Sequel::ValidationFailed => e
+ raise self.class.translate_validation_exception(e, attributes)
+ end
+
+ # Delete operation
+ #
+ # @param [String] id The GUID of the object to delete.
+ def delete(id)
+ obj = find_id_and_validate_access(:delete, id)
+ obj.delete
+ [HTTP::NO_CONTENT, nil]
+ rescue Sequel::ValidationFailed => e
+ raise self.class.translate_validation_exception(e, attributes)
+ end
+
+ # Enumerate operation
+ def enumerate
+ # TODO: filter the ds by what the user can see
+ ds = Query.dataset_from_query_params(model,
+ self.class.query_parameters, @opts)
+ resources = []
+ ds.all.each do |m|
+ resources << ObjectSerialization.to_hash(self.class, m)
+ end
+
+ res = {}
+ res[:total_results] = ds.count
+ res[:prev_url] = nil
+ res[:next_url] = nil
+ res[:resources] = resources
+
+ Yajl::Encoder.encode(res, :pretty => true)
+ end
+
+ # Validates if the current user has rights to perform the given operation
+ # on this class of object. Rasies an auth error if not.
+ #
+ # @param [Symbol] op The type of operation to check for access
+ def validate_class_access(op)
+ validate_access(op, model, @user)
+ end
+
+ # Find an object and validate that the current user has rights to
+ # perform the given operation on that instance.
+ #
+ # Raises an exception if the object can't be found or if the current user
+ # doesn't have access to it.
+ #
+ # @param [Symbol] op The type of operation to check for access
+ #
+ # @param [String] id The GUID of the object to find.
+ #
+ # @return [Sequel::Model] The sequel model for the object, only if
+ # the use has access.
+ def find_id_and_validate_access(op, id)
+ obj = model.find(:id => id)
+ if obj
+ validate_access(op, obj, @user)
+ else
+ raise self.class.not_found_exception.new(id) if obj.nil?
+ end
+ obj
+ end
+
+ # Find an object and validate that the given user has rights
+ # to access the instance.
+ #
+ # Raises an exception if the user does not have rights to peform
+ # the operation on the object.
+ #
+ # @param [Symbol] op The type of operation to check for access
+ #
+ # @param [Object] obj The object for which to validate access.
+ #
+ # @param [Models::User] user The user for which to validate access.
+ def validate_access(op, obj, user)
+ user_perms = Permissions.permissions_for(obj, user)
+ unless self.class.op_allowed_by?(op, user_perms)
+ raise NotAuthenticated unless user
+ raise NotAuthorized
+ end
+ end
+
+ # The model associated with this api endpoint.
+ #
+ # @return [Sequel::Model] The model associated with this api endpoint.
+ def model
+ self.class.model
+ end
+
+ class << self
+ include VCAP::CloudController
+
+ attr_accessor :attributes
+ attr_accessor :to_many_relationships
+ attr_accessor :to_one_relationships
+
+ # basename of the class
+ #
+ # @return [String] basename of the class
+ def class_basename
+ self.name.split("::").last
+ end
+
+ # path
+ #
+ # @return [String] The path/route to the collection associated with
+ # the class.
+ def path
+ "#{ROUTE_PREFIX}/#{class_basename.underscore.pluralize}"
+ end
+
+ # path_id
+ #
+ # @return [String] The path/route to an instance of this class.
+ def path_id
+ "#{path}/:id"
+ end
+
+ # Return the url for a specfic id
+ #
+ # @return [String] The url for a specific instance of this class.
+ def url_for_id(id)
+ "#{path}/#{id}"
+ end
+
+ # Model associated with this rest/api endpoint
+ #
+ # @param [String] name The base name of the model class.
+ #
+ # @return [Sequel::Model] The class of the model associated with
+ # this rest endpoint.
+ def model(name = model_class_name)
+ Models.const_get(name)
+ end
+
+ # Model class name associated with this rest/api endpoint.
+ #
+ # @return [String] The class name of the model associated with
+ # this rest endpoint.
+ def model_class_name
+ class_basename
+ end
+
+ # Model class name associated with this rest/api endpoint.
+ #
+ # @return [String] The class name of the model associated with
+ def not_found_exception_name
+ "#{model_class_name}NotFound"
+ end
+
+ # Lookup the not-found exception for this rest/api endpoint.
+ #
+ # @return [Exception] The vcap not-found exception for this
+ # rest/api endpoint.
+ def not_found_exception
+ Errors.const_get(not_found_exception_name)
+ end
+
+ # Get and set the allowed query paramaeters (sent via the q http
+ # query parmameter) for this rest/api endpoint.
+ #
+ # @param [Array] args One or more attributes that can be used
+ # as query parameters.
+ #
+ # @return [Set] If called with no arguments, returns the list
+ # of query parameters.
+ def query_parameters(*args)
+ if args.empty?
+ @query_parameters ||= Set.new
+ else
+ @query_parameters ||= Set.new
+ @query_parameters |= Set.new(args.map { |a| a.to_s })
+ end
+ end
+
+ # Start the DSL for defining attributes. This is used inside
+ # the api controller classes.
+ def define_attributes(&blk)
+ k = Class.new do
+ include ControllerDSL
+ end
+
+ k.new(self).instance_eval(&blk)
+ end
+
+ # Start the DSL for defining attributes. This is used inside
+ # the api controller classes.
+ #
+ def translate_and_log_exception(logger, e)
+ msg = ["exception not translated: #{e.class} - #{e.message}"]
+ msg[0] = msg[0] + ":"
+ msg.concat(e.backtrace).join("\\n")
+ logger.warn(msg.join("\\n"))
+ Errors::InvalidRequest
+ end
+ end
+ end
+end
View
80 lib/cloud_controller/rest_controller/controller_dsl.rb
@@ -0,0 +1,80 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::RestController
+ # DSL that is available inside define_attributes on a rest controller
+ # class.
+ module ControllerDSL
+ include VCAP::RestAPI
+
+ # these aren't *really* necessary, but it makes .inspect on them
+ # a bit more informative
+ class ToManyAttribute < NamedAttribute; end
+ class ToOneAttribute < NamedAttribute; end
+
+ def initialize(controller)
+ @controller = controller
+ end
+
+ # Define an attribute for the api endpoint
+ #
+ # @param [Symbol] name Name of the attribute.
+ #
+ # @param [Class] schema The JsonSchema or class type of the
+ # named attribute.
+ #
+ # @option opts [[Symbol]] :exclude_in One or more symbols representing
+ # an operation types that the attribute is allowed in, e.g.
+ # :exclude_in => :create, or :exclude_in => [:read, :enumerate], etc
+ #
+ # @option opts [[Symbol]] :optional_in One or more symbols representing
+ # an operation types that the attribute is considered optional in.
+ #
+ # @option opts [Object] :default default value for the attribute if it
+ # isn't supplied.
+ def attribute(name, schema, opts = {})
+ attributes[name] = SchemaAttribute.new(name, schema, opts)
+ end
+
+ # Define a to_many relationship for the api endpoint.
+ #
+ # @param [Symbol] name Name of the relationship.
+ #
+ # @option opts [[Symbol]] :exclude_in One or more symbols representing
+ # an operation types that the attribute is allowed in, e.g.
+ # :exclude_in => :create, or :exclude_in => [:read, :enumerate], etc
+ #
+ # @option opts [[Symbol]] :optional_in One or more symbols representing
+ # an operation types that the attribute is considered optional in.
+ def to_many(name, opts = {})
+ to_many_relationships[name] = ToManyAttribute.new(name, opts)
+ end
+
+ # Define a to_one relationship for the api endpoint.
+ #
+ # @param [Symbol] name Name of the relationship.
+ #
+ # @option opts [[Symbol]] :exclude_in One or more symbols representing
+ # an operation types that the attribute is allowed in, e.g.
+ # :exclude_in => :create, or :exclude_in => [:read, :enumerate], etc
+ #
+ # @option opts [[Symbol]] :optional_in One or more symbols representing
+ # an operation types that the attribute is considered optional in.
+ def to_one(name, opts = {})
+ to_one_relationships[name] = ToOneAttribute.new(name, opts)
+ end
+
+ private
+
+ def attributes
+ @controller.attributes ||= {}
+ end
+
+ def to_many_relationships
+ @controller.to_many_relationships ||= {}
+ end
+
+ def to_one_relationships
+ @controller.to_one_relationships ||= {}
+ end
+ end
+end
View
67 lib/cloud_controller/rest_controller/messages.rb
@@ -0,0 +1,67 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::RestController
+ # Auto generation of Message classes based on the attributes
+ # exposed by a rest endpoint.
+ module Messages
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ # Define the messages exposed by a rest endpoint.
+ def define_messages
+ [:response, :create, :update].each do |type|
+ define_message(type)
+ end
+ end
+
+ private
+
+ def define_message(type)
+ attrs = attributes
+ to_one = @to_one_relationships ||= []
+ to_many = @to_many_relationships ||= []
+
+ klass = Class.new VCAP::RestAPI::Message do
+ attrs.each do |name, attr|
+ unless attr.exclude_in?(type)
+ if (type == :update || (type == :create && attr.default))
+ optional name, attr.schema
+ else
+ required name, attr.schema
+ end
+ end
+ end
+
+ to_one.each do |name, relation|
+ unless relation.exclude_in?(type)
+ if (type == :update || (type == :create &&
+ relation.optional_in?(type)))
+ optional "#{name}_id", Integer
+ else
+ required "#{name}_id", Integer
+ end
+
+ if type == :response
+ optional "#{name}_url", VCAP::RestAPI::Message::HTTPS_URL
+ end
+ end
+ end
+
+ to_many.each do |name, relation|
+ unless relation.exclude_in?(type)
+ if type == :response
+ optional "#{name}_url", VCAP::RestAPI::Message::HTTPS_URL
+ else
+ optional "#{name}_id", [Integer]
+ end
+ end
+ end
+ end
+
+ self.const_set "#{type.to_s.camelize}Message", klass
+ end
+ end
+ end
+end
View
67 lib/cloud_controller/rest_controller/object_serialization.rb
@@ -0,0 +1,67 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::RestController
+ # Serialize objects according in the format required by the vcap
+ # rest api.
+ #
+ # TODO: migrate this to be like messages and routes in that
+ # it is included and mixed in rather than having the controller
+ # passed into it?
+ module ObjectSerialization
+ PRETTY_DEFAULT = true
+
+ # Render an object to json, using export and security properties
+ # set by its controller.
+ #
+ # @param [RestController] controller Controller for the object being
+ # encoded.
+ #
+ # @param [Sequel::Model] obj Object to encode.
+ #
+ # @option opts [Boolean] :pretty Controlls pretty formating of the encoded
+ # json. Defaults to true.
+ #
+ # @return [String] Json encoding of the object.
+ def self.render_json(controller, obj, opts = {})
+ opts[:pretty] = PRETTY_DEFAULT unless opts.has_key?(:pretty)
+ Yajl::Encoder.encode(to_hash(controller, obj), :pretty => opts[:pretty])
+ end
+
+ # Render an object as a hash, using export and security properties
+ # set by its controller.
+ #
+ # @param [RestController] controller Controller for the object being
+ # serialized.
+ #
+ # @param [Sequel::Model] obj Object to encode.
+ #
+ # @return [Hash] Hash encoding of the object.
+ def self.to_hash(controller, obj)
+ rel_hash = relations_hash(controller, obj)
+
+ # TODO: this needs to do a read authz check.
+ entity_hash = obj.to_hash.merge(rel_hash)
+
+ metadata_hash = {
+ "id" => obj.id,
+ "url" => controller.url_for_id(obj.id),
+ "created_at" => obj.created_at,
+ "updated_at" => obj.updated_at
+ }
+
+ { "metadata" => metadata_hash, "entity" => entity_hash }
+ end
+
+ private
+
+ def self.relations_hash(controller, obj)
+ res = {}
+ # FIXME: to_one also
+ controller.to_many_relationships.each do |name, attr|
+ key = "#{controller.class_basename.underscore}_id"
+ res["#{name}_url"] = "/v2/#{name}?q=#{key}:#{obj.id}"
+ end
+ res
+ end
+ end
+end
View
65 lib/cloud_controller/rest_controller/routes.rb
@@ -0,0 +1,65 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::RestController
+
+ # Define routes for the rest endpoint.
+ module Routes
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+
+ # Define routes for the rest endpoint.
+ def define_routes
+ define_create_route
+ define_read_route
+ define_update_route
+ define_delete_route
+ define_enumerate_route
+ end
+
+ private
+
+ def define_create_route
+ klass = self
+ controller.post path, :consumes => [:json] do
+ klass.new(@user, logger, request).dispatch(:create, request.body)
+ end
+ end
+
+ def define_read_route
+ klass = self
+ controller.get path_id do |id|
+ klass.new(@user, logger, request).dispatch(:read, id)
+ end
+ end
+
+ def define_update_route
+ klass = self
+ controller.put path_id, :consumes => [:json] do |id|
+ klass.new(@user, logger, request).dispatch(:update, id, request.body)
+ end
+ end
+
+ def define_delete_route
+ klass = self
+ controller.delete path_id do |id|
+ klass.new(@user, logger, request).dispatch(:delete, id)
+ end
+ end
+
+ def define_enumerate_route
+ klass = self
+ controller.get path, do
+ klass.new(@user, logger, request).dispatch(:enumerate)
+ end
+ end
+
+ def controller
+ VCAP::CloudController::Controller
+ end
+
+ end
+ end
+end
View
24 spec/api/app_space_spec.rb
@@ -0,0 +1,24 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::AppSpace do
+ # FIXME: do this via path?
+ let(:org) { VCAP::CloudController::Models::Organization.make }
+ let(:app_space) { VCAP::CloudController::Models::AppSpace.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/app_spaces",
+ :model => VCAP::CloudController::Models::AppSpace,
+ :basic_attributes => [:name, :organization_id],
+ :required_attributes => [:name, :organization_id],
+ :unique_attributes => [:name, :organization_id],
+ :many_to_many_collection_ids => {
+ :users => lambda { |app_space| make_user_for_app_space(app_space) }
+ },
+ :one_to_many_collection_ids => {
+ :apps => lambda { |app_space| VCAP::CloudController::Models::App.make }
+ }
+ }
+
+end
View
34 spec/api/app_spec.rb
@@ -0,0 +1,34 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::App do
+ let(:app_obj) { VCAP::CloudController::Models::App.make }
+ let(:app_space) { VCAP::CloudController::Models::AppSpace.make }
+ let(:runtime) { VCAP::CloudController::Models::Runtime.make }
+ let(:framework) { VCAP::CloudController::Models::Framework.make }
+
+ # FIXME: make app_space_id a relation check that checks the id and the url
+ # part. do everywhere
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/apps",
+ :model => VCAP::CloudController::Models::App,
+ :basic_attributes => [:name, :app_space_id, :runtime_id, :framework_id],
+ :required_attributes => [:name, :app_space_id, :runtime_id, :framework_id],
+ :unique_attributes => [:name, :app_space_id],
+
+ :many_to_one_collection_ids => {
+ :app_space => lambda { |app| VCAP::CloudController::Models::AppSpace.make },
+ :framework => lambda { |app| VCAP::CloudController::Models::Framework.make },
+ :runtime => lambda { |app| VCAP::CloudController::Models::Runtime.make }
+ },
+ :one_to_many_collection_ids => {
+ :service_bindings =>
+ lambda { |app|
+ service_binding = VCAP::CloudController::Models::ServiceBinding.make
+ service_binding.service_instance.app_space = app.app_space
+ service_binding
+ }
+ }
+ }
+end
View
19 spec/api/framework_spec.rb
@@ -0,0 +1,19 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::Framework do
+ let(:framework) { VCAP::CloudController::Models::Framework.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/frameworks",
+ :model => VCAP::CloudController::Models::Framework,
+ :basic_attributes => [:name, :description],
+ :required_attributes => [:name, :description],
+ :unique_attributes => :name,
+ :one_to_many_collection_ids => {
+ :apps => lambda { |framework| VCAP::CloudController::Models::App.make }
+ }
+ }
+
+end
View
208 spec/api/helpers/collections.rb
@@ -0,0 +1,208 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::ApiSpecHelper
+ shared_context "collections" do |opts, attr, make|
+ before do
+ @opts = opts
+ @attr = attr
+
+ @child_name = attr.to_s.singularize
+
+ @add_method = "add_#{@child_name}"
+ @get_method = "#{@child_name}s"
+
+ @obj = opts[:model].make
+ @other_obj = opts[:model].make
+
+ @child1 = make.call(@obj)
+ @child2 = make.call(@obj)
+ @child3 = make.call(@obj)
+
+ user = VCAP::CloudController::Models::User.make(:admin => true)
+ headers = {}
+ headers["HTTP_AUTHORIZATION"] = user.id
+ @headers = json_headers(headers)
+ end
+
+ def do_write(verb, children, expected_result, expected_children)
+ body = Yajl::Encoder.encode({"#{@child_name}_ids" => children.map { |c| c[:id] }})
+ send(verb, "#{@opts[:path]}/#{@obj.id}", body, @headers)
+ last_response.status.should == expected_result
+
+ @obj.refresh
+ @obj.send(@get_method).length.should == expected_children.length
+ expected_children.each { |c| @obj.send(@get_method).should include(c) }
+ end
+ end
+
+ shared_examples "collection operations" do |opts|
+ describe "collections" do
+ describe "modifying one_to_many collections" do
+ opts[:one_to_many_collection_ids].each do |attr, make|
+ describe "#{attr}" do
+ include_context "collections", opts, attr, make
+ child_name = attr.to_s
+
+ describe "PUT #{opts[:path]}/:id with #{attr} in the request body" do
+ # FIXME: right now, we ignore invalid input
+ it "should return 200 but have no effect (FIXME: extra params on a PUT are currently ignored)" do
+ do_write(:put, [@child1], 201, [])
+ end
+ end
+ end
+ end
+ end
+
+ describe "modifying many_to_many collections" do
+ opts[:many_to_many_collection_ids].each do |attr, make|
+ describe "#{attr}" do
+ include_context "collections", opts, attr, make
+ child_name = attr.to_s.chomp("_ids")
+ path = "#{opts[:path]}/:id"
+
+ describe "POST #{path} with only #{attr} in the request body" do
+ before do
+ do_write(:post, [@child1], 404, [])
+ end
+
+ it "should return 404" do
+ last_response.status.should == 404
+ end
+
+ it_behaves_like "a vcap rest error response"
+ end
+
+ describe "PUT #{path} with only #{attr} in body" do
+ it "[:valid_id] should add a #{attr.to_s.singularize}" do
+ do_write(:put, [@child1], 201, [@child1])
+ end
+
+ it "[:valid_id1, :valid_id2] should add multiple #{attr}" do
+ do_write(:put, [@child1, @child2], 201, [@child1, @child2])
+ end
+
+ it "[:valid_id1, :valid_id2] should replace existing #{attr}" do
+ @obj.send(@add_method, @child1)
+ @obj.send(@get_method).should include(@child1)
+ do_write(:put, [@child2, @child3], 201, [@child2, @child3])
+ @obj.send(@get_method).should_not include(@child1)
+ end
+
+ it "[] should remove all #{child_name}s" do
+ @obj.send(@add_method, @child1)
+ @obj.send(@get_method).should include(@child1)
+ do_write(:put, [], 201, [])
+ @obj.send(@get_method).should_not include(@child1)
+ end
+
+ it "[:invalid_id] should return 400" do
+ @obj.send(@add_method, @child1)
+ @obj.send(@get_method).should include(@child1)
+ do_write(:put, [], 201, [])
+ @obj.send(@get_method).should_not include(@child1)
+ end
+
+ # FIXME: add an error id in the middle of an array test
+
+ # FIXME: other bad json input tests
+ end
+ end
+ end
+ end
+
+ describe "reading collections" do
+ include VCAP::CloudController::RestController
+
+ opts[:many_to_many_collection_ids].merge(opts[:one_to_many_collection_ids]).each do |attr, make|
+ path = "#{opts[:path]}/:id"
+
+ describe "GET #{path} and extract #{attr}_url" do
+ include_context "collections", opts, attr, make
+
+ before do
+ get "#{opts[:path]}/#{@obj.id}", {}, @headers
+ @uri = entity["#{attr}_url"]
+ end
+
+ it "should return a relative uri in the #{attr}_url field" do
+ @uri.should_not be_nil
+ end
+
+ describe "gets on the #{attr}_url with no associated #{attr}" do
+ before do
+ get @uri, {}, @headers
+ end
+
+ it "should return 200" do
+ last_response.status.should == 200
+ end
+
+ it "should return total_results => 0" do
+ decoded_response["total_results"].should == 0
+ end
+
+ it "should return prev_url => nil" do
+ decoded_response.should have_key("prev_url")
+ decoded_response["prev_url"].should be_nil
+ end
+
+ it "should return next_url => nil" do
+ decoded_response["next_url"].should be_nil
+ end
+
+ it "should return resources => []" do
+ decoded_response["resources"].should == []
+ end
+ end
+
+ describe "gets on the #{attr}_url with 2 associated #{attr}" do
+ before do
+ @obj.send(@add_method, @child1)
+ @obj.send(@add_method, @child2)
+ @obj.save
+
+ get @uri, {}, @headers
+ end
+
+ it "should return 200" do
+ last_response.status.should == 200
+ end
+
+ it "should return total_results => 2" do
+ decoded_response["total_results"].should == 2
+ end
+
+ # TODO: these are both nil for now because we aren't doing
+ # full pagination yet
+ it "should return prev_url => nil" do
+ decoded_response.should have_key("prev_url")
+ decoded_response["prev_url"].should be_nil
+ end
+
+ it "should return next_url => nil" do
+ decoded_response["next_url"].should be_nil
+ end
+
+ it "should return resources => [child1, child2]" do
+ os = VCAP::CloudController::RestController::ObjectSerialization
+ name ="#{attr.to_s.singularize.camelize}"
+ child_controller = VCAP::CloudController.const_get(name)
+
+ c1 = os.to_hash(child_controller, @child1)
+ c2 = os.to_hash(child_controller, @child2)
+
+ [c1, c2].each do |c|
+ m = c["metadata"]
+ m["created_at"] = m["created_at"].to_s
+ m["updated_at"] = m["updated_at"].to_s if m["updated_at"]
+ end
+
+ decoded_response["resources"].should == [c1, c2]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
View
229 spec/api/helpers/creating_and_updating.rb
@@ -0,0 +1,229 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::ApiSpecHelper
+
+ shared_examples "creating and updating" do |opts|
+ describe "creating and updating" do
+ # we use the template object to automatically get values
+ # to use during creation from sham
+ template_obj = TemplateObj.new(opts[:model], opts[:required_attributes])
+
+ let(:creation_opts) do
+ # we potentially need to regenerate associations as the db
+ # gets wiped between tests
+ template_obj.refresh
+ attrs = template_obj.attributes.dup
+
+ opts[:extra_attributes].each do |attr|
+ if opts[:required_attributes].include?(attr)
+ attrs[attr.to_s] = Sham.send(attr)
+ end
+ end
+
+ attrs
+ end
+
+ let(:non_synthetic_creation_opts) do
+ res = {}
+ creation_opts.each do |k, v|
+ res[k] = v unless opts[:extra_attributes].include?(k)
+ end
+ res
+ end
+
+ [:post, :put].each do |verb|
+ path_desc = opts[:path]
+ path_desc = "#{opts[:path]}/:id" if verb == :put
+ describe "#{verb.to_s.upcase} #{path_desc}" do
+ context "with all required attributes" do
+ before do
+ json_body = Yajl::Encoder.encode(creation_opts)
+
+ case verb
+ when :post
+ post opts[:path], json_body, json_headers(admin_headers)
+ when :put
+ obj = opts[:model].make creation_opts
+ @orig_created_at = obj.created_at
+ put("#{opts[:path]}/#{obj.id}",
+ json_body, json_headers(admin_headers))
+ end
+ end
+
+ it "should return 201" do
+ last_response.status.should == 201
+ end
+
+ include_examples "return a vcap rest encoded object"
+
+ it "should return the json encoded object in the entity hash" do
+ non_synthetic_creation_opts.keys.each do |k|
+ entity[k.to_s].should_not be_nil
+ entity[k.to_s].should == creation_opts[k]
+ end
+ end
+
+ case verb
+ when :post
+ it "should return the path to the new instance in the location header" do
+ last_response.location.should_not be_nil
+ last_response.location.should match /#{opts[:path]}\/[^ \/]/
+ metadata["url"].should == last_response.location
+ end
+
+ it "should have created the object pointed to in the location header" do
+ obj_id = last_response.location.split("/").last
+ obj = opts[:model][obj_id]
+ non_synthetic_creation_opts.keys.each do |k|
+ obj.send(k).should == creation_opts[k]
+ end
+ end
+
+ it "should have a recent created_at timestamp" do
+ Time.parse(metadata["created_at"]).should be_recent
+ end
+
+ it "should not have an updated_at timestamp" do
+ metadata["updated_at"].should be_nil
+ end
+ when :put
+ it "should not update the created_at timestamp" do
+ metadata["created_at"].should == @orig_created_at.to_s
+ end
+
+ it "should have a recent updated_at timestamp" do
+ metadata["updated_at"].should_not be_nil
+ Time.parse(metadata["updated_at"]).should be_recent
+ end
+ end
+ end
+
+ #
+ # Test each of the required attributes
+ #
+ req_attrs = opts[:required_attributes].dup
+ req_attrs = req_attrs - ["id"] if verb == :put
+ req_attrs.each do |without_attr|
+ context "without the :#{without_attr.to_s} attribute" do
+ let(:filtered_opts) do
+ creation_opts.select do |k, v|
+ k != without_attr.to_s and k != "#{without_attr}_id"
+ end
+ end
+
+ @expected_status = nil
+
+ before do
+ case verb
+ when :post
+ post opts[:path], Yajl::Encoder.encode(filtered_opts), json_headers(admin_headers)
+ when :put
+ obj = opts[:model].make creation_opts
+ put "#{opts[:path]}/#{obj.id}", Yajl::Encoder.encode(filtered_opts), json_headers(admin_headers)
+ end
+ end
+
+ case verb
+ when :post
+ expected_status = 400
+ when :put
+ expected_status = 201
+ end
+
+ it "should return a #{expected_status}" do
+ last_response.status.should == expected_status
+ end
+
+ it "should not return a location header" do
+ last_response.location.should be_nil
+ end
+
+ if verb == :post
+ it_behaves_like "a vcap rest error response", /invalid/
+ end
+ end
+ end
+
+ #
+ # If there are multiple unique attributes, vary them one a time
+ #
+ if opts[:unique_attributes] and opts[:unique_attributes].length > 1
+ opts[:unique_attributes].each do |new_attr|
+ new_attr = new_attr.to_s
+ context "with duplicate attributes other than #{new_attr}" do
+ # FIXME: this is a cut/paste from the model spec, refactor
+ let(:dup_opts) do
+ if opts[:model].associations.include?(new_attr)
+ new_attr = "#{new_attr}_id"
+ end
+
+ # FIXME: this name isn't right now that it is shared with PUT
+ new_creation_opts = creation_opts.dup
+ orig_obj = opts[:model].create new_creation_opts
+ orig_obj.should be_valid
+
+ create_attribute = opts[:create_attribute]
+
+ # create the attribute using the caller supplied lambda,
+ # otherwise, create a second template object and fetch
+ # the value from that
+ val = nil
+ if create_attribute
+ # FIXME: do we use this? do we use it in the model specs
+ val = create_attribute.call(new_attr)
+ end
+
+ if val.nil?
+ another_obj = TemplateObj.new(opts[:model], opts[:required_attributes])
+ another_obj.refresh
+ val = another_obj.attributes[new_attr]
+ end
+
+ new_creation_opts[new_attr] = val
+ new_creation_opts
+ end
+
+ it "should succeed" do
+ post opts[:path], Yajl::Encoder.encode(dup_opts), json_headers(admin_headers)
+ last_response.status.should == 201
+ end
+ end
+ end
+ end
+
+ #
+ # make sure we get failures if all of the unique attributes are the
+ # same
+ #
+ dup_attrs = opts[:unique_attributes].dup
+ dup_attrs = dup_attrs - ["id"] if verb == :put
+ unless dup_attrs.empty?
+ desc = dup_attrs.map { |v| ":#{v}" }.join(", ")
+ desc = "[#{desc}]" if opts[:unique_attributes].length > 1
+ context "with duplicate #{desc}" do
+ before do
+ obj = opts[:model].make creation_opts
+ obj.should be_valid
+
+ case verb
+ when :post
+ post opts[:path], Yajl::Encoder.encode(creation_opts), json_headers(admin_headers)
+ when :put
+ dup_obj = opts[:model].make
+ put "#{opts[:path]}/#{dup_obj.id}", Yajl::Encoder.encode(creation_opts), json_headers(admin_headers)
+ end
+ end
+
+ it "should return 400" do
+ last_response.status.should == 400
+ end
+
+ it_behaves_like "a vcap rest error response", /taken/
+ end
+ end
+
+ end
+ end
+ end
+ end
+end
View
23 spec/api/helpers/deleting.rb
@@ -0,0 +1,23 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::ApiSpecHelper
+ shared_examples "deleting a valid object" do |opts|
+ describe "deleting a valid object" do
+ describe "DELETE #{opts[:path]}/:id" do
+ let (:obj) { opts[:model].make }
+
+ before do
+ delete "#{opts[:path]}/#{obj.id}", {}, admin_headers
+ end
+
+ it "should return 204" do
+ last_response.status.should == 204
+ end
+
+ it "should return an empty response body" do
+ last_response.body.should be_empty
+ end
+ end
+ end
+ end
+end
View
33 spec/api/helpers/invalid_resource.rb
@@ -0,0 +1,33 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::ApiSpecHelper
+ shared_examples "operations on an invalid object" do |opts|
+ describe "operations on an invalid object" do
+ describe "POST #{opts[:path]}/:invalid_id/" do
+ before do
+ post "#{opts[:path]}/999999", {}, json_headers(admin_headers)
+ end
+
+ it "should return 404" do
+ last_response.status.should == 404
+ end
+
+ it_behaves_like "a vcap rest error response", /Unknown request/
+ end
+
+ [:put, :delete, :get].each do |verb|
+ describe "#{verb.upcase} #{opts[:path]}/:invalid_id/" do
+ before do
+ send(verb, "#{opts[:path]}/999999", {}, json_headers(admin_headers))
+ end
+
+ it "should return 400" do
+ last_response.status.should == 400
+ end
+
+ it_behaves_like "a vcap rest error response", "not be found: 999999"
+ end
+ end
+ end
+ end
+end
View
32 spec/api/helpers/reading.rb
@@ -0,0 +1,32 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+module VCAP::CloudController::ApiSpecHelper
+ shared_examples "reading a valid object" do |opts|
+ describe "reading a valid object" do
+ describe "GET #{opts[:path]}/:id" do
+ let (:obj) { opts[:model].make }
+
+ before do
+ get "#{opts[:path]}/#{obj.id}", {}, json_headers(admin_headers)
+ end
+
+ it "should return 200" do
+ last_response.status.should == 200
+ end
+
+ include_examples "return a vcap rest encoded object"
+
+ it "should return the json encoded object in the response body" do
+ expected = obj.to_hash
+ expected.each { |k, v| expected[k] = v.to_s if v.kind_of?(Time) }
+
+ # filter out the relation urls.
+ parsed = entity.select do |k, v|
+ expected.has_key?(k) || (not k =~ /_url/)
+ end
+ parsed.should == expected
+ end
+ end
+ end
+ end
+end
View
3 spec/api/info_spec.rb
@@ -0,0 +1,3 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
View
21 spec/api/organization_spec.rb
@@ -0,0 +1,21 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::Organization do
+ let(:org) { VCAP::CloudController::Models::Organization.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/organizations",
+ :model => VCAP::CloudController::Models::Organization,
+ :basic_attributes => :name,
+ :required_attributes => :name,
+ :unique_attributes => :name,
+ :many_to_many_collection_ids => {
+ :users => lambda { |org| VCAP::CloudController::Models::User.make }
+ },
+ :one_to_many_collection_ids => {
+ :app_spaces => lambda { |org| VCAP::CloudController::Models::AppSpace.make }
+ }
+ }
+end
View
19 spec/api/runtime_spec.rb
@@ -0,0 +1,19 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::Runtime do
+ let(:runtime) { VCAP::CloudController::Models::Runtime.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/runtimes",
+ :model => VCAP::CloudController::Models::Runtime,
+ :basic_attributes => [:name, :description],
+ :required_attributes => [:name, :description],
+ :unique_attributes => :name,
+ :one_to_many_collection_ids => {
+ :apps => lambda { |framework| VCAP::CloudController::Models::App.make }
+ }
+ }
+
+end
View
18 spec/api/service_auth_token_spec.rb
@@ -0,0 +1,18 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::ServiceAuthToken do
+ let(:service_auth_token) { VCAP::CloudController::Models::ServiceAuthToken.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/service_auth_tokens",
+ :model => VCAP::CloudController::Models::ServiceAuthToken,
+ :basic_attributes => [:label, :provider],
+ :required_attributes => [:label, :provider, :token],
+ :unique_attributes => [:label, :provider],
+ :extra_attributes => :token,
+ :sensitive_attributes => :token
+ }
+
+end
View
17 spec/api/service_binding_spec.rb
@@ -0,0 +1,17 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::ServiceBinding do
+ let(:service_binding) { VCAP::CloudController::Models::ServiceBinding.make }
+ let(:app_obj) { VCAP::CloudController::Models::App.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/service_bindings",
+ :model => VCAP::CloudController::Models::ServiceBinding,
+ :basic_attributes => [:credentials, :binding_options, :vendor_data, :app_id, :service_instance_id],
+ :required_attributes => [:credentials, :app_id, :service_instance_id],
+ :unique_attributes => [:app_id, :service_instance_id]
+ }
+
+end
View
23 spec/api/service_instance_spec.rb
@@ -0,0 +1,23 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::ServiceInstance do
+ let(:app_space) { VCAP::CloudController::Models::AppSpace.make }
+ let(:service_plan) { VCAP::CloudController::Models::ServicePlan.make }
+ let(:service_instance) { VCAP::CloudController::Models::ServiceInstance.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/service_instances",
+ :model => VCAP::CloudController::Models::ServiceInstance,
+ :basic_attributes => [:name, :credentials, :vendor_data],
+ :required_attributes => [:name, :credentials, :app_space_id, :service_plan_id],
+ :unique_attributes => [:app_space_id, :name],
+ :one_to_many_collection_ids => {
+ :service_bindings => lambda { |service_instance|
+ make_service_binding_for_service_instance(service_instance)
+ }
+ }
+ }
+
+end
View
20 spec/api/service_plan_spec.rb
@@ -0,0 +1,20 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::ServicePlan do
+ let(:service_plan) { VCAP::CloudController::Models::ServicePlan.make }
+ let(:service) { VCAP::CloudController::Models::Service.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/service_plans",
+ :model => VCAP::CloudController::Models::ServicePlan,
+ :basic_attributes => [:name, :description, :service_id],
+ :required_attributes => [:name, :description, :service_id],
+ :unique_attributes => [:name, :service_id],
+ :one_to_many_collection_ids => {
+ :service_instances => lambda { |service_plan| VCAP::CloudController::Models::ServiceInstance.make }
+ }
+ }
+
+end
View
19 spec/api/service_spec.rb
@@ -0,0 +1,19 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::Service do
+ let(:service) { VCAP::CloudController::Models::Service.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/services",
+ :model => VCAP::CloudController::Models::Service,
+ :basic_attributes => [:label, :provider, :url, :type, :description, :version, :info_url],
+ :required_attributes => [:label, :provider, :url, :type, :description, :version],
+ :unique_attributes => [:label, :provider],
+ :one_to_many_collection_ids => {
+ :service_plans => lambda { |service| VCAP::CloudController::Models::ServicePlan.make }
+ }
+ }
+
+end
View
93 spec/api/spec_helper.rb
@@ -0,0 +1,93 @@
+# Copyright (c) 2009-2012 VMware, Inc.
+
+require File.expand_path("../../spec_helper", __FILE__)
+
+Dir[File.expand_path("../helpers/*", __FILE__)].each do |file|
+ require file
+end
+
+module VCAP::CloudController::ApiSpecHelper
+ include VCAP::CloudController::ModelSpecHelper
+
+ def app
+ VCAP::CloudController::Controller.new
+ end
+
+ def headers_for(user, proxy_user = nil, https = false)
+ headers = {}
+ # FIXME: we should be using the UAA now, so fix this to *really* use it
+ headers["HTTP_AUTHORIZATION"] = user.id if user
+ headers["HTTP_PROXY_USER"] = proxy_user.id if proxy_user
+ headers["X-Forwarded_Proto"] = "https" if https
+ headers
+ end
+
+ def json_headers(headers)
+ headers.merge({ "CONTENT_TYPE" => "application/json"})
+ end
+
+ shared_examples "a CloudController API" do |opts|
+ [:required_attributes, :unique_attributes, :basic_attributes,
+ :extra_attributes, :sensitive_attributes].each do |k|
+ opts[k] ||= []
+ opts[k] = Array[opts[k]] unless opts[k].respond_to?(:each)
+ opts[k].map! { |v| v.to_s }
+ end
+
+ [:many_to_many_collection_ids, :one_to_many_collection_ids].each do |k|
+ opts[k] ||= {}
+ end
+
+ let(:admin_headers) do
+ user = VCAP::CloudController::Models::User.make(:admin => true)
+ headers_for(user)
+ end
+
+ def decoded_response
+ Yajl::Parser.parse(last_response.body)
+ end
+
+ def metadata
+ decoded_response["metadata"]
+ end
+
+ def entity
+ decoded_response["entity"]
+ end
+
+ include_examples "creating and updating", opts
+ include_examples "reading a valid object", opts
+ include_examples "deleting a valid object", opts
+ include_examples "operations on an invalid object", opts
+ include_examples "collection operations", opts
+
+ # FIXME: add update of :created_at, :updated_at, :id, should all fail
+ end
+
+ shared_examples "return a vcap rest encoded object" do
+ it "should return a metadata hash in the response" do
+ metadata.should_not be_nil
+ metadata.should be_a_kind_of(Hash)
+ end
+
+ it "should return an id in the metadata" do
+ metadata["id"].should_not be_nil
+ # used to check if the id was an integer here, but now users
+ # use uaa based ids, which are strings.
+ end
+
+ it "should return a url in the metadata" do
+ metadata["url"].should_not be_nil
+ metadata["url"].should be_a_kind_of(String)
+ end
+
+ it "should return an entity hash in the response" do
+ entity.should_not be_nil
+ entity.should be_a_kind_of(Hash)
+ end
+ end
+end
+
+RSpec.configure do |conf|
+ conf.include VCAP::CloudController::ApiSpecHelper
+end
View
24 spec/api/user_spec.rb
@@ -0,0 +1,24 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+
+require File.expand_path("../spec_helper", __FILE__)
+
+describe VCAP::CloudController::User do
+ let(:user) { u = VCAP::CloudController::Models::User.make }
+
+ it_behaves_like "a CloudController API", {
+ :path => "/v2/users",
+ :model => VCAP::CloudController::Models::User,
+ :basic_attributes => :id,
+ :required_attributes => :id,
+ :unique_attributes => :id,
+ :many_to_many_collection_ids => {
+ :organizations => lambda { |user| VCAP::CloudController::Models::Organization.make },
+ :app_spaces => lambda { |user|
+ org = VCAP::CloudController::Models::Organization.make
+ user.add_organization(org)
+ VCAP::CloudController::Models::AppSpace.make(:organization => org)
+ }
+ }
+ }
+
+end

0 comments on commit 69e6cce

Please sign in to comment.