Repository: digitalocean/doctl
Pull request #1532 (2024-05-16 14:39:25)

[VPC-3359] Implemented vpc peering methods
VPC Peering is a closed beta feature and not available for public use.

[Testing doc ](https://docs.google.com/document/d/1kE451w61E_zhOZ04bTTkvEBJiLSQZWlHVtupRs8oalg/edit)



[VPC-3359]: https://do-internal.atlassian.net/browse/VPC-3359?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Patch:
diff --git a/args.go b/args.go
index d6d4ab43..9b2b381a 100644
--- a/args.go
+++ b/args.go
@@ -468,6 +468,13 @@ const (
 	// ArgVPCIPRange is a VPC range of IP addresses in CIDR notation.
 	ArgVPCIPRange = "ip-range"
 
+	// ArgVPCPeeringName is a name of the VPC Peering.
+	ArgVPCPeeringName = "name"
+	// ArgVPCPeeringVPCIDs is the vpc ids of the peering
+	ArgVPCPeeringVPCIDs = "vpc-ids"
+	// ArgVPCPeeringVPCID is id of the VPC.
+	ArgVPCPeeringVPCID = "vpc-id"
+
 	// ArgReadWrite indicates a generated token should be read/write.
 	ArgReadWrite = "read-write"
 	// ArgRegistry indicates the name of the registry.
diff --git a/commands/displayers/vpc_peering.go b/commands/displayers/vpc_peering.go
new file mode 100644
index 00000000..26ee38f7
--- /dev/null
+++ b/commands/displayers/vpc_peering.go
@@ -0,0 +1,54 @@
+package displayers
+
+import (
+	"github.com/digitalocean/doctl/do"
+	"io"
+	"strings"
+)
+
+type VPCPeering struct {
+	VPCPeerings do.VPCPeerings
+}
+
+var _ Displayable = &VPCPeering{}
+
+func (v *VPCPeering) JSON(out io.Writer) error {
+	return writeJSON(v.VPCPeerings, out)
+}
+
+func (v *VPCPeering) Cols() []string {
+	return []string{
+		"ID",
+		"Name",
+		"VPCIDs",
+		"Status",
+		"Created",
+	}
+}
+
+func (v *VPCPeering) ColMap() map[string]string {
+	return map[string]string{
+		"ID":      "ID",
+		"Name":    "Name",
+		"VPCIDs":  "VPCIDs",
+		"Status":  "Status",
+		"Created": "Created At",
+	}
+}
+
+func (v *VPCPeering) KV() []map[string]any {
+	out := make([]map[string]any, 0, len(v.VPCPeerings))
+
+	for _, v := range v.VPCPeerings {
+		o := map[string]any{
+			"ID":      v.ID,
+			"Name":    v.Name,
+			"VPCIDs":  strings.Join(v.VPCIDs, ","),
+			"Status":  v.Status,
+			"Created": v.CreatedAt,
+		}
+		out = append(out, o)
+	}
+
+	return out
+}
diff --git a/commands/vpc_peerings.go b/commands/vpc_peerings.go
new file mode 100644
index 00000000..f1e0f11c
--- /dev/null
+++ b/commands/vpc_peerings.go
@@ -0,0 +1,270 @@
+package commands
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/digitalocean/doctl"
+	"github.com/digitalocean/doctl/commands/displayers"
+	"github.com/digitalocean/doctl/do"
+	"github.com/digitalocean/godo"
+	"github.com/spf13/cobra"
+)
+
+// VPCPeerings creates the vpc peerings command.
+func VPCPeerings() *Command {
+	cmd := &Command{
+		Command: &cobra.Command{
+			Use:   "peerings",
+			Short: "Display commands that manage VPC Peerings",
+			Long: `The commands under ` + "`" + `doctl vpcs peerings` + "`" + ` are for managing your VPC Peerings.
+With the VPC Peerings commands, you can get, list, create, update, or delete VPC Peerings, and manage their configuration details.`,
+			Hidden: true,
+		},
+	}
+
+	peeringDetails := `
+- The VPC Peering ID
+- The VPC Peering Name
+- The Peered VPC network IDs
+- The VPC Peering Status
+- The VPC Peering creation date, in ISO8601 combined date and time format
+`
+	cmdPeeringGet := CmdBuilder(cmd, RunVPCPeeringGet, "get <id>",
+		"Retrieves a VPC Peering", "Retrieves information about a VPC Peering, including:"+peeringDetails, Writer,
+		aliasOpt("g"), displayerType(&displayers.VPCPeering{}))
+	cmdPeeringGet.Example = `The following example retrieves information about a VPC Peering with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + `: doctl vpcs peerings get f81d4fae-7dec-11d0-a765-00a0c91e6bf6`
+
+	cmdPeeringList := CmdBuilder(cmd, RunVPCPeeringList, "list", "List VPC Peerings", "Retrieves a list of the VPC Peerings on your account, including the following information for each:"+peeringDetails, Writer,
+		aliasOpt("ls"), displayerType(&displayers.VPCPeering{}))
+	AddStringFlag(cmdPeeringList, doctl.ArgVPCPeeringVPCID, "", "",
+		"VPC ID")
+	cmdPeeringList.Example = `The following example lists the VPC Peerings on your account : doctl vpcs peerings list --format Name,VPCIDs`
+
+	cmdPeeringCreate := CmdBuilder(cmd, RunVPCPeeringCreate, "create",
+		"Create a new VPC Peering", "Use this command to create a new VPC Peering on your account.", Writer, aliasOpt("c"))
+	AddStringFlag(cmdPeeringCreate, doctl.ArgVPCPeeringVPCIDs, "", "",
+		"Peering VPC IDs should be comma separated", requiredOpt())
+	AddBoolFlag(cmdPeeringCreate, doctl.ArgCommandWait, "", false, "Boolean that specifies whether to wait for a VPC Peering creation to complete before returning control to the terminal")
+	cmdPeeringCreate.Example = `The following example creates a VPC Peering named ` +
+		"`" + `example-peering-name` + "`" +
+		` : doctl vpcs peerings create example-peering-name --vpc-ids f81d4fae-7dec-11d0-a765-00a0c91e6bf6,3f900b61-30d7-40d8-9711-8c5d6264b268`
+
+	cmdPeeringUpdate := CmdBuilder(cmd, RunVPCPeeringUpdate, "update <id>",
+		"Update a VPC Peering's name", `Use this command to update the name of a VPC Peering`, Writer, aliasOpt("u"))
+	AddStringFlag(cmdPeeringUpdate, doctl.ArgVPCPeeringName, "", "",
+		"The VPC Peering's name", requiredOpt())
+	cmdPeeringUpdate.Example = `The following example updates the name of a VPC Peering with the ID ` +
+		"`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" + ` to ` + "`" + `new-name` + "`" +
+		`: doctl vpcs peerings update f81d4fae-7dec-11d0-a765-00a0c91e6bf6 --name new-name`
+
+	cmdPeeringDelete := CmdBuilder(cmd, RunVPCPeeringDelete, "delete <id>",
+		"Permanently delete a VPC Peering", `Permanently deletes the specified VPC Peering. This is irreversible.`, Writer, aliasOpt("d", "rm"))
+	AddBoolFlag(cmdPeeringDelete, doctl.ArgForce, doctl.ArgShortForce, false,
+		"Delete the VPC Peering without any confirmation prompt")
+	AddBoolFlag(cmdPeeringDelete, doctl.ArgCommandWait, "", false,
+		"Boolean that specifies whether to wait for a VPC Peering deletion to complete before returning control to the terminal")
+	cmdPeeringDelete.Example = `The following example deletes the VPC Peering with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" +
+		`: doctl vpcs peerings delete f81d4fae-7dec-11d0-a765-00a0c91e6bf6`
+
+	return cmd
+}
+
+// RunVPCPeeringGet retrieves an existing VPC Peering by its identifier.
+func RunVPCPeeringGet(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	peeringID := c.Args[0]
+
+	peering, err := c.VPCs().GetPeering(peeringID)
+	if err != nil {
+		return err
+	}
+
+	item := &displayers.VPCPeering{VPCPeerings: do.VPCPeerings{*peering}}
+	return c.Display(item)
+}
+
+// RunVPCPeeringCreate creates a new VPC Peering with a given configuration.
+func RunVPCPeeringCreate(c *CmdConfig) error {
+	if len(c.Args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+	peeringName := c.Args[0]
+
+	r := new(godo.VPCPeeringCreateRequest)
+	r.Name = peeringName
+
+	vpcIDs, err := c.Doit.GetString(c.NS, doctl.ArgVPCPeeringVPCIDs)
+	if err != nil {
+		return err
+	}
+
+	for _, v := range strings.Split(vpcIDs, ",") {
+		if v == "" {
+			return errors.New("VPC ID is empty")
+		}
+
+		r.VPCIDs = append(r.VPCIDs, strings.TrimSpace(v))
+	}
+
+	if len(r.VPCIDs) != 2 {
+		return errors.New("VPC IDs length should be 2")
+	}
+
+	vpcService := c.VPCs()
+	peering, err := vpcService.CreateVPCPeering(r)
+	if err != nil {
+		return err
+	}
+
+	wait, err := c.Doit.GetBool(c.NS, doctl.ArgCommandWait)
+	if err != nil {
+		return err
+	}
+
+	if wait {
+		notice("VPC Peering creation is in progress, waiting for VPC Peering to become active")
+
+		err := waitForVPCPeering(vpcService, peering.ID, "ACTIVE", false)
+		if err != nil {
+			return fmt.Errorf("VPC Peering couldn't enter `active` state: %v", err)
+		}
+
+		peering, _ = vpcService.GetPeering(peering.ID)
+	}
+
+	item := &displayers.VPCPeering{VPCPeerings: do.VPCPeerings{*peering}}
+	return c.Display(item)
+}
+
+// RunVPCPeeringList lists VPC Peerings
+func RunVPCPeeringList(c *CmdConfig) error {
+	vpcID, err := c.Doit.GetString(c.NS, doctl.ArgVPCPeeringVPCID)
+	if err != nil {
+		return err
+	}
+
+	var list do.VPCPeerings
+	if vpcID == "" {
+		list, err = c.VPCs().ListVPCPeerings()
+		if err != nil {
+			return err
+		}
+	} else {
+		list, err = c.VPCs().ListVPCPeeringsByVPCID(vpcID)
+		if err != nil {
+			return err
+		}
+	}
+
+	item := &displayers.VPCPeering{VPCPeerings: list}
+	return c.Display(item)
+}
+
+// RunVPCPeeringUpdate updates an existing VPC  Peering with new configuration.
+func RunVPCPeeringUpdate(c *CmdConfig) error {
+	err := ensureOneArg(c)
+	if err != nil {
+		return err
+	}
+	peeringID := c.Args[0]
+
+	r := new(godo.VPCPeeringUpdateRequest)
+	name, err := c.Doit.GetString(c.NS, doctl.ArgVPCPeeringName)
+	if err != nil {
+		return err
+	}
+	r.Name = name
+
+	peering, err := c.VPCs().UpdateVPCPeering(peeringID, r)
+	if err != nil {
+		return err
+	}
+
+	item := &displayers.VPCPeering{VPCPeerings: do.VPCPeerings{*peering}}
+	return c.Display(item)
+}
+
+// RunVPCPeeringDelete deletes a VPC Peering by its identifier.
+func RunVPCPeeringDelete(c *CmdConfig) error {
+	if len(c.Args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	peeringID := c.Args[0]
+
+	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
+	if err != nil {
+		return err
+	}
+
+	if force || AskForConfirmDelete("VPC Peering", 1) == nil {
+		vpcs := c.VPCs()
+		if err := vpcs.DeleteVPCPeering(peeringID); err != nil {
+			return err
+		}
+
+		wait, err := c.Doit.GetBool(c.NS, doctl.ArgCommandWait)
+		if err != nil {
+			return err
+		}
+
+		if wait {
+			notice("VPC Peering deletion is in progress, waiting for VPC Peering to be deleted")
+
+			err := waitForVPCPeering(vpcs, peeringID, "DELETED", true)
+			if err != nil {
+				return fmt.Errorf("VPC Peering couldn't be deleted : %v", err)
+			}
+		}
+
+	} else {
+		return fmt.Errorf("operation aborted")
+	}
+	notice("VPC Peering is successfully deleted")
+	return nil
+}
+
+func waitForVPCPeering(vpcService do.VPCsService, peeringID 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)
+			}
+		}
+
+		peering, err := vpcService.GetPeering(peeringID)
+		if err != nil {
+			if terminateOnNotFound && strings.Contains(err.Error(), "not found") {
+				return nil
+			}
+			return err
+		}
+
+		if peering.Status == errStatus {
+			return fmt.Errorf("VPC Peering (%s) entered status `%s`", peeringID, errStatus)
+		}
+
+		if peering.Status == wantStatus {
+			return nil
+		}
+
+		attempts++
+		time.Sleep(5 * time.Second)
+	}
+
+	return fmt.Errorf("timeout waiting for VPC Peering (%s) to become %s", peeringID, wantStatus)
+}
diff --git a/commands/vpcs.go b/commands/vpcs.go
index 64e8f787..a3895bed 100644
--- a/commands/vpcs.go
+++ b/commands/vpcs.go
@@ -37,6 +37,8 @@ With the VPC commands, you can list, create, or delete VPCs, and manage their co
 		},
 	}
 
+	cmd.AddCommand(VPCPeerings())
+
 	vpcDetail := `
 
 - The VPC network's ID
diff --git a/do/mocks/VPCsService.go b/do/mocks/VPCsService.go
index 6074c3db..33d86691 100644
--- a/do/mocks/VPCsService.go
+++ b/do/mocks/VPCsService.go
@@ -55,6 +55,21 @@ func (mr *MockVPCsServiceMockRecorder) Create(vpcr any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockVPCsService)(nil).Create), vpcr)
 }
 
+// CreateVPCPeering mocks base method.
+func (m *MockVPCsService) CreateVPCPeering(req *godo.VPCPeeringCreateRequest) (*do.VPCPeering, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateVPCPeering", req)
+	ret0, _ := ret[0].(*do.VPCPeering)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateVPCPeering indicates an expected call of CreateVPCPeering.
+func (mr *MockVPCsServiceMockRecorder) CreateVPCPeering(req any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVPCPeering", reflect.TypeOf((*MockVPCsService)(nil).CreateVPCPeering), req)
+}
+
 // Delete mocks base method.
 func (m *MockVPCsService) Delete(vpcUUID string) error {
 	m.ctrl.T.Helper()
@@ -69,6 +84,20 @@ func (mr *MockVPCsServiceMockRecorder) Delete(vpcUUID any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockVPCsService)(nil).Delete), vpcUUID)
 }
 
+// DeleteVPCPeering mocks base method.
+func (m *MockVPCsService) DeleteVPCPeering(peeringID string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteVPCPeering", peeringID)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteVPCPeering indicates an expected call of DeleteVPCPeering.
+func (mr *MockVPCsServiceMockRecorder) DeleteVPCPeering(peeringID any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVPCPeering", reflect.TypeOf((*MockVPCsService)(nil).DeleteVPCPeering), peeringID)
+}
+
 // Get mocks base method.
 func (m *MockVPCsService) Get(vpcUUID string) (*do.VPC, error) {
 	m.ctrl.T.Helper()
@@ -84,6 +113,21 @@ func (mr *MockVPCsServiceMockRecorder) Get(vpcUUID any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockVPCsService)(nil).Get), vpcUUID)
 }
 
+// GetPeering mocks base method.
+func (m *MockVPCsService) GetPeering(peeringID string) (*do.VPCPeering, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPeering", peeringID)
+	ret0, _ := ret[0].(*do.VPCPeering)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetPeering indicates an expected call of GetPeering.
+func (mr *MockVPCsServiceMockRecorder) GetPeering(peeringID any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeering", reflect.TypeOf((*MockVPCsService)(nil).GetPeering), peeringID)
+}
+
 // List mocks base method.
 func (m *MockVPCsService) List() (do.VPCs, error) {
 	m.ctrl.T.Helper()
@@ -99,6 +143,36 @@ func (mr *MockVPCsServiceMockRecorder) List() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockVPCsService)(nil).List))
 }
 
+// ListVPCPeerings mocks base method.
+func (m *MockVPCsService) ListVPCPeerings() (do.VPCPeerings, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListVPCPeerings")
+	ret0, _ := ret[0].(do.VPCPeerings)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListVPCPeerings indicates an expected call of ListVPCPeerings.
+func (mr *MockVPCsServiceMockRecorder) ListVPCPeerings() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVPCPeerings", reflect.TypeOf((*MockVPCsService)(nil).ListVPCPeerings))
+}
+
+// ListVPCPeeringsByVPCID mocks base method.
+func (m *MockVPCsService) ListVPCPeeringsByVPCID(vpcID string) (do.VPCPeerings, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListVPCPeeringsByVPCID", vpcID)
+	ret0, _ := ret[0].(do.VPCPeerings)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListVPCPeeringsByVPCID indicates an expected call of ListVPCPeeringsByVPCID.
+func (mr *MockVPCsServiceMockRecorder) ListVPCPeeringsByVPCID(vpcID any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVPCPeeringsByVPCID", reflect.TypeOf((*MockVPCsService)(nil).ListVPCPeeringsByVPCID), vpcID)
+}
+
 // PartialUpdate mocks base method.
 func (m *MockVPCsService) PartialUpdate(vpcUUID string, options ...godo.VPCSetField) (*do.VPC, error) {
 	m.ctrl.T.Helper()
@@ -133,3 +207,18 @@ func (mr *MockVPCsServiceMockRecorder) Update(vpcUUID, vpcr any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockVPCsService)(nil).Update), vpcUUID, vpcr)
 }
+
+// UpdateVPCPeering mocks base method.
+func (m *MockVPCsService) UpdateVPCPeering(peeringID string, req *godo.VPCPeeringUpdateRequest) (*do.VPCPeering, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateVPCPeering", peeringID, req)
+	ret0, _ := ret[0].(*do.VPCPeering)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateVPCPeering indicates an expected call of UpdateVPCPeering.
+func (mr *MockVPCsServiceMockRecorder) UpdateVPCPeering(peeringID, req any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateVPCPeering", reflect.TypeOf((*MockVPCsService)(nil).UpdateVPCPeering), peeringID, req)
+}
diff --git a/do/vpcs.go b/do/vpcs.go
index be85bb03..fb4f4644 100644
--- a/do/vpcs.go
+++ b/do/vpcs.go
@@ -27,6 +27,14 @@ type VPC struct {
 // VPCs is a slice of VPC.
 type VPCs []VPC
 
+// VPCPeering wraps a godo VPCPeering
+type VPCPeering struct {
+	*godo.VPCPeering
+}
+
+// VPCPeerings is a slice of VPCPeering
+type VPCPeerings []VPCPeering
+
 // VPCsService is the godo VPCsService interface.
 type VPCsService interface {
 	Get(vpcUUID string) (*VPC, error)
@@ -35,6 +43,12 @@ type VPCsService interface {
 	Update(vpcUUID string, vpcr *godo.VPCUpdateRequest) (*VPC, error)
 	PartialUpdate(vpcUUID string, options ...godo.VPCSetField) (*VPC, error)
 	Delete(vpcUUID string) error
+	GetPeering(peeringID string) (*VPCPeering, error)
+	ListVPCPeerings() (VPCPeerings, error)
+	CreateVPCPeering(req *godo.VPCPeeringCreateRequest) (*VPCPeering, error)
+	UpdateVPCPeering(peeringID string, req *godo.VPCPeeringUpdateRequest) (*VPCPeering, error)
+	DeleteVPCPeering(peeringID string) error
+	ListVPCPeeringsByVPCID(vpcID string) (VPCPeerings, error)
 }
 
 var _ VPCsService = &vpcsService{}
@@ -119,3 +133,91 @@ func (v *vpcsService) Delete(vpcUUID string) error {
 	_, err := v.client.VPCs.Delete(context.TODO(), vpcUUID)
 	return err
 }
+
+func (v *vpcsService) GetPeering(peeringID string) (*VPCPeering, error) {
+	peering, _, err := v.client.VPCs.GetVPCPeering(context.TODO(), peeringID)
+	if err != nil {
+		return nil, err
+	}
+	return &VPCPeering{VPCPeering: peering}, nil
+}
+
+func (v *vpcsService) ListVPCPeerings() (VPCPeerings, error) {
+	f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) {
+		list, resp, err := v.client.VPCs.ListVPCPeerings(context.TODO(), 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([]VPCPeering, len(si))
+	for i := range si {
+		a := si[i].(*godo.VPCPeering)
+		list[i] = VPCPeering{VPCPeering: a}
+	}
+
+	return list, nil
+}
+
+func (v *vpcsService) UpdateVPCPeering(peeringID string, req *godo.VPCPeeringUpdateRequest) (*VPCPeering, error) {
+	peering, _, err := v.client.VPCs.UpdateVPCPeering(context.TODO(), peeringID, req)
+	if err != nil {
+		return nil, err
+	}
+
+	return &VPCPeering{VPCPeering: peering}, nil
+}
+
+func (v *vpcsService) DeleteVPCPeering(peeringID string) error {
+	_, err := v.client.VPCs.DeleteVPCPeering(context.TODO(), peeringID)
+	return err
+}
+
+func (v *vpcsService) CreateVPCPeering(req *godo.VPCPeeringCreateRequest) (*VPCPeering, error) {
+	peering, _, err := v.client.VPCs.CreateVPCPeering(context.TODO(), req)
+	if err != nil {
+		return nil, err
+	}
+	return &VPCPeering{VPCPeering: peering}, nil
+}
+
+func (v *vpcsService) ListVPCPeeringsByVPCID(vpcID string) (VPCPeerings, error) {
+	f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) {
+		list, resp, err := v.client.VPCs.ListVPCPeeringsByVPCID(context.TODO(), vpcID, 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([]VPCPeering, len(si))
+	for i := range si {
+		a := si[i].(*godo.VPCPeering)
+		list[i] = VPCPeering{VPCPeering: a}
+	}
+
+	return list, nil
+}
diff --git a/go.mod b/go.mod
index 255ef672..380485ef 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.21
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/creack/pty v1.1.11
-	github.com/digitalocean/godo v1.115.0
+	github.com/digitalocean/godo v1.116.0
 	github.com/docker/cli v24.0.5+incompatible
 	github.com/docker/docker v24.0.9+incompatible
 	github.com/docker/docker-credential-helpers v0.7.0 // indirect
diff --git a/go.sum b/go.sum
index 877a4d1d..05424209 100644
--- a/go.sum
+++ b/go.sum
@@ -86,8 +86,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/digitalocean/godo v1.115.0 h1:Xv0gwN0t7ldD61QKeYeHHwCEKfTXzAOmnUDgGAzoZw4=
-github.com/digitalocean/godo v1.115.0/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo=
+github.com/digitalocean/godo v1.115.1-0.20240515191029-705fb26c5aa5 h1:bAO9uVDeZhwy1DdKmtAqgjS9+isKWnzxzxiKvNHKNtQ=
+github.com/digitalocean/godo v1.115.1-0.20240515191029-705fb26c5aa5/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo=
+github.com/digitalocean/godo v1.116.0 h1:SuF/Imd1/dE/nYrUFVkJ2itesQNnJQE1a/vmtHknxeE=
+github.com/digitalocean/godo v1.116.0/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo=
 github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc=
 github.com/docker/cli v24.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
index e1fb20fe..1d3102b2 100644
--- a/vendor/github.com/digitalocean/godo/CHANGELOG.md
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Change Log
 
+## [v1.116.0] - 2024-05-16
+
+- #693 - @guptado - Introduce VPC peering methods
+
 ## [v1.115.0] - 2024-05-08
 
 - #688 - @asaha2 - load balancers: support glb active-passive fail-over settings, currently in closed beta
diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go
index 0713df51..1ec4df20 100644
--- a/vendor/github.com/digitalocean/godo/godo.go
+++ b/vendor/github.com/digitalocean/godo/godo.go
@@ -21,7 +21,7 @@ import (
 )
 
 const (
-	libraryVersion = "1.115.0"
+	libraryVersion = "1.116.0"
 	defaultBaseURL = "https://api.digitalocean.com/"
 	userAgent      = "godo/" + libraryVersion
 	mediaType      = "application/json"
diff --git a/vendor/github.com/digitalocean/godo/vpc_peerings.go b/vendor/github.com/digitalocean/godo/vpc_peerings.go
new file mode 100644
index 00000000..e6dfc043
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/vpc_peerings.go
@@ -0,0 +1,199 @@
+package godo
+
+import (
+	"context"
+	"net/http"
+	"time"
+)
+
+const vpcPeeringsPath = "/v2/vpc_peerings"
+
+type vpcPeeringRoot struct {
+	VPCPeering *VPCPeering `json:"vpc_peering"`
+}
+
+type vpcPeeringsRoot struct {
+	VPCPeerings []*VPCPeering `json:"vpc_peerings"`
+	Links       *Links        `json:"links"`
+	Meta        *Meta         `json:"meta"`
+}
+
+// VPCPeering represents a DigitalOcean Virtual Private Cloud Peering configuration.
+type VPCPeering struct {
+	// ID is the generated ID of the VPC Peering
+	ID string `json:"id"`
+	// Name is the name of the VPC Peering
+	Name string `json:"name"`
+	// VPCIDs is the IDs of the pair of VPCs between which a peering is created
+	VPCIDs []string `json:"vpc_ids"`
+	// CreatedAt is time when this VPC Peering was first created
+	CreatedAt time.Time `json:"created_at"`
+	// Status is the status of the VPC Peering
+	Status string `json:"status"`
+}
+
+// VPCPeeringCreateRequest represents a request to create a Virtual Private Cloud Peering
+// for a list of associated VPC IDs.
+type VPCPeeringCreateRequest struct {
+	// Name is the name of the VPC Peering
+	Name string `json:"name"`
+	// VPCIDs is the IDs of the pair of VPCs between which a peering is created
+	VPCIDs []string `json:"vpc_ids"`
+}
+
+// VPCPeeringUpdateRequest represents a request to update a Virtual Private Cloud Peering.
+type VPCPeeringUpdateRequest struct {
+	// Name is the name of the VPC Peering
+	Name string `json:"name"`
+}
+
+// VPCPeeringCreateRequestByVPCID represents a request to create a Virtual Private Cloud Peering
+// for an associated VPC ID.
+type VPCPeeringCreateRequestByVPCID struct {
+	// Name is the name of the VPC Peering
+	Name string `json:"name"`
+	// VPCID is the ID of one of the VPCs with which the peering has to be created
+	VPCID string `json:"vpc_id"`
+}
+
+// CreateVPCPeering creates a new Virtual Private Cloud Peering.
+func (v *VPCsServiceOp) CreateVPCPeering(ctx context.Context, create *VPCPeeringCreateRequest) (*VPCPeering, *Response, error) {
+	path := vpcPeeringsPath
+	req, err := v.client.NewRequest(ctx, http.MethodPost, path, create)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(vpcPeeringRoot)
+	resp, err := v.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root.VPCPeering, resp, nil
+}
+
+// GetVPCPeering retrieves a Virtual Private Cloud Peering.
+func (v *VPCsServiceOp) GetVPCPeering(ctx context.Context, id string) (*VPCPeering, *Response, error) {
+	path := vpcPeeringsPath + "/" + id
+	req, err := v.client.NewRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(vpcPeeringRoot)
+	resp, err := v.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root.VPCPeering, resp, nil
+}
+
+// ListVPCPeerings lists all Virtual Private Cloud Peerings.
+func (v *VPCsServiceOp) ListVPCPeerings(ctx context.Context, opt *ListOptions) ([]*VPCPeering, *Response, error) {
+	path, err := addOptions(vpcPeeringsPath, opt)
+	if err != nil {
+		return nil, nil, err
+	}
+	req, err := v.client.NewRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(vpcPeeringsRoot)
+	resp, err := v.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	if l := root.Links; l != nil {
+		resp.Links = l
+	}
+	if m := root.Meta; m != nil {
+		resp.Meta = m
+	}
+	return root.VPCPeerings, resp, nil
+}
+
+// UpdateVPCPeering updates a Virtual Private Cloud Peering.
+func (v *VPCsServiceOp) UpdateVPCPeering(ctx context.Context, id string, update *VPCPeeringUpdateRequest) (*VPCPeering, *Response, error) {
+	path := vpcPeeringsPath + "/" + id
+	req, err := v.client.NewRequest(ctx, http.MethodPatch, path, update)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(vpcPeeringRoot)
+	resp, err := v.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root.VPCPeering, resp, nil
+}
+
+// DeleteVPCPeering deletes a Virtual Private Cloud Peering.
+func (v *VPCsServiceOp) DeleteVPCPeering(ctx context.Context, id string) (*Response, error) {
+	path := vpcPeeringsPath + "/" + id
+	req, err := v.client.NewRequest(ctx, http.MethodDelete, path, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := v.client.Do(ctx, req, nil)
+	if err != nil {
+		return resp, err
+	}
+	return resp, nil
+}
+
+// CreateVPCPeeringByVPCID creates a new Virtual Private Cloud Peering for requested VPC ID.
+func (v *VPCsServiceOp) CreateVPCPeeringByVPCID(ctx context.Context, id string, create *VPCPeeringCreateRequestByVPCID) (*VPCPeering, *Response, error) {
+	path := vpcsBasePath + "/" + id + "/peerings"
+	req, err := v.client.NewRequest(ctx, http.MethodPost, path, create)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(vpcPeeringRoot)
+	resp, err := v.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root.VPCPeering, resp, nil
+}
+
+// ListVPCPeeringsByVPCID lists all Virtual Private Cloud Peerings for requested VPC ID.
+func (v *VPCsServiceOp) ListVPCPeeringsByVPCID(ctx context.Context, id string, opt *ListOptions) ([]*VPCPeering, *Response, error) {
+	path, err := addOptions(vpcsBasePath+"/"+id+"/peerings", opt)
+	req, err := v.client.NewRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(vpcPeeringsRoot)
+	resp, err := v.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	if l := root.Links; l != nil {
+		resp.Links = l
+	}
+	if m := root.Meta; m != nil {
+		resp.Meta = m
+	}
+	return root.VPCPeerings, resp, nil
+}
+
+// UpdateVPCPeeringByVPCID updates a Virtual Private Cloud Peering for requested VPC ID.
+func (v *VPCsServiceOp) UpdateVPCPeeringByVPCID(ctx context.Context, vpcID, peerID string, update *VPCPeeringUpdateRequest) (*VPCPeering, *Response, error) {
+	path := vpcsBasePath + "/" + vpcID + "/peerings" + "/" + peerID
+	req, err := v.client.NewRequest(ctx, http.MethodPatch, path, update)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	root := new(vpcPeeringRoot)
+	resp, err := v.client.Do(ctx, req, root)
+	if err != nil {
+		return nil, resp, err
+	}
+	return root.VPCPeering, resp, nil
+}
diff --git a/vendor/github.com/digitalocean/godo/vpcs.go b/vendor/github.com/digitalocean/godo/vpcs.go
index f4f22e18..67525190 100644
--- a/vendor/github.com/digitalocean/godo/vpcs.go
+++ b/vendor/github.com/digitalocean/godo/vpcs.go
@@ -19,6 +19,14 @@ type VPCsService interface {
 	Update(context.Context, string, *VPCUpdateRequest) (*VPC, *Response, error)
 	Set(context.Context, string, ...VPCSetField) (*VPC, *Response, error)
 	Delete(context.Context, string) (*Response, error)
+	CreateVPCPeering(context.Context, *VPCPeeringCreateRequest) (*VPCPeering, *Response, error)
+	GetVPCPeering(context.Context, string) (*VPCPeering, *Response, error)
+	ListVPCPeerings(context.Context, *ListOptions) ([]*VPCPeering, *Response, error)
+	UpdateVPCPeering(context.Context, string, *VPCPeeringUpdateRequest) (*VPCPeering, *Response, error)
+	DeleteVPCPeering(context.Context, string) (*Response, error)
+	CreateVPCPeeringByVPCID(context.Context, string, *VPCPeeringCreateRequestByVPCID) (*VPCPeering, *Response, error)
+	ListVPCPeeringsByVPCID(context.Context, string, *ListOptions) ([]*VPCPeering, *Response, error)
+	UpdateVPCPeeringByVPCID(context.Context, string, string, *VPCPeeringUpdateRequest) (*VPCPeering, *Response, error)
 }
 
 var _ VPCsService = &VPCsServiceOp{}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index cb7dee75..3f9b348a 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -58,7 +58,7 @@ github.com/creack/pty
 # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 ## explicit
 github.com/davecgh/go-spew/spew
-# github.com/digitalocean/godo v1.115.0
+# github.com/digitalocean/godo v1.116.0
 ## explicit; go 1.20
 github.com/digitalocean/godo
 github.com/digitalocean/godo/metrics


Test patch:
diff --git a/commands/vpc_peerings_test.go b/commands/vpc_peerings_test.go
new file mode 100644
index 00000000..2c5e549a
--- /dev/null
+++ b/commands/vpc_peerings_test.go
@@ -0,0 +1,142 @@
+package commands
+
+import (
+	"github.com/digitalocean/doctl"
+	"testing"
+
+	"github.com/digitalocean/doctl/do"
+	"github.com/digitalocean/godo"
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	testPeering = do.VPCPeering{
+		VPCPeering: &godo.VPCPeering{
+			Name:   "peering-name",
+			VPCIDs: []string{"f81d4fae-7dec-11d0-a765-00a0c91e6bf6", "3f900b61-30d7-40d8-9711-8c5d6264b268"},
+			Status: "ACTIVE",
+		},
+	}
+
+	testPeeringList = do.VPCPeerings{
+		testPeering,
+	}
+)
+
+func TestVPCPeeringsCommand(t *testing.T) {
+	cmd := VPCPeerings()
+	assert.NotNil(t, cmd)
+	assertCommandNames(t, cmd, "get", "list", "create", "update", "delete")
+}
+
+func TestVPCPeeringGet(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		peeringID := "e819b321-a9a1-4078-b437-8e6b8bf13530"
+		tm.vpcs.EXPECT().GetPeering(peeringID).Return(&testPeering, nil)
+
+		config.Args = append(config.Args, peeringID)
+
+		err := RunVPCPeeringGet(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestVPCPeeringGetNoID(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		err := RunVPCPeeringGet(config)
+		assert.Error(t, err)
+	})
+}
+
+func TestVPCPeeringList(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.vpcs.EXPECT().ListVPCPeerings().Return(testPeeringList, nil)
+
+		err := RunVPCPeeringList(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestVPCPeeringListByVpcID(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		vpcID := "vpc-01"
+		tm.vpcs.EXPECT().ListVPCPeeringsByVPCID(vpcID).Return(testPeeringList, nil)
+
+		config.Doit.Set(config.NS, doctl.ArgVPCPeeringVPCID, vpcID)
+		err := RunVPCPeeringList(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestVPCPeeringCreate(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		r := godo.VPCPeeringCreateRequest{
+			Name:   "peering-name",
+			VPCIDs: []string{"f81d4fae-7dec-11d0-a765-00a0c91e6bf6", "3f900b61-30d7-40d8-9711-8c5d6264b268"},
+		}
+		tm.vpcs.EXPECT().CreateVPCPeering(&r).Return(&testPeering, nil)
+
+		config.Args = append(config.Args, "peering-name")
+		config.Doit.Set(config.NS, doctl.ArgVPCPeeringVPCIDs, "f81d4fae-7dec-11d0-a765-00a0c91e6bf6,3f900b61-30d7-40d8-9711-8c5d6264b268")
+
+		err := RunVPCPeeringCreate(config)
+		assert.NoError(t, err)
+	})
+
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		config.Args = append(config.Args, "peering-name")
+
+		err := RunVPCPeeringCreate(config)
+		assert.EqualError(t, err, "VPC ID is empty")
+	})
+
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		config.Args = append(config.Args, "peering-name")
+		config.Doit.Set(config.NS, doctl.ArgVPCPeeringVPCIDs, "f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
+
+		err := RunVPCPeeringCreate(config)
+		assert.EqualError(t, err, "VPC IDs length should be 2")
+	})
+}
+
+func TestVPCPeeringUpdate(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		peeringID := "peering-uuid1"
+		peeringName := "updated-peering-name"
+		r := godo.VPCPeeringUpdateRequest{Name: peeringName}
+		tm.vpcs.EXPECT().UpdateVPCPeering(peeringID, &r).Return(&testPeering, nil)
+
+		config.Args = append(config.Args, peeringID)
+		config.Doit.Set(config.NS, doctl.ArgVPCPeeringName, "updated-peering-name")
+
+		err := RunVPCPeeringUpdate(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestVPCPeeringUpdateNoID(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		err := RunVPCPeeringUpdate(config)
+		assert.Error(t, err)
+	})
+}
+
+func TestVPCPeeringDelete(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		peeringID := "e819b321-a9a1-4078-b437-8e6b8bf13530"
+		tm.vpcs.EXPECT().DeleteVPCPeering(peeringID).Return(nil)
+
+		config.Args = append(config.Args, peeringID)
+		config.Doit.Set(config.NS, doctl.ArgForce, true)
+
+		err := RunVPCPeeringDelete(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestVPCPeeringDeleteNoID(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		err := RunVPCPeeringDelete(config)
+		assert.Error(t, err)
+	})
+}
diff --git a/commands/vpcs_test.go b/commands/vpcs_test.go
index 5d5660c2..9c0ed424 100644
--- a/commands/vpcs_test.go
+++ b/commands/vpcs_test.go
@@ -27,7 +27,7 @@ var (
 func TestVPCsCommand(t *testing.T) {
 	cmd := VPCs()
 	assert.NotNil(t, cmd)
-	assertCommandNames(t, cmd, "get", "list", "create", "update", "delete")
+	assertCommandNames(t, cmd, "get", "list", "create", "update", "delete", "peerings")
 }
 
 func TestVPCGet(t *testing.T) {