Repository: digitalocean/doctl
Pull request #1658 (2025-02-17 10:23:09)

[VPC-3918] List PIA remote routes
Testing:
```
/doctl --context s2 network --type "partner" interconnect-attachment list-routes bb6da12f-eddc-4250-b40b-f9bef1cebcd1

```

<img width="1651" alt="image" src="https://github.com/user-attachments/assets/f81ab760-8822-46e9-94d9-4d7791d6d392" />



Patch:
diff --git a/commands/displayers/partner_interconnect_attachment.go b/commands/displayers/partner_interconnect_attachment.go
index 30178277..ecdec432 100644
--- a/commands/displayers/partner_interconnect_attachment.go
+++ b/commands/displayers/partner_interconnect_attachment.go
@@ -74,3 +74,41 @@ func (v *PartnerInterconnectAttachment) KV() []map[string]any {
 
 	return out
 }
+
+type PartnerInterconnectAttachmentRoute struct {
+	PartnerInterconnectAttachmentRoutes do.PartnerInterconnectAttachmentRoutes
+}
+
+var _ Displayable = &PartnerInterconnectAttachmentRoute{}
+
+func (v *PartnerInterconnectAttachmentRoute) JSON(out io.Writer) error {
+	return writeJSON(v.PartnerInterconnectAttachmentRoutes, out)
+}
+
+func (v *PartnerInterconnectAttachmentRoute) Cols() []string {
+	return []string{
+		"ID",
+		"Cidr",
+	}
+}
+
+func (v *PartnerInterconnectAttachmentRoute) ColMap() map[string]string {
+	return map[string]string{
+		"ID":   "ID",
+		"Cidr": "Cidr",
+	}
+}
+
+func (v *PartnerInterconnectAttachmentRoute) KV() []map[string]any {
+	out := make([]map[string]any, 0, len(v.PartnerInterconnectAttachmentRoutes))
+
+	for _, ia := range v.PartnerInterconnectAttachmentRoutes {
+		o := map[string]any{
+			"ID":   ia.ID,
+			"Cidr": ia.Cidr,
+		}
+		out = append(out, o)
+	}
+
+	return out
+}
diff --git a/commands/partner_interconnect_attachment.go b/commands/partner_interconnect_attachment.go
index 3ed0771d..ac88f48e 100644
--- a/commands/partner_interconnect_attachment.go
+++ b/commands/partner_interconnect_attachment.go
@@ -121,6 +121,16 @@ With the Partner Interconnect Attachments commands, you can get, list, create, u
 		`: doctl network --type "partner" interconnect-attachment update f81d4fae-7dec-11d0-a765-00a0c91e6bf6 --name "new-name" --
 vpc-ids "270a76ed-1bb7-4c5d-a6a5-e863de086940"`
 
+	interconnectAttachmentRouteDetails := `
+- The Partner Interconnect Attachment ID
+- The Partner Interconnect Attachment Cidr`
+
+	cmdPartnerIARouteList := CmdBuilder(cmd, RunPartnerInterconnectAttachmentRouteList, "list-routes", "List Network Interconnect Attachment Routes", "Retrieves a list of the Network Interconnect Attachment Routes on your account, including the following information for each:"+interconnectAttachmentRouteDetails, Writer,
+		aliasOpt("ls-routes"), displayerType(&displayers.PartnerInterconnectAttachment{}))
+	AddStringFlag(cmdPartnerIARouteList, doctl.ArgInterconnectAttachmentType, "", "partner", "Specify interconnect attachment type (e.g., partner)")
+	cmdPartnerIARouteList.Example = `The following example lists the Network Interconnect Attachments on your account :` +
+		` doctl network --type "partner" interconnect-attachment list-routes --format ID,Cidr `
+
 	return cmd
 }
 
@@ -260,7 +270,7 @@ func RunPartnerInterconnectAttachmentUpdate(c *CmdConfig) error {
 	if err != nil {
 		return err
 	}
-	peeringID := c.Args[0]
+	iaID := c.Args[0]
 
 	r := new(godo.PartnerInterconnectAttachmentUpdateRequest)
 	name, err := c.Doit.GetString(c.NS, doctl.ArgPartnerInterconnectAttachmentName)
@@ -275,7 +285,7 @@ func RunPartnerInterconnectAttachmentUpdate(c *CmdConfig) error {
 	}
 	r.VPCIDs = strings.Split(vpcIDs, ",")
 
-	interconnectAttachment, err := c.PartnerInterconnectAttachments().UpdatePartnerInterconnectAttachment(peeringID, r)
+	interconnectAttachment, err := c.PartnerInterconnectAttachments().UpdatePartnerInterconnectAttachment(iaID, r)
 	if err != nil {
 		return err
 	}
@@ -336,6 +346,28 @@ func RunPartnerInterconnectAttachmentDelete(c *CmdConfig) error {
 	return nil
 }
 
+// RunPartnerInterconnectAttachmentRouteList lists Partner Interconnect Attachment routes
+func RunPartnerInterconnectAttachmentRouteList(c *CmdConfig) error {
+	if err := ensurePartnerAttachmentType(c); err != nil {
+		return err
+	}
+
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	iaID := c.Args[0]
+
+	pias := c.PartnerInterconnectAttachments()
+	routeList, err := pias.ListPartnerInterconnectAttachmentRoutes(iaID)
+	if err != nil {
+		return err
+	}
+
+	item := &displayers.PartnerInterconnectAttachmentRoute{PartnerInterconnectAttachmentRoutes: routeList}
+	return c.Display(item)
+}
+
 func waitForPIA(pias do.PartnerInterconnectAttachmentsService, iaID string, wantStatus string, terminateOnNotFound bool) error {
 	const maxAttempts = 360
 	const errStatus = "ERROR"
diff --git a/do/mocks/PartnerInterconnectAttachmentsService.go b/do/mocks/PartnerInterconnectAttachmentsService.go
index 3174ca74..404c5d33 100644
--- a/do/mocks/PartnerInterconnectAttachmentsService.go
+++ b/do/mocks/PartnerInterconnectAttachmentsService.go
@@ -85,6 +85,21 @@ func (mr *MockPartnerInterconnectAttachmentsServiceMockRecorder) GetPartnerInter
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPartnerInterconnectAttachment", reflect.TypeOf((*MockPartnerInterconnectAttachmentsService)(nil).GetPartnerInterconnectAttachment), iaID)
 }
 
+// ListPartnerInterconnectAttachmentRoutes mocks base method.
+func (m *MockPartnerInterconnectAttachmentsService) ListPartnerInterconnectAttachmentRoutes(iaID string) (do.PartnerInterconnectAttachmentRoutes, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListPartnerInterconnectAttachmentRoutes", iaID)
+	ret0, _ := ret[0].(do.PartnerInterconnectAttachmentRoutes)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListPartnerInterconnectAttachmentRoutes indicates an expected call of ListPartnerInterconnectAttachmentRoutes.
+func (mr *MockPartnerInterconnectAttachmentsServiceMockRecorder) ListPartnerInterconnectAttachmentRoutes(iaID any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPartnerInterconnectAttachmentRoutes", reflect.TypeOf((*MockPartnerInterconnectAttachmentsService)(nil).ListPartnerInterconnectAttachmentRoutes), iaID)
+}
+
 // ListPartnerInterconnectAttachments mocks base method.
 func (m *MockPartnerInterconnectAttachmentsService) ListPartnerInterconnectAttachments() (do.PartnerInterconnectAttachments, error) {
 	m.ctrl.T.Helper()
diff --git a/do/partner_interconnect_attachments.go b/do/partner_interconnect_attachments.go
index f7ea118f..2972205a 100644
--- a/do/partner_interconnect_attachments.go
+++ b/do/partner_interconnect_attachments.go
@@ -27,6 +27,14 @@ type PartnerInterconnectAttachment struct {
 // PartnerInterconnectAttachments is a slice of PartnerInterconnectAttachment.
 type PartnerInterconnectAttachments []PartnerInterconnectAttachment
 
+// PartnerInterconnectAttachmentRoute wraps a godo RemoteRoute.
+type PartnerInterconnectAttachmentRoute struct {
+	*godo.RemoteRoute
+}
+
+// PartnerInterconnectAttachmentRoutes is a slice of PartnerInterconnectAttachmentRoute.
+type PartnerInterconnectAttachmentRoutes []PartnerInterconnectAttachmentRoute
+
 // PartnerInterconnectAttachmentsService is an interface for interacting with
 // DigitalOcean's partner interconnect attachments api.
 type PartnerInterconnectAttachmentsService interface {
@@ -35,6 +43,7 @@ type PartnerInterconnectAttachmentsService interface {
 	ListPartnerInterconnectAttachments() (PartnerInterconnectAttachments, error)
 	DeletePartnerInterconnectAttachment(iaID string) error
 	UpdatePartnerInterconnectAttachment(iaID string, req *godo.PartnerInterconnectAttachmentUpdateRequest) (*PartnerInterconnectAttachment, error)
+	ListPartnerInterconnectAttachmentRoutes(iaID string) (PartnerInterconnectAttachmentRoutes, error)
 }
 
 var _ PartnerInterconnectAttachmentsService = &partnerInterconnectAttachmentsService{}
@@ -111,3 +120,33 @@ func (p *partnerInterconnectAttachmentsService) UpdatePartnerInterconnectAttachm
 
 	return &PartnerInterconnectAttachment{PartnerInterconnectAttachment: partnerIA}, nil
 }
+
+// ListPartnerInterconnectAttachmentRoutes lists all partner interconnect attachment routes.
+func (p *partnerInterconnectAttachmentsService) ListPartnerInterconnectAttachmentRoutes(iaID string) (PartnerInterconnectAttachmentRoutes, error) {
+	f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) {
+		list, resp, err := p.client.PartnerInterconnectAttachments.ListRoutes(context.TODO(), iaID, opt)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		si := make([]any, len(list))
+		for i := range list {
+			si[i] = list[i]
+		}
+
+		return si, resp, err
+	}
+
+	si, err := PaginateResp(f)
+	if err != nil {
+		return nil, err
+	}
+
+	list := make([]PartnerInterconnectAttachmentRoute, len(si))
+	for i := range si {
+		a := si[i].(*godo.RemoteRoute)
+		list[i] = PartnerInterconnectAttachmentRoute{RemoteRoute: a}
+	}
+
+	return list, nil
+}


Test patch:
diff --git a/commands/partner_interconnect_attachment_test.go b/commands/partner_interconnect_attachment_test.go
index 2f9eda14..97de2c9a 100644
--- a/commands/partner_interconnect_attachment_test.go
+++ b/commands/partner_interconnect_attachment_test.go
@@ -28,13 +28,24 @@ var (
 	testPartnerIAList = do.PartnerInterconnectAttachments{
 		testPartnerAttachment,
 	}
+
+	testPartnerAttachmentRoute = do.PartnerInterconnectAttachmentRoute{
+		RemoteRoute: &godo.RemoteRoute{
+			ID:   "test-route-id",
+			Cidr: "10.10.0.0/24",
+		},
+	}
+
+	testPartnerIARouteList = do.PartnerInterconnectAttachmentRoutes{
+		testPartnerAttachmentRoute,
+	}
 )
 
 func TestPartnerInterconnectAttachmentsCommand(t *testing.T) {
 	cmd := PartnerInterconnectAttachments()
 	assert.NotNil(t, cmd)
 
-	assertCommandNames(t, cmd, "create", "get", "list", "delete", "update")
+	assertCommandNames(t, cmd, "create", "get", "list", "delete", "update", "list-routes")
 }
 
 func TestPartnerInterconnectAttachmentCreate(t *testing.T) {
@@ -150,3 +161,16 @@ func TestInterconnectAttachmentsUpdateNoID(t *testing.T) {
 		assert.Error(t, err)
 	})
 }
+
+func TestInterconnectAttachmentRoutesList(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		config.Doit.Set(config.NS, doctl.ArgInterconnectAttachmentType, "partner")
+
+		iaID := "ia-uuid1"
+		config.Args = append(config.Args, iaID)
+		tm.partnerInterconnectAttachment.EXPECT().ListPartnerInterconnectAttachmentRoutes(iaID).Return(testPartnerIARouteList, nil)
+
+		err := RunPartnerInterconnectAttachmentRouteList(config)
+		assert.NoError(t, err)
+	})
+}
diff --git a/integration/partner_interconnect_attachment_test.go b/integration/partner_interconnect_attachment_test.go
index cf6267d8..ba63e8bd 100644
--- a/integration/partner_interconnect_attachment_test.go
+++ b/integration/partner_interconnect_attachment_test.go
@@ -17,7 +17,8 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-var partnerAttachmentCreateResponse = `
+var (
+	partnerAttachmentCreateResponse = `
 {
   "partner_interconnect_attachment": {
     "id": "12345",
@@ -38,11 +39,37 @@ var partnerAttachmentCreateResponse = `
 }
 `
 
-var partnerAttachmentCreateOutput = `
+	partnerAttachmentCreateOutput = `
 ID       Name         State     Connection Bandwidth (MBPS)    Region    NaaS Provider    VPC IDs                                 Created At                       BGP Local ASN    BGP Local Router IP    BGP Peer ASN    BGP Peer Router IP
 12345    doctl-pia    active    50                             stage2    MEGAPORT         d35e5cb7-7957-4643-8e3a-1ab4eb3a494c    2025-01-30 12:00:00 +0000 UTC    0                                       0
 `
 
+	interconnectListRoutesOutput = `
+ID                                      Cidr
+a0eb6eb0-fa38-41a8-a5de-1a75524667fe    169.250.0.0/29
+`
+
+	interconnectListRoutesResponse = `
+{
+  "remote_routes": [
+	{"id": "a0eb6eb0-fa38-41a8-a5de-1a75524667fe", "cidr": "169.250.0.0/29"}
+  ],
+  "links": {
+    "pages": {
+      "last": "http://localhost/v2/partner_interconnect/attachments?page=1&per_page=1",
+      "next": "http://localhost/v2/partner_interconnect/attachments?page=2&per_page=1"
+    }
+  },
+  "links": {
+    "pages": {}
+  },
+  "meta": {
+    "total": 1
+  }
+}
+`
+)
+
 var _ = suite("partner_interconnect_attachments/create", func(t *testing.T, when spec.G, it spec.S) {
 	var (
 		expect *require.Assertions
@@ -117,3 +144,75 @@ var _ = suite("partner_interconnect_attachments/create", func(t *testing.T, when
 		})
 	})
 })
+
+var _ = suite("partner_interconnect_attachments/list-routes", func(t *testing.T, when spec.G, it spec.S) {
+	var (
+		expect *require.Assertions
+		server *httptest.Server
+	)
+
+	it.Before(func() {
+		expect = require.New(t)
+
+		server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			switch req.URL.Path {
+			case "/v2/partner_interconnect/attachments/c5537207-ebf0-47cb-bc10-6fac717cd672/remote_routes":
+				auth := req.Header.Get("Authorization")
+				if auth != "Bearer some-magic-token" {
+					w.WriteHeader(http.StatusUnauthorized)
+					return
+				}
+
+				if req.Method != http.MethodGet {
+					w.WriteHeader(http.StatusMethodNotAllowed)
+					return
+				}
+
+				w.Write([]byte(interconnectListRoutesResponse))
+			default:
+				dump, err := httputil.DumpRequest(req, true)
+				if err != nil {
+					t.Fatal("failed to dump request")
+				}
+
+				t.Fatalf("received unknown request: %s", dump)
+			}
+		}))
+	})
+
+	when("no flags are passed", func() {
+		it("gets the specified VPC", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"network",
+				"interconnect-attachment",
+				"list-routes",
+				"c5537207-ebf0-47cb-bc10-6fac717cd672",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(interconnectListRoutesOutput), strings.TrimSpace(string(output)))
+		})
+	})
+
+	when("format and no-header flags are passed", func() {
+		it("gets the specified VPC", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"network",
+				"interconnect-attachment",
+				"list-routes",
+				"--format", "Cidr",
+				"--no-header",
+				"c5537207-ebf0-47cb-bc10-6fac717cd672",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal("169.250.0.0/29", strings.TrimSpace(string(output)))
+		})
+	})
+})