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
2 changes: 2 additions & 0 deletions api/cloudcontroller/ccversion/minimum_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ const (
MinVersionPerRouteOpts = "3.183.0"

MinVersionCanarySteps = "3.189.0"

MinVersionServiceBindingStrategy = "3.205.0"
)
32 changes: 32 additions & 0 deletions command/flag/service_binding_strategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package flag

import (
"strings"

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

type ServiceBindingStrategy struct {
Strategy resources.BindingStrategyType
IsSet bool
}

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)
h.IsSet = true
default:
return &flags.Error{
Type: flags.ErrRequired,
Message: `STRATEGY must be "single" or "multiple"`,
}
}
return nil
}
68 changes: 68 additions & 0 deletions command/flag/service_binding_strategy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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{}
})

When("unmarshal has not been called", func() {
It("is marked as not set", func() {
Expect(sbs.IsSet).To(BeFalse())
})
})

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))
Expect(sbs.IsSet).To(BeTrue())
},
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())
})
})
})
})
28 changes: 22 additions & 6 deletions command/v7/bind_service_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package v7
import (
"code.cloudfoundry.org/cli/v8/actor/actionerror"
"code.cloudfoundry.org/cli/v8/actor/v7action"
"code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccversion"
"code.cloudfoundry.org/cli/v8/command"
"code.cloudfoundry.org/cli/v8/command/flag"
"code.cloudfoundry.org/cli/v8/command/v7/shared"
"code.cloudfoundry.org/cli/v8/types"
Expand All @@ -11,18 +13,26 @@ 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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Enhanced integration test.

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 {
if err := cmd.SharedActor.CheckTarget(true, true); err != nil {
return err
}

if cmd.ServiceBindingStrategy.IsSet {
err := command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionServiceBindingStrategy, "--strategy")
if err != nil {
return err
}
}

if err := cmd.displayIntro(); err != nil {
return err
}
Expand All @@ -33,6 +43,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 Author

Choose a reason for hiding this comment

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

Added test.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, minimum required ccng version is 3.205.0. I've added a check and a test!

})
cmd.UI.DisplayWarnings(warnings)

Expand Down Expand Up @@ -82,7 +93,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 +96 to +101
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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated test.

}

func (cmd BindServiceCommand) Examples() string {
Expand Down
27 changes: 27 additions & 0 deletions command/v7/bind_service_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"code.cloudfoundry.org/cli/v8/actor/actionerror"
"code.cloudfoundry.org/cli/v8/actor/v7action"
"code.cloudfoundry.org/cli/v8/command/commandfakes"
"code.cloudfoundry.org/cli/v8/command/translatableerror"
v7 "code.cloudfoundry.org/cli/v8/command/v7"
"code.cloudfoundry.org/cli/v8/command/v7/v7fakes"
"code.cloudfoundry.org/cli/v8/resources"
"code.cloudfoundry.org/cli/v8/types"
"code.cloudfoundry.org/cli/v8/util/configv3"
"code.cloudfoundry.org/cli/v8/util/ui"
Expand Down Expand Up @@ -39,6 +41,7 @@ var _ = Describe("bind-service Command", func() {
BeforeEach(func() {
testUI = ui.NewTestUI(NewBuffer(), NewBuffer(), NewBuffer())
fakeConfig = new(commandfakes.FakeConfig)
fakeConfig.APIVersionReturns("3.205.0")
fakeSharedActor = new(commandfakes.FakeSharedActor)
fakeActor = new(v7fakes.FakeActor)

Expand Down Expand Up @@ -110,6 +113,30 @@ var _ = Describe("bind-service Command", func() {
})
})

When("strategy flag is set", func() {
BeforeEach(func() {
setFlag(&cmd, "--strategy", "multiple")
})

It("passes the strategy to the actor", func() {
Expect(executeErr).NotTo(HaveOccurred())
Expect(fakeActor.CreateServiceAppBindingCallCount()).To(Equal(1))
Expect(fakeActor.CreateServiceAppBindingArgsForCall(0).Strategy).
To(Equal(resources.MultipleBindingStrategy))
})

It("fails when the cc version is below the minimum", func() {
fakeConfig.APIVersionReturns("3.204.0")
executeErr = cmd.Execute(nil)

Expect(executeErr).To(MatchError(translatableerror.MinimumCFAPIVersionNotMetError{
Command: "--strategy",
CurrentVersion: "3.204.0",
MinimumVersion: "3.205.0",
}))
})
})

When("binding already exists", func() {
BeforeEach(func() {
fakeActor.CreateServiceAppBindingReturns(
Expand Down
5 changes: 5 additions & 0 deletions integration/v7/isolated/bind_service_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ var _ = Describe("bind-service command", func() {
Say(`\n`),
Say(`\s+cf bind-service APP_NAME SERVICE_INSTANCE --binding-name BINDING_NAME\n`),
Say(`\n`),
Say(`\s+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.\n`),
Say(`\s+This is useful for credential rotation scenarios.\n`),
Say(`\n`),
Say(`\s+cf bind-service APP_NAME SERVICE_INSTANCE --strategy multiple\n`),
Say(`EXAMPLES:\n`),
Say(`\s+Linux/Mac:\n`),
Say(`\s+cf bind-service myapp mydb -c '\{"permissions":"read-only"\}'\n`),
Expand All @@ -59,6 +63,7 @@ var _ = Describe("bind-service command", func() {
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+--strategy\s+Service binding strategy. Valid values are 'single' \(default\) and 'multiple'.\n`),
Say(`\s+--wait, -w\s+Wait for the operation to complete\n`),
Say(`\n`),
Say(`SEE ALSO:\n`),
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" or "multiple" (if empty, "single" is set as default by backend)
Strategy BindingStrategyType `jsonry:"strategy,omitempty"`
}

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