Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions actor/v7action/service_app_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type CreateServiceAppBindingParams struct {
AppName string
BindingName string
Parameters types.OptionalObject
Strategy resources.BindingStrategyType
}

type DeleteServiceAppBindingParams struct {
Expand All @@ -41,7 +42,7 @@ func (actor Actor) CreateServiceAppBinding(params CreateServiceAppBindingParams)
return
},
func() (warnings ccv3.Warnings, err error) {
jobURL, warnings, err = actor.createServiceAppBinding(serviceInstance.GUID, app.GUID, params.BindingName, params.Parameters)
jobURL, warnings, err = actor.createServiceAppBinding(serviceInstance.GUID, app.GUID, params.BindingName, params.Parameters, params.Strategy)
return
},
func() (warnings ccv3.Warnings, err error) {
Expand Down Expand Up @@ -102,13 +103,14 @@ func (actor Actor) DeleteServiceAppBinding(params DeleteServiceAppBindingParams)
}
}

func (actor Actor) createServiceAppBinding(serviceInstanceGUID, appGUID, bindingName string, parameters types.OptionalObject) (ccv3.JobURL, ccv3.Warnings, error) {
func (actor Actor) createServiceAppBinding(serviceInstanceGUID, appGUID, bindingName string, parameters types.OptionalObject, strategy resources.BindingStrategyType) (ccv3.JobURL, ccv3.Warnings, error) {
jobURL, warnings, err := actor.CloudControllerClient.CreateServiceCredentialBinding(resources.ServiceCredentialBinding{
Type: resources.AppBinding,
Name: bindingName,
ServiceInstanceGUID: serviceInstanceGUID,
AppGUID: appGUID,
Parameters: parameters,
Strategy: strategy,
})
switch err.(type) {
case nil:
Expand Down
3 changes: 3 additions & 0 deletions actor/v7action/service_app_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var _ = Describe("Service App Binding Action", func() {
bindingName = "fake-binding-name"
spaceGUID = "fake-space-guid"
fakeJobURL = ccv3.JobURL("fake-job-url")
strategy = "single"
)

var (
Expand Down Expand Up @@ -87,6 +88,7 @@ var _ = Describe("Service App Binding Action", func() {
Parameters: types.NewOptionalObject(map[string]interface{}{
"foo": "bar",
}),
Strategy: resources.SingleBindingStrategy,
}
})

Expand Down Expand Up @@ -202,6 +204,7 @@ var _ = Describe("Service App Binding Action", func() {
Parameters: types.NewOptionalObject(map[string]interface{}{
"foo": "bar",
}),
Strategy: strategy,
}))
})

Expand Down
30 changes: 30 additions & 0 deletions command/flag/service_binding_strategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package flag

import (
"strings"

"code.cloudfoundry.org/cli/v8/resources"
flags "github.com/jessevdk/go-flags"
)

type ServiceBindingStrategy struct {
Strategy resources.BindingStrategyType
}

func (ServiceBindingStrategy) Complete(prefix string) []flags.Completion {
return completions([]string{"single", "multiple"}, prefix, false)
}

func (h *ServiceBindingStrategy) UnmarshalFlag(val string) error {
valLower := strings.ToLower(val)
switch valLower {
case "single", "multiple":
h.Strategy = resources.BindingStrategyType(valLower)
default:
return &flags.Error{
Type: flags.ErrRequired,
Message: `STRATEGY must be "single" or "multiple"`,
}
}
return nil
}
61 changes: 61 additions & 0 deletions command/flag/service_binding_strategy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package flag_test

import (
. "code.cloudfoundry.org/cli/v8/command/flag"
"code.cloudfoundry.org/cli/v8/resources"
flags "github.com/jessevdk/go-flags"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("ServiceBindingStrategy", func() {
var sbs ServiceBindingStrategy

Describe("Complete", func() {
DescribeTable("returns list of completions",
func(prefix string, matches []flags.Completion) {
completions := sbs.Complete(prefix)
Expect(completions).To(Equal(matches))
},
Entry("returns 'single' when passed 's'", "s",
[]flags.Completion{{Item: "single"}}),
Entry("returns 'single' when passed 'S'", "S",
[]flags.Completion{{Item: "single"}}),
Entry("returns 'multiple' when passed 'm'", "m",
[]flags.Completion{{Item: "multiple"}}),
Entry("returns 'multiple' when passed 'M'", "M",
[]flags.Completion{{Item: "multiple"}}),
Entry("returns 'single' and 'multiple' when passed ''", "",
[]flags.Completion{{Item: "single"}, {Item: "multiple"}}),
)
})

Describe("UnmarshalFlag", func() {
BeforeEach(func() {
sbs = ServiceBindingStrategy{}
})

DescribeTable("downcases and sets strategy",
func(input string, expected resources.BindingStrategyType) {
err := sbs.UnmarshalFlag(input)
Expect(err).ToNot(HaveOccurred())
Expect(sbs.Strategy).To(Equal(expected))
},
Entry("sets 'single' when passed 'single'", "single", resources.SingleBindingStrategy),
Entry("sets 'single' when passed 'sInGlE'", "sInGlE", resources.SingleBindingStrategy),
Entry("sets 'multiple' when passed 'multiple'", "multiple", resources.MultipleBindingStrategy),
Entry("sets 'multiple' when passed 'MuLtIpLe'", "MuLtIpLe", resources.MultipleBindingStrategy),
)

When("passed anything else", func() {
It("returns an error", func() {
err := sbs.UnmarshalFlag("banana")
Expect(err).To(MatchError(&flags.Error{
Type: flags.ErrRequired,
Message: `STRATEGY must be "single" or "multiple"`,
}))
Expect(sbs.Strategy).To(BeEmpty())
})
})
})
})
19 changes: 13 additions & 6 deletions command/v7/bind_service_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
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 operation to complete"`
relatedCommands interface{} `related_commands:"services"`
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."`
ServiceBindingStrategy flag.ServiceBindingStrategy `long:"strategy" description:"Service binding strategy. Valid values are 'single' (default) and 'multiple'."`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering we added this new flag, we will need to update the help integration test here:
https://github.com/cloudfoundry/cli/blob/v8/integration/v7/isolated/bind_service_command_test.go#L59-L62

Wait bool `short:"w" long:"wait" description:"Wait for the operation to complete"`
relatedCommands interface{} `related_commands:"services"`
}

func (cmd BindServiceCommand) Execute(args []string) error {
Expand All @@ -33,6 +34,7 @@ func (cmd BindServiceCommand) Execute(args []string) error {
AppName: cmd.RequiredArgs.AppName,
BindingName: cmd.BindingName.Value,
Parameters: types.OptionalObject(cmd.ParametersAsJSON),
Strategy: cmd.ServiceBindingStrategy.Strategy,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: May be consider adding a unit test case in command/v7/bind_service_command_test.go that explicitly verifies the strategy parameter:

   When("strategy flag is set", func() {
       BeforeEach(func() {
           setFlag(&cmd, "--strategy", "multiple")
       })
       
       It("passes the strategy to the actor", func() {
           Expect(fakeActor.CreateServiceAppBindingArgsForCall(0).Strategy).
               To(Equal(resources.MultipleBindingStrategy))
       })
   })

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Based on the RFC document it seems like this is a new functionality added in CAPI. If that is the case, there has to be a minimum CAPI version required when using this feature right?

Please let me know if I am misunderstanding this but if that is the case, we will need to add a conditional check in the implementation and may be in the API call so that this works with the older version of the CAPI as well as throw meaningful error message when used with older version of CAPI.

There are many places we have done this but one of the example you can see here: https://github.com/cloudfoundry/cli/blob/main/command/v7/buildpacks_command.go#L32-L37

})
cmd.UI.DisplayWarnings(warnings)

Expand Down Expand Up @@ -82,7 +84,12 @@ Example of valid JSON object:

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`
CF_NAME bind-service APP_NAME SERVICE_INSTANCE --binding-name BINDING_NAME

Optionally provide the binding strategy type. Valid options are 'single' (default) and 'multiple'. The 'multiple' strategy allows multiple bindings between the same app and service instance.
This is useful for credential rotation scenarios.

CF_NAME bind-service APP_NAME SERVICE_INSTANCE --strategy multiple`
Comment on lines +87 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will need to update the help integration test here related to this change.
https://github.com/cloudfoundry/cli/blob/v8/integration/v7/isolated/bind_service_command_test.go#L42-L43

}

func (cmd BindServiceCommand) Examples() string {
Expand Down
9 changes: 9 additions & 0 deletions resources/service_credential_binding_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ const (
KeyBinding ServiceCredentialBindingType = "key"
)

type BindingStrategyType string

const (
SingleBindingStrategy BindingStrategyType = "single"
MultipleBindingStrategy BindingStrategyType = "multiple"
)

type ServiceCredentialBinding struct {
// Type is either "app" or "key"
Type ServiceCredentialBindingType `jsonry:"type,omitempty"`
Expand All @@ -31,6 +38,8 @@ type ServiceCredentialBinding struct {
LastOperation LastOperation `jsonry:"last_operation"`
// Parameters can be specified when creating a binding
Parameters types.OptionalObject `jsonry:"parameters"`
// Strategy can be "single" (default) or "multiple"
Strategy BindingStrategyType `jsonry:"strategy,omitempty"`
Comment on lines +41 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Might be good to add a comment mentioning that empty value related defaulting is done at the backend which defaults to single as you have mentioned. It might not be obvious what the (default) means here.

}

func (s ServiceCredentialBinding) MarshalJSON() ([]byte, error) {
Expand Down
13 changes: 12 additions & 1 deletion resources/service_credential_binding_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ var _ = Describe("service credential binding resource", func() {
}
}`,
),
Entry(
"strategy",
ServiceCredentialBinding{
Strategy: SingleBindingStrategy,
},
`{
"strategy": "single"
}`,
),
Entry(
"everything",
ServiceCredentialBinding{
Expand All @@ -71,6 +80,7 @@ var _ = Describe("service credential binding resource", func() {
Parameters: types.NewOptionalObject(map[string]interface{}{
"foo": "bar",
}),
Strategy: MultipleBindingStrategy,
},
`{
"type": "app",
Expand All @@ -90,7 +100,8 @@ var _ = Describe("service credential binding resource", func() {
},
"parameters": {
"foo": "bar"
}
},
"strategy": "multiple"
}`,
),
)
Expand Down
Loading