From bdda3551bcb05a1b6a96022854b1cebd4fc21ec8 Mon Sep 17 00:00:00 2001 From: Aaron Schlesinger Date: Fri, 9 Jun 2017 10:43:15 -0700 Subject: [PATCH 1/6] Adding a fake broker server This is a continuation of https://github.com/kubernetes-incubator/service-catalog/pull/533, and is a pre-requisite for https://github.com/kubernetes-incubator/service-catalog/pull/923 --- glide.lock | 73 +- glide.yaml | 2 + pkg/brokerapi/fake/server/bind_request.go | 12 + pkg/brokerapi/fake/server/convert_catalog.go | 44 + pkg/brokerapi/fake/server/create_func.go | 17 + .../fake/server/deprovision_request.go | 11 + pkg/brokerapi/fake/server/handler.go | 110 ++ pkg/brokerapi/fake/server/init.go | 9 + .../fake/server/last_operation_request.go | 6 + .../fake/server/provision_request.go | 12 + pkg/brokerapi/fake/server/server.go | 18 + pkg/brokerapi/fake/server/unbind_request.go | 12 + pkg/brokerapi/fake/server/update_request.go | 12 + .../ww/goautoneg/.hg_archival.txt | 1 + vendor/code.cloudfoundry.org/lager/LICENSE | 201 +++ vendor/code.cloudfoundry.org/lager/NOTICE | 15 + vendor/code.cloudfoundry.org/lager/README.md | 78 + .../code.cloudfoundry.org/lager/chug/chug.go | 130 ++ .../lager/chug/chug_suite_test.go | 13 + .../lager/chug/chug_test.go | 247 +++ .../lager/chug/match_log_entry_test.go | 41 + .../lager/ginkgoreporter/ginkgo_reporter.go | 155 ++ .../ginkgoreporter_suite_test.go | 13 + .../ginkgoreporter/ginkgoreporter_test.go | 185 ++ .../lager/lager_suite_test.go | 13 + .../lager/lagertest/test_sink.go | 71 + vendor/code.cloudfoundry.org/lager/logger.go | 179 ++ .../lager/logger_test.go | 358 ++++ vendor/code.cloudfoundry.org/lager/models.go | 30 + .../lager/reconfigurable_sink.go | 35 + .../lager/reconfigurable_sink_test.go | 66 + .../lager/writer_sink.go | 38 + .../lager/writer_sink_test.go | 107 ++ vendor/github.com/gorilla/mux/README.md | 19 +- vendor/github.com/gorilla/mux/mux_test.go | 278 ++- vendor/github.com/gorilla/mux/route.go | 49 +- .../pivotal-cf/brokerapi/.travis.yml | 11 + .../pivotal-cf/brokerapi/Godeps/Godeps.json | 180 ++ .../pivotal-cf/brokerapi/Godeps/Readme | 5 + .../github.com/pivotal-cf/brokerapi/LICENSE | 201 +++ vendor/github.com/pivotal-cf/brokerapi/NOTICE | 10 + .../github.com/pivotal-cf/brokerapi/README.md | 31 + vendor/github.com/pivotal-cf/brokerapi/api.go | 355 ++++ .../pivotal-cf/brokerapi/api_suite_test.go | 39 + .../pivotal-cf/brokerapi/api_test.go | 1541 +++++++++++++++++ .../pivotal-cf/brokerapi/auth/auth.go | 44 + .../brokerapi/auth/auth_suite_test.go | 13 + .../pivotal-cf/brokerapi/auth/auth_test.go | 102 ++ .../pivotal-cf/brokerapi/catalog.go | 65 + .../pivotal-cf/brokerapi/catalog_test.go | 163 ++ .../pivotal-cf/brokerapi/failure_response.go | 113 ++ .../brokerapi/failure_response_test.go | 82 + .../brokerapi/fakes/fake_service_broker.go | 324 ++++ .../brokerapi/fixtures/async_required.json | 4 + .../brokerapi/fixtures/binding.json | 8 + ...nding_with_experimental_volume_mounts.json | 17 + .../fixtures/binding_with_route_service.json | 9 + .../fixtures/binding_with_syslog.json | 9 + .../fixtures/binding_with_volume_mounts.json | 20 + .../brokerapi/fixtures/catalog.json | 27 + .../fixtures/instance_limit_error.json | 3 + .../invalid_async_provision_error.json | 3 + .../fixtures/last_operation_succeeded.json | 4 + .../fixtures/operation_data_response.json | 3 + .../brokerapi/fixtures/provisioning.json | 1 + .../fixtures/provisioning_with_dashboard.json | 3 + .../pivotal-cf/brokerapi/response.go | 49 + .../pivotal-cf/brokerapi/response_test.go | 70 + .../pivotal-cf/brokerapi/service_broker.go | 187 ++ 69 files changed, 6264 insertions(+), 92 deletions(-) create mode 100644 pkg/brokerapi/fake/server/bind_request.go create mode 100644 pkg/brokerapi/fake/server/convert_catalog.go create mode 100644 pkg/brokerapi/fake/server/create_func.go create mode 100644 pkg/brokerapi/fake/server/deprovision_request.go create mode 100644 pkg/brokerapi/fake/server/handler.go create mode 100644 pkg/brokerapi/fake/server/init.go create mode 100644 pkg/brokerapi/fake/server/last_operation_request.go create mode 100644 pkg/brokerapi/fake/server/provision_request.go create mode 100644 pkg/brokerapi/fake/server/server.go create mode 100644 pkg/brokerapi/fake/server/unbind_request.go create mode 100644 pkg/brokerapi/fake/server/update_request.go create mode 100644 vendor/code.cloudfoundry.org/lager/LICENSE create mode 100644 vendor/code.cloudfoundry.org/lager/NOTICE create mode 100644 vendor/code.cloudfoundry.org/lager/README.md create mode 100644 vendor/code.cloudfoundry.org/lager/chug/chug.go create mode 100644 vendor/code.cloudfoundry.org/lager/chug/chug_suite_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/chug/chug_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/chug/match_log_entry_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgo_reporter.go create mode 100644 vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_suite_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/lager_suite_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/lagertest/test_sink.go create mode 100644 vendor/code.cloudfoundry.org/lager/logger.go create mode 100644 vendor/code.cloudfoundry.org/lager/logger_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/models.go create mode 100644 vendor/code.cloudfoundry.org/lager/reconfigurable_sink.go create mode 100644 vendor/code.cloudfoundry.org/lager/reconfigurable_sink_test.go create mode 100644 vendor/code.cloudfoundry.org/lager/writer_sink.go create mode 100644 vendor/code.cloudfoundry.org/lager/writer_sink_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/.travis.yml create mode 100644 vendor/github.com/pivotal-cf/brokerapi/Godeps/Godeps.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/Godeps/Readme create mode 100644 vendor/github.com/pivotal-cf/brokerapi/LICENSE create mode 100644 vendor/github.com/pivotal-cf/brokerapi/NOTICE create mode 100644 vendor/github.com/pivotal-cf/brokerapi/README.md create mode 100644 vendor/github.com/pivotal-cf/brokerapi/api.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/api_suite_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/api_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/auth/auth.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/auth/auth_suite_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/auth/auth_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/catalog.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/catalog_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/failure_response.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/failure_response_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fakes/fake_service_broker.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/async_required.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/binding.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_experimental_volume_mounts.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_route_service.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_syslog.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_volume_mounts.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/catalog.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/instance_limit_error.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/invalid_async_provision_error.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/last_operation_succeeded.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/operation_data_response.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning_with_dashboard.json create mode 100644 vendor/github.com/pivotal-cf/brokerapi/response.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/response_test.go create mode 100644 vendor/github.com/pivotal-cf/brokerapi/service_broker.go diff --git a/glide.lock b/glide.lock index 766c0f0d574..9c8d2a400f8 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 1d3632dc4b800ded5ca641abeeed846aa103e17a69fd80c023c1c66da0c30c4d -updated: 2017-05-05T11:55:59.493581821-07:00 +hash: 62c3838a43f667530907c83fc266d0aaa72b42a27b1fa8cf834844f61c6c5eb8 +updated: 2017-06-09T09:44:21.859689962-07:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -8,6 +8,8 @@ imports: subpackages: - compute/metadata - internal +- name: code.cloudfoundry.org/lager + version: 62951a8009ab331bb21dc418074fa54e66eb9b6a - name: github.com/beorn7/perks version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 subpackages: @@ -97,7 +99,7 @@ imports: - name: github.com/gorilla/handlers version: 13d73096a474cac93275c679c7b8a2dc17ddba82 - name: github.com/gorilla/mux - version: 599cba5e7b6137d46ddf58fb1765f5d928e69604 + version: bcd8bc72b08df0f70df986b97f95590779502d31 - name: github.com/grpc-ecosystem/grpc-gateway version: f52d055dc48aec25854ed7d31862f78913cf17d1 subpackages: @@ -164,6 +166,10 @@ imports: - types - name: github.com/pborman/uuid version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 +- name: github.com/pivotal-cf/brokerapi + version: 6d25b9398d9f05880ca8f480134a88c8d2df69bc + subpackages: + - auth - name: github.com/pkg/errors version: a22138067af1c4942683050411a841ade67fe1eb - name: github.com/prometheus/client_golang @@ -549,9 +555,70 @@ imports: version: 9497139cb62015905ba5b3d11836f2b0c117ff80 subpackages: - pkg/api + - pkg/api/install - pkg/api/v1 + - pkg/apis/apps + - pkg/apis/apps/install + - pkg/apis/apps/v1beta1 + - pkg/apis/authentication + - pkg/apis/authentication/install + - pkg/apis/authentication/v1 + - pkg/apis/authentication/v1beta1 + - pkg/apis/authorization + - pkg/apis/authorization/install + - pkg/apis/authorization/v1 + - pkg/apis/authorization/v1beta1 + - pkg/apis/autoscaling + - pkg/apis/autoscaling/install + - pkg/apis/autoscaling/v1 + - pkg/apis/autoscaling/v2alpha1 + - pkg/apis/batch + - pkg/apis/batch/install + - pkg/apis/batch/v1 + - pkg/apis/batch/v2alpha1 + - pkg/apis/certificates + - pkg/apis/certificates/install + - pkg/apis/certificates/v1beta1 - pkg/apis/componentconfig - pkg/apis/extensions + - pkg/apis/extensions/install + - pkg/apis/extensions/v1beta1 + - pkg/apis/policy + - pkg/apis/policy/install + - pkg/apis/policy/v1beta1 + - pkg/apis/rbac + - pkg/apis/rbac/install + - pkg/apis/rbac/v1alpha1 + - pkg/apis/rbac/v1beta1 + - pkg/apis/settings + - pkg/apis/settings/install + - pkg/apis/settings/v1alpha1 + - pkg/apis/storage + - pkg/apis/storage/install + - pkg/apis/storage/v1 + - pkg/apis/storage/v1beta1 + - pkg/client/clientset_generated/clientset + - pkg/client/clientset_generated/clientset/scheme + - pkg/client/clientset_generated/clientset/typed/apps/v1beta1 + - pkg/client/clientset_generated/clientset/typed/authentication/v1 + - pkg/client/clientset_generated/clientset/typed/authentication/v1beta1 + - pkg/client/clientset_generated/clientset/typed/authorization/v1 + - pkg/client/clientset_generated/clientset/typed/authorization/v1beta1 + - pkg/client/clientset_generated/clientset/typed/autoscaling/v1 + - pkg/client/clientset_generated/clientset/typed/autoscaling/v2alpha1 + - pkg/client/clientset_generated/clientset/typed/batch/v1 + - pkg/client/clientset_generated/clientset/typed/batch/v2alpha1 + - pkg/client/clientset_generated/clientset/typed/certificates/v1beta1 + - pkg/client/clientset_generated/clientset/typed/core/v1 + - pkg/client/clientset_generated/clientset/typed/extensions/v1beta1 + - pkg/client/clientset_generated/clientset/typed/policy/v1beta1 + - pkg/client/clientset_generated/clientset/typed/rbac/v1alpha1 + - pkg/client/clientset_generated/clientset/typed/rbac/v1beta1 + - pkg/client/clientset_generated/clientset/typed/settings/v1alpha1 + - pkg/client/clientset_generated/clientset/typed/storage/v1 + - pkg/client/clientset_generated/clientset/typed/storage/v1beta1 + - pkg/client/leaderelection + - pkg/client/leaderelection/resourcelock - pkg/util - pkg/util/configz - pkg/util/interrupt diff --git a/glide.yaml b/glide.yaml index 602ba2cc60b..5f61dc06af0 100644 --- a/glide.yaml +++ b/glide.yaml @@ -65,3 +65,5 @@ import: version: v1.1.0 - package: github.com/onsi/ginkgo version: v1.3.1 +- package: github.com/pivotal-cf/brokerapi +- package: code.cloudfoundry.org/lager diff --git a/pkg/brokerapi/fake/server/bind_request.go b/pkg/brokerapi/fake/server/bind_request.go new file mode 100644 index 00000000000..3a9e0a03881 --- /dev/null +++ b/pkg/brokerapi/fake/server/bind_request.go @@ -0,0 +1,12 @@ +package server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// BindRequest is the struct to contain details of a bind request +type BindRequest struct { + InstanceID string + BindingID string + Details brokerapi.BindDetails +} diff --git a/pkg/brokerapi/fake/server/convert_catalog.go b/pkg/brokerapi/fake/server/convert_catalog.go new file mode 100644 index 00000000000..cd3863a40b8 --- /dev/null +++ b/pkg/brokerapi/fake/server/convert_catalog.go @@ -0,0 +1,44 @@ +package server + +import ( + pkgbrokerapi "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + "github.com/pivotal-cf/brokerapi" +) + +// ConvertCatalog converts a (github.com/kubernetes-incubator/service-catalog/pkg/brokerapi).Catalog +// to an array of brokerapi.Services +func ConvertCatalog(cat *pkgbrokerapi.Catalog) []brokerapi.Service { + ret := make([]brokerapi.Service, len(cat.Services)) + for i, svc := range cat.Services { + ret[i] = convertService(svc) + } + return ret +} + +func convertService(svc *pkgbrokerapi.Service) brokerapi.Service { + return brokerapi.Service{ + ID: svc.ID, + Name: svc.Name, + Description: svc.Description, + Bindable: svc.Bindable, + Tags: svc.Tags, + PlanUpdatable: svc.PlanUpdateable, + Plans: convertPlans(svc.Plans), + // TODO: convert Requires, Metadata, DashboardClient + } +} + +func convertPlans(plans []pkgbrokerapi.ServicePlan) []brokerapi.ServicePlan { + ret := make([]brokerapi.ServicePlan, len(plans)) + for i, plan := range plans { + ret[i] = brokerapi.ServicePlan{ + ID: plan.ID, + Name: plan.Name, + Description: plan.Description, + Free: &plan.Free, + Bindable: plan.Bindable, + // TODO: convert Metadata + } + } + return ret +} diff --git a/pkg/brokerapi/fake/server/create_func.go b/pkg/brokerapi/fake/server/create_func.go new file mode 100644 index 00000000000..73bdae284d0 --- /dev/null +++ b/pkg/brokerapi/fake/server/create_func.go @@ -0,0 +1,17 @@ +package server + +import ( + "net/http/httptest" + + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker" +) + +// NewCreateFunc creates a new brokerapi.CreateFunc according to a broker server running +// in srv +func NewCreateFunc(srv *httptest.Server, user, pass string) brokerapi.CreateFunc { + // type CreateFunc func(name, url, username, password string) BrokerClient + return brokerapi.CreateFunc(func(name, url, username, password string) brokerapi.BrokerClient { + return openservicebroker.NewClient("testclient", srv.URL, user, pass) + }) +} diff --git a/pkg/brokerapi/fake/server/deprovision_request.go b/pkg/brokerapi/fake/server/deprovision_request.go new file mode 100644 index 00000000000..363bf112f99 --- /dev/null +++ b/pkg/brokerapi/fake/server/deprovision_request.go @@ -0,0 +1,11 @@ +package server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// DeprovisionRequest is the struct to contain details of a single deprovision request +type DeprovisionRequest struct { + InstanceID string + Details brokerapi.DeprovisionDetails +} diff --git a/pkg/brokerapi/fake/server/handler.go b/pkg/brokerapi/fake/server/handler.go new file mode 100644 index 00000000000..94bcf9aab45 --- /dev/null +++ b/pkg/brokerapi/fake/server/handler.go @@ -0,0 +1,110 @@ +package server + +import ( + "context" + + "github.com/pivotal-cf/brokerapi" +) + +// Handler is a fake implementation oif a brokerapi.ServiceBroker +type Handler struct { + Catalog []brokerapi.Service + CatalogRequests int + + ProvisionResp brokerapi.ProvisionedServiceSpec + ProvisionRespError error + ProvisionRequests []ProvisionRequest + + DeprovisionResp brokerapi.DeprovisionServiceSpec + DeprovisonRespErr error + DeprovisionRequests []DeprovisionRequest + + BindResp brokerapi.Binding + BindRespErr error + BindRequests []BindRequest + + UnbindRespErr error + UnbindRequests []UnbindRequest + + UpdateResp brokerapi.UpdateServiceSpec + UpdateRespErr error + UpdateRequests []UpdateRequest + + LastOperationResp brokerapi.LastOperation + LastOperationRespErr error + LastOperationRequests []LastOperationRequest +} + +// NewHandler creates a new fake server handler +func NewHandler() *Handler { + return &Handler{} +} + +// Services is the interface implementation of brokerapi.ServiceBroker +func (h *Handler) Services(ctx context.Context) []brokerapi.Service { + h.CatalogRequests++ + return h.Catalog +} + +// Provision is the interface implementation of brokerapi.ServiceBroker +func (h *Handler) Provision( + ctx context.Context, + instanceID string, + details brokerapi.ProvisionDetails, + asyncAllowed bool, +) (brokerapi.ProvisionedServiceSpec, error) { + h.ProvisionRequests = append(h.ProvisionRequests, ProvisionRequest{ + InstanceID: instanceID, + Details: details, + AsyncAllowed: asyncAllowed, + }) + return h.ProvisionResp, h.ProvisionRespError +} + +// Deprovision is the interface implementation of brokerapi.ServiceBroker +func (h *Handler) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + h.DeprovisionRequests = append(h.DeprovisionRequests, DeprovisionRequest{ + InstanceID: instanceID, + Details: details, + }) + return h.DeprovisionResp, h.DeprovisonRespErr +} + +// Bind is the interface implementation of brokerapi.ServiceBroker +func (h *Handler) Bind(context context.Context, instanceID, bindingID string, details brokerapi.BindDetails) (brokerapi.Binding, error) { + h.BindRequests = append(h.BindRequests, BindRequest{ + InstanceID: instanceID, + BindingID: bindingID, + Details: details, + }) + return h.BindResp, h.BindRespErr +} + +// Unbind is the interface implementation of brokerapi.ServiceBroker +func (h *Handler) Unbind(context context.Context, instanceID, bindingID string, details brokerapi.UnbindDetails) error { + h.UnbindRequests = append(h.UnbindRequests, UnbindRequest{ + InstanceID: instanceID, + BindingID: bindingID, + Details: details, + }) + return h.UnbindRespErr +} + +// Update is the interface implementation of brokerapi.ServiceBroker +func (h *Handler) Update(context context.Context, instanceID string, details brokerapi.UpdateDetails, asyncAllowed bool) (brokerapi.UpdateServiceSpec, error) { + h.UpdateRequests = append(h.UpdateRequests, UpdateRequest{ + InstanceID: instanceID, + Details: details, + AsyncAllowed: asyncAllowed, + }) + return h.UpdateResp, h.UpdateRespErr +} + +// LastOperation is the interface implementation of brokerapi.ServiceBroker +func (h *Handler) LastOperation(context context.Context, instanceID, operationData string) (brokerapi.LastOperation, error) { + h.LastOperationRequests = append(h.LastOperationRequests, LastOperationRequest{ + InstanceID: instanceID, + OperationData: operationData, + }) + return h.LastOperationResp, h.LastOperationRespErr +} diff --git a/pkg/brokerapi/fake/server/init.go b/pkg/brokerapi/fake/server/init.go new file mode 100644 index 00000000000..0e1ca47c380 --- /dev/null +++ b/pkg/brokerapi/fake/server/init.go @@ -0,0 +1,9 @@ +package server + +import ( + "code.cloudfoundry.org/lager" +) + +var ( + logger = lager.NewLogger("server") +) diff --git a/pkg/brokerapi/fake/server/last_operation_request.go b/pkg/brokerapi/fake/server/last_operation_request.go new file mode 100644 index 00000000000..e0491784d2f --- /dev/null +++ b/pkg/brokerapi/fake/server/last_operation_request.go @@ -0,0 +1,6 @@ +package server + +type LastOperationRequest struct { + InstanceID string + OperationData string +} diff --git a/pkg/brokerapi/fake/server/provision_request.go b/pkg/brokerapi/fake/server/provision_request.go new file mode 100644 index 00000000000..061d4671036 --- /dev/null +++ b/pkg/brokerapi/fake/server/provision_request.go @@ -0,0 +1,12 @@ +package server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// ProvisionRequest is the struct to house details of a single provision request +type ProvisionRequest struct { + InstanceID string + Details brokerapi.ProvisionDetails + AsyncAllowed bool +} diff --git a/pkg/brokerapi/fake/server/server.go b/pkg/brokerapi/fake/server/server.go new file mode 100644 index 00000000000..547e5815b44 --- /dev/null +++ b/pkg/brokerapi/fake/server/server.go @@ -0,0 +1,18 @@ +package server + +import ( + "net/http/httptest" + + "github.com/pivotal-cf/brokerapi" +) + +// Run runs a new test server from the given broker handler and auth credentials +func Run(hdl *Handler, username, password string) *httptest.Server { + httpHandler := brokerapi.New(hdl, logger, brokerapi.BrokerCredentials{ + Username: username, + Password: password, + }) + + srv := httptest.NewServer(httpHandler) + return srv +} diff --git a/pkg/brokerapi/fake/server/unbind_request.go b/pkg/brokerapi/fake/server/unbind_request.go new file mode 100644 index 00000000000..983a46cf588 --- /dev/null +++ b/pkg/brokerapi/fake/server/unbind_request.go @@ -0,0 +1,12 @@ +package server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// UnbindRequest is the struct to house details of a single unbind request +type UnbindRequest struct{ + InstanceID string + BindingID string + Details brokerapi.UnbindDetails +} diff --git a/pkg/brokerapi/fake/server/update_request.go b/pkg/brokerapi/fake/server/update_request.go new file mode 100644 index 00000000000..bd8a64d6b33 --- /dev/null +++ b/pkg/brokerapi/fake/server/update_request.go @@ -0,0 +1,12 @@ +package server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// UpdateRequest is the struct that contains details of a single update request +type UpdateRequest struct { + InstanceID string + Details brokerapi.UpdateDetails + AsyncAllowed bool +} diff --git a/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt b/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt index 3c3827ab7d1..b9a2ff98457 100644 --- a/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt +++ b/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt @@ -3,3 +3,4 @@ node: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 branch: default latesttag: null latesttagdistance: 5 +changessincelatesttag: 5 diff --git a/vendor/code.cloudfoundry.org/lager/LICENSE b/vendor/code.cloudfoundry.org/lager/LICENSE new file mode 100644 index 00000000000..5c304d1a4a7 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/code.cloudfoundry.org/lager/NOTICE b/vendor/code.cloudfoundry.org/lager/NOTICE new file mode 100644 index 00000000000..ff96b880bdd --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/NOTICE @@ -0,0 +1,15 @@ +lager + +Copyright (c) 2014-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/code.cloudfoundry.org/lager/README.md b/vendor/code.cloudfoundry.org/lager/README.md new file mode 100644 index 00000000000..c9f28cc6db0 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/README.md @@ -0,0 +1,78 @@ +lager +===== + +**Note**: This repository should be imported as `code.cloudfoundry.org/lager`. + +Lager is a logging library for go. + +## Usage + +Instantiate a logger with the name of your component. + +```go +import ( + "code.cloudfoundry.org/lager" +) + +logger := lager.NewLogger("my-app") +``` + +### Sinks + +Lager can write logs to a variety of destinations. You can specify the destinations +using Lager sinks: + +To write to an arbitrary `Writer` object: + +```go +logger.RegisterSink(lager.NewWriterSink(myWriter, lager.INFO)) +``` + +### Emitting logs + +Lager supports the usual level-based logging, with an optional argument for arbitrary key-value data. + +```go +logger.Info("doing-stuff", lager.Data{ + "informative": true, +}) +``` + +output: +```json +{ "source": "my-app", "message": "doing-stuff", "data": { "informative": true }, "timestamp": 1232345, "log_level": 1 } +``` + +Error messages also take an `Error` object: + +```go +logger.Error("failed-to-do-stuff", errors.New("Something went wrong")) +``` + +output: +```json +{ "source": "my-app", "message": "failed-to-do-stuff", "data": { "error": "Something went wrong" }, "timestamp": 1232345, "log_level": 1 } +``` + +### Sessions + +You can avoid repetition of contextual data using 'Sessions': + +```go + +contextualLogger := logger.Session("my-task", lager.Data{ + "request-id": 5, +}) + +contextualLogger.Info("my-action") +``` + +output: + +```json +{ "source": "my-app", "message": "my-task.my-action", "data": { "request-id": 5 }, "timestamp": 1232345, "log_level": 1 } +``` + +## License + +Lager is [Apache 2.0](https://github.com/cloudfoundry/lager/blob/master/LICENSE) licensed. diff --git a/vendor/code.cloudfoundry.org/lager/chug/chug.go b/vendor/code.cloudfoundry.org/lager/chug/chug.go new file mode 100644 index 00000000000..80672fbcb9a --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/chug/chug.go @@ -0,0 +1,130 @@ +package chug + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "io" + "strconv" + "strings" + "time" + + "code.cloudfoundry.org/lager" +) + +type Entry struct { + IsLager bool + Raw []byte + Log LogEntry +} + +type LogEntry struct { + Timestamp time.Time + LogLevel lager.LogLevel + + Source string + Message string + Session string + + Error error + Trace string + + Data lager.Data +} + +func Chug(reader io.Reader, out chan<- Entry) { + scanner := bufio.NewReader(reader) + for { + line, err := scanner.ReadBytes('\n') + if line != nil { + out <- entry(bytes.TrimSuffix(line, []byte{'\n'})) + } + if err != nil { + break + } + } + close(out) +} + +func entry(raw []byte) (entry Entry) { + copiedBytes := make([]byte, len(raw)) + copy(copiedBytes, raw) + entry = Entry{ + IsLager: false, + Raw: copiedBytes, + } + + rawString := string(raw) + idx := strings.Index(rawString, "{") + if idx == -1 { + return + } + + var lagerLog lager.LogFormat + decoder := json.NewDecoder(strings.NewReader(rawString[idx:])) + err := decoder.Decode(&lagerLog) + if err != nil { + return + } + + entry.Log, entry.IsLager = convertLagerLog(lagerLog) + + return +} + +func convertLagerLog(lagerLog lager.LogFormat) (LogEntry, bool) { + timestamp, err := strconv.ParseFloat(lagerLog.Timestamp, 64) + + if err != nil { + return LogEntry{}, false + } + + data := lagerLog.Data + + var logErr error + if lagerLog.LogLevel == lager.ERROR || lagerLog.LogLevel == lager.FATAL { + dataErr, ok := lagerLog.Data["error"] + if ok { + errorString, ok := dataErr.(string) + if !ok { + return LogEntry{}, false + } + logErr = errors.New(errorString) + delete(lagerLog.Data, "error") + } + } + + var logTrace string + dataTrace, ok := lagerLog.Data["trace"] + if ok { + logTrace, ok = dataTrace.(string) + if !ok { + return LogEntry{}, false + } + delete(lagerLog.Data, "trace") + } + + var logSession string + dataSession, ok := lagerLog.Data["session"] + if ok { + logSession, ok = dataSession.(string) + if !ok { + return LogEntry{}, false + } + delete(lagerLog.Data, "session") + } + + return LogEntry{ + Timestamp: time.Unix(0, int64(timestamp*1e9)), + LogLevel: lagerLog.LogLevel, + Source: lagerLog.Source, + Message: lagerLog.Message, + Session: logSession, + + Error: logErr, + Trace: logTrace, + + Data: data, + }, true +} diff --git a/vendor/code.cloudfoundry.org/lager/chug/chug_suite_test.go b/vendor/code.cloudfoundry.org/lager/chug/chug_suite_test.go new file mode 100644 index 00000000000..46cc34c22e7 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/chug/chug_suite_test.go @@ -0,0 +1,13 @@ +package chug_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestChug(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Chug Suite") +} diff --git a/vendor/code.cloudfoundry.org/lager/chug/chug_test.go b/vendor/code.cloudfoundry.org/lager/chug/chug_test.go new file mode 100644 index 00000000000..7c262316579 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/chug/chug_test.go @@ -0,0 +1,247 @@ +package chug_test + +import ( + "errors" + "io" + "time" + + "code.cloudfoundry.org/lager" + . "code.cloudfoundry.org/lager/chug" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Chug", func() { + var ( + logger lager.Logger + stream chan Entry + pipeReader *io.PipeReader + pipeWriter *io.PipeWriter + ) + + BeforeEach(func() { + pipeReader, pipeWriter = io.Pipe() + logger = lager.NewLogger("chug-test") + logger.RegisterSink(lager.NewWriterSink(pipeWriter, lager.DEBUG)) + stream = make(chan Entry, 100) + go Chug(pipeReader, stream) + }) + + AfterEach(func() { + pipeWriter.Close() + Eventually(stream).Should(BeClosed()) + }) + + Context("when fed a stream of well-formed lager messages", func() { + It("should return parsed lager messages", func() { + data := lager.Data{"some-float": 3.0, "some-string": "foo"} + logger.Debug("chug", data) + logger.Info("again", data) + + entry := <-stream + Expect(entry.IsLager).To(BeTrue()) + Expect(entry.Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.DEBUG, + Source: "chug-test", + Message: "chug-test.chug", + Data: data, + })) + + entry = <-stream + Expect(entry.IsLager).To(BeTrue()) + Expect(entry.Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.again", + Data: data, + })) + + }) + + It("should parse the timestamp", func() { + logger.Debug("chug") + entry := <-stream + Expect(entry.Log.Timestamp).To(BeTemporally("~", time.Now(), 10*time.Millisecond)) + }) + + Context("when parsing an error message", func() { + It("should include the error", func() { + data := lager.Data{"some-float": 3.0, "some-string": "foo"} + logger.Error("chug", errors.New("some-error"), data) + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.ERROR, + Source: "chug-test", + Message: "chug-test.chug", + Error: errors.New("some-error"), + Data: lager.Data{"some-float": 3.0, "some-string": "foo"}, + })) + + }) + }) + + Context("when parsing an info message with an error", func() { + It("should not take the error out of the data map", func() { + data := lager.Data{"some-float": 3.0, "some-string": "foo", "error": "some-error"} + logger.Info("chug", data) + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.chug", + Error: nil, + Data: lager.Data{"some-float": 3.0, "some-string": "foo", "error": "some-error"}, + })) + + }) + }) + + Context("when multiple sessions have been established", func() { + It("should build up the task array appropriately", func() { + firstSession := logger.Session("first-session") + firstSession.Info("encabulate") + nestedSession := firstSession.Session("nested-session-1") + nestedSession.Info("baconize") + firstSession.Info("remodulate") + nestedSession.Info("ergonomize") + nestedSession = firstSession.Session("nested-session-2") + nestedSession.Info("modernify") + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.encabulate", + Session: "1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.nested-session-1.baconize", + Session: "1.1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.remodulate", + Session: "1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.nested-session-1.ergonomize", + Session: "1.1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.nested-session-2.modernify", + Session: "1.2", + Data: lager.Data{}, + })) + + }) + }) + }) + + Context("handling lager JSON that is surrounded by non-JSON", func() { + var input []byte + var entry Entry + + BeforeEach(func() { + input = []byte(`[some-component][e]{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":0,"data":{"some-float":3,"some-string":"foo"}}...some trailing stuff`) + pipeWriter.Write(input) + pipeWriter.Write([]byte("\n")) + + Eventually(stream).Should(Receive(&entry)) + }) + + It("should be a lager message", func() { + Expect(entry.IsLager).To(BeTrue()) + }) + + It("should contain all the data in Raw", func() { + Expect(entry.Raw).To(Equal(input)) + }) + + It("should succesfully parse the lager message", func() { + Expect(entry.Log.Source).To(Equal("chug-test")) + }) + }) + + Context("handling malformed/non-lager data", func() { + var input []byte + var entry Entry + + JustBeforeEach(func() { + pipeWriter.Write(input) + pipeWriter.Write([]byte("\n")) + + Eventually(stream).Should(Receive(&entry)) + }) + + itReturnsRawData := func() { + It("returns raw data", func() { + Expect(entry.IsLager).To(BeFalse()) + Expect(entry.Log).To(BeZero()) + Expect(entry.Raw).To(Equal(input)) + }) + } + + Context("when fed a stream of malformed lager messages", func() { + Context("when the timestamp is invalid", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"tomorrow","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","error":7}}`) + }) + + itReturnsRawData() + }) + + Context("when the error does not parse", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","error":7}}`) + }) + + itReturnsRawData() + }) + + Context("when the trace does not parse", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","trace":7}}`) + }) + + itReturnsRawData() + }) + + Context("when the session does not parse", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","session":7}}`) + }) + + itReturnsRawData() + }) + }) + + Context("When fed JSON that is not a lager message at all", func() { + BeforeEach(func() { + input = []byte(`{"source":"chattanooga"}`) + }) + + itReturnsRawData() + }) + + Context("When fed none-JSON that is not a lager message at all", func() { + BeforeEach(func() { + input = []byte(`ß`) + }) + + itReturnsRawData() + }) + }) +}) diff --git a/vendor/code.cloudfoundry.org/lager/chug/match_log_entry_test.go b/vendor/code.cloudfoundry.org/lager/chug/match_log_entry_test.go new file mode 100644 index 00000000000..03d6c77b308 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/chug/match_log_entry_test.go @@ -0,0 +1,41 @@ +package chug_test + +import ( + "fmt" + "reflect" + + "code.cloudfoundry.org/lager/chug" + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +func MatchLogEntry(entry chug.LogEntry) types.GomegaMatcher { + return &logEntryMatcher{entry} +} + +type logEntryMatcher struct { + entry chug.LogEntry +} + +func (m *logEntryMatcher) Match(actual interface{}) (success bool, err error) { + actualEntry, ok := actual.(chug.LogEntry) + if !ok { + return false, fmt.Errorf("MatchLogEntry must be passed a chug.LogEntry. Got:\n%s", format.Object(actual, 1)) + } + + return m.entry.LogLevel == actualEntry.LogLevel && + m.entry.Source == actualEntry.Source && + m.entry.Message == actualEntry.Message && + m.entry.Session == actualEntry.Session && + reflect.DeepEqual(m.entry.Error, actualEntry.Error) && + m.entry.Trace == actualEntry.Trace && + reflect.DeepEqual(m.entry.Data, actualEntry.Data), nil +} + +func (m *logEntryMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to equal", m.entry) +} + +func (m *logEntryMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to equal", m.entry) +} diff --git a/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgo_reporter.go b/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgo_reporter.go new file mode 100644 index 00000000000..00b7b8f14ae --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgo_reporter.go @@ -0,0 +1,155 @@ +package ginkgoreporter + +import ( + "fmt" + "io" + "time" + + "code.cloudfoundry.org/lager" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +type SuiteStartSummary struct { + RandomSeed int64 `json:"random_seed"` + SuiteDescription string `json:"description"` + NumberOfSpecsThatWillBeRun int `json:"num_specs"` +} + +type SuiteEndSummary struct { + SuiteDescription string `json:"description"` + Passed bool + NumberOfSpecsThatWillBeRun int `json:"num_specs"` + NumberOfPassedSpecs int `json:"num_passed"` + NumberOfFailedSpecs int `json:"num_failed"` +} + +type SpecSummary struct { + Name []string `json:"name"` + Location string `json:"location"` + State string `json:"state"` + Passed bool `json:"passed"` + RunTime time.Duration `json:"run_time"` + + StackTrace string `json:"stack_trace,omitempty"` +} + +type SetupSummary struct { + Name string `json:"name"` + State string `json:"state"` + Passed bool `json:"passed"` + RunTime time.Duration `json:"run_time,omitempty"` + + StackTrace string `json:"stack_trace,omitempty"` +} + +func New(writer io.Writer) *GinkgoReporter { + logger := lager.NewLogger("ginkgo") + logger.RegisterSink(lager.NewWriterSink(writer, lager.DEBUG)) + return &GinkgoReporter{ + writer: writer, + logger: logger, + } +} + +type GinkgoReporter struct { + logger lager.Logger + writer io.Writer + session lager.Logger +} + +func (g *GinkgoReporter) wrappedWithNewlines(f func()) { + g.writer.Write([]byte("\n")) + f() + g.writer.Write([]byte("\n")) +} + +func (g *GinkgoReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + if config.ParallelTotal > 1 { + var session = g.logger + for i := 0; i < config.ParallelNode; i++ { + session = g.logger.Session(fmt.Sprintf("node-%d", i+1)) + } + g.logger = session + } +} + +func (g *GinkgoReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { +} + +func (g *GinkgoReporter) SpecWillRun(specSummary *types.SpecSummary) { + g.wrappedWithNewlines(func() { + g.session = g.logger.Session("spec") + g.session.Info("start", lager.Data{ + "summary": SpecSummary{ + Name: specSummary.ComponentTexts, + Location: specSummary.ComponentCodeLocations[len(specSummary.ComponentTexts)-1].String(), + }, + }) + }) +} + +func (g *GinkgoReporter) SpecDidComplete(specSummary *types.SpecSummary) { + g.wrappedWithNewlines(func() { + if g.session == nil { + return + } + summary := SpecSummary{ + Name: specSummary.ComponentTexts, + Location: specSummary.ComponentCodeLocations[len(specSummary.ComponentTexts)-1].String(), + State: stateAsString(specSummary.State), + Passed: passed(specSummary.State), + RunTime: specSummary.RunTime, + } + + if passed(specSummary.State) { + g.session.Info("end", lager.Data{ + "summary": summary, + }) + } else { + summary.StackTrace = specSummary.Failure.Location.FullStackTrace + g.session.Error("end", errorForFailure(specSummary.Failure), lager.Data{ + "summary": summary, + }) + } + g.session = nil + }) +} + +func (g *GinkgoReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { +} + +func (g *GinkgoReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { +} + +func stateAsString(state types.SpecState) string { + switch state { + case types.SpecStatePending: + return "PENDING" + case types.SpecStateSkipped: + return "SKIPPED" + case types.SpecStatePassed: + return "PASSED" + case types.SpecStateFailed: + return "FAILED" + case types.SpecStatePanicked: + return "PANICKED" + case types.SpecStateTimedOut: + return "TIMED OUT" + default: + return "INVALID" + } +} + +func passed(state types.SpecState) bool { + return !(state == types.SpecStateFailed || state == types.SpecStatePanicked || state == types.SpecStateTimedOut) +} + +func errorForFailure(failure types.SpecFailure) error { + message := failure.Message + if failure.ForwardedPanic != "" { + message += fmt.Sprintf("%s", failure.ForwardedPanic) + } + + return fmt.Errorf("%s\n%s", message, failure.Location.String()) +} diff --git a/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_suite_test.go b/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_suite_test.go new file mode 100644 index 00000000000..fa079140827 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_suite_test.go @@ -0,0 +1,13 @@ +package ginkgoreporter_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGinkgoReporter(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "GinkgoReporter Suite") +} diff --git a/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_test.go b/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_test.go new file mode 100644 index 00000000000..49cde07318b --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_test.go @@ -0,0 +1,185 @@ +package ginkgoreporter_test + +import ( + "bytes" + "encoding/json" + "time" + + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/chug" + . "code.cloudfoundry.org/lager/ginkgoreporter" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ginkgoreporter", func() { + var ( + reporter reporters.Reporter + buffer *bytes.Buffer + ) + + BeforeEach(func() { + buffer = &bytes.Buffer{} + reporter = New(buffer) + }) + + fetchLogs := func() []chug.LogEntry { + out := make(chan chug.Entry, 1000) + chug.Chug(buffer, out) + logs := []chug.LogEntry{} + for entry := range out { + if entry.IsLager { + logs = append(logs, entry.Log) + } + } + return logs + } + + jsonRoundTrip := func(object interface{}) interface{} { + jsonEncoded, err := json.Marshal(object) + Expect(err).NotTo(HaveOccurred()) + var out interface{} + err = json.Unmarshal(jsonEncoded, &out) + Expect(err).NotTo(HaveOccurred()) + return out + } + + Describe("Announcing specs", func() { + var summary *types.SpecSummary + BeforeEach(func() { + summary = &types.SpecSummary{ + ComponentTexts: []string{"A", "B"}, + ComponentCodeLocations: []types.CodeLocation{ + { + FileName: "file/a", + LineNumber: 3, + FullStackTrace: "some-stack-trace", + }, + { + FileName: "file/b", + LineNumber: 4, + FullStackTrace: "some-stack-trace", + }, + }, + RunTime: time.Minute, + State: types.SpecStatePassed, + } + }) + + Context("when running in parallel", func() { + It("should include the node # in the session and message", func() { + configType := config.GinkgoConfigType{ + ParallelTotal: 3, + ParallelNode: 2, + } + suiteSummary := &types.SuiteSummary{} + reporter.SpecSuiteWillBegin(configType, suiteSummary) + + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + + logs := fetchLogs() + Expect(logs[0].Session).To(Equal("2.1")) + Expect(logs[0].Message).To(Equal("ginkgo.node-2.spec.start")) + Expect(logs[1].Session).To(Equal("2.1")) + Expect(logs[1].Message).To(Equal("ginkgo.node-2.spec.end")) + Expect(logs[2].Session).To(Equal("2.2")) + Expect(logs[0].Message).To(Equal("ginkgo.node-2.spec.start")) + Expect(logs[3].Session).To(Equal("2.2")) + Expect(logs[1].Message).To(Equal("ginkgo.node-2.spec.end")) + }) + }) + + Describe("incrementing sessions", func() { + It("should increment the session counter as specs run", func() { + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + + logs := fetchLogs() + Expect(logs[0].Session).To(Equal("1")) + Expect(logs[1].Session).To(Equal("1")) + Expect(logs[2].Session).To(Equal("2")) + Expect(logs[3].Session).To(Equal("2")) + }) + }) + + Context("when a spec starts", func() { + BeforeEach(func() { + reporter.SpecWillRun(summary) + }) + + It("should log about the spec starting", func() { + log := fetchLogs()[0] + Expect(log.LogLevel).To(Equal(lager.INFO)) + Expect(log.Source).To(Equal("ginkgo")) + Expect(log.Message).To(Equal("ginkgo.spec.start")) + Expect(log.Session).To(Equal("1")) + Expect(log.Data["summary"]).To(Equal(jsonRoundTrip(SpecSummary{ + Name: []string{"A", "B"}, + Location: "file/b:4", + }))) + + }) + + Context("when the spec succeeds", func() { + It("should info", func() { + reporter.SpecDidComplete(summary) + log := fetchLogs()[1] + Expect(log.LogLevel).To(Equal(lager.INFO)) + Expect(log.Source).To(Equal("ginkgo")) + Expect(log.Message).To(Equal("ginkgo.spec.end")) + Expect(log.Session).To(Equal("1")) + Expect(log.Data["summary"]).To(Equal(jsonRoundTrip(SpecSummary{ + Name: []string{"A", "B"}, + Location: "file/b:4", + State: "PASSED", + Passed: true, + RunTime: time.Minute, + }))) + + }) + }) + + Context("when the spec fails", func() { + BeforeEach(func() { + summary.State = types.SpecStateFailed + summary.Failure = types.SpecFailure{ + Message: "something failed!", + Location: types.CodeLocation{ + FileName: "some/file", + LineNumber: 3, + FullStackTrace: "some-stack-trace", + }, + } + }) + + It("should error", func() { + reporter.SpecDidComplete(summary) + log := fetchLogs()[1] + Expect(log.LogLevel).To(Equal(lager.ERROR)) + Expect(log.Source).To(Equal("ginkgo")) + Expect(log.Message).To(Equal("ginkgo.spec.end")) + Expect(log.Session).To(Equal("1")) + Expect(log.Error.Error()).To(Equal("something failed!\nsome/file:3")) + Expect(log.Data["summary"]).To(Equal(jsonRoundTrip(SpecSummary{ + Name: []string{"A", "B"}, + Location: "file/b:4", + State: "FAILED", + Passed: false, + RunTime: time.Minute, + StackTrace: "some-stack-trace", + }))) + + }) + }) + }) + }) +}) diff --git a/vendor/code.cloudfoundry.org/lager/lager_suite_test.go b/vendor/code.cloudfoundry.org/lager/lager_suite_test.go new file mode 100644 index 00000000000..b7670a7f1b4 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/lager_suite_test.go @@ -0,0 +1,13 @@ +package lager_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLager(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Lager Suite") +} diff --git a/vendor/code.cloudfoundry.org/lager/lagertest/test_sink.go b/vendor/code.cloudfoundry.org/lager/lagertest/test_sink.go new file mode 100644 index 00000000000..79782ab0504 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/lagertest/test_sink.go @@ -0,0 +1,71 @@ +package lagertest + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega/gbytes" + + "code.cloudfoundry.org/lager" +) + +type TestLogger struct { + lager.Logger + *TestSink +} + +type TestSink struct { + lager.Sink + buffer *gbytes.Buffer +} + +func NewTestLogger(component string) *TestLogger { + logger := lager.NewLogger(component) + + testSink := NewTestSink() + logger.RegisterSink(testSink) + logger.RegisterSink(lager.NewWriterSink(ginkgo.GinkgoWriter, lager.DEBUG)) + + return &TestLogger{logger, testSink} +} + +func NewTestSink() *TestSink { + buffer := gbytes.NewBuffer() + + return &TestSink{ + Sink: lager.NewWriterSink(buffer, lager.DEBUG), + buffer: buffer, + } +} + +func (s *TestSink) Buffer() *gbytes.Buffer { + return s.buffer +} + +func (s *TestSink) Logs() []lager.LogFormat { + logs := []lager.LogFormat{} + + decoder := json.NewDecoder(bytes.NewBuffer(s.buffer.Contents())) + for { + var log lager.LogFormat + if err := decoder.Decode(&log); err == io.EOF { + return logs + } else if err != nil { + panic(err) + } + logs = append(logs, log) + } + + return logs +} + +func (s *TestSink) LogMessages() []string { + logs := s.Logs() + messages := make([]string, 0, len(logs)) + for _, log := range logs { + messages = append(messages, log.Message) + } + return messages +} diff --git a/vendor/code.cloudfoundry.org/lager/logger.go b/vendor/code.cloudfoundry.org/lager/logger.go new file mode 100644 index 00000000000..70727655a65 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/logger.go @@ -0,0 +1,179 @@ +package lager + +import ( + "fmt" + "runtime" + "sync/atomic" + "time" +) + +const StackTraceBufferSize = 1024 * 100 + +type Logger interface { + RegisterSink(Sink) + Session(task string, data ...Data) Logger + SessionName() string + Debug(action string, data ...Data) + Info(action string, data ...Data) + Error(action string, err error, data ...Data) + Fatal(action string, err error, data ...Data) + WithData(Data) Logger +} + +type logger struct { + component string + task string + sinks []Sink + sessionID string + nextSession uint32 + data Data +} + +func NewLogger(component string) Logger { + return &logger{ + component: component, + task: component, + sinks: []Sink{}, + data: Data{}, + } +} + +func (l *logger) RegisterSink(sink Sink) { + l.sinks = append(l.sinks, sink) +} + +func (l *logger) SessionName() string { + return l.task +} + +func (l *logger) Session(task string, data ...Data) Logger { + sid := atomic.AddUint32(&l.nextSession, 1) + + var sessionIDstr string + + if l.sessionID != "" { + sessionIDstr = fmt.Sprintf("%s.%d", l.sessionID, sid) + } else { + sessionIDstr = fmt.Sprintf("%d", sid) + } + + return &logger{ + component: l.component, + task: fmt.Sprintf("%s.%s", l.task, task), + sinks: l.sinks, + sessionID: sessionIDstr, + data: l.baseData(data...), + } +} + +func (l *logger) WithData(data Data) Logger { + return &logger{ + component: l.component, + task: l.task, + sinks: l.sinks, + sessionID: l.sessionID, + data: l.baseData(data), + } +} + +func (l *logger) Debug(action string, data ...Data) { + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: DEBUG, + Data: l.baseData(data...), + } + + for _, sink := range l.sinks { + sink.Log(log) + } +} + +func (l *logger) Info(action string, data ...Data) { + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: INFO, + Data: l.baseData(data...), + } + + for _, sink := range l.sinks { + sink.Log(log) + } +} + +func (l *logger) Error(action string, err error, data ...Data) { + logData := l.baseData(data...) + + if err != nil { + logData["error"] = err.Error() + } + + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: ERROR, + Data: logData, + } + + for _, sink := range l.sinks { + sink.Log(log) + } +} + +func (l *logger) Fatal(action string, err error, data ...Data) { + logData := l.baseData(data...) + + stackTrace := make([]byte, StackTraceBufferSize) + stackSize := runtime.Stack(stackTrace, false) + stackTrace = stackTrace[:stackSize] + + if err != nil { + logData["error"] = err.Error() + } + + logData["trace"] = string(stackTrace) + + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: FATAL, + Data: logData, + } + + for _, sink := range l.sinks { + sink.Log(log) + } + + panic(err) +} + +func (l *logger) baseData(givenData ...Data) Data { + data := Data{} + + for k, v := range l.data { + data[k] = v + } + + if len(givenData) > 0 { + for _, dataArg := range givenData { + for key, val := range dataArg { + data[key] = val + } + } + } + + if l.sessionID != "" { + data["session"] = l.sessionID + } + + return data +} + +func currentTimestamp() string { + return fmt.Sprintf("%.9f", float64(time.Now().UnixNano())/1e9) +} diff --git a/vendor/code.cloudfoundry.org/lager/logger_test.go b/vendor/code.cloudfoundry.org/lager/logger_test.go new file mode 100644 index 00000000000..1d7e173addc --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/logger_test.go @@ -0,0 +1,358 @@ +package lager_test + +import ( + "errors" + "fmt" + "strconv" + "time" + + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/lagertest" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Logger", func() { + var logger lager.Logger + var testSink *lagertest.TestSink + + var component = "my-component" + var action = "my-action" + var logData = lager.Data{ + "foo": "bar", + "a-number": 7, + } + var anotherLogData = lager.Data{ + "baz": "quux", + "b-number": 43, + } + + BeforeEach(func() { + logger = lager.NewLogger(component) + testSink = lagertest.NewTestSink() + logger.RegisterSink(testSink) + }) + + var TestCommonLogFeatures = func(level lager.LogLevel) { + var log lager.LogFormat + + BeforeEach(func() { + log = testSink.Logs()[0] + }) + + It("writes a log to the sink", func() { + Expect(testSink.Logs()).To(HaveLen(1)) + }) + + It("records the source component", func() { + Expect(log.Source).To(Equal(component)) + }) + + It("outputs a properly-formatted message", func() { + Expect(log.Message).To(Equal(fmt.Sprintf("%s.%s", component, action))) + }) + + It("has a timestamp", func() { + expectedTime := float64(time.Now().UnixNano()) / 1e9 + parsedTimestamp, err := strconv.ParseFloat(log.Timestamp, 64) + Expect(err).NotTo(HaveOccurred()) + Expect(parsedTimestamp).To(BeNumerically("~", expectedTime, 1.0)) + }) + + It("sets the proper output level", func() { + Expect(log.LogLevel).To(Equal(level)) + }) + } + + var TestLogData = func() { + var log lager.LogFormat + + BeforeEach(func() { + log = testSink.Logs()[0] + }) + + It("data contains custom user data", func() { + Expect(log.Data["foo"]).To(Equal("bar")) + Expect(log.Data["a-number"]).To(BeNumerically("==", 7)) + Expect(log.Data["baz"]).To(Equal("quux")) + Expect(log.Data["b-number"]).To(BeNumerically("==", 43)) + }) + } + + Describe("Session", func() { + var session lager.Logger + + BeforeEach(func() { + session = logger.Session("sub-action") + }) + + Describe("the returned logger", func() { + JustBeforeEach(func() { + session.Debug("some-debug-action", lager.Data{"level": "debug"}) + session.Info("some-info-action", lager.Data{"level": "info"}) + session.Error("some-error-action", errors.New("oh no!"), lager.Data{"level": "error"}) + + defer func() { + recover() + }() + + session.Fatal("some-fatal-action", errors.New("oh no!"), lager.Data{"level": "fatal"}) + }) + + It("logs with a shared session id in the data", func() { + Expect(testSink.Logs()[0].Data["session"]).To(Equal("1")) + Expect(testSink.Logs()[1].Data["session"]).To(Equal("1")) + Expect(testSink.Logs()[2].Data["session"]).To(Equal("1")) + Expect(testSink.Logs()[3].Data["session"]).To(Equal("1")) + }) + + It("logs with the task added to the message", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.sub-action.some-debug-action")) + Expect(testSink.Logs()[1].Message).To(Equal("my-component.sub-action.some-info-action")) + Expect(testSink.Logs()[2].Message).To(Equal("my-component.sub-action.some-error-action")) + Expect(testSink.Logs()[3].Message).To(Equal("my-component.sub-action.some-fatal-action")) + }) + + It("logs with the original data", func() { + Expect(testSink.Logs()[0].Data["level"]).To(Equal("debug")) + Expect(testSink.Logs()[1].Data["level"]).To(Equal("info")) + Expect(testSink.Logs()[2].Data["level"]).To(Equal("error")) + Expect(testSink.Logs()[3].Data["level"]).To(Equal("fatal")) + }) + + Context("with data", func() { + BeforeEach(func() { + session = logger.Session("sub-action", lager.Data{"foo": "bar"}) + }) + + It("logs with the data added to the message", func() { + Expect(testSink.Logs()[0].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[1].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[2].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[3].Data["foo"]).To(Equal("bar")) + }) + + It("keeps the original data", func() { + Expect(testSink.Logs()[0].Data["level"]).To(Equal("debug")) + Expect(testSink.Logs()[1].Data["level"]).To(Equal("info")) + Expect(testSink.Logs()[2].Data["level"]).To(Equal("error")) + Expect(testSink.Logs()[3].Data["level"]).To(Equal("fatal")) + }) + }) + + Context("with another session", func() { + BeforeEach(func() { + session = logger.Session("next-sub-action") + }) + + It("logs with a shared session id in the data", func() { + Expect(testSink.Logs()[0].Data["session"]).To(Equal("2")) + Expect(testSink.Logs()[1].Data["session"]).To(Equal("2")) + Expect(testSink.Logs()[2].Data["session"]).To(Equal("2")) + Expect(testSink.Logs()[3].Data["session"]).To(Equal("2")) + }) + + It("logs with the task added to the message", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.next-sub-action.some-debug-action")) + Expect(testSink.Logs()[1].Message).To(Equal("my-component.next-sub-action.some-info-action")) + Expect(testSink.Logs()[2].Message).To(Equal("my-component.next-sub-action.some-error-action")) + Expect(testSink.Logs()[3].Message).To(Equal("my-component.next-sub-action.some-fatal-action")) + }) + }) + + Describe("WithData", func() { + BeforeEach(func() { + session = logger.WithData(lager.Data{"foo": "bar"}) + }) + + It("returns a new logger with the given data", func() { + Expect(testSink.Logs()[0].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[1].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[2].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[3].Data["foo"]).To(Equal("bar")) + }) + + It("does not append to the logger's task", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.some-debug-action")) + }) + }) + + Context("with a nested session", func() { + BeforeEach(func() { + session = session.Session("sub-sub-action") + }) + + It("logs with a shared session id in the data", func() { + Expect(testSink.Logs()[0].Data["session"]).To(Equal("1.1")) + Expect(testSink.Logs()[1].Data["session"]).To(Equal("1.1")) + Expect(testSink.Logs()[2].Data["session"]).To(Equal("1.1")) + Expect(testSink.Logs()[3].Data["session"]).To(Equal("1.1")) + }) + + It("logs with the task added to the message", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.sub-action.sub-sub-action.some-debug-action")) + Expect(testSink.Logs()[1].Message).To(Equal("my-component.sub-action.sub-sub-action.some-info-action")) + Expect(testSink.Logs()[2].Message).To(Equal("my-component.sub-action.sub-sub-action.some-error-action")) + Expect(testSink.Logs()[3].Message).To(Equal("my-component.sub-action.sub-sub-action.some-fatal-action")) + }) + }) + }) + }) + + Describe("Debug", func() { + Context("with log data", func() { + BeforeEach(func() { + logger.Debug(action, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.DEBUG) + TestLogData() + }) + + Context("with no log data", func() { + BeforeEach(func() { + logger.Debug(action) + }) + + TestCommonLogFeatures(lager.DEBUG) + }) + }) + + Describe("Info", func() { + Context("with log data", func() { + BeforeEach(func() { + logger.Info(action, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.INFO) + TestLogData() + }) + + Context("with no log data", func() { + BeforeEach(func() { + logger.Info(action) + }) + + TestCommonLogFeatures(lager.INFO) + }) + }) + + Describe("Error", func() { + var err = errors.New("oh noes!") + Context("with log data", func() { + BeforeEach(func() { + logger.Error(action, err, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.ERROR) + TestLogData() + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + }) + + Context("with no log data", func() { + BeforeEach(func() { + logger.Error(action, err) + }) + + TestCommonLogFeatures(lager.ERROR) + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + }) + + Context("with no error", func() { + BeforeEach(func() { + logger.Error(action, nil) + }) + + TestCommonLogFeatures(lager.ERROR) + + It("does not contain the error message", func() { + Expect(testSink.Logs()[0].Data).NotTo(HaveKey("error")) + }) + }) + }) + + Describe("Fatal", func() { + var err = errors.New("oh noes!") + var fatalErr interface{} + + Context("with log data", func() { + BeforeEach(func() { + defer func() { + fatalErr = recover() + }() + + logger.Fatal(action, err, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.FATAL) + TestLogData() + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + + It("data contains stack trace", func() { + Expect(testSink.Logs()[0].Data["trace"]).NotTo(BeEmpty()) + }) + + It("panics with the provided error", func() { + Expect(fatalErr).To(Equal(err)) + }) + }) + + Context("with no log data", func() { + BeforeEach(func() { + defer func() { + fatalErr = recover() + }() + + logger.Fatal(action, err) + }) + + TestCommonLogFeatures(lager.FATAL) + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + + It("data contains stack trace", func() { + Expect(testSink.Logs()[0].Data["trace"]).NotTo(BeEmpty()) + }) + + It("panics with the provided error", func() { + Expect(fatalErr).To(Equal(err)) + }) + }) + + Context("with no error", func() { + BeforeEach(func() { + defer func() { + fatalErr = recover() + }() + + logger.Fatal(action, nil) + }) + + TestCommonLogFeatures(lager.FATAL) + + It("does not contain the error message", func() { + Expect(testSink.Logs()[0].Data).NotTo(HaveKey("error")) + }) + + It("data contains stack trace", func() { + Expect(testSink.Logs()[0].Data["trace"]).NotTo(BeEmpty()) + }) + + It("panics with the provided error (i.e. nil)", func() { + Expect(fatalErr).To(BeNil()) + }) + }) + }) +}) diff --git a/vendor/code.cloudfoundry.org/lager/models.go b/vendor/code.cloudfoundry.org/lager/models.go new file mode 100644 index 00000000000..94c0dac459e --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/models.go @@ -0,0 +1,30 @@ +package lager + +import "encoding/json" + +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + ERROR + FATAL +) + +type Data map[string]interface{} + +type LogFormat struct { + Timestamp string `json:"timestamp"` + Source string `json:"source"` + Message string `json:"message"` + LogLevel LogLevel `json:"log_level"` + Data Data `json:"data"` +} + +func (log LogFormat) ToJSON() []byte { + content, err := json.Marshal(log) + if err != nil { + panic(err) + } + return content +} diff --git a/vendor/code.cloudfoundry.org/lager/reconfigurable_sink.go b/vendor/code.cloudfoundry.org/lager/reconfigurable_sink.go new file mode 100644 index 00000000000..7c3b228e3be --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/reconfigurable_sink.go @@ -0,0 +1,35 @@ +package lager + +import "sync/atomic" + +type ReconfigurableSink struct { + sink Sink + + minLogLevel int32 +} + +func NewReconfigurableSink(sink Sink, initialMinLogLevel LogLevel) *ReconfigurableSink { + return &ReconfigurableSink{ + sink: sink, + + minLogLevel: int32(initialMinLogLevel), + } +} + +func (sink *ReconfigurableSink) Log(log LogFormat) { + minLogLevel := LogLevel(atomic.LoadInt32(&sink.minLogLevel)) + + if log.LogLevel < minLogLevel { + return + } + + sink.sink.Log(log) +} + +func (sink *ReconfigurableSink) SetMinLevel(level LogLevel) { + atomic.StoreInt32(&sink.minLogLevel, int32(level)) +} + +func (sink *ReconfigurableSink) GetMinLevel() LogLevel { + return LogLevel(atomic.LoadInt32(&sink.minLogLevel)) +} diff --git a/vendor/code.cloudfoundry.org/lager/reconfigurable_sink_test.go b/vendor/code.cloudfoundry.org/lager/reconfigurable_sink_test.go new file mode 100644 index 00000000000..466b73707d3 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/reconfigurable_sink_test.go @@ -0,0 +1,66 @@ +package lager_test + +import ( + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/lagertest" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ReconfigurableSink", func() { + var ( + testSink *lagertest.TestSink + + sink *lager.ReconfigurableSink + ) + + BeforeEach(func() { + testSink = lagertest.NewTestSink() + + sink = lager.NewReconfigurableSink(testSink, lager.INFO) + }) + + It("returns the current level", func() { + Expect(sink.GetMinLevel()).To(Equal(lager.INFO)) + }) + + Context("when logging above the minimum log level", func() { + var log lager.LogFormat + + BeforeEach(func() { + log = lager.LogFormat{LogLevel: lager.INFO, Message: "hello world"} + sink.Log(log) + }) + + It("writes to the given sink", func() { + Expect(testSink.Buffer().Contents()).To(MatchJSON(log.ToJSON())) + }) + }) + + Context("when logging below the minimum log level", func() { + BeforeEach(func() { + sink.Log(lager.LogFormat{LogLevel: lager.DEBUG, Message: "hello world"}) + }) + + It("does not write to the given writer", func() { + Expect(testSink.Buffer().Contents()).To(BeEmpty()) + }) + }) + + Context("when reconfigured to a new log level", func() { + BeforeEach(func() { + sink.SetMinLevel(lager.DEBUG) + }) + + It("writes logs above the new log level", func() { + log := lager.LogFormat{LogLevel: lager.DEBUG, Message: "hello world"} + sink.Log(log) + Expect(testSink.Buffer().Contents()).To(MatchJSON(log.ToJSON())) + }) + + It("returns the newly updated level", func() { + Expect(sink.GetMinLevel()).To(Equal(lager.DEBUG)) + }) + }) +}) diff --git a/vendor/code.cloudfoundry.org/lager/writer_sink.go b/vendor/code.cloudfoundry.org/lager/writer_sink.go new file mode 100644 index 00000000000..bb8fbf15103 --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/writer_sink.go @@ -0,0 +1,38 @@ +package lager + +import ( + "io" + "sync" +) + +// A Sink represents a write destination for a Logger. It provides +// a thread-safe interface for writing logs +type Sink interface { + //Log to the sink. Best effort -- no need to worry about errors. + Log(LogFormat) +} + +type writerSink struct { + writer io.Writer + minLogLevel LogLevel + writeL *sync.Mutex +} + +func NewWriterSink(writer io.Writer, minLogLevel LogLevel) Sink { + return &writerSink{ + writer: writer, + minLogLevel: minLogLevel, + writeL: new(sync.Mutex), + } +} + +func (sink *writerSink) Log(log LogFormat) { + if log.LogLevel < sink.minLogLevel { + return + } + + sink.writeL.Lock() + sink.writer.Write(log.ToJSON()) + sink.writer.Write([]byte("\n")) + sink.writeL.Unlock() +} diff --git a/vendor/code.cloudfoundry.org/lager/writer_sink_test.go b/vendor/code.cloudfoundry.org/lager/writer_sink_test.go new file mode 100644 index 00000000000..ca9ba0b345b --- /dev/null +++ b/vendor/code.cloudfoundry.org/lager/writer_sink_test.go @@ -0,0 +1,107 @@ +package lager_test + +import ( + "fmt" + "runtime" + "strings" + "sync" + + "code.cloudfoundry.org/lager" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("WriterSink", func() { + const MaxThreads = 100 + + var sink lager.Sink + var writer *copyWriter + + BeforeSuite(func() { + runtime.GOMAXPROCS(MaxThreads) + }) + + BeforeEach(func() { + writer = NewCopyWriter() + sink = lager.NewWriterSink(writer, lager.INFO) + }) + + Context("when logging above the minimum log level", func() { + BeforeEach(func() { + sink.Log(lager.LogFormat{LogLevel: lager.INFO, Message: "hello world"}) + }) + + It("writes to the given writer", func() { + Expect(writer.Copy()).To(MatchJSON(`{"message":"hello world","log_level":1,"timestamp":"","source":"","data":null}`)) + }) + }) + + Context("when logging below the minimum log level", func() { + BeforeEach(func() { + sink.Log(lager.LogFormat{LogLevel: lager.DEBUG, Message: "hello world"}) + }) + + It("does not write to the given writer", func() { + Expect(writer.Copy()).To(Equal([]byte{})) + }) + }) + + Context("when logging from multiple threads", func() { + var content = "abcdefg " + + BeforeEach(func() { + wg := new(sync.WaitGroup) + for i := 0; i < MaxThreads; i++ { + wg.Add(1) + go func() { + sink.Log(lager.LogFormat{LogLevel: lager.INFO, Message: content}) + wg.Done() + }() + } + wg.Wait() + }) + + It("writes to the given writer", func() { + lines := strings.Split(string(writer.Copy()), "\n") + for _, line := range lines { + if line == "" { + continue + } + Expect(line).To(MatchJSON(fmt.Sprintf(`{"message":"%s","log_level":1,"timestamp":"","source":"","data":null}`, content))) + } + }) + }) +}) + +// copyWriter is an INTENTIONALLY UNSAFE writer. Use it to test code that +// should be handling thread safety. +type copyWriter struct { + contents []byte + lock *sync.RWMutex +} + +func NewCopyWriter() *copyWriter { + return ©Writer{ + contents: []byte{}, + lock: new(sync.RWMutex), + } +} + +// no, we really mean RLock on write. +func (writer *copyWriter) Write(p []byte) (n int, err error) { + writer.lock.RLock() + defer writer.lock.RUnlock() + + writer.contents = append(writer.contents, p...) + return len(p), nil +} + +func (writer *copyWriter) Copy() []byte { + writer.lock.Lock() + defer writer.lock.Unlock() + + contents := make([]byte, len(writer.contents)) + copy(contents, writer.contents) + return contents +} diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md index cdab8784d11..56c67134f89 100644 --- a/vendor/github.com/gorilla/mux/README.md +++ b/vendor/github.com/gorilla/mux/README.md @@ -179,6 +179,7 @@ package main import ( "fmt" "net/http" + "strings" "github.com/gorilla/mux" ) @@ -190,15 +191,25 @@ func handler(w http.ResponseWriter, r *http.Request) { func main() { r := mux.NewRouter() r.HandleFunc("/", handler) - r.HandleFunc("/products", handler) - r.HandleFunc("/articles", handler) - r.HandleFunc("/articles/{id}", handler) + r.Methods("POST").HandleFunc("/products", handler) + r.Methods("GET").HandleFunc("/articles", handler) + r.Methods("GET", "PUT").HandleFunc("/articles/{id}", handler) r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { t, err := route.GetPathTemplate() if err != nil { return err } - fmt.Println(t) + // p will contain regular expression is compatible with regular expression in Perl, Python, and other languages. + // for instance the regular expression for path '/articles/{id}' will be '^/articles/(?P[^/]+)$' + p, err := route.GetPathRegexp() + if err != nil { + return err + } + m, err := route.GetMethods() + if err != nil { + return err + } + fmt.Println(strings.Join(m, ","), t, p) return nil }) http.Handle("/", r) diff --git a/vendor/github.com/gorilla/mux/mux_test.go b/vendor/github.com/gorilla/mux/mux_test.go index 405aca6de94..19ef5a8cceb 100644 --- a/vendor/github.com/gorilla/mux/mux_test.go +++ b/vendor/github.com/gorilla/mux/mux_test.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strings" "testing" ) @@ -31,10 +32,13 @@ type routeTest struct { route *Route // the route being tested request *http.Request // a request to test the route vars map[string]string // the expected vars of the match - host string // the expected host of the match - path string // the expected path of the match - pathTemplate string // the expected path template to match - hostTemplate string // the expected host template to match + scheme string // the expected scheme of the built URL + host string // the expected host of the built URL + path string // the expected path of the built URL + pathTemplate string // the expected path template of the route + hostTemplate string // the expected host template of the route + methods []string // the expected route methods + pathRegexp string // the expected path regexp shouldMatch bool // whether the request is expected to match the route at all shouldRedirect bool // whether the request should result in a redirect } @@ -195,46 +199,6 @@ func TestHost(t *testing.T) { hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, shouldMatch: true, }, - { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/a"), - vars: map[string]string{"category": "a"}, - host: "", - path: "/a", - pathTemplate: `/{category:a|b/c}`, - shouldMatch: true, - }, - { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/b/c"), - vars: map[string]string{"category": "b/c"}, - host: "", - path: "/b/c", - pathTemplate: `/{category:a|b/c}`, - shouldMatch: true, - }, - { - title: "Path route with multiple patterns with pipe, match", - route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), - request: newRequest("GET", "http://localhost/a/product_name/1"), - vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, - host: "", - path: "/a/product_name/1", - pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, - }, - { - title: "Path route with multiple patterns with pipe, match", - route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), - request: newRequest("GET", "http://localhost/b/c/product_name/1"), - vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, - host: "", - path: "/b/c/product_name/1", - pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, - }, } for _, test := range tests { testRoute(t, test) @@ -270,6 +234,7 @@ func TestPath(t *testing.T) { host: "", path: "/111", pathTemplate: `/111/`, + pathRegexp: `^/111/$`, shouldMatch: false, }, { @@ -290,6 +255,7 @@ func TestPath(t *testing.T) { host: "", path: "/", pathTemplate: `/`, + pathRegexp: `^/$`, shouldMatch: true, }, { @@ -333,6 +299,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/111/{v1:[0-9]{3}}/333`, + pathRegexp: `^/111/(?P[0-9]{3})/333$`, shouldMatch: false, }, { @@ -343,6 +310,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, + pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, shouldMatch: true, }, { @@ -353,6 +321,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, + pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, shouldMatch: false, }, { @@ -363,6 +332,7 @@ func TestPath(t *testing.T) { host: "", path: "/a/product_name/1", pathTemplate: `/{category:a|(?:b/c)}/{product}/{id:[0-9]+}`, + pathRegexp: `^/(?Pa|(?:b/c))/(?P[^/]+)/(?P[0-9]+)$`, shouldMatch: true, }, { @@ -373,6 +343,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/111/{v-1:[0-9]{3}}/333`, + pathRegexp: `^/111/(?P[0-9]{3})/333$`, shouldMatch: true, }, { @@ -383,6 +354,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}`, + pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, shouldMatch: true, }, { @@ -393,6 +365,7 @@ func TestPath(t *testing.T) { host: "", path: "/a/product_name/1", pathTemplate: `/{product-category:a|(?:b/c)}/{product-name}/{product-id:[0-9]+}`, + pathRegexp: `^/(?Pa|(?:b/c))/(?P[^/]+)/(?P[0-9]+)$`, shouldMatch: true, }, { @@ -403,6 +376,7 @@ func TestPath(t *testing.T) { host: "", path: "/daily-2016-01-01", pathTemplate: `/{type:(?i:daily|mini|variety)}-{date:\d{4,4}-\d{2,2}-\d{2,2}}`, + pathRegexp: `^/(?P(?i:daily|mini|variety))-(?P\d{4,4}-\d{2,2}-\d{2,2})$`, shouldMatch: true, }, { @@ -413,6 +387,47 @@ func TestPath(t *testing.T) { host: "", path: "/111/222", pathTemplate: `/{v1:[0-9]*}{v2:[a-z]*}/{v3:[0-9]*}`, + pathRegexp: `^/(?P[0-9]*)(?P[a-z]*)/(?P[0-9]*)$`, + shouldMatch: true, + }, + { + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/a"), + vars: map[string]string{"category": "a"}, + host: "", + path: "/a", + pathTemplate: `/{category:a|b/c}`, + shouldMatch: true, + }, + { + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/b/c"), + vars: map[string]string{"category": "b/c"}, + host: "", + path: "/b/c", + pathTemplate: `/{category:a|b/c}`, + shouldMatch: true, + }, + { + title: "Path route with multiple patterns with pipe, match", + route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), + request: newRequest("GET", "http://localhost/a/product_name/1"), + vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, + host: "", + path: "/a/product_name/1", + pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, + shouldMatch: true, + }, + { + title: "Path route with multiple patterns with pipe, match", + route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), + request: newRequest("GET", "http://localhost/b/c/product_name/1"), + vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, + host: "", + path: "/b/c/product_name/1", + pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, shouldMatch: true, }, } @@ -421,6 +436,7 @@ func TestPath(t *testing.T) { testRoute(t, test) testTemplate(t, test) testUseEscapedRoute(t, test) + testRegexp(t, test) } } @@ -502,15 +518,28 @@ func TestPathPrefix(t *testing.T) { } } -func TestHostPath(t *testing.T) { +func TestSchemeHostPath(t *testing.T) { tests := []routeTest{ { title: "Host and Path route, match", route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), vars: map[string]string{}, - host: "", - path: "", + scheme: "http", + host: "aaa.bbb.ccc", + path: "/111/222/333", + pathTemplate: `/111/222/333`, + hostTemplate: `aaa.bbb.ccc`, + shouldMatch: true, + }, + { + title: "Scheme, Host, and Path route, match", + route: new(Route).Schemes("https").Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "https://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{}, + scheme: "https", + host: "aaa.bbb.ccc", + path: "/111/222/333", pathTemplate: `/111/222/333`, hostTemplate: `aaa.bbb.ccc`, shouldMatch: true, @@ -520,8 +549,9 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), vars: map[string]string{}, - host: "", - path: "", + scheme: "http", + host: "aaa.bbb.ccc", + path: "/111/222/333", pathTemplate: `/111/222/333`, hostTemplate: `aaa.bbb.ccc`, shouldMatch: false, @@ -531,6 +561,19 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), vars: map[string]string{"v1": "bbb", "v2": "222"}, + scheme: "http", + host: "aaa.bbb.ccc", + path: "/111/222/333", + pathTemplate: `/111/{v2:[0-9]{3}}/333`, + hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, + shouldMatch: true, + }, + { + title: "Scheme, Host, and Path route with host and path patterns, match", + route: new(Route).Schemes("ftp", "ssss").Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "ssss://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + scheme: "ftp", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/111/{v2:[0-9]{3}}/333`, @@ -542,6 +585,7 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), vars: map[string]string{"v1": "bbb", "v2": "222"}, + scheme: "http", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/111/{v2:[0-9]{3}}/333`, @@ -553,6 +597,7 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + scheme: "http", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, @@ -564,6 +609,7 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + scheme: "http", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, @@ -635,7 +681,6 @@ func TestHeaders(t *testing.T) { testRoute(t, test) testTemplate(t, test) } - } func TestMethods(t *testing.T) { @@ -647,6 +692,7 @@ func TestMethods(t *testing.T) { vars: map[string]string{}, host: "", path: "", + methods: []string{"GET", "POST"}, shouldMatch: true, }, { @@ -656,6 +702,7 @@ func TestMethods(t *testing.T) { vars: map[string]string{}, host: "", path: "", + methods: []string{"GET", "POST"}, shouldMatch: true, }, { @@ -665,13 +712,25 @@ func TestMethods(t *testing.T) { vars: map[string]string{}, host: "", path: "", + methods: []string{"GET", "POST"}, shouldMatch: false, }, + { + title: "Route without methods", + route: new(Route), + request: newRequest("PUT", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + methods: []string{}, + shouldMatch: true, + }, } for _, test := range tests { testRoute(t, test) testTemplate(t, test) + testMethods(t, test) } } @@ -910,30 +969,43 @@ func TestSchemes(t *testing.T) { tests := []routeTest{ // Schemes { - title: "Schemes route, match https", - route: new(Route).Schemes("https", "ftp"), + title: "Schemes route, default scheme, match http, build http", + route: new(Route).Host("localhost"), + request: newRequest("GET", "http://localhost"), + scheme: "http", + host: "localhost", + shouldMatch: true, + }, + { + title: "Schemes route, match https, build https", + route: new(Route).Schemes("https", "ftp").Host("localhost"), request: newRequest("GET", "https://localhost"), - vars: map[string]string{}, - host: "", - path: "", + scheme: "https", + host: "localhost", shouldMatch: true, }, { - title: "Schemes route, match ftp", - route: new(Route).Schemes("https", "ftp"), + title: "Schemes route, match ftp, build https", + route: new(Route).Schemes("https", "ftp").Host("localhost"), request: newRequest("GET", "ftp://localhost"), - vars: map[string]string{}, - host: "", - path: "", + scheme: "https", + host: "localhost", + shouldMatch: true, + }, + { + title: "Schemes route, match ftp, build ftp", + route: new(Route).Schemes("ftp", "https").Host("localhost"), + request: newRequest("GET", "ftp://localhost"), + scheme: "ftp", + host: "localhost", shouldMatch: true, }, { title: "Schemes route, bad scheme", - route: new(Route).Schemes("https", "ftp"), + route: new(Route).Schemes("https", "ftp").Host("localhost"), request: newRequest("GET", "http://localhost"), - vars: map[string]string{}, - host: "", - path: "", + scheme: "https", + host: "localhost", shouldMatch: false, }, } @@ -1393,7 +1465,7 @@ func TestSubrouterErrorHandling(t *testing.T) { func TestPanicOnCapturingGroups(t *testing.T) { defer func() { if recover() == nil { - t.Errorf("(Test that capturing groups now fail fast) Expected panic, however test completed sucessfully.\n") + t.Errorf("(Test that capturing groups now fail fast) Expected panic, however test completed successfully.\n") } }() NewRouter().NewRoute().Path("/{type:(promo|special)}/{promoId}.json") @@ -1420,10 +1492,15 @@ func testRoute(t *testing.T, test routeTest) { route := test.route vars := test.vars shouldMatch := test.shouldMatch - host := test.host - path := test.path - url := test.host + test.path shouldRedirect := test.shouldRedirect + uri := url.URL{ + Scheme: test.scheme, + Host: test.host, + Path: test.path, + } + if uri.Scheme == "" { + uri.Scheme = "http" + } var match RouteMatch ok := route.Match(request, &match) @@ -1436,28 +1513,51 @@ func testRoute(t *testing.T, test routeTest) { return } if shouldMatch { - if test.vars != nil && !stringMapEqual(test.vars, match.Vars) { + if vars != nil && !stringMapEqual(vars, match.Vars) { t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) return } - if host != "" { - u, _ := test.route.URLHost(mapToPairs(match.Vars)...) - if host != u.Host { - t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route)) + if test.scheme != "" { + u, err := route.URL(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if uri.Scheme != u.Scheme { + t.Errorf("(%v) URLScheme not equal: expected %v, got %v", test.title, uri.Scheme, u.Scheme) return } } - if path != "" { - u, _ := route.URLPath(mapToPairs(match.Vars)...) - if path != u.Path { - t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route)) + if test.host != "" { + u, err := test.route.URLHost(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URLHost error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if uri.Scheme != u.Scheme { + t.Errorf("(%v) URLHost scheme not equal: expected %v, got %v -- %v", test.title, uri.Scheme, u.Scheme, getRouteTemplate(route)) + return + } + if uri.Host != u.Host { + t.Errorf("(%v) URLHost host not equal: expected %v, got %v -- %v", test.title, uri.Host, u.Host, getRouteTemplate(route)) + return + } + } + if test.path != "" { + u, err := route.URLPath(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URLPath error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if uri.Path != u.Path { + t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, uri.Path, u.Path, getRouteTemplate(route)) return } } - if url != "" { - u, _ := route.URL(mapToPairs(match.Vars)...) - if url != u.Host+u.Path { - t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route)) + if test.host != "" && test.path != "" { + u, err := route.URL(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if expected, got := uri.String(), u.String(); expected != got { + t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, expected, got, getRouteTemplate(route)) return } } @@ -1499,6 +1599,22 @@ func testTemplate(t *testing.T, test routeTest) { } } +func testMethods(t *testing.T, test routeTest) { + route := test.route + methods, _ := route.GetMethods() + if strings.Join(methods, ",") != strings.Join(test.methods, ",") { + t.Errorf("(%v) GetMethods not equal: expected %v, got %v", test.title, test.methods, methods) + } +} + +func testRegexp(t *testing.T, test routeTest) { + route := test.route + routePathRegexp, regexpErr := route.GetPathRegexp() + if test.pathRegexp != "" && regexpErr == nil && routePathRegexp != test.pathRegexp { + t.Errorf("(%v) GetPathRegexp not equal: expected %v, got %v", test.title, test.pathRegexp, routePathRegexp) + } +} + type TestA301ResponseWriter struct { hh http.Header status int diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go index 5544c1fd6ba..56dcbbdc50a 100644 --- a/vendor/github.com/gorilla/mux/route.go +++ b/vendor/github.com/gorilla/mux/route.go @@ -31,6 +31,8 @@ type Route struct { skipClean bool // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" useEncodedPath bool + // The scheme used when building URLs. + buildScheme string // If true, this route never matches: it is only used to build URLs. buildOnly bool // The name used to build URLs. @@ -394,6 +396,9 @@ func (r *Route) Schemes(schemes ...string) *Route { for k, v := range schemes { schemes[k] = strings.ToLower(v) } + if r.buildScheme == "" && len(schemes) > 0 { + r.buildScheme = schemes[0] + } return r.addMatcher(schemeMatcher(schemes)) } @@ -478,11 +483,13 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) { } var scheme, host, path string if r.regexp.host != nil { - // Set a default scheme. - scheme = "http" if host, err = r.regexp.host.url(values); err != nil { return nil, err } + scheme = "http" + if r.buildScheme != "" { + scheme = r.buildScheme + } } if r.regexp.path != nil { if path, err = r.regexp.path.url(values); err != nil { @@ -514,10 +521,14 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) { if err != nil { return nil, err } - return &url.URL{ + u := &url.URL{ Scheme: "http", Host: host, - }, nil + } + if r.buildScheme != "" { + u.Scheme = r.buildScheme + } + return u, nil } // URLPath builds the path part of the URL for a route. See Route.URL(). @@ -558,6 +569,36 @@ func (r *Route) GetPathTemplate() (string, error) { return r.regexp.path.template, nil } +// GetPathRegexp returns the expanded regular expression used to match route path. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathRegexp() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.path == nil { + return "", errors.New("mux: route does not have a path") + } + return r.regexp.path.regexp.String(), nil +} + +// GetMethods returns the methods the route matches against +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An empty list will be returned if route does not have methods. +func (r *Route) GetMethods() ([]string, error) { + if r.err != nil { + return nil, r.err + } + for _, m := range r.matchers { + if methods, ok := m.(methodMatcher); ok { + return []string(methods), nil + } + } + return nil, nil +} + // GetHostTemplate returns the template used to build the // route match. // This is useful for building simple REST API documentation and for instrumentation diff --git a/vendor/github.com/pivotal-cf/brokerapi/.travis.yml b/vendor/github.com/pivotal-cf/brokerapi/.travis.yml new file mode 100644 index 00000000000..241fa1661fe --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/.travis.yml @@ -0,0 +1,11 @@ +language: go + +go: + - 1.7.3 + +install: + - go get -v github.com/tools/godep + - godep restore + - go get -v github.com/onsi/ginkgo/ginkgo + +script: ginkgo -r diff --git a/vendor/github.com/pivotal-cf/brokerapi/Godeps/Godeps.json b/vendor/github.com/pivotal-cf/brokerapi/Godeps/Godeps.json new file mode 100644 index 00000000000..27202aa3a33 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/Godeps/Godeps.json @@ -0,0 +1,180 @@ +{ + "ImportPath": "github.com/pivotal-cf/brokerapi", + "GoVersion": "go1.7", + "GodepVersion": "v74", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "code.cloudfoundry.org/lager", + "Rev": "62951a8009ab331bb21dc418074fa54e66eb9b6a" + }, + { + "ImportPath": "code.cloudfoundry.org/lager/lagertest", + "Rev": "62951a8009ab331bb21dc418074fa54e66eb9b6a" + }, + { + "ImportPath": "github.com/drewolson/testflight", + "Rev": "7040c250b4721006c536587ff8ee8ff573838edb" + }, + { + "ImportPath": "github.com/gorilla/mux", + "Comment": "v1.1-21-gcf79e51", + "Rev": "cf79e51a62d8219d52060dfc1b4e810414ba2d15" + }, + { + "ImportPath": "github.com/onsi/ginkgo", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/config", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/codelocation", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/containernode", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/failer", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/leafnodes", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/remote", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/spec", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/specrunner", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/suite", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/testingtproxy", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/writer", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/reporters", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/reporters/stenographer", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/types", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/gomega", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/format", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/gbytes", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/assertion", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/asyncassertion", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/oraclematcher", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/testingtsupport", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/edge", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/node", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/util", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/types", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/pborman/uuid", + "Comment": "v1.0-11-gc55201b", + "Rev": "c55201b036063326c5b1b89ccfe45a184973d073" + }, + { + "ImportPath": "golang.org/x/sys/unix", + "Rev": "a408501be4d17ee978c04a618e7a1b22af058c0e" + }, + { + "ImportPath": "gopkg.in/yaml.v2", + "Rev": "e4d366fc3c7938e2958e662b4258c7a89e1f0e3e" + } + ] +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/Godeps/Readme b/vendor/github.com/pivotal-cf/brokerapi/Godeps/Readme new file mode 100644 index 00000000000..4cdaa53d56d --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/vendor/github.com/pivotal-cf/brokerapi/LICENSE b/vendor/github.com/pivotal-cf/brokerapi/LICENSE new file mode 100644 index 00000000000..5c304d1a4a7 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/pivotal-cf/brokerapi/NOTICE b/vendor/github.com/pivotal-cf/brokerapi/NOTICE new file mode 100644 index 00000000000..85a3bd09f02 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/NOTICE @@ -0,0 +1,10 @@ +brokerapi + +Copyright (c) 2014-2015 Pivotal Software, Inc. All Rights Reserved. + +This product is licensed to you under the Apache License, Version 2.0 (the "License"). +You may not use this product except in compliance with the License. + +This product may include a number of subcomponents with separate copyright notices +and license terms. Your use of these subcomponents is subject to the terms and +conditions of the subcomponent's license, as noted in the LICENSE file. diff --git a/vendor/github.com/pivotal-cf/brokerapi/README.md b/vendor/github.com/pivotal-cf/brokerapi/README.md new file mode 100644 index 00000000000..dc66bef3012 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/README.md @@ -0,0 +1,31 @@ +# brokerapi + +[![Build Status](https://travis-ci.org/pivotal-cf/brokerapi.svg?branch=master)](https://travis-ci.org/pivotal-cf/brokerapi) + +A Go package for building [V2 Cloud Foundry Service Brokers](https://docs.cloudfoundry.org/services/api.html). + +## [Docs](https://godoc.org/github.com/pivotal-cf/brokerapi) + +## Dependencies + +- Go 1.7+ +- [lager](https://github.com/cloudfoundry/lager) +- [gorilla/mux](https://github.com/gorilla/mux) + +## Usage + +`brokerapi` defines a [`ServiceBroker`](https://godoc.org/github.com/pivotal-cf/brokerapi#ServiceBroker) interface. Pass an implementation of this to [`brokerapi.New`](https://godoc.org/github.com/pivotal-cf/brokerapi#New), which returns an `http.Handler` that you can use to serve handle HTTP requests. + +Alternatively, if you already have a `*mux.Router` that you want to attach service broker routes to, you can use [`brokerapi.AttachRoutes`](https://godoc.org/github.com/pivotal-cf/brokerapi#AttachRoutes). + +## Error types + +`brokerapi` defines a handful of error types in `service_broker.go` for some common error cases that your service broker may encounter. Return these from your `ServiceBroker` methods where appropriate, and `brokerapi` will do the "right thing" (™), and give Cloud Foundry an appropriate status code, as per the [Service Broker API specification](https://docs.cloudfoundry.org/services/api.html). + +### Custom Errors + +`NewFailureResponse()` allows you to return a custom error from any of the `ServiceBroker` interface methods which return an error. Within this you must define an error, a HTTP response status code and a logging key. You can also use the `NewFailureResponseBuilder()` to add a custom `Error:` value in the response, or indicate that the broker should return an empty response rather than the error message. + +## Example Service Broker + +You can see the [cf-redis](https://github.com/pivotal-cf/cf-redis-broker/blob/2f0e9a8ebb1012a9be74bbef2d411b0b3b60352f/broker/broker.go) service broker uses the BrokerAPI package to create a service broker for Redis. diff --git a/vendor/github.com/pivotal-cf/brokerapi/api.go b/vendor/github.com/pivotal-cf/brokerapi/api.go new file mode 100644 index 00000000000..0f5655b8775 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/api.go @@ -0,0 +1,355 @@ +package brokerapi + +import ( + "encoding/json" + "net/http" + "strconv" + + "code.cloudfoundry.org/lager" + "github.com/gorilla/mux" + "github.com/pivotal-cf/brokerapi/auth" +) + +const ( + provisionLogKey = "provision" + deprovisionLogKey = "deprovision" + bindLogKey = "bind" + unbindLogKey = "unbind" + lastOperationLogKey = "lastOperation" + + instanceIDLogKey = "instance-id" + instanceDetailsLogKey = "instance-details" + bindingIDLogKey = "binding-id" + + invalidServiceDetailsErrorKey = "invalid-service-details" + invalidBindDetailsErrorKey = "invalid-bind-details" + instanceLimitReachedErrorKey = "instance-limit-reached" + instanceAlreadyExistsErrorKey = "instance-already-exists" + bindingAlreadyExistsErrorKey = "binding-already-exists" + instanceMissingErrorKey = "instance-missing" + bindingMissingErrorKey = "binding-missing" + asyncRequiredKey = "async-required" + planChangeNotSupportedKey = "plan-change-not-supported" + unknownErrorKey = "unknown-error" + invalidRawParamsKey = "invalid-raw-params" + appGuidNotProvidedErrorKey = "app-guid-not-provided" +) + +type BrokerCredentials struct { + Username string + Password string +} + +func New(serviceBroker ServiceBroker, logger lager.Logger, brokerCredentials BrokerCredentials) http.Handler { + router := mux.NewRouter() + AttachRoutes(router, serviceBroker, logger) + return auth.NewWrapper(brokerCredentials.Username, brokerCredentials.Password).Wrap(router) +} + +func AttachRoutes(router *mux.Router, serviceBroker ServiceBroker, logger lager.Logger) { + handler := serviceBrokerHandler{serviceBroker: serviceBroker, logger: logger} + router.HandleFunc("/v2/catalog", handler.catalog).Methods("GET") + + router.HandleFunc("/v2/service_instances/{instance_id}", handler.provision).Methods("PUT") + router.HandleFunc("/v2/service_instances/{instance_id}", handler.deprovision).Methods("DELETE") + router.HandleFunc("/v2/service_instances/{instance_id}/last_operation", handler.lastOperation).Methods("GET") + router.HandleFunc("/v2/service_instances/{instance_id}", handler.update).Methods("PATCH") + + router.HandleFunc("/v2/service_instances/{instance_id}/service_bindings/{binding_id}", handler.bind).Methods("PUT") + router.HandleFunc("/v2/service_instances/{instance_id}/service_bindings/{binding_id}", handler.unbind).Methods("DELETE") +} + +type serviceBrokerHandler struct { + serviceBroker ServiceBroker + logger lager.Logger +} + +func (h serviceBrokerHandler) catalog(w http.ResponseWriter, req *http.Request) { + catalog := CatalogResponse{ + Services: h.serviceBroker.Services(req.Context()), + } + + h.respond(w, http.StatusOK, catalog) +} + +func (h serviceBrokerHandler) provision(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + + logger := h.logger.Session(provisionLogKey, lager.Data{ + instanceIDLogKey: instanceID, + }) + + var details ProvisionDetails + if err := json.NewDecoder(req.Body).Decode(&details); err != nil { + logger.Error(invalidServiceDetailsErrorKey, err) + h.respond(w, http.StatusUnprocessableEntity, ErrorResponse{ + Description: err.Error(), + }) + return + } + + acceptsIncompleteFlag, _ := strconv.ParseBool(req.URL.Query().Get("accepts_incomplete")) + + logger = logger.WithData(lager.Data{ + instanceDetailsLogKey: details, + }) + + provisionResponse, err := h.serviceBroker.Provision(req.Context(), instanceID, details, acceptsIncompleteFlag) + + if err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + if provisionResponse.IsAsync { + h.respond(w, http.StatusAccepted, ProvisioningResponse{ + DashboardURL: provisionResponse.DashboardURL, + OperationData: provisionResponse.OperationData, + }) + } else { + h.respond(w, http.StatusCreated, ProvisioningResponse{ + DashboardURL: provisionResponse.DashboardURL, + }) + } +} + +func (h serviceBrokerHandler) update(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + + var details UpdateDetails + if err := json.NewDecoder(req.Body).Decode(&details); err != nil { + h.logger.Error(invalidServiceDetailsErrorKey, err) + h.respond(w, http.StatusUnprocessableEntity, ErrorResponse{ + Description: err.Error(), + }) + return + } + + acceptsIncompleteFlag, _ := strconv.ParseBool(req.URL.Query().Get("accepts_incomplete")) + + updateServiceSpec, err := h.serviceBroker.Update(req.Context(), instanceID, details, acceptsIncompleteFlag) + if err != nil { + switch err := err.(type) { + case *FailureResponse: + h.logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(h.logger), err.ErrorResponse()) + default: + h.logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + statusCode := http.StatusOK + if updateServiceSpec.IsAsync { + statusCode = http.StatusAccepted + } + h.respond(w, statusCode, UpdateResponse{OperationData: updateServiceSpec.OperationData}) +} + +func (h serviceBrokerHandler) deprovision(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + logger := h.logger.Session(deprovisionLogKey, lager.Data{ + instanceIDLogKey: instanceID, + }) + + details := DeprovisionDetails{ + PlanID: req.FormValue("plan_id"), + ServiceID: req.FormValue("service_id"), + } + asyncAllowed := req.FormValue("accepts_incomplete") == "true" + + deprovisionSpec, err := h.serviceBroker.Deprovision(req.Context(), instanceID, details, asyncAllowed) + if err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + if deprovisionSpec.IsAsync { + h.respond(w, http.StatusAccepted, DeprovisionResponse{OperationData: deprovisionSpec.OperationData}) + } else { + h.respond(w, http.StatusOK, EmptyResponse{}) + } +} + +func (h serviceBrokerHandler) bind(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + bindingID := vars["binding_id"] + + logger := h.logger.Session(bindLogKey, lager.Data{ + instanceIDLogKey: instanceID, + bindingIDLogKey: bindingID, + }) + + var details BindDetails + if err := json.NewDecoder(req.Body).Decode(&details); err != nil { + logger.Error(invalidBindDetailsErrorKey, err) + h.respond(w, http.StatusUnprocessableEntity, ErrorResponse{ + Description: err.Error(), + }) + return + } + + binding, err := h.serviceBroker.Bind(req.Context(), instanceID, bindingID, details) + if err != nil { + switch err := err.(type) { + case *FailureResponse: + statusCode := err.ValidatedStatusCode(logger) + errorResponse := err.ErrorResponse() + if err == ErrInstanceDoesNotExist { + // work around ErrInstanceDoesNotExist having different pre-refactor behaviour to other actions + errorResponse = ErrorResponse{ + Description: err.Error(), + } + statusCode = http.StatusNotFound + } + logger.Error(err.LoggerAction(), err) + h.respond(w, statusCode, errorResponse) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + brokerAPIVersion := req.Header.Get("X-Broker-Api-Version") + if brokerAPIVersion == "2.8" || brokerAPIVersion == "2.9" { + experimentalVols := []ExperimentalVolumeMount{} + + for _, vol := range binding.VolumeMounts { + experimentalConfig, err := json.Marshal(vol.Device.MountConfig) + if err != nil { + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{Description: err.Error()}) + return + } + + experimentalVols = append(experimentalVols, ExperimentalVolumeMount{ + ContainerPath: vol.ContainerDir, + Mode: vol.Mode, + Private: ExperimentalVolumeMountPrivate{ + Driver: vol.Driver, + GroupID: vol.Device.VolumeId, + Config: string(experimentalConfig), + }, + }) + } + + experimentalBinding := ExperimentalVolumeMountBindingResponse{ + Credentials: binding.Credentials, + RouteServiceURL: binding.RouteServiceURL, + SyslogDrainURL: binding.SyslogDrainURL, + VolumeMounts: experimentalVols, + } + h.respond(w, http.StatusCreated, experimentalBinding) + return + } + + h.respond(w, http.StatusCreated, binding) +} + +func (h serviceBrokerHandler) unbind(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + bindingID := vars["binding_id"] + + logger := h.logger.Session(unbindLogKey, lager.Data{ + instanceIDLogKey: instanceID, + bindingIDLogKey: bindingID, + }) + + details := UnbindDetails{ + PlanID: req.FormValue("plan_id"), + ServiceID: req.FormValue("service_id"), + } + + if err := h.serviceBroker.Unbind(req.Context(), instanceID, bindingID, details); err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + h.respond(w, http.StatusOK, EmptyResponse{}) +} + +func (h serviceBrokerHandler) lastOperation(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + operationData := req.FormValue("operation") + + logger := h.logger.Session(lastOperationLogKey, lager.Data{ + instanceIDLogKey: instanceID, + }) + + logger.Info("starting-check-for-operation") + + lastOperation, err := h.serviceBroker.LastOperation(req.Context(), instanceID, operationData) + + if err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + logger.WithData(lager.Data{"state": lastOperation.State}).Info("done-check-for-operation") + + lastOperationResponse := LastOperationResponse{ + State: lastOperation.State, + Description: lastOperation.Description, + } + + h.respond(w, http.StatusOK, lastOperationResponse) +} + +func (h serviceBrokerHandler) respond(w http.ResponseWriter, status int, response interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + encoder := json.NewEncoder(w) + err := encoder.Encode(response) + if err != nil { + h.logger.Error("encoding response", err, lager.Data{"status": status, "response": response}) + } +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/api_suite_test.go b/vendor/github.com/pivotal-cf/brokerapi/api_suite_test.go new file mode 100644 index 00000000000..ecf111d8bea --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/api_suite_test.go @@ -0,0 +1,39 @@ +package brokerapi_test + +import ( + "fmt" + "io/ioutil" + "path" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pborman/uuid" +) + +func TestAPI(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "API Suite") +} + +func fixture(name string) string { + filePath := path.Join("fixtures", name) + contents, err := ioutil.ReadFile(filePath) + if err != nil { + panic(fmt.Sprintf("Could not read fixture: %s", name)) + } + + return string(contents) +} + +func uniqueID() string { + return uuid.NewRandom().String() +} + +func uniqueInstanceID() string { + return uniqueID() +} + +func uniqueBindingID() string { + return uniqueID() +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/api_test.go b/vendor/github.com/pivotal-cf/brokerapi/api_test.go new file mode 100644 index 00000000000..75558d42915 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/api_test.go @@ -0,0 +1,1541 @@ +package brokerapi_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/lagertest" + "github.com/drewolson/testflight" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pivotal-cf/brokerapi" + "github.com/pivotal-cf/brokerapi/fakes" +) + +var _ = Describe("Service Broker API", func() { + var fakeServiceBroker *fakes.FakeServiceBroker + var brokerAPI http.Handler + var brokerLogger *lagertest.TestLogger + var credentials = brokerapi.BrokerCredentials{ + Username: "username", + Password: "password", + } + + makeInstanceProvisioningRequest := func(instanceID string, details map[string]interface{}, queryString string) *testflight.Response { + response := &testflight.Response{} + + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := "/v2/service_instances/" + instanceID + queryString + + buffer := &bytes.Buffer{} + json.NewEncoder(buffer).Encode(details) + request, err := http.NewRequest("PUT", path, buffer) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth(credentials.Username, credentials.Password) + + response = r.Do(request) + }) + return response + } + + makeInstanceProvisioningRequestWithAcceptsIncomplete := func(instanceID string, details map[string]interface{}, acceptsIncomplete bool) *testflight.Response { + var acceptsIncompleteFlag string + + if acceptsIncomplete { + acceptsIncompleteFlag = "?accepts_incomplete=true" + } else { + acceptsIncompleteFlag = "?accepts_incomplete=false" + } + + return makeInstanceProvisioningRequest(instanceID, details, acceptsIncompleteFlag) + } + + lastLogLine := func() lager.LogFormat { + noOfLogLines := len(brokerLogger.Logs()) + if noOfLogLines == 0 { + // better way to raise error? + err := errors.New("expected some log lines but there were none") + Expect(err).NotTo(HaveOccurred()) + } + + return brokerLogger.Logs()[noOfLogLines-1] + } + + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + brokerLogger = lagertest.NewTestLogger("broker-api") + brokerAPI = brokerapi.New(fakeServiceBroker, brokerLogger, credentials) + }) + + Describe("response headers", func() { + makeRequest := func() *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + request.SetBasicAuth(credentials.Username, credentials.Password) + brokerAPI.ServeHTTP(recorder, request) + return recorder + } + + It("has a Content-Type header", func() { + response := makeRequest() + + header := response.Header().Get("Content-Type") + Ω(header).Should(Equal("application/json")) + }) + }) + + Describe("request context", func() { + var ( + ctx context.Context + ) + + makeRequest := func(method, path, body string) *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + request, _ := http.NewRequest(method, path, strings.NewReader(body)) + request.SetBasicAuth(credentials.Username, credentials.Password) + request = request.WithContext(ctx) + brokerAPI.ServeHTTP(recorder, request) + return recorder + } + + BeforeEach(func() { + ctx = context.WithValue(context.Background(), "test_context", true) + }) + + It("catalog endpoint passes the request context to the broker", func() { + makeRequest("GET", "/v2/catalog", "") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("provision endpoint passes the request context to the broker", func() { + makeRequest("PUT", "/v2/service_instances/instance-id", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("deprovision endpoint passes the request context to the broker", func() { + makeRequest("DELETE", "/v2/service_instances/instance-id", "") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("bind endpoint passes the request context to the broker", func() { + makeRequest("PUT", "/v2/service_instances/instance-id/service_bindings/binding-id", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("unbind endpoint passes the request context to the broker", func() { + makeRequest("DELETE", "/v2/service_instances/instance-id/service_bindings/binding-id", "") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("update endpoint passes the request context to the broker", func() { + makeRequest("PATCH", "/v2/service_instances/instance-id", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("last operation endpoint passes the request context to the broker", func() { + makeRequest("GET", "/v2/service_instances/instance-id/last_operation", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + }) + + Describe("authentication", func() { + makeRequestWithoutAuth := func() *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + response = r.Do(request) + }) + return response + } + + makeRequestWithAuth := func(username string, password string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + request.SetBasicAuth(username, password) + + response = r.Do(request) + }) + return response + } + + makeRequestWithUnrecognizedAuth := func() *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + // dXNlcm5hbWU6cGFzc3dvcmQ= is base64 encoding of 'username:password', + // ie, a correctly encoded basic authorization header + request.Header["Authorization"] = []string{"NOTBASIC dXNlcm5hbWU6cGFzc3dvcmQ="} + + response = r.Do(request) + }) + return response + } + + It("returns 401 when the authorization header has an incorrect password", func() { + response := makeRequestWithAuth("username", "fake_password") + Expect(response.StatusCode).To(Equal(401)) + }) + + It("returns 401 when the authorization header has an incorrect username", func() { + response := makeRequestWithAuth("fake_username", "password") + Expect(response.StatusCode).To(Equal(401)) + }) + + It("returns 401 when there is no authorization header", func() { + response := makeRequestWithoutAuth() + Expect(response.StatusCode).To(Equal(401)) + }) + + It("returns 401 when there is a unrecognized authorization header", func() { + response := makeRequestWithUnrecognizedAuth() + Expect(response.StatusCode).To(Equal(401)) + }) + + It("does not call through to the service broker when not authenticated", func() { + makeRequestWithAuth("username", "fake_password") + Ω(fakeServiceBroker.BrokerCalled).ShouldNot(BeTrue(), + "broker should not have been hit when authentication failed", + ) + }) + }) + + Describe("catalog endpoint", func() { + makeCatalogRequest := func() *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + It("returns a 200", func() { + response := makeCatalogRequest() + Expect(response.StatusCode).To(Equal(200)) + }) + + It("returns valid catalog json", func() { + response := makeCatalogRequest() + Expect(response.Body).To(MatchJSON(fixture("catalog.json"))) + }) + }) + + Describe("instance lifecycle endpoint", func() { + makeInstanceDeprovisioningRequest := func(instanceID, queryString string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s?plan_id=plan-id&service_id=service-id", instanceID) + if queryString != "" { + path = fmt.Sprintf("%s&%s", path, queryString) + } + request, err := http.NewRequest("DELETE", path, strings.NewReader("")) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth("username", "password") + + response = r.Do(request) + + }) + return response + } + + Describe("provisioning", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + provisionDetails = map[string]interface{}{ + "service_id": "service-id", + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + }) + + It("calls Provision on the service broker with all params", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(fakeServiceBroker.ProvisionDetails).To(Equal(brokerapi.ProvisionDetails{ + ServiceID: "service-id", + PlanID: "plan-id", + OrganizationGUID: "organization-guid", + SpaceGUID: "space-guid", + })) + }) + + It("calls Provision on the service broker with the instance id", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(fakeServiceBroker.ProvisionedInstanceIDs).To(ContainElement(instanceID)) + }) + + Context("when the broker returns some operation data", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + OperationDataToReturn: "some-operation-data", + } + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + ShouldProvisionAsync: true, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns the operation data to the cloud controller", func() { + resp := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(resp.Body).To(MatchJSON(fixture("operation_data_response.json"))) + }) + }) + + Context("when there are arbitrary params", func() { + var rawParams string + + BeforeEach(func() { + provisionDetails["parameters"] = map[string]interface{}{ + "string": "some-string", + "number": 1, + "object": struct{ Name string }{"some-name"}, + "array": []interface{}{"a", "b", "c"}, + } + rawParams = `{ + "string":"some-string", + "number":1, + "object": { "Name": "some-name" }, + "array": [ "a", "b", "c" ] + }` + }) + + It("calls Provision on the service broker with all params", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(string(fakeServiceBroker.ProvisionDetails.RawParameters)).To(MatchJSON(rawParams)) + }) + + It("calls Provision with details with raw parameters", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + detailsWithRawParameters := brokerapi.DetailsWithRawParameters(fakeServiceBroker.ProvisionDetails) + rawParameters := detailsWithRawParameters.GetRawParameters() + Expect(string(rawParameters)).To(MatchJSON(rawParams)) + }) + }) + + Context("when the instance does not exist", func() { + It("returns a 201", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(201)) + }) + + It("returns empty json", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(fixture("provisioning.json"))) + }) + + Context("when the broker returns a dashboard URL", func() { + BeforeEach(func() { + fakeServiceBroker.DashboardURL = "some-dashboard-url" + }) + + It("returns json with dasboard URL", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(fixture("provisioning_with_dashboard.json"))) + }) + }) + + Context("when the instance limit has been reached", func() { + BeforeEach(func() { + for i := 0; i < fakeServiceBroker.InstanceLimit; i++ { + makeInstanceProvisioningRequest(uniqueInstanceID(), provisionDetails, "") + } + }) + + It("returns a 500", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(fixture("instance_limit_error.json"))) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + + Expect(lastLogLine().Message).To(ContainSubstring(".provision.instance-limit-reached")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance limit for this service has been reached")) + }) + }) + + Context("when an unexpected error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.ProvisionError = errors.New("broker failed") + }) + + It("returns a 500", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{"description":"broker failed"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("broker failed")) + }) + }) + + Context("when a custom error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.ProvisionError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + + Context("RawParameters are not valid JSON", func() { + BeforeEach(func() { + fakeServiceBroker.ProvisionError = brokerapi.ErrRawParamsInvalid + }) + + It("returns a 422", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{"description":"The format of the parameters is not valid JSON"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.invalid-raw-params")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("The format of the parameters is not valid JSON")) + }) + }) + + Context("when we send invalid json", func() { + makeBadInstanceProvisioningRequest := func(instanceID string) *testflight.Response { + response := &testflight.Response{} + + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := "/v2/service_instances/" + instanceID + + body := strings.NewReader("{{{{{") + request, err := http.NewRequest("PUT", path, body) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth(credentials.Username, credentials.Password) + + response = r.Do(request) + }) + + return response + } + + It("returns a 422 bad request", func() { + response := makeBadInstanceProvisioningRequest(instanceID) + Expect(response.StatusCode).Should(Equal(http.StatusUnprocessableEntity)) + }) + + It("logs a message", func() { + makeBadInstanceProvisioningRequest(instanceID) + Expect(lastLogLine().Message).To(ContainSubstring(".provision.invalid-service-details")) + }) + }) + }) + + Context("when the instance already exists", func() { + BeforeEach(func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + It("returns a 409", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(409)) + }) + + It("returns an empty JSON object", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.instance-already-exists")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance already exists")) + }) + }) + + Describe("accepts_incomplete", func() { + Context("when the accepts_incomplete flag is true", func() { + It("calls ProvisionAsync on the service broker", func() { + acceptsIncomplete := true + makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, acceptsIncomplete) + Expect(fakeServiceBroker.ProvisionDetails).To(Equal(brokerapi.ProvisionDetails{ + ServiceID: "service-id", + PlanID: "plan-id", + OrganizationGUID: "organization-guid", + SpaceGUID: "space-guid", + })) + + Expect(fakeServiceBroker.ProvisionedInstanceIDs).To(ContainElement(instanceID)) + }) + + Context("when the broker chooses to provision asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + ShouldProvisionAsync: true, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 202", func() { + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, true) + Expect(response.StatusCode).To(Equal(http.StatusAccepted)) + }) + }) + + Context("when the broker chooses to provision synchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + ShouldProvisionAsync: false, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 201", func() { + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, true) + Expect(response.StatusCode).To(Equal(http.StatusCreated)) + }) + }) + }) + + Context("when the accepts_incomplete flag is false", func() { + It("returns a 201", func() { + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, false) + Expect(response.StatusCode).To(Equal(http.StatusCreated)) + }) + + Context("when broker can only respond asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 422", func() { + acceptsIncomplete := false + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, acceptsIncomplete) + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + Expect(response.Body).To(MatchJSON(fixture("async_required.json"))) + }) + }) + }) + + Context("when the accepts_incomplete flag is missing", func() { + It("returns a 201", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(http.StatusCreated)) + }) + + Context("when broker can only respond asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 422", func() { + acceptsIncomplete := false + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, acceptsIncomplete) + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + Expect(response.Body).To(MatchJSON(fixture("async_required.json"))) + }) + }) + }) + }) + }) + + Describe("updating", func() { + var ( + instanceID string + details map[string]interface{} + queryString string + + response *testflight.Response + ) + + makeInstanceUpdateRequest := func(instanceID string, details map[string]interface{}, queryString string) *testflight.Response { + response := &testflight.Response{} + + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := "/v2/service_instances/" + instanceID + queryString + + buffer := &bytes.Buffer{} + json.NewEncoder(buffer).Encode(details) + request, err := http.NewRequest("PATCH", path, buffer) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth(credentials.Username, credentials.Password) + + response = r.Do(request) + }) + return response + } + + BeforeEach(func() { + instanceID = uniqueInstanceID() + details = map[string]interface{}{ + "service_id": "some-service-id", + "plan_id": "new-plan", + "parameters": map[string]interface{}{ + "new-param": "new-param-value", + }, + "previous_values": map[string]interface{}{ + "service_id": "service-id", + "plan_id": "old-plan", + "organization_id": "org-id", + "space_id": "space-id", + }, + } + }) + + JustBeforeEach(func() { + response = makeInstanceUpdateRequest(instanceID, details, queryString) + }) + + Context("when the broker returns no error", func() { + Context("when the broker responds synchronously", func() { + It("returns HTTP 200", func() { + Expect(response.StatusCode).To(Equal(http.StatusOK)) + }) + + It("returns JSON content type", func() { + Expect(response.RawResponse.Header.Get("Content-Type")).To(Equal("application/json")) + }) + + It("returns empty JSON body", func() { + Expect(response.Body).To(Equal("{}\n")) + }) + + It("calls broker with instanceID and update details", func() { + Expect(fakeServiceBroker.UpdatedInstanceIDs).To(ConsistOf(instanceID)) + Expect(fakeServiceBroker.UpdateDetails.ServiceID).To(Equal("some-service-id")) + Expect(fakeServiceBroker.UpdateDetails.PlanID).To(Equal("new-plan")) + Expect(fakeServiceBroker.UpdateDetails.PreviousValues).To(Equal(brokerapi.PreviousValues{ + PlanID: "old-plan", + ServiceID: "service-id", + OrgID: "org-id", + SpaceID: "space-id", + }, + )) + Expect(fakeServiceBroker.UpdateDetails.RawParameters).To(Equal(json.RawMessage(`{"new-param":"new-param-value"}`))) + }) + + It("calls update with details with raw parameters", func() { + detailsWithRawParameters := brokerapi.DetailsWithRawParameters(fakeServiceBroker.UpdateDetails) + rawParameters := detailsWithRawParameters.GetRawParameters() + Expect(rawParameters).To(Equal(json.RawMessage(`{"new-param":"new-param-value"}`))) + }) + + Context("when accepts_incomplete=true", func() { + BeforeEach(func() { + queryString = "?accepts_incomplete=true" + }) + + It("tells broker async is allowed", func() { + Expect(fakeServiceBroker.AsyncAllowed).To(BeTrue()) + }) + }) + + Context("when accepts_incomplete is not supplied", func() { + BeforeEach(func() { + queryString = "" + }) + + It("tells broker async not allowed", func() { + Expect(fakeServiceBroker.AsyncAllowed).To(BeFalse()) + }) + }) + }) + + Context("when the broker responds asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker.ShouldReturnAsync = true + }) + + It("returns HTTP 202", func() { + Expect(response.StatusCode).To(Equal(http.StatusAccepted)) + }) + + Context("when the broker responds with operation data", func() { + BeforeEach(func() { + fakeServiceBroker.OperationDataToReturn = "some-operation-data" + }) + + It("returns the operation data to the cloud controller", func() { + Expect(response.Body).To(MatchJSON(fixture("operation_data_response.json"))) + }) + }) + }) + }) + + Context("when the broker indicates that it needs async support", func() { + BeforeEach(func() { + fakeServiceBroker.UpdateError = brokerapi.ErrAsyncRequired + }) + + It("returns HTTP 422", func() { + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + + It("returns a descriptive message", func() { + var body map[string]string + err := json.Unmarshal([]byte(response.Body), &body) + Expect(err).ToNot(HaveOccurred()) + Expect(body["error"]).To(Equal("AsyncRequired")) + Expect(body["description"]).To(Equal("This service plan requires client support for asynchronous service operations.")) + }) + }) + + Context("when the broker indicates that the plan cannot be upgraded", func() { + BeforeEach(func() { + fakeServiceBroker.UpdateError = brokerapi.ErrPlanChangeNotSupported + }) + + It("returns HTTP 422", func() { + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + + It("returns a descriptive message", func() { + var body map[string]string + err := json.Unmarshal([]byte(response.Body), &body) + Expect(err).ToNot(HaveOccurred()) + Expect(body["error"]).To(Equal("PlanChangeNotSupported")) + Expect(body["description"]).To(Equal("The requested plan migration cannot be performed")) + }) + }) + + Context("when the broker errors in an unknown way", func() { + BeforeEach(func() { + fakeServiceBroker.UpdateError = errors.New("some horrible internal error") + }) + + It("returns HTTP 500", func() { + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns a descriptive message", func() { + var body map[string]string + err := json.Unmarshal([]byte(response.Body), &body) + Expect(err).ToNot(HaveOccurred()) + Expect(body["description"]).To(Equal("some horrible internal error")) + }) + }) + }) + + Describe("deprovisioning", func() { + It("calls Deprovision on the service broker with the instance id", func() { + instanceID := uniqueInstanceID() + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(fakeServiceBroker.DeprovisionedInstanceIDs).To(ContainElement(instanceID)) + }) + + Context("when the instance exists", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + + provisionDetails = map[string]interface{}{ + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + itReturnsStatus := func(expectedStatus int, queryString string) { + It(fmt.Sprintf("returns HTTP %d", expectedStatus), func() { + response := makeInstanceDeprovisioningRequest(instanceID, queryString) + Expect(response.StatusCode).To(Equal(expectedStatus)) + }) + } + + itReturnsEmptyJsonObject := func(queryString string) { + It("returns an empty JSON object", func() { + response := makeInstanceDeprovisioningRequest(instanceID, queryString) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + } + + Context("when the broker can only operate synchronously", func() { + Context("when the accepts_incomplete flag is not set", func() { + itReturnsStatus(200, "") + itReturnsEmptyJsonObject("") + }) + + Context("when the accepts_incomplete flag is set to true", func() { + itReturnsStatus(200, "accepts_incomplete=true") + itReturnsEmptyJsonObject("accepts_incomplete=true") + }) + }) + + Context("when the broker can only operate asynchronously", func() { + BeforeEach(func() { + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + Context("when the accepts_incomplete flag is not set", func() { + itReturnsStatus(http.StatusUnprocessableEntity, "") + + It("returns a descriptive error", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.Body).To(MatchJSON(fixture("async_required.json"))) + }) + }) + + Context("when the accepts_incomplete flag is set to true", func() { + itReturnsStatus(202, "accepts_incomplete=true") + itReturnsEmptyJsonObject("accepts_incomplete=true") + }) + + Context("when the broker returns operation data", func() { + BeforeEach(func() { + fakeServiceBroker.OperationDataToReturn = "some-operation-data" + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + itReturnsStatus(202, "accepts_incomplete=true") + + It("returns the operation data to the cloud controller", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "accepts_incomplete=true") + Expect(response.Body).To(MatchJSON(fixture("operation_data_response.json"))) + }) + }) + }) + + Context("when the broker can operate both synchronously and asynchronously", func() { + BeforeEach(func() { + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + Context("when the accepts_incomplete flag is not set", func() { + itReturnsStatus(200, "") + itReturnsEmptyJsonObject("") + }) + + Context("when the accepts_incomplete flag is set to true", func() { + itReturnsStatus(202, "accepts_incomplete=true") + itReturnsEmptyJsonObject("accepts_incomplete=true") + }) + }) + + It("contains plan_id", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(fakeServiceBroker.DeprovisionDetails.PlanID).To(Equal("plan-id")) + }) + + It("contains service_id", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(fakeServiceBroker.DeprovisionDetails.ServiceID).To(Equal("service-id")) + }) + }) + + Context("when the instance does not exist", func() { + var instanceID string + + It("returns a 410", func() { + response := makeInstanceDeprovisioningRequest(uniqueInstanceID(), "") + Expect(response.StatusCode).To(Equal(410)) + }) + + It("returns an empty JSON object", func() { + response := makeInstanceDeprovisioningRequest(uniqueInstanceID(), "") + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(lastLogLine().Message).To(ContainSubstring(".deprovision.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + }) + }) + + Context("when instance deprovisioning fails", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + provisionDetails = map[string]interface{}{ + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + Context("when an unexpected error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.DeprovisionError = errors.New("broker failed") + }) + + It("returns a 500", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.Body).To(MatchJSON(`{"description":"broker failed"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(lastLogLine().Message).To(ContainSubstring(".deprovision.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("broker failed")) + }) + }) + + Context("when a custom error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.DeprovisionError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(lastLogLine().Message).To(ContainSubstring(".deprovision.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + }) + }) + + Describe("binding lifecycle endpoint", func() { + makeBindingRequestWithSpecificAPIVersion := func(instanceID, bindingID string, details map[string]interface{}, apiVersion string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s/service_bindings/%s", + instanceID, bindingID) + + buffer := &bytes.Buffer{} + + if details != nil { + json.NewEncoder(buffer).Encode(details) + } + + request, err := http.NewRequest("PUT", path, buffer) + + Expect(err).NotTo(HaveOccurred()) + + request.Header.Add("Content-Type", "application/json") + request.Header.Add("X-Broker-Api-Version", apiVersion) + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + makeBindingRequest := func(instanceID, bindingID string, details map[string]interface{}) *testflight.Response { + return makeBindingRequestWithSpecificAPIVersion(instanceID, bindingID, details, "2.10") + } + + Describe("binding", func() { + var ( + instanceID string + bindingID string + details map[string]interface{} + ) + + BeforeEach(func() { + instanceID = uniqueInstanceID() + bindingID = uniqueBindingID() + details = map[string]interface{}{ + "app_guid": "app_guid", + "plan_id": "plan_id", + "service_id": "service_id", + "parameters": map[string]interface{}{ + "new-param": "new-param-value", + }, + } + }) + + Context("when the associated instance exists", func() { + It("calls Bind on the service broker with the instance and binding ids", func() { + makeBindingRequest(instanceID, bindingID, details) + Expect(fakeServiceBroker.BoundInstanceIDs).To(ContainElement(instanceID)) + Expect(fakeServiceBroker.BoundBindingIDs).To(ContainElement(bindingID)) + Expect(fakeServiceBroker.BoundBindingDetails).To(Equal(brokerapi.BindDetails{ + AppGUID: "app_guid", + PlanID: "plan_id", + ServiceID: "service_id", + RawParameters: json.RawMessage(`{"new-param":"new-param-value"}`), + })) + }) + + It("calls bind with details with raw parameters", func() { + makeBindingRequest(instanceID, bindingID, details) + detailsWithRawParameters := brokerapi.DetailsWithRawParameters(fakeServiceBroker.BoundBindingDetails) + rawParameters := detailsWithRawParameters.GetRawParameters() + Expect(rawParameters).To(Equal(json.RawMessage(`{"new-param":"new-param-value"}`))) + }) + + It("returns the credentials returned by Bind", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding.json"))) + }) + + It("returns a 201", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(201)) + }) + + Context("when syslog_drain_url is being passed", func() { + BeforeEach(func() { + fakeServiceBroker.SyslogDrainURL = "some-drain-url" + }) + + It("responds with the syslog drain url", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding_with_syslog.json"))) + }) + }) + + Context("when route_service_url is being passed", func() { + BeforeEach(func() { + fakeServiceBroker.RouteServiceURL = "some-route-url" + }) + + It("responds with the route service url", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding_with_route_service.json"))) + }) + }) + + Context("when a volume mount is being passed", func() { + BeforeEach(func() { + fakeServiceBroker.VolumeMounts = []brokerapi.VolumeMount{{ + Driver: "driver", + ContainerDir: "/dev/null", + Mode: "rw", + DeviceType: "shared", + Device: brokerapi.SharedDevice{ + VolumeId: "some-guid", + MountConfig: map[string]interface{}{"key": "value"}, + }, + }} + }) + + Context("when the broker API version is greater than 2.9", func() { + It("responds with a volume mount", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding_with_volume_mounts.json"))) + }) + }) + + Context("when the broker API version is 2.9", func() { + It("responds with an experimental volume mount", func() { + response := makeBindingRequestWithSpecificAPIVersion(uniqueInstanceID(), uniqueBindingID(), details, "2.9") + Expect(response.Body).To(MatchJSON(fixture("binding_with_experimental_volume_mounts.json"))) + }) + }) + + Context("when the broker API version is 2.8", func() { + It("responds with an experimental volume mount", func() { + response := makeBindingRequestWithSpecificAPIVersion(uniqueInstanceID(), uniqueBindingID(), details, "2.8") + Expect(response.Body).To(MatchJSON(fixture("binding_with_experimental_volume_mounts.json"))) + }) + }) + }) + + Context("when no bind details are being passed", func() { + It("returns a 422", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), nil) + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + }) + + Context("when there are arbitrary params", func() { + BeforeEach(func() { + details["parameters"] = map[string]interface{}{ + "string": "some-string", + "number": 1, + "object": struct{ Name string }{"some-name"}, + "array": []interface{}{"a", "b", "c"}, + } + }) + + It("calls Bind on the service broker with all params", func() { + rawParams := `{ + "string":"some-string", + "number":1, + "object": { "Name": "some-name" }, + "array": [ "a", "b", "c" ] + }` + makeBindingRequest(instanceID, bindingID, details) + Expect(string(fakeServiceBroker.BoundBindingDetails.RawParameters)).To(MatchJSON(rawParams)) + }) + }) + + Context("when there is a app_guid in the bind_resource", func() { + BeforeEach(func() { + details["bind_resource"] = map[string]interface{}{"app_guid": "a-guid"} + }) + + It("calls Bind on the service broker with the bind_resource", func() { + makeBindingRequest(instanceID, bindingID, details) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource).NotTo(BeNil()) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.AppGuid).To(Equal("a-guid")) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.Route).To(BeEmpty()) + }) + }) + + Context("when there is a route in the bind_resource", func() { + BeforeEach(func() { + details["bind_resource"] = map[string]interface{}{"route": "route.cf-apps.com"} + }) + + It("calls Bind on the service broker with the bind_resource", func() { + makeBindingRequest(instanceID, bindingID, details) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource).NotTo(BeNil()) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.Route).To(Equal("route.cf-apps.com")) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.AppGuid).To(BeEmpty()) + }) + }) + }) + + Context("when the associated instance does not exist", func() { + var instanceID string + + BeforeEach(func() { + fakeServiceBroker.BindError = brokerapi.ErrInstanceDoesNotExist + }) + + It("returns a 404", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(404)) + }) + + It("returns an error JSON object", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(`{"description":"instance does not exist"}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeBindingRequest(instanceID, uniqueBindingID(), details) + Expect(lastLogLine().Message).To(ContainSubstring(".bind.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + }) + }) + + Context("when the requested binding already exists", func() { + var instanceID string + + BeforeEach(func() { + fakeServiceBroker.BindError = brokerapi.ErrBindingAlreadyExists + }) + + It("returns a 409", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(409)) + }) + + It("returns an error JSON object", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(`{"description":"binding already exists"}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeBindingRequest(instanceID, uniqueBindingID(), details) + makeBindingRequest(instanceID, uniqueBindingID(), details) + + Expect(lastLogLine().Message).To(ContainSubstring(".bind.binding-already-exists")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("binding already exists")) + }) + }) + + Context("when the binding returns an unknown error", func() { + BeforeEach(func() { + fakeServiceBroker.BindError = errors.New("unknown error") + }) + + It("returns a generic 500 error response", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(500)) + Expect(response.Body).To(MatchJSON(`{"description":"unknown error"}`)) + }) + + It("logs a detailed error message", func() { + makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + + Expect(lastLogLine().Message).To(ContainSubstring(".bind.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("unknown error")) + }) + }) + + Context("when the binding returns a custom error", func() { + BeforeEach(func() { + fakeServiceBroker.BindError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(lastLogLine().Message).To(ContainSubstring(".bind.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + + Describe("unbinding", func() { + makeUnbindingRequest := func(instanceID string, bindingID string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s/service_bindings/%s?plan_id=plan-id&service_id=service-id", + instanceID, bindingID) + request, _ := http.NewRequest("DELETE", path, strings.NewReader("")) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + Context("when the associated instance exists", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + provisionDetails = map[string]interface{}{ + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + Context("and the binding exists", func() { + var bindingID string + + BeforeEach(func() { + bindingID = uniqueBindingID() + makeBindingRequest(instanceID, bindingID, map[string]interface{}{}) + }) + + It("returns a 200", func() { + response := makeUnbindingRequest(instanceID, bindingID) + Expect(response.StatusCode).To(Equal(200)) + }) + + It("returns an empty JSON object", func() { + response := makeUnbindingRequest(instanceID, bindingID) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("contains plan_id", func() { + makeUnbindingRequest(instanceID, bindingID) + Expect(fakeServiceBroker.UnbindingDetails.PlanID).To(Equal("plan-id")) + }) + + It("contains service_id", func() { + makeUnbindingRequest(instanceID, bindingID) + Expect(fakeServiceBroker.UnbindingDetails.ServiceID).To(Equal("service-id")) + }) + }) + + Context("but the binding does not exist", func() { + It("returns a 410", func() { + response := makeUnbindingRequest(instanceID, "does-not-exist") + Expect(response.StatusCode).To(Equal(410)) + }) + + It("logs an appropriate error message", func() { + makeUnbindingRequest(instanceID, "does-not-exist") + + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.binding-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("binding does not exist")) + }) + + It("returns an empty JSON object", func() { + response := makeUnbindingRequest(instanceID, "does-not-exist") + Expect(response.Body).To(MatchJSON(`{}`)) + }) + }) + }) + + Context("when the associated instance does not exist", func() { + var instanceID string + + It("returns a 410", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.StatusCode).To(Equal(http.StatusGone)) + }) + + It("returns an empty JSON object", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeUnbindingRequest(instanceID, uniqueBindingID()) + + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + }) + }) + + Context("when unbinding returns an unknown error", func() { + BeforeEach(func() { + fakeServiceBroker.UnbindError = errors.New("unknown error") + }) + + It("returns a generic 500 error response", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.StatusCode).To(Equal(500)) + Expect(response.Body).To(MatchJSON(`{"description":"unknown error"}`)) + }) + + It("logs a detailed error message", func() { + makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("unknown error")) + }) + }) + + Context("when unbinding returns a custom error", func() { + BeforeEach(func() { + fakeServiceBroker.UnbindError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + + Describe("last_operation", func() { + makeLastOperationRequest := func(instanceID, operationData string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s/last_operation", instanceID) + if operationData != "" { + path = fmt.Sprintf("%s?operation=%s", path, url.QueryEscape(operationData)) + } + + request, _ := http.NewRequest("GET", path, strings.NewReader("")) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + It("calls the broker with the relevant instance ID", func() { + instanceID := "instanceID" + makeLastOperationRequest(instanceID, "") + Expect(fakeServiceBroker.LastOperationInstanceID).To(Equal(instanceID)) + }) + + It("calls the broker with the URL decoded operation data", func() { + instanceID := "an-instance" + operationData := `{"foo":"bar"}` + makeLastOperationRequest(instanceID, operationData) + Expect(fakeServiceBroker.LastOperationData).To(Equal(operationData)) + }) + + It("should return succeeded if the operation completed successfully", func() { + fakeServiceBroker.LastOperationState = "succeeded" + fakeServiceBroker.LastOperationDescription = "some description" + + instanceID := "instanceID" + response := makeLastOperationRequest(instanceID, "") + + logs := brokerLogger.Logs() + + Expect(logs[0].Message).To(ContainSubstring(".lastOperation.starting-check-for-operation")) + Expect(logs[0].Data["instance-id"]).To(ContainSubstring(instanceID)) + + Expect(logs[1].Message).To(ContainSubstring(".lastOperation.done-check-for-operation")) + Expect(logs[1].Data["instance-id"]).To(ContainSubstring(instanceID)) + Expect(logs[1].Data["state"]).To(ContainSubstring(string(fakeServiceBroker.LastOperationState))) + + Expect(response.StatusCode).To(Equal(200)) + Expect(response.Body).To(MatchJSON(fixture("last_operation_succeeded.json"))) + }) + + It("should return a 410 and log in case the instance id is not found", func() { + fakeServiceBroker.LastOperationError = brokerapi.ErrInstanceDoesNotExist + instanceID := "non-existing" + response := makeLastOperationRequest(instanceID, "") + + Expect(lastLogLine().Message).To(ContainSubstring(".lastOperation.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + + Expect(response.StatusCode).To(Equal(410)) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + Context("when last_operation returns an unknown error", func() { + BeforeEach(func() { + fakeServiceBroker.LastOperationError = errors.New("unknown error") + }) + + It("returns a generic 500 error response", func() { + response := makeLastOperationRequest("instanceID", "") + + Expect(response.StatusCode).To(Equal(500)) + Expect(response.Body).To(MatchJSON(`{"description": "unknown error"}`)) + }) + + It("logs a detailed error message", func() { + makeLastOperationRequest("instanceID", "") + + Expect(lastLogLine().Message).To(ContainSubstring(".lastOperation.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("unknown error")) + }) + }) + + Context("when last_operation returns a custom error", func() { + BeforeEach(func() { + fakeServiceBroker.LastOperationError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeLastOperationRequest("instanceID", "") + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeLastOperationRequest("instanceID", "") + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeLastOperationRequest("instanceID", "") + Expect(lastLogLine().Message).To(ContainSubstring(".lastOperation.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + }) +}) diff --git a/vendor/github.com/pivotal-cf/brokerapi/auth/auth.go b/vendor/github.com/pivotal-cf/brokerapi/auth/auth.go new file mode 100644 index 00000000000..f9225e36571 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/auth/auth.go @@ -0,0 +1,44 @@ +package auth + +import "net/http" + +type Wrapper struct { + username string + password string +} + +func NewWrapper(username, password string) *Wrapper { + return &Wrapper{ + username: username, + password: password, + } +} + +const notAuthorized = "Not Authorized" + +func (wrapper *Wrapper) Wrap(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !authorized(wrapper, r) { + http.Error(w, notAuthorized, http.StatusUnauthorized) + return + } + + handler.ServeHTTP(w, r) + }) +} + +func (wrapper *Wrapper) WrapFunc(handlerFunc http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !authorized(wrapper, r) { + http.Error(w, notAuthorized, http.StatusUnauthorized) + return + } + + handlerFunc(w, r) + }) +} + +func authorized(wrapper *Wrapper, r *http.Request) bool { + username, password, isOk := r.BasicAuth() + return isOk && username == wrapper.username && password == wrapper.password +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/auth/auth_suite_test.go b/vendor/github.com/pivotal-cf/brokerapi/auth/auth_suite_test.go new file mode 100644 index 00000000000..e2d2a57d989 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/auth/auth_suite_test.go @@ -0,0 +1,13 @@ +package auth_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestAuth(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Auth Suite") +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/auth/auth_test.go b/vendor/github.com/pivotal-cf/brokerapi/auth/auth_test.go new file mode 100644 index 00000000000..da196c819b5 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/auth/auth_test.go @@ -0,0 +1,102 @@ +package auth_test + +import ( + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/pivotal-cf/brokerapi/auth" +) + +var _ = Describe("Auth Wrapper", func() { + var ( + username string + password string + httpRecorder *httptest.ResponseRecorder + ) + + newRequest := func(username, password string) *http.Request { + request, err := http.NewRequest("GET", "", nil) + Expect(err).NotTo(HaveOccurred()) + request.SetBasicAuth(username, password) + return request + } + + BeforeEach(func() { + username = "username" + password = "password" + httpRecorder = httptest.NewRecorder() + }) + + Describe("wrapped handler", func() { + var wrappedHandler http.Handler + + BeforeEach(func() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + }) + wrappedHandler = auth.NewWrapper(username, password).Wrap(handler) + }) + + It("works when the credentials are correct", func() { + request := newRequest(username, password) + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusCreated)) + }) + + It("fails when the username is empty", func() { + request := newRequest("", password) + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the password is empty", func() { + request := newRequest(username, "") + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the credentials are wrong", func() { + request := newRequest("thats", "apar") + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + }) + + Describe("wrapped handlerFunc", func() { + var wrappedHandlerFunc http.HandlerFunc + + BeforeEach(func() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + }) + wrappedHandlerFunc = auth.NewWrapper(username, password).WrapFunc(handler) + }) + + It("works when the credentials are correct", func() { + request := newRequest(username, password) + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusCreated)) + }) + + It("fails when the username is empty", func() { + request := newRequest("", password) + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the password is empty", func() { + request := newRequest(username, "") + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the credentials are wrong", func() { + request := newRequest("thats", "apar") + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + }) +}) diff --git a/vendor/github.com/pivotal-cf/brokerapi/catalog.go b/vendor/github.com/pivotal-cf/brokerapi/catalog.go new file mode 100644 index 00000000000..4ff6b7257ac --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/catalog.go @@ -0,0 +1,65 @@ +package brokerapi + +type Service struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Bindable bool `json:"bindable"` + Tags []string `json:"tags,omitempty"` + PlanUpdatable bool `json:"plan_updateable"` + Plans []ServicePlan `json:"plans"` + Requires []RequiredPermission `json:"requires,omitempty"` + Metadata *ServiceMetadata `json:"metadata,omitempty"` + DashboardClient *ServiceDashboardClient `json:"dashboard_client,omitempty"` +} + +type ServiceDashboardClient struct { + ID string `json:"id"` + Secret string `json:"secret"` + RedirectURI string `json:"redirect_uri"` +} + +type ServicePlan struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Free *bool `json:"free,omitempty"` + Bindable *bool `json:"bindable,omitempty"` + Metadata *ServicePlanMetadata `json:"metadata,omitempty"` +} + +type ServicePlanMetadata struct { + DisplayName string `json:"displayName,omitempty"` + Bullets []string `json:"bullets,omitempty"` + Costs []ServicePlanCost `json:"costs,omitempty"` +} + +type ServicePlanCost struct { + Amount map[string]float64 `json:"amount"` + Unit string `json:"unit"` +} + +type ServiceMetadata struct { + DisplayName string `json:"displayName,omitempty"` + ImageUrl string `json:"imageUrl,omitempty"` + LongDescription string `json:"longDescription,omitempty"` + ProviderDisplayName string `json:"providerDisplayName,omitempty"` + DocumentationUrl string `json:"documentationUrl,omitempty"` + SupportUrl string `json:"supportUrl,omitempty"` +} + +func FreeValue(v bool) *bool { + return &v +} + +func BindableValue(v bool) *bool { + return &v +} + +type RequiredPermission string + +const ( + PermissionRouteForwarding = RequiredPermission("route_forwarding") + PermissionSyslogDrain = RequiredPermission("syslog_drain") + PermissionVolumeMount = RequiredPermission("volume_mount") +) diff --git a/vendor/github.com/pivotal-cf/brokerapi/catalog_test.go b/vendor/github.com/pivotal-cf/brokerapi/catalog_test.go new file mode 100644 index 00000000000..cde1bda2cc6 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/catalog_test.go @@ -0,0 +1,163 @@ +package brokerapi_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pivotal-cf/brokerapi" +) + +var _ = Describe("Catalog", func() { + Describe("Service", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + service := brokerapi.Service{ + ID: "ID-1", + Name: "Cassandra", + Description: "A Cassandra Plan", + Bindable: true, + Plans: []brokerapi.ServicePlan{}, + Metadata: &brokerapi.ServiceMetadata{}, + Tags: []string{"test"}, + PlanUpdatable: true, + DashboardClient: &brokerapi.ServiceDashboardClient{ + ID: "Dashboard ID", + Secret: "dashboardsecret", + RedirectURI: "the.dashboa.rd", + }, + } + jsonString := `{ + "id":"ID-1", + "name":"Cassandra", + "description":"A Cassandra Plan", + "bindable":true, + "plan_updateable":true, + "tags":["test"], + "plans":[], + "dashboard_client":{ + "id":"Dashboard ID", + "secret":"dashboardsecret", + "redirect_uri":"the.dashboa.rd" + }, + "metadata":{ + + } + }` + Expect(json.Marshal(service)).To(MatchJSON(jsonString)) + }) + }) + + It("encodes the optional 'requires' fields", func() { + service := brokerapi.Service{ + ID: "ID-1", + Name: "Cassandra", + Description: "A Cassandra Plan", + Bindable: true, + Plans: []brokerapi.ServicePlan{}, + Metadata: &brokerapi.ServiceMetadata{}, + Tags: []string{"test"}, + PlanUpdatable: true, + Requires: []brokerapi.RequiredPermission{ + brokerapi.PermissionRouteForwarding, + brokerapi.PermissionSyslogDrain, + brokerapi.PermissionVolumeMount, + }, + DashboardClient: &brokerapi.ServiceDashboardClient{ + ID: "Dashboard ID", + Secret: "dashboardsecret", + RedirectURI: "the.dashboa.rd", + }, + } + jsonString := `{ + "id":"ID-1", + "name":"Cassandra", + "description":"A Cassandra Plan", + "bindable":true, + "plan_updateable":true, + "tags":["test"], + "plans":[], + "requires": ["route_forwarding", "syslog_drain", "volume_mount"], + "dashboard_client":{ + "id":"Dashboard ID", + "secret":"dashboardsecret", + "redirect_uri":"the.dashboa.rd" + }, + "metadata":{ + + } + }` + Expect(json.Marshal(service)).To(MatchJSON(jsonString)) + }) + }) + + Describe("ServicePlan", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + plan := brokerapi.ServicePlan{ + ID: "ID-1", + Name: "Cassandra", + Description: "A Cassandra Plan", + Bindable: brokerapi.BindableValue(true), + Free: brokerapi.FreeValue(true), + Metadata: &brokerapi.ServicePlanMetadata{ + Bullets: []string{"hello", "its me"}, + DisplayName: "name", + }, + } + jsonString := `{ + "id":"ID-1", + "name":"Cassandra", + "description":"A Cassandra Plan", + "free": true, + "bindable": true, + "metadata":{ + "bullets":["hello", "its me"], + "displayName":"name" + } + }` + + Expect(json.Marshal(plan)).To(MatchJSON(jsonString)) + }) + }) + }) + + Describe("ServicePlanMetadata", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + metadata := brokerapi.ServicePlanMetadata{ + Bullets: []string{"test"}, + DisplayName: "Some display name", + } + jsonString := `{"bullets":["test"],"displayName":"Some display name"}` + + Expect(json.Marshal(metadata)).To(MatchJSON(jsonString)) + }) + }) + }) + + Describe("ServiceMetadata", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + metadata := brokerapi.ServiceMetadata{ + DisplayName: "Cassandra", + LongDescription: "A long description of Cassandra", + DocumentationUrl: "doc", + SupportUrl: "support", + ImageUrl: "image", + ProviderDisplayName: "display", + } + jsonString := `{ + "displayName":"Cassandra", + "longDescription":"A long description of Cassandra", + "documentationUrl":"doc", + "supportUrl":"support", + "imageUrl":"image", + "providerDisplayName":"display" + }` + + Expect(json.Marshal(metadata)).To(MatchJSON(jsonString)) + }) + }) + }) +}) diff --git a/vendor/github.com/pivotal-cf/brokerapi/failure_response.go b/vendor/github.com/pivotal-cf/brokerapi/failure_response.go new file mode 100644 index 00000000000..2fc7501b70e --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/failure_response.go @@ -0,0 +1,113 @@ +// Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. +// This program and the accompanying materials are made available under the terms of the under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +package brokerapi + +import ( + "net/http" + + "fmt" + + "code.cloudfoundry.org/lager" +) + +// FailureResponse can be returned from any of the `ServiceBroker` interface methods +// which allow an error to be returned. Doing so will provide greater control over +// the HTTP response. +type FailureResponse struct { + error + statusCode int + loggerAction string + emptyResponse bool + errorKey string +} + +// NewFailureResponse returns a pointer to a new instance of FailureResponse. +// err will by default be used as both a logging message and HTTP response description. +// statusCode is the HTTP status code to be returned, must be 4xx or 5xx +// loggerAction is a short description which will be used as the action if the error is logged. +func NewFailureResponse(err error, statusCode int, loggerAction string) *FailureResponse { + return &FailureResponse{ + error: err, + statusCode: statusCode, + loggerAction: loggerAction, + } +} + +// ErrorResponse returns an interface{} which will be JSON encoded and form the body +// of the HTTP response +func (f *FailureResponse) ErrorResponse() interface{} { + if f.emptyResponse { + return EmptyResponse{} + } + + return ErrorResponse{ + Description: f.error.Error(), + Error: f.errorKey, + } +} + +// ValidatedStatusCode returns the HTTP response status code. If the code is not 4xx +// or 5xx, an InternalServerError will be returned instead. +func (f *FailureResponse) ValidatedStatusCode(logger lager.Logger) int { + if f.statusCode < 400 || 600 <= f.statusCode { + if logger != nil { + logger.Error("validating-status-code", fmt.Errorf("Invalid failure http response code: 600, expected 4xx or 5xx, returning internal server error: 500.")) + } + return http.StatusInternalServerError + } + return f.statusCode +} + +// LoggerAction returns the loggerAction, used as the action when logging +func (f *FailureResponse) LoggerAction() string { + return f.loggerAction +} + +// FailureResponseBuilder provides a fluent set of methods to build a *FailureResponse. +type FailureResponseBuilder struct { + error + statusCode int + loggerAction string + emptyResponse bool + errorKey string +} + +// NewFailureResponseBuilder returns a pointer to a newly instantiated FailureResponseBuilder +// Accepts required arguments to create a FailureResponse. +func NewFailureResponseBuilder(err error, statusCode int, loggerAction string) *FailureResponseBuilder { + return &FailureResponseBuilder{ + error: err, + statusCode: statusCode, + loggerAction: loggerAction, + emptyResponse: false, + } +} + +// WithErrorKey adds a custom ErrorKey which will be used in FailureResponse to add an `Error` +// field to the JSON HTTP response body +func (f *FailureResponseBuilder) WithErrorKey(errorKey string) *FailureResponseBuilder { + f.errorKey = errorKey + return f +} + +// WithEmptyResponse will cause the built FailureResponse to return an empty JSON object as the +// HTTP response body +func (f *FailureResponseBuilder) WithEmptyResponse() *FailureResponseBuilder { + f.emptyResponse = true + return f +} + +// Build returns the generated FailureResponse built using previously configured variables. +func (f *FailureResponseBuilder) Build() *FailureResponse { + return &FailureResponse{ + error: f.error, + statusCode: f.statusCode, + loggerAction: f.loggerAction, + emptyResponse: f.emptyResponse, + errorKey: f.errorKey, + } +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/failure_response_test.go b/vendor/github.com/pivotal-cf/brokerapi/failure_response_test.go new file mode 100644 index 00000000000..0475d8e9249 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/failure_response_test.go @@ -0,0 +1,82 @@ +package brokerapi_test + +import ( + "github.com/pivotal-cf/brokerapi" + + "errors" + + "net/http" + + "code.cloudfoundry.org/lager" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("FailureResponse", func() { + Describe("ErrorResponse", func() { + It("returns a ErrorResponse containing the error message", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), http.StatusForbidden, "log-key") + Expect(failureResponse.ErrorResponse()).To(Equal(brokerapi.ErrorResponse{ + Description: "my error message", + })) + }) + + Context("when the error key is provided", func() { + It("returns a ErrorResponse containing the error message and the error key", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithErrorKey("error key").Build() + Expect(failureResponse.ErrorResponse()).To(Equal(brokerapi.ErrorResponse{ + Description: "my error message", + Error: "error key", + })) + }) + }) + + Context("when created with empty response", func() { + It("returns an EmptyResponse", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithEmptyResponse().Build() + Expect(failureResponse.ErrorResponse()).To(Equal(brokerapi.EmptyResponse{})) + }) + }) + }) + + Describe("ValidatedStatusCode", func() { + It("returns the status code that was passed in", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), http.StatusForbidden, "log-key") + Expect(failureResponse.ValidatedStatusCode(nil)).To(Equal(http.StatusForbidden)) + }) + + It("when error key is provided it returns the status code that was passed in", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithErrorKey("error key").Build() + Expect(failureResponse.ValidatedStatusCode(nil)).To(Equal(http.StatusForbidden)) + }) + + Context("when the status code is invalid", func() { + It("returns 500", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), 600, "log-key") + Expect(failureResponse.ValidatedStatusCode(nil)).To(Equal(http.StatusInternalServerError)) + }) + + It("logs that the status has been changed", func() { + log := gbytes.NewBuffer() + logger := lager.NewLogger("test") + logger.RegisterSink(lager.NewWriterSink(log, lager.DEBUG)) + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), 600, "log-key") + failureResponse.ValidatedStatusCode(logger) + Expect(log).To(gbytes.Say("Invalid failure http response code: 600, expected 4xx or 5xx, returning internal server error: 500.")) + }) + }) + }) + + Describe("LoggerAction", func() { + It("returns the logger action that was passed in", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithErrorKey("error key").Build() + Expect(failureResponse.LoggerAction()).To(Equal("log-key")) + }) + + It("when error key is provided it returns the logger action that was passed in", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), http.StatusForbidden, "log-key") + Expect(failureResponse.LoggerAction()).To(Equal("log-key")) + }) + }) +}) diff --git a/vendor/github.com/pivotal-cf/brokerapi/fakes/fake_service_broker.go b/vendor/github.com/pivotal-cf/brokerapi/fakes/fake_service_broker.go new file mode 100644 index 00000000000..9c44a50f38c --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fakes/fake_service_broker.go @@ -0,0 +1,324 @@ +package fakes + +import ( + "context" + + "github.com/pivotal-cf/brokerapi" +) + +type FakeServiceBroker struct { + ProvisionDetails brokerapi.ProvisionDetails + UpdateDetails brokerapi.UpdateDetails + DeprovisionDetails brokerapi.DeprovisionDetails + + ProvisionedInstanceIDs []string + DeprovisionedInstanceIDs []string + UpdatedInstanceIDs []string + + BoundInstanceIDs []string + BoundBindingIDs []string + BoundBindingDetails brokerapi.BindDetails + SyslogDrainURL string + RouteServiceURL string + VolumeMounts []brokerapi.VolumeMount + + UnbindingDetails brokerapi.UnbindDetails + + InstanceLimit int + + ProvisionError error + BindError error + UnbindError error + DeprovisionError error + LastOperationError error + UpdateError error + + BrokerCalled bool + LastOperationState brokerapi.LastOperationState + LastOperationDescription string + + AsyncAllowed bool + + ShouldReturnAsync bool + DashboardURL string + OperationDataToReturn string + + LastOperationInstanceID string + LastOperationData string + + ReceivedContext bool +} + +type FakeAsyncServiceBroker struct { + FakeServiceBroker + ShouldProvisionAsync bool +} + +type FakeAsyncOnlyServiceBroker struct { + FakeServiceBroker +} + +func (fakeBroker *FakeServiceBroker) Services(context context.Context) []brokerapi.Service { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + return []brokerapi.Service{ + { + ID: "0A789746-596F-4CEA-BFAC-A0795DA056E3", + Name: "p-cassandra", + Description: "Cassandra service for application development and testing", + Bindable: true, + PlanUpdatable: true, + Plans: []brokerapi.ServicePlan{ + { + ID: "ABE176EE-F69F-4A96-80CE-142595CC24E3", + Name: "default", + Description: "The default Cassandra plan", + Metadata: &brokerapi.ServicePlanMetadata{ + Bullets: []string{}, + DisplayName: "Cassandra", + }, + }, + }, + Metadata: &brokerapi.ServiceMetadata{ + DisplayName: "Cassandra", + LongDescription: "Long description", + DocumentationUrl: "http://thedocs.com", + SupportUrl: "http://helpme.no", + }, + Tags: []string{ + "pivotal", + "cassandra", + }, + }, + } +} + +func (fakeBroker *FakeServiceBroker) Provision(context context.Context, instanceID string, details brokerapi.ProvisionDetails, asyncAllowed bool) (brokerapi.ProvisionedServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.ProvisionError != nil { + return brokerapi.ProvisionedServiceSpec{}, fakeBroker.ProvisionError + } + + if len(fakeBroker.ProvisionedInstanceIDs) >= fakeBroker.InstanceLimit { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceLimitMet + } + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceAlreadyExists + } + + fakeBroker.ProvisionDetails = details + fakeBroker.ProvisionedInstanceIDs = append(fakeBroker.ProvisionedInstanceIDs, instanceID) + return brokerapi.ProvisionedServiceSpec{DashboardURL: fakeBroker.DashboardURL}, nil +} + +func (fakeBroker *FakeAsyncServiceBroker) Provision(context context.Context, instanceID string, details brokerapi.ProvisionDetails, asyncAllowed bool) (brokerapi.ProvisionedServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.ProvisionError != nil { + return brokerapi.ProvisionedServiceSpec{}, fakeBroker.ProvisionError + } + + if len(fakeBroker.ProvisionedInstanceIDs) >= fakeBroker.InstanceLimit { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceLimitMet + } + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceAlreadyExists + } + + fakeBroker.ProvisionDetails = details + fakeBroker.ProvisionedInstanceIDs = append(fakeBroker.ProvisionedInstanceIDs, instanceID) + return brokerapi.ProvisionedServiceSpec{IsAsync: fakeBroker.ShouldProvisionAsync, DashboardURL: fakeBroker.DashboardURL, OperationData: fakeBroker.OperationDataToReturn}, nil +} + +func (fakeBroker *FakeAsyncOnlyServiceBroker) Provision(context context.Context, instanceID string, details brokerapi.ProvisionDetails, asyncAllowed bool) (brokerapi.ProvisionedServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.ProvisionError != nil { + return brokerapi.ProvisionedServiceSpec{}, fakeBroker.ProvisionError + } + + if len(fakeBroker.ProvisionedInstanceIDs) >= fakeBroker.InstanceLimit { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceLimitMet + } + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceAlreadyExists + } + + if !asyncAllowed { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrAsyncRequired + } + + fakeBroker.ProvisionDetails = details + fakeBroker.ProvisionedInstanceIDs = append(fakeBroker.ProvisionedInstanceIDs, instanceID) + return brokerapi.ProvisionedServiceSpec{IsAsync: true, DashboardURL: fakeBroker.DashboardURL}, nil +} + +func (fakeBroker *FakeServiceBroker) Update(context context.Context, instanceID string, details brokerapi.UpdateDetails, asyncAllowed bool) (brokerapi.UpdateServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.UpdateError != nil { + return brokerapi.UpdateServiceSpec{}, fakeBroker.UpdateError + } + + fakeBroker.UpdateDetails = details + fakeBroker.UpdatedInstanceIDs = append(fakeBroker.UpdatedInstanceIDs, instanceID) + fakeBroker.AsyncAllowed = asyncAllowed + return brokerapi.UpdateServiceSpec{IsAsync: fakeBroker.ShouldReturnAsync, OperationData: fakeBroker.OperationDataToReturn}, nil +} + +func (fakeBroker *FakeServiceBroker) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.DeprovisionError != nil { + return brokerapi.DeprovisionServiceSpec{}, fakeBroker.DeprovisionError + } + + fakeBroker.DeprovisionDetails = details + fakeBroker.DeprovisionedInstanceIDs = append(fakeBroker.DeprovisionedInstanceIDs, instanceID) + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.DeprovisionServiceSpec{}, nil + } + return brokerapi.DeprovisionServiceSpec{IsAsync: false}, brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeAsyncOnlyServiceBroker) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.DeprovisionError != nil { + return brokerapi.DeprovisionServiceSpec{IsAsync: true}, fakeBroker.DeprovisionError + } + + if !asyncAllowed { + return brokerapi.DeprovisionServiceSpec{IsAsync: true}, brokerapi.ErrAsyncRequired + } + + fakeBroker.DeprovisionedInstanceIDs = append(fakeBroker.DeprovisionedInstanceIDs, instanceID) + fakeBroker.DeprovisionDetails = details + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.DeprovisionServiceSpec{IsAsync: true, OperationData: fakeBroker.OperationDataToReturn}, nil + } + + return brokerapi.DeprovisionServiceSpec{IsAsync: true, OperationData: fakeBroker.OperationDataToReturn}, brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeAsyncServiceBroker) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.DeprovisionError != nil { + return brokerapi.DeprovisionServiceSpec{IsAsync: asyncAllowed}, fakeBroker.DeprovisionError + } + + fakeBroker.DeprovisionedInstanceIDs = append(fakeBroker.DeprovisionedInstanceIDs, instanceID) + fakeBroker.DeprovisionDetails = details + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.DeprovisionServiceSpec{IsAsync: asyncAllowed, OperationData: fakeBroker.OperationDataToReturn}, nil + } + + return brokerapi.DeprovisionServiceSpec{OperationData: fakeBroker.OperationDataToReturn, IsAsync: asyncAllowed}, brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeServiceBroker) Bind(context context.Context, instanceID, bindingID string, details brokerapi.BindDetails) (brokerapi.Binding, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.BindError != nil { + return brokerapi.Binding{}, fakeBroker.BindError + } + + fakeBroker.BoundBindingDetails = details + + fakeBroker.BoundInstanceIDs = append(fakeBroker.BoundInstanceIDs, instanceID) + fakeBroker.BoundBindingIDs = append(fakeBroker.BoundBindingIDs, bindingID) + + return brokerapi.Binding{ + Credentials: FakeCredentials{ + Host: "127.0.0.1", + Port: 3000, + Username: "batman", + Password: "robin", + }, + SyslogDrainURL: fakeBroker.SyslogDrainURL, + RouteServiceURL: fakeBroker.RouteServiceURL, + VolumeMounts: fakeBroker.VolumeMounts, + }, nil +} + +func (fakeBroker *FakeServiceBroker) Unbind(context context.Context, instanceID, bindingID string, details brokerapi.UnbindDetails) error { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.UnbindError != nil { + return fakeBroker.UnbindError + } + + fakeBroker.UnbindingDetails = details + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + if sliceContains(bindingID, fakeBroker.BoundBindingIDs) { + return nil + } + return brokerapi.ErrBindingDoesNotExist + } + + return brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeServiceBroker) LastOperation(context context.Context, instanceID, operationData string) (brokerapi.LastOperation, error) { + fakeBroker.LastOperationInstanceID = instanceID + fakeBroker.LastOperationData = operationData + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.LastOperationError != nil { + return brokerapi.LastOperation{}, fakeBroker.LastOperationError + } + + return brokerapi.LastOperation{State: fakeBroker.LastOperationState, Description: fakeBroker.LastOperationDescription}, nil +} + +type FakeCredentials struct { + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` +} + +func sliceContains(needle string, haystack []string) bool { + for _, element := range haystack { + if element == needle { + return true + } + } + return false +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/async_required.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/async_required.json new file mode 100644 index 00000000000..e3c4593728b --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/async_required.json @@ -0,0 +1,4 @@ +{ + "error": "AsyncRequired", + "description": "This service plan requires client support for asynchronous service operations." +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding.json new file mode 100644 index 00000000000..c6d5afaa29a --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding.json @@ -0,0 +1,8 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + } +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_experimental_volume_mounts.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_experimental_volume_mounts.json new file mode 100644 index 00000000000..2f234412625 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_experimental_volume_mounts.json @@ -0,0 +1,17 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "volume_mounts": [{ + "container_path": "/dev/null", + "mode": "rw", + "private": { + "driver": "driver", + "group_id": "some-guid", + "config": "{\"key\":\"value\"}" + } + }] +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_route_service.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_route_service.json new file mode 100644 index 00000000000..87264a5d0e1 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_route_service.json @@ -0,0 +1,9 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "route_service_url": "some-route-url" +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_syslog.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_syslog.json new file mode 100644 index 00000000000..3e8caae9b61 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_syslog.json @@ -0,0 +1,9 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "syslog_drain_url": "some-drain-url" +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_volume_mounts.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_volume_mounts.json new file mode 100644 index 00000000000..6b56b90b0fa --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_volume_mounts.json @@ -0,0 +1,20 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "volume_mounts": [{ + "driver": "driver", + "container_dir": "/dev/null", + "mode": "rw", + "device_type": "shared", + "device": { + "volume_id": "some-guid", + "mount_config": { + "key": "value" + } + } + }] +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/catalog.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/catalog.json new file mode 100644 index 00000000000..bf6c71cdf40 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/catalog.json @@ -0,0 +1,27 @@ +{ + "services": [{ + "bindable": true, + "description": "Cassandra service for application development and testing", + "id": "0A789746-596F-4CEA-BFAC-A0795DA056E3", + "name": "p-cassandra", + "plan_updateable": true, + "plans": [{ + "description": "The default Cassandra plan", + "id": "ABE176EE-F69F-4A96-80CE-142595CC24E3", + "name": "default", + "metadata": { + "displayName": "Cassandra" + } + }], + "metadata": { + "displayName": "Cassandra", + "longDescription": "Long description", + "documentationUrl": "http://thedocs.com", + "supportUrl": "http://helpme.no" + }, + "tags": [ + "pivotal", + "cassandra" + ] + }] +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/instance_limit_error.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/instance_limit_error.json new file mode 100644 index 00000000000..df66adbcd4d --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/instance_limit_error.json @@ -0,0 +1,3 @@ +{ + "description": "instance limit for this service has been reached" +} \ No newline at end of file diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/invalid_async_provision_error.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/invalid_async_provision_error.json new file mode 100644 index 00000000000..4507fe973ff --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/invalid_async_provision_error.json @@ -0,0 +1,3 @@ +{ + "description": "broker attempted to provision asynchronously when not supported by the caller" +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/last_operation_succeeded.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/last_operation_succeeded.json new file mode 100644 index 00000000000..5d9a5a43fc0 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/last_operation_succeeded.json @@ -0,0 +1,4 @@ +{ + "state": "succeeded", + "description": "some description" +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/operation_data_response.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/operation_data_response.json new file mode 100644 index 00000000000..e5f86ae28ca --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/operation_data_response.json @@ -0,0 +1,3 @@ +{ + "operation": "some-operation-data" +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning.json @@ -0,0 +1 @@ +{} diff --git a/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning_with_dashboard.json b/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning_with_dashboard.json new file mode 100644 index 00000000000..0a31f48bd30 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning_with_dashboard.json @@ -0,0 +1,3 @@ +{ + "dashboard_url": "some-dashboard-url" +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/response.go b/vendor/github.com/pivotal-cf/brokerapi/response.go new file mode 100644 index 00000000000..64ada789d37 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/response.go @@ -0,0 +1,49 @@ +package brokerapi + +type EmptyResponse struct{} + +type ErrorResponse struct { + Error string `json:"error,omitempty"` + Description string `json:"description"` +} + +type CatalogResponse struct { + Services []Service `json:"services"` +} + +type ProvisioningResponse struct { + DashboardURL string `json:"dashboard_url,omitempty"` + OperationData string `json:"operation,omitempty"` +} + +type UpdateResponse struct { + OperationData string `json:"operation,omitempty"` +} + +type DeprovisionResponse struct { + OperationData string `json:"operation,omitempty"` +} + +type LastOperationResponse struct { + State LastOperationState `json:"state"` + Description string `json:"description,omitempty"` +} + +type ExperimentalVolumeMountBindingResponse struct { + Credentials interface{} `json:"credentials"` + SyslogDrainURL string `json:"syslog_drain_url,omitempty"` + RouteServiceURL string `json:"route_service_url,omitempty"` + VolumeMounts []ExperimentalVolumeMount `json:"volume_mounts,omitempty"` +} + +type ExperimentalVolumeMount struct { + ContainerPath string `json:"container_path"` + Mode string `json:"mode"` + Private ExperimentalVolumeMountPrivate `json:"private"` +} + +type ExperimentalVolumeMountPrivate struct { + Driver string `json:"driver"` + GroupID string `json:"group_id"` + Config string `json:"config"` +} diff --git a/vendor/github.com/pivotal-cf/brokerapi/response_test.go b/vendor/github.com/pivotal-cf/brokerapi/response_test.go new file mode 100644 index 00000000000..d024296d8d0 --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/response_test.go @@ -0,0 +1,70 @@ +package brokerapi_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pivotal-cf/brokerapi" +) + +var _ = Describe("Catalog Response", func() { + Describe("JSON encoding", func() { + It("has a list of services", func() { + catalogResponse := brokerapi.CatalogResponse{ + Services: []brokerapi.Service{}, + } + jsonString := `{"services":[]}` + + Expect(json.Marshal(catalogResponse)).To(MatchJSON(jsonString)) + }) + }) +}) + +var _ = Describe("Provisioning Response", func() { + Describe("JSON encoding", func() { + Context("when the dashboard URL is not present", func() { + It("does not return it in the JSON", func() { + provisioningResponse := brokerapi.ProvisioningResponse{} + jsonString := `{}` + + Expect(json.Marshal(provisioningResponse)).To(MatchJSON(jsonString)) + }) + }) + + Context("when the dashboard URL is present", func() { + It("returns it in the JSON", func() { + provisioningResponse := brokerapi.ProvisioningResponse{ + DashboardURL: "http://example.com/broker", + } + jsonString := `{"dashboard_url":"http://example.com/broker"}` + + Expect(json.Marshal(provisioningResponse)).To(MatchJSON(jsonString)) + }) + }) + }) +}) + +var _ = Describe("Binding Response", func() { + Describe("JSON encoding", func() { + It("has a credentials object", func() { + binding := brokerapi.Binding{} + jsonString := `{"credentials":null}` + + Expect(json.Marshal(binding)).To(MatchJSON(jsonString)) + }) + }) +}) + +var _ = Describe("Error Response", func() { + Describe("JSON encoding", func() { + It("has a description field", func() { + errorResponse := brokerapi.ErrorResponse{ + Description: "a bad thing happened", + } + jsonString := `{"description":"a bad thing happened"}` + + Expect(json.Marshal(errorResponse)).To(MatchJSON(jsonString)) + }) + }) +}) diff --git a/vendor/github.com/pivotal-cf/brokerapi/service_broker.go b/vendor/github.com/pivotal-cf/brokerapi/service_broker.go new file mode 100644 index 00000000000..01d83c4715b --- /dev/null +++ b/vendor/github.com/pivotal-cf/brokerapi/service_broker.go @@ -0,0 +1,187 @@ +package brokerapi + +import ( + "context" + "encoding/json" + "errors" + "net/http" +) + +type ServiceBroker interface { + Services(context context.Context) []Service + + Provision(context context.Context, instanceID string, details ProvisionDetails, asyncAllowed bool) (ProvisionedServiceSpec, error) + Deprovision(context context.Context, instanceID string, details DeprovisionDetails, asyncAllowed bool) (DeprovisionServiceSpec, error) + + Bind(context context.Context, instanceID, bindingID string, details BindDetails) (Binding, error) + Unbind(context context.Context, instanceID, bindingID string, details UnbindDetails) error + + Update(context context.Context, instanceID string, details UpdateDetails, asyncAllowed bool) (UpdateServiceSpec, error) + + LastOperation(context context.Context, instanceID, operationData string) (LastOperation, error) +} + +type DetailsWithRawParameters interface { + GetRawParameters() json.RawMessage +} + +func (d ProvisionDetails) GetRawParameters() json.RawMessage { + return d.RawParameters +} + +func (d BindDetails) GetRawParameters() json.RawMessage { + return d.RawParameters +} + +func (d UpdateDetails) GetRawParameters() json.RawMessage { + return d.RawParameters +} + +type ProvisionDetails struct { + ServiceID string `json:"service_id"` + PlanID string `json:"plan_id"` + OrganizationGUID string `json:"organization_guid"` + SpaceGUID string `json:"space_guid"` + RawParameters json.RawMessage `json:"parameters,omitempty"` +} + +type ProvisionedServiceSpec struct { + IsAsync bool + DashboardURL string + OperationData string +} + +type BindDetails struct { + AppGUID string `json:"app_guid"` + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` + BindResource *BindResource `json:"bind_resource,omitempty"` + RawParameters json.RawMessage `json:"parameters,omitempty"` +} + +type BindResource struct { + AppGuid string `json:"app_guid,omitempty"` + Route string `json:"route,omitempty"` +} + +type UnbindDetails struct { + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` +} + +type UpdateServiceSpec struct { + IsAsync bool + OperationData string +} + +type DeprovisionServiceSpec struct { + IsAsync bool + OperationData string +} + +type DeprovisionDetails struct { + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` +} + +type UpdateDetails struct { + ServiceID string `json:"service_id"` + PlanID string `json:"plan_id"` + RawParameters json.RawMessage `json:"parameters,omitempty"` + PreviousValues PreviousValues `json:"previous_values"` +} + +type PreviousValues struct { + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` + OrgID string `json:"organization_id"` + SpaceID string `json:"space_id"` +} + +type LastOperation struct { + State LastOperationState + Description string +} + +type LastOperationState string + +const ( + InProgress LastOperationState = "in progress" + Succeeded LastOperationState = "succeeded" + Failed LastOperationState = "failed" +) + +type Binding struct { + Credentials interface{} `json:"credentials"` + SyslogDrainURL string `json:"syslog_drain_url,omitempty"` + RouteServiceURL string `json:"route_service_url,omitempty"` + VolumeMounts []VolumeMount `json:"volume_mounts,omitempty"` +} + +type VolumeMount struct { + Driver string `json:"driver"` + ContainerDir string `json:"container_dir"` + Mode string `json:"mode"` + DeviceType string `json:"device_type"` + Device SharedDevice `json:"device"` +} + +type SharedDevice struct { + VolumeId string `json:"volume_id"` + MountConfig map[string]interface{} `json:"mount_config"` +} + +const ( + instanceExistsMsg = "instance already exists" + instanceDoesntExistMsg = "instance does not exist" + serviceLimitReachedMsg = "instance limit for this service has been reached" + servicePlanQuotaExceededMsg = "The quota for this service plan has been exceeded. Please contact your Operator for help." + serviceQuotaExceededMsg = "The quota for this service has been exceeded. Please contact your Operator for help." + bindingExistsMsg = "binding already exists" + bindingDoesntExistMsg = "binding does not exist" + asyncRequiredMsg = "This service plan requires client support for asynchronous service operations." + planChangeUnsupportedMsg = "The requested plan migration cannot be performed" + rawInvalidParamsMsg = "The format of the parameters is not valid JSON" + appGuidMissingMsg = "app_guid is a required field but was not provided" +) + +var ( + ErrInstanceAlreadyExists = NewFailureResponseBuilder( + errors.New(instanceExistsMsg), http.StatusConflict, instanceAlreadyExistsErrorKey, + ).WithEmptyResponse().Build() + + ErrInstanceDoesNotExist = NewFailureResponseBuilder( + errors.New(instanceDoesntExistMsg), http.StatusGone, instanceMissingErrorKey, + ).WithEmptyResponse().Build() + + ErrInstanceLimitMet = NewFailureResponse( + errors.New(serviceLimitReachedMsg), http.StatusInternalServerError, instanceLimitReachedErrorKey, + ) + + ErrBindingAlreadyExists = NewFailureResponse( + errors.New(bindingExistsMsg), http.StatusConflict, bindingAlreadyExistsErrorKey, + ) + + ErrBindingDoesNotExist = NewFailureResponseBuilder( + errors.New(bindingDoesntExistMsg), http.StatusGone, bindingMissingErrorKey, + ).WithEmptyResponse().Build() + + ErrAsyncRequired = NewFailureResponseBuilder( + errors.New(asyncRequiredMsg), http.StatusUnprocessableEntity, asyncRequiredKey, + ).WithErrorKey("AsyncRequired").Build() + + ErrPlanChangeNotSupported = NewFailureResponseBuilder( + errors.New(planChangeUnsupportedMsg), http.StatusUnprocessableEntity, planChangeNotSupportedKey, + ).WithErrorKey("PlanChangeNotSupported").Build() + + ErrRawParamsInvalid = NewFailureResponse( + errors.New(rawInvalidParamsMsg), http.StatusUnprocessableEntity, invalidRawParamsKey, + ) + + ErrAppGuidNotProvided = NewFailureResponse( + errors.New(appGuidMissingMsg), http.StatusUnprocessableEntity, appGuidNotProvidedErrorKey, + ) + + ErrPlanQuotaExceeded = errors.New(servicePlanQuotaExceededMsg) + ErrServiceQuotaExceeded = errors.New(serviceQuotaExceededMsg) +) From f8269b5129f3b9f70137ec7a47446bebe6657cee Mon Sep 17 00:00:00 2001 From: Aaron Schlesinger Date: Fri, 9 Jun 2017 11:06:25 -0700 Subject: [PATCH 2/6] converting a single test to use the broker server --- pkg/controller/controller_broker_test.go | 29 ++++++++--- pkg/controller/controller_test.go | 61 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/pkg/controller/controller_broker_test.go b/pkg/controller/controller_broker_test.go index 3e588d80cb9..d1326030b41 100644 --- a/pkg/controller/controller_broker_test.go +++ b/pkg/controller/controller_broker_test.go @@ -24,6 +24,7 @@ import ( "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" fakebrokerapi "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake" + fakebrokerserver "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -111,13 +112,20 @@ func TestShouldReconcileBroker(t *testing.T) { } func TestReconcileBroker(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, _ := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + const ( + brokerUsername = "testuser" + brokerPassword = "testpassword" + ) + controllerParams, err := newTestControllerWithBrokerServer(brokerUsername, brokerPassword) + if err != nil { + t.Fatal(err) + } + defer controllerParams.Close() - testController.reconcileBroker(getTestBroker()) + controllerParams.BrokerServerHandler.Catalog = fakebrokerserver.ConvertCatalog(getTestCatalog()) + controllerParams.Controller.reconcileBroker(getTestBroker()) - actions := fakeCatalogClient.Actions() + actions := controllerParams.FakeCatalogClient.Actions() assertNumberOfActions(t, actions, 2) // first action should be a create action for a service class @@ -128,15 +136,22 @@ func TestReconcileBroker(t *testing.T) { assertBrokerReadyTrue(t, updatedBroker) // verify no kube resources created - assertNumberOfActions(t, fakeKubeClient.Actions(), 0) + assertNumberOfActions(t, controllerParams.FakeKubeClient.Actions(), 0) - events := getRecordedEvents(testController) + events := getRecordedEvents(controllerParams.Controller) assertNumEvents(t, events, 1) expectedEvent := api.EventTypeNormal + " " + successFetchedCatalogReason + " " + successFetchedCatalogMessage if e, a := expectedEvent, events[0]; e != a { t.Fatalf("Received unexpected event: %v", a) } + + if controllerParams.BrokerServerHandler.CatalogRequests != 1 { + t.Fatalf( + "expected 1 catalog request, got %d", + controllerParams.BrokerServerHandler.CatalogRequests, + ) + } } func TestReconcileBrokerExistingServiceClass(t *testing.T) { diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 5828da475d1..d9075c6c4a4 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -18,6 +18,7 @@ package controller import ( "encoding/json" + "net/http/httptest" "reflect" "runtime/debug" "testing" @@ -34,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/diff" + fakebrokerserver "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server" clientgofake "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/pkg/api/v1" clientgotesting "k8s.io/client-go/testing" @@ -977,6 +979,65 @@ func newTestController(t *testing.T) ( return fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController.(*controller), serviceCatalogSharedInformers } +type testControllerWithBrokerServer struct { + FakeKubeClient *clientgofake.Clientset + FakeCatalogClient *servicecatalogclientset.Clientset + Controller *controller + Informers v1alpha1informers.Interface + BrokerServerHandler *fakebrokerserver.Handler + BrokerServer *httptest.Server +} + +func (t *testControllerWithBrokerServer) Close() { + t.BrokerServer.Close() +} + +func newTestControllerWithBrokerServer( + brokerUsername, + brokerPassword string, +) (*testControllerWithBrokerServer, error) { + // create a fake kube client + fakeKubeClient := &clientgofake.Clientset{} + // create a fake sc client + fakeCatalogClient := &servicecatalogclientset.Clientset{} + + brokerHandler := fakebrokerserver.NewHandler() + brokerServer := fakebrokerserver.Run(brokerHandler, brokerUsername, brokerPassword) + brokerClFunc := fakebrokerserver.NewCreateFunc(brokerServer, brokerUsername, brokerPassword) + + // create informers + informerFactory := servicecataloginformers.NewSharedInformerFactory(fakeCatalogClient, 0) + serviceCatalogSharedInformers := informerFactory.Servicecatalog().V1alpha1() + + fakeRecorder := record.NewFakeRecorder(5) + + // create a test controller + testController, err := NewController( + fakeKubeClient, + fakeCatalogClient.ServicecatalogV1alpha1(), + serviceCatalogSharedInformers.Brokers(), + serviceCatalogSharedInformers.ServiceClasses(), + serviceCatalogSharedInformers.Instances(), + serviceCatalogSharedInformers.Bindings(), + brokerClFunc, + 24*time.Hour, + true, /* enable OSB context profile */ + fakeRecorder, + ) + if err != nil { + return nil, err + } + + return &testControllerWithBrokerServer{ + FakeKubeClient: fakeKubeClient, + FakeCatalogClient: fakeCatalogClient, + Controller: testController.(*controller), + Informers: serviceCatalogSharedInformers, + BrokerServerHandler: brokerHandler, + BrokerServer: brokerServer, + }, nil +} + func getRecordedEvents(testController *controller) []string { source := testController.recorder.(*record.FakeRecorder).Events done := false From 52807a096c8065bee558e7e70cf8b794af7542a0 Mon Sep 17 00:00:00 2001 From: Aaron Schlesinger Date: Fri, 9 Jun 2017 11:20:12 -0700 Subject: [PATCH 3/6] adding boilerplate and docs --- pkg/brokerapi/fake/server/bind_request.go | 16 ++++++++++++++ pkg/brokerapi/fake/server/convert_catalog.go | 16 ++++++++++++++ pkg/brokerapi/fake/server/create_func.go | 16 ++++++++++++++ .../fake/server/deprovision_request.go | 16 ++++++++++++++ pkg/brokerapi/fake/server/handler.go | 16 ++++++++++++++ pkg/brokerapi/fake/server/init.go | 16 ++++++++++++++ .../fake/server/last_operation_request.go | 20 ++++++++++++++++- .../fake/server/provision_request.go | 16 ++++++++++++++ pkg/brokerapi/fake/server/server.go | 16 ++++++++++++++ pkg/brokerapi/fake/server/unbind_request.go | 22 ++++++++++++++++--- pkg/brokerapi/fake/server/update_request.go | 16 ++++++++++++++ 11 files changed, 182 insertions(+), 4 deletions(-) diff --git a/pkg/brokerapi/fake/server/bind_request.go b/pkg/brokerapi/fake/server/bind_request.go index 3a9e0a03881..c4d236b113f 100644 --- a/pkg/brokerapi/fake/server/bind_request.go +++ b/pkg/brokerapi/fake/server/bind_request.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/convert_catalog.go b/pkg/brokerapi/fake/server/convert_catalog.go index cd3863a40b8..269e2b45f66 100644 --- a/pkg/brokerapi/fake/server/convert_catalog.go +++ b/pkg/brokerapi/fake/server/convert_catalog.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/create_func.go b/pkg/brokerapi/fake/server/create_func.go index 73bdae284d0..582e4c7b960 100644 --- a/pkg/brokerapi/fake/server/create_func.go +++ b/pkg/brokerapi/fake/server/create_func.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/deprovision_request.go b/pkg/brokerapi/fake/server/deprovision_request.go index 363bf112f99..e7da462b118 100644 --- a/pkg/brokerapi/fake/server/deprovision_request.go +++ b/pkg/brokerapi/fake/server/deprovision_request.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/handler.go b/pkg/brokerapi/fake/server/handler.go index 94bcf9aab45..ac87afcdb24 100644 --- a/pkg/brokerapi/fake/server/handler.go +++ b/pkg/brokerapi/fake/server/handler.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/init.go b/pkg/brokerapi/fake/server/init.go index 0e1ca47c380..f174e97846e 100644 --- a/pkg/brokerapi/fake/server/init.go +++ b/pkg/brokerapi/fake/server/init.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/last_operation_request.go b/pkg/brokerapi/fake/server/last_operation_request.go index e0491784d2f..d5c2545a8fd 100644 --- a/pkg/brokerapi/fake/server/last_operation_request.go +++ b/pkg/brokerapi/fake/server/last_operation_request.go @@ -1,6 +1,24 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server +// LastOperationRequest is the struct that contains details of a single request to get the last +// operation of an ongoing broker action type LastOperationRequest struct { - InstanceID string + InstanceID string OperationData string } diff --git a/pkg/brokerapi/fake/server/provision_request.go b/pkg/brokerapi/fake/server/provision_request.go index 061d4671036..b7f546ccfcd 100644 --- a/pkg/brokerapi/fake/server/provision_request.go +++ b/pkg/brokerapi/fake/server/provision_request.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/server.go b/pkg/brokerapi/fake/server/server.go index 547e5815b44..3d425d4e3e4 100644 --- a/pkg/brokerapi/fake/server/server.go +++ b/pkg/brokerapi/fake/server/server.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( diff --git a/pkg/brokerapi/fake/server/unbind_request.go b/pkg/brokerapi/fake/server/unbind_request.go index 983a46cf588..b5780e0faf3 100644 --- a/pkg/brokerapi/fake/server/unbind_request.go +++ b/pkg/brokerapi/fake/server/unbind_request.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( @@ -5,8 +21,8 @@ import ( ) // UnbindRequest is the struct to house details of a single unbind request -type UnbindRequest struct{ +type UnbindRequest struct { InstanceID string - BindingID string - Details brokerapi.UnbindDetails + BindingID string + Details brokerapi.UnbindDetails } diff --git a/pkg/brokerapi/fake/server/update_request.go b/pkg/brokerapi/fake/server/update_request.go index bd8a64d6b33..ea7919b3865 100644 --- a/pkg/brokerapi/fake/server/update_request.go +++ b/pkg/brokerapi/fake/server/update_request.go @@ -1,3 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package server import ( From 49ff83d222413b1a41eefd92df19bd2110ff2737 Mon Sep 17 00:00:00 2001 From: Aaron Schlesinger Date: Mon, 12 Jun 2017 16:08:20 -0700 Subject: [PATCH 4/6] naming the pivotal brokerapi package to eliminate possible ambiguity --- pkg/brokerapi/fake/server/convert_catalog.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/brokerapi/fake/server/convert_catalog.go b/pkg/brokerapi/fake/server/convert_catalog.go index 269e2b45f66..4ee84710b0a 100644 --- a/pkg/brokerapi/fake/server/convert_catalog.go +++ b/pkg/brokerapi/fake/server/convert_catalog.go @@ -18,21 +18,21 @@ package server import ( pkgbrokerapi "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" - "github.com/pivotal-cf/brokerapi" + pivbrokerapi "github.com/pivotal-cf/brokerapi" ) // ConvertCatalog converts a (github.com/kubernetes-incubator/service-catalog/pkg/brokerapi).Catalog // to an array of brokerapi.Services -func ConvertCatalog(cat *pkgbrokerapi.Catalog) []brokerapi.Service { - ret := make([]brokerapi.Service, len(cat.Services)) +func ConvertCatalog(cat *pkgbrokerapi.Catalog) []pivbrokerapi.Service { + ret := make([]pivbrokerapi.Service, len(cat.Services)) for i, svc := range cat.Services { ret[i] = convertService(svc) } return ret } -func convertService(svc *pkgbrokerapi.Service) brokerapi.Service { - return brokerapi.Service{ +func convertService(svc *pkgbrokerapi.Service) pivbrokerapi.Service { + return pivbrokerapi.Service{ ID: svc.ID, Name: svc.Name, Description: svc.Description, @@ -44,10 +44,10 @@ func convertService(svc *pkgbrokerapi.Service) brokerapi.Service { } } -func convertPlans(plans []pkgbrokerapi.ServicePlan) []brokerapi.ServicePlan { - ret := make([]brokerapi.ServicePlan, len(plans)) +func convertPlans(plans []pkgbrokerapi.ServicePlan) []pivbrokerapi.ServicePlan { + ret := make([]pivbrokerapi.ServicePlan, len(plans)) for i, plan := range plans { - ret[i] = brokerapi.ServicePlan{ + ret[i] = pivbrokerapi.ServicePlan{ ID: plan.ID, Name: plan.Name, Description: plan.Description, From ad5a45f7f537b7d7440064cd25cd086e73cde7d9 Mon Sep 17 00:00:00 2001 From: Aaron Schlesinger Date: Mon, 12 Jun 2017 16:09:24 -0700 Subject: [PATCH 5/6] adding clarifying comment to the catalog requests var --- pkg/brokerapi/fake/server/handler.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/brokerapi/fake/server/handler.go b/pkg/brokerapi/fake/server/handler.go index ac87afcdb24..0e5d131476e 100644 --- a/pkg/brokerapi/fake/server/handler.go +++ b/pkg/brokerapi/fake/server/handler.go @@ -24,7 +24,9 @@ import ( // Handler is a fake implementation oif a brokerapi.ServiceBroker type Handler struct { - Catalog []brokerapi.Service + Catalog []brokerapi.Service + // Since there are no data passed to catalog calls, this is just the number of calls + // that were made to the catalog endpoint CatalogRequests int ProvisionResp brokerapi.ProvisionedServiceSpec From ae9d176d634ada0856f2d6a60fdccdbf13391a20 Mon Sep 17 00:00:00 2001 From: Aaron Schlesinger Date: Mon, 12 Jun 2017 16:14:49 -0700 Subject: [PATCH 6/6] adding better godocs --- pkg/brokerapi/fake/server/handler.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pkg/brokerapi/fake/server/handler.go b/pkg/brokerapi/fake/server/handler.go index 0e5d131476e..6777d6dfa72 100644 --- a/pkg/brokerapi/fake/server/handler.go +++ b/pkg/brokerapi/fake/server/handler.go @@ -22,7 +22,9 @@ import ( "github.com/pivotal-cf/brokerapi" ) -// Handler is a fake implementation oif a brokerapi.ServiceBroker +// Handler is a fake implementation oif a brokerapi.ServiceBroker. It's useful as a mock +// because it has pre-canned response values for use in testing, and also keeps track of calls +// made to it. Handler is not concurrency-safe type Handler struct { Catalog []brokerapi.Service // Since there are no data passed to catalog calls, this is just the number of calls @@ -58,13 +60,14 @@ func NewHandler() *Handler { return &Handler{} } -// Services is the interface implementation of brokerapi.ServiceBroker +// Services increments h.CatalogRequests and returns h.Catalog func (h *Handler) Services(ctx context.Context) []brokerapi.Service { h.CatalogRequests++ return h.Catalog } -// Provision is the interface implementation of brokerapi.ServiceBroker +// Provision adds an element to h.ProvisionRequests and returns +// h.ProvisionResp, h.ProvisionRespError func (h *Handler) Provision( ctx context.Context, instanceID string, @@ -79,7 +82,8 @@ func (h *Handler) Provision( return h.ProvisionResp, h.ProvisionRespError } -// Deprovision is the interface implementation of brokerapi.ServiceBroker +// Deprovision adds an element to h.DeprovisionRequests and returns +// h.DeprovisionResp, h.DeprovisionRespErr func (h *Handler) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { h.DeprovisionRequests = append(h.DeprovisionRequests, DeprovisionRequest{ InstanceID: instanceID, @@ -88,7 +92,7 @@ func (h *Handler) Deprovision(context context.Context, instanceID string, detail return h.DeprovisionResp, h.DeprovisonRespErr } -// Bind is the interface implementation of brokerapi.ServiceBroker +// Bind adds an element to h.BindRequqests and returns h.BindResp, h.BindRespErr func (h *Handler) Bind(context context.Context, instanceID, bindingID string, details brokerapi.BindDetails) (brokerapi.Binding, error) { h.BindRequests = append(h.BindRequests, BindRequest{ InstanceID: instanceID, @@ -98,7 +102,7 @@ func (h *Handler) Bind(context context.Context, instanceID, bindingID string, de return h.BindResp, h.BindRespErr } -// Unbind is the interface implementation of brokerapi.ServiceBroker +// Unbind adds an element to h.UnbindRequests and returns h.UnbindRespErr func (h *Handler) Unbind(context context.Context, instanceID, bindingID string, details brokerapi.UnbindDetails) error { h.UnbindRequests = append(h.UnbindRequests, UnbindRequest{ InstanceID: instanceID, @@ -108,7 +112,7 @@ func (h *Handler) Unbind(context context.Context, instanceID, bindingID string, return h.UnbindRespErr } -// Update is the interface implementation of brokerapi.ServiceBroker +// Update adds an element to h.UpdateRequests and returns h.UpdateResp, h.UpdateRespErr func (h *Handler) Update(context context.Context, instanceID string, details brokerapi.UpdateDetails, asyncAllowed bool) (brokerapi.UpdateServiceSpec, error) { h.UpdateRequests = append(h.UpdateRequests, UpdateRequest{ InstanceID: instanceID, @@ -118,7 +122,8 @@ func (h *Handler) Update(context context.Context, instanceID string, details bro return h.UpdateResp, h.UpdateRespErr } -// LastOperation is the interface implementation of brokerapi.ServiceBroker +// LastOperation adds an element to h.LastOperationRequests and returns +// h.LastOperationResp, h.LastOperationRespErr func (h *Handler) LastOperation(context context.Context, instanceID, operationData string) (brokerapi.LastOperation, error) { h.LastOperationRequests = append(h.LastOperationRequests, LastOperationRequest{ InstanceID: instanceID,