diff --git a/args.go b/args.go index 43599d5ea..787fb3f5e 100644 --- a/args.go +++ b/args.go @@ -525,6 +525,10 @@ const ( // ArgInterconnectAttachmentType is the type of the Interconnect Attachment e.g. "partner". ArgInterconnectAttachmentType = "type" + // ArgPartnerInterconnectAttachmentName is a name of the Partner Interconnect Attachment. + ArgPartnerInterconnectAttachmentName = "name" + // ArgPartnerInterconnectAttachmentVPCIDs are the IDs of the VPCs which the Partner Interconnect Attachment is connected + ArgPartnerInterconnectAttachmentVPCIDs = "vpc-ids" // ArgReadWrite indicates a generated token should be read/write. ArgReadWrite = "read-write" diff --git a/commands/partner_interconnect_attachments.go b/commands/partner_interconnect_attachments.go index 32940d1b8..784d6e401 100644 --- a/commands/partner_interconnect_attachments.go +++ b/commands/partner_interconnect_attachments.go @@ -2,7 +2,9 @@ package commands import ( "fmt" + "os" "strings" + "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -10,6 +12,7 @@ import ( "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" "github.com/digitalocean/doctl/do" + "github.com/digitalocean/godo" ) // Network creates the partner commands @@ -68,6 +71,22 @@ With the Partner Interconnect Attachments commands, you can get or list, create, cmdPartnerIAList.Example = `The following example lists the Network Interconnect Attachments on your account :" + " doctl network --type "partner" interconnect-attachment list --format Name,VPCIDs` + cmdPartnerIADelete := CmdBuilder(cmd, RunPartnerInterconnectAttachmentDelete, "delete ", + "Deletes a Partner Interconnect Attachment", "Deletes information about a Partner Interconnect Attachment. This is irreversible ", Writer, + aliasOpt("rm"), displayerType(&displayers.PartnerInterconnectAttachment{})) + cmdPartnerIADelete.Example = `The following example deletes a Partner Interconnect Attachment with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + + `: doctl network --type "partner" interconnect-attachment delete f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + + cmdPartnerIAUpdate := CmdBuilder(cmd, RunPartnerInterconnectAttachmentUpdate, "update ", + "Update a Partner Interconnect Attachment's name and configuration", `Use this command to update the name and and configuration of a Partner Interconnect Attachment`, Writer, aliasOpt("u")) + AddStringFlag(cmdPartnerIAUpdate, doctl.ArgPartnerInterconnectAttachmentName, "", "", + "The Partner Interconnect Attachment's name", requiredOpt()) + AddStringFlag(cmdPartnerIAUpdate, doctl.ArgPartnerInterconnectAttachmentVPCIDs, "", "", + "The Partner Interconnect Attachment's vpc ids", requiredOpt()) + cmdPartnerIAUpdate.Example = `The following example updates the name of a Partner Interconnect Attachment with the ID ` + + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + ` to ` + "`" + `new-name` + "`" + + `: doctl network --type "partner" interconnect-attachment update f81d4fae-7dec-11d0-a765-00a0c91e6bf6 --name new-name` + return cmd } @@ -121,3 +140,127 @@ func RunPartnerInterconnectAttachmentList(c *CmdConfig) error { item := &displayers.PartnerInterconnectAttachment{PartnerInterconnectAttachments: list} return c.Display(item) } + +// RunPartnerInterconnectAttachmentUpdate updates an existing Partner Interconnect Attachment with new configuration. +func RunPartnerInterconnectAttachmentUpdate(c *CmdConfig) error { + if err := ensurePartnerAttachmentType(c); err != nil { + return err + } + + err := ensureOneArg(c) + if err != nil { + return err + } + peeringID := c.Args[0] + + r := new(godo.PartnerInterconnectAttachmentUpdateRequest) + name, err := c.Doit.GetString(c.NS, doctl.ArgPartnerInterconnectAttachmentName) + if err != nil { + return err + } + r.Name = name + + vpcIDs, err := c.Doit.GetString(c.NS, doctl.ArgPartnerInterconnectAttachmentVPCIDs) + if err != nil { + return err + } + r.VPCIDs = strings.Split(vpcIDs, ",") + + interconnectAttachment, err := c.VPCs().UpdatePartnerInterconnectAttachment(peeringID, r) + if err != nil { + return err + } + + item := &displayers.PartnerInterconnectAttachment{ + PartnerInterconnectAttachments: do.PartnerInterconnectAttachments{*interconnectAttachment}, + } + return c.Display(item) +} + +// RunPartnerInterconnectAttachmentDelete deletes an existing Partner Interconnect Attachment by its identifier. +func RunPartnerInterconnectAttachmentDelete(c *CmdConfig) error { + + if err := ensurePartnerAttachmentType(c); err != nil { + return err + } + + err := ensureOneArg(c) + if err != nil { + return err + } + iaID := c.Args[0] + + force, err := c.Doit.GetBool(c.NS, doctl.ArgForce) + if err != nil { + return err + } + + if force || AskForConfirmDelete("Partner Interconnect Attachment", 1) == nil { + + vpcs := c.VPCs() + err := vpcs.DeletePartnerInterconnectAttachment(iaID) + if err != nil { + return err + } + + wait, err := c.Doit.GetBool(c.NS, doctl.ArgCommandWait) + if err != nil { + return err + } + + if wait { + notice("Partner Interconnect Attachment is in progress, waiting for Partner Interconnect Attachment to be deleted") + + err := waitForPIA(vpcs, iaID, "DELETED", true) + if err != nil { + return fmt.Errorf("Partner Interconnect Attachment couldn't be deleted : %v", err) + } + notice("Partner Interconnect Attachment is successfully deleted") + } else { + notice("Partner Interconnect Attachment deletion request accepted") + } + + } else { + return fmt.Errorf("operation aborted") + } + + return nil +} + +func waitForPIA(vpcService do.VPCsService, iaID string, wantStatus string, terminateOnNotFound bool) error { + const maxAttempts = 360 + const errStatus = "ERROR" + attempts := 0 + printNewLineSet := false + + for i := 0; i < maxAttempts; i++ { + if attempts != 0 { + fmt.Fprint(os.Stderr, ".") + if !printNewLineSet { + printNewLineSet = true + defer fmt.Fprintln(os.Stderr) + } + } + + interconnectAttachment, err := vpcService.GetPartnerInterconnectAttachment(iaID) + if err != nil { + if terminateOnNotFound && strings.Contains(err.Error(), "not found") { + return nil + } + return err + } + + if interconnectAttachment.State == errStatus { + return fmt.Errorf("Partner Interconnect Attachment (%s) entered status `%s`", iaID, errStatus) + } + + if interconnectAttachment.State == wantStatus { + return nil + } + + attempts++ + time.Sleep(5 * time.Second) + } + + return fmt.Errorf("timeout waiting for Partner Interconnect Attachment (%s) to become %s", iaID, wantStatus) +} diff --git a/commands/partner_interconnect_attachments_test.go b/commands/partner_interconnect_attachments_test.go index 1d133a00b..63afb93b5 100644 --- a/commands/partner_interconnect_attachments_test.go +++ b/commands/partner_interconnect_attachments_test.go @@ -1,6 +1,7 @@ package commands import ( + "strings" "testing" "github.com/digitalocean/godo" @@ -26,7 +27,7 @@ var ( func TestInterconnectAttachmentsCommand(t *testing.T) { cmd := PartnerInterconnectAttachments() assert.NotNil(t, cmd) - assertCommandNames(t, cmd, "get", "list") + assertCommandNames(t, cmd, "get", "list", "delete", "update") } func TestInterconnectAttachmentsGet(t *testing.T) { @@ -59,3 +60,46 @@ func TestInterconnectAttachmentsList(t *testing.T) { assert.NoError(t, err) }) } + +func TestInterconnectAttachmentsDelete(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + config.Doit.Set("network", doctl.ArgInterconnectAttachmentType, "partner") + + iaID := "e819b321-a9a1-4078-b437-8e6b8bf13530" + tm.vpcs.EXPECT().DeletePartnerInterconnectAttachment(iaID).Return(nil) + + config.Args = append(config.Args, iaID) + config.Doit.Set(config.NS, doctl.ArgForce, true) + + err := RunPartnerInterconnectAttachmentDelete(config) + assert.NoError(t, err) + }) +} + +func TestInterconnectAttachmentsUpdate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + config.Doit.Set("network", doctl.ArgInterconnectAttachmentType, "partner") + + iaID := "ia-uuid1" + iaName := "ia-name" + vpcIDs := "f81d4fae-7dec-11d0-a765-00a0c91e6bf6,3f900b61-30d7-40d8-9711-8c5d6264b268" + r := godo.PartnerInterconnectAttachmentUpdateRequest{Name: iaName, VPCIDs: strings.Split(vpcIDs, ",")} + tm.vpcs.EXPECT().UpdatePartnerInterconnectAttachment(iaID, &r).Return(&testIA, nil) + + config.Args = append(config.Args, iaID) + config.Doit.Set(config.NS, doctl.ArgPartnerInterconnectAttachmentName, iaName) + config.Doit.Set(config.NS, doctl.ArgPartnerInterconnectAttachmentVPCIDs, vpcIDs) + + err := RunPartnerInterconnectAttachmentUpdate(config) + assert.NoError(t, err) + }) +} + +func TestInterconnectAttachmentsUpdateNoID(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + config.Doit.Set("network", doctl.ArgInterconnectAttachmentType, "partner") + + err := RunPartnerInterconnectAttachmentUpdate(config) + assert.Error(t, err) + }) +} diff --git a/do/mocks/VPCsService.go b/do/mocks/VPCsService.go index b99484b4a..ef028f422 100644 --- a/do/mocks/VPCsService.go +++ b/do/mocks/VPCsService.go @@ -85,6 +85,20 @@ func (mr *MockVPCsServiceMockRecorder) Delete(vpcUUID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockVPCsService)(nil).Delete), vpcUUID) } +// DeletePartnerInterconnectAttachment mocks base method. +func (m *MockVPCsService) DeletePartnerInterconnectAttachment(iaID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePartnerInterconnectAttachment", iaID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePartnerInterconnectAttachment indicates an expected call of DeletePartnerInterconnectAttachment. +func (mr *MockVPCsServiceMockRecorder) DeletePartnerInterconnectAttachment(iaID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePartnerInterconnectAttachment", reflect.TypeOf((*MockVPCsService)(nil).DeletePartnerInterconnectAttachment), iaID) +} + // DeleteVPCPeering mocks base method. func (m *MockVPCsService) DeleteVPCPeering(peeringID string) error { m.ctrl.T.Helper() @@ -239,6 +253,21 @@ func (mr *MockVPCsServiceMockRecorder) Update(vpcUUID, vpcr any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockVPCsService)(nil).Update), vpcUUID, vpcr) } +// UpdatePartnerInterconnectAttachment mocks base method. +func (m *MockVPCsService) UpdatePartnerInterconnectAttachment(iaID string, req *godo.PartnerInterconnectAttachmentUpdateRequest) (*do.PartnerInterconnectAttachment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePartnerInterconnectAttachment", iaID, req) + ret0, _ := ret[0].(*do.PartnerInterconnectAttachment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdatePartnerInterconnectAttachment indicates an expected call of UpdatePartnerInterconnectAttachment. +func (mr *MockVPCsServiceMockRecorder) UpdatePartnerInterconnectAttachment(iaID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePartnerInterconnectAttachment", reflect.TypeOf((*MockVPCsService)(nil).UpdatePartnerInterconnectAttachment), iaID, req) +} + // UpdateVPCPeering mocks base method. func (m *MockVPCsService) UpdateVPCPeering(peeringID string, req *godo.VPCPeeringUpdateRequest) (*do.VPCPeering, error) { m.ctrl.T.Helper() diff --git a/do/vpcs.go b/do/vpcs.go index a483a2424..eb4689aa1 100644 --- a/do/vpcs.go +++ b/do/vpcs.go @@ -59,6 +59,8 @@ type VPCsService interface { ListVPCPeeringsByVPCID(vpcID string) (VPCPeerings, error) GetPartnerInterconnectAttachment(iaID string) (*PartnerInterconnectAttachment, error) ListPartnerInterconnectAttachments() (PartnerInterconnectAttachments, error) + DeletePartnerInterconnectAttachment(iaID string) error + UpdatePartnerInterconnectAttachment(iaID string, req *godo.PartnerInterconnectAttachmentUpdateRequest) (*PartnerInterconnectAttachment, error) } var _ VPCsService = &vpcsService{} @@ -268,3 +270,17 @@ func (v *vpcsService) ListPartnerInterconnectAttachments() (PartnerInterconnectA return list, nil } + +func (v *vpcsService) DeletePartnerInterconnectAttachment(iaID string) error { + _, err := v.client.PartnerInterconnectAttachments.Delete(context.TODO(), iaID) + return err +} + +func (v *vpcsService) UpdatePartnerInterconnectAttachment(iaID string, req *godo.PartnerInterconnectAttachmentUpdateRequest) (*PartnerInterconnectAttachment, error) { + partnerIA, _, err := v.client.PartnerInterconnectAttachments.Update(context.TODO(), iaID, req) + if err != nil { + return nil, err + } + + return &PartnerInterconnectAttachment{PartnerInterconnectAttachment: partnerIA}, nil +}