Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
kirederik authored Jan 7, 2019
2 parents e2a818b + 644cda4 commit d017e77
Show file tree
Hide file tree
Showing 12 changed files with 1,170 additions and 82 deletions.
29 changes: 19 additions & 10 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.0"
version = "^1.6.1"

[[constraint]]
name = "github.com/onsi/ginkgo"
Expand Down
50 changes: 41 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,65 @@
# brokerapi

[![Build Status](https://travis-ci.org/pivotal-cf/brokerapi.svg?branch=master)](https://travis-ci.org/pivotal-cf/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 Open Service Broker API](https://github.com/openservicebrokerapi/servicebroker/) compliant Service Brokers.
A Go package for building [V2 Open Service Broker
API](https://github.com/openservicebrokerapi/servicebroker/) compliant Service
Brokers.

## [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)
- [gorilla/mux v1.6.1+](https://github.com/gorilla/mux)

We use [dep](https://github.com/golang/dep) to manager our dependencies. Use `dep ensure` in order to download the required packages.
We use [dep](https://github.com/golang/dep) to manager our dependencies. Use
`dep ensure` in order to download the required packages.

## 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.
`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).
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).
Note in this case, the Basic Authentication and Originating Identity middleware
will not be set up, so you will have to attach them manually if required.

## 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).
`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.
`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.

## Originating Identity

The request context for every request contains the unparsed
`X-Broker-API-Originating-Identity` header under the key
`originatingIdentityKey`. More details on how the Open Service Broker API
manages request originating identity is available
[here](https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#originating-identity).

## 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.
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.
11 changes: 9 additions & 2 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/pivotal-cf/brokerapi/middlewares/originating_identity_header"
"net/http"
"strconv"

Expand Down Expand Up @@ -62,6 +63,7 @@ const (
invalidServiceID = "invalid-service-id"
invalidPlanID = "invalid-plan-id"
concurrentAccessKey = "get-instance-during-update"
maintenanceInfoConflictKey = "maintenance-info-conflict"
)

var (
Expand All @@ -79,7 +81,12 @@ type BrokerCredentials struct {
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)

authMiddleware := auth.NewWrapper(brokerCredentials.Username, brokerCredentials.Password).Wrap
router.Use(authMiddleware)
router.Use(originating_identity_header.AddToContext)

return router
}

func AttachRoutes(router *mux.Router, serviceBroker ServiceBroker, logger lager.Logger) {
Expand Down Expand Up @@ -790,4 +797,4 @@ func checkBrokerAPIVersionHdr(req *http.Request) (brokerVersion, error) {
return version, errors.New("X-Broker-API-Version Header must be 2.x")
}
return version, nil
}
}
98 changes: 98 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,56 @@ var _ = Describe("Service Broker API", func() {
})
})

Describe("OriginatingIdentityHeader", func(){

var (
fakeServiceBroker *fakes.AutoFakeServiceBroker
req *http.Request
testServer *httptest.Server
)

BeforeEach(func() {
fakeServiceBroker = new(fakes.AutoFakeServiceBroker)
brokerAPI = brokerapi.New(fakeServiceBroker, brokerLogger, credentials)

testServer = httptest.NewServer(brokerAPI)
var err error
req, err = http.NewRequest("GET", testServer.URL+"/v2/catalog", nil)
Expect(err).NotTo(HaveOccurred())
req.Header.Add("X-Broker-API-Version", "2.14")
req.SetBasicAuth(credentials.Username, credentials.Password)
})

AfterEach(func() {
testServer.Close()
})

When("X-Broker-API-Originating-Identity is passed", func(){
It("Adds it to the context", func(){
originatingIdentity := "Originating Identity Name"
req.Header.Add("X-Broker-API-Originating-Identity", originatingIdentity)

_, err := http.DefaultClient.Do(req)
Expect(err).NotTo(HaveOccurred())

Expect(fakeServiceBroker.ServicesCallCount()).To(Equal(1), "Services was not called")
ctx := fakeServiceBroker.ServicesArgsForCall(0)
Expect(ctx.Value("originatingIdentity")).To(Equal(originatingIdentity))

})
})
When("X-Broker-API-Originating-Identity is not passed", func(){
It("Adds empty originatingIdentity to the context", func(){
_, err := http.DefaultClient.Do(req)
Expect(err).NotTo(HaveOccurred())

Expect(fakeServiceBroker.ServicesCallCount()).To(Equal(1), "Services was not called")
ctx := fakeServiceBroker.ServicesArgsForCall(0)
Expect(ctx.Value("originatingIdentity")).To(Equal(""))
})
})
})

Describe("catalog endpoint", func() {
makeCatalogRequest := func(apiVersion string, fail bool) *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
Expand Down Expand Up @@ -348,6 +398,12 @@ var _ = Describe("Service Broker API", func() {
"plan_id": "plan-id",
"organization_guid": "organization-guid",
"space_guid": "space-guid",
"maintenance_info": map[string]interface{}{
"public": map[string]string{
"k8s-version": "0.0.1-alpha2",
},
"private": "just a sha thing",
},
}
})

Expand All @@ -358,6 +414,12 @@ var _ = Describe("Service Broker API", func() {
PlanID: "plan-id",
OrganizationGUID: "organization-guid",
SpaceGUID: "space-guid",
MaintenanceInfo: brokerapi.MaintenanceInfo{
Public: map[string]string{
"k8s-version": "0.0.1-alpha2",
},
Private: "just a sha thing",
},
}))
})

Expand Down Expand Up @@ -630,6 +692,12 @@ var _ = Describe("Service Broker API", func() {
PlanID: "plan-id",
OrganizationGUID: "organization-guid",
SpaceGUID: "space-guid",
MaintenanceInfo: brokerapi.MaintenanceInfo{
Public: map[string]string{
"k8s-version": "0.0.1-alpha2",
},
Private: "just a sha thing",
},
}))

Expect(fakeServiceBroker.ProvisionedInstanceIDs).To(ContainElement(instanceID))
Expand Down Expand Up @@ -830,6 +898,12 @@ var _ = Describe("Service Broker API", func() {
"context": map[string]interface{}{
"new-context": "new-context-value",
},
"maintenance_info": map[string]interface{}{
"public": map[string]string{
"k8s-version": "0.0.1-alpha2",
},
"private": "just a sha thing",
},
}
queryString = "?accept_incomplete=true"
})
Expand Down Expand Up @@ -888,6 +962,10 @@ var _ = Describe("Service Broker API", func() {
},
))
Expect(fakeServiceBroker.UpdateDetails.RawParameters).To(Equal(json.RawMessage(`{"new-param":"new-param-value"}`)))
Expect(fakeServiceBroker.UpdateDetails.MaintenanceInfo).To(Equal(brokerapi.MaintenanceInfo{
Public: map[string]string{"k8s-version": "0.0.1-alpha2"},
Private: "just a sha thing"},
))
})

It("calls update with details with raw parameters", func() {
Expand Down Expand Up @@ -2097,5 +2175,25 @@ var _ = Describe("Service Broker API", func() {
})
})
})

Describe("get binding", func() {
It("responds with 500 when the broker fails with an unknown error", func() {
fakeServiceBroker.GetBindingError = errors.New("something failed")

response := makeGetBindingRequestWithSpecificAPIVersion("some-instance", "some-binding", "2.14")
Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
Expect(lastLogLine().Message).To(ContainSubstring("broker-api.getBinding.unknown-error"))
Expect(lastLogLine().Data["error"]).To(ContainSubstring("something failed"))
})

It("returns the appropriate status code when it fails with a known error", func() {
fakeServiceBroker.GetBindingError = brokerapi.NewFailureResponse(errors.New("some error"), http.StatusUnprocessableEntity, "fire")

response := makeGetBindingRequestWithSpecificAPIVersion("some-instance", "some-binding", "2.14")
Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity))
Expect(lastLogLine().Message).To(ContainSubstring("broker-api.getBinding.fire"))
Expect(lastLogLine().Data["error"]).To(ContainSubstring("some error"))
})
})
})
})
42 changes: 25 additions & 17 deletions catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@ import (
)

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"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Bindable bool `json:"bindable"`
InstancesRetrievable bool `json:"instances_retrievable,omitempty"`
BindingsRetrievable bool `json:"bindings_retrievable,omitempty"`
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 {
Expand All @@ -43,13 +45,14 @@ type ServiceDashboardClient struct {
}

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"`
Schemas *ServiceSchemas `json:"schemas,omitempty"`
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"`
Schemas *ServiceSchemas `json:"schemas,omitempty"`
MaintenanceInfo *MaintenanceInfo `json:"maintenance_info,omitempty"`
}

type ServiceSchemas struct {
Expand Down Expand Up @@ -93,6 +96,11 @@ type ServiceMetadata struct {
AdditionalMetadata map[string]interface{}
}

type MaintenanceInfo struct {
Public map[string]string `json:"public,omitempty"`
Private string `json:"private,omitempty"`
}

func FreeValue(v bool) *bool {
return &v
}
Expand Down
Loading

0 comments on commit d017e77

Please sign in to comment.