diff --git a/cmd/delete_disk.go b/cmd/delete_disk.go index d32d1cdda..d65a27238 100644 --- a/cmd/delete_disk.go +++ b/cmd/delete_disk.go @@ -21,6 +21,10 @@ func (c DeleteDiskCmd) Run(opts DeleteDiskOpts) error { return err } + if opts.Dynamic { + return c.director.DeleteDynamicDisk(opts.Args.CID) + } + disk, err := c.director.FindOrphanDisk(opts.Args.CID) if err != nil { return err diff --git a/cmd/delete_disk_test.go b/cmd/delete_disk_test.go index 3b43c2c9a..3ea5a4b7a 100644 --- a/cmd/delete_disk_test.go +++ b/cmd/delete_disk_test.go @@ -80,5 +80,39 @@ var _ = Describe("DeleteDiskCmd", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("fake-err")) }) + + Context("when --dynamic", func() { + BeforeEach(func() { + deleteDiskOpts = opts.DeleteDiskOpts{ + Args: opts.DeleteDiskArgs{CID: "my-disk"}, + Dynamic: true, + } + }) + + It("deletes the dynamic disk", func() { + err := act() + Expect(err).ToNot(HaveOccurred()) + + Expect(director.DeleteDynamicDiskArgsForCall(0)).To(Equal("my-disk")) + }) + + It("returns error if deletion fails", func() { + director.DeleteDynamicDiskReturns(errors.New("fake-err")) + + err := act() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("fake-err")) + }) + + It("does not delete if confirmation is rejected", func() { + ui.AskedConfirmationErr = errors.New("stop") + + err := act() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("stop")) + + Expect(director.DeleteDynamicDiskCallCount()).To(Equal(0)) + }) + }) }) }) diff --git a/cmd/disks.go b/cmd/disks.go index b79909179..415a53b71 100644 --- a/cmd/disks.go +++ b/cmd/disks.go @@ -19,8 +19,16 @@ func NewDisksCmd(ui boshui.UI, director boshdir.Director) DisksCmd { } func (c DisksCmd) Run(opts DisksOpts) error { + if opts.Orphaned && opts.Dynamic { + return errors.New("Only one of --orphaned or --dynamic is supported") //nolint:staticcheck + } + + if opts.Dynamic { + return c.runDynamic() + } + if !opts.Orphaned { - return errors.New("Only --orphaned is supported") //nolint:staticcheck + return errors.New("Only --orphaned or --dynamic is supported") //nolint:staticcheck } disks, err := c.director.OrphanDisks() @@ -56,3 +64,42 @@ func (c DisksCmd) Run(opts DisksOpts) error { return nil } + +func (c DisksCmd) runDynamic() error { + disks, err := c.director.DynamicDisks() + if err != nil { + return err + } + + table := boshtbl.Table{ + Content: "dynamic disks", + Header: []boshtbl.Header{ + boshtbl.NewHeader("Name"), + boshtbl.NewHeader("Disk CID"), + boshtbl.NewHeader("Size"), + boshtbl.NewHeader("Disk Pool"), + boshtbl.NewHeader("Deployment"), + boshtbl.NewHeader("Instance"), + boshtbl.NewHeader("AZ"), + boshtbl.NewHeader("CPI"), + }, + SortBy: []boshtbl.ColumnSort{{Column: 0}}, + } + + for _, d := range disks { + table.Rows = append(table.Rows, []boshtbl.Value{ + boshtbl.NewValueString(d.Name()), + boshtbl.NewValueString(d.DiskCID()), + boshtbl.NewValueMegaBytes(d.Size()), + boshtbl.NewValueString(d.DiskPoolName()), + boshtbl.NewValueString(d.DeploymentName()), + boshtbl.NewValueString(d.InstanceName()), + boshtbl.NewValueString(d.AvailabilityZone()), + boshtbl.NewValueString(d.CPI()), + }) + } + + c.ui.PrintTable(table) + + return nil +} diff --git a/cmd/disks_test.go b/cmd/disks_test.go index 4cb4b67bb..66fbfb773 100644 --- a/cmd/disks_test.go +++ b/cmd/disks_test.go @@ -106,7 +106,77 @@ var _ = Describe("DisksCmd", func() { }) It("returns error if orphaned disks were not requested", func() { - Expect(act()).To(Equal(errors.New("Only --orphaned is supported"))) + Expect(act()).To(Equal(errors.New("Only --orphaned or --dynamic is supported"))) + }) + + It("returns error if both --orphaned and --dynamic are requested", func() { + disksOpts.Orphaned = true + disksOpts.Dynamic = true + Expect(act()).To(Equal(errors.New("Only one of --orphaned or --dynamic is supported"))) + }) + + Context("when dynamic disks requested", func() { + BeforeEach(func() { + disksOpts.Dynamic = true + }) + + It("lists dynamic disks", func() { + disks := []boshdir.DynamicDisk{ + &fakedir.FakeDynamicDisk{ + NameStub: func() string { return "my-disk" }, + DiskCIDStub: func() string { return "disk-cid-1" }, + SizeStub: func() uint64 { return 2048 }, + DiskPoolNameStub: func() string { return "large" }, + DeploymentNameStub: func() string { return "my-deployment" }, + InstanceNameStub: func() string { return "api/abc123" }, + AvailabilityZoneStub: func() string { return "z1" }, + CPIStub: func() string { return "aws_cpi" }, + }, + } + + director.DynamicDisksReturns(disks, nil) + + err := act() + Expect(err).ToNot(HaveOccurred()) + + Expect(ui.Table).To(Equal(boshtbl.Table{ + Content: "dynamic disks", + + Header: []boshtbl.Header{ + boshtbl.NewHeader("Name"), + boshtbl.NewHeader("Disk CID"), + boshtbl.NewHeader("Size"), + boshtbl.NewHeader("Disk Pool"), + boshtbl.NewHeader("Deployment"), + boshtbl.NewHeader("Instance"), + boshtbl.NewHeader("AZ"), + boshtbl.NewHeader("CPI"), + }, + + SortBy: []boshtbl.ColumnSort{{Column: 0}}, + + Rows: [][]boshtbl.Value{ + { + boshtbl.NewValueString("my-disk"), + boshtbl.NewValueString("disk-cid-1"), + boshtbl.NewValueMegaBytes(2048), + boshtbl.NewValueString("large"), + boshtbl.NewValueString("my-deployment"), + boshtbl.NewValueString("api/abc123"), + boshtbl.NewValueString("z1"), + boshtbl.NewValueString("aws_cpi"), + }, + }, + })) + }) + + It("returns error if dynamic disks cannot be retrieved", func() { + director.DynamicDisksReturns(nil, errors.New("fake-err")) + + err := act() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("fake-err")) + }) }) }) }) diff --git a/cmd/opts/opts.go b/cmd/opts/opts.go index 4be6b19f9..7123e7dfa 100644 --- a/cmd/opts/opts.go +++ b/cmd/opts/opts.go @@ -745,16 +745,18 @@ type DeleteNetworkArgs struct { type DisksOpts struct { Orphaned bool `long:"orphaned" short:"o" description:"List orphaned disks"` + Dynamic bool `long:"dynamic" description:"List dynamic disks"` cmd } type DeleteDiskOpts struct { - Args DeleteDiskArgs `positional-args:"true" required:"true"` + Args DeleteDiskArgs `positional-args:"true" required:"true"` + Dynamic bool `long:"dynamic" description:"Delete a dynamic disk by name"` cmd } type DeleteDiskArgs struct { - CID string `positional-arg-name:"CID"` + CID string `positional-arg-name:"CID-OR-NAME"` } type OrphanDiskOpts struct { diff --git a/cmd/opts/opts_test.go b/cmd/opts/opts_test.go index bccf004b8..23a211524 100644 --- a/cmd/opts/opts_test.go +++ b/cmd/opts/opts_test.go @@ -2205,6 +2205,14 @@ var _ = Describe("Opts", func() { )) }) }) + + Describe("Dynamic", func() { + It("contains desired values", func() { + Expect(getStructTagForName("Dynamic", opts)).To(Equal( + `long:"dynamic" description:"List dynamic disks"`, + )) + }) + }) }) Describe("DeleteDiskOpts", func() { @@ -2219,6 +2227,14 @@ var _ = Describe("Opts", func() { Expect(getStructTagForName("Args", opts)).To(Equal(`positional-args:"true" required:"true"`)) }) }) + + Describe("Dynamic", func() { + It("contains desired values", func() { + Expect(getStructTagForName("Dynamic", opts)).To(Equal( + `long:"dynamic" description:"Delete a dynamic disk by name"`, + )) + }) + }) }) Describe("DeleteDiskArgs", func() { @@ -2231,7 +2247,7 @@ var _ = Describe("Opts", func() { Describe("CID", func() { It("contains desired values", func() { Expect(getStructTagForName("CID", opts)).To(Equal( - `positional-arg-name:"CID"`, + `positional-arg-name:"CID-OR-NAME"`, )) }) }) diff --git a/director/directorfakes/fake_director.go b/director/directorfakes/fake_director.go index 005417839..47d30e131 100644 --- a/director/directorfakes/fake_director.go +++ b/director/directorfakes/fake_director.go @@ -87,6 +87,17 @@ type FakeDirector struct { result1 bool result2 error } + DeleteDynamicDiskStub func(string) error + deleteDynamicDiskMutex sync.RWMutex + deleteDynamicDiskArgsForCall []struct { + arg1 string + } + deleteDynamicDiskReturns struct { + result1 error + } + deleteDynamicDiskReturnsOnCall map[int]struct { + result1 error + } DeploymentsStub func() ([]director.Deployment, error) deploymentsMutex sync.RWMutex deploymentsArgsForCall []struct { @@ -185,6 +196,18 @@ type FakeDirector struct { downloadResourceUncheckedReturnsOnCall map[int]struct { result1 error } + DynamicDisksStub func() ([]director.DynamicDisk, error) + dynamicDisksMutex sync.RWMutex + dynamicDisksArgsForCall []struct { + } + dynamicDisksReturns struct { + result1 []director.DynamicDisk + result2 error + } + dynamicDisksReturnsOnCall map[int]struct { + result1 []director.DynamicDisk + result2 error + } EnableResurrectionStub func(bool) error enableResurrectionMutex sync.RWMutex enableResurrectionArgsForCall []struct { @@ -1101,6 +1124,67 @@ func (fake *FakeDirector) DeleteConfigByIDReturnsOnCall(i int, result1 bool, res }{result1, result2} } +func (fake *FakeDirector) DeleteDynamicDisk(arg1 string) error { + fake.deleteDynamicDiskMutex.Lock() + ret, specificReturn := fake.deleteDynamicDiskReturnsOnCall[len(fake.deleteDynamicDiskArgsForCall)] + fake.deleteDynamicDiskArgsForCall = append(fake.deleteDynamicDiskArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.DeleteDynamicDiskStub + fakeReturns := fake.deleteDynamicDiskReturns + fake.recordInvocation("DeleteDynamicDisk", []interface{}{arg1}) + fake.deleteDynamicDiskMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDirector) DeleteDynamicDiskCallCount() int { + fake.deleteDynamicDiskMutex.RLock() + defer fake.deleteDynamicDiskMutex.RUnlock() + return len(fake.deleteDynamicDiskArgsForCall) +} + +func (fake *FakeDirector) DeleteDynamicDiskCalls(stub func(string) error) { + fake.deleteDynamicDiskMutex.Lock() + defer fake.deleteDynamicDiskMutex.Unlock() + fake.DeleteDynamicDiskStub = stub +} + +func (fake *FakeDirector) DeleteDynamicDiskArgsForCall(i int) string { + fake.deleteDynamicDiskMutex.RLock() + defer fake.deleteDynamicDiskMutex.RUnlock() + argsForCall := fake.deleteDynamicDiskArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeDirector) DeleteDynamicDiskReturns(result1 error) { + fake.deleteDynamicDiskMutex.Lock() + defer fake.deleteDynamicDiskMutex.Unlock() + fake.DeleteDynamicDiskStub = nil + fake.deleteDynamicDiskReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeDirector) DeleteDynamicDiskReturnsOnCall(i int, result1 error) { + fake.deleteDynamicDiskMutex.Lock() + defer fake.deleteDynamicDiskMutex.Unlock() + fake.DeleteDynamicDiskStub = nil + if fake.deleteDynamicDiskReturnsOnCall == nil { + fake.deleteDynamicDiskReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteDynamicDiskReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeDirector) Deployments() ([]director.Deployment, error) { fake.deploymentsMutex.Lock() ret, specificReturn := fake.deploymentsReturnsOnCall[len(fake.deploymentsArgsForCall)] @@ -1578,6 +1662,62 @@ func (fake *FakeDirector) DownloadResourceUncheckedReturnsOnCall(i int, result1 }{result1} } +func (fake *FakeDirector) DynamicDisks() ([]director.DynamicDisk, error) { + fake.dynamicDisksMutex.Lock() + ret, specificReturn := fake.dynamicDisksReturnsOnCall[len(fake.dynamicDisksArgsForCall)] + fake.dynamicDisksArgsForCall = append(fake.dynamicDisksArgsForCall, struct { + }{}) + stub := fake.DynamicDisksStub + fakeReturns := fake.dynamicDisksReturns + fake.recordInvocation("DynamicDisks", []interface{}{}) + fake.dynamicDisksMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeDirector) DynamicDisksCallCount() int { + fake.dynamicDisksMutex.RLock() + defer fake.dynamicDisksMutex.RUnlock() + return len(fake.dynamicDisksArgsForCall) +} + +func (fake *FakeDirector) DynamicDisksCalls(stub func() ([]director.DynamicDisk, error)) { + fake.dynamicDisksMutex.Lock() + defer fake.dynamicDisksMutex.Unlock() + fake.DynamicDisksStub = stub +} + +func (fake *FakeDirector) DynamicDisksReturns(result1 []director.DynamicDisk, result2 error) { + fake.dynamicDisksMutex.Lock() + defer fake.dynamicDisksMutex.Unlock() + fake.DynamicDisksStub = nil + fake.dynamicDisksReturns = struct { + result1 []director.DynamicDisk + result2 error + }{result1, result2} +} + +func (fake *FakeDirector) DynamicDisksReturnsOnCall(i int, result1 []director.DynamicDisk, result2 error) { + fake.dynamicDisksMutex.Lock() + defer fake.dynamicDisksMutex.Unlock() + fake.DynamicDisksStub = nil + if fake.dynamicDisksReturnsOnCall == nil { + fake.dynamicDisksReturnsOnCall = make(map[int]struct { + result1 []director.DynamicDisk + result2 error + }) + } + fake.dynamicDisksReturnsOnCall[i] = struct { + result1 []director.DynamicDisk + result2 error + }{result1, result2} +} + func (fake *FakeDirector) EnableResurrection(arg1 bool) error { fake.enableResurrectionMutex.Lock() ret, specificReturn := fake.enableResurrectionReturnsOnCall[len(fake.enableResurrectionArgsForCall)] diff --git a/director/directorfakes/fake_dynamic_disk.go b/director/directorfakes/fake_dynamic_disk.go new file mode 100644 index 000000000..feace2d98 --- /dev/null +++ b/director/directorfakes/fake_dynamic_disk.go @@ -0,0 +1,541 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package directorfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-cli/v7/director" +) + +type FakeDynamicDisk struct { + AvailabilityZoneStub func() string + availabilityZoneMutex sync.RWMutex + availabilityZoneArgsForCall []struct { + } + availabilityZoneReturns struct { + result1 string + } + availabilityZoneReturnsOnCall map[int]struct { + result1 string + } + CPIStub func() string + cPIMutex sync.RWMutex + cPIArgsForCall []struct { + } + cPIReturns struct { + result1 string + } + cPIReturnsOnCall map[int]struct { + result1 string + } + DeploymentNameStub func() string + deploymentNameMutex sync.RWMutex + deploymentNameArgsForCall []struct { + } + deploymentNameReturns struct { + result1 string + } + deploymentNameReturnsOnCall map[int]struct { + result1 string + } + DiskCIDStub func() string + diskCIDMutex sync.RWMutex + diskCIDArgsForCall []struct { + } + diskCIDReturns struct { + result1 string + } + diskCIDReturnsOnCall map[int]struct { + result1 string + } + DiskPoolNameStub func() string + diskPoolNameMutex sync.RWMutex + diskPoolNameArgsForCall []struct { + } + diskPoolNameReturns struct { + result1 string + } + diskPoolNameReturnsOnCall map[int]struct { + result1 string + } + InstanceNameStub func() string + instanceNameMutex sync.RWMutex + instanceNameArgsForCall []struct { + } + instanceNameReturns struct { + result1 string + } + instanceNameReturnsOnCall map[int]struct { + result1 string + } + NameStub func() string + nameMutex sync.RWMutex + nameArgsForCall []struct { + } + nameReturns struct { + result1 string + } + nameReturnsOnCall map[int]struct { + result1 string + } + SizeStub func() uint64 + sizeMutex sync.RWMutex + sizeArgsForCall []struct { + } + sizeReturns struct { + result1 uint64 + } + sizeReturnsOnCall map[int]struct { + result1 uint64 + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeDynamicDisk) AvailabilityZone() string { + fake.availabilityZoneMutex.Lock() + ret, specificReturn := fake.availabilityZoneReturnsOnCall[len(fake.availabilityZoneArgsForCall)] + fake.availabilityZoneArgsForCall = append(fake.availabilityZoneArgsForCall, struct { + }{}) + stub := fake.AvailabilityZoneStub + fakeReturns := fake.availabilityZoneReturns + fake.recordInvocation("AvailabilityZone", []interface{}{}) + fake.availabilityZoneMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) AvailabilityZoneCallCount() int { + fake.availabilityZoneMutex.RLock() + defer fake.availabilityZoneMutex.RUnlock() + return len(fake.availabilityZoneArgsForCall) +} + +func (fake *FakeDynamicDisk) AvailabilityZoneCalls(stub func() string) { + fake.availabilityZoneMutex.Lock() + defer fake.availabilityZoneMutex.Unlock() + fake.AvailabilityZoneStub = stub +} + +func (fake *FakeDynamicDisk) AvailabilityZoneReturns(result1 string) { + fake.availabilityZoneMutex.Lock() + defer fake.availabilityZoneMutex.Unlock() + fake.AvailabilityZoneStub = nil + fake.availabilityZoneReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) AvailabilityZoneReturnsOnCall(i int, result1 string) { + fake.availabilityZoneMutex.Lock() + defer fake.availabilityZoneMutex.Unlock() + fake.AvailabilityZoneStub = nil + if fake.availabilityZoneReturnsOnCall == nil { + fake.availabilityZoneReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.availabilityZoneReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) CPI() string { + fake.cPIMutex.Lock() + ret, specificReturn := fake.cPIReturnsOnCall[len(fake.cPIArgsForCall)] + fake.cPIArgsForCall = append(fake.cPIArgsForCall, struct { + }{}) + stub := fake.CPIStub + fakeReturns := fake.cPIReturns + fake.recordInvocation("CPI", []interface{}{}) + fake.cPIMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) CPICallCount() int { + fake.cPIMutex.RLock() + defer fake.cPIMutex.RUnlock() + return len(fake.cPIArgsForCall) +} + +func (fake *FakeDynamicDisk) CPICalls(stub func() string) { + fake.cPIMutex.Lock() + defer fake.cPIMutex.Unlock() + fake.CPIStub = stub +} + +func (fake *FakeDynamicDisk) CPIReturns(result1 string) { + fake.cPIMutex.Lock() + defer fake.cPIMutex.Unlock() + fake.CPIStub = nil + fake.cPIReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) CPIReturnsOnCall(i int, result1 string) { + fake.cPIMutex.Lock() + defer fake.cPIMutex.Unlock() + fake.CPIStub = nil + if fake.cPIReturnsOnCall == nil { + fake.cPIReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.cPIReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) DeploymentName() string { + fake.deploymentNameMutex.Lock() + ret, specificReturn := fake.deploymentNameReturnsOnCall[len(fake.deploymentNameArgsForCall)] + fake.deploymentNameArgsForCall = append(fake.deploymentNameArgsForCall, struct { + }{}) + stub := fake.DeploymentNameStub + fakeReturns := fake.deploymentNameReturns + fake.recordInvocation("DeploymentName", []interface{}{}) + fake.deploymentNameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) DeploymentNameCallCount() int { + fake.deploymentNameMutex.RLock() + defer fake.deploymentNameMutex.RUnlock() + return len(fake.deploymentNameArgsForCall) +} + +func (fake *FakeDynamicDisk) DeploymentNameCalls(stub func() string) { + fake.deploymentNameMutex.Lock() + defer fake.deploymentNameMutex.Unlock() + fake.DeploymentNameStub = stub +} + +func (fake *FakeDynamicDisk) DeploymentNameReturns(result1 string) { + fake.deploymentNameMutex.Lock() + defer fake.deploymentNameMutex.Unlock() + fake.DeploymentNameStub = nil + fake.deploymentNameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) DeploymentNameReturnsOnCall(i int, result1 string) { + fake.deploymentNameMutex.Lock() + defer fake.deploymentNameMutex.Unlock() + fake.DeploymentNameStub = nil + if fake.deploymentNameReturnsOnCall == nil { + fake.deploymentNameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.deploymentNameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) DiskCID() string { + fake.diskCIDMutex.Lock() + ret, specificReturn := fake.diskCIDReturnsOnCall[len(fake.diskCIDArgsForCall)] + fake.diskCIDArgsForCall = append(fake.diskCIDArgsForCall, struct { + }{}) + stub := fake.DiskCIDStub + fakeReturns := fake.diskCIDReturns + fake.recordInvocation("DiskCID", []interface{}{}) + fake.diskCIDMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) DiskCIDCallCount() int { + fake.diskCIDMutex.RLock() + defer fake.diskCIDMutex.RUnlock() + return len(fake.diskCIDArgsForCall) +} + +func (fake *FakeDynamicDisk) DiskCIDCalls(stub func() string) { + fake.diskCIDMutex.Lock() + defer fake.diskCIDMutex.Unlock() + fake.DiskCIDStub = stub +} + +func (fake *FakeDynamicDisk) DiskCIDReturns(result1 string) { + fake.diskCIDMutex.Lock() + defer fake.diskCIDMutex.Unlock() + fake.DiskCIDStub = nil + fake.diskCIDReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) DiskCIDReturnsOnCall(i int, result1 string) { + fake.diskCIDMutex.Lock() + defer fake.diskCIDMutex.Unlock() + fake.DiskCIDStub = nil + if fake.diskCIDReturnsOnCall == nil { + fake.diskCIDReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.diskCIDReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) DiskPoolName() string { + fake.diskPoolNameMutex.Lock() + ret, specificReturn := fake.diskPoolNameReturnsOnCall[len(fake.diskPoolNameArgsForCall)] + fake.diskPoolNameArgsForCall = append(fake.diskPoolNameArgsForCall, struct { + }{}) + stub := fake.DiskPoolNameStub + fakeReturns := fake.diskPoolNameReturns + fake.recordInvocation("DiskPoolName", []interface{}{}) + fake.diskPoolNameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) DiskPoolNameCallCount() int { + fake.diskPoolNameMutex.RLock() + defer fake.diskPoolNameMutex.RUnlock() + return len(fake.diskPoolNameArgsForCall) +} + +func (fake *FakeDynamicDisk) DiskPoolNameCalls(stub func() string) { + fake.diskPoolNameMutex.Lock() + defer fake.diskPoolNameMutex.Unlock() + fake.DiskPoolNameStub = stub +} + +func (fake *FakeDynamicDisk) DiskPoolNameReturns(result1 string) { + fake.diskPoolNameMutex.Lock() + defer fake.diskPoolNameMutex.Unlock() + fake.DiskPoolNameStub = nil + fake.diskPoolNameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) DiskPoolNameReturnsOnCall(i int, result1 string) { + fake.diskPoolNameMutex.Lock() + defer fake.diskPoolNameMutex.Unlock() + fake.DiskPoolNameStub = nil + if fake.diskPoolNameReturnsOnCall == nil { + fake.diskPoolNameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.diskPoolNameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) InstanceName() string { + fake.instanceNameMutex.Lock() + ret, specificReturn := fake.instanceNameReturnsOnCall[len(fake.instanceNameArgsForCall)] + fake.instanceNameArgsForCall = append(fake.instanceNameArgsForCall, struct { + }{}) + stub := fake.InstanceNameStub + fakeReturns := fake.instanceNameReturns + fake.recordInvocation("InstanceName", []interface{}{}) + fake.instanceNameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) InstanceNameCallCount() int { + fake.instanceNameMutex.RLock() + defer fake.instanceNameMutex.RUnlock() + return len(fake.instanceNameArgsForCall) +} + +func (fake *FakeDynamicDisk) InstanceNameCalls(stub func() string) { + fake.instanceNameMutex.Lock() + defer fake.instanceNameMutex.Unlock() + fake.InstanceNameStub = stub +} + +func (fake *FakeDynamicDisk) InstanceNameReturns(result1 string) { + fake.instanceNameMutex.Lock() + defer fake.instanceNameMutex.Unlock() + fake.InstanceNameStub = nil + fake.instanceNameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) InstanceNameReturnsOnCall(i int, result1 string) { + fake.instanceNameMutex.Lock() + defer fake.instanceNameMutex.Unlock() + fake.InstanceNameStub = nil + if fake.instanceNameReturnsOnCall == nil { + fake.instanceNameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.instanceNameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) Name() string { + fake.nameMutex.Lock() + ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)] + fake.nameArgsForCall = append(fake.nameArgsForCall, struct { + }{}) + stub := fake.NameStub + fakeReturns := fake.nameReturns + fake.recordInvocation("Name", []interface{}{}) + fake.nameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) NameCallCount() int { + fake.nameMutex.RLock() + defer fake.nameMutex.RUnlock() + return len(fake.nameArgsForCall) +} + +func (fake *FakeDynamicDisk) NameCalls(stub func() string) { + fake.nameMutex.Lock() + defer fake.nameMutex.Unlock() + fake.NameStub = stub +} + +func (fake *FakeDynamicDisk) NameReturns(result1 string) { + fake.nameMutex.Lock() + defer fake.nameMutex.Unlock() + fake.NameStub = nil + fake.nameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) NameReturnsOnCall(i int, result1 string) { + fake.nameMutex.Lock() + defer fake.nameMutex.Unlock() + fake.NameStub = nil + if fake.nameReturnsOnCall == nil { + fake.nameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.nameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeDynamicDisk) Size() uint64 { + fake.sizeMutex.Lock() + ret, specificReturn := fake.sizeReturnsOnCall[len(fake.sizeArgsForCall)] + fake.sizeArgsForCall = append(fake.sizeArgsForCall, struct { + }{}) + stub := fake.SizeStub + fakeReturns := fake.sizeReturns + fake.recordInvocation("Size", []interface{}{}) + fake.sizeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeDynamicDisk) SizeCallCount() int { + fake.sizeMutex.RLock() + defer fake.sizeMutex.RUnlock() + return len(fake.sizeArgsForCall) +} + +func (fake *FakeDynamicDisk) SizeCalls(stub func() uint64) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = stub +} + +func (fake *FakeDynamicDisk) SizeReturns(result1 uint64) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + fake.sizeReturns = struct { + result1 uint64 + }{result1} +} + +func (fake *FakeDynamicDisk) SizeReturnsOnCall(i int, result1 uint64) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + if fake.sizeReturnsOnCall == nil { + fake.sizeReturnsOnCall = make(map[int]struct { + result1 uint64 + }) + } + fake.sizeReturnsOnCall[i] = struct { + result1 uint64 + }{result1} +} + +func (fake *FakeDynamicDisk) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeDynamicDisk) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ director.DynamicDisk = new(FakeDynamicDisk) diff --git a/director/dynamic_disks.go b/director/dynamic_disks.go new file mode 100644 index 000000000..075734389 --- /dev/null +++ b/director/dynamic_disks.go @@ -0,0 +1,86 @@ +package director + +import ( + "fmt" + "net/url" + + bosherr "github.com/cloudfoundry/bosh-utils/errors" +) + +type DynamicDiskImpl struct { + name string + diskCID string + deploymentName string + instanceName string + availabilityZone string + size uint64 + diskPoolName string + cpi string +} + +func (d DynamicDiskImpl) Name() string { return d.name } +func (d DynamicDiskImpl) DiskCID() string { return d.diskCID } +func (d DynamicDiskImpl) DeploymentName() string { return d.deploymentName } +func (d DynamicDiskImpl) InstanceName() string { return d.instanceName } +func (d DynamicDiskImpl) AvailabilityZone() string { return d.availabilityZone } +func (d DynamicDiskImpl) Size() uint64 { return d.size } +func (d DynamicDiskImpl) DiskPoolName() string { return d.diskPoolName } +func (d DynamicDiskImpl) CPI() string { return d.cpi } + +type DynamicDiskResp struct { + Name string `json:"name"` + DiskCID string `json:"disk_cid"` + Deployment string `json:"deployment"` + Instance string `json:"instance"` + AvailabilityZone string `json:"availability_zone"` + Size uint64 `json:"size"` + DiskPoolName string `json:"disk_pool_name"` + CPI string `json:"cpi"` +} + +// --- DirectorImpl delegation --- + +func (d DirectorImpl) DeleteDynamicDisk(diskName string) error { + return d.client.DeleteDynamicDisk(diskName) +} + +func (d DirectorImpl) DynamicDisks() ([]DynamicDisk, error) { + return d.client.DynamicDisks() +} + +// --- HTTP Client methods --- + +func (c Client) DeleteDynamicDisk(diskName string) error { + if len(diskName) == 0 { + return bosherr.Error("Expected non-empty dynamic disk name") + } + + path := fmt.Sprintf("/dynamic_disks/%s", url.PathEscape(diskName)) + _, err := c.taskClientRequest.DeleteResult(path) + if err != nil { + return bosherr.WrapErrorf(err, "Deleting dynamic disk '%s'", diskName) + } + return nil +} + +func (c Client) DynamicDisks() ([]DynamicDisk, error) { + var resps []DynamicDiskResp + if err := c.clientRequest.Get("/dynamic_disks", &resps); err != nil { + return nil, bosherr.WrapError(err, "Listing dynamic disks") + } + + var disks []DynamicDisk + for _, r := range resps { + disks = append(disks, DynamicDiskImpl{ + name: r.Name, + diskCID: r.DiskCID, + deploymentName: r.Deployment, + instanceName: r.Instance, + availabilityZone: r.AvailabilityZone, + size: r.Size, + diskPoolName: r.DiskPoolName, + cpi: r.CPI, + }) + } + return disks, nil +} diff --git a/director/dynamic_disks_test.go b/director/dynamic_disks_test.go new file mode 100644 index 000000000..66de4a885 --- /dev/null +++ b/director/dynamic_disks_test.go @@ -0,0 +1,155 @@ +package director_test + +import ( + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/ghttp" + + . "github.com/cloudfoundry/bosh-cli/v7/director" +) + +var _ = Describe("Director", func() { + var ( + director Director + server *ghttp.Server + ) + + BeforeEach(func() { + director, server = BuildServer() + }) + + AfterEach(func() { + server.Close() + }) + + Describe("DynamicDisks", func() { + It("returns dynamic disks", func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/dynamic_disks"), + ghttp.VerifyBasicAuth("username", "password"), + ghttp.RespondWith(http.StatusOK, `[ + { + "name": "disk1", + "disk_cid": "cid1", + "deployment": "dep1", + "instance": "instance1", + "availability_zone": "az1", + "size": 1000, + "disk_pool_name": "small", + "cpi": "cpi1" + }, + { + "name": "disk2", + "disk_cid": "cid2", + "deployment": "dep2", + "instance": "instance2", + "availability_zone": "az2", + "size": 2000, + "disk_pool_name": "large", + "cpi": "cpi2" + } +]`), + ), + ) + + disks, err := director.DynamicDisks() + Expect(err).ToNot(HaveOccurred()) + Expect(disks).To(HaveLen(2)) + + Expect(disks[0].Name()).To(Equal("disk1")) + Expect(disks[0].DiskCID()).To(Equal("cid1")) + Expect(disks[0].DeploymentName()).To(Equal("dep1")) + Expect(disks[0].InstanceName()).To(Equal("instance1")) + Expect(disks[0].AvailabilityZone()).To(Equal("az1")) + Expect(disks[0].Size()).To(Equal(uint64(1000))) + Expect(disks[0].DiskPoolName()).To(Equal("small")) + Expect(disks[0].CPI()).To(Equal("cpi1")) + + Expect(disks[1].Name()).To(Equal("disk2")) + Expect(disks[1].DiskCID()).To(Equal("cid2")) + Expect(disks[1].DeploymentName()).To(Equal("dep2")) + Expect(disks[1].InstanceName()).To(Equal("instance2")) + Expect(disks[1].AvailabilityZone()).To(Equal("az2")) + Expect(disks[1].Size()).To(Equal(uint64(2000))) + Expect(disks[1].DiskPoolName()).To(Equal("large")) + Expect(disks[1].CPI()).To(Equal("cpi2")) + }) + + It("returns error if response is non-200", func() { + AppendBadRequest(ghttp.VerifyRequest("GET", "/dynamic_disks"), server) + + _, err := director.DynamicDisks() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "Listing dynamic disks: Director responded with non-successful status code")) + }) + + It("returns error if response cannot be unmarshalled", func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/dynamic_disks"), + ghttp.RespondWith(http.StatusOK, ``), + ), + ) + + _, err := director.DynamicDisks() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "Listing dynamic disks: Unmarshaling Director response")) + }) + }) + + Describe("DeleteDynamicDisk", func() { + It("deletes dynamic disk by name", func() { + ConfigureTaskResult( + ghttp.CombineHandlers( + ghttp.VerifyRequest("DELETE", "/dynamic_disks/my-disk"), + ghttp.VerifyBasicAuth("username", "password"), + ), + "", + server, + ) + + Expect(director.DeleteDynamicDisk("my-disk")).ToNot(HaveOccurred()) + }) + + It("does url encoding for disk name", func() { + var verifyRawPath = func(path string) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + Expect(req.RequestURI).To(Equal(path)) + } + } + + ConfigureTaskResult( + ghttp.CombineHandlers( + verifyRawPath("/dynamic_disks/my%3Bdisk"), + ghttp.VerifyRequest("DELETE", "/dynamic_disks/my;disk"), + ghttp.VerifyBasicAuth("username", "password"), + ), + "", + server, + ) + + Expect(director.DeleteDynamicDisk("my;disk")).ToNot(HaveOccurred()) + }) + + It("returns error without contacting the director if disk name is empty", func() { + err := director.DeleteDynamicDisk("") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Expected non-empty dynamic disk name")) + Expect(server.ReceivedRequests()).To(BeEmpty()) + }) + + It("returns error if response is non-200", func() { + AppendBadRequest(ghttp.VerifyRequest("DELETE", "/dynamic_disks/my-disk"), server) + + err := director.DeleteDynamicDisk("my-disk") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "Deleting dynamic disk 'my-disk': Director responded with non-successful status code")) + }) + }) +}) diff --git a/director/interfaces.go b/director/interfaces.go index f68c0867b..be7560ffb 100644 --- a/director/interfaces.go +++ b/director/interfaces.go @@ -77,6 +77,10 @@ type Director interface { OrphanDisks() ([]OrphanDisk, error) OrphanDisk(string) error + // Dynamic disk operations (TNZ-99509, TNZ-109499) + DeleteDynamicDisk(diskName string) error + DynamicDisks() ([]DynamicDisk, error) + FindOrphanNetwork(string) (OrphanNetwork, error) OrphanNetworks() ([]OrphanNetwork, error) @@ -322,6 +326,19 @@ type OrphanDisk interface { Delete() error } +//counterfeiter:generate . DynamicDisk + +type DynamicDisk interface { + Name() string + DiskCID() string + DeploymentName() string + InstanceName() string + AvailabilityZone() string + Size() uint64 + DiskPoolName() string + CPI() string +} + //counterfeiter:generate . OrphanNetwork type OrphanNetwork interface {