diff --git a/actor/v7action/service_app_binding.go b/actor/v7action/service_app_binding.go index 71326c39808..5cc8100e2d8 100644 --- a/actor/v7action/service_app_binding.go +++ b/actor/v7action/service_app_binding.go @@ -15,6 +15,7 @@ type CreateServiceAppBindingParams struct { AppName string BindingName string Parameters types.OptionalObject + Strategy resources.BindingStrategyType } type DeleteServiceAppBindingParams struct { @@ -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) { @@ -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: diff --git a/actor/v7action/service_app_binding_test.go b/actor/v7action/service_app_binding_test.go index 8203077cde1..56efb6008b1 100644 --- a/actor/v7action/service_app_binding_test.go +++ b/actor/v7action/service_app_binding_test.go @@ -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 ( @@ -87,6 +88,7 @@ var _ = Describe("Service App Binding Action", func() { Parameters: types.NewOptionalObject(map[string]interface{}{ "foo": "bar", }), + Strategy: resources.SingleBindingStrategy, } }) @@ -202,6 +204,7 @@ var _ = Describe("Service App Binding Action", func() { Parameters: types.NewOptionalObject(map[string]interface{}{ "foo": "bar", }), + Strategy: strategy, })) }) diff --git a/command/flag/service_binding_strategy.go b/command/flag/service_binding_strategy.go new file mode 100644 index 00000000000..74f60cb24ad --- /dev/null +++ b/command/flag/service_binding_strategy.go @@ -0,0 +1,30 @@ +package flag + +import ( + "strings" + + "code.cloudfoundry.org/cli/v9/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 +} diff --git a/command/flag/service_binding_strategy_test.go b/command/flag/service_binding_strategy_test.go new file mode 100644 index 00000000000..9fd54d999b0 --- /dev/null +++ b/command/flag/service_binding_strategy_test.go @@ -0,0 +1,61 @@ +package flag_test + +import ( + . "code.cloudfoundry.org/cli/v9/command/flag" + "code.cloudfoundry.org/cli/v9/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()) + }) + }) + }) +}) diff --git a/command/v7/bind_service_command.go b/command/v7/bind_service_command.go index 04ab1a0b1ed..07702ef6df1 100644 --- a/command/v7/bind_service_command.go +++ b/command/v7/bind_service_command.go @@ -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 { @@ -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) @@ -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 { diff --git a/resources/service_credential_binding_resource.go b/resources/service_credential_binding_resource.go index da0d7b4126d..a8e4563ea5c 100644 --- a/resources/service_credential_binding_resource.go +++ b/resources/service_credential_binding_resource.go @@ -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"` @@ -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) { diff --git a/resources/service_credential_binding_resource_test.go b/resources/service_credential_binding_resource_test.go index d4beb474aaf..37326093627 100644 --- a/resources/service_credential_binding_resource_test.go +++ b/resources/service_credential_binding_resource_test.go @@ -60,6 +60,15 @@ var _ = Describe("service credential binding resource", func() { } }`, ), + Entry( + "strategy", + ServiceCredentialBinding{ + Strategy: SingleBindingStrategy, + }, + `{ + "strategy": "single" + }`, + ), Entry( "everything", ServiceCredentialBinding{ @@ -71,6 +80,7 @@ var _ = Describe("service credential binding resource", func() { Parameters: types.NewOptionalObject(map[string]interface{}{ "foo": "bar", }), + Strategy: MultipleBindingStrategy, }, `{ "type": "app", @@ -90,7 +100,8 @@ var _ = Describe("service credential binding resource", func() { }, "parameters": { "foo": "bar" - } + }, + "strategy": "multiple" }`, ), )