diff --git a/internal/cmd/instance/transfer.go b/internal/cmd/instance/transfer.go index bb43327e..a85cdb7e 100644 --- a/internal/cmd/instance/transfer.go +++ b/internal/cmd/instance/transfer.go @@ -61,7 +61,7 @@ func (trc *TransferCmd) Prepare(prepare cmd.PrepareFunc) *cobra.Command { } result.Flags().BoolVarP(&trc.force, "force", "f", false, "Force transfer without confirmation") - result.Flags().StringVarP(&trc.instanceID, "id", "", "", "Id of the instance. Required in case when there are instances with same name") + result.Flags().StringVarP(&trc.instanceID, "id", "", "", cmd.INSTANCE_ID_DESCRIPTION) result.Flags().StringVarP(&trc.fromPlatformID, "from", "", "", "ID of the platform from which you want to move the instance") result.Flags().StringVarP(&trc.toPlatformID, "to", "", "", "ID of the platform to which you want to move the instance") cmd.AddFormatFlag(result.Flags()) @@ -102,11 +102,11 @@ func (trc *TransferCmd) Run() error { return err } if len(instances.ServiceInstances) == 0 { - return fmt.Errorf("no instances found with name %s", trc.instanceName) + return fmt.Errorf(cmd.NO_INSTANCES_FOUND, trc.instanceName) } if len(instances.ServiceInstances) > 1 { - return fmt.Errorf("more than 1 instance found with name %s. Use --id flag to specify one", trc.instanceName) + return fmt.Errorf(cmd.FOUND_TOO_MANY_INSTANCES, trc.instanceName, "transfer") } trc.instanceID = instances.ServiceInstances[0].ID diff --git a/internal/cmd/instance/transfer_test.go b/internal/cmd/instance/transfer_test.go index 3bee3538..3a8dc3eb 100644 --- a/internal/cmd/instance/transfer_test.go +++ b/internal/cmd/instance/transfer_test.go @@ -20,7 +20,7 @@ import ( "bytes" "encoding/json" "errors" - + "fmt" "github.com/Peripli/service-manager-cli/internal/cmd" "github.com/Peripli/service-manager-cli/pkg/smclient/smclientfakes" "github.com/Peripli/service-manager-cli/pkg/types" @@ -120,7 +120,7 @@ var _ = Describe("Transfer Command test", func() { Context("when no instance id is provided", func() { It("should require flag for instance id", func() { err := invalidTransferCommandExecution("instance-name", "--from", "from_platform", "--to", "to_platform") - Expect(err.Error()).To(Equal("more than 1 instance found with name instance-name. Use --id flag to specify one")) + Expect(err.Error()).To(Equal(fmt.Sprintf(cmd.FOUND_TOO_MANY_INSTANCES,"instance-name","transfer"))) }) }) @@ -141,7 +141,8 @@ var _ = Describe("Transfer Command test", func() { It("should fail to transfer", func() { err := invalidTransferCommandExecution("no-instance", "--from", "from_platform", "--to", "to_platform") - Expect(err.Error()).To(Equal("no instances found with name no-instance")) + message:=fmt.Sprintf(cmd.NO_INSTANCES_FOUND,"no-instance") + Expect(err.Error()).To(Equal(message)) }) }) diff --git a/internal/cmd/instance/update_instance.go b/internal/cmd/instance/update_instance.go index da4c3724..1c996d70 100644 --- a/internal/cmd/instance/update_instance.go +++ b/internal/cmd/instance/update_instance.go @@ -29,16 +29,15 @@ import ( type UpdateCmd struct { *cmd.Context - instance types.ServiceInstance - instanceName string - planName string - parametersJSON string - outputFormat output.Format + instance types.ServiceInstance + instanceName string + planName string + parametersJSON string + outputFormat output.Format } - func NewUpdateInstanceCmd(context *cmd.Context) *UpdateCmd { - return &UpdateCmd{Context: context,instance: types.ServiceInstance{}} + return &UpdateCmd{Context: context, instance: types.ServiceInstance{}} } // Prepare returns cobra command @@ -81,11 +80,12 @@ func (uc *UpdateCmd) Run() error { return err } if len(instances.ServiceInstances) == 0 { - return fmt.Errorf("no instances found with name %s", uc.instanceName) + return fmt.Errorf(cmd.NO_INSTANCES_FOUND, uc.instanceName) } if len(instances.ServiceInstances) > 1 { - return fmt.Errorf("more than 1 instance found with name %s. Use --id flag to specify one", uc.instanceName) + return fmt.Errorf(cmd.FOUND_TOO_MANY_INSTANCES, uc.instanceName, "update") + } instanceBeforeUpdate = &instances.ServiceInstances[0] diff --git a/internal/cmd/instance/update_instance_test.go b/internal/cmd/instance/update_instance_test.go index b425ee57..b6d79111 100644 --- a/internal/cmd/instance/update_instance_test.go +++ b/internal/cmd/instance/update_instance_test.go @@ -171,7 +171,7 @@ var _ = Describe("update instance command test", func() { Context("by name", func() { It("should return an error", func() { err := invalidUpdateInstanceCommandExecution("instance-name", "--new-name", "new name") - Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("no instances found with name %s", "instance-name"))) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf(cmd.NO_INSTANCES_FOUND, "instance-name"))) }) }) @@ -203,7 +203,7 @@ var _ = Describe("update instance command test", func() { }) It("should return an error", func() { err := invalidUpdateInstanceCommandExecution("instance-name", "--new-name", "new name") - Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("more than 1 instance found with name %s", "instance-name"))) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf(cmd.FOUND_TOO_MANY_INSTANCES, "instance-name","update"))) }) }) diff --git a/internal/cmd/instance/update_sharing_instance.go b/internal/cmd/instance/update_sharing_instance.go new file mode 100644 index 00000000..d86329e2 --- /dev/null +++ b/internal/cmd/instance/update_sharing_instance.go @@ -0,0 +1,120 @@ +/* + * Copyright 2018 The Service Manager Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package instance + +import ( + "github.com/Peripli/service-manager-cli/internal/cmd" + "github.com/Peripli/service-manager-cli/internal/output" + "github.com/Peripli/service-manager-cli/pkg/query" + "github.com/Peripli/service-manager-cli/pkg/types" + "github.com/Peripli/service-manager/pkg/web" + + "fmt" + + "github.com/spf13/cobra" +) + +type UpdateSharingCmd struct { + *cmd.Context + instanceName string + instanceID string + outputFormat output.Format + share bool + action string +} + +// NewUpdateSharingCmd returns new share/unshare instance command with context +func NewUpdateSharingCmd(context *cmd.Context, share bool) *UpdateSharingCmd { + return &UpdateSharingCmd{Context: context, share: share} +} + +// Prepare returns cobra command +func (shc *UpdateSharingCmd) Prepare(prepare cmd.PrepareFunc) *cobra.Command { + result := &cobra.Command{ + PreRunE: prepare(shc, shc.Context), + RunE: cmd.RunE(shc), + } + if shc.share { + shc.action = "share" + result.Use = "share-instance [name] --id service-instance-id " + result.Short = "Share a service instance" + result.Long = `Share a service instance so that it can be consumed from various platforms in your subaccount. +Instance can be shared only if it was created with the plan that supports instance sharing. For more information, see the documentation of the service whose instance you want to share. To check if the service instance was created with a shareable plan, use 'smctl list-plans'.` + } else { + shc.action = "unshare" + result.Use = "unshare-instance [name] --id service-instance-id " + result.Short = "Unshare a service instance" + result.Long = `Unshare a service instance to disable its consumption from any but the original platform in which it was created in your subaccount. If an instance you want to unshare has references, an error is returned` + } + result.Flags().StringVarP(&shc.instanceID, "id", "", "", cmd.INSTANCE_ID_DESCRIPTION) + cmd.AddFormatFlag(result.Flags()) + return result +} + +// Validate validates command's arguments +func (shc *UpdateSharingCmd) Validate(args []string) error { + if len(args) < 1 { + return fmt.Errorf("service instance name is required") + } + shc.instanceName = args[0] + return nil +} + +// Run runs the command's logic +func (shc *UpdateSharingCmd) Run() error { + if shc.instanceID == "" { + instances, err := shc.Client.ListInstances(&query.Parameters{ + FieldQuery: []string{ + fmt.Sprintf("name eq '%s'", shc.instanceName), + }, + GeneralParams: shc.Parameters.GeneralParams, + }) + if err != nil { + return err + } + if len(instances.ServiceInstances) == 0 { + return fmt.Errorf(cmd.NO_INSTANCES_FOUND, shc.instanceName) + } + + if len(instances.ServiceInstances) > 1 { + return fmt.Errorf(cmd.FOUND_TOO_MANY_INSTANCES, shc.instanceName, shc.action) + } + + shc.instanceID = instances.ServiceInstances[0].ID + } + shc.Parameters.GeneralParams = append(shc.Parameters.GeneralParams, fmt.Sprintf("%s=%s", web.QueryParamAsync, "false")) + shared:=new(bool) + *shared = shc.share + resultInstance, _, err := shc.Client.UpdateInstance(shc.instanceID, &types.ServiceInstance{ + Shared: shared, + }, &query.Parameters{ + GeneralParams: shc.Parameters.GeneralParams, + }) + if err != nil { + output.PrintMessage(shc.Output, fmt.Sprintf("Couldn't %s the service instance. ", shc.action)) + return err + } + + output.PrintServiceManagerObject(shc.Output, shc.outputFormat, resultInstance) + output.Println(shc.Output) + return nil +} + +// SetOutputFormat set output format +func (shc *UpdateSharingCmd) SetOutputFormat(format output.Format) { + shc.outputFormat = format +} diff --git a/internal/cmd/instance/update_sharing_instance_test.go b/internal/cmd/instance/update_sharing_instance_test.go new file mode 100644 index 00000000..fde72da4 --- /dev/null +++ b/internal/cmd/instance/update_sharing_instance_test.go @@ -0,0 +1,209 @@ +/* + * Copyright 2018 The Service Manager Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package instance + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/Peripli/service-manager-cli/internal/cmd" + "github.com/Peripli/service-manager-cli/pkg/smclient/smclientfakes" + "github.com/Peripli/service-manager-cli/pkg/types" + "github.com/Peripli/service-manager/pkg/util" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + "io/ioutil" + "net/http" +) + +var _ = Describe("Update sharing instance command test", func() { + var client *smclientfakes.FakeClient + var command *UpdateSharingCmd + var buffer *bytes.Buffer + var instance *types.ServiceInstance + var instances *types.ServiceInstances + validUpdateSharingExecution := func(args ...string) *cobra.Command { + instance = &types.ServiceInstance{ + Name: args[0], + } + for i, arg := range args { + if arg == "--id" { + instance.ID = args[i+1] + } + } + operation := &types.Operation{ + State: "in progress", + } + + client.StatusReturns(operation, nil) + client.ListInstancesReturns(&types.ServiceInstances{ServiceInstances: []types.ServiceInstance{*instance}}, nil) + client.UpdateInstanceReturns(instance, "", nil) + piCmd := command.Prepare(cmd.SmPrepare) + piCmd.SetArgs(args) + Expect(piCmd.Execute()).ToNot(HaveOccurred()) + return piCmd + } + + invalidUpdateSharingCommandExecution := func(args ...string) error { + shCmd := command.Prepare(cmd.SmPrepare) + shCmd.SetArgs(args) + return shCmd.Execute() + } + type testCase struct { + share bool + commandName string + } + tests := []testCase{ + testCase{true, "share"}, + testCase{false, "unshare"}, + } + for _, test := range tests { + Describe(fmt.Sprintf("%s instance", test.commandName), func() { + BeforeEach(func() { + buffer = &bytes.Buffer{} + client = &smclientfakes.FakeClient{} + context := &cmd.Context{Output: buffer, Client: client} + command = NewUpdateSharingCmd(context, test.share) + }) + + Context("valid sync", func() { + Context("with name", func() { + It("should print object", func() { + validUpdateSharingExecution("myinstancename") + tableOutputExpected := instance.TableData().String() + Expect(buffer.String()).To(ContainSubstring(tableOutputExpected)) + }) + + }) + Context("with id", func() { + It("should print object", func() { + validUpdateSharingExecution("myinstancename", "--id", "serviceinstanceid") + tableOutputExpected := instance.TableData().String() + Expect(buffer.String()).To(ContainSubstring(tableOutputExpected)) + }) + }) + + Context("with json output flag", func() { + It("should be printed in json output format", func() { + validUpdateSharingExecution("instance-name", "--output", "json") + jsonByte, _ := json.MarshalIndent(instance, "", " ") + jsonOutputExpected := string(jsonByte) + "\n" + Expect(buffer.String()).To(ContainSubstring(jsonOutputExpected)) + }) + }) + + Context("with yaml output flag", func() { + It("should be printed in yaml output format", func() { + validUpdateSharingExecution("instance-name", "--output", "yaml") + yamlByte, _ := yaml.Marshal(instance) + yamlOutputExpected := string(yamlByte) + "\n" + Expect(buffer.String()).To(ContainSubstring(yamlOutputExpected)) + }) + }) + }) + + Context("invalid execution", func() { + JustBeforeEach(func() { + client.ListInstancesReturns(instances, nil) + + }) + + When("invalid flag", func() { + It("should return an error", func() { + err := invalidUpdateSharingCommandExecution("instance name", "--fl", "fff") + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unknown flag: --fl")) + }) + }) + When("async is used", func() { + It("should return an error", func() { + err := invalidUpdateSharingCommandExecution("instance name", "--mode", "async") + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unknown flag: --mode")) + }) + }) + + When("service instance not found", func() { + BeforeEach(func() { + instances = &types.ServiceInstances{} + }) + + It("should return an error", func() { + err := invalidUpdateSharingCommandExecution("instance-name") + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf(cmd.NO_INSTANCES_FOUND, "instance-name"))) + }) + + }) + Context("when more than once instance found", func() { + BeforeEach(func() { + client.ListInstancesReturnsOnCall(0, &types.ServiceInstances{ + ServiceInstances: []types.ServiceInstance{ + types.ServiceInstance{ + Name: "instance-name", + }, + types.ServiceInstance{ + Name: "instance-name", + }, + }, + }, nil) + }) + It("should return an error", func() { + err := invalidUpdateSharingCommandExecution("instance-name") + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf(cmd.FOUND_TOO_MANY_INSTANCES, "instance-name", test.commandName))) + }) + + }) + Context("backend error", func() { + BeforeEach(func() { + client.ListInstancesReturnsOnCall(0, &types.ServiceInstances{ + ServiceInstances: []types.ServiceInstance{ + types.ServiceInstance{ + Name: "instance-name", + }, + }, + }, nil) + body := ioutil.NopCloser(bytes.NewReader([]byte("HTTP response error"))) + expectedError := util.HandleResponseError(&http.Response{Body: body}) + client.UpdateInstanceReturns(nil, "", expectedError) + }) + It("should return error's description", func() { + err := invalidUpdateSharingCommandExecution("instance-name") + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("HTTP response error")) + Expect(buffer.String()).To(ContainSubstring(fmt.Sprintf("Couldn't %s the service instance. ", test.commandName))) + + }) + + }) + + Context("with invalid output format", func() { + It("should return error", func() { + invFormat := "invalid-format" + err := invalidUpdateSharingCommandExecution("instance name", "--output", invFormat) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(Equal("unknown output: " + invFormat)) + }) + }) + + }) + + }) + + } +}) diff --git a/internal/cmd/messages.go b/internal/cmd/messages.go new file mode 100644 index 00000000..bc860b0e --- /dev/null +++ b/internal/cmd/messages.go @@ -0,0 +1,5 @@ +package cmd + +const NO_INSTANCES_FOUND = "Couldn't find an instance with the name \"%s\"." +const INSTANCE_ID_DESCRIPTION = "The instance ID. Required if there is more than one instance with the same name." +const FOUND_TOO_MANY_INSTANCES = "Found more than one instance with the name \"%s\". Use the --id flag to specify which instance to %s." diff --git a/main.go b/main.go index dcf7d8bc..2b2f55e3 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,9 @@ func main() { instance.NewDeprovisionCmd(cmdContext, os.Stdin), instance.NewTransferCmd(cmdContext, os.Stdin), instance.NewUpdateInstanceCmd(cmdContext), + instance.NewUpdateSharingCmd(cmdContext,true), + instance.NewUpdateSharingCmd(cmdContext,false), + }, PrepareFn: cmd.SmPrepare, diff --git a/pkg/types/service_instance.go b/pkg/types/service_instance.go index 16768565..0c4d0bf1 100644 --- a/pkg/types/service_instance.go +++ b/pkg/types/service_instance.go @@ -19,8 +19,6 @@ package types import ( "encoding/json" "fmt" - "strconv" - "github.com/Peripli/service-manager/pkg/types" ) @@ -42,9 +40,9 @@ type ServiceInstance struct { Context json.RawMessage `json:"context,omitempty" yaml:"context,omitempty"` PreviousValues json.RawMessage `json:"-" yaml:"-"` - Ready bool `json:"ready" yaml:"ready"` - Usable bool `json:"usable" yaml:"usable"` - + Ready *bool `json:"ready,omitempty" yaml:"ready,omitempty"` + Usable *bool `json:"usable,omitempty" yaml:"usable,omitempty"` + Shared *bool `json:"shared,omitempty" yaml:"shared,omitempty"` LastOperation *types.Operation `json:"last_operation,omitempty" yaml:"last_operation,omitempty"` } @@ -61,13 +59,13 @@ func (si *ServiceInstance) IsEmpty() bool { // TableData returns the data to populate a table func (si *ServiceInstance) TableData() *TableData { result := &TableData{Vertical: true} - result.Headers = []string{"ID", "Name", "Service Plan ID", "Platform ID", "Created", "Updated", "Ready", "Usable", "Labels", "Last Op"} + result.Headers = []string{"ID", "Name", "Service Plan ID", "Platform ID", "Shared", "Created", "Updated", "Ready", "Usable", "Labels", "Last Op"} lastState := "-" if si.LastOperation != nil { lastState = formatLastOp(si.LastOperation) } - row := []string{si.ID, si.Name, si.ServicePlanID, si.PlatformID, si.CreatedAt, si.UpdatedAt, strconv.FormatBool(si.Ready), strconv.FormatBool(si.Usable), formatLabels(si.Labels), lastState} + row := []string{si.ID, si.Name, si.ServicePlanID, si.PlatformID, formatNullableBool(si.Shared), si.CreatedAt, si.UpdatedAt, formatNullableBool(si.Ready), formatNullableBool(si.Usable), formatLabels(si.Labels), lastState} result.Data = append(result.Data, row) return result @@ -102,7 +100,7 @@ func (si *ServiceInstances) IsEmpty() bool { // TableData returns the data to populate a table func (si *ServiceInstances) TableData() *TableData { result := &TableData{Vertical: si.Vertical} - result.Headers = []string{"ID", "Name", "Service Plan ID", "Platform ID", "Created", "Updated", "Ready", "Usable", "Labels"} + result.Headers = []string{"ID", "Name", "Service Plan ID", "Platform ID", "Shared", "Created", "Updated", "Ready", "Usable", "Labels"} addLastOpColumn := false for _, instance := range si.ServiceInstances { @@ -111,7 +109,7 @@ func (si *ServiceInstances) TableData() *TableData { lastState = formatLastOp(instance.LastOperation) addLastOpColumn = true } - row := []string{instance.ID, instance.Name, instance.ServicePlanID, instance.PlatformID, instance.CreatedAt, instance.UpdatedAt, strconv.FormatBool(instance.Ready), strconv.FormatBool(instance.Usable), formatLabels(instance.Labels), lastState} + row := []string{instance.ID, instance.Name, instance.ServicePlanID, instance.PlatformID, formatNullableBool(instance.Shared), instance.CreatedAt, instance.UpdatedAt, formatNullableBool(instance.Ready), formatNullableBool(instance.Usable), formatLabels(instance.Labels), lastState} result.Data = append(result.Data, row) } diff --git a/pkg/types/service_plan.go b/pkg/types/service_plan.go index c589f280..09da9044 100644 --- a/pkg/types/service_plan.go +++ b/pkg/types/service_plan.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "github.com/Peripli/service-manager/pkg/types" + "github.com/tidwall/gjson" "strconv" ) @@ -132,12 +133,20 @@ func (sp *ServicePlans) IsEmpty() bool { // TableData returns the data to populate a table func (sp *ServicePlans) TableData() *TableData { result := &TableData{} - result.Headers = []string{"ID", "Name", "Description", "Offering ID", "Ready", "Labels"} + result.Headers = []string{"ID", "Name", "Shareable", "Description", "Offering ID", "Ready", "Labels"} for _, v := range sp.ServicePlans { - row := []string{v.ID, v.Name, v.Description, v.ServiceOfferingID, strconv.FormatBool(v.Ready), formatLabels(v.Labels)} + row := []string{v.ID, v.Name, strconv.FormatBool(v.ShareableProperty()), v.Description, v.ServiceOfferingID, strconv.FormatBool(v.Ready), formatLabels(v.Labels)} result.Data = append(result.Data, row) } return result } + +func (sp *ServicePlan) ShareableProperty() bool { + if sp.Metadata == nil { + return false + } + return gjson.GetBytes(sp.Metadata, "supportInstanceSharing").Bool() + +} diff --git a/pkg/types/tableData.go b/pkg/types/tableData.go index f31682e6..9418f42d 100644 --- a/pkg/types/tableData.go +++ b/pkg/types/tableData.go @@ -156,6 +156,12 @@ func max(arr ...int) int { return tmp } +func formatNullableBool(val *bool) string{ + if val == nil { + return "false" + } + return fmt.Sprintf("%v", *val) +} func formatLabels(labels types.Labels) string { formattedLabels := make([]string, 0, len(labels)) for i, v := range labels {