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'."`
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,
})
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`
}

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"`
}

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