diff --git a/actor/v7action/cloud_controller_client.go b/actor/v7action/cloud_controller_client.go index 01d2a82aa08..dc1cf13c28d 100644 --- a/actor/v7action/cloud_controller_client.go +++ b/actor/v7action/cloud_controller_client.go @@ -36,6 +36,7 @@ type CloudControllerClient interface { CreateRoute(route resources.Route) (resources.Route, ccv3.Warnings, error) CreateRouteBinding(binding resources.RouteBinding) (ccv3.JobURL, ccv3.Warnings, error) CreateServiceBroker(serviceBroker resources.ServiceBroker) (ccv3.JobURL, ccv3.Warnings, error) + CreateServiceCredentialBinding(binding resources.ServiceCredentialBinding) (ccv3.JobURL, ccv3.Warnings, error) CreateServiceInstance(serviceInstance resources.ServiceInstance) (ccv3.JobURL, ccv3.Warnings, error) CreateSecurityGroup(securityGroup resources.SecurityGroup) (resources.SecurityGroup, ccv3.Warnings, error) CreateSpace(space resources.Space) (resources.Space, ccv3.Warnings, error) diff --git a/actor/v7action/service_app_binding.go b/actor/v7action/service_app_binding.go new file mode 100644 index 00000000000..203312a029b --- /dev/null +++ b/actor/v7action/service_app_binding.go @@ -0,0 +1,73 @@ +package v7action + +import ( + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" + "code.cloudfoundry.org/cli/resources" + "code.cloudfoundry.org/cli/types" + "code.cloudfoundry.org/cli/util/railway" +) + +type CreateServiceAppBindingParams struct { + SpaceGUID string + ServiceInstanceName string + AppName string + BindingName string + Parameters types.OptionalObject +} + +func (actor Actor) CreateServiceAppBinding(params CreateServiceAppBindingParams) (chan PollJobEvent, Warnings, error) { + var ( + serviceInstance resources.ServiceInstance + app resources.Application + jobURL ccv3.JobURL + stream chan PollJobEvent + ) + + warnings, err := railway.Sequentially( + func() (warnings ccv3.Warnings, err error) { + serviceInstance, _, warnings, err = actor.getServiceInstanceByNameAndSpace(params.ServiceInstanceName, params.SpaceGUID) + return + }, + func() (warnings ccv3.Warnings, err error) { + app, warnings, err = actor.CloudControllerClient.GetApplicationByNameAndSpace(params.AppName, params.SpaceGUID) + return + }, + func() (warnings ccv3.Warnings, err error) { + jobURL, warnings, err = actor.createServiceAppBinding(serviceInstance.GUID, app.GUID, params.BindingName, params.Parameters) + return + }, + func() (warnings ccv3.Warnings, err error) { + stream = actor.PollJobToEventStream(jobURL) + return + }, + ) + + switch err.(type) { + case nil: + return stream, Warnings(warnings), nil + case ccerror.ApplicationNotFoundError: + return nil, Warnings(warnings), actionerror.ApplicationNotFoundError{Name: params.AppName} + default: + return nil, Warnings(warnings), err + } +} + +func (actor Actor) createServiceAppBinding(serviceInstanceGUID, appGUID, bindingName string, parameters types.OptionalObject) (ccv3.JobURL, ccv3.Warnings, error) { + jobURL, warnings, err := actor.CloudControllerClient.CreateServiceCredentialBinding(resources.ServiceCredentialBinding{ + Type: resources.AppBinding, + Name: bindingName, + ServiceInstanceGUID: serviceInstanceGUID, + AppGUID: appGUID, + Parameters: parameters, + }) + switch err.(type) { + case nil: + return jobURL, warnings, nil + case ccerror.ResourceAlreadyExistsError: + return "", warnings, actionerror.ResourceAlreadyExistsError{Message: err.Error()} + default: + return "", warnings, err + } +} diff --git a/actor/v7action/service_app_binding_test.go b/actor/v7action/service_app_binding_test.go new file mode 100644 index 00000000000..43442399b51 --- /dev/null +++ b/actor/v7action/service_app_binding_test.go @@ -0,0 +1,250 @@ +package v7action_test + +import ( + "errors" + + "code.cloudfoundry.org/cli/actor/actionerror" + . "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/actor/v7action/v7actionfakes" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" + "code.cloudfoundry.org/cli/resources" + "code.cloudfoundry.org/cli/types" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Service App Binding Action", func() { + var ( + actor *Actor + fakeCloudControllerClient *v7actionfakes.FakeCloudControllerClient + ) + + BeforeEach(func() { + fakeCloudControllerClient = new(v7actionfakes.FakeCloudControllerClient) + actor = NewActor(fakeCloudControllerClient, nil, nil, nil, nil, nil) + }) + + Describe("CreateServiceAppBinding", func() { + const ( + serviceInstanceName = "fake-service-instance-name" + serviceInstanceGUID = "fake-service-instance-guid" + appName = "fake-app-name" + appGUID = "fake-app-guid" + bindingName = "fake-binding-name" + spaceGUID = "fake-space-guid" + fakeJobURL = ccv3.JobURL("fake-job-url") + ) + + var ( + params CreateServiceAppBindingParams + warnings Warnings + executionError error + stream chan PollJobEvent + ) + + BeforeEach(func() { + fakeCloudControllerClient.GetServiceInstanceByNameAndSpaceReturns( + resources.ServiceInstance{ + Name: serviceInstanceName, + GUID: serviceInstanceGUID, + }, + ccv3.IncludedResources{}, + ccv3.Warnings{"get instance warning"}, + nil, + ) + + fakeCloudControllerClient.GetApplicationByNameAndSpaceReturns( + resources.Application{ + GUID: appGUID, + Name: appName, + }, + ccv3.Warnings{"get app warning"}, + nil, + ) + + fakeCloudControllerClient.CreateServiceCredentialBindingReturns( + fakeJobURL, + ccv3.Warnings{"create binding warning"}, + nil, + ) + + fakeStream := make(chan ccv3.PollJobEvent) + fakeCloudControllerClient.PollJobToEventStreamReturns(fakeStream) + go func() { + fakeStream <- ccv3.PollJobEvent{ + State: constant.JobPolling, + Warnings: ccv3.Warnings{"poll warning"}, + } + }() + + params = CreateServiceAppBindingParams{ + SpaceGUID: spaceGUID, + ServiceInstanceName: serviceInstanceName, + AppName: appName, + BindingName: bindingName, + Parameters: types.NewOptionalObject(map[string]interface{}{ + "foo": "bar", + }), + } + }) + + JustBeforeEach(func() { + stream, warnings, executionError = actor.CreateServiceAppBinding(params) + }) + + It("returns an event stream, warnings, and no errors", func() { + Expect(executionError).NotTo(HaveOccurred()) + + Expect(warnings).To(ConsistOf(Warnings{ + "get instance warning", + "get app warning", + "create binding warning", + })) + + Eventually(stream).Should(Receive(Equal(PollJobEvent{ + State: JobPolling, + Warnings: Warnings{"poll warning"}, + Err: nil, + }))) + }) + + Describe("service instance lookup", func() { + It("makes the correct call", func() { + Expect(fakeCloudControllerClient.GetServiceInstanceByNameAndSpaceCallCount()).To(Equal(1)) + actualServiceInstanceName, actualSpaceGUID, actualQuery := fakeCloudControllerClient.GetServiceInstanceByNameAndSpaceArgsForCall(0) + Expect(actualServiceInstanceName).To(Equal(serviceInstanceName)) + Expect(actualSpaceGUID).To(Equal(spaceGUID)) + Expect(actualQuery).To(BeEmpty()) + }) + + When("not found", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetServiceInstanceByNameAndSpaceReturns( + resources.ServiceInstance{}, + ccv3.IncludedResources{}, + ccv3.Warnings{"get instance warning"}, + ccerror.ServiceInstanceNotFoundError{Name: serviceInstanceName}, + ) + }) + + It("returns the error and warning", func() { + Expect(warnings).To(ContainElement("get instance warning")) + Expect(executionError).To(MatchError(actionerror.ServiceInstanceNotFoundError{Name: serviceInstanceName})) + }) + }) + + When("fails", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetServiceInstanceByNameAndSpaceReturns( + resources.ServiceInstance{}, + ccv3.IncludedResources{}, + ccv3.Warnings{"get instance warning"}, + errors.New("boof"), + ) + }) + + It("returns the error and warning", func() { + Expect(warnings).To(ContainElement("get instance warning")) + Expect(executionError).To(MatchError("boof")) + }) + }) + }) + + Describe("app lookup", func() { + It("makes the correct call", func() { + Expect(fakeCloudControllerClient.GetApplicationByNameAndSpaceCallCount()).To(Equal(1)) + actualAppName, actualSpaceGUID := fakeCloudControllerClient.GetApplicationByNameAndSpaceArgsForCall(0) + Expect(actualAppName).To(Equal(appName)) + Expect(actualSpaceGUID).To(Equal(spaceGUID)) + }) + + When("not found", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetApplicationByNameAndSpaceReturns( + resources.Application{}, + ccv3.Warnings{"get app warning"}, + ccerror.ApplicationNotFoundError{Name: appName}, + ) + }) + + It("returns the error and warning", func() { + Expect(warnings).To(ContainElement("get app warning")) + Expect(executionError).To(MatchError(actionerror.ApplicationNotFoundError{Name: appName})) + }) + }) + + When("fails", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetApplicationByNameAndSpaceReturns( + resources.Application{}, + ccv3.Warnings{"get app warning"}, + errors.New("boom"), + ) + }) + + It("returns the error and warning", func() { + Expect(warnings).To(ContainElement("get app warning")) + Expect(executionError).To(MatchError("boom")) + }) + }) + }) + + Describe("initiating the create", func() { + It("makes the correct call", func() { + Expect(fakeCloudControllerClient.CreateServiceCredentialBindingCallCount()).To(Equal(1)) + Expect(fakeCloudControllerClient.CreateServiceCredentialBindingArgsForCall(0)).To(Equal(resources.ServiceCredentialBinding{ + Type: resources.AppBinding, + Name: bindingName, + ServiceInstanceGUID: serviceInstanceGUID, + AppGUID: appGUID, + Parameters: types.NewOptionalObject(map[string]interface{}{ + "foo": "bar", + }), + })) + }) + + When("binding already exists", func() { + BeforeEach(func() { + fakeCloudControllerClient.CreateServiceCredentialBindingReturns( + "", + ccv3.Warnings{"create binding warning"}, + ccerror.ResourceAlreadyExistsError{ + Message: "The app is already bound to the service instance", + }, + ) + }) + + It("returns an actionerror and warnings", func() { + Expect(warnings).To(ContainElement("create binding warning")) + Expect(executionError).To(MatchError(actionerror.ResourceAlreadyExistsError{ + Message: "The app is already bound to the service instance", + })) + }) + }) + + When("fails", func() { + BeforeEach(func() { + fakeCloudControllerClient.CreateServiceCredentialBindingReturns( + "", + ccv3.Warnings{"create binding warning"}, + errors.New("boop"), + ) + }) + + It("returns the error and warnings", func() { + Expect(warnings).To(ContainElement("create binding warning")) + Expect(executionError).To(MatchError("boop")) + }) + }) + }) + + Describe("polling the job", func() { + It("polls the job", func() { + Expect(fakeCloudControllerClient.PollJobToEventStreamCallCount()).To(Equal(1)) + Expect(fakeCloudControllerClient.PollJobToEventStreamArgsForCall(0)).To(Equal(fakeJobURL)) + }) + }) + }) +}) diff --git a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go index 555c53e0e36..cc73b9b4413 100644 --- a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go +++ b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go @@ -367,6 +367,21 @@ type FakeCloudControllerClient struct { result2 ccv3.Warnings result3 error } + CreateServiceCredentialBindingStub func(resources.ServiceCredentialBinding) (ccv3.JobURL, ccv3.Warnings, error) + createServiceCredentialBindingMutex sync.RWMutex + createServiceCredentialBindingArgsForCall []struct { + arg1 resources.ServiceCredentialBinding + } + createServiceCredentialBindingReturns struct { + result1 ccv3.JobURL + result2 ccv3.Warnings + result3 error + } + createServiceCredentialBindingReturnsOnCall map[int]struct { + result1 ccv3.JobURL + result2 ccv3.Warnings + result3 error + } CreateServiceInstanceStub func(resources.ServiceInstance) (ccv3.JobURL, ccv3.Warnings, error) createServiceInstanceMutex sync.RWMutex createServiceInstanceArgsForCall []struct { @@ -4148,6 +4163,72 @@ func (fake *FakeCloudControllerClient) CreateServiceBrokerReturnsOnCall(i int, r }{result1, result2, result3} } +func (fake *FakeCloudControllerClient) CreateServiceCredentialBinding(arg1 resources.ServiceCredentialBinding) (ccv3.JobURL, ccv3.Warnings, error) { + fake.createServiceCredentialBindingMutex.Lock() + ret, specificReturn := fake.createServiceCredentialBindingReturnsOnCall[len(fake.createServiceCredentialBindingArgsForCall)] + fake.createServiceCredentialBindingArgsForCall = append(fake.createServiceCredentialBindingArgsForCall, struct { + arg1 resources.ServiceCredentialBinding + }{arg1}) + fake.recordInvocation("CreateServiceCredentialBinding", []interface{}{arg1}) + fake.createServiceCredentialBindingMutex.Unlock() + if fake.CreateServiceCredentialBindingStub != nil { + return fake.CreateServiceCredentialBindingStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.createServiceCredentialBindingReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeCloudControllerClient) CreateServiceCredentialBindingCallCount() int { + fake.createServiceCredentialBindingMutex.RLock() + defer fake.createServiceCredentialBindingMutex.RUnlock() + return len(fake.createServiceCredentialBindingArgsForCall) +} + +func (fake *FakeCloudControllerClient) CreateServiceCredentialBindingCalls(stub func(resources.ServiceCredentialBinding) (ccv3.JobURL, ccv3.Warnings, error)) { + fake.createServiceCredentialBindingMutex.Lock() + defer fake.createServiceCredentialBindingMutex.Unlock() + fake.CreateServiceCredentialBindingStub = stub +} + +func (fake *FakeCloudControllerClient) CreateServiceCredentialBindingArgsForCall(i int) resources.ServiceCredentialBinding { + fake.createServiceCredentialBindingMutex.RLock() + defer fake.createServiceCredentialBindingMutex.RUnlock() + argsForCall := fake.createServiceCredentialBindingArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeCloudControllerClient) CreateServiceCredentialBindingReturns(result1 ccv3.JobURL, result2 ccv3.Warnings, result3 error) { + fake.createServiceCredentialBindingMutex.Lock() + defer fake.createServiceCredentialBindingMutex.Unlock() + fake.CreateServiceCredentialBindingStub = nil + fake.createServiceCredentialBindingReturns = struct { + result1 ccv3.JobURL + result2 ccv3.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeCloudControllerClient) CreateServiceCredentialBindingReturnsOnCall(i int, result1 ccv3.JobURL, result2 ccv3.Warnings, result3 error) { + fake.createServiceCredentialBindingMutex.Lock() + defer fake.createServiceCredentialBindingMutex.Unlock() + fake.CreateServiceCredentialBindingStub = nil + if fake.createServiceCredentialBindingReturnsOnCall == nil { + fake.createServiceCredentialBindingReturnsOnCall = make(map[int]struct { + result1 ccv3.JobURL + result2 ccv3.Warnings + result3 error + }) + } + fake.createServiceCredentialBindingReturnsOnCall[i] = struct { + result1 ccv3.JobURL + result2 ccv3.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeCloudControllerClient) CreateServiceInstance(arg1 resources.ServiceInstance) (ccv3.JobURL, ccv3.Warnings, error) { fake.createServiceInstanceMutex.Lock() ret, specificReturn := fake.createServiceInstanceReturnsOnCall[len(fake.createServiceInstanceArgsForCall)] @@ -14036,6 +14117,8 @@ func (fake *FakeCloudControllerClient) Invocations() map[string][][]interface{} defer fake.createSecurityGroupMutex.RUnlock() fake.createServiceBrokerMutex.RLock() defer fake.createServiceBrokerMutex.RUnlock() + fake.createServiceCredentialBindingMutex.RLock() + defer fake.createServiceCredentialBindingMutex.RUnlock() fake.createServiceInstanceMutex.RLock() defer fake.createServiceInstanceMutex.RUnlock() fake.createSpaceMutex.RLock() diff --git a/api/cloudcontroller/ccv3/errors.go b/api/cloudcontroller/ccv3/errors.go index 5b250b31d21..d5e135f6c1d 100644 --- a/api/cloudcontroller/ccv3/errors.go +++ b/api/cloudcontroller/ccv3/errors.go @@ -176,6 +176,9 @@ func handleUnprocessableEntity(errorResponse ccerror.V3Error) error { return ccerror.SecurityGroupNotBound{Message: err.Message} case errorResponse.Title == "CF-ServiceInstanceAlreadyBoundToSameRoute": return ccerror.ResourceAlreadyExistsError{Message: err.Message} + case strings.Contains(errorString, + "The app is already bound to the service instance"): + return ccerror.ResourceAlreadyExistsError{Message: err.Message} default: return err } diff --git a/api/cloudcontroller/ccv3/errors_test.go b/api/cloudcontroller/ccv3/errors_test.go index 3d5ea61c4d9..6c086b04175 100644 --- a/api/cloudcontroller/ccv3/errors_test.go +++ b/api/cloudcontroller/ccv3/errors_test.go @@ -497,6 +497,27 @@ var _ = Describe("Error Wrapper", func() { Expect(makeError).To(MatchError(ccerror.UnprocessableEntityError{Message: "SomeCC Error Message"})) }) }) + + When("a service app binding already exists", func() { + BeforeEach(func() { + serverResponse = ` +{ + "errors": [ + { + "code": 10008, + "detail": "The app is already bound to the service instance", + "title": "CF-UnprocessableEntity" + } + ] +}` + }) + + It("returns an ResourceAlreadyExistsError", func() { + Expect(makeError).To(MatchError(ccerror.ResourceAlreadyExistsError{ + Message: "The app is already bound to the service instance", + })) + }) + }) }) }) diff --git a/api/cloudcontroller/ccv3/internal/api_routes.go b/api/cloudcontroller/ccv3/internal/api_routes.go index e6dbb024860..7bf028f5944 100644 --- a/api/cloudcontroller/ccv3/internal/api_routes.go +++ b/api/cloudcontroller/ccv3/internal/api_routes.go @@ -156,6 +156,7 @@ const ( PostSecurityGroupRequest = "PostSecurityGroup" PostSecurityGroupStagingSpaceRequest = "PostSecurityGroupStagingSpace" PostSecurityGroupRunningSpaceRequest = "PostSecurityGroupRunningSpace" + PostServiceCredentialBindingRequest = "PostServiceCredentialBinding" PostServiceBrokerRequest = "PostServiceBroker" PostServiceInstanceRequest = "PostServiceInstance" PostServiceInstanceRelationshipsSharedSpacesRequest = "PostServiceInstanceRelationshipsSharedSpaces" @@ -283,6 +284,7 @@ var APIRoutes = map[string]Route{ PostServiceBrokerRequest: {Path: "/v3/service_brokers", Method: http.MethodPost}, DeleteServiceBrokerRequest: {Path: "/v3/service_brokers/:service_broker_guid", Method: http.MethodDelete}, PatchServiceBrokerRequest: {Path: "/v3/service_brokers/:service_broker_guid", Method: http.MethodPatch}, + PostServiceCredentialBindingRequest: {Path: "/v3/service_credential_bindings", Method: http.MethodPost}, GetServiceCredentialBindingsRequest: {Path: "/v3/service_credential_bindings", Method: http.MethodGet}, GetServiceCredentialBindingDetailsRequest: {Path: "/v3/service_credential_bindings/:service_credential_binding_guid/details", Method: http.MethodGet}, GetServiceInstancesRequest: {Path: "/v3/service_instances", Method: http.MethodGet}, diff --git a/api/cloudcontroller/ccv3/service_credential_binding.go b/api/cloudcontroller/ccv3/service_credential_binding.go index 68b774e7b9a..aad67f6a86d 100644 --- a/api/cloudcontroller/ccv3/service_credential_binding.go +++ b/api/cloudcontroller/ccv3/service_credential_binding.go @@ -6,6 +6,13 @@ import ( "code.cloudfoundry.org/cli/util/lookuptable" ) +func (client *Client) CreateServiceCredentialBinding(binding resources.ServiceCredentialBinding) (JobURL, Warnings, error) { + return client.MakeRequest(RequestParams{ + RequestName: internal.PostServiceCredentialBindingRequest, + RequestBody: binding, + }) +} + // GetServiceCredentialBindings queries the CC API with the specified query // and returns a slice of ServiceCredentialBindings. Additionally if Apps are // included in the API response (by having `include=app` in the query) then the diff --git a/api/cloudcontroller/ccv3/service_credential_binding_test.go b/api/cloudcontroller/ccv3/service_credential_binding_test.go index fd693d01f44..52a67d93194 100644 --- a/api/cloudcontroller/ccv3/service_credential_binding_test.go +++ b/api/cloudcontroller/ccv3/service_credential_binding_test.go @@ -2,6 +2,7 @@ package ccv3_test import ( "encoding/json" + "errors" "fmt" "net/http" @@ -26,6 +27,43 @@ var _ = Describe("Service Instance", func() { client, _ = NewFakeRequesterTestClient(requester) }) + Describe("CreateServiceCredentialBinding", func() { + When("the request succeeds", func() { + It("returns warnings and no errors", func() { + requester.MakeRequestReturns("fake-job-url", Warnings{"fake-warning"}, nil) + + binding := resources.ServiceCredentialBinding{ + ServiceInstanceGUID: "fake-service-instance-guid", + AppGUID: "fake-app-guid", + } + + jobURL, warnings, err := client.CreateServiceCredentialBinding(binding) + + Expect(jobURL).To(Equal(JobURL("fake-job-url"))) + Expect(warnings).To(ConsistOf("fake-warning")) + Expect(err).NotTo(HaveOccurred()) + + Expect(requester.MakeRequestCallCount()).To(Equal(1)) + Expect(requester.MakeRequestArgsForCall(0)).To(Equal(RequestParams{ + RequestName: internal.PostServiceCredentialBindingRequest, + RequestBody: binding, + })) + }) + }) + + When("the request fails", func() { + It("returns errors and warnings", func() { + requester.MakeRequestReturns("", Warnings{"fake-warning"}, errors.New("bang")) + + jobURL, warnings, err := client.CreateServiceCredentialBinding(resources.ServiceCredentialBinding{}) + + Expect(jobURL).To(BeEmpty()) + Expect(warnings).To(ConsistOf("fake-warning")) + Expect(err).To(MatchError("bang")) + }) + }) + }) + Describe("GetServiceCredentialBindings", func() { var ( query []Query diff --git a/command/common/command_list_v7.go b/command/common/command_list_v7.go index 73d49985cdd..a89eeb9232b 100644 --- a/command/common/command_list_v7.go +++ b/command/common/command_list_v7.go @@ -27,7 +27,7 @@ type commandList struct { BindRouteService v7.BindRouteServiceCommand `command:"bind-route-service" alias:"brs" description:"Bind a service instance to an HTTP route"` BindRunningSecurityGroup v7.BindRunningSecurityGroupCommand `command:"bind-running-security-group" description:"Bind a security group to the list of security groups to be used for running applications"` BindSecurityGroup v7.BindSecurityGroupCommand `command:"bind-security-group" description:"Bind a security group to a particular space, or all existing spaces of an org"` - BindService v6.BindServiceCommand `command:"bind-service" alias:"bs" description:"Bind a service instance to an app"` + BindService v7.BindServiceCommand `command:"bind-service" alias:"bs" description:"Bind a service instance to an app"` BindStagingSecurityGroup v7.BindStagingSecurityGroupCommand `command:"bind-staging-security-group" description:"Bind a security group to the list of security groups to be used for staging applications globally"` Buildpacks v7.BuildpacksCommand `command:"buildpacks" description:"List all buildpacks"` CancelDeployment v7.CancelDeploymentCommand `command:"cancel-deployment" description:"Cancel the most recent deployment for an app. Resets the current droplet to the previous deployment's droplet."` diff --git a/command/v7/actor.go b/command/v7/actor.go index fa2f58b8630..d7e5c29435d 100644 --- a/command/v7/actor.go +++ b/command/v7/actor.go @@ -47,6 +47,7 @@ type Actor interface { CreateRoute(spaceGUID, domainName, hostname, path string, port int) (resources.Route, v7action.Warnings, error) CreateRouteBinding(params v7action.CreateRouteBindingParams) (chan v7action.PollJobEvent, v7action.Warnings, error) CreateSecurityGroup(name, filePath string) (v7action.Warnings, error) + CreateServiceAppBinding(params v7action.CreateServiceAppBindingParams) (chan v7action.PollJobEvent, v7action.Warnings, error) CreateServiceBroker(model resources.ServiceBroker) (v7action.Warnings, error) CreateSharedDomain(domainName string, internal bool, routerGroupName string) (v7action.Warnings, error) CreateSpace(spaceName, orgGUID string) (resources.Space, v7action.Warnings, error) diff --git a/command/v7/bind_service_command.go b/command/v7/bind_service_command.go new file mode 100644 index 00000000000..d1529df0a3d --- /dev/null +++ b/command/v7/bind_service_command.go @@ -0,0 +1,128 @@ +package v7 + +import ( + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/command/v7/shared" + "code.cloudfoundry.org/cli/types" +) + +type BindServiceCommand struct { + BaseCommand + + RequiredArgs flag.BindServiceArgs `positional-args:"yes"` + BindingName flag.BindingName `long:"binding-name" description:"Name to expose service instance to app process with (Default: service instance name)"` + ParametersAsJSON flag.JSONOrFileWithValidation `short:"c" description:"Valid JSON object containing service-specific configuration parameters, provided either in-line or in a file. For a list of supported configuration parameters, see documentation for the particular service offering."` + Wait bool `short:"w" long:"wait" description:"Wait for the bind operation to complete"` + relatedCommands interface{} `related_commands:"services"` +} + +func (cmd BindServiceCommand) Execute(args []string) error { + if err := cmd.SharedActor.CheckTarget(true, true); err != nil { + return err + } + + if err := cmd.displayIntro(); err != nil { + return err + } + + stream, warnings, err := cmd.Actor.CreateServiceAppBinding(v7action.CreateServiceAppBindingParams{ + SpaceGUID: cmd.Config.TargetedSpace().GUID, + ServiceInstanceName: cmd.RequiredArgs.ServiceInstanceName, + AppName: cmd.RequiredArgs.AppName, + BindingName: cmd.BindingName.Value, + Parameters: types.OptionalObject(cmd.ParametersAsJSON), + }) + cmd.UI.DisplayWarnings(warnings) + + switch err.(type) { + case nil: + case actionerror.ResourceAlreadyExistsError: + cmd.UI.DisplayText("App {{.AppName}} is already bound to service instance {{.ServiceInstanceName}}.", cmd.names()) + cmd.UI.DisplayOK() + return nil + default: + return err + } + + completed, err := shared.WaitForResult(stream, cmd.UI, cmd.Wait) + switch { + case err != nil: + return err + case completed: + cmd.UI.DisplayOK() + cmd.UI.DisplayText("TIP: Use 'cf restage {{.AppName}}' to ensure your env variable changes take effect", cmd.names()) + return nil + default: + cmd.UI.DisplayOK() + cmd.UI.DisplayText("Binding in progress. Use 'cf service {{.ServiceInstanceName}}' to check operation status.", cmd.names()) + cmd.UI.DisplayNewline() + cmd.UI.DisplayText("TIP: Once this operation succeeds, use 'cf restage {{.AppName}}' to ensure your env variable changes take effect", cmd.names()) + return nil + } +} + +func (cmd BindServiceCommand) Usage() string { + return `CF_NAME bind-service APP_NAME SERVICE_INSTANCE [-c PARAMETERS_AS_JSON] [--binding-name BINDING_NAME] + +Optionally provide service-specific configuration parameters in a valid JSON object in-line: + +CF_NAME bind-service APP_NAME SERVICE_INSTANCE -c '{"name":"value","name":"value"}' + +Optionally provide a file containing service-specific configuration parameters in a valid JSON object. +The path to the parameters file can be an absolute or relative path to a file. + +CF_NAME bind-service APP_NAME SERVICE_INSTANCE -c PATH_TO_FILE + +Example of valid JSON object: +{ + "permissions": "read-only" +} + +Optionally provide a binding name for the association between an app and a service instance: + +CF_NAME bind-service APP_NAME SERVICE_INSTANCE --binding-name BINDING_NAME` +} + +func (cmd BindServiceCommand) Examples() string { + return ` +Linux/Mac: + CF_NAME bind-service myapp mydb -c '{"permissions":"read-only"}' + +Windows Command Line: + CF_NAME bind-service myapp mydb -c "{\"permissions\":\"read-only\"}" + +Windows PowerShell: + CF_NAME bind-service myapp mydb -c '{\"permissions\":\"read-only\"}' + +CF_NAME bind-service myapp mydb -c ~/workspace/tmp/instance_config.json --binding-name BINDING_NAME +` +} + +func (cmd BindServiceCommand) displayIntro() error { + user, err := cmd.Config.CurrentUser() + if err != nil { + return err + } + + cmd.UI.DisplayTextWithFlavor( + "Binding service instance {{.ServiceInstance}} to app {{.AppName}} in org {{.Org}} / space {{.Space}} as {{.User}}...", + map[string]interface{}{ + "ServiceInstance": cmd.RequiredArgs.ServiceInstanceName, + "AppName": cmd.RequiredArgs.AppName, + "User": user.Name, + "Space": cmd.Config.TargetedSpace().Name, + "Org": cmd.Config.TargetedOrganization().Name, + }, + ) + + return nil +} + +func (cmd BindServiceCommand) names() map[string]interface{} { + return map[string]interface{}{ + "ServiceInstanceName": cmd.RequiredArgs.ServiceInstanceName, + "AppName": cmd.RequiredArgs.AppName, + } +} diff --git a/command/v7/bind_service_command_test.go b/command/v7/bind_service_command_test.go new file mode 100644 index 00000000000..e2ec55dac4c --- /dev/null +++ b/command/v7/bind_service_command_test.go @@ -0,0 +1,333 @@ +package v7_test + +import ( + "errors" + + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/command/commandfakes" + v7 "code.cloudfoundry.org/cli/command/v7" + "code.cloudfoundry.org/cli/command/v7/v7fakes" + "code.cloudfoundry.org/cli/types" + "code.cloudfoundry.org/cli/util/configv3" + "code.cloudfoundry.org/cli/util/ui" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("bind-service Command", func() { + var ( + cmd v7.BindServiceCommand + testUI *ui.UI + fakeConfig *commandfakes.FakeConfig + fakeSharedActor *commandfakes.FakeSharedActor + executeErr error + fakeActor *v7fakes.FakeActor + ) + + const ( + fakeUserName = "fake-user-name" + fakeServiceInstanceName = "fake-service-instance-name" + fakeBindingName = "fake-binding-name" + fakeAppName = "fake-app-name" + fakeOrgName = "fake-org-name" + fakeSpaceName = "fake-space-name" + fakeSpaceGUID = "fake-space-guid" + ) + + BeforeEach(func() { + testUI = ui.NewTestUI(NewBuffer(), NewBuffer(), NewBuffer()) + fakeConfig = new(commandfakes.FakeConfig) + fakeSharedActor = new(commandfakes.FakeSharedActor) + fakeActor = new(v7fakes.FakeActor) + + cmd = v7.BindServiceCommand{ + BaseCommand: v7.BaseCommand{ + UI: testUI, + Config: fakeConfig, + SharedActor: fakeSharedActor, + Actor: fakeActor, + }, + } + + fakeConfig.TargetedSpaceReturns(configv3.Space{ + Name: fakeSpaceName, + GUID: fakeSpaceGUID, + }) + + fakeConfig.TargetedOrganizationReturns(configv3.Organization{Name: fakeOrgName}) + + fakeConfig.CurrentUserReturns(configv3.User{Name: fakeUserName}, nil) + + fakeActor.CreateServiceAppBindingReturns( + nil, + v7action.Warnings{"fake warning"}, + nil, + ) + + setPositionalFlags(&cmd, fakeAppName, fakeServiceInstanceName) + setFlag(&cmd, "--binding-name", fakeBindingName) + setFlag(&cmd, "-c", `{"foo": "bar"}`) + }) + + JustBeforeEach(func() { + executeErr = cmd.Execute(nil) + }) + + It("checks the user is logged in, and targeting an org and space", func() { + Expect(executeErr).NotTo(HaveOccurred()) + + Expect(fakeSharedActor.CheckTargetCallCount()).To(Equal(1)) + actualOrg, actualSpace := fakeSharedActor.CheckTargetArgsForCall(0) + Expect(actualOrg).To(BeTrue()) + Expect(actualSpace).To(BeTrue()) + }) + + It("delegates to the actor", func() { + Expect(fakeActor.CreateServiceAppBindingCallCount()).To(Equal(1)) + Expect(fakeActor.CreateServiceAppBindingArgsForCall(0)).To(Equal(v7action.CreateServiceAppBindingParams{ + SpaceGUID: fakeSpaceGUID, + ServiceInstanceName: fakeServiceInstanceName, + AppName: fakeAppName, + BindingName: fakeBindingName, + Parameters: types.NewOptionalObject(map[string]interface{}{"foo": "bar"}), + })) + }) + + Describe("intro message", func() { + It("prints an intro and warnings", func() { + Expect(executeErr).NotTo(HaveOccurred()) + Expect(testUI.Err).To(Say("fake warning")) + Expect(testUI.Out).To(Say( + `Binding service instance %s to app %s in org %s / space %s as %s\.\.\.\n`, + fakeServiceInstanceName, + fakeAppName, + fakeOrgName, + fakeSpaceName, + fakeUserName, + )) + }) + }) + + When("binding already exists", func() { + BeforeEach(func() { + fakeActor.CreateServiceAppBindingReturns( + nil, + v7action.Warnings{"fake warning"}, + actionerror.ResourceAlreadyExistsError{}, + ) + }) + + It("prints a message and warnings", func() { + Expect(testUI.Out).To(SatisfyAll( + Say( + `App %s is already bound to service instance %s\.\n`, + fakeAppName, + fakeServiceInstanceName, + ), + Say(`OK\n`), + )) + + Expect(testUI.Err).To(Say("fake warning")) + }) + }) + + Describe("processing the response stream", func() { + Context("nil stream", func() { + It("prints a message and warnings", func() { + Expect(testUI.Out).To(SatisfyAll( + Say(`OK\n`), + Say(`\n`), + Say(`TIP: Use 'cf restage %s' to ensure your env variable changes take effect\n`, fakeAppName), + )) + + Expect(testUI.Err).To(Say("fake warning")) + }) + }) + + Context("stream goes to complete", func() { + BeforeEach(func() { + eventStream := make(chan v7action.PollJobEvent) + go func() { + eventStream <- v7action.PollJobEvent{ + State: v7action.JobProcessing, + Warnings: v7action.Warnings{"job processing warning"}, + } + eventStream <- v7action.PollJobEvent{ + State: v7action.JobComplete, + Warnings: v7action.Warnings{"job complete warning"}, + } + close(eventStream) + }() + + fakeActor.CreateServiceAppBindingReturns( + eventStream, + v7action.Warnings{"fake warning"}, + nil, + ) + }) + + It("prints a message and warnings", func() { + Expect(testUI.Out).To(SatisfyAll( + Say(`OK\n`), + Say(`\n`), + Say(`TIP: Use 'cf restage %s' to ensure your env variable changes take effect\n`, fakeAppName), + )) + + Expect(testUI.Err).To(SatisfyAll( + Say("fake warning"), + Say("job processing warning"), + Say("job complete warning"), + )) + }) + }) + + Context("stream goes to polling", func() { + BeforeEach(func() { + eventStream := make(chan v7action.PollJobEvent) + go func() { + eventStream <- v7action.PollJobEvent{ + State: v7action.JobProcessing, + Warnings: v7action.Warnings{"job processing warning"}, + } + eventStream <- v7action.PollJobEvent{ + State: v7action.JobPolling, + Warnings: v7action.Warnings{"job polling warning"}, + } + }() + + fakeActor.CreateServiceAppBindingReturns( + eventStream, + v7action.Warnings{"fake warning"}, + nil, + ) + }) + + It("prints a message and warnings", func() { + Expect(testUI.Out).To(SatisfyAll( + Say(`OK\n`), + Say(`\n`), + Say(`Binding in progress. Use 'cf service %s' to check operation status.\n`, fakeServiceInstanceName), + Say(`\n`), + Say(`TIP: Once this operation succeeds, use 'cf restage %s' to ensure your env variable changes take effect\n`, fakeAppName), + )) + + Expect(testUI.Err).To(SatisfyAll( + Say("fake warning"), + Say("job processing warning"), + Say("job polling warning"), + )) + }) + }) + + Context("stream goes to error", func() { + BeforeEach(func() { + eventStream := make(chan v7action.PollJobEvent) + go func() { + eventStream <- v7action.PollJobEvent{ + State: v7action.JobFailed, + Warnings: v7action.Warnings{"job failed warning"}, + Err: errors.New("boom"), + } + }() + + fakeActor.CreateServiceAppBindingReturns( + eventStream, + v7action.Warnings{"fake warning"}, + nil, + ) + }) + + It("prints warnings and returns the error", func() { + Expect(executeErr).To(MatchError("boom")) + + Expect(testUI.Err).To(SatisfyAll( + Say("fake warning"), + Say("job failed warning"), + )) + }) + }) + + When("--wait flag specified", func() { + BeforeEach(func() { + eventStream := make(chan v7action.PollJobEvent) + go func() { + eventStream <- v7action.PollJobEvent{ + State: v7action.JobProcessing, + Warnings: v7action.Warnings{"job processing warning"}, + } + eventStream <- v7action.PollJobEvent{ + State: v7action.JobPolling, + Warnings: v7action.Warnings{"job polling warning"}, + } + eventStream <- v7action.PollJobEvent{ + State: v7action.JobComplete, + Warnings: v7action.Warnings{"job complete warning"}, + } + close(eventStream) + }() + + fakeActor.CreateServiceAppBindingReturns( + eventStream, + v7action.Warnings{"fake warning"}, + nil, + ) + + setFlag(&cmd, "--wait") + }) + + It("waits for the event stream to complete", func() { + Expect(testUI.Out).To(SatisfyAll( + Say(`Waiting for the operation to complete\.\.\.\n`), + Say(`\n`), + Say(`OK\n`), + Say(`\n`), + Say(`TIP: Use 'cf restage %s' to ensure your env variable changes take effect\n`, fakeAppName), + )) + + Expect(testUI.Err).To(SatisfyAll( + Say("fake warning"), + Say("job processing warning"), + Say("job polling warning"), + Say("job complete warning"), + )) + }) + }) + }) + + When("checking the target returns an error", func() { + BeforeEach(func() { + fakeSharedActor.CheckTargetReturns(errors.New("explode")) + }) + + It("returns the error", func() { + Expect(executeErr).To(MatchError("explode")) + }) + }) + + When("actor returns error", func() { + BeforeEach(func() { + fakeActor.CreateServiceAppBindingReturns( + nil, + v7action.Warnings{"fake warning"}, + errors.New("boom"), + ) + }) + + It("prints warnings and returns the error", func() { + Expect(testUI.Err).To(Say("fake warning")) + Expect(executeErr).To(MatchError("boom")) + }) + }) + + When("getting the username returns an error", func() { + BeforeEach(func() { + fakeConfig.CurrentUserReturns(configv3.User{}, errors.New("bad thing")) + }) + + It("returns the error", func() { + Expect(executeErr).To(MatchError("bad thing")) + }) + }) +}) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 6af47546927..66c6466ecb7 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -422,6 +422,21 @@ type FakeActor struct { result1 v7action.Warnings result2 error } + CreateServiceAppBindingStub func(v7action.CreateServiceAppBindingParams) (chan v7action.PollJobEvent, v7action.Warnings, error) + createServiceAppBindingMutex sync.RWMutex + createServiceAppBindingArgsForCall []struct { + arg1 v7action.CreateServiceAppBindingParams + } + createServiceAppBindingReturns struct { + result1 chan v7action.PollJobEvent + result2 v7action.Warnings + result3 error + } + createServiceAppBindingReturnsOnCall map[int]struct { + result1 chan v7action.PollJobEvent + result2 v7action.Warnings + result3 error + } CreateServiceBrokerStub func(resources.ServiceBroker) (v7action.Warnings, error) createServiceBrokerMutex sync.RWMutex createServiceBrokerArgsForCall []struct { @@ -5179,6 +5194,72 @@ func (fake *FakeActor) CreateSecurityGroupReturnsOnCall(i int, result1 v7action. }{result1, result2} } +func (fake *FakeActor) CreateServiceAppBinding(arg1 v7action.CreateServiceAppBindingParams) (chan v7action.PollJobEvent, v7action.Warnings, error) { + fake.createServiceAppBindingMutex.Lock() + ret, specificReturn := fake.createServiceAppBindingReturnsOnCall[len(fake.createServiceAppBindingArgsForCall)] + fake.createServiceAppBindingArgsForCall = append(fake.createServiceAppBindingArgsForCall, struct { + arg1 v7action.CreateServiceAppBindingParams + }{arg1}) + fake.recordInvocation("CreateServiceAppBinding", []interface{}{arg1}) + fake.createServiceAppBindingMutex.Unlock() + if fake.CreateServiceAppBindingStub != nil { + return fake.CreateServiceAppBindingStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.createServiceAppBindingReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeActor) CreateServiceAppBindingCallCount() int { + fake.createServiceAppBindingMutex.RLock() + defer fake.createServiceAppBindingMutex.RUnlock() + return len(fake.createServiceAppBindingArgsForCall) +} + +func (fake *FakeActor) CreateServiceAppBindingCalls(stub func(v7action.CreateServiceAppBindingParams) (chan v7action.PollJobEvent, v7action.Warnings, error)) { + fake.createServiceAppBindingMutex.Lock() + defer fake.createServiceAppBindingMutex.Unlock() + fake.CreateServiceAppBindingStub = stub +} + +func (fake *FakeActor) CreateServiceAppBindingArgsForCall(i int) v7action.CreateServiceAppBindingParams { + fake.createServiceAppBindingMutex.RLock() + defer fake.createServiceAppBindingMutex.RUnlock() + argsForCall := fake.createServiceAppBindingArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeActor) CreateServiceAppBindingReturns(result1 chan v7action.PollJobEvent, result2 v7action.Warnings, result3 error) { + fake.createServiceAppBindingMutex.Lock() + defer fake.createServiceAppBindingMutex.Unlock() + fake.CreateServiceAppBindingStub = nil + fake.createServiceAppBindingReturns = struct { + result1 chan v7action.PollJobEvent + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeActor) CreateServiceAppBindingReturnsOnCall(i int, result1 chan v7action.PollJobEvent, result2 v7action.Warnings, result3 error) { + fake.createServiceAppBindingMutex.Lock() + defer fake.createServiceAppBindingMutex.Unlock() + fake.CreateServiceAppBindingStub = nil + if fake.createServiceAppBindingReturnsOnCall == nil { + fake.createServiceAppBindingReturnsOnCall = make(map[int]struct { + result1 chan v7action.PollJobEvent + result2 v7action.Warnings + result3 error + }) + } + fake.createServiceAppBindingReturnsOnCall[i] = struct { + result1 chan v7action.PollJobEvent + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeActor) CreateServiceBroker(arg1 resources.ServiceBroker) (v7action.Warnings, error) { fake.createServiceBrokerMutex.Lock() ret, specificReturn := fake.createServiceBrokerReturnsOnCall[len(fake.createServiceBrokerArgsForCall)] @@ -18255,6 +18336,8 @@ func (fake *FakeActor) Invocations() map[string][][]interface{} { defer fake.createRouteBindingMutex.RUnlock() fake.createSecurityGroupMutex.RLock() defer fake.createSecurityGroupMutex.RUnlock() + fake.createServiceAppBindingMutex.RLock() + defer fake.createServiceAppBindingMutex.RUnlock() fake.createServiceBrokerMutex.RLock() defer fake.createServiceBrokerMutex.RUnlock() fake.createSharedDomainMutex.RLock() diff --git a/integration/v7/isolated/bind_service_command_test.go b/integration/v7/isolated/bind_service_command_test.go index ef059710a4b..355a4cf3fe1 100644 --- a/integration/v7/isolated/bind_service_command_test.go +++ b/integration/v7/isolated/bind_service_command_test.go @@ -1,14 +1,12 @@ package isolated import ( - "io/ioutil" "os" - "path/filepath" "time" - "code.cloudfoundry.org/cli/integration/helpers/servicebrokerstub" - "code.cloudfoundry.org/cli/integration/helpers" + "code.cloudfoundry.org/cli/integration/helpers/servicebrokerstub" + "code.cloudfoundry.org/cli/resources" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gbytes" @@ -16,346 +14,400 @@ import ( ) var _ = Describe("bind-service command", func() { + const command = "bind-service" + Describe("help", func() { - When("--help flag is set", func() { - It("Displays command usage to output", func() { - session := helpers.CF("bind-service", "--help") - Eventually(session).Should(Say("NAME:")) - Eventually(session).Should(Say("bind-service - Bind a service instance to an app")) - - Eventually(session).Should(Say("USAGE:")) - Eventually(session).Should(Say("cf bind-service APP_NAME SERVICE_INSTANCE \\[-c PARAMETERS_AS_JSON\\] \\[--binding-name BINDING_NAME\\]")) - Eventually(session).Should(Say("Optionally provide service-specific configuration parameters in a valid JSON object in-line:")) - Eventually(session).Should(Say("cf bind-service APP_NAME SERVICE_INSTANCE -c '{\"name\":\"value\",\"name\":\"value\"}'")) - Eventually(session).Should(Say("Optionally provide a file containing service-specific configuration parameters in a valid JSON object.")) - Eventually(session).Should(Say("The path to the parameters file can be an absolute or relative path to a file.")) - Eventually(session).Should(Say("cf bind-service APP_NAME SERVICE_INSTANCE -c PATH_TO_FILE")) - Eventually(session).Should(Say("Example of valid JSON object:")) - Eventually(session).Should(Say("{")) - Eventually(session).Should(Say("\"permissions\": \"read-only\"")) - Eventually(session).Should(Say("}")) - Eventually(session).Should(Say("Optionally provide a binding name for the association between an app and a service instance:")) - Eventually(session).Should(Say("cf bind-service APP_NAME SERVICE_INSTANCE --binding-name BINDING_NAME")) - Eventually(session).Should(Say("EXAMPLES:")) - Eventually(session).Should(Say("Linux/Mac:")) - Eventually(session).Should(Say("cf bind-service myapp mydb -c '{\"permissions\":\"read-only\"}'")) - Eventually(session).Should(Say("Windows Command Line:")) - Eventually(session).Should(Say("cf bind-service myapp mydb -c \"{\\\\\"permissions\\\\\":\\\\\"read-only\\\\\"}\"")) - Eventually(session).Should(Say("Windows PowerShell:")) - Eventually(session).Should(Say("cf bind-service myapp mydb -c '{\\\\\"permissions\\\\\":\\\\\"read-only\\\\\"}'")) - Eventually(session).Should(Say("cf bind-service myapp mydb -c ~/workspace/tmp/instance_config.json --binding-name BINDING_NAME")) - Eventually(session).Should(Say("ALIAS:")) - Eventually(session).Should(Say("bs")) - Eventually(session).Should(Say("OPTIONS:")) - Eventually(session).Should(Say("--binding-name\\s+Name to expose service instance to app process with \\(Default: service instance name\\)")) - Eventually(session).Should(Say("-c\\s+Valid JSON object containing service-specific configuration parameters, provided either in-line or in a file. For a list of supported configuration parameters, see documentation for the particular service offering.")) - Eventually(session).Should(Say("SEE ALSO:")) - Eventually(session).Should(Say("services")) + matchHelpMessage := SatisfyAll( + Say(`NAME:\n`), + Say(`\s+bind-service - Bind a service instance to an app\n`), + Say(`\n`), + Say(`USAGE:\n`), + Say(`\s+cf bind-service APP_NAME SERVICE_INSTANCE \[-c PARAMETERS_AS_JSON\] \[--binding-name BINDING_NAME\]\n`), + Say(`\n`), + Say(`\s+Optionally provide service-specific configuration parameters in a valid JSON object in-line:\n`), + Say(`\n`), + Say(`\s+cf bind-service APP_NAME SERVICE_INSTANCE -c '\{"name":"value","name":"value"\}'\n`), + Say(`\n`), + Say(`\s+Optionally provide a file containing service-specific configuration parameters in a valid JSON object.\n`), + Say(`\s+The path to the parameters file can be an absolute or relative path to a file.\n`), + Say(`\s+cf bind-service APP_NAME SERVICE_INSTANCE -c PATH_TO_FILE\n`), + Say(`\n`), + Say(`\s+Example of valid JSON object:\n`), + Say(`\s+{\n`), + Say(`\s+"permissions": "read-only"\n`), + Say(`\s+}\n`), + Say(`\n`), + Say(`\s+Optionally provide a binding name for the association between an app and a service instance:\n`), + Say(`\n`), + Say(`\s+cf bind-service APP_NAME SERVICE_INSTANCE --binding-name BINDING_NAME\n`), + Say(`\n`), + Say(`EXAMPLES:\n`), + Say(`\s+Linux/Mac:\n`), + Say(`\s+cf bind-service myapp mydb -c '\{"permissions":"read-only"\}'\n`), + Say(`\n`), + Say(`\s+Windows Command Line:\n`), + Say(`\s+cf bind-service myapp mydb -c "\{\\"permissions\\":\\"read-only\\"\}"\n`), + Say(`\n`), + Say(`\s+Windows PowerShell:\n`), + Say(`\s+cf bind-service myapp mydb -c '\{\\"permissions\\":\\"read-only\\"\}'\n`), + Say(`\n`), + Say(`\s+cf bind-service myapp mydb -c ~/workspace/tmp/instance_config.json --binding-name BINDING_NAME\n`), + Say(`\n`), + Say(`ALIAS:\n`), + Say(`\s+bs\n`), + Say(`\n`), + Say(`OPTIONS:\n`), + Say(`\s+--binding-name\s+Name to expose service instance to app process with \(Default: service instance name\)\n`), + Say(`\s+-c\s+Valid JSON object containing service-specific configuration parameters, provided either in-line or in a file. For a list of supported configuration parameters, see documentation for the particular service offering.\n`), + Say(`\s+--wait, -w\s+Wait for the bind operation to complete\n`), + Say(`\n`), + Say(`SEE ALSO:\n`), + Say(`\s+services\n`), + ) + + When("the -h flag is specified", func() { + It("succeeds and prints help", func() { + session := helpers.CF(command, "-h") Eventually(session).Should(Exit(0)) + Expect(session.Out).To(matchHelpMessage) }) }) - }) - var ( - serviceInstance string - appName string - ) + When("the --help flag is specified", func() { + It("succeeds and prints help", func() { + session := helpers.CF(command, "--help") + Eventually(session).Should(Exit(0)) + Expect(session.Out).To(matchHelpMessage) + }) + }) - BeforeEach(func() { - serviceInstance = helpers.PrefixedRandomName("si") - appName = helpers.PrefixedRandomName("app") - }) + When("no arguments are provided", func() { + It("displays a warning, the help text, and exits 1", func() { + session := helpers.CF(command) + Eventually(session).Should(Exit(1)) + Expect(session.Err).To(Say("Incorrect Usage: the required arguments `APP_NAME` and `SERVICE_INSTANCE` were not provided")) + Expect(session.Out).To(matchHelpMessage) + }) + }) - When("the environment is not setup correctly", func() { - It("fails with the appropriate errors", func() { - helpers.CheckEnvironmentTargetedCorrectly(true, true, ReadOnlyOrg, "bind-service", "app-name", "service-name") + When("unknown flag is passed", func() { + It("displays a warning, the help text, and exits 1", func() { + session := helpers.CF(command, "-u") + Eventually(session).Should(Exit(1)) + Expect(session.Err).To(Say("Incorrect Usage: unknown flag `u")) + Expect(session.Out).To(matchHelpMessage) + }) }) - }) - When("provided invalid flag values", func() { - When("the --binding-name flag is provided and its value is the empty string", func() { - It("returns an invalid usage error and the help text", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "--binding-name", "") - Eventually(session.Err).Should(Say("--binding-name must be at least 1 character in length")) + When("-c is provided with invalid JSON", func() { + It("displays a warning, the help text, and exits 1", func() { + session := helpers.CF(command, "-c", `{"not":json"}`) + Eventually(session).Should(Exit(1)) + Expect(session.Err).To(Say("Incorrect Usage: Invalid configuration provided for -c flag. Please provide a valid JSON object or path to a file containing a valid JSON object.")) + Expect(session.Out).To(matchHelpMessage) + }) + }) + + When("-c is provided with invalid JSON file", func() { + It("displays a warning, the help text, and exits 1", func() { + filename := helpers.TempFileWithContent(`{"not":json"}`) + defer os.Remove(filename) + + session := helpers.CF(command, "-c", filename) + Eventually(session).Should(Exit(1)) + Expect(session.Err).To(Say("Incorrect Usage: Invalid configuration provided for -c flag. Please provide a valid JSON object or path to a file containing a valid JSON object.")) + Expect(session.Out).To(matchHelpMessage) + }) + }) - Eventually(session).Should(Say("NAME:")) + When("--binding-name is provided with empty value", func() { + It("displays a warning, the help text, and exits 1", func() { + session := helpers.CF(command, "appName", "serviceInstanceName", "--binding-name", "") Eventually(session).Should(Exit(1)) + Expect(session.Err).To(Say("Incorrect Usage: --binding-name must be at least 1 character in length")) + Expect(session.Out).To(matchHelpMessage) }) }) }) - When("the environment is setup correctly", func() { + When("the environment is not setup correctly", func() { + It("fails with the appropriate errors", func() { + helpers.CheckEnvironmentTargetedCorrectly(true, true, ReadOnlyOrg, "bind-service", "app-name", "service-name") + }) + }) + + When("targeting a space", func() { var ( - org string - space string - username string + orgName string + spaceName string + username string ) + getBinding := func(serviceInstanceName string) resources.ServiceCredentialBinding { + var receiver struct { + Resources []resources.ServiceCredentialBinding `json:"resources"` + } + helpers.Curl(&receiver, "/v3/service_credential_bindings?service_instance_names=%s", serviceInstanceName) + Expect(receiver.Resources).To(HaveLen(1)) + return receiver.Resources[0] + } + + getParameters := func(serviceInstanceName string) (receiver map[string]interface{}) { + binding := getBinding(serviceInstanceName) + helpers.Curl(&receiver, "/v3/service_credential_bindings/%s/parameters", binding.GUID) + return + } + BeforeEach(func() { - org = helpers.NewOrgName() - space = helpers.NewSpaceName() - username, _ = helpers.GetCredentials() + orgName = helpers.NewOrgName() + spaceName = helpers.NewSpaceName() + helpers.SetupCF(orgName, spaceName) - helpers.SetupCF(org, space) + username, _ = helpers.GetCredentials() }) AfterEach(func() { - helpers.QuickDeleteOrg(org) + helpers.QuickDeleteOrg(orgName) }) - When("the app does not exist", func() { - It("displays FAILED and app not found", func() { - session := helpers.CF("bind-service", "does-not-exist", serviceInstance) - Eventually(session).Should(Say("FAILED")) - Eventually(session.Err).Should(Say("App '%s' not found", "does-not-exist")) - Eventually(session).Should(Exit(1)) - }) - }) + Context("user-provided route service", func() { + var ( + serviceInstanceName string + appName string + bindingName string + ) - When("the app exists", func() { BeforeEach(func() { + serviceInstanceName = helpers.NewServiceInstanceName() + Eventually(helpers.CF("cups", serviceInstanceName)).Should(Exit(0)) + + appName = helpers.NewAppName() helpers.WithHelloWorldApp(func(appDir string) { Eventually(helpers.CF("push", appName, "--no-start", "-p", appDir, "-b", "staticfile_buildpack", "--no-route")).Should(Exit(0)) }) + + bindingName = helpers.RandomName() }) - When("the service does not exist", func() { - It("displays FAILED and service not found", func() { - session := helpers.CF("bind-service", appName, "does-not-exist") - Eventually(session).Should(Say("FAILED")) - Eventually(session.Err).Should(Say("Service instance '%s' not found", "does-not-exist")) - Eventually(session).Should(Exit(1)) - }) + It("creates a binding", func() { + session := helpers.CF(command, appName, serviceInstanceName, "--binding-name", bindingName) + Eventually(session).Should(Exit(0)) + + Expect(session.Out).To(SatisfyAll( + Say(`Binding service instance %s to app %s in org %s / space %s as %s\.\.\.\n`, serviceInstanceName, appName, orgName, spaceName, username), + Say(`OK\n`), + Say(`\n`), + Say(`TIP: Use 'cf restage %s' to ensure your env variable changes take effect`, appName), + )) + + Expect(string(session.Err.Contents())).To(BeEmpty()) + + binding := getBinding(serviceInstanceName) + Expect(binding.Name).To(Equal(bindingName)) + Expect(binding.LastOperation.State).To(BeEquivalentTo("succeeded")) }) - When("the service exists", func() { - BeforeEach(func() { - Eventually(helpers.CF("create-user-provided-service", serviceInstance, "-p", "{}")).Should(Exit(0)) - helpers.WithHelloWorldApp(func(appDir string) { - Eventually(helpers.CF("push", appName, "--no-start", "-p", appDir, "-b", "staticfile_buildpack", "--no-route")).Should(Exit(0)) - }) - }) + // -------------------------------------------------------------------------------------- + // Will not work until this is fixed: https://www.pivotaltracker.com/story/show/175515392 + // -------------------------------------------------------------------------------------- + // + //When("parameters are specified", func() { + // It("fails with an error returned by the CC", func() { + // session := helpers.CF(command, appName, serviceInstanceName, "-c", `{"foo":"bar"}`) + // Eventually(session).Should(Exit(1)) + // + // Expect(session.Out).To(SatisfyAll( + // Say(`Binding service instance %s to app %s in org %s / space %s as %s\.\.\.\n`, serviceInstanceName, appName, orgName, spaceName, username), + // Say(`\n`), + // Say(`FAILED\n`), + // )) + // + // Expect(session.Err).To(Say(`Binding parameters are not supported for user-provided service instances\n`)) + // }) + //}) + }) - AfterEach(func() { - Eventually(helpers.CF("unbind-service", appName, serviceInstance)).Should(Exit(0)) - Eventually(helpers.CF("delete-service", serviceInstance, "-f")).Should(Exit(0)) - }) + Context("managed service instance with synchronous broker response", func() { + var ( + broker *servicebrokerstub.ServiceBrokerStub + serviceInstanceName string + appName string + bindingName string + ) - It("binds the service to the app, displays OK and TIP", func() { - session := helpers.CF("bind-service", appName, serviceInstance) - Eventually(session).Should(Say("Binding service %s to app %s in org %s / space %s as %s...", serviceInstance, appName, org, space, username)) + BeforeEach(func() { + broker = servicebrokerstub.EnableServiceAccess() + serviceInstanceName = helpers.NewServiceInstanceName() + helpers.CreateManagedServiceInstance(broker.FirstServiceOfferingName(), broker.FirstServicePlanName(), serviceInstanceName) - Eventually(session).Should(Say("OK")) - Eventually(session).Should(Say("TIP: Use 'cf restage %s' to ensure your env variable changes take effect", appName)) - Eventually(session).Should(Exit(0)) + appName = helpers.NewAppName() + helpers.WithHelloWorldApp(func(appDir string) { + Eventually(helpers.CF("push", appName, "--no-start", "-p", appDir, "-b", "staticfile_buildpack", "--no-route")).Should(Exit(0)) }) - When("the service is already bound to an app", func() { - BeforeEach(func() { - Eventually(helpers.CF("bind-service", appName, serviceInstance)).Should(Exit(0)) - }) + bindingName = helpers.RandomName() + }) - It("displays OK and that the app is already bound to the service", func() { - session := helpers.CF("bind-service", appName, serviceInstance) + AfterEach(func() { + broker.Forget() + }) - Eventually(session).Should(Say("Binding service %s to app %s in org %s / space %s as %s...", serviceInstance, appName, org, space, username)) - Eventually(session).Should(Say("App %s is already bound to %s.", appName, serviceInstance)) - Eventually(session).Should(Say("OK")) + It("creates a binding", func() { + session := helpers.CF(command, appName, serviceInstanceName, "--binding-name", bindingName) + Eventually(session).Should(Exit(0)) - Eventually(session).Should(Exit(0)) - }) - }) + Expect(session.Out).To(SatisfyAll( + Say(`Binding service instance %s to app %s in org %s / space %s as %s\.\.\.\n`, serviceInstanceName, appName, orgName, spaceName, username), + Say(`OK\n`), + Say(`\n`), + Say(`TIP: Use 'cf restage %s' to ensure your env variable changes take effect`, appName), + )) - When("the --binding-name flag is provided and the value is a non-empty string", func() { - It("binds the service to the app, displays OK and TIP", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "--binding-name", "i-am-a-binding") - Eventually(session.Out).Should(Say("Binding service %s to app %s with binding name %s in org %s / space %s as %s...", serviceInstance, appName, "i-am-a-binding", org, space, username)) + Expect(string(session.Err.Contents())).To(BeEmpty()) - Eventually(session.Out).Should(Say("OK")) - Eventually(session.Out).Should(Say("TIP: Use 'cf restage %s' to ensure your env variable changes take effect", appName)) - Eventually(session).Should(Exit(0)) - }) - }) + binding := getBinding(serviceInstanceName) + Expect(binding.Name).To(Equal(bindingName)) + Expect(binding.LastOperation.State).To(BeEquivalentTo("succeeded")) + }) - When("configuration parameters are provided in a file", func() { - var configurationFile *os.File - - When("the file-path does not exist", func() { - It("displays FAILED and the invalid configuration error", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "-c", "i-do-not-exist") - Eventually(session.Err).Should(Say("Invalid configuration provided for -c flag. Please provide a valid JSON object or path to a file containing a valid JSON object.")) - - Eventually(session).Should(Exit(1)) - }) - }) - - When("the file contians invalid json", func() { - BeforeEach(func() { - var err error - content := []byte("{i-am-very-bad-json") - configurationFile, err = ioutil.TempFile("", "CF_CLI") - Expect(err).ToNot(HaveOccurred()) - - _, err = configurationFile.Write(content) - Expect(err).ToNot(HaveOccurred()) - - err = configurationFile.Close() - Expect(err).ToNot(HaveOccurred()) - }) - - AfterEach(func() { - Expect(os.RemoveAll(configurationFile.Name())).ToNot(HaveOccurred()) - }) - - It("displays FAILED and the invalid configuration error", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "-c", configurationFile.Name()) - Eventually(session.Err).Should(Say("Invalid configuration provided for -c flag. Please provide a valid JSON object or path to a file containing a valid JSON object.")) - - Eventually(session).Should(Exit(1)) - }) - }) - - When("the file-path is relative", func() { - BeforeEach(func() { - var err error - content := []byte("{\"i-am-good-json\":\"good-boy\"}") - configurationFile, err = ioutil.TempFile("", "CF_CLI") - Expect(err).ToNot(HaveOccurred()) - - _, err = configurationFile.Write(content) - Expect(err).ToNot(HaveOccurred()) - - err = configurationFile.Close() - Expect(err).ToNot(HaveOccurred()) - }) - - AfterEach(func() { - Expect(os.RemoveAll(configurationFile.Name())).ToNot(HaveOccurred()) - }) - - It("binds the service to the app, displays OK and TIP", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "-c", configurationFile.Name()) - Eventually(session).Should(Say("Binding service %s to app %s in org %s / space %s as %s...", serviceInstance, appName, org, space, username)) - - Eventually(session).Should(Say("OK")) - Eventually(session).Should(Say("TIP: Use 'cf restage %s' to ensure your env variable changes take effect", appName)) - Eventually(session).Should(Exit(0)) - }) - }) - - When("the file-path is absolute", func() { - BeforeEach(func() { - var err error - content := []byte("{\"i-am-good-json\":\"good-boy\"}") - configurationFile, err = ioutil.TempFile("", "CF_CLI") - Expect(err).ToNot(HaveOccurred()) - - _, err = configurationFile.Write(content) - Expect(err).ToNot(HaveOccurred()) - - err = configurationFile.Close() - Expect(err).ToNot(HaveOccurred()) - }) - - AfterEach(func() { - Expect(os.RemoveAll(configurationFile.Name())).ToNot(HaveOccurred()) - }) - - It("binds the service to the app, displays OK and TIP", func() { - absolutePath, err := filepath.Abs(configurationFile.Name()) - Expect(err).ToNot(HaveOccurred()) - session := helpers.CF("bind-service", appName, serviceInstance, "-c", absolutePath) - Eventually(session).Should(Say("Binding service %s to app %s in org %s / space %s as %s...", serviceInstance, appName, org, space, username)) - - Eventually(session).Should(Say("OK")) - Eventually(session).Should(Say("TIP: Use 'cf restage %s' to ensure your env variable changes take effect", appName)) - Eventually(session).Should(Exit(0)) - }) - }) - }) + When("parameters are specified", func() { + It("sends the parameters to the broker", func() { + session := helpers.CF(command, appName, serviceInstanceName, "-c", `{"foo":"bar"}`) + Eventually(session).Should(Exit(0)) - When("configuration paramters are provided as in-line JSON", func() { - When("the JSON is invalid", func() { - It("displays FAILED and the invalid configuration error", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "-c", "i-am-invalid-json") - Eventually(session.Err).Should(Say("Invalid configuration provided for -c flag. Please provide a valid JSON object or path to a file containing a valid JSON object.")) - - Eventually(session).Should(Exit(1)) - }) - }) - - When("the JSON is valid", func() { - It("binds the service to the app, displays OK and TIP", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "-c", "{\"i-am-valid-json\":\"dope dude\"}") - Eventually(session).Should(Say("Binding service %s to app %s in org %s / space %s as %s...", serviceInstance, appName, org, space, username)) - - Eventually(session).Should(Say("OK")) - Eventually(session).Should(Say("TIP: Use 'cf restage %s' to ensure your env variable changes take effect", appName)) - Eventually(session).Should(Exit(0)) - }) - }) + Expect(getParameters(serviceInstanceName)).To(Equal(map[string]interface{}{"foo": "bar"})) }) }) + }) + + Context("managed service instance with asynchronous broker response", func() { + var ( + broker *servicebrokerstub.ServiceBrokerStub + serviceInstanceName string + appName string + bindingName string + ) - When("the service is provided by a broker", func() { - var broker *servicebrokerstub.ServiceBrokerStub + BeforeEach(func() { + broker = servicebrokerstub.New().WithAsyncDelay(time.Second).EnableServiceAccess() + serviceInstanceName = helpers.NewServiceInstanceName() + helpers.CreateManagedServiceInstance(broker.FirstServiceOfferingName(), broker.FirstServicePlanName(), serviceInstanceName) - AfterEach(func() { - broker.Forget() + appName = helpers.NewAppName() + helpers.WithHelloWorldApp(func(appDir string) { + Eventually(helpers.CF("push", appName, "--no-start", "-p", appDir, "-b", "staticfile_buildpack", "--no-route")).Should(Exit(0)) }) - When("the service binding is blocking", func() { - BeforeEach(func() { - helpers.SkipIfV7() // V8 implementation of `cf service` does not show bindings yet + bindingName = helpers.RandomName() + }) + + AfterEach(func() { + broker.Forget() + }) + + It("start to create a binding", func() { + session := helpers.CF(command, appName, serviceInstanceName, "--binding-name", bindingName) + Eventually(session).Should(Exit(0)) + + Expect(session.Out).To(SatisfyAll( + Say(`Binding service instance %s to app %s in org %s / space %s as %s\.\.\.\n`, serviceInstanceName, appName, orgName, spaceName, username), + Say(`OK\n`), + Say(`\n`), + Say(`Binding in progress. Use 'cf service %s' to check operation status\.\n`, serviceInstanceName), + Say(`\n`), + Say(`TIP: Once this operation succeeds, use 'cf restage %s' to ensure your env variable changes take effect`, appName), + )) + + Expect(string(session.Err.Contents())).To(BeEmpty()) + + binding := getBinding(serviceInstanceName) + Expect(binding.Name).To(Equal(bindingName)) + Expect(binding.LastOperation.State).To(BeEquivalentTo("in progress")) + }) + + When("--wait flag specified", func() { + It("waits for completion", func() { + session := helpers.CF(command, appName, serviceInstanceName, "--binding-name", bindingName, "--wait") + Eventually(session).Should(Exit(0)) - broker = servicebrokerstub.EnableServiceAccess() + Expect(session.Out).To(SatisfyAll( + Say(`Binding service instance %s to app %s in org %s / space %s as %s\.\.\.\n`, serviceInstanceName, appName, orgName, spaceName, username), + Say(`Waiting for the operation to complete\.+\n`), + Say(`\n`), + Say(`OK\n`), + )) - Eventually(helpers.CF("create-service", broker.FirstServiceOfferingName(), broker.FirstServicePlanName(), serviceInstance)).Should(Exit(0)) - }) + Expect(string(session.Err.Contents())).To(BeEmpty()) - It("binds the service to the app, displays OK and TIP", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "-c", `{"wheres":"waldo"}`) - Eventually(session).Should(Say("Binding service %s to app %s in org %s / space %s as %s...", serviceInstance, appName, org, space, username)) + Expect(getBinding(serviceInstanceName).LastOperation.State).To(BeEquivalentTo("succeeded")) + }) + }) + }) - Eventually(session).Should(Say("OK")) - Eventually(session).Should(Say("TIP: Use 'cf restage %s' to ensure your env variable changes take effect", appName)) - Eventually(session).Should(Exit(0)) + Context("binding already exists", func() { + var ( + serviceInstanceName string + appName string + ) - session = helpers.CF("service", serviceInstance) - Eventually(session).Should(Say(appName)) - Eventually(session).Should(Exit(0)) - }) + BeforeEach(func() { + serviceInstanceName = helpers.NewServiceInstanceName() + Eventually(helpers.CF("cups", serviceInstanceName)).Should(Exit(0)) + + appName = helpers.NewAppName() + helpers.WithHelloWorldApp(func(appDir string) { + Eventually(helpers.CF("push", appName, "--no-start", "-p", appDir, "-b", "staticfile_buildpack", "--no-route")).Should(Exit(0)) }) - When("the service binding is asynchronous", func() { - BeforeEach(func() { - helpers.SkipIfV7() // V8 implementation of `cf service` does not show bindings yet + Eventually(helpers.CF(command, appName, serviceInstanceName)).Should(Exit(0)) + }) + + It("says OK", func() { + session := helpers.CF(command, appName, serviceInstanceName) + Eventually(session).Should(Exit(0)) + + Expect(session.Out).To(SatisfyAll( + Say(`Binding service instance %s to app %s in org %s / space %s as %s\.\.\.\n`, serviceInstanceName, appName, orgName, spaceName, username), + Say(`App %s is already bound to service instance %s.\n`, appName, serviceInstanceName), + Say(`OK\n`), + )) - broker = servicebrokerstub.New().WithAsyncDelay(time.Millisecond).EnableServiceAccess() + Expect(string(session.Err.Contents())).To(BeEmpty()) + }) + }) - Eventually(helpers.CF("create-service", broker.FirstServiceOfferingName(), broker.FirstServicePlanName(), serviceInstance)).Should(Exit(0)) + Context("app does not exist", func() { + var serviceInstanceName string - Eventually(func() *Session { - session := helpers.CF("service", serviceInstance) - return session.Wait() - }, time.Minute*5, time.Second*5).Should(Say("create succeeded")) - }) + BeforeEach(func() { + serviceInstanceName = helpers.NewServiceInstanceName() + Eventually(helpers.CF("cups", serviceInstanceName)).Should(Exit(0)) + }) - It("binds the service to the app, displays OK and TIP", func() { - session := helpers.CF("bind-service", appName, serviceInstance, "-c", `{"wheres":"waldo"}`) - Eventually(session).Should(Say("Binding service %s to app %s in org %s / space %s as %s...", serviceInstance, appName, org, space, username)) + It("displays FAILED and app not found", func() { + session := helpers.CF("bind-service", "does-not-exist", serviceInstanceName) + Eventually(session).Should(Exit(1)) + Expect(session.Out).To(Say("FAILED")) + Expect(session.Err).To(Say("App 'does-not-exist' not found")) + }) + }) - Eventually(session).Should(Say("OK")) - Eventually(session).Should(Say("Binding in progress. Use 'cf service %s' to check operation status.", serviceInstance)) - Eventually(session).Should(Say("TIP: Once this operation succeeds, use 'cf restage %s' to ensure your env variable changes take effect", appName)) - Eventually(session).Should(Exit(0)) + Context("service instance does not exist", func() { + var appName string - session = helpers.CF("service", serviceInstance) - Eventually(session).Should(Say(appName)) - Eventually(session).Should(Exit(0)) - }) + BeforeEach(func() { + appName = helpers.NewAppName() + helpers.WithHelloWorldApp(func(appDir string) { + Eventually(helpers.CF("push", appName, "--no-start", "-p", appDir, "-b", "staticfile_buildpack", "--no-route")).Should(Exit(0)) }) }) + + It("displays FAILED and service not found", func() { + session := helpers.CF("bind-service", appName, "does-not-exist") + Eventually(session).Should(Exit(1)) + Expect(session.Out).To(Say("FAILED")) + Expect(session.Err).To(Say("Service instance 'does-not-exist' not found")) + }) }) }) }) diff --git a/integration/v7/isolated/service_command_test.go b/integration/v7/isolated/service_command_test.go index b1305b16678..49fcaf2717b 100644 --- a/integration/v7/isolated/service_command_test.go +++ b/integration/v7/isolated/service_command_test.go @@ -518,7 +518,7 @@ var _ = Describe("service command", func() { bindingName1 = helpers.RandomName() bindingName2 = helpers.RandomName() - broker = servicebrokerstub.New().EnableServiceAccess() + broker = servicebrokerstub.New().WithAsyncDelay(time.Millisecond).EnableServiceAccess() helpers.CreateManagedServiceInstance( broker.FirstServiceOfferingName(), @@ -531,10 +531,8 @@ var _ = Describe("service command", func() { Eventually(helpers.CF("push", appName2, "--no-start", "-p", appDir, "-b", "staticfile_buildpack", "--no-route")).Should(Exit(0)) }) - const asyncDelay = time.Minute // Forces bind to be "in progress" for predictable output - broker.WithAsyncDelay(asyncDelay).Configure() - Eventually(helpers.CF("bind-service", appName1, serviceInstanceName, "--binding-name", bindingName1)).Should(Exit(0)) - Eventually(helpers.CF("bind-service", appName2, serviceInstanceName, "--binding-name", bindingName2)).Should(Exit(0)) + Eventually(helpers.CF("bind-service", appName1, serviceInstanceName, "--binding-name", bindingName1, "--wait")).Should(Exit(0)) + Eventually(helpers.CF("bind-service", appName2, serviceInstanceName, "--binding-name", bindingName2, "--wait")).Should(Exit(0)) }) It("displays the bound apps", func() { @@ -544,8 +542,8 @@ var _ = Describe("service command", func() { Expect(session).To(SatisfyAll( Say(`Bound apps:\n`), Say(`name\s+binding name\s+status\s+message\n`), - Say(`%s\s+%s\s+create in progress\s*\n`, appName1, bindingName1), - Say(`%s\s+%s\s+create in progress\s*\n`, appName2, bindingName2), + Say(`%s\s+%s\s+create succeeded\s+very happy service\n`, appName1, bindingName1), + Say(`%s\s+%s\s+create succeeded\s+very happy service\n`, appName2, bindingName2), )) }) }) diff --git a/resources/service_credential_binding_resource.go b/resources/service_credential_binding_resource.go index 193830b0ef5..0cd9bb988d5 100644 --- a/resources/service_credential_binding_resource.go +++ b/resources/service_credential_binding_resource.go @@ -1,6 +1,7 @@ package resources import ( + "code.cloudfoundry.org/cli/types" "code.cloudfoundry.org/jsonry" ) @@ -28,6 +29,8 @@ type ServiceCredentialBinding struct { AppSpaceGUID string `jsonry:"-"` // LastOperation is the last operation on the service credential binding LastOperation LastOperation `jsonry:"last_operation"` + // Parameters can be specified when creating a binding + Parameters types.OptionalObject `jsonry:"parameters"` } func (s ServiceCredentialBinding) MarshalJSON() ([]byte, error) { diff --git a/resources/service_credential_binding_resource_test.go b/resources/service_credential_binding_resource_test.go index 413bcf2ba33..f73c02079ae 100644 --- a/resources/service_credential_binding_resource_test.go +++ b/resources/service_credential_binding_resource_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" . "code.cloudfoundry.org/cli/resources" + "code.cloudfoundry.org/cli/types" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" @@ -47,6 +48,19 @@ var _ = Describe("service credential binding resource", func() { } }`, ), + Entry( + "parameters", + ServiceCredentialBinding{ + Parameters: types.NewOptionalObject(map[string]interface{}{ + "foo": "bar", + }), + }, + `{ + "parameters": { + "foo": "bar" + } + }`, + ), Entry( "everything", ServiceCredentialBinding{ @@ -55,6 +69,9 @@ var _ = Describe("service credential binding resource", func() { Name: "fake-name", AppGUID: "fake-app-guid", ServiceInstanceGUID: "fake-service-instance-guid", + Parameters: types.NewOptionalObject(map[string]interface{}{ + "foo": "bar", + }), }, `{ "type": "app", @@ -71,6 +88,9 @@ var _ = Describe("service credential binding resource", func() { "guid": "fake-app-guid" } } + }, + "parameters": { + "foo": "bar" } }`, ),